chore: refactor virtual list
parent
b19ca0aaaf
commit
150ebf15a5
|
@ -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), {
|
||||||
|
|
|
@ -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>>;
|
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;
|
||||||
|
|
|
@ -16,7 +16,7 @@ const PropTypes = {
|
||||||
get func() {
|
get func() {
|
||||||
return {
|
return {
|
||||||
type: Function,
|
type: Function,
|
||||||
} as BaseTypes;
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
get bool() {
|
get bool() {
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 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> {
|
||||||
prefixCls: PropTypes.string,
|
scrollTop: number;
|
||||||
data: PropTypes.array,
|
scrollMoving: boolean;
|
||||||
height: PropTypes.number,
|
mergedData: T[];
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
const List = {
|
const List = defineComponent({
|
||||||
props: ListProps,
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
name: 'List',
|
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) {
|
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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
|
@ -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;
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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) ||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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