chore: refactor virtual list
parent
b19ca0aaaf
commit
150ebf15a5
|
@ -2,7 +2,7 @@ import { getOptionProps } from './props-util';
|
|||
|
||||
export default {
|
||||
methods: {
|
||||
setState(state = {}, callback: () => any) {
|
||||
setState(state = {}, callback: () => void) {
|
||||
let newState = typeof state === 'function' ? state(this.$data, this.$props) : state;
|
||||
if (this.getDerivedStateFromProps) {
|
||||
const s = this.getDerivedStateFromProps(getOptionProps(this), {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
function createRef() {
|
||||
const func = function setRef(node) {
|
||||
func.current = node;
|
||||
};
|
||||
return func;
|
||||
}
|
||||
|
||||
export default createRef;
|
|
@ -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;
|
|
@ -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>>;
|
||||
// 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 Key = string | number;
|
||||
|
||||
export declare type DefaultFactory<T> = (props: Data) => T | null | undefined;
|
||||
export declare interface PropOptions<T = any, D = T> {
|
||||
type?: PropType<T> | true | null;
|
||||
|
|
|
@ -16,7 +16,7 @@ const PropTypes = {
|
|||
get func() {
|
||||
return {
|
||||
type: Function,
|
||||
} as BaseTypes;
|
||||
};
|
||||
},
|
||||
|
||||
get bool() {
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
// based on rc-resize-observer 0.1.3
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import { findDOMNode } from '../_util/props-util';
|
||||
|
||||
// Still need to be compatible with React 15, we use class component here
|
||||
const VueResizeObserver = {
|
||||
const VueResizeObserver = defineComponent({
|
||||
name: 'ResizeObserver',
|
||||
mixins: [BaseMixin],
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
onResize: Function,
|
||||
onResize: Function as PropType<
|
||||
(size: { width: number; height: number; offsetWidth: number; offsetHeight: number }) => void
|
||||
>,
|
||||
},
|
||||
data() {
|
||||
beforeCreate() {
|
||||
this.currentElement = null;
|
||||
this.resizeObserver = null;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
|
@ -54,7 +59,7 @@ const VueResizeObserver = {
|
|||
}
|
||||
},
|
||||
|
||||
handleResize(entries) {
|
||||
handleResize(entries: ResizeObserverEntry[]) {
|
||||
const { target } = entries[0];
|
||||
const { width, height } = target.getBoundingClientRect();
|
||||
/**
|
||||
|
@ -82,8 +87,8 @@ const VueResizeObserver = {
|
|||
},
|
||||
|
||||
render() {
|
||||
return this.$slots.default && this.$slots.default()[0];
|
||||
return this.$slots.default?.()[0];
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default VueResizeObserver;
|
|
@ -1,10 +1,23 @@
|
|||
import classNames from '../_util/classNames';
|
||||
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 innerStyle = {
|
||||
let innerStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
|
@ -43,15 +56,8 @@ const Filter = ({ height, offset, prefixCls, onInnerResize }, { slots }) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Filter.displayName = 'Filter';
|
||||
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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 Item from './Item';
|
||||
import ScrollBar from './ScrollBar';
|
||||
|
@ -7,18 +20,24 @@ import useFrameWheel from './hooks/useFrameWheel';
|
|||
import useMobileTouchMove from './hooks/useMobileTouchMove';
|
||||
import useOriginScroll from './hooks/useOriginScroll';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { computed, nextTick, onBeforeUnmount, reactive, watchEffect } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import createRef from '../_util/createRef';
|
||||
import { RenderFunc, SharedConfig } from './interface';
|
||||
|
||||
const EMPTY_DATA = [];
|
||||
|
||||
const ScrollStyle = {
|
||||
const ScrollStyle: CSSProperties = {
|
||||
overflowY: 'auto',
|
||||
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) => {
|
||||
const eleIndex = startIndex + index;
|
||||
const node = renderFunc(item, eleIndex, {
|
||||
|
@ -33,43 +52,58 @@ function renderChildren(list, startIndex, endIndex, setNodeRef, renderFunc, { ge
|
|||
});
|
||||
}
|
||||
|
||||
const ListProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
data: PropTypes.array,
|
||||
height: PropTypes.number,
|
||||
itemHeight: PropTypes.number,
|
||||
/** If not match virtual scroll condition, Set List still use height of container. */
|
||||
fullHeight: PropTypes.bool.def(true),
|
||||
itemKey: PropTypes.any,
|
||||
component: PropTypes.any,
|
||||
/** Set `false` will always use real scroll instead of virtual one */
|
||||
virtual: PropTypes.bool,
|
||||
children: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
};
|
||||
export interface ListState<T = object> {
|
||||
scrollTop: number;
|
||||
scrollMoving: boolean;
|
||||
mergedData: T[];
|
||||
}
|
||||
|
||||
const List = {
|
||||
props: ListProps,
|
||||
const List = defineComponent({
|
||||
inheritAttrs: false,
|
||||
name: 'List',
|
||||
props: {
|
||||
prefixCls: PropTypes.string,
|
||||
data: PropTypes.array,
|
||||
height: PropTypes.number,
|
||||
itemHeight: PropTypes.number,
|
||||
/** If not match virtual scroll condition, Set List still use height of container. */
|
||||
fullHeight: PropTypes.bool,
|
||||
itemKey: {
|
||||
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 */
|
||||
virtual: PropTypes.bool,
|
||||
children: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
},
|
||||
setup(props) {
|
||||
// ================================= MISC =================================
|
||||
|
||||
const inVirtual = computed(() => {
|
||||
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,
|
||||
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 ===============================
|
||||
const getKey = item => {
|
||||
const getKey = (item: Record<string, any>) => {
|
||||
if (typeof props.itemKey === 'function') {
|
||||
return props.itemKey(item);
|
||||
}
|
||||
|
@ -81,8 +115,8 @@ const List = {
|
|||
};
|
||||
|
||||
// ================================ Scroll ================================
|
||||
function syncScrollTop(newTop) {
|
||||
let value;
|
||||
function syncScrollTop(newTop: number | ((prev: number) => number)) {
|
||||
let value: number;
|
||||
if (typeof newTop === 'function') {
|
||||
value = newTop(state.scrollTop);
|
||||
} else {
|
||||
|
@ -91,8 +125,8 @@ const List = {
|
|||
|
||||
const alignedTop = keepInRange(value);
|
||||
|
||||
if (componentRef.current) {
|
||||
componentRef.current.scrollTop = alignedTop;
|
||||
if (componentRef.value) {
|
||||
componentRef.value.scrollTop = alignedTop;
|
||||
}
|
||||
|
||||
state.scrollTop = alignedTop;
|
||||
|
@ -112,9 +146,9 @@ const List = {
|
|||
};
|
||||
}
|
||||
let itemTop = 0;
|
||||
let startIndex;
|
||||
let startOffset;
|
||||
let endIndex;
|
||||
let startIndex: number | undefined;
|
||||
let startOffset: number | undefined;
|
||||
let endIndex: number | undefined;
|
||||
const dataLen = state.mergedData.length;
|
||||
for (let i = 0; i < dataLen; i += 1) {
|
||||
const item = state.mergedData[i];
|
||||
|
@ -122,7 +156,7 @@ const List = {
|
|||
|
||||
const cacheHeight = heights[key];
|
||||
const currentItemBottom =
|
||||
itemTop + (cacheHeight === undefined ? props.itemHeight : cacheHeight);
|
||||
itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
|
||||
|
||||
if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
|
||||
startIndex = i;
|
||||
|
@ -130,7 +164,7 @@ const List = {
|
|||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -157,9 +191,9 @@ const List = {
|
|||
};
|
||||
});
|
||||
// =============================== 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);
|
||||
if (!Number.isNaN(maxScrollHeight.value)) {
|
||||
newTop = Math.min(newTop, maxScrollHeight.value);
|
||||
|
@ -173,15 +207,15 @@ const List = {
|
|||
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
|
||||
|
||||
// ================================ Scroll ================================
|
||||
function onScrollBar(newScrollTop) {
|
||||
function onScrollBar(newScrollTop: number) {
|
||||
const newTop = newScrollTop;
|
||||
syncScrollTop(newTop);
|
||||
}
|
||||
|
||||
// This code may only trigger in test case.
|
||||
// But we still need a sync if some special escape
|
||||
function onFallbackScroll(e) {
|
||||
const { scrollTop: newScrollTop } = e.currentTarget;
|
||||
function onFallbackScroll(e: UIEvent) {
|
||||
const { scrollTop: newScrollTop } = e.currentTarget as Element;
|
||||
if (newScrollTop !== state.scrollTop) {
|
||||
syncScrollTop(newScrollTop);
|
||||
}
|
||||
|
@ -209,29 +243,29 @@ const List = {
|
|||
return false;
|
||||
}
|
||||
|
||||
onRawWheel({ preventDefault() {}, deltaY });
|
||||
onRawWheel({ preventDefault() {}, deltaY } as WheelEvent);
|
||||
return true;
|
||||
});
|
||||
// Firefox only
|
||||
function onMozMousePixelScroll(e) {
|
||||
function onMozMousePixelScroll(e: MouseEvent) {
|
||||
if (inVirtual.value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
const removeEventListener = () => {
|
||||
if (componentRef.current) {
|
||||
componentRef.current.removeEventListener('wheel', onRawWheel);
|
||||
componentRef.current.removeEventListener('DOMMouseScroll', onFireFoxScroll);
|
||||
componentRef.current.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
|
||||
if (componentRef.value) {
|
||||
componentRef.value.removeEventListener('wheel', onRawWheel);
|
||||
componentRef.value.removeEventListener('DOMMouseScroll', onFireFoxScroll);
|
||||
componentRef.value.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
|
||||
}
|
||||
};
|
||||
watchEffect(() => {
|
||||
nextTick(() => {
|
||||
if (componentRef.current) {
|
||||
if (componentRef.value) {
|
||||
removeEventListener();
|
||||
componentRef.current.addEventListener('wheel', onRawWheel);
|
||||
componentRef.current.addEventListener('DOMMouseScroll', onFireFoxScroll);
|
||||
componentRef.current.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
|
||||
componentRef.value.addEventListener('wheel', onRawWheel);
|
||||
componentRef.value.addEventListener('DOMMouseScroll', onFireFoxScroll);
|
||||
componentRef.value.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -252,15 +286,15 @@ const List = {
|
|||
);
|
||||
|
||||
const componentStyle = computed(() => {
|
||||
let cs = null;
|
||||
let cs: CSSProperties | null = null;
|
||||
if (props.height) {
|
||||
cs = { [props.fullHeight ? 'height' : 'maxHeight']: props.height + 'px', ...ScrollStyle };
|
||||
|
||||
if (inVirtual.value) {
|
||||
cs.overflowY = 'hidden';
|
||||
cs!.overflowY = 'hidden';
|
||||
|
||||
if (state.scrollMoving) {
|
||||
cs.pointerEvents = 'none';
|
||||
cs!.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +339,6 @@ const List = {
|
|||
componentStyle,
|
||||
onFallbackScroll,
|
||||
onScrollBar,
|
||||
componentRef,
|
||||
inVirtual,
|
||||
collectHeight,
|
||||
sharedConfig,
|
||||
|
@ -332,7 +365,7 @@ const List = {
|
|||
<Component
|
||||
class={`${prefixCls}-holder`}
|
||||
style={componentStyle}
|
||||
ref={componentRef}
|
||||
ref="componentRef"
|
||||
onScroll={onFallbackScroll}
|
||||
>
|
||||
<Filler
|
||||
|
@ -364,6 +397,6 @@ const List = {
|
|||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default List;
|
|
@ -1,4 +1,4 @@
|
|||
import { reactive } from 'vue';
|
||||
import { defineComponent, PropType, reactive } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import createRef from '../_util/createRef';
|
||||
import raf from '../_util/raf';
|
||||
|
@ -6,29 +6,18 @@ import PropTypes from '../_util/vue-types';
|
|||
|
||||
const MIN_SIZE = 20;
|
||||
|
||||
// export interface ScrollBarProps {
|
||||
// prefixCls: string;
|
||||
// scrollTop: number;
|
||||
// scrollHeight: number;
|
||||
// height: number;
|
||||
// count: number;
|
||||
// onScroll: (scrollTop: number) => void;
|
||||
// onStartMove: () => void;
|
||||
// onStopMove: () => void;
|
||||
// }
|
||||
interface ScrollBarState {
|
||||
dragging: boolean;
|
||||
pageY: number | null;
|
||||
startTop: number | null;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
// interface ScrollBarState {
|
||||
// dragging: boolean;
|
||||
// pageY: number;
|
||||
// startTop: number;
|
||||
// visible: boolean;
|
||||
// }
|
||||
|
||||
function getPageY(e) {
|
||||
function getPageY(e: MouseEvent | TouchEvent) {
|
||||
return 'touches' in e ? e.touches[0].pageY : e.pageY;
|
||||
}
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'ScrollBar',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
|
@ -37,9 +26,15 @@ export default {
|
|||
scrollHeight: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
count: PropTypes.number,
|
||||
onScroll: PropTypes.func,
|
||||
onStartMove: PropTypes.func,
|
||||
onStopMove: PropTypes.func,
|
||||
onScroll: {
|
||||
type: Function as PropType<(scrollTop: number) => void>,
|
||||
},
|
||||
onStartMove: {
|
||||
type: Function as PropType<() => void>,
|
||||
},
|
||||
onStopMove: {
|
||||
type: Function as PropType<() => void>,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
|
@ -47,7 +42,7 @@ export default {
|
|||
scrollbarRef: createRef(),
|
||||
thumbRef: createRef(),
|
||||
visibleTimeout: null,
|
||||
state: reactive({
|
||||
state: reactive<ScrollBarState>({
|
||||
dragging: false,
|
||||
pageY: null,
|
||||
startTop: null,
|
||||
|
@ -83,11 +78,11 @@ export default {
|
|||
}, 2000);
|
||||
},
|
||||
|
||||
onScrollbarTouchStart(e) {
|
||||
onScrollbarTouchStart(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
onContainerMouseDown(e) {
|
||||
onContainerMouseDown(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
|
@ -114,7 +109,7 @@ export default {
|
|||
},
|
||||
|
||||
// ======================= Thumb =======================
|
||||
onMouseDown(e) {
|
||||
onMouseDown(e: MouseEvent | TouchEvent) {
|
||||
const { onStartMove } = this.$props;
|
||||
|
||||
Object.assign(this.state, {
|
||||
|
@ -129,7 +124,7 @@ export default {
|
|||
e.preventDefault();
|
||||
},
|
||||
|
||||
onMouseMove(e) {
|
||||
onMouseMove(e: MouseEvent | TouchEvent) {
|
||||
const { dragging, pageY, startTop } = this.state;
|
||||
const { onScroll } = this.$props;
|
||||
|
||||
|
@ -203,7 +198,7 @@ export default {
|
|||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: visible ? null : 'none',
|
||||
display: visible ? undefined : 'none',
|
||||
}}
|
||||
onMousedown={this.onContainerMouseDown}
|
||||
onMousemove={this.delayHidden}
|
||||
|
@ -229,4 +224,4 @@ export default {
|
|||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
|
@ -1,22 +1,33 @@
|
|||
import { Ref } from 'vue';
|
||||
import raf from '../../_util/raf';
|
||||
import isFF from '../utils/isFirefox';
|
||||
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 nextFrame = null;
|
||||
let nextFrame: number | null | undefined = null;
|
||||
|
||||
// Firefox patch
|
||||
let wheelValue = null;
|
||||
let wheelValue: null = null;
|
||||
let isMouseScroll = false;
|
||||
|
||||
// Scroll status sync
|
||||
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
|
||||
|
||||
function onWheel(event) {
|
||||
function onWheel(event: { preventDefault?: any; deltaY?: any }) {
|
||||
if (!inVirtual.value) return;
|
||||
|
||||
raf.cancel(nextFrame);
|
||||
raf.cancel(nextFrame!);
|
||||
|
||||
const { deltaY } = event;
|
||||
offsetRef += deltaY;
|
||||
|
@ -40,7 +51,7 @@ export default function useFrameWheel(inVirtual, isScrollAtTop, isScrollAtBottom
|
|||
}
|
||||
|
||||
// A patch for firefox
|
||||
function onFireFoxScroll(event) {
|
||||
function onFireFoxScroll(event: { detail: any }) {
|
||||
if (!inVirtual.value) return;
|
||||
|
||||
isMouseScroll = event.detail === wheelValue;
|
|
@ -1,9 +1,15 @@
|
|||
import { reactive, ref } from 'vue';
|
||||
import { findDOMNode } from '../../_util/props-util';
|
||||
import { reactive, Ref, ref, VNodeProps } from 'vue';
|
||||
import { GetKey } from '../interface';
|
||||
|
||||
export default function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
const instance = new Map();
|
||||
const heights = reactive({});
|
||||
type CacheMap = Record<string, number>;
|
||||
|
||||
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 heightUpdateId = 0;
|
||||
function collectHeight() {
|
||||
|
@ -15,11 +21,10 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
|
|||
let changed = false;
|
||||
instance.forEach((element, key) => {
|
||||
if (element && element.offsetParent) {
|
||||
const htmlElement = findDOMNode(element);
|
||||
const { offsetHeight } = htmlElement;
|
||||
if (heights[key] !== offsetHeight) {
|
||||
const { offsetHeight } = element;
|
||||
if (heights[key!] !== offsetHeight) {
|
||||
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 origin = instance.get(key);
|
||||
|
|
@ -1,19 +1,28 @@
|
|||
import { watch } from 'vue';
|
||||
import { watch, Ref } from 'vue';
|
||||
|
||||
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 touchY = 0;
|
||||
|
||||
let element = null;
|
||||
let element: HTMLElement | null = null;
|
||||
|
||||
// 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) {
|
||||
const currentY = Math.ceil(e.touches[0].pageY);
|
||||
let offsetY = touchY - currentY;
|
||||
|
@ -41,31 +50,25 @@ export default function useMobileTouchMove(inVirtual, listRef, callback) {
|
|||
cleanUpEvents();
|
||||
};
|
||||
|
||||
const onTouchStart = e => {
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
cleanUpEvents();
|
||||
|
||||
if (e.touches.length === 1 && !touched) {
|
||||
touched = true;
|
||||
touchY = Math.ceil(e.touches[0].pageY);
|
||||
|
||||
element = e.target;
|
||||
element.addEventListener('touchmove', onTouchMove);
|
||||
element.addEventListener('touchend', onTouchEnd);
|
||||
element = e.target as HTMLElement;
|
||||
element!.addEventListener('touchmove', onTouchMove);
|
||||
element!.addEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
};
|
||||
|
||||
cleanUpEvents = () => {
|
||||
if (element) {
|
||||
element.removeEventListener('touchmove', onTouchMove);
|
||||
element.removeEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
};
|
||||
watch(inVirtual, val => {
|
||||
listRef.current.removeEventListener('touchstart', onTouchStart);
|
||||
listRef.value.removeEventListener('touchstart', onTouchStart);
|
||||
cleanUpEvents();
|
||||
clearInterval(interval);
|
||||
if (val.value) {
|
||||
listRef.current.addEventListener('touchstart', onTouchStart);
|
||||
if (val) {
|
||||
listRef.value.addEventListener('touchstart', onTouchStart);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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
|
||||
let lock = false;
|
||||
let lockTimeout = null;
|
||||
let lockTimeout: any = null;
|
||||
function lockScroll() {
|
||||
clearTimeout(lockTimeout);
|
||||
|
||||
|
@ -11,7 +13,7 @@ export default (isScrollAtTop, isScrollAtBottom) => {
|
|||
lock = false;
|
||||
}, 50);
|
||||
}
|
||||
return (deltaY, smoothOffset = false) => {
|
||||
return (deltaY: number, smoothOffset = false) => {
|
||||
const originScroll =
|
||||
// Pass origin wheel when on the top
|
||||
(deltaY < 0 && isScrollAtTop.value) ||
|
|
@ -1,41 +1,43 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import { Data } from '../../_util/type';
|
||||
import { Ref } from 'vue';
|
||||
import raf from '../../_util/raf';
|
||||
import { GetKey } from '../interface';
|
||||
import { ListState } from '../List';
|
||||
|
||||
export default function useScrollTo(
|
||||
containerRef,
|
||||
state,
|
||||
heights,
|
||||
containerRef: Ref<Element | undefined>,
|
||||
state: ListState,
|
||||
heights: Data,
|
||||
props,
|
||||
getKey,
|
||||
collectHeight,
|
||||
syncScrollTop,
|
||||
getKey: GetKey,
|
||||
collectHeight: () => void,
|
||||
syncScrollTop: (newTop: number) => void,
|
||||
) {
|
||||
let scroll = null;
|
||||
let scroll: number | null = null;
|
||||
|
||||
return arg => {
|
||||
raf.cancel(scroll);
|
||||
raf.cancel(scroll!);
|
||||
const data = state.mergedData;
|
||||
const itemHeight = props.itemHeight;
|
||||
if (typeof arg === 'number') {
|
||||
syncScrollTop(arg);
|
||||
} else if (arg && typeof arg === 'object') {
|
||||
let index;
|
||||
let index: number;
|
||||
const { align } = arg;
|
||||
|
||||
if ('index' in arg) {
|
||||
({ index } = arg);
|
||||
} else {
|
||||
index = data.findIndex(item => getKey(item) === arg.key);
|
||||
index = data.findIndex((item: object) => getKey(item) === arg.key);
|
||||
}
|
||||
|
||||
const { offset = 0 } = arg;
|
||||
|
||||
// We will retry 3 times in case dynamic height shaking
|
||||
const syncScroll = (times, targetAlign) => {
|
||||
if (times < 0 || !containerRef.current) return;
|
||||
const syncScroll = (times: number, targetAlign?: 'top' | 'bottom') => {
|
||||
if (times < 0 || !containerRef.value) return;
|
||||
|
||||
const height = containerRef.current.clientHeight;
|
||||
const height = containerRef.value.clientHeight;
|
||||
let needCollectHeight = false;
|
||||
let newTargetAlign = targetAlign;
|
||||
|
||||
|
@ -51,7 +53,7 @@ export default function useScrollTo(
|
|||
for (let i = 0; i <= index; i += 1) {
|
||||
const key = getKey(data[i]);
|
||||
itemTop = stackTop;
|
||||
const cacheHeight = heights[key];
|
||||
const cacheHeight = heights[key!];
|
||||
itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
|
||||
|
||||
stackTop = itemBottom;
|
||||
|
@ -62,7 +64,7 @@ export default function useScrollTo(
|
|||
}
|
||||
|
||||
// Scroll to
|
||||
let targetTop = null;
|
||||
let targetTop: number | null = null;
|
||||
|
||||
switch (mergedAlign) {
|
||||
case 'top':
|
||||
|
@ -73,7 +75,7 @@ export default function useScrollTo(
|
|||
break;
|
||||
|
||||
default: {
|
||||
const { scrollTop } = containerRef.current;
|
||||
const { scrollTop } = containerRef.value;
|
||||
const scrollBottom = scrollTop + height;
|
||||
if (itemTop < scrollTop) {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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;
|
Loading…
Reference in New Issue