diff --git a/antdv-demo b/antdv-demo
index 05da262e3..83ab203d1 160000
--- a/antdv-demo
+++ b/antdv-demo
@@ -1 +1 @@
-Subproject commit 05da262e31f9c6cc524154df13f5e2b05c20c1c8
+Subproject commit 83ab203d1ab9861132f6efd1e74015507c0e45f6
diff --git a/components/vc-virtual-list/Filler.jsx b/components/vc-virtual-list/Filler.jsx
new file mode 100644
index 000000000..0704c70b6
--- /dev/null
+++ b/components/vc-virtual-list/Filler.jsx
@@ -0,0 +1,38 @@
+import classNames from '../_util/classNames';
+
+const Filter = ({ height, offset, prefixCls }, { slots }) => {
+ let outerStyle = {};
+
+ let innerStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ };
+
+ if (offset !== undefined) {
+ outerStyle = { height, position: 'relative', overflow: 'hidden' };
+
+ innerStyle = {
+ ...innerStyle,
+ transform: `translateY(${offset}px)`,
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ };
+ }
+
+ return (
+
+
+ {slots.default?.()}
+
+
+ );
+};
+
+export default Filter;
diff --git a/components/vc-virtual-list/List.jsx b/components/vc-virtual-list/List.jsx
new file mode 100644
index 000000000..1d141c657
--- /dev/null
+++ b/components/vc-virtual-list/List.jsx
@@ -0,0 +1,576 @@
+import { getOptionProps, getStyle } from '../_util/props-util';
+import PropTypes from '../_util/vue-types';
+import { cloneElement } from '../_util/vnode';
+import BaseMixin from '../_util/BaseMixin';
+import Filler from './Filler';
+import {
+ getNodeHeight,
+ requireVirtual,
+ getElementScrollPercentage,
+ getRangeIndex,
+ alignScrollTop,
+ getItemAbsoluteTop,
+ getItemRelativeTop,
+ getScrollPercentage,
+ getCompareItemRelativeTop,
+ GHOST_ITEM_KEY,
+} from './utils/itemUtil';
+import { getIndexByStartLoc, findListDiffIndex } from './utils/algorithmUtil';
+
+const ITEM_SCALE_RATE = 1;
+
+const ScrollStyle = {
+ overflowY: 'auto',
+ overflowAnchor: 'none',
+};
+
+/**
+ *
+ * Virtual list display logic:
+ * 1. scroll / initialize trigger measure
+ * 2. Get location item of current `scrollTop`
+ * 3. [Render] Render visible items
+ * 4. Get all the visible items height
+ * 5. [Render] Update top item `margin-top` to fit the position
+ *
+ * Algorithm:
+ * We split scroll bar into equal slice. An item with whatever height occupy the same range slice.
+ * When `scrollTop` change,
+ * it will calculate the item percentage position and move item to the position.
+ * Then calculate other item position base on the located item.
+ *
+ * Concept:
+ *
+ * # located item
+ * The base position item which other items position calculate base on.
+ */
+
+export default {
+ name: 'List',
+ mixins: [BaseMixin],
+ props: {
+ prefixCls: PropTypes.string,
+ children: PropTypes.func,
+ 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: PropTypes.any,
+ component: PropTypes.string,
+ /** Disable scroll check. Usually used on animation control */
+ disabled: PropTypes.bool,
+ /** Set `false` will always use real scroll instead of virtual one */
+ virtual: PropTypes.bool,
+ /** When `disabled`, trigger if changed item not render. */
+ onSkipRender: PropTypes.func,
+ },
+ data() {
+ const props = getOptionProps(this);
+ const { height, itemHeight, data, virtual } = props;
+ this.cachedProps = props;
+ this.itemElements = {};
+ this.itemElementHeights = {};
+ return {
+ status: 'NONE',
+ scrollTop: null,
+ itemIndex: 0,
+ itemOffsetPtg: 0,
+ startIndex: 0,
+ endIndex: 0,
+ startItemTop: 0,
+ isVirtual: requireVirtual(height, itemHeight, data.length, virtual),
+ itemCount: data.length,
+ ...this.getDerivedStateFromProps(props),
+ };
+ },
+ watch: {
+ disabled(val) {
+ if (!val) {
+ const props = getOptionProps(this);
+ this.itemCount = props.data.length;
+ }
+ },
+ },
+ /**
+ * Phase 1: Initial should sync with default scroll top
+ */
+ mounted() {
+ if (this.$refs.list) {
+ this.$refs.list.scrollTop = 0;
+ this.onScroll(null);
+ }
+ },
+ /**
+ * Phase 4: Record used item height
+ * Phase 5: Trigger re-render to use correct position
+ */
+ updated() {
+ this.$nextTick(() => {
+ const { status } = this.$data;
+ const { data, height, itemHeight, disabled, onSkipRender, virtual } = getOptionProps(this);
+ const prevData = this.cachedProps.data || [];
+
+ let changedItemIndex = null;
+ if (prevData.length !== data.length) {
+ const diff = findListDiffIndex(prevData, data, this.getItemKey);
+ changedItemIndex = diff ? diff.index : null;
+ }
+
+ if (disabled) {
+ // Should trigger `onSkipRender` to tell that diff component is not render in the list
+ if (data.length > prevData.length) {
+ const { startIndex, endIndex } = this.$data;
+ if (
+ onSkipRender &&
+ (changedItemIndex === null ||
+ changedItemIndex < startIndex ||
+ endIndex < changedItemIndex)
+ ) {
+ onSkipRender();
+ }
+ }
+ return;
+ }
+
+ const isVirtual = requireVirtual(height, itemHeight, data.length, virtual);
+ let nextStatus = status;
+ if (this.$data.isVirtual !== isVirtual) {
+ nextStatus = isVirtual ? 'SWITCH_TO_VIRTUAL' : 'SWITCH_TO_RAW';
+ this.setState({
+ isVirtual,
+ status: nextStatus,
+ });
+
+ /**
+ * We will wait a tick to let list turn to virtual list.
+ * And then use virtual list sync logic to adjust the scroll.
+ */
+ if (nextStatus === 'SWITCH_TO_VIRTUAL') {
+ return;
+ }
+ }
+
+ if (status === 'MEASURE_START') {
+ const { startIndex, itemIndex, itemOffsetPtg } = this.$data;
+ const { scrollTop } = this.$refs.list;
+
+ // Record here since measure item height will get warning in `render`
+ this.collectItemHeights();
+
+ // Calculate top visible item top offset
+ const locatedItemTop = getItemAbsoluteTop({
+ itemIndex,
+ itemOffsetPtg,
+ itemElementHeights: this.itemElementHeights,
+ scrollTop,
+ scrollPtg: getElementScrollPercentage(this.$refs.list),
+ clientHeight: this.$refs.list.clientHeight,
+ getItemKey: this.getIndexKey,
+ });
+
+ let startItemTop = locatedItemTop;
+ for (let index = itemIndex - 1; index >= startIndex; index -= 1) {
+ startItemTop -= this.itemElementHeights[this.getIndexKey(index)] || 0;
+ }
+
+ this.setState({
+ status: 'MEASURE_DONE',
+ startItemTop,
+ });
+ }
+
+ if (status === 'SWITCH_TO_RAW') {
+ /**
+ * After virtual list back to raw list,
+ * we update the `scrollTop` to real top instead of percentage top.
+ */
+ const {
+ cacheScroll: { itemIndex, relativeTop },
+ } = this.$data;
+ let rawTop = relativeTop;
+ for (let index = 0; index < itemIndex; index += 1) {
+ rawTop -= this.itemElementHeights[this.getIndexKey(index)] || 0;
+ }
+
+ this.lockScroll = true;
+ this.$refs.list.current.scrollTop = -rawTop;
+
+ this.setState({
+ status: 'MEASURE_DONE',
+ itemIndex: 0,
+ });
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ this.lockScroll = false;
+ });
+ });
+ } else if (prevData.length !== data.length && changedItemIndex !== null && height) {
+ /**
+ * Re-calculate the item position since `data` length changed.
+ * [IMPORTANT] We use relative position calculate here.
+ */
+ let { itemIndex: originItemIndex } = this.$data;
+ const {
+ itemOffsetPtg: originItemOffsetPtg,
+ startIndex: originStartIndex,
+ endIndex: originEndIndex,
+ scrollTop: originScrollTop,
+ } = this.$data;
+
+ // 1. Refresh item heights
+ this.collectItemHeights();
+
+ // 1. Get origin located item top
+ let originLocatedItemRelativeTop;
+
+ if (this.$data.status === 'SWITCH_TO_VIRTUAL') {
+ originItemIndex = 0;
+ originLocatedItemRelativeTop = -this.$data.scrollTop;
+ } else {
+ originLocatedItemRelativeTop = getItemRelativeTop({
+ itemIndex: originItemIndex,
+ itemOffsetPtg: originItemOffsetPtg,
+ itemElementHeights: this.itemElementHeights,
+ scrollPtg: getScrollPercentage({
+ scrollTop: originScrollTop,
+ scrollHeight: prevData.length * itemHeight,
+ clientHeight: this.$refs.list.current.clientHeight,
+ }),
+ clientHeight: this.$refs.list.current.clientHeight,
+ getItemKey: index => this.getIndexKey(index, this.cachedProps),
+ });
+ }
+
+ // 2. Find the compare item
+ let originCompareItemIndex = changedItemIndex - 1;
+ // Use next one since there are not more item before removed
+ if (originCompareItemIndex < 0) {
+ originCompareItemIndex = 0;
+ }
+
+ // 3. Find the compare item top
+ const originCompareItemTop = getCompareItemRelativeTop({
+ locatedItemRelativeTop: originLocatedItemRelativeTop,
+ locatedItemIndex: originItemIndex,
+ compareItemIndex: originCompareItemIndex,
+ startIndex: originStartIndex,
+ endIndex: originEndIndex,
+ getItemKey: index => this.getIndexKey(index, this.cachedProps),
+ itemElementHeights: this.itemElementHeights,
+ });
+
+ if (nextStatus === 'SWITCH_TO_RAW') {
+ /**
+ * We will record current measure relative item top and apply in raw list after list turned
+ */
+ this.setState({
+ cacheScroll: {
+ itemIndex: originCompareItemIndex,
+ relativeTop: originCompareItemTop,
+ },
+ });
+ } else {
+ this.internalScrollTo({
+ itemIndex: originCompareItemIndex,
+ relativeTop: originCompareItemTop,
+ });
+ }
+ } else if (nextStatus === 'SWITCH_TO_RAW') {
+ // This is only trigger when height changes that all items can show in raw
+ // Let's reset back to top
+ this.setState({
+ cacheScroll: {
+ itemIndex: 0,
+ relativeTop: 0,
+ },
+ });
+ }
+
+ this.cachedProps = getOptionProps(this);
+ });
+ },
+ methods: {
+ getDerivedStateFromProps(nextProps) {
+ if (!nextProps.disabled) {
+ return {
+ itemCount: nextProps.data.length,
+ };
+ }
+
+ return null;
+ },
+ /**
+ * Phase 2: Trigger render since we should re-calculate current position.
+ */
+ onScroll(e) {
+ const { data, height, itemHeight, disabled } = this.$props;
+
+ const { scrollTop: originScrollTop, clientHeight, scrollHeight } = this.$refs.list;
+ const scrollTop = alignScrollTop(originScrollTop, scrollHeight - clientHeight);
+
+ // Skip if `scrollTop` not change to avoid shake
+ if (scrollTop === this.$data.scrollTop || this.lockScroll || disabled) {
+ return;
+ }
+
+ const scrollPtg = getElementScrollPercentage(this.$refs.list);
+ const visibleCount = Math.ceil(height / itemHeight);
+
+ const { itemIndex, itemOffsetPtg, startIndex, endIndex } = getRangeIndex(
+ scrollPtg,
+ data.length,
+ visibleCount,
+ );
+
+ this.setState({
+ status: 'MEASURE_START',
+ scrollTop,
+ itemIndex,
+ itemOffsetPtg,
+ startIndex,
+ endIndex,
+ });
+
+ this.triggerOnScroll(e);
+ },
+ onRawScroll(e) {
+ const { scrollTop } = this.$refs.list;
+ this.setState({ scrollTop });
+ this.triggerOnScroll(e);
+ },
+ triggerOnScroll(e) {
+ if (e) {
+ this.$emit('scroll', e);
+ }
+ },
+ /**
+ * Phase 4: Render item and get all the visible items height
+ */
+ renderChildren(list, startIndex, renderFunc) {
+ const { status } = this.$data;
+ // We should measure rendered item height
+ return list.map((item, index) => {
+ const eleIndex = startIndex + index;
+ const node = renderFunc(item, eleIndex, {
+ style: status === 'MEASURE_START' ? { visibility: 'hidden' } : {},
+ });
+ const eleKey = this.getIndexKey(eleIndex);
+
+ // Pass `key` and `ref` for internal measure
+ return cloneElement(node, {
+ key: eleKey,
+ ref: eleKey,
+ });
+ });
+ },
+ getIndexKey(index, props) {
+ const mergedProps = props || getOptionProps(this);
+ const { data = [] } = mergedProps;
+
+ // Return ghost key as latest index item
+ if (index === data.length) {
+ return GHOST_ITEM_KEY;
+ }
+
+ const item = data[index];
+ if (!item) {
+ /* istanbul ignore next */
+ console.error('Not find index item. Please report this since it is a bug.');
+ }
+
+ return this.getItemKey(item, mergedProps);
+ },
+ getItemKey(item, props) {
+ const { itemKey } = props || getOptionProps(this);
+
+ return typeof itemKey === 'function' ? itemKey(item) : item[itemKey];
+ },
+ /**
+ * Collect current rendered dom element item heights
+ */
+ collectItemHeights(range) {
+ const { startIndex, endIndex } = range || this.$data;
+ const { data } = getOptionProps(this);
+
+ // Record here since measure item height will get warning in `render`
+ for (let index = startIndex; index <= endIndex; index += 1) {
+ const item = data[index];
+
+ // Only collect exist item height
+ if (item) {
+ const eleKey = this.getItemKey(item);
+ this.itemElementHeights[eleKey] = getNodeHeight(this.refs[`itemElement-${eleKey}`]);
+ }
+ }
+ },
+ internalScrollTo(relativeScroll) {
+ const { itemIndex: compareItemIndex, relativeTop: compareItemRelativeTop } = relativeScroll;
+ const { scrollTop: originScrollTop } = this.$data;
+ const { data, itemHeight, height } = getOptionProps(this);
+
+ // 1. Find the best match compare item top
+ let bestSimilarity = Number.MAX_VALUE;
+ let bestScrollTop = null;
+ let bestItemIndex = null;
+ let bestItemOffsetPtg = null;
+ let bestStartIndex = null;
+ let bestEndIndex = null;
+
+ let missSimilarity = 0;
+
+ const scrollHeight = data.length * itemHeight;
+ const { clientHeight } = this.$refs.list;
+ const maxScrollTop = scrollHeight - clientHeight;
+
+ for (let i = 0; i < maxScrollTop; i += 1) {
+ const scrollTop = getIndexByStartLoc(0, maxScrollTop, originScrollTop, i);
+
+ const scrollPtg = getScrollPercentage({ scrollTop, scrollHeight, clientHeight });
+ const visibleCount = Math.ceil(height / itemHeight);
+
+ const { itemIndex, itemOffsetPtg, startIndex, endIndex } = getRangeIndex(
+ scrollPtg,
+ data.length,
+ visibleCount,
+ );
+
+ // No need to check if compare item out of the index to save performance
+ if (startIndex <= compareItemIndex && compareItemIndex <= endIndex) {
+ // 1.1 Get measure located item relative top
+ const locatedItemRelativeTop = getItemRelativeTop({
+ itemIndex,
+ itemOffsetPtg,
+ itemElementHeights: this.itemElementHeights,
+ scrollPtg,
+ clientHeight,
+ getItemKey: this.getIndexKey,
+ });
+
+ const compareItemTop = getCompareItemRelativeTop({
+ locatedItemRelativeTop,
+ locatedItemIndex: itemIndex,
+ compareItemIndex, // Same as origin index
+ startIndex,
+ endIndex,
+ getItemKey: this.getIndexKey,
+ itemElementHeights: this.itemElementHeights,
+ });
+
+ // 1.2 Find best match compare item top
+ const similarity = Math.abs(compareItemTop - compareItemRelativeTop);
+ if (similarity < bestSimilarity) {
+ bestSimilarity = similarity;
+ bestScrollTop = scrollTop;
+ bestItemIndex = itemIndex;
+ bestItemOffsetPtg = itemOffsetPtg;
+ bestStartIndex = startIndex;
+ bestEndIndex = endIndex;
+
+ missSimilarity = 0;
+ } else {
+ missSimilarity += 1;
+ }
+ }
+
+ // If keeping 10 times not match similarity,
+ // check more scrollTop is meaningless.
+ // Here boundary is set to 10.
+ if (missSimilarity > 10) {
+ break;
+ }
+ }
+
+ // 2. Re-scroll if has best scroll match
+ if (bestScrollTop !== null) {
+ this.lockScroll = true;
+ this.$refs.list.current.scrollTop = bestScrollTop;
+
+ this.setState({
+ status: 'MEASURE_START',
+ scrollTop: bestScrollTop,
+ itemIndex: bestItemIndex,
+ itemOffsetPtg: bestItemOffsetPtg,
+ startIndex: bestStartIndex,
+ endIndex: bestEndIndex,
+ });
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ this.lockScroll = false;
+ });
+ });
+ }
+ },
+ },
+ render() {
+ const { isVirtual, itemCount } = this.$data;
+ const {
+ prefixCls,
+ height,
+ itemHeight,
+ fullHeight = true,
+ component: Component = 'div',
+ data,
+ children,
+ itemKey,
+ onSkipRender,
+ disabled,
+ virtual,
+ ...restProps
+ } = getOptionProps(this);
+ const style = getStyle(this);
+
+ if (!isVirtual) {
+ /**
+ * Virtual list switch is works on component updated.
+ * We should double check here if need cut the content.
+ */
+ const shouldVirtual = requireVirtual(height, itemHeight, data.length, virtual);
+
+ return (
+
+
+ {this.renderChildren(
+ shouldVirtual ? data.slice(0, Math.ceil(height / itemHeight)) : data,
+ 0,
+ children,
+ )}
+
+
+ );
+ }
+
+ // Use virtual list
+ const mergedStyle = {
+ ...style,
+ height,
+ ...ScrollStyle,
+ };
+
+ const { status, startIndex, endIndex, startItemTop } = this.$data;
+ const contentHeight = itemCount * itemHeight * ITEM_SCALE_RATE;
+
+ return (
+
+
+ {this.renderChildren(data.slice(startIndex, endIndex + 1), startIndex, children)}
+
+
+ );
+ },
+};
diff --git a/components/vc-virtual-list/index.js b/components/vc-virtual-list/index.js
new file mode 100644
index 000000000..1e2ddb439
--- /dev/null
+++ b/components/vc-virtual-list/index.js
@@ -0,0 +1,3 @@
+import List from './List';
+
+export default List;
diff --git a/components/vc-virtual-list/utils/algorithmUtil.js b/components/vc-virtual-list/utils/algorithmUtil.js
new file mode 100644
index 000000000..faf7b3163
--- /dev/null
+++ b/components/vc-virtual-list/utils/algorithmUtil.js
@@ -0,0 +1,81 @@
+/**
+ * Get index with specific start index one by one. e.g.
+ * min: 3, max: 9, start: 6
+ *
+ * Return index is:
+ * [0]: 6
+ * [1]: 7
+ * [2]: 5
+ * [3]: 8
+ * [4]: 4
+ * [5]: 9
+ * [6]: 3
+ */
+export function getIndexByStartLoc(min, max, start, index) {
+ const beforeCount = start - min;
+ const afterCount = max - start;
+ const balanceCount = Math.min(beforeCount, afterCount) * 2;
+
+ // Balance
+ if (index <= balanceCount) {
+ const stepIndex = Math.floor(index / 2);
+ if (index % 2) {
+ return start + stepIndex + 1;
+ }
+ return start - stepIndex;
+ }
+
+ // One is out of range
+ if (beforeCount > afterCount) {
+ return start - (index - afterCount);
+ }
+ return start + (index - beforeCount);
+}
+
+/**
+ * We assume that 2 list has only 1 item diff and others keeping the order.
+ * So we can use dichotomy algorithm to find changed one.
+ */
+export function findListDiffIndex(originList, targetList, getKey) {
+ const originLen = originList.length;
+ const targetLen = targetList.length;
+
+ let shortList;
+ let longList;
+
+ if (originLen === 0 && targetLen === 0) {
+ return null;
+ }
+
+ if (originLen < targetLen) {
+ shortList = originList;
+ longList = targetList;
+ } else {
+ shortList = targetList;
+ longList = originList;
+ }
+
+ const notExistKey = { __EMPTY_ITEM__: true };
+ function getItemKey(item) {
+ if (item !== undefined) {
+ return getKey(item);
+ }
+ return notExistKey;
+ }
+
+ // Loop to find diff one
+ let diffIndex = null;
+ let multiple = Math.abs(originLen - targetLen) !== 1;
+ for (let i = 0; i < longList.length; i += 1) {
+ const shortKey = getItemKey(shortList[i]);
+ const longKey = getItemKey(longList[i]);
+
+ if (shortKey !== longKey) {
+ diffIndex = i;
+ multiple = multiple || shortKey !== getItemKey(longList[i + 1]);
+ break;
+ }
+ }
+
+ return diffIndex === null ? null : { index: diffIndex, multiple };
+}
diff --git a/components/vc-virtual-list/utils/itemUtil.js b/components/vc-virtual-list/utils/itemUtil.js
new file mode 100644
index 000000000..e432018ee
--- /dev/null
+++ b/components/vc-virtual-list/utils/itemUtil.js
@@ -0,0 +1,147 @@
+/**
+ * Our algorithm have additional one ghost item
+ * whose index as `data.length` to simplify the calculation
+ */
+export const GHOST_ITEM_KEY = '__vc_ghost_item__';
+
+/**
+ * Safari has the elasticity effect which provides negative `scrollTop` value.
+ * We should ignore it since will make scroll animation shake.
+ */
+export function alignScrollTop(scrollTop, scrollRange) {
+ if (scrollTop < 0) {
+ return 0;
+ }
+ if (scrollTop >= scrollRange) {
+ return scrollRange;
+ }
+
+ return scrollTop;
+}
+
+/**
+ * Get node `offsetHeight`. We prefer node is a dom element directly.
+ * But if not provided, downgrade to `findDOMNode` to get the real dom element.
+ */
+export function getNodeHeight(node) {
+ return node ? node.offsetHeight : 0;
+}
+
+/**
+ * Calculate the located item absolute top with whole scroll height
+ */
+export function getItemAbsoluteTop({ scrollTop, ...rest }) {
+ return scrollTop + getItemRelativeTop(rest);
+}
+
+/**
+ * Calculate the located item related top with current window height
+ */
+export function getItemRelativeTop({
+ itemIndex,
+ itemOffsetPtg,
+ itemElementHeights,
+ scrollPtg,
+ clientHeight,
+ getItemKey,
+}) {
+ const locatedItemHeight = itemElementHeights[getItemKey(itemIndex)] || 0;
+ const locatedItemTop = scrollPtg * clientHeight;
+ const locatedItemOffset = itemOffsetPtg * locatedItemHeight;
+ return Math.floor(locatedItemTop - locatedItemOffset);
+}
+
+export function getCompareItemRelativeTop({
+ locatedItemRelativeTop,
+ locatedItemIndex,
+ compareItemIndex,
+ startIndex,
+ endIndex,
+ getItemKey,
+ itemElementHeights,
+}) {
+ let originCompareItemTop = locatedItemRelativeTop;
+ const compareItemKey = getItemKey(compareItemIndex);
+
+ if (compareItemIndex <= locatedItemIndex) {
+ for (let index = locatedItemIndex; index >= startIndex; index -= 1) {
+ const key = getItemKey(index);
+ if (key === compareItemKey) {
+ break;
+ }
+
+ const prevItemKey = getItemKey(index - 1);
+ originCompareItemTop -= itemElementHeights[prevItemKey] || 0;
+ }
+ } else {
+ for (let index = locatedItemIndex; index <= endIndex; index += 1) {
+ const key = getItemKey(index);
+ if (key === compareItemKey) {
+ break;
+ }
+
+ originCompareItemTop += itemElementHeights[key] || 0;
+ }
+ }
+
+ return originCompareItemTop;
+}
+
+export function getScrollPercentage({ scrollTop, scrollHeight, clientHeight }) {
+ if (scrollHeight <= clientHeight) {
+ return 0;
+ }
+
+ const scrollRange = scrollHeight - clientHeight;
+ const alignedScrollTop = alignScrollTop(scrollTop, scrollRange);
+ const scrollTopPtg = alignedScrollTop / scrollRange;
+ return scrollTopPtg;
+}
+
+export function getElementScrollPercentage(element) {
+ if (!element) {
+ return 0;
+ }
+
+ return getScrollPercentage(element);
+}
+
+/**
+ * Get location item and its align percentage with the scroll percentage.
+ * We should measure current scroll position to decide which item is the location item.
+ * And then fill the top count and bottom count with the base of location item.
+ *
+ * `total` should be the real count instead of `total - 1` in calculation.
+ */
+function getLocationItem(scrollPtg, total) {
+ const itemIndex = Math.floor(scrollPtg * total);
+ const itemTopPtg = itemIndex / total;
+ const itemBottomPtg = (itemIndex + 1) / total;
+ const itemOffsetPtg = (scrollPtg - itemTopPtg) / (itemBottomPtg - itemTopPtg);
+
+ return {
+ index: itemIndex,
+ offsetPtg: itemOffsetPtg,
+ };
+}
+
+/**
+ * Get display items start, end, located item index. This is pure math calculation
+ */
+export function getRangeIndex(scrollPtg, itemCount, visibleCount) {
+ const { index, offsetPtg } = getLocationItem(scrollPtg, itemCount);
+
+ const beforeCount = Math.ceil(scrollPtg * visibleCount);
+ const afterCount = Math.ceil((1 - scrollPtg) * visibleCount);
+
+ return {
+ itemIndex: index,
+ itemOffsetPtg: offsetPtg,
+ startIndex: Math.max(0, index - beforeCount),
+ endIndex: Math.min(itemCount - 1, index + afterCount),
+ };
+}
+
+export function requireVirtual(height, itemHeight, count, virtual) {
+ return virtual !== false && typeof height === 'number' && count * itemHeight > height;
+}