UNPKG

@z-cloud/virtual-browser

Version:

适用于浏览器端的虚拟列表类

249 lines (248 loc) 8.35 kB
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 { BasicVirtualizer, debounce } from "@z-cloud/virtual-vanilla"; import { getElementOffsetTop } from "./utils/index.js"; const eventListenerOptions = { passive: true }; const getScrollElementRect = (element) => { const isElement = element instanceof HTMLElement; const width = isElement ? element.offsetWidth : element.innerWidth; const height = isElement ? element.offsetHeight : element.innerHeight; return { width, height }; }; class BrowserVirtualizer extends BasicVirtualizer { constructor(options) { super(); __publicField(this, "scrollElement", null); __publicField(this, "targetWindow", null); __publicField(this, "elementObserver", null); __publicField(this, "dynamicElementsCache", /* @__PURE__ */ new Map()); __publicField(this, "scrollToIndexTimeoutId", null); __publicField(this, "elementMounted", (element) => { if (!element) { this.dynamicElementsCache.forEach((ele, key) => { var _a; if (!ele.isConnected) { (_a = this.elementObserver) == null ? void 0 : _a.unobserve(ele); this.dynamicElementsCache.delete(key); } }); return; } this.handleElementSizeChange( element, this.options.horizontal ? element.offsetWidth : element.offsetHeight ); }); __publicField(this, "resetScrolling", debounce(() => { this.scrolling = false; this.notify(); }, 160)); this.setOptions(options); } init(scrollElement) { if (!scrollElement || this.scrollElement === scrollElement) { return; } this.scrollElement = scrollElement; if (this.scrollElement instanceof HTMLElement) { this.targetWindow = this.scrollElement.ownerDocument.defaultView; this.unsubscribes.push(this.observeElementRect()); } else { this.targetWindow = this.scrollElement.window; this.unsubscribes.push(this.observeWindowRect()); } this.unsubscribes.push(this.addScrollEventListener()); this.initElementObserver(); this.scrollToOffset( typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset ); this.notify(); } observeElementRect() { const scrollElement = this.scrollElement; const targetWindow = this.targetWindow; if (!targetWindow || !globalThis.ResizeObserver) { return; } this.setScrollElementRect(getScrollElementRect(scrollElement)); const observe = new ResizeObserver((entries) => { var _a; for (const entry of entries) { const boxSize = (_a = entry.borderBoxSize) == null ? void 0 : _a[0]; if (boxSize) { this.setScrollElementRect({ width: boxSize.inlineSize, height: boxSize.blockSize }); return; } } this.setScrollElementRect(getScrollElementRect(scrollElement)); }); observe.observe(scrollElement, { box: "border-box" }); return () => { observe.unobserve(scrollElement); }; } observeWindowRect() { const targetWindow = this.targetWindow; if (!targetWindow) { return; } const handleResize = () => { this.setScrollElementRect(getScrollElementRect(targetWindow)); }; handleResize(); targetWindow.addEventListener("resize", handleResize); return () => { targetWindow.removeEventListener("resize", handleResize); }; } initElementObserver() { if (!this.targetWindow || !globalThis.ResizeObserver) { return; } this.elementObserver = new ResizeObserver((entries) => { var _a; for (const entry of entries) { const boxSize = (_a = entry.borderBoxSize) == null ? void 0 : _a[0]; if (boxSize) { this.handleElementSizeChange( entry.target, boxSize[this.options.horizontal ? "inlineSize" : "blockSize"] ); } } }); } resizeItemSize(index, size) { const item = this.items[index]; if (!item) { return; } const delta = size - item.size; if (Math.abs(delta) >= 1) { const offset = this.getScrollOffset(); if (item.start < offset) { this.scrollTo(offset + delta); } this.dynamicSizeCache.set(index, size); this.pendingDynamicSizeIndexes.push(index); this.scrolling = false; this.clearFnMemo(["geItemsWithtMemo", "notifyWithMemo"]); this.notify(); } } handleElementSizeChange(element, size) { var _a, _b; const index = Number.parseInt(element.dataset.index ?? "-1", 10); const item = this.getItems()[index]; if (!item) { return; } const key = item.key; const prevElement = this.dynamicElementsCache.get(key); if (prevElement !== element) { if (prevElement) { (_a = this.elementObserver) == null ? void 0 : _a.unobserve(prevElement); } (_b = this.elementObserver) == null ? void 0 : _b.observe(element, { box: "border-box" }); this.dynamicElementsCache.set(item.key, element); } if (element.isConnected) { this.resizeItemSize(index, size); } } /** * 添加滚动监听 */ addScrollEventListener() { const scrollElement = this.scrollElement; if (!this.targetWindow || !scrollElement) { return; } const scrollHandler = () => { let offset = 0; const { horizontal } = this.options; if (scrollElement instanceof HTMLElement) { offset = horizontal ? scrollElement["scrollLeft"] : scrollElement["scrollTop"]; } else { offset = horizontal ? scrollElement.scrollX : scrollElement.scrollY; } this.scrollOffset = offset; this.scrolling = true; this.resetScrolling(this.targetWindow); this.notify(); }; scrollElement.addEventListener("scroll", scrollHandler, eventListenerOptions); return () => { scrollElement.removeEventListener("scroll", scrollHandler); }; } scrollTo(offset, behavior = "instant") { var _a; (_a = this.scrollElement) == null ? void 0 : _a.scrollTo({ [this.options.horizontal ? "left" : "top"]: offset, behavior }); } scrollToOffset(offset, behavior) { this.cancelScrollToIndex(); this.scrollTo(offset, behavior); } cancelScrollToIndex() { var _a; if (this.scrollToIndexTimeoutId !== null) { (_a = this.targetWindow) == null ? void 0 : _a.clearTimeout(this.scrollToIndexTimeoutId); this.scrollToIndexTimeoutId = null; } } scrollToIndex(index, { align, behavior } = {}) { this.cancelScrollToIndex(); const safeIndex = Math.max(0, Math.min(index, this.options.count - 1)); const offset = this.getOffsetForIndex(safeIndex, align); if (!offset) { return; } this.scrollTo(offset, this.dynamicMode() ? "instant" : behavior); if (this.dynamicMode() && this.targetWindow) { this.scrollToIndexTimeoutId = this.targetWindow.setTimeout(() => { this.scrollToIndexTimeoutId = null; if (this.dynamicElementsCache.has(this.options.getItemKey(safeIndex))) { const calcOffset = this.getOffsetForIndex(safeIndex, align); if (!calcOffset) { return; } const currentScrollOffset = this.getScrollOffset(); if (Math.abs(calcOffset - currentScrollOffset) > 1) { this.scrollToIndex(safeIndex, { align, behavior: "instant" }); } } else { this.scrollToIndex(safeIndex, { align, behavior: "instant" }); } }); } } clean() { var _a; this.unsubscribes = this.unsubscribes.filter((unsub) => { if (typeof unsub === "function") { unsub(); } return false; }); (_a = this.elementObserver) == null ? void 0 : _a.disconnect(); this.elementObserver = null; this.dynamicSizeCache.clear(); this.pendingDynamicSizeIndexes = []; this.scrollElement = null; this.targetWindow = null; } } export { BrowserVirtualizer, getElementOffsetTop };