UNPKG

@z-cloud/virtual-vanilla

Version:

提供跨平台(浏览器,小程序)的虚拟列表公共基类

274 lines (273 loc) 9.36 kB
"use strict"; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const utils = require("./utils.cjs"); const defaultGetItemKey = (index) => index; const defaultCustomeRange = (range) => { const start = Math.max(range.startIndex - range.overscan, 0); const end = Math.min(range.endIndex + range.overscan, range.count - 1); return Array.from({ length: end - start + 1 }, (_, index) => index + start); }; class BasicVirtualizer { constructor() { __publicField(this, "options"); __publicField(this, "items", []); __publicField(this, "scrollElementRect"); __publicField(this, "scrollOffset"); // 是否滚动状态 __publicField(this, "scrolling", false); __publicField(this, "range"); __publicField(this, "unsubscribes", []); __publicField(this, "dynamicSizeCache", /* @__PURE__ */ new Map()); __publicField(this, "pendingDynamicSizeIndexes", []); /** * 所有虚拟项数据 */ __publicField(this, "geItemsWithtMemo", utils.memoFnResult( (count, paddingStart, scrollMargin) => { const { getItemKey, lanes, gap, size: configSize } = this.options; const startIndex = this.pendingDynamicSizeIndexes.length > 0 ? Math.min(...this.pendingDynamicSizeIndexes) : 0; const nextItems = this.items.slice(0, startIndex); this.pendingDynamicSizeIndexes = []; for (let index = startIndex; index < count; index++) { const key = getItemKey(index); const lastItem = lanes === 1 ? nextItems[index - 1] : this.getLastItemForLane(nextItems, index); const start = lastItem ? lastItem.end + gap : paddingStart + scrollMargin; const dynamicSize = this.dynamicSizeCache.get(index); const size = dynamicSize ?? (typeof configSize === "function" ? configSize(index) : configSize); const lane = !lastItem ? index % lanes : lastItem.lane; nextItems[index] = { key, index, start, end: start + size, size, lane }; } return this.items = nextItems; } )); __publicField(this, "getVirtualItemsWithMemo", utils.memoFnResult((indexes, items) => { return indexes.map((index) => items[index]); })); __publicField(this, "getVirtualIndexesWithMemo", utils.memoFnResult( (count, overscan, startIndex, endIndex) => { if (startIndex === void 0 || endIndex === void 0) { return []; } return this.options.customeRange({ startIndex, endIndex, overscan, count }); } )); __publicField(this, "calculateRangeWithMemo", utils.memoFnResult( (lanes, scrollElementSize, scrollOffset, items) => { if (items.length === 0 || scrollElementSize === 0) { return; } const lastIndex = items.length - 1; if (items.length <= lanes) { return { startIndex: 0, endIndex: lastIndex }; } let startIndex = this.calculateStartIndex(scrollOffset); let endIndex = startIndex; if (lanes === 1) { while (endIndex < lastIndex && items[endIndex].end < scrollElementSize + scrollOffset) { endIndex++; } } else if (lanes > 1) { const endLanes = Array(lanes).fill(0); while (endIndex < lastIndex && endLanes.some((end) => end < scrollElementSize + scrollOffset)) { const item = items[endIndex]; endLanes[item.lane] = item.end; endIndex++; } startIndex = Math.max(0, startIndex - startIndex % lanes); endIndex = Math.min(lastIndex, endIndex + (lanes - 1 - endIndex % lanes)); } return { startIndex, endIndex }; } )); // eslint-disable-next-line @typescript-eslint/no-unused-vars __publicField(this, "notifyWithMemo", utils.memoFnResult( (scrolling, _startIndex, _endIndex) => { this.options.onChange(scrolling); } )); } setOptions(options) { Object.entries(options).forEach(([key, value]) => { if (typeof value === "undefined") { Reflect.deleteProperty(options, key); } }); this.options = { initialOffset: 0, overscan: 1, paddingStart: 0, paddingEnd: 0, scrollMargin: 0, horizontal: false, gap: 0, lanes: 1, getItemKey: defaultGetItemKey, customeRange: defaultCustomeRange, onChange: () => { }, ...options }; } dynamicMode() { return this.dynamicSizeCache.size > 0; } setScrollElementRect(rect) { this.scrollElementRect = rect; this.notify(); } /** * 获取每条lane的最后一个item 并返回end值最小的那一个 * @param items * @param index */ getLastItemForLane(items, index) { const lastItems = /* @__PURE__ */ new Map(); for (let i = index - 1; i >= 0; i--) { const item = items[i]; const previousLastItem = lastItems.get(item.lane); if (previousLastItem == null || item.end > previousLastItem.end) { lastItems.set(item.lane, item); } if (lastItems.size === this.options.lanes) { break; } } if (lastItems.size === this.options.lanes) { return [...lastItems.values()].sort((a, b) => { if (a.end === b.end) { return a.index - b.index; } return a.end - b.end; })[0]; } return void 0; } getItems() { return this.geItemsWithtMemo( this.options.count, this.options.paddingStart, this.options.scrollMargin ); } getVirtualItems() { const indexes = this.getVirtualIndexes(); const items = this.getItems(); return this.getVirtualItemsWithMemo(indexes, items); } getVirtualIndexes() { var _a, _b; const { count, overscan } = this.options; this.range = this.calculateRangeIndex(); return this.getVirtualIndexesWithMemo( count, overscan, (_a = this.range) == null ? void 0 : _a.startIndex, (_b = this.range) == null ? void 0 : _b.endIndex ); } getScrollOffset() { this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset); return this.scrollOffset; } getSize() { var _a; return ((_a = this.scrollElementRect) == null ? void 0 : _a[this.options.horizontal ? "width" : "height"]) ?? 0; } getTotalSize() { var _a; const { lanes, scrollMargin, paddingStart, paddingEnd } = this.options; const items = this.getItems(); let end = 0; if (items.length === 0) { end = paddingStart; } else if (lanes === 1) { end = ((_a = items[items.length - 1]) == null ? void 0 : _a.end) ?? 0; } else { const endLanes = Array(lanes).fill(null); let endIndex = items.length - 1; while (endIndex > -1 && endLanes.some((val) => val === null)) { const item = items[endIndex]; if (endLanes[item.lane] === null) { endLanes[item.lane] = item.end; } endIndex--; } end = Math.max(...endLanes.filter((val) => val !== null)); } return Math.max(0, end + paddingEnd - scrollMargin); } calculateStartIndex(scrollOffset) { let lowIndex = 0; let highIndex = this.items.length - 1; while (lowIndex <= highIndex) { const middleIndex = Math.floor((lowIndex + highIndex) / 2); const currentOffset = this.items[middleIndex].start; if (currentOffset < scrollOffset) { lowIndex = middleIndex + 1; } else if (currentOffset > scrollOffset) { highIndex = middleIndex - 1; } else { return middleIndex; } } return lowIndex > 0 ? lowIndex - 1 : 0; } calculateRangeIndex() { return this.calculateRangeWithMemo( this.options.lanes, this.getSize(), this.getScrollOffset(), this.getItems() ); } getOffsetForAlign(offset, align = "start", itemSize = 0) { const size = this.getSize(); if (align === "center") { offset += (itemSize - size) / 2; } else if (align === "end") { offset -= size; } const maxScrollOffset = this.getTotalSize() - size; return Math.max(0, Math.min(maxScrollOffset, offset)); } getOffsetForIndex(index, align = "start") { const item = this.items[index]; if (!item) { return 0; } const offset = align === "end" ? item.end : item.start; return this.getOffsetForAlign(offset, align, item.size); } notify() { const range = this.calculateRangeIndex(); this.notifyWithMemo(this.scrolling, range == null ? void 0 : range.startIndex, range == null ? void 0 : range.endIndex); } /** * 清除指定函数的缓存 * @param names 需要清除缓存的函数名称 */ clearFnMemo(names) { names.forEach((name) => { this[name].clear(); }); } } exports.BasicVirtualizer = BasicVirtualizer;