element-plus
Version:
A Component Library for Vue3.0
300 lines (251 loc) • 6.75 kB
text/typescript
import throwError from '@element-plus/utils/error'
import createList from '../builders/buildList'
import { isHorizontal } from '../utils'
import {
AUTO_ALIGNMENT,
CENTERED_ALIGNMENT,
DEFAULT_DYNAMIC_LIST_ITEM_SIZE,
DefaultListProps,
END_ALIGNMENT,
SMART_ALIGNMENT,
START_ALIGNMENT,
} from '../defaults'
import type { ListCache, ListItem, ItemSize } from '../types'
import type { ExtractPropTypes } from 'vue'
type Props = ExtractPropTypes<typeof DefaultListProps>
const SCOPE = 'ElDynamicSizeList'
const getItemFromCache = (
props: Props,
index: number,
listCache: ListCache,
): ListItem => {
const { itemSize } = props
const { items, lastVisitedIndex } = listCache
if (index > lastVisitedIndex) {
let offset = 0
if (lastVisitedIndex >= 0) {
const item = items[lastVisitedIndex]
offset = item.offset + item.size
}
for (let i = lastVisitedIndex + 1; i <= index; i++) {
const size = (itemSize as ItemSize)(i)
items[i] = {
offset,
size,
}
offset += size
}
listCache.lastVisitedIndex = index
}
return items[index]
}
const findItem = (
props: Props,
listCache: ListCache,
offset: number,
) => {
const { items, lastVisitedIndex } = listCache
const lastVisitedOffset =
lastVisitedIndex > 0 ? items[lastVisitedIndex].offset : 0
if (lastVisitedOffset >= offset) {
return bs(
props,
listCache,
0,
lastVisitedIndex,
offset,
)
}
return es(
props,
listCache,
Math.max(0, lastVisitedIndex),
offset,
)
}
// bs stands for binary search which has approximately time complexity of O(Log n)
// space complexity of O(1)
// in this case we use it for search the offset of each item, since
// the cached items' offset is monotonically increasing
const bs = (
props: Props,
listCache: ListCache,
low: number,
high: number,
offset: number,
) => {
while (low <= high) {
const mid = low + Math.floor((high - low) / 2)
const currentOffset = getItemFromCache(props, mid, listCache).offset
if (currentOffset === offset) {
return mid
} else if (currentOffset < offset) {
low = mid + 1
} else if (currentOffset > offset) {
high = mid - 1
}
}
return Math.max(0, low - 1)
}
// es stands for exponential search which has time complexity of O(Log n) and
// space complexity of O(1) in the case of finding the boundary element.
// the exponential indicator in this case is 2.
// for more detail about exponential search click this link
// https://www.freecodecamp.org/news/search-algorithms-exponential-search-explained/
const es = (
props: Props,
listCache: ListCache,
index: number,
offset: number,
) => {
const { total } = props
let exponent = 1
while (
index < total &&
getItemFromCache(props, index, listCache).offset < offset
) {
index += exponent
exponent *= 2
}
return bs(
props,
listCache,
Math.floor(index / 2),
Math.min(index, total - 1),
offset,
)
}
const getEstimatedTotalSize = (
{ total }: Props,
{ items, estimatedItemSize, lastVisitedIndex }: ListCache,
) => {
let totalSizeOfMeasuredItems = 0
if (lastVisitedIndex >= total) {
lastVisitedIndex = total - 1
}
if (lastVisitedIndex >= 0) {
const item = items[lastVisitedIndex]
totalSizeOfMeasuredItems = item.offset + item.size
}
const numUnmeasuredItems = total - lastVisitedIndex - 1
const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedItemSize
return totalSizeOfMeasuredItems + totalSizeOfUnmeasuredItems
}
const DynamicSizeList = createList({
name: 'ElDynamicSizeList',
getItemOffset: (
props,
index,
listCache,
) => getItemFromCache(props, index, listCache).offset,
getItemSize: (
_,
index,
{ items },
)=> items[index].size,
getEstimatedTotalSize,
getOffset: (
props,
index,
alignment,
scrollOffset,
listCache,
) => {
const { height, layout, width } = props
const size = (isHorizontal(layout) ? width : height) as number
const item = getItemFromCache(props, index, listCache)
const estimatedTotalSize = getEstimatedTotalSize(props, listCache)
const maxOffset = Math.max(
0,
Math.min(estimatedTotalSize - size, item.offset),
)
const minOffset = Math.max(
0,
item.offset - size + item.size,
)
if (alignment === SMART_ALIGNMENT) {
if (
scrollOffset >= minOffset - size &&
scrollOffset <= maxOffset + size
) {
alignment = AUTO_ALIGNMENT
} else {
alignment = CENTERED_ALIGNMENT
}
}
switch (alignment) {
case START_ALIGNMENT: {
return maxOffset
}
case END_ALIGNMENT: {
return minOffset
}
case CENTERED_ALIGNMENT: {
return Math.round(minOffset + (maxOffset - minOffset) / 2)
}
case AUTO_ALIGNMENT:
default: {
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset
} else if (scrollOffset < minOffset) {
return minOffset
} else {
return maxOffset
}
}
}
},
getStartIndexForOffset: (
props,
offset,
listCache,
) => findItem(props, listCache, offset),
getStopIndexForStartIndex: (
props,
startIndex,
scrollOffset,
listCache,
) => {
const { height, total, layout, width } = props
const size = (isHorizontal(layout) ? width : height) as number
const item = getItemFromCache(props, startIndex, listCache)
const maxOffset = scrollOffset + size
let offset = item.offset + item.size
let stopIndex = startIndex
while (stopIndex < total - 1 && offset < maxOffset) {
stopIndex++
offset += getItemFromCache(props, stopIndex, listCache).size
}
return stopIndex
},
initCache({ estimatedItemSize = DEFAULT_DYNAMIC_LIST_ITEM_SIZE }, instance) {
const cache = {
items: {},
estimatedItemSize,
lastVisitedIndex: -1,
} as ListCache
cache.clearCacheAfterIndex = (index: number,
forceUpdate = true) => {
cache.lastVisitedIndex = Math.min(
cache.lastVisitedIndex,
index - 1,
)
instance.exposed.getItemStyleCache(-1)
if (forceUpdate) {
instance.proxy.$forceUpdate()
}
}
return cache
},
clearCache: false,
validateProps: ({ itemSize }) => {
if (process.env.NODE_ENV !== 'production') {
if (typeof itemSize !== 'function') {
throwError(SCOPE, `
itemSize is required as function, but the given value was ${typeof itemSize}
`)
}
}
},
})
export default DynamicSizeList