chore: refactor virtual list

pull/2933/head
Amour1688 2020-10-01 17:20:10 +08:00
parent b19ca0aaaf
commit 150ebf15a5
19 changed files with 274 additions and 186 deletions

View File

@ -2,7 +2,7 @@ import { getOptionProps } from './props-util';
export default { export default {
methods: { methods: {
setState(state = {}, callback: () => any) { setState(state = {}, callback: () => void) {
let newState = typeof state === 'function' ? state(this.$data, this.$props) : state; let newState = typeof state === 'function' ? state(this.$data, this.$props) : state;
if (this.getDerivedStateFromProps) { if (this.getDerivedStateFromProps) {
const s = this.getDerivedStateFromProps(getOptionProps(this), { const s = this.getDerivedStateFromProps(getOptionProps(this), {

View File

@ -1,8 +0,0 @@
function createRef() {
const func = function setRef(node) {
func.current = node;
};
return func;
}
export default createRef;

View File

@ -0,0 +1,18 @@
interface RefObject<T> {
readonly current: T | null;
}
function createRef<T>() {
// const refObject = {
// current: null
// }
// Object.seal(refObject);
// return refObject;
function setRef(node: T) {
Object.assign(setRef, { current: node });
}
return setRef;
}
export default createRef;

View File

@ -1,4 +1,4 @@
import { PropType } from 'vue'; import { PropType, VNodeProps } from 'vue';
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead // https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead
@ -25,6 +25,8 @@ export type EventHandlers<E> = {
export type Data = Record<string, unknown>; export type Data = Record<string, unknown>;
export type Key = string | number;
export declare type DefaultFactory<T> = (props: Data) => T | null | undefined; export declare type DefaultFactory<T> = (props: Data) => T | null | undefined;
export declare interface PropOptions<T = any, D = T> { export declare interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null; type?: PropType<T> | true | null;

View File

@ -16,7 +16,7 @@ const PropTypes = {
get func() { get func() {
return { return {
type: Function, type: Function,
} as BaseTypes; };
}, },
get bool() { get bool() {

View File

@ -1,19 +1,24 @@
// based on rc-resize-observer 0.1.3 // based on rc-resize-observer 0.1.3
import { defineComponent, PropType } from 'vue';
import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
import BaseMixin from '../_util/BaseMixin'; import BaseMixin from '../_util/BaseMixin';
import { findDOMNode } from '../_util/props-util'; import { findDOMNode } from '../_util/props-util';
// Still need to be compatible with React 15, we use class component here // Still need to be compatible with React 15, we use class component here
const VueResizeObserver = { const VueResizeObserver = defineComponent({
name: 'ResizeObserver', name: 'ResizeObserver',
mixins: [BaseMixin], mixins: [BaseMixin],
props: { props: {
disabled: Boolean, disabled: Boolean,
onResize: Function, onResize: Function as PropType<
(size: { width: number; height: number; offsetWidth: number; offsetHeight: number }) => void
>,
}, },
data() { beforeCreate() {
this.currentElement = null; this.currentElement = null;
this.resizeObserver = null; this.resizeObserver = null;
},
data() {
return { return {
width: 0, width: 0,
height: 0, height: 0,
@ -54,7 +59,7 @@ const VueResizeObserver = {
} }
}, },
handleResize(entries) { handleResize(entries: ResizeObserverEntry[]) {
const { target } = entries[0]; const { target } = entries[0];
const { width, height } = target.getBoundingClientRect(); const { width, height } = target.getBoundingClientRect();
/** /**
@ -82,8 +87,8 @@ const VueResizeObserver = {
}, },
render() { render() {
return this.$slots.default && this.$slots.default()[0]; return this.$slots.default?.()[0];
}, },
}; });
export default VueResizeObserver; export default VueResizeObserver;

View File

@ -1,10 +1,23 @@
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import ResizeObserver from '../vc-resize-observer'; import ResizeObserver from '../vc-resize-observer';
import { CSSProperties, FunctionalComponent } from 'vue';
const Filter = ({ height, offset, prefixCls, onInnerResize }, { slots }) => { interface FillerProps {
prefixCls?: string;
/** Virtual filler height. Should be `count * itemMinHeight` */
height: number;
/** Set offset of visible items. Should be the top of start item position */
offset?: number;
onInnerResize?: () => void;
}
const Filter: FunctionalComponent<FillerProps> = (
{ height, offset, prefixCls, onInnerResize },
{ slots },
) => {
let outerStyle = {}; let outerStyle = {};
let innerStyle = { let innerStyle: CSSProperties = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}; };
@ -43,15 +56,8 @@ const Filter = ({ height, offset, prefixCls, onInnerResize }, { slots }) => {
</div> </div>
); );
}; };
Filter.displayName = 'Filter'; Filter.displayName = 'Filter';
Filter.inheritAttrs = false; Filter.inheritAttrs = false;
Filter.props = {
prefixCls: String,
/** Virtual filler height. Should be `count * itemMinHeight` */
height: Number,
/** Set offset of visible items. Should be the top of start item position */
offset: Number,
onInnerResize: Function,
};
export default Filter; export default Filter;

View File

@ -1,17 +0,0 @@
import { cloneVNode } from 'vue';
function Item({ setRef }, { slots }) {
const children = slots?.default();
return children && children.length
? cloneVNode(children[0], {
ref: setRef,
})
: children;
}
Item.props = {
setRef: {
type: Function,
default: () => {},
},
};
export default Item;

View File

@ -0,0 +1,17 @@
import { cloneVNode, FunctionalComponent } from 'vue';
export interface ItemProps {
setRef: (element: HTMLElement) => void;
}
const Item: FunctionalComponent<ItemProps> = ({ setRef }, { slots }) => {
const children = slots.default?.();
return children && children.length
? cloneVNode(children[0], {
ref: setRef,
})
: children;
};
export default Item;

View File

@ -1,3 +1,16 @@
import {
ref,
defineComponent,
PropType,
watchEffect,
Component,
computed,
nextTick,
onBeforeUnmount,
reactive,
CSSProperties,
} from 'vue';
import { Key } from '../_util/type';
import Filler from './Filler'; import Filler from './Filler';
import Item from './Item'; import Item from './Item';
import ScrollBar from './ScrollBar'; import ScrollBar from './ScrollBar';
@ -7,18 +20,24 @@ import useFrameWheel from './hooks/useFrameWheel';
import useMobileTouchMove from './hooks/useMobileTouchMove'; import useMobileTouchMove from './hooks/useMobileTouchMove';
import useOriginScroll from './hooks/useOriginScroll'; import useOriginScroll from './hooks/useOriginScroll';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { computed, nextTick, onBeforeUnmount, reactive, watchEffect } from 'vue';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import createRef from '../_util/createRef'; import { RenderFunc, SharedConfig } from './interface';
const EMPTY_DATA = []; const EMPTY_DATA = [];
const ScrollStyle = { const ScrollStyle: CSSProperties = {
overflowY: 'auto', overflowY: 'auto',
overflowAnchor: 'none', overflowAnchor: 'none',
}; };
function renderChildren(list, startIndex, endIndex, setNodeRef, renderFunc, { getKey }) { function renderChildren<T>(
list: T[],
startIndex: number,
endIndex: number,
setNodeRef: (item: T, element: HTMLElement) => void,
renderFunc: RenderFunc<T>,
{ getKey }: SharedConfig<T>,
) {
return list.slice(startIndex, endIndex + 1).map((item, index) => { return list.slice(startIndex, endIndex + 1).map((item, index) => {
const eleIndex = startIndex + index; const eleIndex = startIndex + index;
const node = renderFunc(item, eleIndex, { const node = renderFunc(item, eleIndex, {
@ -33,43 +52,58 @@ function renderChildren(list, startIndex, endIndex, setNodeRef, renderFunc, { ge
}); });
} }
const ListProps = { export interface ListState<T = object> {
scrollTop: number;
scrollMoving: boolean;
mergedData: T[];
}
const List = defineComponent({
inheritAttrs: false,
name: 'List',
props: {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
data: PropTypes.array, data: PropTypes.array,
height: PropTypes.number, height: PropTypes.number,
itemHeight: PropTypes.number, itemHeight: PropTypes.number,
/** If not match virtual scroll condition, Set List still use height of container. */ /** If not match virtual scroll condition, Set List still use height of container. */
fullHeight: PropTypes.bool.def(true), fullHeight: PropTypes.bool,
itemKey: PropTypes.any, itemKey: {
component: PropTypes.any, type: [String, Number, Function] as PropType<Key | ((item: object) => Key)>,
required: true,
},
component: {
type: [String, Object] as PropType<string | Component>,
},
/** Set `false` will always use real scroll instead of virtual one */ /** Set `false` will always use real scroll instead of virtual one */
virtual: PropTypes.bool, virtual: PropTypes.bool,
children: PropTypes.func, children: PropTypes.func,
onScroll: PropTypes.func, onScroll: PropTypes.func,
}; },
const List = {
props: ListProps,
inheritAttrs: false,
name: 'List',
setup(props) { setup(props) {
// ================================= MISC ================================= // ================================= MISC =================================
const inVirtual = computed(() => { const inVirtual = computed(() => {
const { height, itemHeight, data, virtual } = props; const { height, itemHeight, data, virtual } = props;
return virtual !== false && height && itemHeight && data && itemHeight * data.length > height; return !!(
virtual !== false &&
height &&
itemHeight &&
data &&
itemHeight * data.length > height
);
}); });
const state = reactive({ const state = reactive<ListState>({
scrollTop: 0, scrollTop: 0,
scrollMoving: false, scrollMoving: false,
mergedData: computed(() => props.data || EMPTY_DATA), mergedData: computed(() => props.data || EMPTY_DATA) as any,
}); });
const componentRef = createRef(); const componentRef = ref<Element>();
// =============================== Item Key =============================== // =============================== Item Key ===============================
const getKey = item => { const getKey = (item: Record<string, any>) => {
if (typeof props.itemKey === 'function') { if (typeof props.itemKey === 'function') {
return props.itemKey(item); return props.itemKey(item);
} }
@ -81,8 +115,8 @@ const List = {
}; };
// ================================ Scroll ================================ // ================================ Scroll ================================
function syncScrollTop(newTop) { function syncScrollTop(newTop: number | ((prev: number) => number)) {
let value; let value: number;
if (typeof newTop === 'function') { if (typeof newTop === 'function') {
value = newTop(state.scrollTop); value = newTop(state.scrollTop);
} else { } else {
@ -91,8 +125,8 @@ const List = {
const alignedTop = keepInRange(value); const alignedTop = keepInRange(value);
if (componentRef.current) { if (componentRef.value) {
componentRef.current.scrollTop = alignedTop; componentRef.value.scrollTop = alignedTop;
} }
state.scrollTop = alignedTop; state.scrollTop = alignedTop;
@ -112,9 +146,9 @@ const List = {
}; };
} }
let itemTop = 0; let itemTop = 0;
let startIndex; let startIndex: number | undefined;
let startOffset; let startOffset: number | undefined;
let endIndex; let endIndex: number | undefined;
const dataLen = state.mergedData.length; const dataLen = state.mergedData.length;
for (let i = 0; i < dataLen; i += 1) { for (let i = 0; i < dataLen; i += 1) {
const item = state.mergedData[i]; const item = state.mergedData[i];
@ -122,7 +156,7 @@ const List = {
const cacheHeight = heights[key]; const cacheHeight = heights[key];
const currentItemBottom = const currentItemBottom =
itemTop + (cacheHeight === undefined ? props.itemHeight : cacheHeight); itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
if (currentItemBottom >= state.scrollTop && startIndex === undefined) { if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
startIndex = i; startIndex = i;
@ -130,7 +164,7 @@ const List = {
} }
// Check item bottom in the range. We will render additional one item for motion usage // Check item bottom in the range. We will render additional one item for motion usage
if (currentItemBottom > state.scrollTop + props.height && endIndex === undefined) { if (currentItemBottom > state.scrollTop + props.height! && endIndex === undefined) {
endIndex = i; endIndex = i;
} }
@ -157,9 +191,9 @@ const List = {
}; };
}); });
// =============================== In Range =============================== // =============================== In Range ===============================
const maxScrollHeight = computed(() => calRes.value.scrollHeight - props.height); const maxScrollHeight = computed(() => calRes.value.scrollHeight! - props.height!);
function keepInRange(newScrollTop) { function keepInRange(newScrollTop: number) {
let newTop = Math.max(newScrollTop, 0); let newTop = Math.max(newScrollTop, 0);
if (!Number.isNaN(maxScrollHeight.value)) { if (!Number.isNaN(maxScrollHeight.value)) {
newTop = Math.min(newTop, maxScrollHeight.value); newTop = Math.min(newTop, maxScrollHeight.value);
@ -173,15 +207,15 @@ const List = {
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom); const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
// ================================ Scroll ================================ // ================================ Scroll ================================
function onScrollBar(newScrollTop) { function onScrollBar(newScrollTop: number) {
const newTop = newScrollTop; const newTop = newScrollTop;
syncScrollTop(newTop); syncScrollTop(newTop);
} }
// This code may only trigger in test case. // This code may only trigger in test case.
// But we still need a sync if some special escape // But we still need a sync if some special escape
function onFallbackScroll(e) { function onFallbackScroll(e: UIEvent) {
const { scrollTop: newScrollTop } = e.currentTarget; const { scrollTop: newScrollTop } = e.currentTarget as Element;
if (newScrollTop !== state.scrollTop) { if (newScrollTop !== state.scrollTop) {
syncScrollTop(newScrollTop); syncScrollTop(newScrollTop);
} }
@ -209,29 +243,29 @@ const List = {
return false; return false;
} }
onRawWheel({ preventDefault() {}, deltaY }); onRawWheel({ preventDefault() {}, deltaY } as WheelEvent);
return true; return true;
}); });
// Firefox only // Firefox only
function onMozMousePixelScroll(e) { function onMozMousePixelScroll(e: MouseEvent) {
if (inVirtual.value) { if (inVirtual.value) {
e.preventDefault(); e.preventDefault();
} }
} }
const removeEventListener = () => { const removeEventListener = () => {
if (componentRef.current) { if (componentRef.value) {
componentRef.current.removeEventListener('wheel', onRawWheel); componentRef.value.removeEventListener('wheel', onRawWheel);
componentRef.current.removeEventListener('DOMMouseScroll', onFireFoxScroll); componentRef.value.removeEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.current.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll); componentRef.value.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
} }
}; };
watchEffect(() => { watchEffect(() => {
nextTick(() => { nextTick(() => {
if (componentRef.current) { if (componentRef.value) {
removeEventListener(); removeEventListener();
componentRef.current.addEventListener('wheel', onRawWheel); componentRef.value.addEventListener('wheel', onRawWheel);
componentRef.current.addEventListener('DOMMouseScroll', onFireFoxScroll); componentRef.value.addEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.current.addEventListener('MozMousePixelScroll', onMozMousePixelScroll); componentRef.value.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
} }
}); });
}); });
@ -252,15 +286,15 @@ const List = {
); );
const componentStyle = computed(() => { const componentStyle = computed(() => {
let cs = null; let cs: CSSProperties | null = null;
if (props.height) { if (props.height) {
cs = { [props.fullHeight ? 'height' : 'maxHeight']: props.height + 'px', ...ScrollStyle }; cs = { [props.fullHeight ? 'height' : 'maxHeight']: props.height + 'px', ...ScrollStyle };
if (inVirtual.value) { if (inVirtual.value) {
cs.overflowY = 'hidden'; cs!.overflowY = 'hidden';
if (state.scrollMoving) { if (state.scrollMoving) {
cs.pointerEvents = 'none'; cs!.pointerEvents = 'none';
} }
} }
} }
@ -305,7 +339,6 @@ const List = {
componentStyle, componentStyle,
onFallbackScroll, onFallbackScroll,
onScrollBar, onScrollBar,
componentRef,
inVirtual, inVirtual,
collectHeight, collectHeight,
sharedConfig, sharedConfig,
@ -332,7 +365,7 @@ const List = {
<Component <Component
class={`${prefixCls}-holder`} class={`${prefixCls}-holder`}
style={componentStyle} style={componentStyle}
ref={componentRef} ref="componentRef"
onScroll={onFallbackScroll} onScroll={onFallbackScroll}
> >
<Filler <Filler
@ -364,6 +397,6 @@ const List = {
</div> </div>
); );
}, },
}; });
export default List; export default List;

View File

@ -1,4 +1,4 @@
import { reactive } from 'vue'; import { defineComponent, PropType, reactive } from 'vue';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import createRef from '../_util/createRef'; import createRef from '../_util/createRef';
import raf from '../_util/raf'; import raf from '../_util/raf';
@ -6,29 +6,18 @@ import PropTypes from '../_util/vue-types';
const MIN_SIZE = 20; const MIN_SIZE = 20;
// export interface ScrollBarProps { interface ScrollBarState {
// prefixCls: string; dragging: boolean;
// scrollTop: number; pageY: number | null;
// scrollHeight: number; startTop: number | null;
// height: number; visible: boolean;
// count: number; }
// onScroll: (scrollTop: number) => void;
// onStartMove: () => void;
// onStopMove: () => void;
// }
// interface ScrollBarState { function getPageY(e: MouseEvent | TouchEvent) {
// dragging: boolean;
// pageY: number;
// startTop: number;
// visible: boolean;
// }
function getPageY(e) {
return 'touches' in e ? e.touches[0].pageY : e.pageY; return 'touches' in e ? e.touches[0].pageY : e.pageY;
} }
export default { export default defineComponent({
name: 'ScrollBar', name: 'ScrollBar',
inheritAttrs: false, inheritAttrs: false,
props: { props: {
@ -37,9 +26,15 @@ export default {
scrollHeight: PropTypes.number, scrollHeight: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
count: PropTypes.number, count: PropTypes.number,
onScroll: PropTypes.func, onScroll: {
onStartMove: PropTypes.func, type: Function as PropType<(scrollTop: number) => void>,
onStopMove: PropTypes.func, },
onStartMove: {
type: Function as PropType<() => void>,
},
onStopMove: {
type: Function as PropType<() => void>,
},
}, },
setup() { setup() {
return { return {
@ -47,7 +42,7 @@ export default {
scrollbarRef: createRef(), scrollbarRef: createRef(),
thumbRef: createRef(), thumbRef: createRef(),
visibleTimeout: null, visibleTimeout: null,
state: reactive({ state: reactive<ScrollBarState>({
dragging: false, dragging: false,
pageY: null, pageY: null,
startTop: null, startTop: null,
@ -83,11 +78,11 @@ export default {
}, 2000); }, 2000);
}, },
onScrollbarTouchStart(e) { onScrollbarTouchStart(e: TouchEvent) {
e.preventDefault(); e.preventDefault();
}, },
onContainerMouseDown(e) { onContainerMouseDown(e: MouseEvent) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}, },
@ -114,7 +109,7 @@ export default {
}, },
// ======================= Thumb ======================= // ======================= Thumb =======================
onMouseDown(e) { onMouseDown(e: MouseEvent | TouchEvent) {
const { onStartMove } = this.$props; const { onStartMove } = this.$props;
Object.assign(this.state, { Object.assign(this.state, {
@ -129,7 +124,7 @@ export default {
e.preventDefault(); e.preventDefault();
}, },
onMouseMove(e) { onMouseMove(e: MouseEvent | TouchEvent) {
const { dragging, pageY, startTop } = this.state; const { dragging, pageY, startTop } = this.state;
const { onScroll } = this.$props; const { onScroll } = this.$props;
@ -203,7 +198,7 @@ export default {
bottom: 0, bottom: 0,
right: 0, right: 0,
position: 'absolute', position: 'absolute',
display: visible ? null : 'none', display: visible ? undefined : 'none',
}} }}
onMousedown={this.onContainerMouseDown} onMousedown={this.onContainerMouseDown}
onMousemove={this.delayHidden} onMousemove={this.delayHidden}
@ -229,4 +224,4 @@ export default {
</div> </div>
); );
}, },
}; });

View File

@ -1,22 +1,33 @@
import { Ref } from 'vue';
import raf from '../../_util/raf'; import raf from '../../_util/raf';
import isFF from '../utils/isFirefox'; import isFF from '../utils/isFirefox';
import useOriginScroll from './useOriginScroll'; import useOriginScroll from './useOriginScroll';
export default function useFrameWheel(inVirtual, isScrollAtTop, isScrollAtBottom, onWheelDelta) { interface FireFoxDOMMouseScrollEvent {
detail: number;
preventDefault: Function;
}
export default function useFrameWheel(
inVirtual: Ref<boolean>,
isScrollAtTop: Ref<boolean>,
isScrollAtBottom: Ref<boolean>,
onWheelDelta: (offset: number) => void,
): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] {
let offsetRef = 0; let offsetRef = 0;
let nextFrame = null; let nextFrame: number | null | undefined = null;
// Firefox patch // Firefox patch
let wheelValue = null; let wheelValue: null = null;
let isMouseScroll = false; let isMouseScroll = false;
// Scroll status sync // Scroll status sync
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom); const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
function onWheel(event) { function onWheel(event: { preventDefault?: any; deltaY?: any }) {
if (!inVirtual.value) return; if (!inVirtual.value) return;
raf.cancel(nextFrame); raf.cancel(nextFrame!);
const { deltaY } = event; const { deltaY } = event;
offsetRef += deltaY; offsetRef += deltaY;
@ -40,7 +51,7 @@ export default function useFrameWheel(inVirtual, isScrollAtTop, isScrollAtBottom
} }
// A patch for firefox // A patch for firefox
function onFireFoxScroll(event) { function onFireFoxScroll(event: { detail: any }) {
if (!inVirtual.value) return; if (!inVirtual.value) return;
isMouseScroll = event.detail === wheelValue; isMouseScroll = event.detail === wheelValue;

View File

@ -1,9 +1,15 @@
import { reactive, ref } from 'vue'; import { reactive, Ref, ref, VNodeProps } from 'vue';
import { findDOMNode } from '../../_util/props-util'; import { GetKey } from '../interface';
export default function useHeights(getKey, onItemAdd, onItemRemove) { type CacheMap = Record<string, number>;
const instance = new Map();
const heights = reactive({}); export default function useHeights<T>(
getKey: GetKey<T>,
onItemAdd?: ((item: T) => void) | null,
onItemRemove?: ((item: T) => void) | null,
): [(item: T, instance: HTMLElement) => void, () => void, CacheMap, Ref<number>] {
const instance = new Map<VNodeProps['key'], HTMLElement>();
const heights = reactive<CacheMap>({});
let updatedMark = ref(0); let updatedMark = ref(0);
let heightUpdateId = 0; let heightUpdateId = 0;
function collectHeight() { function collectHeight() {
@ -15,11 +21,10 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
let changed = false; let changed = false;
instance.forEach((element, key) => { instance.forEach((element, key) => {
if (element && element.offsetParent) { if (element && element.offsetParent) {
const htmlElement = findDOMNode(element); const { offsetHeight } = element;
const { offsetHeight } = htmlElement; if (heights[key!] !== offsetHeight) {
if (heights[key] !== offsetHeight) {
changed = true; changed = true;
heights[key] = htmlElement.offsetHeight; heights[key!] = element.offsetHeight;
} }
} }
}); });
@ -29,7 +34,7 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
}); });
} }
function setInstance(item, ins) { function setInstance(item: T, ins: HTMLElement) {
const key = getKey(item); const key = getKey(item);
const origin = instance.get(key); const origin = instance.get(key);

View File

@ -1,19 +1,28 @@
import { watch } from 'vue'; import { watch, Ref } from 'vue';
const SMOOTH_PTG = 14 / 15; const SMOOTH_PTG = 14 / 15;
export default function useMobileTouchMove(inVirtual, listRef, callback) { export default function useMobileTouchMove(
inVirtual: Ref<boolean>,
listRef: Ref<Element | undefined>,
callback: (offsetY: number, smoothOffset?: boolean) => boolean,
) {
let touched = false; let touched = false;
let touchY = 0; let touchY = 0;
let element = null; let element: HTMLElement | null = null;
// Smooth scroll // Smooth scroll
let interval = null; let interval: any = null;
let cleanUpEvents; const cleanUpEvents = () => {
if (element) {
element.removeEventListener('touchmove', onTouchMove);
element.removeEventListener('touchend', onTouchEnd);
}
};
const onTouchMove = e => { const onTouchMove = (e: TouchEvent) => {
if (touched) { if (touched) {
const currentY = Math.ceil(e.touches[0].pageY); const currentY = Math.ceil(e.touches[0].pageY);
let offsetY = touchY - currentY; let offsetY = touchY - currentY;
@ -41,31 +50,25 @@ export default function useMobileTouchMove(inVirtual, listRef, callback) {
cleanUpEvents(); cleanUpEvents();
}; };
const onTouchStart = e => { const onTouchStart = (e: TouchEvent) => {
cleanUpEvents(); cleanUpEvents();
if (e.touches.length === 1 && !touched) { if (e.touches.length === 1 && !touched) {
touched = true; touched = true;
touchY = Math.ceil(e.touches[0].pageY); touchY = Math.ceil(e.touches[0].pageY);
element = e.target; element = e.target as HTMLElement;
element.addEventListener('touchmove', onTouchMove); element!.addEventListener('touchmove', onTouchMove);
element.addEventListener('touchend', onTouchEnd); element!.addEventListener('touchend', onTouchEnd);
} }
}; };
cleanUpEvents = () => {
if (element) {
element.removeEventListener('touchmove', onTouchMove);
element.removeEventListener('touchend', onTouchEnd);
}
};
watch(inVirtual, val => { watch(inVirtual, val => {
listRef.current.removeEventListener('touchstart', onTouchStart); listRef.value.removeEventListener('touchstart', onTouchStart);
cleanUpEvents(); cleanUpEvents();
clearInterval(interval); clearInterval(interval);
if (val.value) { if (val) {
listRef.current.addEventListener('touchstart', onTouchStart); listRef.value.addEventListener('touchstart', onTouchStart);
} }
}); });
} }

View File

@ -1,7 +1,9 @@
export default (isScrollAtTop, isScrollAtBottom) => { import { Ref } from 'vue';
export default (isScrollAtTop: Ref<boolean>, isScrollAtBottom: Ref<boolean>) => {
// Do lock for a wheel when scrolling // Do lock for a wheel when scrolling
let lock = false; let lock = false;
let lockTimeout = null; let lockTimeout: any = null;
function lockScroll() { function lockScroll() {
clearTimeout(lockTimeout); clearTimeout(lockTimeout);
@ -11,7 +13,7 @@ export default (isScrollAtTop, isScrollAtBottom) => {
lock = false; lock = false;
}, 50); }, 50);
} }
return (deltaY, smoothOffset = false) => { return (deltaY: number, smoothOffset = false) => {
const originScroll = const originScroll =
// Pass origin wheel when on the top // Pass origin wheel when on the top
(deltaY < 0 && isScrollAtTop.value) || (deltaY < 0 && isScrollAtTop.value) ||

View File

@ -1,41 +1,43 @@
/* eslint-disable no-param-reassign */ import { Data } from '../../_util/type';
import { Ref } from 'vue';
import raf from '../../_util/raf'; import raf from '../../_util/raf';
import { GetKey } from '../interface';
import { ListState } from '../List';
export default function useScrollTo( export default function useScrollTo(
containerRef, containerRef: Ref<Element | undefined>,
state, state: ListState,
heights, heights: Data,
props, props,
getKey, getKey: GetKey,
collectHeight, collectHeight: () => void,
syncScrollTop, syncScrollTop: (newTop: number) => void,
) { ) {
let scroll = null; let scroll: number | null = null;
return arg => { return arg => {
raf.cancel(scroll); raf.cancel(scroll!);
const data = state.mergedData; const data = state.mergedData;
const itemHeight = props.itemHeight; const itemHeight = props.itemHeight;
if (typeof arg === 'number') { if (typeof arg === 'number') {
syncScrollTop(arg); syncScrollTop(arg);
} else if (arg && typeof arg === 'object') { } else if (arg && typeof arg === 'object') {
let index; let index: number;
const { align } = arg; const { align } = arg;
if ('index' in arg) { if ('index' in arg) {
({ index } = arg); ({ index } = arg);
} else { } else {
index = data.findIndex(item => getKey(item) === arg.key); index = data.findIndex((item: object) => getKey(item) === arg.key);
} }
const { offset = 0 } = arg; const { offset = 0 } = arg;
// We will retry 3 times in case dynamic height shaking // We will retry 3 times in case dynamic height shaking
const syncScroll = (times, targetAlign) => { const syncScroll = (times: number, targetAlign?: 'top' | 'bottom') => {
if (times < 0 || !containerRef.current) return; if (times < 0 || !containerRef.value) return;
const height = containerRef.current.clientHeight; const height = containerRef.value.clientHeight;
let needCollectHeight = false; let needCollectHeight = false;
let newTargetAlign = targetAlign; let newTargetAlign = targetAlign;
@ -51,7 +53,7 @@ export default function useScrollTo(
for (let i = 0; i <= index; i += 1) { for (let i = 0; i <= index; i += 1) {
const key = getKey(data[i]); const key = getKey(data[i]);
itemTop = stackTop; itemTop = stackTop;
const cacheHeight = heights[key]; const cacheHeight = heights[key!];
itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight); itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
stackTop = itemBottom; stackTop = itemBottom;
@ -62,7 +64,7 @@ export default function useScrollTo(
} }
// Scroll to // Scroll to
let targetTop = null; let targetTop: number | null = null;
switch (mergedAlign) { switch (mergedAlign) {
case 'top': case 'top':
@ -73,7 +75,7 @@ export default function useScrollTo(
break; break;
default: { default: {
const { scrollTop } = containerRef.current; const { scrollTop } = containerRef.value;
const scrollBottom = scrollTop + height; const scrollBottom = scrollTop + height;
if (itemTop < scrollTop) { if (itemTop < scrollTop) {
newTargetAlign = 'top'; newTargetAlign = 'top';
@ -83,7 +85,7 @@ export default function useScrollTo(
} }
} }
if (targetTop !== null && targetTop !== containerRef.current.scrollTop) { if (targetTop !== null && targetTop !== containerRef.value.scrollTop) {
syncScrollTop(targetTop); syncScrollTop(targetTop);
} }
} }

View File

@ -0,0 +1,14 @@
import { CSSProperties, VNodeTypes } from 'vue';
import { Key } from '../_util/type';
export type RenderFunc<T> = (
item: T,
index: number,
props: { style?: CSSProperties },
) => VNodeTypes;
export interface SharedConfig<T> {
getKey: (item: T) => Key;
}
export type GetKey<T = object> = (item: T) => Key;