feat: virtual list
parent
b36043c241
commit
0d34f45895
|
@ -1 +1 @@
|
|||
Subproject commit 05da262e31f9c6cc524154df13f5e2b05c20c1c8
|
||||
Subproject commit 83ab203d1ab9861132f6efd1e74015507c0e45f6
|
|
@ -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 (
|
||||
<div style={outerStyle}>
|
||||
<div
|
||||
style={innerStyle}
|
||||
class={classNames({
|
||||
[`${prefixCls}-holder-inner`]: prefixCls,
|
||||
})}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filter;
|
|
@ -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 (
|
||||
<Component
|
||||
style={
|
||||
height
|
||||
? { ...style, [fullHeight ? 'height' : 'maxHeight']: height, ...ScrollStyle }
|
||||
: style
|
||||
}
|
||||
{...restProps}
|
||||
onScroll={this.onRawScroll}
|
||||
ref="list"
|
||||
>
|
||||
<Filler prefixCls={prefixCls} height={height}>
|
||||
{this.renderChildren(
|
||||
shouldVirtual ? data.slice(0, Math.ceil(height / itemHeight)) : data,
|
||||
0,
|
||||
children,
|
||||
)}
|
||||
</Filler>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
// Use virtual list
|
||||
const mergedStyle = {
|
||||
...style,
|
||||
height,
|
||||
...ScrollStyle,
|
||||
};
|
||||
|
||||
const { status, startIndex, endIndex, startItemTop } = this.$data;
|
||||
const contentHeight = itemCount * itemHeight * ITEM_SCALE_RATE;
|
||||
|
||||
return (
|
||||
<Component style={mergedStyle} {...restProps} onScroll={this.onScroll} ref="list">
|
||||
<Filler
|
||||
prefixCls={prefixCls}
|
||||
height={contentHeight}
|
||||
offset={status === 'MEASURE_DONE' ? startItemTop : 0}
|
||||
>
|
||||
{this.renderChildren(data.slice(startIndex, endIndex + 1), startIndex, children)}
|
||||
</Filler>
|
||||
</Component>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
import List from './List';
|
||||
|
||||
export default List;
|
|
@ -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 };
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue