fix: select dynamic virtual list
parent
604372ff2d
commit
b2aa49d064
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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!];
|
||||||
|
|
Loading…
Reference in New Issue