100 lines
6.5 KiB
JavaScript
100 lines
6.5 KiB
JavaScript
|
|
/**
|
||
|
|
* VirtualScroll Utility Module
|
||
|
|
*/
|
||
|
|
const VirtualScroll = (options = {}) => {
|
||
|
|
const itemHeights = new Map()
|
||
|
|
const groupHeights = new Map()
|
||
|
|
let groupItemCount = 1
|
||
|
|
const avg = globalThis.Util.newAvg()
|
||
|
|
let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false
|
||
|
|
|
||
|
|
const providedItemHeight = options.itemHeight || null;
|
||
|
|
|
||
|
|
return {
|
||
|
|
reset: (list, container) => {
|
||
|
|
listInited = false; itemHeights.clear(); groupHeights.clear(); avg.clear(); topMargin = 0; itemMarginTop = null; itemMarginBottom = null;
|
||
|
|
if (!list?.length) return [];
|
||
|
|
const size = list.length; groupItemCount = Math.ceil(Math.sqrt(size)) || 10;
|
||
|
|
const style = window.getComputedStyle(container);
|
||
|
|
padTop = parseFloat(style.paddingTop) || 0; rowGap = parseFloat(style.rowGap) || 0;
|
||
|
|
const visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / (providedItemHeight || 32)));
|
||
|
|
return list.slice(0, Math.min(visibleCount * 3, size));
|
||
|
|
},
|
||
|
|
init: (list, refreshCallback) => {
|
||
|
|
if (listInited) return;
|
||
|
|
const size = list.length;
|
||
|
|
let defaultHeight = providedItemHeight || avg.get() || 32;
|
||
|
|
if (size > 0 && typeof list[0] === 'object' && list[0] !== null && list[0]._itemHeight) {
|
||
|
|
defaultHeight = list[0]._itemHeight;
|
||
|
|
}
|
||
|
|
avg.add(defaultHeight);
|
||
|
|
if (itemMarginTop === null) { itemMarginTop = 0; itemMarginBottom = 0; }
|
||
|
|
for (let i = 0; i < size; i++) {
|
||
|
|
if (!itemHeights.has(i)) {
|
||
|
|
const ih = (typeof list[i] === 'object' && list[i] !== null && list[i]._itemHeight) ? list[i]._itemHeight : defaultHeight;
|
||
|
|
itemHeights.set(i, ih);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (let i = 0; i < size; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, size - i) * defaultHeight);
|
||
|
|
listInited = true; refreshCallback();
|
||
|
|
},
|
||
|
|
update: (absoluteIndex, node) => {
|
||
|
|
if (node.offsetHeight === 0) return;
|
||
|
|
if (itemMarginTop === null) {
|
||
|
|
const style = window.getComputedStyle(node);
|
||
|
|
itemMarginTop = parseFloat(style.marginTop) || 0; itemMarginBottom = parseFloat(style.marginBottom) || 0;
|
||
|
|
}
|
||
|
|
if (absoluteIndex === 0 && !topMargin) topMargin = itemMarginTop;
|
||
|
|
const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + rowGap;
|
||
|
|
const oldHeight = itemHeights.get(absoluteIndex);
|
||
|
|
if (newHeight !== oldHeight) {
|
||
|
|
itemHeights.set(absoluteIndex, newHeight); avg.add(newHeight);
|
||
|
|
const offset = newHeight - (oldHeight || 0), groupIndex = absoluteIndex - (absoluteIndex % groupItemCount);
|
||
|
|
if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
calc: (container, list) => {
|
||
|
|
if (!listInited || !list) return null;
|
||
|
|
const size = list.length;
|
||
|
|
const avgVal = Math.max(16, avg.get() || 32);
|
||
|
|
let visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / avgVal));
|
||
|
|
let prev = padTop + topMargin + rowGap, post = 0, status = 0, listStartIndex = 0, listEndIndex = 0;
|
||
|
|
let renderedList = [];
|
||
|
|
const scrollTop = container.scrollTop;
|
||
|
|
let loopCount = 0;
|
||
|
|
|
||
|
|
for (let i = 0; i < size; i++) {
|
||
|
|
if (++loopCount > size * 2) throw new Error('VirtualScroll infinite loop');
|
||
|
|
if (status === 0) {
|
||
|
|
const gh = groupHeights.get(i);
|
||
|
|
if (gh && prev + gh <= scrollTop && (i + groupItemCount < size)) {
|
||
|
|
prev += gh; i += groupItemCount - 1;
|
||
|
|
} else {
|
||
|
|
const ih = itemHeights.get(i);
|
||
|
|
if (prev + ih <= scrollTop && i < size - 1) {
|
||
|
|
prev += ih;
|
||
|
|
} else {
|
||
|
|
status = 1;
|
||
|
|
let visibleStartIndex = Math.max(0, i);
|
||
|
|
listStartIndex = Math.max(0, visibleStartIndex - visibleCount);
|
||
|
|
listEndIndex = Math.min(listStartIndex + visibleCount * 3, size);
|
||
|
|
i = listEndIndex - 1;
|
||
|
|
renderedList = list.slice(listStartIndex, listEndIndex);
|
||
|
|
for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (status === 1) {
|
||
|
|
const gh = groupHeights.get(i);
|
||
|
|
if (gh) { post += gh; i += groupItemCount - 1; }
|
||
|
|
else post += itemHeights.get(i);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const finalPrevHeight = Math.max(0, prev - padTop - topMargin - rowGap - (listStartIndex > 0 ? rowGap : 0));
|
||
|
|
const finalPostHeight = post > 0 ? Math.max(0, post - 2 * rowGap) : 0;
|
||
|
|
return { prevHeight: finalPrevHeight, postHeight: finalPostHeight, renderedList, listStartIndex };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
globalThis.VirtualScroll = VirtualScroll;
|