@z-cloud/virtual-vanilla
Version:
提供跨平台(浏览器,小程序)的虚拟列表公共基类
274 lines (273 loc) • 9.23 kB
JavaScript
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);
import { memoFnResult } from "./utils.js";
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", 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", memoFnResult((indexes, items) => {
return indexes.map((index) => items[index]);
}));
__publicField(this, "getVirtualIndexesWithMemo", memoFnResult(
(count, overscan, startIndex, endIndex) => {
if (startIndex === void 0 || endIndex === void 0) {
return [];
}
return this.options.customeRange({
startIndex,
endIndex,
overscan,
count
});
}
));
__publicField(this, "calculateRangeWithMemo", 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", 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();
});
}
}
export {
BasicVirtualizer
};