Amour1688
4 years ago
6 changed files with 846 additions and 1 deletions
@ -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