fix: select dynamic virtual list

refactor-list
tangjinzhou 2021-06-22 15:33:11 +08:00
parent 604372ff2d
commit b2aa49d064
3 changed files with 102 additions and 89 deletions

View File

@ -9,6 +9,7 @@ import {
onBeforeUnmount, onBeforeUnmount,
reactive, reactive,
CSSProperties, CSSProperties,
watch,
} from 'vue'; } from 'vue';
import { Key } from '../_util/type'; import { Key } from '../_util/type';
import Filler from './Filler'; import Filler from './Filler';
@ -53,10 +54,9 @@ function renderChildren<T>(
}); });
} }
export interface ListState<T = object> { export interface ListState {
scrollTop: number; scrollTop: number;
scrollMoving: boolean; scrollMoving: boolean;
mergedData: T[];
} }
const List = defineComponent({ const List = defineComponent({
@ -97,7 +97,10 @@ const List = defineComponent({
const state = reactive<ListState>({ const state = reactive<ListState>({
scrollTop: 0, scrollTop: 0,
scrollMoving: false, scrollMoving: false,
mergedData: computed(() => props.data || EMPTY_DATA) as any, });
const mergedData = computed(() => {
return props.data || EMPTY_DATA;
}); });
const componentRef = ref<HTMLDivElement>(); const componentRef = ref<HTMLDivElement>();
@ -108,7 +111,7 @@ const List = defineComponent({
if (typeof props.itemKey === 'function') { if (typeof props.itemKey === 'function') {
return props.itemKey(item); return props.itemKey(item);
} }
return item[props.itemKey]; return item?.[props.itemKey];
}; };
const sharedConfig = { const sharedConfig = {
@ -135,83 +138,94 @@ const List = defineComponent({
// ================================ Height ================================ // ================================ Height ================================
const [setInstance, collectHeight, heights] = useHeights(getKey, null, null); const [setInstance, collectHeight, heights] = useHeights(getKey, null, null);
const calRes = ref(); const calRes = ref<{
watchEffect(() => { scrollHeight?: number;
if (!useVirtual.value) { start?: number;
calRes.value = { end?: number;
scrollHeight: undefined, offset?: number;
start: 0, }>({});
end: state.mergedData.length - 1, watch(
offset: undefined, [inVirtual, useVirtual, () => state.scrollTop, mergedData, heights, () => props.height],
}; () => {
return; if (!useVirtual.value) {
} calRes.value = {
scrollHeight: undefined,
// Always use virtual scroll bar in avoid shaking start: 0,
if (!inVirtual.value) { end: mergedData.value.length - 1,
calRes.value = { offset: undefined,
scrollHeight: fillerInnerRef.value?.offsetHeight || 0, };
start: 0, return;
end: state.mergedData.length - 1,
offset: undefined,
};
return;
}
let itemTop = 0;
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];
const key = getKey(item);
const cacheHeight = heights[key];
const currentItemBottom =
itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
startIndex = i;
startOffset = itemTop;
} }
// Check item bottom in the range. We will render additional one item for motion usage // Always use virtual scroll bar in avoid shaking
if (currentItemBottom > state.scrollTop + props.height! && endIndex === undefined) { if (!inVirtual.value) {
endIndex = i; calRes.value = {
scrollHeight: fillerInnerRef.value?.offsetHeight || 0,
start: 0,
end: mergedData.value.length - 1,
offset: undefined,
};
return;
} }
itemTop = currentItemBottom; let itemTop = 0;
} let startIndex: number | undefined;
let startOffset: number | undefined;
let endIndex: number | undefined;
const dataLen = mergedData.value.length;
const data = mergedData.value;
for (let i = 0; i < dataLen; i += 1) {
const item = data[i];
const key = getKey(item);
// Fallback to normal if not match. This code should never reach const cacheHeight = heights[key];
/* istanbul ignore next */ const currentItemBottom =
if (startIndex === undefined) { itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
startIndex = 0;
startOffset = 0;
}
if (endIndex === undefined) {
endIndex = state.mergedData.length - 1;
}
// Give cache to improve scroll experience if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
endIndex = Math.min(endIndex + 1, state.mergedData.length); startIndex = i;
calRes.value = { startOffset = itemTop;
scrollHeight: itemTop, }
start: startIndex,
end: endIndex, // Check item bottom in the range. We will render additional one item for motion usage
offset: startOffset, if (currentItemBottom > state.scrollTop + props.height! && endIndex === undefined) {
}; endIndex = i;
}); }
itemTop = currentItemBottom;
}
// Fallback to normal if not match. This code should never reach
/* istanbul ignore next */
if (startIndex === undefined) {
startIndex = 0;
startOffset = 0;
}
if (endIndex === undefined) {
endIndex = dataLen - 1;
}
// Give cache to improve scroll experience
endIndex = Math.min(endIndex + 1, dataLen);
calRes.value = {
scrollHeight: itemTop,
start: startIndex,
end: endIndex,
offset: startOffset,
};
},
{ immediate: true },
);
// =============================== In Range =============================== // =============================== In Range ===============================
const maxScrollHeight = computed(() => calRes.value.scrollHeight! - props.height!); const maxScrollHeight = computed(() => calRes.value.scrollHeight! - props.height!);
function keepInRange(newScrollTop: number) { function keepInRange(newScrollTop: number) {
let newTop = Math.max(newScrollTop, 0); let newTop = newScrollTop;
if (!Number.isNaN(maxScrollHeight.value)) { if (!Number.isNaN(maxScrollHeight.value)) {
newTop = Math.min(newTop, maxScrollHeight.value); newTop = Math.min(newTop, maxScrollHeight.value);
} }
newTop = Math.max(newTop, 0);
return newTop; return newTop;
} }
@ -226,8 +240,7 @@ const List = defineComponent({
syncScrollTop(newTop); syncScrollTop(newTop);
} }
// This code may only trigger in test case. // When data size reduce. It may trigger native scroll event back to fit scroll position
// But we still need a sync if some special escape
function onFallbackScroll(e: UIEvent) { function onFallbackScroll(e: UIEvent) {
const { scrollTop: newScrollTop } = e.currentTarget as Element; const { scrollTop: newScrollTop } = e.currentTarget as Element;
if (Math.abs(newScrollTop - state.scrollTop) >= 1) { if (Math.abs(newScrollTop - state.scrollTop) >= 1) {
@ -299,7 +312,7 @@ const List = defineComponent({
// ================================= Ref ================================== // ================================= Ref ==================================
const scrollTo = useScrollTo( const scrollTo = useScrollTo(
componentRef, componentRef,
state, mergedData,
heights, heights,
props, props,
getKey, getKey,
@ -328,6 +341,7 @@ const List = defineComponent({
return { return {
state, state,
mergedData,
componentStyle, componentStyle,
scrollTo, scrollTo,
onFallbackScroll, onFallbackScroll,
@ -360,7 +374,7 @@ const List = defineComponent({
...restProps ...restProps
} = { ...this.$props, ...this.$attrs } as any; } = { ...this.$props, ...this.$attrs } as any;
const mergedClassName = classNames(prefixCls, className); const mergedClassName = classNames(prefixCls, className);
const { scrollTop, mergedData } = this.state; const { scrollTop } = this.state;
const { scrollHeight, offset, start, end } = this.calRes; const { scrollHeight, offset, start, end } = this.calRes;
const { const {
componentStyle, componentStyle,
@ -370,6 +384,7 @@ const List = defineComponent({
collectHeight, collectHeight,
sharedConfig, sharedConfig,
setInstance, setInstance,
mergedData,
} = this; } = this;
const listChildren = renderChildren( const listChildren = renderChildren(
mergedData, mergedData,

View File

@ -208,37 +208,34 @@ export default defineComponent({
const ptg = scrollTop / enableScrollRange; const ptg = scrollTop / enableScrollRange;
return ptg * enableHeightRange; return ptg * enableHeightRange;
}, },
// Not show scrollbar when height is large thane scrollHeight // Not show scrollbar when height is large than scrollHeight
getVisible() { showScroll() {
const { visible } = this.state;
const { height, scrollHeight } = this.$props; const { height, scrollHeight } = this.$props;
return scrollHeight > height;
if (height >= scrollHeight) {
return false;
}
return visible;
}, },
}, },
render() { render() {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { dragging } = this.state; const { dragging, visible } = this.state;
const { prefixCls } = this.$props; const { prefixCls } = this.$props;
const spinHeight = this.getSpinHeight() + 'px'; const spinHeight = this.getSpinHeight() + 'px';
const top = this.getTop() + 'px'; const top = this.getTop() + 'px';
const visible = this.getVisible(); const canScroll = this.showScroll();
const mergedVisible = canScroll && visible;
return ( return (
<div <div
ref={this.scrollbarRef} ref={this.scrollbarRef}
class={`${prefixCls}-scrollbar`} class={classNames(`${prefixCls}-scrollbar`, {
[`${prefixCls}-scrollbar-show`]: canScroll,
})}
style={{ style={{
width: '8px', width: '8px',
top: 0, top: 0,
bottom: 0, bottom: 0,
right: 0, right: 0,
position: 'absolute', position: 'absolute',
display: visible ? undefined : 'none', display: mergedVisible ? undefined : 'none',
}} }}
onMousedown={this.onContainerMouseDown} onMousedown={this.onContainerMouseDown}
onMousemove={this.delayHidden} onMousemove={this.delayHidden}

View File

@ -1,12 +1,11 @@
import { Data } from '../../_util/type'; import { Data } from '../../_util/type';
import { Ref } from 'vue'; import { ComputedRef, Ref } from 'vue';
import raf from '../../_util/raf'; import raf from '../../_util/raf';
import { GetKey } from '../interface'; import { GetKey } from '../interface';
import { ListState } from '../List';
export default function useScrollTo( export default function useScrollTo(
containerRef: Ref<Element | undefined>, containerRef: Ref<Element | undefined>,
state: ListState, mergedData: ComputedRef<any[]>,
heights: Data, heights: Data,
props, props,
getKey: GetKey, getKey: GetKey,
@ -25,7 +24,7 @@ export default function useScrollTo(
// Normal scroll logic // Normal scroll logic
raf.cancel(scroll!); raf.cancel(scroll!);
const data = state.mergedData; const data = mergedData.value;
const itemHeight = props.itemHeight; const itemHeight = props.itemHeight;
if (typeof arg === 'number') { if (typeof arg === 'number') {
syncScrollTop(arg); syncScrollTop(arg);
@ -58,7 +57,9 @@ export default function useScrollTo(
let itemTop = 0; let itemTop = 0;
let itemBottom = 0; let itemBottom = 0;
for (let i = 0; i <= index; i += 1) { const maxLen = Math.min(data.length, index);
for (let i = 0; i <= maxLen; i += 1) {
const key = getKey(data[i]); const key = getKey(data[i]);
itemTop = stackTop; itemTop = stackTop;
const cacheHeight = heights[key!]; const cacheHeight = heights[key!];