@vielzeug/scroll 26.10.0 → 26.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,4 +48,4 @@ virt.dispose();
48
48
 
49
49
  ## License
50
50
 
51
- MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
51
+ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu): part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
@@ -1 +1 @@
1
- {"version":3,"file":"_axis1d.cjs","names":[],"sources":["../src/_axis1d.ts"],"sourcesContent":["/**\n * Shared 1D axis primitive: prefix-sum offset table, O(log n) binary search,\n * incremental rebuild, measurement batching via microtask, and dedup guards.\n *\n * Used directly by grid-virtualizer (numeric row/col indices) and composed\n * into virtualizer (which layers VirtualKey translation on top).\n */\n\n/** A single rendered virtual item — position and size on one axis. */\nexport interface VirtualItem {\n end: number;\n index: number;\n size: number;\n start: number;\n}\n\nexport interface Axis1DRange {\n renderEnd: number;\n renderStart: number;\n}\n\n/**\n * Create a self-contained 1D offset-table axis.\n *\n * @param initialCount - Number of items.\n * @param sizeAtFn - Returns the resolved size for a given index (measured or estimated).\n * The virtualizer passes a closure that reads the live measurement cache,\n * so changes to the cache are reflected automatically without re-setting.\n * @param initialGap - Gap between consecutive items (non-negative integer, pixels).\n */\nexport function createAxis1D(initialCount: number, sizeAtFn: (index: number) => number, initialGap: number) {\n let count = initialCount;\n let gap = initialGap;\n const sizeAt = sizeAtFn;\n\n let offsets: number[] = [];\n let totalSize = 0;\n\n // ─── Dedup guards ───────────────────────────────────────────────────────────\n // Track the last-emitted render range and total size so computeVisible() can\n // skip re-rendering when nothing changed.\n\n let prevStart = -1;\n let prevEnd = -1;\n let prevTotalSize = -1;\n\n // ─── Pending measurement flush ─────────────────────────────────────────────\n\n let pendingBuild = false;\n let minChangedIndex = Infinity;\n\n // ─── Offset helpers ─────────────────────────────────────────────────────────\n\n function startAt(i: number): number {\n return offsets[i] ?? 0;\n }\n\n function endAt(i: number): number {\n return startAt(i) + sizeAt(i);\n }\n\n // ─── Full rebuild ───────────────────────────────────────────────────────────\n\n /**\n * Full O(n) prefix-sum rebuild.\n * Pass `forceEmit = true` to reset dedup guards, ensuring computeVisible()\n * emits on the next call even if the rendered range is unchanged.\n */\n function rebuild(forceEmit = false): void {\n if (forceEmit) {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n const next: number[] = [0];\n let pos = 0;\n\n for (let i = 0; i < count; i++) {\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n next.push(pos);\n }\n\n offsets = next;\n totalSize = pos;\n }\n\n // ─── Incremental rebuild ────────────────────────────────────────────────────\n\n /**\n * O(count - fromIndex) incremental rebuild — used after measurement.\n * Always resets dedup guards because measurements change layout.\n */\n function rebuildFrom(fromIndex: number): void {\n if (!Number.isFinite(fromIndex) || fromIndex >= count) return;\n\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n\n let pos = offsets[fromIndex] ?? 0;\n\n for (let i = fromIndex; i < count; i++) {\n offsets[i] = pos;\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n }\n\n offsets[count] = pos;\n totalSize = pos;\n }\n\n // ─── Binary search ──────────────────────────────────────────────────────────\n\n function findFirst(viewportStart: number): number {\n let lo = 0;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n\n if (endAt(mid) <= viewportStart) lo = mid + 1;\n else hi = mid;\n }\n\n return lo;\n }\n\n function findLast(viewportEnd: number, from: number): number {\n let lo = from;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n\n if (startAt(mid) < viewportEnd) lo = mid;\n else hi = mid - 1;\n }\n\n return lo;\n }\n\n /**\n * Compute the render range for a given viewport window and overscan values.\n * Returns indices clamped to [0, count - 1].\n */\n function computeRange(\n viewportStart: number,\n viewportEnd: number,\n overscanStart: number,\n overscanEnd: number,\n ): Axis1DRange {\n const first = findFirst(viewportStart);\n const last = findLast(viewportEnd, first);\n\n return {\n renderEnd: Math.min(count - 1, last + overscanEnd),\n renderStart: Math.max(0, first - overscanStart),\n };\n }\n\n // ─── Dedup helpers ──────────────────────────────────────────────────────────\n\n /** Returns `true` when start/end/totalSize are all identical to the last commit. */\n function isDedupSame(start: number, end: number): boolean {\n return start === prevStart && end === prevEnd && totalSize === prevTotalSize;\n }\n\n function commitDedup(start: number, end: number): void {\n prevStart = start;\n prevEnd = end;\n prevTotalSize = totalSize;\n }\n\n function resetDedup(): void {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n // ─── Microtask scheduling ───────────────────────────────────────────────────\n\n /**\n * Schedule an incremental rebuild on the next microtask, then call `onFlush`.\n * Multiple calls before the microtask fires are coalesced into one rebuild.\n * Returns without scheduling if a build is already pending.\n */\n function scheduleBuild(onFlush: () => void): void {\n if (pendingBuild) return;\n\n pendingBuild = true;\n queueMicrotask(() => {\n pendingBuild = false;\n\n const from = minChangedIndex;\n\n minChangedIndex = Infinity;\n rebuildFrom(from);\n onFlush();\n });\n }\n\n /** Mark an item index as having a changed measurement. Drives incremental rebuilds. */\n function markChanged(index: number): void {\n if (index < minChangedIndex) minChangedIndex = index;\n }\n\n // ─── Item descriptor ────────────────────────────────────────────────────────\n\n /** Build a VirtualItem for a given index using current offset table. */\n function itemAt(index: number): VirtualItem {\n const start = startAt(index);\n const size = sizeAt(index);\n\n return { end: start + size, index, size, start };\n }\n\n // ─── Returned object ────────────────────────────────────────────────────────\n\n return {\n commitDedup,\n computeRange,\n /**\n * Atomically read and reset minChangedIndex to Infinity.\n * Used by grid-virtualizer's coordinated flush so it can consume the\n * changed-index without going through axis1d's own scheduleBuild.\n */\n consumeMinChangedIndex(): number {\n const idx = minChangedIndex;\n\n minChangedIndex = Infinity;\n\n return idx;\n },\n get count(): number {\n return count;\n },\n endAt,\n findFirst,\n findLast,\n /** Current gap between items (pixels). Used by callers to dedup gap changes. */\n get gap(): number {\n return gap;\n },\n isDedupSame,\n itemAt,\n markChanged,\n get minChangedIndex(): number {\n return minChangedIndex;\n },\n get pendingBuild(): boolean {\n return pendingBuild;\n },\n get prevEnd(): number {\n return prevEnd;\n },\n get prevStart(): number {\n return prevStart;\n },\n get prevTotalSize(): number {\n return prevTotalSize;\n },\n rebuild,\n rebuildFrom,\n resetDedup,\n scheduleBuild,\n setCount(n: number): void {\n count = n;\n },\n setGap(n: number): void {\n gap = n;\n },\n sizeAt(i: number): number {\n return sizeAt(i);\n },\n startAt,\n get totalSize(): number {\n return totalSize;\n },\n };\n}\n\nexport type Axis1D = ReturnType<typeof createAxis1D>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"_axis1d.cjs","names":[],"sources":["../src/_axis1d.ts"],"sourcesContent":["/**\n * Shared 1D axis primitive: prefix-sum offset table, O(log n) binary search,\n * incremental rebuild, measurement batching via microtask, and dedup guards.\n *\n * Used directly by grid-virtualizer (numeric row/col indices) and composed\n * into virtualizer (which layers VirtualKey translation on top).\n */\n\n/** A single rendered virtual item: position and size on one axis. */\nexport interface VirtualItem {\n end: number;\n index: number;\n size: number;\n start: number;\n}\n\nexport interface Axis1DRange {\n renderEnd: number;\n renderStart: number;\n}\n\n/**\n * Create a self-contained 1D offset-table axis.\n *\n * @param initialCount - Number of items.\n * @param sizeAtFn - Returns the resolved size for a given index (measured or estimated).\n * The virtualizer passes a closure that reads the live measurement cache,\n * so changes to the cache are reflected automatically without re-setting.\n * @param initialGap - Gap between consecutive items (non-negative integer, pixels).\n */\nexport function createAxis1D(initialCount: number, sizeAtFn: (index: number) => number, initialGap: number) {\n let count = initialCount;\n let gap = initialGap;\n const sizeAt = sizeAtFn;\n\n let offsets: number[] = [];\n let totalSize = 0;\n\n // ─── Dedup guards ───────────────────────────────────────────────────────────\n // Track the last-emitted render range and total size so computeVisible() can\n // skip re-rendering when nothing changed.\n\n let prevStart = -1;\n let prevEnd = -1;\n let prevTotalSize = -1;\n\n // ─── Pending measurement flush ─────────────────────────────────────────────\n\n let pendingBuild = false;\n let minChangedIndex = Infinity;\n\n // ─── Offset helpers ─────────────────────────────────────────────────────────\n\n function startAt(i: number): number {\n return offsets[i] ?? 0;\n }\n\n function endAt(i: number): number {\n return startAt(i) + sizeAt(i);\n }\n\n // ─── Full rebuild ───────────────────────────────────────────────────────────\n\n /**\n * Full O(n) prefix-sum rebuild.\n * Pass `forceEmit = true` to reset dedup guards, ensuring computeVisible()\n * emits on the next call even if the rendered range is unchanged.\n */\n function rebuild(forceEmit = false): void {\n if (forceEmit) {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n const next: number[] = [0];\n let pos = 0;\n\n for (let i = 0; i < count; i++) {\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n next.push(pos);\n }\n\n offsets = next;\n totalSize = pos;\n }\n\n // ─── Incremental rebuild ────────────────────────────────────────────────────\n\n /**\n * O(count - fromIndex) incremental rebuild: used after measurement.\n * Always resets dedup guards because measurements change layout.\n */\n function rebuildFrom(fromIndex: number): void {\n if (!Number.isFinite(fromIndex) || fromIndex >= count) return;\n\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n\n let pos = offsets[fromIndex] ?? 0;\n\n for (let i = fromIndex; i < count; i++) {\n offsets[i] = pos;\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n }\n\n offsets[count] = pos;\n totalSize = pos;\n }\n\n // ─── Binary search ──────────────────────────────────────────────────────────\n\n function findFirst(viewportStart: number): number {\n let lo = 0;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n\n if (endAt(mid) <= viewportStart) lo = mid + 1;\n else hi = mid;\n }\n\n return lo;\n }\n\n function findLast(viewportEnd: number, from: number): number {\n let lo = from;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n\n if (startAt(mid) < viewportEnd) lo = mid;\n else hi = mid - 1;\n }\n\n return lo;\n }\n\n /**\n * Compute the render range for a given viewport window and overscan values.\n * Returns indices clamped to [0, count - 1].\n */\n function computeRange(\n viewportStart: number,\n viewportEnd: number,\n overscanStart: number,\n overscanEnd: number,\n ): Axis1DRange {\n const first = findFirst(viewportStart);\n const last = findLast(viewportEnd, first);\n\n return {\n renderEnd: Math.min(count - 1, last + overscanEnd),\n renderStart: Math.max(0, first - overscanStart),\n };\n }\n\n // ─── Dedup helpers ──────────────────────────────────────────────────────────\n\n /** Returns `true` when start/end/totalSize are all identical to the last commit. */\n function isDedupSame(start: number, end: number): boolean {\n return start === prevStart && end === prevEnd && totalSize === prevTotalSize;\n }\n\n function commitDedup(start: number, end: number): void {\n prevStart = start;\n prevEnd = end;\n prevTotalSize = totalSize;\n }\n\n function resetDedup(): void {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n // ─── Microtask scheduling ───────────────────────────────────────────────────\n\n /**\n * Schedule an incremental rebuild on the next microtask, then call `onFlush`.\n * Multiple calls before the microtask fires are coalesced into one rebuild.\n * Returns without scheduling if a build is already pending.\n */\n function scheduleBuild(onFlush: () => void): void {\n if (pendingBuild) return;\n\n pendingBuild = true;\n queueMicrotask(() => {\n pendingBuild = false;\n\n const from = minChangedIndex;\n\n minChangedIndex = Infinity;\n rebuildFrom(from);\n onFlush();\n });\n }\n\n /** Mark an item index as having a changed measurement. Drives incremental rebuilds. */\n function markChanged(index: number): void {\n if (index < minChangedIndex) minChangedIndex = index;\n }\n\n // ─── Item descriptor ────────────────────────────────────────────────────────\n\n /** Build a VirtualItem for a given index using current offset table. */\n function itemAt(index: number): VirtualItem {\n const start = startAt(index);\n const size = sizeAt(index);\n\n return { end: start + size, index, size, start };\n }\n\n // ─── Returned object ────────────────────────────────────────────────────────\n\n return {\n commitDedup,\n computeRange,\n /**\n * Atomically read and reset minChangedIndex to Infinity.\n * Used by grid-virtualizer's coordinated flush so it can consume the\n * changed-index without going through axis1d's own scheduleBuild.\n */\n consumeMinChangedIndex(): number {\n const idx = minChangedIndex;\n\n minChangedIndex = Infinity;\n\n return idx;\n },\n get count(): number {\n return count;\n },\n endAt,\n findFirst,\n findLast,\n /** Current gap between items (pixels). Used by callers to dedup gap changes. */\n get gap(): number {\n return gap;\n },\n isDedupSame,\n itemAt,\n markChanged,\n get minChangedIndex(): number {\n return minChangedIndex;\n },\n get pendingBuild(): boolean {\n return pendingBuild;\n },\n get prevEnd(): number {\n return prevEnd;\n },\n get prevStart(): number {\n return prevStart;\n },\n get prevTotalSize(): number {\n return prevTotalSize;\n },\n rebuild,\n rebuildFrom,\n resetDedup,\n scheduleBuild,\n setCount(n: number): void {\n count = n;\n },\n setGap(n: number): void {\n gap = n;\n },\n sizeAt(i: number): number {\n return sizeAt(i);\n },\n startAt,\n get totalSize(): number {\n return totalSize;\n },\n };\n}\n\nexport type Axis1D = ReturnType<typeof createAxis1D>;\n"],"mappings":"AA8BA,SAAgB,EAAa,EAAsB,EAAqC,EAAoB,CAC1G,IAAI,EAAQ,EACR,EAAM,EACJ,EAAS,EAEX,EAAoB,CAAC,EACrB,EAAY,EAMZ,EAAY,GACZ,EAAU,GACV,EAAgB,GAIhB,EAAe,GACf,EAAkB,IAItB,SAAS,EAAQ,EAAmB,CAClC,OAAO,EAAQ,IAAM,CACvB,CAEA,SAAS,EAAM,EAAmB,CAChC,OAAO,EAAQ,CAAC,EAAI,EAAO,CAAC,CAC9B,CASA,SAAS,EAAQ,EAAY,GAAa,CACpC,IACF,EAAY,GACZ,EAAU,GACV,EAAgB,IAGlB,IAAM,EAAiB,CAAC,CAAC,EACrB,EAAM,EAEV,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,IACzB,GAAO,EAAO,CAAC,GAAK,EAAI,EAAQ,EAAI,EAAM,GAC1C,EAAK,KAAK,CAAG,EAGf,EAAU,EACV,EAAY,CACd,CAQA,SAAS,EAAY,EAAyB,CAC5C,GAAI,CAAC,OAAO,SAAS,CAAS,GAAK,GAAa,EAAO,OAEvD,EAAY,GACZ,EAAU,GACV,EAAgB,GAEhB,IAAI,EAAM,EAAQ,IAAc,EAEhC,IAAK,IAAI,EAAI,EAAW,EAAI,EAAO,IACjC,EAAQ,GAAK,EACb,GAAO,EAAO,CAAC,GAAK,EAAI,EAAQ,EAAI,EAAM,GAG5C,EAAQ,GAAS,EACjB,EAAY,CACd,CAIA,SAAS,EAAU,EAA+B,CAChD,IAAI,EAAK,EACL,EAAK,EAAQ,EAEjB,KAAO,EAAK,GAAI,CACd,IAAM,EAAO,EAAK,GAAO,EAErB,EAAM,CAAG,GAAK,EAAe,EAAK,EAAM,EACvC,EAAK,CACZ,CAEA,OAAO,CACT,CAEA,SAAS,EAAS,EAAqB,EAAsB,CAC3D,IAAI,EAAK,EACL,EAAK,EAAQ,EAEjB,KAAO,EAAK,GAAI,CACd,IAAM,EAAO,EAAK,EAAK,GAAM,EAEzB,EAAQ,CAAG,EAAI,EAAa,EAAK,EAChC,EAAK,EAAM,CAClB,CAEA,OAAO,CACT,CAMA,SAAS,EACP,EACA,EACA,EACA,EACa,CACb,IAAM,EAAQ,EAAU,CAAa,EAC/B,EAAO,EAAS,EAAa,CAAK,EAExC,MAAO,CACL,UAAW,KAAK,IAAI,EAAQ,EAAG,EAAO,CAAW,EACjD,YAAa,KAAK,IAAI,EAAG,EAAQ,CAAa,CAChD,CACF,CAKA,SAAS,EAAY,EAAe,EAAsB,CACxD,OAAO,IAAU,GAAa,IAAQ,GAAW,IAAc,CACjE,CAEA,SAAS,EAAY,EAAe,EAAmB,CACrD,EAAY,EACZ,EAAU,EACV,EAAgB,CAClB,CAEA,SAAS,GAAmB,CAC1B,EAAY,GACZ,EAAU,GACV,EAAgB,EAClB,CASA,SAAS,EAAc,EAA2B,CAC5C,IAEJ,EAAe,GACf,mBAAqB,CACnB,EAAe,GAEf,IAAM,EAAO,EAEb,EAAkB,IAClB,EAAY,CAAI,EAChB,EAAQ,CACV,CAAC,EACH,CAGA,SAAS,EAAY,EAAqB,CACpC,EAAQ,IAAiB,EAAkB,EACjD,CAKA,SAAS,EAAO,EAA4B,CAC1C,IAAM,EAAQ,EAAQ,CAAK,EACrB,EAAO,EAAO,CAAK,EAEzB,MAAO,CAAE,IAAK,EAAQ,EAAM,QAAO,OAAM,OAAM,CACjD,CAIA,MAAO,CACL,cACA,eAMA,wBAAiC,CAC/B,IAAM,EAAM,EAIZ,MAFA,GAAkB,IAEX,CACT,EACA,IAAI,OAAgB,CAClB,OAAO,CACT,EACA,QACA,YACA,WAEA,IAAI,KAAc,CAChB,OAAO,CACT,EACA,cACA,SACA,cACA,IAAI,iBAA0B,CAC5B,OAAO,CACT,EACA,IAAI,cAAwB,CAC1B,OAAO,CACT,EACA,IAAI,SAAkB,CACpB,OAAO,CACT,EACA,IAAI,WAAoB,CACtB,OAAO,CACT,EACA,IAAI,eAAwB,CAC1B,OAAO,CACT,EACA,UACA,cACA,aACA,gBACA,SAAS,EAAiB,CACxB,EAAQ,CACV,EACA,OAAO,EAAiB,CACtB,EAAM,CACR,EACA,OAAO,EAAmB,CACxB,OAAO,EAAO,CAAC,CACjB,EACA,UACA,IAAI,WAAoB,CACtB,OAAO,CACT,CACF,CACF"}
package/dist/_axis1d.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Used directly by grid-virtualizer (numeric row/col indices) and composed
6
6
  * into virtualizer (which layers VirtualKey translation on top).
7
7
  */
8
- /** A single rendered virtual item — position and size on one axis. */
8
+ /** A single rendered virtual item: position and size on one axis. */
9
9
  export interface VirtualItem {
10
10
  end: number;
11
11
  index: number;
@@ -1 +1 @@
1
- {"version":3,"file":"_axis1d.d.ts","sourceRoot":"","sources":["../src/_axis1d.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,UAAU,EAAE,MAAM;yBAyI5E,MAAM,OAAO,MAAM,KAAG,IAAI;kCArBrC,MAAM,eACR,MAAM,iBACJ,MAAM,eACR,MAAM,KAClB,WAAW;IAuEZ;;;;OAIG;8BACuB,MAAM;oBAOnB,MAAM;eAhLH,MAAM,KAAG,MAAM;+BAwDC,MAAM,KAAG,MAAM;4BAclB,MAAM,QAAQ,MAAM,KAAG,MAAM;IAgH1D,gFAAgF;kBACrE,MAAM;yBA7ES,MAAM,OAAO,MAAM,KAAG,OAAO;oBA8ClC,MAAM,KAAG,WAAW;yBAPf,MAAM,KAAG,IAAI;8BA4ChB,MAAM;2BAGT,OAAO;sBAGZ,MAAM;wBAGJ,MAAM;4BAGF,MAAM;sCA9LQ,IAAI;6BAyBT,MAAM,KAAG,IAAI;sBAgFtB,IAAI;6BAaK,MAAM,IAAI,KAAG,IAAI;gBA+EnC,MAAM,GAAG,IAAI;cAGf,MAAM,GAAG,IAAI;cAGb,MAAM,GAAG,MAAM;iBA1NP,MAAM,KAAG,MAAM;wBA8NhB,MAAM;EAI1B;AAED,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC"}
1
+ {"version":3,"file":"_axis1d.d.ts","sourceRoot":"","sources":["../src/_axis1d.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,UAAU,EAAE,MAAM;yBAyI5E,MAAM,OAAO,MAAM,KAAG,IAAI;kCArBrC,MAAM,eACR,MAAM,iBACJ,MAAM,eACR,MAAM,KAClB,WAAW;IAuEZ;;;;OAIG;8BACuB,MAAM;oBAOnB,MAAM;eAhLH,MAAM,KAAG,MAAM;+BAwDC,MAAM,KAAG,MAAM;4BAclB,MAAM,QAAQ,MAAM,KAAG,MAAM;IAgH1D,gFAAgF;kBACrE,MAAM;yBA7ES,MAAM,OAAO,MAAM,KAAG,OAAO;oBA8ClC,MAAM,KAAG,WAAW;yBAPf,MAAM,KAAG,IAAI;8BA4ChB,MAAM;2BAGT,OAAO;sBAGZ,MAAM;wBAGJ,MAAM;4BAGF,MAAM;sCA9LQ,IAAI;6BAyBT,MAAM,KAAG,IAAI;sBAgFtB,IAAI;6BAaK,MAAM,IAAI,KAAG,IAAI;gBA+EnC,MAAM,GAAG,IAAI;cAGf,MAAM,GAAG,IAAI;cAGb,MAAM,GAAG,MAAM;iBA1NP,MAAM,KAAG,MAAM;wBA8NhB,MAAM;EAI1B;AAED,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"_axis1d.js","names":[],"sources":["../src/_axis1d.ts"],"sourcesContent":["/**\n * Shared 1D axis primitive: prefix-sum offset table, O(log n) binary search,\n * incremental rebuild, measurement batching via microtask, and dedup guards.\n *\n * Used directly by grid-virtualizer (numeric row/col indices) and composed\n * into virtualizer (which layers VirtualKey translation on top).\n */\n\n/** A single rendered virtual item — position and size on one axis. */\nexport interface VirtualItem {\n end: number;\n index: number;\n size: number;\n start: number;\n}\n\nexport interface Axis1DRange {\n renderEnd: number;\n renderStart: number;\n}\n\n/**\n * Create a self-contained 1D offset-table axis.\n *\n * @param initialCount - Number of items.\n * @param sizeAtFn - Returns the resolved size for a given index (measured or estimated).\n * The virtualizer passes a closure that reads the live measurement cache,\n * so changes to the cache are reflected automatically without re-setting.\n * @param initialGap - Gap between consecutive items (non-negative integer, pixels).\n */\nexport function createAxis1D(initialCount: number, sizeAtFn: (index: number) => number, initialGap: number) {\n let count = initialCount;\n let gap = initialGap;\n const sizeAt = sizeAtFn;\n\n let offsets: number[] = [];\n let totalSize = 0;\n\n // ─── Dedup guards ───────────────────────────────────────────────────────────\n // Track the last-emitted render range and total size so computeVisible() can\n // skip re-rendering when nothing changed.\n\n let prevStart = -1;\n let prevEnd = -1;\n let prevTotalSize = -1;\n\n // ─── Pending measurement flush ─────────────────────────────────────────────\n\n let pendingBuild = false;\n let minChangedIndex = Infinity;\n\n // ─── Offset helpers ─────────────────────────────────────────────────────────\n\n function startAt(i: number): number {\n return offsets[i] ?? 0;\n }\n\n function endAt(i: number): number {\n return startAt(i) + sizeAt(i);\n }\n\n // ─── Full rebuild ───────────────────────────────────────────────────────────\n\n /**\n * Full O(n) prefix-sum rebuild.\n * Pass `forceEmit = true` to reset dedup guards, ensuring computeVisible()\n * emits on the next call even if the rendered range is unchanged.\n */\n function rebuild(forceEmit = false): void {\n if (forceEmit) {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n const next: number[] = [0];\n let pos = 0;\n\n for (let i = 0; i < count; i++) {\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n next.push(pos);\n }\n\n offsets = next;\n totalSize = pos;\n }\n\n // ─── Incremental rebuild ────────────────────────────────────────────────────\n\n /**\n * O(count - fromIndex) incremental rebuild — used after measurement.\n * Always resets dedup guards because measurements change layout.\n */\n function rebuildFrom(fromIndex: number): void {\n if (!Number.isFinite(fromIndex) || fromIndex >= count) return;\n\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n\n let pos = offsets[fromIndex] ?? 0;\n\n for (let i = fromIndex; i < count; i++) {\n offsets[i] = pos;\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n }\n\n offsets[count] = pos;\n totalSize = pos;\n }\n\n // ─── Binary search ──────────────────────────────────────────────────────────\n\n function findFirst(viewportStart: number): number {\n let lo = 0;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n\n if (endAt(mid) <= viewportStart) lo = mid + 1;\n else hi = mid;\n }\n\n return lo;\n }\n\n function findLast(viewportEnd: number, from: number): number {\n let lo = from;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n\n if (startAt(mid) < viewportEnd) lo = mid;\n else hi = mid - 1;\n }\n\n return lo;\n }\n\n /**\n * Compute the render range for a given viewport window and overscan values.\n * Returns indices clamped to [0, count - 1].\n */\n function computeRange(\n viewportStart: number,\n viewportEnd: number,\n overscanStart: number,\n overscanEnd: number,\n ): Axis1DRange {\n const first = findFirst(viewportStart);\n const last = findLast(viewportEnd, first);\n\n return {\n renderEnd: Math.min(count - 1, last + overscanEnd),\n renderStart: Math.max(0, first - overscanStart),\n };\n }\n\n // ─── Dedup helpers ──────────────────────────────────────────────────────────\n\n /** Returns `true` when start/end/totalSize are all identical to the last commit. */\n function isDedupSame(start: number, end: number): boolean {\n return start === prevStart && end === prevEnd && totalSize === prevTotalSize;\n }\n\n function commitDedup(start: number, end: number): void {\n prevStart = start;\n prevEnd = end;\n prevTotalSize = totalSize;\n }\n\n function resetDedup(): void {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n // ─── Microtask scheduling ───────────────────────────────────────────────────\n\n /**\n * Schedule an incremental rebuild on the next microtask, then call `onFlush`.\n * Multiple calls before the microtask fires are coalesced into one rebuild.\n * Returns without scheduling if a build is already pending.\n */\n function scheduleBuild(onFlush: () => void): void {\n if (pendingBuild) return;\n\n pendingBuild = true;\n queueMicrotask(() => {\n pendingBuild = false;\n\n const from = minChangedIndex;\n\n minChangedIndex = Infinity;\n rebuildFrom(from);\n onFlush();\n });\n }\n\n /** Mark an item index as having a changed measurement. Drives incremental rebuilds. */\n function markChanged(index: number): void {\n if (index < minChangedIndex) minChangedIndex = index;\n }\n\n // ─── Item descriptor ────────────────────────────────────────────────────────\n\n /** Build a VirtualItem for a given index using current offset table. */\n function itemAt(index: number): VirtualItem {\n const start = startAt(index);\n const size = sizeAt(index);\n\n return { end: start + size, index, size, start };\n }\n\n // ─── Returned object ────────────────────────────────────────────────────────\n\n return {\n commitDedup,\n computeRange,\n /**\n * Atomically read and reset minChangedIndex to Infinity.\n * Used by grid-virtualizer's coordinated flush so it can consume the\n * changed-index without going through axis1d's own scheduleBuild.\n */\n consumeMinChangedIndex(): number {\n const idx = minChangedIndex;\n\n minChangedIndex = Infinity;\n\n return idx;\n },\n get count(): number {\n return count;\n },\n endAt,\n findFirst,\n findLast,\n /** Current gap between items (pixels). Used by callers to dedup gap changes. */\n get gap(): number {\n return gap;\n },\n isDedupSame,\n itemAt,\n markChanged,\n get minChangedIndex(): number {\n return minChangedIndex;\n },\n get pendingBuild(): boolean {\n return pendingBuild;\n },\n get prevEnd(): number {\n return prevEnd;\n },\n get prevStart(): number {\n return prevStart;\n },\n get prevTotalSize(): number {\n return prevTotalSize;\n },\n rebuild,\n rebuildFrom,\n resetDedup,\n scheduleBuild,\n setCount(n: number): void {\n count = n;\n },\n setGap(n: number): void {\n gap = n;\n },\n sizeAt(i: number): number {\n return sizeAt(i);\n },\n startAt,\n get totalSize(): number {\n return totalSize;\n },\n };\n}\n\nexport type Axis1D = ReturnType<typeof createAxis1D>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"_axis1d.js","names":[],"sources":["../src/_axis1d.ts"],"sourcesContent":["/**\n * Shared 1D axis primitive: prefix-sum offset table, O(log n) binary search,\n * incremental rebuild, measurement batching via microtask, and dedup guards.\n *\n * Used directly by grid-virtualizer (numeric row/col indices) and composed\n * into virtualizer (which layers VirtualKey translation on top).\n */\n\n/** A single rendered virtual item: position and size on one axis. */\nexport interface VirtualItem {\n end: number;\n index: number;\n size: number;\n start: number;\n}\n\nexport interface Axis1DRange {\n renderEnd: number;\n renderStart: number;\n}\n\n/**\n * Create a self-contained 1D offset-table axis.\n *\n * @param initialCount - Number of items.\n * @param sizeAtFn - Returns the resolved size for a given index (measured or estimated).\n * The virtualizer passes a closure that reads the live measurement cache,\n * so changes to the cache are reflected automatically without re-setting.\n * @param initialGap - Gap between consecutive items (non-negative integer, pixels).\n */\nexport function createAxis1D(initialCount: number, sizeAtFn: (index: number) => number, initialGap: number) {\n let count = initialCount;\n let gap = initialGap;\n const sizeAt = sizeAtFn;\n\n let offsets: number[] = [];\n let totalSize = 0;\n\n // ─── Dedup guards ───────────────────────────────────────────────────────────\n // Track the last-emitted render range and total size so computeVisible() can\n // skip re-rendering when nothing changed.\n\n let prevStart = -1;\n let prevEnd = -1;\n let prevTotalSize = -1;\n\n // ─── Pending measurement flush ─────────────────────────────────────────────\n\n let pendingBuild = false;\n let minChangedIndex = Infinity;\n\n // ─── Offset helpers ─────────────────────────────────────────────────────────\n\n function startAt(i: number): number {\n return offsets[i] ?? 0;\n }\n\n function endAt(i: number): number {\n return startAt(i) + sizeAt(i);\n }\n\n // ─── Full rebuild ───────────────────────────────────────────────────────────\n\n /**\n * Full O(n) prefix-sum rebuild.\n * Pass `forceEmit = true` to reset dedup guards, ensuring computeVisible()\n * emits on the next call even if the rendered range is unchanged.\n */\n function rebuild(forceEmit = false): void {\n if (forceEmit) {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n const next: number[] = [0];\n let pos = 0;\n\n for (let i = 0; i < count; i++) {\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n next.push(pos);\n }\n\n offsets = next;\n totalSize = pos;\n }\n\n // ─── Incremental rebuild ────────────────────────────────────────────────────\n\n /**\n * O(count - fromIndex) incremental rebuild: used after measurement.\n * Always resets dedup guards because measurements change layout.\n */\n function rebuildFrom(fromIndex: number): void {\n if (!Number.isFinite(fromIndex) || fromIndex >= count) return;\n\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n\n let pos = offsets[fromIndex] ?? 0;\n\n for (let i = fromIndex; i < count; i++) {\n offsets[i] = pos;\n pos += sizeAt(i) + (i < count - 1 ? gap : 0);\n }\n\n offsets[count] = pos;\n totalSize = pos;\n }\n\n // ─── Binary search ──────────────────────────────────────────────────────────\n\n function findFirst(viewportStart: number): number {\n let lo = 0;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n\n if (endAt(mid) <= viewportStart) lo = mid + 1;\n else hi = mid;\n }\n\n return lo;\n }\n\n function findLast(viewportEnd: number, from: number): number {\n let lo = from;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n\n if (startAt(mid) < viewportEnd) lo = mid;\n else hi = mid - 1;\n }\n\n return lo;\n }\n\n /**\n * Compute the render range for a given viewport window and overscan values.\n * Returns indices clamped to [0, count - 1].\n */\n function computeRange(\n viewportStart: number,\n viewportEnd: number,\n overscanStart: number,\n overscanEnd: number,\n ): Axis1DRange {\n const first = findFirst(viewportStart);\n const last = findLast(viewportEnd, first);\n\n return {\n renderEnd: Math.min(count - 1, last + overscanEnd),\n renderStart: Math.max(0, first - overscanStart),\n };\n }\n\n // ─── Dedup helpers ──────────────────────────────────────────────────────────\n\n /** Returns `true` when start/end/totalSize are all identical to the last commit. */\n function isDedupSame(start: number, end: number): boolean {\n return start === prevStart && end === prevEnd && totalSize === prevTotalSize;\n }\n\n function commitDedup(start: number, end: number): void {\n prevStart = start;\n prevEnd = end;\n prevTotalSize = totalSize;\n }\n\n function resetDedup(): void {\n prevStart = -1;\n prevEnd = -1;\n prevTotalSize = -1;\n }\n\n // ─── Microtask scheduling ───────────────────────────────────────────────────\n\n /**\n * Schedule an incremental rebuild on the next microtask, then call `onFlush`.\n * Multiple calls before the microtask fires are coalesced into one rebuild.\n * Returns without scheduling if a build is already pending.\n */\n function scheduleBuild(onFlush: () => void): void {\n if (pendingBuild) return;\n\n pendingBuild = true;\n queueMicrotask(() => {\n pendingBuild = false;\n\n const from = minChangedIndex;\n\n minChangedIndex = Infinity;\n rebuildFrom(from);\n onFlush();\n });\n }\n\n /** Mark an item index as having a changed measurement. Drives incremental rebuilds. */\n function markChanged(index: number): void {\n if (index < minChangedIndex) minChangedIndex = index;\n }\n\n // ─── Item descriptor ────────────────────────────────────────────────────────\n\n /** Build a VirtualItem for a given index using current offset table. */\n function itemAt(index: number): VirtualItem {\n const start = startAt(index);\n const size = sizeAt(index);\n\n return { end: start + size, index, size, start };\n }\n\n // ─── Returned object ────────────────────────────────────────────────────────\n\n return {\n commitDedup,\n computeRange,\n /**\n * Atomically read and reset minChangedIndex to Infinity.\n * Used by grid-virtualizer's coordinated flush so it can consume the\n * changed-index without going through axis1d's own scheduleBuild.\n */\n consumeMinChangedIndex(): number {\n const idx = minChangedIndex;\n\n minChangedIndex = Infinity;\n\n return idx;\n },\n get count(): number {\n return count;\n },\n endAt,\n findFirst,\n findLast,\n /** Current gap between items (pixels). Used by callers to dedup gap changes. */\n get gap(): number {\n return gap;\n },\n isDedupSame,\n itemAt,\n markChanged,\n get minChangedIndex(): number {\n return minChangedIndex;\n },\n get pendingBuild(): boolean {\n return pendingBuild;\n },\n get prevEnd(): number {\n return prevEnd;\n },\n get prevStart(): number {\n return prevStart;\n },\n get prevTotalSize(): number {\n return prevTotalSize;\n },\n rebuild,\n rebuildFrom,\n resetDedup,\n scheduleBuild,\n setCount(n: number): void {\n count = n;\n },\n setGap(n: number): void {\n gap = n;\n },\n sizeAt(i: number): number {\n return sizeAt(i);\n },\n startAt,\n get totalSize(): number {\n return totalSize;\n },\n };\n}\n\nexport type Axis1D = ReturnType<typeof createAxis1D>;\n"],"mappings":"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"}
package/dist/_dev.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"_dev.cjs","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SCROLL_PROD__?: boolean }).__SCROLL_PROD__;\n\n/** @internal @security Messages may include user-supplied data. */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/scroll] ${msg}`);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":"AAGA,SAAgB,EAAK,EAAmB,CAExC"}
1
+ {"version":3,"file":"_dev.cjs","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SCROLL_PROD__?: boolean }).__SCROLL_PROD__;\n\n/** @internal @security Messages may include user-supplied data. */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/scroll] ${msg}`);\n}\n\n/** @internal: Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":"AAGA,SAAgB,EAAK,EAAmB,CAExC"}
package/dist/_dev.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"_dev.js","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SCROLL_PROD__?: boolean }).__SCROLL_PROD__;\n\n/** @internal @security Messages may include user-supplied data. */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/scroll] ${msg}`);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":"AAGA,SAAgB,EAAK,EAAmB,CAExC"}
1
+ {"version":3,"file":"_dev.js","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __SCROLL_PROD__?: boolean }).__SCROLL_PROD__;\n\n/** @internal @security Messages may include user-supplied data. */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/scroll] ${msg}`);\n}\n\n/** @internal: Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":"AAGA,SAAgB,EAAK,EAAmB,CAExC"}
@@ -1 +1 @@
1
- {"version":3,"file":"_utils.cjs","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { warn } from './_dev';\n\nexport type ScrollTarget = HTMLElement | Window;\n\nexport type VirtualKey = number | string;\n\n/** Shared measurement cache. Pass the same instance to multiple virtualizers\n * for scroll restoration or SSR pre-measurement. */\nexport type MeasurementCache = Map<VirtualKey, number>;\n\n/** Create a new, empty measurement cache. */\nexport function createMeasurementCache(): MeasurementCache {\n return new Map();\n}\n\nexport type Overscan = number | { end?: number; start?: number };\n\nexport const DEFAULT_ESTIMATE_SIZE = 36;\nexport const DEFAULT_OVERSCAN = 3;\n\nexport function createSnapshotStore<T>(initialSnapshot: T): {\n dispose: () => void;\n getSnapshot: () => T;\n publish: (snapshot: T) => void;\n subscribe: (listener: () => void) => () => void;\n} {\n const listeners = new Set<() => void>();\n let snapshot = initialSnapshot;\n let disposed = false;\n\n return {\n dispose() {\n disposed = true;\n listeners.clear();\n },\n getSnapshot() {\n return snapshot;\n },\n publish(nextSnapshot) {\n if (disposed) return;\n snapshot = nextSnapshot;\n\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n },\n subscribe(listener) {\n if (disposed) return () => {};\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\n// ─── Numeric helpers ───────────────────────────────────────────────────────────\n\nexport function toNonNegativeInt(value: number, fallback = 0): number {\n if (!Number.isFinite(value)) return fallback;\n\n return Math.max(0, Math.floor(value));\n}\n\nexport function toPositiveNumber(value: number, fallback: number): number {\n if (!Number.isFinite(value) || value <= 0 || value > 1e7) return fallback;\n\n return value;\n}\n\nexport function isWindowTarget(target: ScrollTarget): target is Window {\n return (\n (typeof Window !== 'undefined' && target instanceof Window) ||\n (typeof (target as Window).innerHeight === 'number' && typeof (target as Window).document === 'object')\n );\n}\n\nexport function normalizeOverscan(overscan: Overscan | undefined, defaultVal: number): { end: number; start: number } {\n if (typeof overscan === 'number') {\n const n = toNonNegativeInt(overscan, defaultVal);\n\n return { end: n, start: n };\n }\n\n return {\n end: toNonNegativeInt(overscan?.end ?? defaultVal),\n start: toNonNegativeInt(overscan?.start ?? defaultVal),\n };\n}\n\n/**\n * Create a `ResizeObserver` that reports size changes on `el` via `onSize`, tied to\n * `signal` so it is disconnected automatically when the owning factory disposes —\n * even if the caller never calls the returned disconnect function.\n */\nexport function observeResize(\n signal: AbortSignal,\n el: HTMLElement,\n onSize: (entry: ResizeObserverEntry) => void,\n): () => void {\n const ro = new ResizeObserver((entries) => {\n for (const entry of entries) onSize(entry);\n });\n\n ro.observe(el);\n signal.addEventListener('abort', () => ro.disconnect(), { once: true });\n\n return () => ro.disconnect();\n}\n\nexport function resolveEstimateFn(\n estimate: number | ((index: number) => number) | undefined,\n defaultSize: number,\n): (index: number) => number {\n if (typeof estimate === 'function') {\n return (index: number) => {\n try {\n return toPositiveNumber(estimate(index), defaultSize);\n } catch (err) {\n warn(`estimateSize threw for index ${index}: ${err}`);\n\n return defaultSize;\n }\n };\n }\n\n const size = typeof estimate === 'number' ? toPositiveNumber(estimate, defaultSize) : defaultSize;\n\n return () => size;\n}\n"],"mappings":"sBAWA,SAAgB,GAA2C,CACzD,OAAO,IAAI,GACb,CAIA,IAAa,EAAwB,GACxB,EAAmB,EAEhC,SAAgB,EAAuB,EAKrC,CACA,IAAM,EAAY,IAAI,IAClB,EAAW,EACX,EAAW,GAEf,MAAO,CACL,SAAU,CACR,EAAW,GACX,EAAU,MAAM,CAClB,EACA,aAAc,CACZ,OAAO,CACT,EACA,QAAQ,EAAc,CAChB,MACJ,GAAW,EAEX,IAAK,IAAM,IAAY,CAAC,GAAG,CAAS,EAClC,GAAI,CACF,EAAS,CACX,OAAS,EAAO,CACd,mBAAqB,CACnB,MAAM,CACR,CAAC,CACH,CATS,CAWb,EACA,UAAU,EAAU,CAGlB,OAFI,MAAuB,CAAC,GAC5B,EAAU,IAAI,CAAQ,MACT,EAAU,OAAO,CAAQ,EACxC,CACF,CACF,CAIA,SAAgB,EAAiB,EAAe,EAAW,EAAW,CAGpE,OAFK,OAAO,SAAS,CAAK,EAEnB,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EAFA,CAGtC,CAEA,SAAgB,EAAiB,EAAe,EAA0B,CAGxE,MAFI,CAAC,OAAO,SAAS,CAAK,GAAK,GAAS,GAAK,EAAQ,IAAY,EAE1D,CACT,CAEA,SAAgB,EAAe,EAAwC,CACrE,OACG,OAAO,OAAW,KAAe,aAAkB,QACnD,OAAQ,EAAkB,aAAgB,UAAY,OAAQ,EAAkB,UAAa,QAElG,CAEA,SAAgB,EAAkB,EAAgC,EAAoD,CACpH,GAAI,OAAO,GAAa,SAAU,CAChC,IAAM,EAAI,EAAiB,EAAU,CAAU,EAE/C,MAAO,CAAE,IAAK,EAAG,MAAO,CAAE,CAC5B,CAEA,MAAO,CACL,IAAK,EAAiB,GAAU,KAAO,CAAU,EACjD,MAAO,EAAiB,GAAU,OAAS,CAAU,CACvD,CACF,CAOA,SAAgB,EACd,EACA,EACA,EACY,CACZ,IAAM,EAAK,IAAI,eAAgB,GAAY,CACzC,IAAK,IAAM,KAAS,EAAS,EAAO,CAAK,CAC3C,CAAC,EAKD,OAHA,EAAG,QAAQ,CAAE,EACb,EAAO,iBAAiB,YAAe,EAAG,WAAW,EAAG,CAAE,KAAM,EAAK,CAAC,MAEzD,EAAG,WAAW,CAC7B,CAEA,SAAgB,EACd,EACA,EAC2B,CAC3B,GAAI,OAAO,GAAa,WACtB,MAAQ,IAAkB,CACxB,GAAI,CACF,OAAO,EAAiB,EAAS,CAAK,EAAG,CAAW,CACtD,OAAS,EAAK,CAGZ,MAFK,GAAgC,EAAhC,EAA0C,EAA1C,EAEE,CACT,CACF,EAGF,IAAM,EAAO,OAAO,GAAa,SAAW,EAAiB,EAAU,CAAW,EAAI,EAEtF,UAAa,CACf"}
1
+ {"version":3,"file":"_utils.cjs","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { warn } from './_dev';\n\nexport type ScrollTarget = HTMLElement | Window;\n\nexport type VirtualKey = number | string;\n\n/** Shared measurement cache. Pass the same instance to multiple virtualizers\n * for scroll restoration or SSR pre-measurement. */\nexport type MeasurementCache = Map<VirtualKey, number>;\n\n/** Create a new, empty measurement cache. */\nexport function createMeasurementCache(): MeasurementCache {\n return new Map();\n}\n\nexport type Overscan = number | { end?: number; start?: number };\n\nexport const DEFAULT_ESTIMATE_SIZE = 36;\nexport const DEFAULT_OVERSCAN = 3;\n\nexport function createSnapshotStore<T>(initialSnapshot: T): {\n dispose: () => void;\n getSnapshot: () => T;\n publish: (snapshot: T) => void;\n subscribe: (listener: () => void) => () => void;\n} {\n const listeners = new Set<() => void>();\n let snapshot = initialSnapshot;\n let disposed = false;\n\n return {\n dispose() {\n disposed = true;\n listeners.clear();\n },\n getSnapshot() {\n return snapshot;\n },\n publish(nextSnapshot) {\n if (disposed) return;\n snapshot = nextSnapshot;\n\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n },\n subscribe(listener) {\n if (disposed) return () => {};\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\n// ─── Numeric helpers ───────────────────────────────────────────────────────────\n\nexport function toNonNegativeInt(value: number, fallback = 0): number {\n if (!Number.isFinite(value)) return fallback;\n\n return Math.max(0, Math.floor(value));\n}\n\nexport function toPositiveNumber(value: number, fallback: number): number {\n if (!Number.isFinite(value) || value <= 0 || value > 1e7) return fallback;\n\n return value;\n}\n\nexport function isWindowTarget(target: ScrollTarget): target is Window {\n return (\n (typeof Window !== 'undefined' && target instanceof Window) ||\n (typeof (target as Window).innerHeight === 'number' && typeof (target as Window).document === 'object')\n );\n}\n\nexport function normalizeOverscan(overscan: Overscan | undefined, defaultVal: number): { end: number; start: number } {\n if (typeof overscan === 'number') {\n const n = toNonNegativeInt(overscan, defaultVal);\n\n return { end: n, start: n };\n }\n\n return {\n end: toNonNegativeInt(overscan?.end ?? defaultVal),\n start: toNonNegativeInt(overscan?.start ?? defaultVal),\n };\n}\n\n/**\n * Create a `ResizeObserver` that reports size changes on `el` via `onSize`, tied to\n * `signal` so it is disconnected automatically when the owning factory disposes :\n * even if the caller never calls the returned disconnect function.\n */\nexport function observeResize(\n signal: AbortSignal,\n el: HTMLElement,\n onSize: (entry: ResizeObserverEntry) => void,\n): () => void {\n const ro = new ResizeObserver((entries) => {\n for (const entry of entries) onSize(entry);\n });\n\n ro.observe(el);\n signal.addEventListener('abort', () => ro.disconnect(), { once: true });\n\n return () => ro.disconnect();\n}\n\nexport function resolveEstimateFn(\n estimate: number | ((index: number) => number) | undefined,\n defaultSize: number,\n): (index: number) => number {\n if (typeof estimate === 'function') {\n return (index: number) => {\n try {\n return toPositiveNumber(estimate(index), defaultSize);\n } catch (err) {\n warn(`estimateSize threw for index ${index}: ${err}`);\n\n return defaultSize;\n }\n };\n }\n\n const size = typeof estimate === 'number' ? toPositiveNumber(estimate, defaultSize) : defaultSize;\n\n return () => size;\n}\n"],"mappings":"sBAWA,SAAgB,GAA2C,CACzD,OAAO,IAAI,GACb,CAIA,IAAa,EAAwB,GACxB,EAAmB,EAEhC,SAAgB,EAAuB,EAKrC,CACA,IAAM,EAAY,IAAI,IAClB,EAAW,EACX,EAAW,GAEf,MAAO,CACL,SAAU,CACR,EAAW,GACX,EAAU,MAAM,CAClB,EACA,aAAc,CACZ,OAAO,CACT,EACA,QAAQ,EAAc,CAChB,MACJ,GAAW,EAEX,IAAK,IAAM,IAAY,CAAC,GAAG,CAAS,EAClC,GAAI,CACF,EAAS,CACX,OAAS,EAAO,CACd,mBAAqB,CACnB,MAAM,CACR,CAAC,CACH,CATS,CAWb,EACA,UAAU,EAAU,CAGlB,OAFI,MAAuB,CAAC,GAC5B,EAAU,IAAI,CAAQ,MACT,EAAU,OAAO,CAAQ,EACxC,CACF,CACF,CAIA,SAAgB,EAAiB,EAAe,EAAW,EAAW,CAGpE,OAFK,OAAO,SAAS,CAAK,EAEnB,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EAFA,CAGtC,CAEA,SAAgB,EAAiB,EAAe,EAA0B,CAGxE,MAFI,CAAC,OAAO,SAAS,CAAK,GAAK,GAAS,GAAK,EAAQ,IAAY,EAE1D,CACT,CAEA,SAAgB,EAAe,EAAwC,CACrE,OACG,OAAO,OAAW,KAAe,aAAkB,QACnD,OAAQ,EAAkB,aAAgB,UAAY,OAAQ,EAAkB,UAAa,QAElG,CAEA,SAAgB,EAAkB,EAAgC,EAAoD,CACpH,GAAI,OAAO,GAAa,SAAU,CAChC,IAAM,EAAI,EAAiB,EAAU,CAAU,EAE/C,MAAO,CAAE,IAAK,EAAG,MAAO,CAAE,CAC5B,CAEA,MAAO,CACL,IAAK,EAAiB,GAAU,KAAO,CAAU,EACjD,MAAO,EAAiB,GAAU,OAAS,CAAU,CACvD,CACF,CAOA,SAAgB,EACd,EACA,EACA,EACY,CACZ,IAAM,EAAK,IAAI,eAAgB,GAAY,CACzC,IAAK,IAAM,KAAS,EAAS,EAAO,CAAK,CAC3C,CAAC,EAKD,OAHA,EAAG,QAAQ,CAAE,EACb,EAAO,iBAAiB,YAAe,EAAG,WAAW,EAAG,CAAE,KAAM,EAAK,CAAC,MAEzD,EAAG,WAAW,CAC7B,CAEA,SAAgB,EACd,EACA,EAC2B,CAC3B,GAAI,OAAO,GAAa,WACtB,MAAQ,IAAkB,CACxB,GAAI,CACF,OAAO,EAAiB,EAAS,CAAK,EAAG,CAAW,CACtD,OAAS,EAAK,CAGZ,MAFK,GAAgC,EAAhC,EAA0C,EAA1C,EAEE,CACT,CACF,EAGF,IAAM,EAAO,OAAO,GAAa,SAAW,EAAiB,EAAU,CAAW,EAAI,EAEtF,UAAa,CACf"}
package/dist/_utils.d.ts CHANGED
@@ -26,7 +26,7 @@ export declare function normalizeOverscan(overscan: Overscan | undefined, defaul
26
26
  };
27
27
  /**
28
28
  * Create a `ResizeObserver` that reports size changes on `el` via `onSize`, tied to
29
- * `signal` so it is disconnected automatically when the owning factory disposes —
29
+ * `signal` so it is disconnected automatically when the owning factory disposes :
30
30
  * even if the caller never calls the returned disconnect function.
31
31
  */
32
32
  export declare function observeResize(signal: AbortSignal, el: HTMLElement, onSize: (entry: ResizeObserverEntry) => void): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"_utils.js","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { warn } from './_dev';\n\nexport type ScrollTarget = HTMLElement | Window;\n\nexport type VirtualKey = number | string;\n\n/** Shared measurement cache. Pass the same instance to multiple virtualizers\n * for scroll restoration or SSR pre-measurement. */\nexport type MeasurementCache = Map<VirtualKey, number>;\n\n/** Create a new, empty measurement cache. */\nexport function createMeasurementCache(): MeasurementCache {\n return new Map();\n}\n\nexport type Overscan = number | { end?: number; start?: number };\n\nexport const DEFAULT_ESTIMATE_SIZE = 36;\nexport const DEFAULT_OVERSCAN = 3;\n\nexport function createSnapshotStore<T>(initialSnapshot: T): {\n dispose: () => void;\n getSnapshot: () => T;\n publish: (snapshot: T) => void;\n subscribe: (listener: () => void) => () => void;\n} {\n const listeners = new Set<() => void>();\n let snapshot = initialSnapshot;\n let disposed = false;\n\n return {\n dispose() {\n disposed = true;\n listeners.clear();\n },\n getSnapshot() {\n return snapshot;\n },\n publish(nextSnapshot) {\n if (disposed) return;\n snapshot = nextSnapshot;\n\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n },\n subscribe(listener) {\n if (disposed) return () => {};\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\n// ─── Numeric helpers ───────────────────────────────────────────────────────────\n\nexport function toNonNegativeInt(value: number, fallback = 0): number {\n if (!Number.isFinite(value)) return fallback;\n\n return Math.max(0, Math.floor(value));\n}\n\nexport function toPositiveNumber(value: number, fallback: number): number {\n if (!Number.isFinite(value) || value <= 0 || value > 1e7) return fallback;\n\n return value;\n}\n\nexport function isWindowTarget(target: ScrollTarget): target is Window {\n return (\n (typeof Window !== 'undefined' && target instanceof Window) ||\n (typeof (target as Window).innerHeight === 'number' && typeof (target as Window).document === 'object')\n );\n}\n\nexport function normalizeOverscan(overscan: Overscan | undefined, defaultVal: number): { end: number; start: number } {\n if (typeof overscan === 'number') {\n const n = toNonNegativeInt(overscan, defaultVal);\n\n return { end: n, start: n };\n }\n\n return {\n end: toNonNegativeInt(overscan?.end ?? defaultVal),\n start: toNonNegativeInt(overscan?.start ?? defaultVal),\n };\n}\n\n/**\n * Create a `ResizeObserver` that reports size changes on `el` via `onSize`, tied to\n * `signal` so it is disconnected automatically when the owning factory disposes —\n * even if the caller never calls the returned disconnect function.\n */\nexport function observeResize(\n signal: AbortSignal,\n el: HTMLElement,\n onSize: (entry: ResizeObserverEntry) => void,\n): () => void {\n const ro = new ResizeObserver((entries) => {\n for (const entry of entries) onSize(entry);\n });\n\n ro.observe(el);\n signal.addEventListener('abort', () => ro.disconnect(), { once: true });\n\n return () => ro.disconnect();\n}\n\nexport function resolveEstimateFn(\n estimate: number | ((index: number) => number) | undefined,\n defaultSize: number,\n): (index: number) => number {\n if (typeof estimate === 'function') {\n return (index: number) => {\n try {\n return toPositiveNumber(estimate(index), defaultSize);\n } catch (err) {\n warn(`estimateSize threw for index ${index}: ${err}`);\n\n return defaultSize;\n }\n };\n }\n\n const size = typeof estimate === 'number' ? toPositiveNumber(estimate, defaultSize) : defaultSize;\n\n return () => size;\n}\n"],"mappings":"kBAWA,SAAgB,GAA2C,CACzD,OAAO,IAAI,GACb,CAIA,IAAa,EAAwB,GACxB,EAAmB,EAEhC,SAAgB,EAAuB,EAKrC,CACA,IAAM,EAAY,IAAI,IAClB,EAAW,EACX,EAAW,GAEf,MAAO,CACL,SAAU,CACR,EAAW,GACX,EAAU,MAAM,CAClB,EACA,aAAc,CACZ,OAAO,CACT,EACA,QAAQ,EAAc,CAChB,MACJ,GAAW,EAEX,IAAK,IAAM,IAAY,CAAC,GAAG,CAAS,EAClC,GAAI,CACF,EAAS,CACX,OAAS,EAAO,CACd,mBAAqB,CACnB,MAAM,CACR,CAAC,CACH,CATS,CAWb,EACA,UAAU,EAAU,CAGlB,OAFI,MAAuB,CAAC,GAC5B,EAAU,IAAI,CAAQ,MACT,EAAU,OAAO,CAAQ,EACxC,CACF,CACF,CAIA,SAAgB,EAAiB,EAAe,EAAW,EAAW,CAGpE,OAFK,OAAO,SAAS,CAAK,EAEnB,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EAFA,CAGtC,CAEA,SAAgB,EAAiB,EAAe,EAA0B,CAGxE,MAFI,CAAC,OAAO,SAAS,CAAK,GAAK,GAAS,GAAK,EAAQ,IAAY,EAE1D,CACT,CAEA,SAAgB,EAAe,EAAwC,CACrE,OACG,OAAO,OAAW,KAAe,aAAkB,QACnD,OAAQ,EAAkB,aAAgB,UAAY,OAAQ,EAAkB,UAAa,QAElG,CAEA,SAAgB,EAAkB,EAAgC,EAAoD,CACpH,GAAI,OAAO,GAAa,SAAU,CAChC,IAAM,EAAI,EAAiB,EAAU,CAAU,EAE/C,MAAO,CAAE,IAAK,EAAG,MAAO,CAAE,CAC5B,CAEA,MAAO,CACL,IAAK,EAAiB,GAAU,KAAO,CAAU,EACjD,MAAO,EAAiB,GAAU,OAAS,CAAU,CACvD,CACF,CAOA,SAAgB,EACd,EACA,EACA,EACY,CACZ,IAAM,EAAK,IAAI,eAAgB,GAAY,CACzC,IAAK,IAAM,KAAS,EAAS,EAAO,CAAK,CAC3C,CAAC,EAKD,OAHA,EAAG,QAAQ,CAAE,EACb,EAAO,iBAAiB,YAAe,EAAG,WAAW,EAAG,CAAE,KAAM,EAAK,CAAC,MAEzD,EAAG,WAAW,CAC7B,CAEA,SAAgB,EACd,EACA,EAC2B,CAC3B,GAAI,OAAO,GAAa,WACtB,MAAQ,IAAkB,CACxB,GAAI,CACF,OAAO,EAAiB,EAAS,CAAK,EAAG,CAAW,CACtD,OAAS,EAAK,CAGZ,MAFK,GAAgC,EAAhC,EAA0C,EAA1C,EAEE,CACT,CACF,EAGF,IAAM,EAAO,OAAO,GAAa,SAAW,EAAiB,EAAU,CAAW,EAAI,EAEtF,UAAa,CACf"}
1
+ {"version":3,"file":"_utils.js","names":[],"sources":["../src/_utils.ts"],"sourcesContent":["import { warn } from './_dev';\n\nexport type ScrollTarget = HTMLElement | Window;\n\nexport type VirtualKey = number | string;\n\n/** Shared measurement cache. Pass the same instance to multiple virtualizers\n * for scroll restoration or SSR pre-measurement. */\nexport type MeasurementCache = Map<VirtualKey, number>;\n\n/** Create a new, empty measurement cache. */\nexport function createMeasurementCache(): MeasurementCache {\n return new Map();\n}\n\nexport type Overscan = number | { end?: number; start?: number };\n\nexport const DEFAULT_ESTIMATE_SIZE = 36;\nexport const DEFAULT_OVERSCAN = 3;\n\nexport function createSnapshotStore<T>(initialSnapshot: T): {\n dispose: () => void;\n getSnapshot: () => T;\n publish: (snapshot: T) => void;\n subscribe: (listener: () => void) => () => void;\n} {\n const listeners = new Set<() => void>();\n let snapshot = initialSnapshot;\n let disposed = false;\n\n return {\n dispose() {\n disposed = true;\n listeners.clear();\n },\n getSnapshot() {\n return snapshot;\n },\n publish(nextSnapshot) {\n if (disposed) return;\n snapshot = nextSnapshot;\n\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n },\n subscribe(listener) {\n if (disposed) return () => {};\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\n// ─── Numeric helpers ───────────────────────────────────────────────────────────\n\nexport function toNonNegativeInt(value: number, fallback = 0): number {\n if (!Number.isFinite(value)) return fallback;\n\n return Math.max(0, Math.floor(value));\n}\n\nexport function toPositiveNumber(value: number, fallback: number): number {\n if (!Number.isFinite(value) || value <= 0 || value > 1e7) return fallback;\n\n return value;\n}\n\nexport function isWindowTarget(target: ScrollTarget): target is Window {\n return (\n (typeof Window !== 'undefined' && target instanceof Window) ||\n (typeof (target as Window).innerHeight === 'number' && typeof (target as Window).document === 'object')\n );\n}\n\nexport function normalizeOverscan(overscan: Overscan | undefined, defaultVal: number): { end: number; start: number } {\n if (typeof overscan === 'number') {\n const n = toNonNegativeInt(overscan, defaultVal);\n\n return { end: n, start: n };\n }\n\n return {\n end: toNonNegativeInt(overscan?.end ?? defaultVal),\n start: toNonNegativeInt(overscan?.start ?? defaultVal),\n };\n}\n\n/**\n * Create a `ResizeObserver` that reports size changes on `el` via `onSize`, tied to\n * `signal` so it is disconnected automatically when the owning factory disposes :\n * even if the caller never calls the returned disconnect function.\n */\nexport function observeResize(\n signal: AbortSignal,\n el: HTMLElement,\n onSize: (entry: ResizeObserverEntry) => void,\n): () => void {\n const ro = new ResizeObserver((entries) => {\n for (const entry of entries) onSize(entry);\n });\n\n ro.observe(el);\n signal.addEventListener('abort', () => ro.disconnect(), { once: true });\n\n return () => ro.disconnect();\n}\n\nexport function resolveEstimateFn(\n estimate: number | ((index: number) => number) | undefined,\n defaultSize: number,\n): (index: number) => number {\n if (typeof estimate === 'function') {\n return (index: number) => {\n try {\n return toPositiveNumber(estimate(index), defaultSize);\n } catch (err) {\n warn(`estimateSize threw for index ${index}: ${err}`);\n\n return defaultSize;\n }\n };\n }\n\n const size = typeof estimate === 'number' ? toPositiveNumber(estimate, defaultSize) : defaultSize;\n\n return () => size;\n}\n"],"mappings":"kBAWA,SAAgB,GAA2C,CACzD,OAAO,IAAI,GACb,CAIA,IAAa,EAAwB,GACxB,EAAmB,EAEhC,SAAgB,EAAuB,EAKrC,CACA,IAAM,EAAY,IAAI,IAClB,EAAW,EACX,EAAW,GAEf,MAAO,CACL,SAAU,CACR,EAAW,GACX,EAAU,MAAM,CAClB,EACA,aAAc,CACZ,OAAO,CACT,EACA,QAAQ,EAAc,CAChB,MACJ,GAAW,EAEX,IAAK,IAAM,IAAY,CAAC,GAAG,CAAS,EAClC,GAAI,CACF,EAAS,CACX,OAAS,EAAO,CACd,mBAAqB,CACnB,MAAM,CACR,CAAC,CACH,CATS,CAWb,EACA,UAAU,EAAU,CAGlB,OAFI,MAAuB,CAAC,GAC5B,EAAU,IAAI,CAAQ,MACT,EAAU,OAAO,CAAQ,EACxC,CACF,CACF,CAIA,SAAgB,EAAiB,EAAe,EAAW,EAAW,CAGpE,OAFK,OAAO,SAAS,CAAK,EAEnB,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EAFA,CAGtC,CAEA,SAAgB,EAAiB,EAAe,EAA0B,CAGxE,MAFI,CAAC,OAAO,SAAS,CAAK,GAAK,GAAS,GAAK,EAAQ,IAAY,EAE1D,CACT,CAEA,SAAgB,EAAe,EAAwC,CACrE,OACG,OAAO,OAAW,KAAe,aAAkB,QACnD,OAAQ,EAAkB,aAAgB,UAAY,OAAQ,EAAkB,UAAa,QAElG,CAEA,SAAgB,EAAkB,EAAgC,EAAoD,CACpH,GAAI,OAAO,GAAa,SAAU,CAChC,IAAM,EAAI,EAAiB,EAAU,CAAU,EAE/C,MAAO,CAAE,IAAK,EAAG,MAAO,CAAE,CAC5B,CAEA,MAAO,CACL,IAAK,EAAiB,GAAU,KAAO,CAAU,EACjD,MAAO,EAAiB,GAAU,OAAS,CAAU,CACvD,CACF,CAOA,SAAgB,EACd,EACA,EACA,EACY,CACZ,IAAM,EAAK,IAAI,eAAgB,GAAY,CACzC,IAAK,IAAM,KAAS,EAAS,EAAO,CAAK,CAC3C,CAAC,EAKD,OAHA,EAAG,QAAQ,CAAE,EACb,EAAO,iBAAiB,YAAe,EAAG,WAAW,EAAG,CAAE,KAAM,EAAK,CAAC,MAEzD,EAAG,WAAW,CAC7B,CAEA,SAAgB,EACd,EACA,EAC2B,CAC3B,GAAI,OAAO,GAAa,WACtB,MAAQ,IAAkB,CACxB,GAAI,CACF,OAAO,EAAiB,EAAS,CAAK,EAAG,CAAW,CACtD,OAAS,EAAK,CAGZ,MAFK,GAAgC,EAAhC,EAA0C,EAA1C,EAEE,CACT,CACF,EAGF,IAAM,EAAO,OAAO,GAAa,SAAW,EAAiB,EAAU,CAAW,EAAI,EAEtF,UAAa,CACf"}
@@ -1 +1 @@
1
- {"version":3,"file":"dom-virtual-list.cjs","names":[],"sources":["../src/dom-virtual-list.ts"],"sourcesContent":["import {\n createSnapshotStore,\n DEFAULT_ESTIMATE_SIZE,\n DEFAULT_OVERSCAN,\n type MeasurementCache,\n type Overscan,\n} from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigError, ScrollRangeError } from './errors';\nimport {\n createVirtualizer,\n type ScrollToIndexOptions,\n type VirtualItem,\n type Virtualizer,\n type VirtualizerState,\n type VirtualKey,\n} from './virtualizer';\n\nexport type {\n MeasurementCache,\n Overscan,\n ScrollToIndexOptions,\n VirtualItem,\n Virtualizer,\n VirtualizerState,\n VirtualKey,\n};\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** A `VirtualItem` enriched with the corresponding data record. */\nexport type VirtualRenderItem<T> = VirtualItem & { readonly data: T };\n\n/**\n * Recycle a DOM node by key. If the pool has a live node for `key`, it is\n * returned and reused; otherwise `create()` is called to produce a new one.\n */\nexport type RecycleFn = (key: VirtualKey, create: () => HTMLElement) => HTMLElement;\n\nexport type DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>;\n listEl: HTMLElement;\n recycle: RecycleFn;\n /** Sticky items from the underlying virtualizer, enriched with data. */\n stickyItems: Array<VirtualRenderItem<T>>;\n totalSize: number;\n};\n\nexport type StickToBottomOptions = {\n /** Enable/disable the behavior. Default: `true` once this object is provided. */\n enabled?: boolean;\n /**\n * Distance in pixels from the end still considered \"at the end\" — the bottom edge in\n * vertical mode, the trailing edge in horizontal mode. Default: `48`.\n */\n threshold?: number;\n};\n\nexport type DomVirtualListOptions<T> = {\n /** Custom teardown that clears listEl. Defaults to `listEl.textContent = ''`. */\n clear?: (listEl: HTMLElement) => void;\n estimateSize?: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n horizontal?: boolean;\n listElement: HTMLElement;\n /** External measurement cache for scroll restoration. */\n measurementCache?: MeasurementCache;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n scrollElement: HTMLElement | Window;\n /**\n * Auto-scroll to the end after `setItems()` whenever the list was already at (or near) the\n * end just before the update — the chat \"stick to bottom on new message\" pattern. Fires on\n * *any* update while at the end, not just growth, so it also follows a streaming last\n * item that grows in place without changing `items.length`. Does nothing while the user\n * has scrolled away from the end. Pass `true` for defaults, or an options object.\n */\n stickToBottom?: boolean | StickToBottomOptions;\n /** Mark items as sticky headers. Receives the item index and the item data. */\n sticky?: (index: number, item: T) => boolean;\n};\n\n/**\n * R11: Controller extends Virtualizer so all methods (scrollToIndex, refresh,\n * scrollToOffset, etc.) are accessible directly on the controller without\n * needing to unwrap an inner virtualizer handle.\n *\n * `prepend` and `update` are omitted — use `setItems()` for item updates and\n * there is no direct `prepend` concept in DomVirtualList.\n */\nexport type DomVirtualListController<T> = Omit<Virtualizer, 'prepend' | 'update'> & {\n setItems: (items: T[]) => void;\n};\n\nexport type VirtualScrollerOptions<T> = Omit<DomVirtualListOptions<T>, 'listElement' | 'scrollElement'> & {\n /** Additional CSS class names on the generated scroll container. */\n containerClass?: string;\n};\n\n// ─── Node pool ────────────────────────────────────────────────────────────────\n\nfunction createNodePool() {\n let live = new Map<VirtualKey, HTMLElement>();\n let nextLive = new Map<VirtualKey, HTMLElement>();\n const stale: HTMLElement[] = [];\n let inCycle = false;\n\n return {\n acquire(key: VirtualKey, create: () => HTMLElement): HTMLElement {\n if (!inCycle) return create();\n\n const existing = live.get(key);\n\n if (existing) {\n nextLive.set(key, existing);\n\n return existing;\n }\n\n const node = stale.pop() ?? create();\n\n nextLive.set(key, node);\n\n return node;\n },\n\n beginCycle(): void {\n nextLive = new Map();\n inCycle = true;\n },\n\n clear(): void {\n for (const node of live.values()) node.remove();\n\n live.clear();\n nextLive.clear();\n stale.length = 0;\n inCycle = false;\n },\n\n endCycle(): void {\n if (!inCycle) return;\n\n inCycle = false;\n\n for (const [key, node] of live) {\n if (!nextLive.has(key)) {\n node.remove();\n stale.push(node);\n }\n }\n\n live = nextLive;\n },\n };\n}\n\n// ─── Implementation ────────────────────────────────────────────────────────────\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n if (typeof options.estimateSize === 'number') requirePositiveNumber(options.estimateSize, 'estimateSize');\n\n if (options.gap !== undefined) requireNonNegativeInteger(options.gap, 'gap');\n\n if (options.overscan !== undefined) validateOverscan(options.overscan);\n\n if (options.stickToBottom !== undefined && typeof options.stickToBottom !== 'boolean') {\n if (options.stickToBottom === null || Array.isArray(options.stickToBottom)) {\n throw new ScrollConfigError('stickToBottom must be a boolean or options object.');\n }\n\n if (options.stickToBottom.threshold !== undefined) {\n requireNonNegativeNumber(options.stickToBottom.threshold, 'stickToBottom.threshold');\n }\n }\n\n let currentItems: T[] = [];\n let disposed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\n const stateStore = createSnapshotStore<VirtualizerState>({ items: [], stickyItems: [], totalSize: 0 });\n\n // Publish state before invoking the render callback.\n function emitState(state: VirtualizerState): void {\n stateStore.publish(state);\n handleChange(state);\n }\n\n // Pool must be declared before virtualizer since handleChange (passed as onChange)\n // is invoked during createVirtualizer initialization via computeVisible.\n const pool = createNodePool();\n\n // Virtualizer is lazily created on the first non-empty setItems call.\n let virtualizer: Virtualizer | null = null;\n\n function resolveKey(index: number): VirtualKey {\n const item = currentItems[index];\n\n if (item !== undefined && options.getItemKey) return options.getItemKey(index, item);\n\n return index;\n }\n\n function resolveEstimate(index: number): number {\n if (options.estimateSize === undefined) return DEFAULT_ESTIMATE_SIZE;\n\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n const item = currentItems[index];\n\n return item !== undefined ? options.estimateSize(index, item) : DEFAULT_ESTIMATE_SIZE;\n }\n\n function applyListSize(totalSize: number): void {\n if (options.horizontal) {\n listEl.style.height = '';\n listEl.style.width = `${totalSize}px`;\n } else {\n listEl.style.height = `${totalSize}px`;\n listEl.style.width = '';\n }\n }\n\n /**\n * R10: Throw rather than silently produce `undefined as T`.\n * This catches bugs where `vi.index` is out of range for `currentItems`.\n */\n function toRenderItem(vi: VirtualItem): VirtualRenderItem<T> {\n const data = currentItems[vi.index];\n\n if (data === undefined) {\n throw new ScrollRangeError(\n `toRenderItem: index ${vi.index} is out of range (currentItems.length=${currentItems.length})`,\n );\n }\n\n return { ...vi, data };\n }\n\n function handleChange(state: VirtualizerState): void {\n applyListSize(state.totalSize);\n\n pool.beginCycle();\n\n // R5: try/finally ensures endCycle() runs even if render() throws, keeping\n // the pool in a consistent state.\n try {\n options.render({\n items: state.items.map(toRenderItem),\n listEl,\n recycle: (key, create) => pool.acquire(key, create),\n stickyItems: state.stickyItems.map(toRenderItem),\n totalSize: state.totalSize,\n });\n } finally {\n pool.endCycle();\n }\n }\n\n function clearAndReset(): void {\n pool.clear();\n\n if (options.clear) {\n options.clear(listEl);\n } else {\n listEl.textContent = '';\n }\n\n listEl.style.height = '';\n listEl.style.width = '';\n listEl.style.position = '';\n listEl.style.contain = '';\n }\n\n const DEFAULT_STICK_THRESHOLD = 48;\n\n function resolveStickToBottom(): { enabled: boolean; threshold: number } {\n const opt = options.stickToBottom;\n\n if (!opt) return { enabled: false, threshold: DEFAULT_STICK_THRESHOLD };\n\n if (opt === true) return { enabled: true, threshold: DEFAULT_STICK_THRESHOLD };\n\n return { enabled: opt.enabled ?? true, threshold: opt.threshold ?? DEFAULT_STICK_THRESHOLD };\n }\n\n function spawnVirtualizer(): Virtualizer {\n const v = createVirtualizer(options.scrollElement, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveKey,\n horizontal: options.horizontal,\n measurementCache: options.measurementCache,\n onChange: emitState,\n overscan: options.overscan ?? DEFAULT_OVERSCAN,\n sticky: options.sticky\n ? (index) => {\n const item = currentItems[index];\n\n return item !== undefined && options.sticky!(index, item);\n }\n : undefined,\n });\n\n virtualizer = v;\n listEl.style.position = 'relative';\n listEl.style.contain = 'layout';\n\n return v;\n }\n\n function _dispose(): void {\n if (disposed) return;\n\n disposed = true;\n stateStore.dispose();\n ac.abort();\n virtualizer?.dispose();\n virtualizer = null;\n clearAndReset();\n }\n\n return {\n // ── Virtualizer passthrough (R11) ──────────────────────────────────────\n get count() {\n return virtualizer?.count ?? 0;\n },\n\n get disposalSignal() {\n return ac.signal;\n },\n\n dispose: _dispose,\n\n get disposed() {\n return disposed;\n },\n\n getSnapshot: stateStore.getSnapshot,\n\n invalidate() {\n if (disposed) return;\n\n virtualizer?.invalidate();\n },\n\n isAtEnd(threshold) {\n return virtualizer?.isAtEnd(threshold) ?? true;\n },\n\n get isScrolling() {\n return virtualizer?.isScrolling ?? false;\n },\n\n get items() {\n return virtualizer?.items ?? [];\n },\n\n measure(index, size) {\n if (disposed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (disposed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (disposed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (disposed) return;\n\n virtualizer?.refresh();\n },\n\n remeasure() {\n if (disposed) return;\n\n virtualizer?.remeasure();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (disposed) return;\n\n // Read *before* mutating state — the \"was the list already at the end?\" check must\n // reflect the pre-update layout, not the one `render()` is about to produce below.\n const stick = resolveStickToBottom();\n const wasAtEnd = stick.enabled && (virtualizer?.isAtEnd(stick.threshold) ?? true);\n\n currentItems = items;\n\n if (items.length === 0) {\n virtualizer?.dispose();\n virtualizer = null;\n stateStore.publish({ items: [], stickyItems: [], totalSize: 0 });\n clearAndReset();\n\n return;\n }\n\n if (!virtualizer) {\n const v = spawnVirtualizer();\n\n if (wasAtEnd) v.scrollToBottom();\n\n return;\n }\n\n const countChanged = items.length !== virtualizer.count;\n\n // Only count needs explicit update — estimateSize and getItemKey are\n // closures that already reflect the latest currentItems automatically.\n virtualizer.update({ count: items.length });\n\n // When count changed, update() already triggered rebuild + computeVisible().\n // Only force re-emission when count is unchanged (data changed, count same).\n if (!countChanged) {\n // refresh() re-emits with current sizes for stable keys;\n // invalidate() clears position-based measurements when no stable keys.\n if (options.getItemKey) {\n virtualizer.refresh();\n } else {\n virtualizer.invalidate();\n }\n } else if (!options.getItemKey) {\n // Count changed AND no stable keys: position-based measurements are now\n // stale. Clear them so the next render remeasures from fresh estimates.\n virtualizer.invalidate();\n }\n\n if (wasAtEnd) virtualizer.scrollToBottom();\n },\n\n get stickyItems() {\n return virtualizer?.stickyItems ?? [];\n },\n\n subscribe: stateStore.subscribe,\n\n [Symbol.dispose]: _dispose,\n\n get totalSize() {\n return virtualizer?.totalSize ?? 0;\n },\n };\n}\n\n// ─── F5: createVirtualScroller ────────────────────────────────────────────────\n\n/**\n * High-level factory that creates the scroll container and inner list element,\n * appends them to `container`, and returns a fully wired `DomVirtualListController`.\n *\n * @example\n * ```ts\n * const list = createVirtualScroller(document.getElementById('root')!, {\n * render({ items, listEl, recycle }) { … },\n * });\n * list.setItems(data);\n * ```\n */\nexport function createVirtualScroller<T>(\n container: HTMLElement,\n options: VirtualScrollerOptions<T>,\n): DomVirtualListController<T> {\n const scrollEl = document.createElement('div');\n\n scrollEl.style.cssText = options.horizontal\n ? 'overflow: auto hidden; width: 100%; height: 100%;'\n : 'overflow: hidden auto; width: 100%; height: 100%;';\n\n if (options.containerClass) scrollEl.className = options.containerClass;\n\n const listEl = document.createElement('div');\n\n scrollEl.appendChild(listEl);\n container.appendChild(scrollEl);\n\n let ctrl: DomVirtualListController<T>;\n\n try {\n ctrl = createDomVirtualList<T>({\n ...options,\n listElement: listEl,\n scrollElement: scrollEl,\n });\n } catch (e) {\n // Remove the scroll container if construction fails so we don't leak DOM nodes.\n scrollEl.remove();\n throw e;\n }\n\n // Override dispose and [Symbol.dispose] to also remove the scroll container.\n // Capture the original dispose before overwriting so there's no self-reference.\n const innerDispose = ctrl.dispose.bind(ctrl);\n\n return Object.assign(ctrl, {\n dispose() {\n innerDispose();\n scrollEl.remove();\n },\n [Symbol.dispose]() {\n innerDispose();\n scrollEl.remove();\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"dom-virtual-list.cjs","names":[],"sources":["../src/dom-virtual-list.ts"],"sourcesContent":["import {\n createSnapshotStore,\n DEFAULT_ESTIMATE_SIZE,\n DEFAULT_OVERSCAN,\n type MeasurementCache,\n type Overscan,\n} from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigError, ScrollRangeError } from './errors';\nimport {\n createVirtualizer,\n type ScrollToIndexOptions,\n type VirtualItem,\n type Virtualizer,\n type VirtualizerState,\n type VirtualKey,\n} from './virtualizer';\n\nexport type {\n MeasurementCache,\n Overscan,\n ScrollToIndexOptions,\n VirtualItem,\n Virtualizer,\n VirtualizerState,\n VirtualKey,\n};\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** A `VirtualItem` enriched with the corresponding data record. */\nexport type VirtualRenderItem<T> = VirtualItem & { readonly data: T };\n\n/**\n * Recycle a DOM node by key. If the pool has a live node for `key`, it is\n * returned and reused; otherwise `create()` is called to produce a new one.\n */\nexport type RecycleFn = (key: VirtualKey, create: () => HTMLElement) => HTMLElement;\n\nexport type DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>;\n listEl: HTMLElement;\n recycle: RecycleFn;\n /** Sticky items from the underlying virtualizer, enriched with data. */\n stickyItems: Array<VirtualRenderItem<T>>;\n totalSize: number;\n};\n\nexport type StickToBottomOptions = {\n /** Enable/disable the behavior. Default: `true` once this object is provided. */\n enabled?: boolean;\n /**\n * Distance in pixels from the end still considered \"at the end\": the bottom edge in\n * vertical mode, the trailing edge in horizontal mode. Default: `48`.\n */\n threshold?: number;\n};\n\nexport type DomVirtualListOptions<T> = {\n /** Custom teardown that clears listEl. Defaults to `listEl.textContent = ''`. */\n clear?: (listEl: HTMLElement) => void;\n estimateSize?: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n horizontal?: boolean;\n listElement: HTMLElement;\n /** External measurement cache for scroll restoration. */\n measurementCache?: MeasurementCache;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n scrollElement: HTMLElement | Window;\n /**\n * Auto-scroll to the end after `setItems()` whenever the list was already at (or near) the\n * end just before the update: the chat \"stick to bottom on new message\" pattern. Fires on\n * *any* update while at the end, not just growth, so it also follows a streaming last\n * item that grows in place without changing `items.length`. Does nothing while the user\n * has scrolled away from the end. Pass `true` for defaults, or an options object.\n */\n stickToBottom?: boolean | StickToBottomOptions;\n /** Mark items as sticky headers. Receives the item index and the item data. */\n sticky?: (index: number, item: T) => boolean;\n};\n\n/**\n * R11: Controller extends Virtualizer so all methods (scrollToIndex, refresh,\n * scrollToOffset, etc.) are accessible directly on the controller without\n * needing to unwrap an inner virtualizer handle.\n *\n * `prepend` and `update` are omitted: use `setItems()` for item updates and\n * there is no direct `prepend` concept in DomVirtualList.\n */\nexport type DomVirtualListController<T> = Omit<Virtualizer, 'prepend' | 'update'> & {\n setItems: (items: T[]) => void;\n};\n\nexport type VirtualScrollerOptions<T> = Omit<DomVirtualListOptions<T>, 'listElement' | 'scrollElement'> & {\n /** Additional CSS class names on the generated scroll container. */\n containerClass?: string;\n};\n\n// ─── Node pool ────────────────────────────────────────────────────────────────\n\nfunction createNodePool() {\n let live = new Map<VirtualKey, HTMLElement>();\n let nextLive = new Map<VirtualKey, HTMLElement>();\n const stale: HTMLElement[] = [];\n let inCycle = false;\n\n return {\n acquire(key: VirtualKey, create: () => HTMLElement): HTMLElement {\n if (!inCycle) return create();\n\n const existing = live.get(key);\n\n if (existing) {\n nextLive.set(key, existing);\n\n return existing;\n }\n\n const node = stale.pop() ?? create();\n\n nextLive.set(key, node);\n\n return node;\n },\n\n beginCycle(): void {\n nextLive = new Map();\n inCycle = true;\n },\n\n clear(): void {\n for (const node of live.values()) node.remove();\n\n live.clear();\n nextLive.clear();\n stale.length = 0;\n inCycle = false;\n },\n\n endCycle(): void {\n if (!inCycle) return;\n\n inCycle = false;\n\n for (const [key, node] of live) {\n if (!nextLive.has(key)) {\n node.remove();\n stale.push(node);\n }\n }\n\n live = nextLive;\n },\n };\n}\n\n// ─── Implementation ────────────────────────────────────────────────────────────\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n if (typeof options.estimateSize === 'number') requirePositiveNumber(options.estimateSize, 'estimateSize');\n\n if (options.gap !== undefined) requireNonNegativeInteger(options.gap, 'gap');\n\n if (options.overscan !== undefined) validateOverscan(options.overscan);\n\n if (options.stickToBottom !== undefined && typeof options.stickToBottom !== 'boolean') {\n if (options.stickToBottom === null || Array.isArray(options.stickToBottom)) {\n throw new ScrollConfigError('stickToBottom must be a boolean or options object.');\n }\n\n if (options.stickToBottom.threshold !== undefined) {\n requireNonNegativeNumber(options.stickToBottom.threshold, 'stickToBottom.threshold');\n }\n }\n\n let currentItems: T[] = [];\n let disposed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\n const stateStore = createSnapshotStore<VirtualizerState>({ items: [], stickyItems: [], totalSize: 0 });\n\n // Publish state before invoking the render callback.\n function emitState(state: VirtualizerState): void {\n stateStore.publish(state);\n handleChange(state);\n }\n\n // Pool must be declared before virtualizer since handleChange (passed as onChange)\n // is invoked during createVirtualizer initialization via computeVisible.\n const pool = createNodePool();\n\n // Virtualizer is lazily created on the first non-empty setItems call.\n let virtualizer: Virtualizer | null = null;\n\n function resolveKey(index: number): VirtualKey {\n const item = currentItems[index];\n\n if (item !== undefined && options.getItemKey) return options.getItemKey(index, item);\n\n return index;\n }\n\n function resolveEstimate(index: number): number {\n if (options.estimateSize === undefined) return DEFAULT_ESTIMATE_SIZE;\n\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n const item = currentItems[index];\n\n return item !== undefined ? options.estimateSize(index, item) : DEFAULT_ESTIMATE_SIZE;\n }\n\n function applyListSize(totalSize: number): void {\n if (options.horizontal) {\n listEl.style.height = '';\n listEl.style.width = `${totalSize}px`;\n } else {\n listEl.style.height = `${totalSize}px`;\n listEl.style.width = '';\n }\n }\n\n /**\n * R10: Throw rather than silently produce `undefined as T`.\n * This catches bugs where `vi.index` is out of range for `currentItems`.\n */\n function toRenderItem(vi: VirtualItem): VirtualRenderItem<T> {\n const data = currentItems[vi.index];\n\n if (data === undefined) {\n throw new ScrollRangeError(\n `toRenderItem: index ${vi.index} is out of range (currentItems.length=${currentItems.length})`,\n );\n }\n\n return { ...vi, data };\n }\n\n function handleChange(state: VirtualizerState): void {\n applyListSize(state.totalSize);\n\n pool.beginCycle();\n\n // R5: try/finally ensures endCycle() runs even if render() throws, keeping\n // the pool in a consistent state.\n try {\n options.render({\n items: state.items.map(toRenderItem),\n listEl,\n recycle: (key, create) => pool.acquire(key, create),\n stickyItems: state.stickyItems.map(toRenderItem),\n totalSize: state.totalSize,\n });\n } finally {\n pool.endCycle();\n }\n }\n\n function clearAndReset(): void {\n pool.clear();\n\n if (options.clear) {\n options.clear(listEl);\n } else {\n listEl.textContent = '';\n }\n\n listEl.style.height = '';\n listEl.style.width = '';\n listEl.style.position = '';\n listEl.style.contain = '';\n }\n\n const DEFAULT_STICK_THRESHOLD = 48;\n\n function resolveStickToBottom(): { enabled: boolean; threshold: number } {\n const opt = options.stickToBottom;\n\n if (!opt) return { enabled: false, threshold: DEFAULT_STICK_THRESHOLD };\n\n if (opt === true) return { enabled: true, threshold: DEFAULT_STICK_THRESHOLD };\n\n return { enabled: opt.enabled ?? true, threshold: opt.threshold ?? DEFAULT_STICK_THRESHOLD };\n }\n\n function spawnVirtualizer(): Virtualizer {\n const v = createVirtualizer(options.scrollElement, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveKey,\n horizontal: options.horizontal,\n measurementCache: options.measurementCache,\n onChange: emitState,\n overscan: options.overscan ?? DEFAULT_OVERSCAN,\n sticky: options.sticky\n ? (index) => {\n const item = currentItems[index];\n\n return item !== undefined && options.sticky!(index, item);\n }\n : undefined,\n });\n\n virtualizer = v;\n listEl.style.position = 'relative';\n listEl.style.contain = 'layout';\n\n return v;\n }\n\n function _dispose(): void {\n if (disposed) return;\n\n disposed = true;\n stateStore.dispose();\n ac.abort();\n virtualizer?.dispose();\n virtualizer = null;\n clearAndReset();\n }\n\n return {\n // ── Virtualizer passthrough (R11) ──────────────────────────────────────\n get count() {\n return virtualizer?.count ?? 0;\n },\n\n get disposalSignal() {\n return ac.signal;\n },\n\n dispose: _dispose,\n\n get disposed() {\n return disposed;\n },\n\n getSnapshot: stateStore.getSnapshot,\n\n invalidate() {\n if (disposed) return;\n\n virtualizer?.invalidate();\n },\n\n isAtEnd(threshold) {\n return virtualizer?.isAtEnd(threshold) ?? true;\n },\n\n get isScrolling() {\n return virtualizer?.isScrolling ?? false;\n },\n\n get items() {\n return virtualizer?.items ?? [];\n },\n\n measure(index, size) {\n if (disposed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (disposed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (disposed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (disposed) return;\n\n virtualizer?.refresh();\n },\n\n remeasure() {\n if (disposed) return;\n\n virtualizer?.remeasure();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (disposed) return;\n\n // Read *before* mutating state: the \"was the list already at the end?\" check must\n // reflect the pre-update layout, not the one `render()` is about to produce below.\n const stick = resolveStickToBottom();\n const wasAtEnd = stick.enabled && (virtualizer?.isAtEnd(stick.threshold) ?? true);\n\n currentItems = items;\n\n if (items.length === 0) {\n virtualizer?.dispose();\n virtualizer = null;\n stateStore.publish({ items: [], stickyItems: [], totalSize: 0 });\n clearAndReset();\n\n return;\n }\n\n if (!virtualizer) {\n const v = spawnVirtualizer();\n\n if (wasAtEnd) v.scrollToBottom();\n\n return;\n }\n\n const countChanged = items.length !== virtualizer.count;\n\n // Only count needs explicit update: estimateSize and getItemKey are\n // closures that already reflect the latest currentItems automatically.\n virtualizer.update({ count: items.length });\n\n // When count changed, update() already triggered rebuild + computeVisible().\n // Only force re-emission when count is unchanged (data changed, count same).\n if (!countChanged) {\n // refresh() re-emits with current sizes for stable keys;\n // invalidate() clears position-based measurements when no stable keys.\n if (options.getItemKey) {\n virtualizer.refresh();\n } else {\n virtualizer.invalidate();\n }\n } else if (!options.getItemKey) {\n // Count changed AND no stable keys: position-based measurements are now\n // stale. Clear them so the next render remeasures from fresh estimates.\n virtualizer.invalidate();\n }\n\n if (wasAtEnd) virtualizer.scrollToBottom();\n },\n\n get stickyItems() {\n return virtualizer?.stickyItems ?? [];\n },\n\n subscribe: stateStore.subscribe,\n\n [Symbol.dispose]: _dispose,\n\n get totalSize() {\n return virtualizer?.totalSize ?? 0;\n },\n };\n}\n\n// ─── F5: createVirtualScroller ────────────────────────────────────────────────\n\n/**\n * High-level factory that creates the scroll container and inner list element,\n * appends them to `container`, and returns a fully wired `DomVirtualListController`.\n *\n * @example\n * ```ts\n * const list = createVirtualScroller(document.getElementById('root')!, {\n * render({ items, listEl, recycle }) { … },\n * });\n * list.setItems(data);\n * ```\n */\nexport function createVirtualScroller<T>(\n container: HTMLElement,\n options: VirtualScrollerOptions<T>,\n): DomVirtualListController<T> {\n const scrollEl = document.createElement('div');\n\n scrollEl.style.cssText = options.horizontal\n ? 'overflow: auto hidden; width: 100%; height: 100%;'\n : 'overflow: hidden auto; width: 100%; height: 100%;';\n\n if (options.containerClass) scrollEl.className = options.containerClass;\n\n const listEl = document.createElement('div');\n\n scrollEl.appendChild(listEl);\n container.appendChild(scrollEl);\n\n let ctrl: DomVirtualListController<T>;\n\n try {\n ctrl = createDomVirtualList<T>({\n ...options,\n listElement: listEl,\n scrollElement: scrollEl,\n });\n } catch (e) {\n // Remove the scroll container if construction fails so we don't leak DOM nodes.\n scrollEl.remove();\n throw e;\n }\n\n // Override dispose and [Symbol.dispose] to also remove the scroll container.\n // Capture the original dispose before overwriting so there's no self-reference.\n const innerDispose = ctrl.dispose.bind(ctrl);\n\n return Object.assign(ctrl, {\n dispose() {\n innerDispose();\n scrollEl.remove();\n },\n [Symbol.dispose]() {\n innerDispose();\n scrollEl.remove();\n },\n });\n}\n"],"mappings":"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"}
@@ -22,7 +22,7 @@ export type StickToBottomOptions = {
22
22
  /** Enable/disable the behavior. Default: `true` once this object is provided. */
23
23
  enabled?: boolean;
24
24
  /**
25
- * Distance in pixels from the end still considered "at the end" — the bottom edge in
25
+ * Distance in pixels from the end still considered "at the end": the bottom edge in
26
26
  * vertical mode, the trailing edge in horizontal mode. Default: `48`.
27
27
  */
28
28
  threshold?: number;
@@ -42,7 +42,7 @@ export type DomVirtualListOptions<T> = {
42
42
  scrollElement: HTMLElement | Window;
43
43
  /**
44
44
  * Auto-scroll to the end after `setItems()` whenever the list was already at (or near) the
45
- * end just before the update — the chat "stick to bottom on new message" pattern. Fires on
45
+ * end just before the update: the chat "stick to bottom on new message" pattern. Fires on
46
46
  * *any* update while at the end, not just growth, so it also follows a streaming last
47
47
  * item that grows in place without changing `items.length`. Does nothing while the user
48
48
  * has scrolled away from the end. Pass `true` for defaults, or an options object.
@@ -56,7 +56,7 @@ export type DomVirtualListOptions<T> = {
56
56
  * scrollToOffset, etc.) are accessible directly on the controller without
57
57
  * needing to unwrap an inner virtualizer handle.
58
58
  *
59
- * `prepend` and `update` are omitted — use `setItems()` for item updates and
59
+ * `prepend` and `update` are omitted: use `setItems()` for item updates and
60
60
  * there is no direct `prepend` concept in DomVirtualList.
61
61
  */
62
62
  export type DomVirtualListController<T> = Omit<Virtualizer, 'prepend' | 'update'> & {
@@ -1 +1 @@
1
- {"version":3,"file":"dom-virtual-list.js","names":[],"sources":["../src/dom-virtual-list.ts"],"sourcesContent":["import {\n createSnapshotStore,\n DEFAULT_ESTIMATE_SIZE,\n DEFAULT_OVERSCAN,\n type MeasurementCache,\n type Overscan,\n} from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigError, ScrollRangeError } from './errors';\nimport {\n createVirtualizer,\n type ScrollToIndexOptions,\n type VirtualItem,\n type Virtualizer,\n type VirtualizerState,\n type VirtualKey,\n} from './virtualizer';\n\nexport type {\n MeasurementCache,\n Overscan,\n ScrollToIndexOptions,\n VirtualItem,\n Virtualizer,\n VirtualizerState,\n VirtualKey,\n};\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** A `VirtualItem` enriched with the corresponding data record. */\nexport type VirtualRenderItem<T> = VirtualItem & { readonly data: T };\n\n/**\n * Recycle a DOM node by key. If the pool has a live node for `key`, it is\n * returned and reused; otherwise `create()` is called to produce a new one.\n */\nexport type RecycleFn = (key: VirtualKey, create: () => HTMLElement) => HTMLElement;\n\nexport type DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>;\n listEl: HTMLElement;\n recycle: RecycleFn;\n /** Sticky items from the underlying virtualizer, enriched with data. */\n stickyItems: Array<VirtualRenderItem<T>>;\n totalSize: number;\n};\n\nexport type StickToBottomOptions = {\n /** Enable/disable the behavior. Default: `true` once this object is provided. */\n enabled?: boolean;\n /**\n * Distance in pixels from the end still considered \"at the end\" — the bottom edge in\n * vertical mode, the trailing edge in horizontal mode. Default: `48`.\n */\n threshold?: number;\n};\n\nexport type DomVirtualListOptions<T> = {\n /** Custom teardown that clears listEl. Defaults to `listEl.textContent = ''`. */\n clear?: (listEl: HTMLElement) => void;\n estimateSize?: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n horizontal?: boolean;\n listElement: HTMLElement;\n /** External measurement cache for scroll restoration. */\n measurementCache?: MeasurementCache;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n scrollElement: HTMLElement | Window;\n /**\n * Auto-scroll to the end after `setItems()` whenever the list was already at (or near) the\n * end just before the update — the chat \"stick to bottom on new message\" pattern. Fires on\n * *any* update while at the end, not just growth, so it also follows a streaming last\n * item that grows in place without changing `items.length`. Does nothing while the user\n * has scrolled away from the end. Pass `true` for defaults, or an options object.\n */\n stickToBottom?: boolean | StickToBottomOptions;\n /** Mark items as sticky headers. Receives the item index and the item data. */\n sticky?: (index: number, item: T) => boolean;\n};\n\n/**\n * R11: Controller extends Virtualizer so all methods (scrollToIndex, refresh,\n * scrollToOffset, etc.) are accessible directly on the controller without\n * needing to unwrap an inner virtualizer handle.\n *\n * `prepend` and `update` are omitted — use `setItems()` for item updates and\n * there is no direct `prepend` concept in DomVirtualList.\n */\nexport type DomVirtualListController<T> = Omit<Virtualizer, 'prepend' | 'update'> & {\n setItems: (items: T[]) => void;\n};\n\nexport type VirtualScrollerOptions<T> = Omit<DomVirtualListOptions<T>, 'listElement' | 'scrollElement'> & {\n /** Additional CSS class names on the generated scroll container. */\n containerClass?: string;\n};\n\n// ─── Node pool ────────────────────────────────────────────────────────────────\n\nfunction createNodePool() {\n let live = new Map<VirtualKey, HTMLElement>();\n let nextLive = new Map<VirtualKey, HTMLElement>();\n const stale: HTMLElement[] = [];\n let inCycle = false;\n\n return {\n acquire(key: VirtualKey, create: () => HTMLElement): HTMLElement {\n if (!inCycle) return create();\n\n const existing = live.get(key);\n\n if (existing) {\n nextLive.set(key, existing);\n\n return existing;\n }\n\n const node = stale.pop() ?? create();\n\n nextLive.set(key, node);\n\n return node;\n },\n\n beginCycle(): void {\n nextLive = new Map();\n inCycle = true;\n },\n\n clear(): void {\n for (const node of live.values()) node.remove();\n\n live.clear();\n nextLive.clear();\n stale.length = 0;\n inCycle = false;\n },\n\n endCycle(): void {\n if (!inCycle) return;\n\n inCycle = false;\n\n for (const [key, node] of live) {\n if (!nextLive.has(key)) {\n node.remove();\n stale.push(node);\n }\n }\n\n live = nextLive;\n },\n };\n}\n\n// ─── Implementation ────────────────────────────────────────────────────────────\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n if (typeof options.estimateSize === 'number') requirePositiveNumber(options.estimateSize, 'estimateSize');\n\n if (options.gap !== undefined) requireNonNegativeInteger(options.gap, 'gap');\n\n if (options.overscan !== undefined) validateOverscan(options.overscan);\n\n if (options.stickToBottom !== undefined && typeof options.stickToBottom !== 'boolean') {\n if (options.stickToBottom === null || Array.isArray(options.stickToBottom)) {\n throw new ScrollConfigError('stickToBottom must be a boolean or options object.');\n }\n\n if (options.stickToBottom.threshold !== undefined) {\n requireNonNegativeNumber(options.stickToBottom.threshold, 'stickToBottom.threshold');\n }\n }\n\n let currentItems: T[] = [];\n let disposed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\n const stateStore = createSnapshotStore<VirtualizerState>({ items: [], stickyItems: [], totalSize: 0 });\n\n // Publish state before invoking the render callback.\n function emitState(state: VirtualizerState): void {\n stateStore.publish(state);\n handleChange(state);\n }\n\n // Pool must be declared before virtualizer since handleChange (passed as onChange)\n // is invoked during createVirtualizer initialization via computeVisible.\n const pool = createNodePool();\n\n // Virtualizer is lazily created on the first non-empty setItems call.\n let virtualizer: Virtualizer | null = null;\n\n function resolveKey(index: number): VirtualKey {\n const item = currentItems[index];\n\n if (item !== undefined && options.getItemKey) return options.getItemKey(index, item);\n\n return index;\n }\n\n function resolveEstimate(index: number): number {\n if (options.estimateSize === undefined) return DEFAULT_ESTIMATE_SIZE;\n\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n const item = currentItems[index];\n\n return item !== undefined ? options.estimateSize(index, item) : DEFAULT_ESTIMATE_SIZE;\n }\n\n function applyListSize(totalSize: number): void {\n if (options.horizontal) {\n listEl.style.height = '';\n listEl.style.width = `${totalSize}px`;\n } else {\n listEl.style.height = `${totalSize}px`;\n listEl.style.width = '';\n }\n }\n\n /**\n * R10: Throw rather than silently produce `undefined as T`.\n * This catches bugs where `vi.index` is out of range for `currentItems`.\n */\n function toRenderItem(vi: VirtualItem): VirtualRenderItem<T> {\n const data = currentItems[vi.index];\n\n if (data === undefined) {\n throw new ScrollRangeError(\n `toRenderItem: index ${vi.index} is out of range (currentItems.length=${currentItems.length})`,\n );\n }\n\n return { ...vi, data };\n }\n\n function handleChange(state: VirtualizerState): void {\n applyListSize(state.totalSize);\n\n pool.beginCycle();\n\n // R5: try/finally ensures endCycle() runs even if render() throws, keeping\n // the pool in a consistent state.\n try {\n options.render({\n items: state.items.map(toRenderItem),\n listEl,\n recycle: (key, create) => pool.acquire(key, create),\n stickyItems: state.stickyItems.map(toRenderItem),\n totalSize: state.totalSize,\n });\n } finally {\n pool.endCycle();\n }\n }\n\n function clearAndReset(): void {\n pool.clear();\n\n if (options.clear) {\n options.clear(listEl);\n } else {\n listEl.textContent = '';\n }\n\n listEl.style.height = '';\n listEl.style.width = '';\n listEl.style.position = '';\n listEl.style.contain = '';\n }\n\n const DEFAULT_STICK_THRESHOLD = 48;\n\n function resolveStickToBottom(): { enabled: boolean; threshold: number } {\n const opt = options.stickToBottom;\n\n if (!opt) return { enabled: false, threshold: DEFAULT_STICK_THRESHOLD };\n\n if (opt === true) return { enabled: true, threshold: DEFAULT_STICK_THRESHOLD };\n\n return { enabled: opt.enabled ?? true, threshold: opt.threshold ?? DEFAULT_STICK_THRESHOLD };\n }\n\n function spawnVirtualizer(): Virtualizer {\n const v = createVirtualizer(options.scrollElement, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveKey,\n horizontal: options.horizontal,\n measurementCache: options.measurementCache,\n onChange: emitState,\n overscan: options.overscan ?? DEFAULT_OVERSCAN,\n sticky: options.sticky\n ? (index) => {\n const item = currentItems[index];\n\n return item !== undefined && options.sticky!(index, item);\n }\n : undefined,\n });\n\n virtualizer = v;\n listEl.style.position = 'relative';\n listEl.style.contain = 'layout';\n\n return v;\n }\n\n function _dispose(): void {\n if (disposed) return;\n\n disposed = true;\n stateStore.dispose();\n ac.abort();\n virtualizer?.dispose();\n virtualizer = null;\n clearAndReset();\n }\n\n return {\n // ── Virtualizer passthrough (R11) ──────────────────────────────────────\n get count() {\n return virtualizer?.count ?? 0;\n },\n\n get disposalSignal() {\n return ac.signal;\n },\n\n dispose: _dispose,\n\n get disposed() {\n return disposed;\n },\n\n getSnapshot: stateStore.getSnapshot,\n\n invalidate() {\n if (disposed) return;\n\n virtualizer?.invalidate();\n },\n\n isAtEnd(threshold) {\n return virtualizer?.isAtEnd(threshold) ?? true;\n },\n\n get isScrolling() {\n return virtualizer?.isScrolling ?? false;\n },\n\n get items() {\n return virtualizer?.items ?? [];\n },\n\n measure(index, size) {\n if (disposed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (disposed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (disposed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (disposed) return;\n\n virtualizer?.refresh();\n },\n\n remeasure() {\n if (disposed) return;\n\n virtualizer?.remeasure();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (disposed) return;\n\n // Read *before* mutating state — the \"was the list already at the end?\" check must\n // reflect the pre-update layout, not the one `render()` is about to produce below.\n const stick = resolveStickToBottom();\n const wasAtEnd = stick.enabled && (virtualizer?.isAtEnd(stick.threshold) ?? true);\n\n currentItems = items;\n\n if (items.length === 0) {\n virtualizer?.dispose();\n virtualizer = null;\n stateStore.publish({ items: [], stickyItems: [], totalSize: 0 });\n clearAndReset();\n\n return;\n }\n\n if (!virtualizer) {\n const v = spawnVirtualizer();\n\n if (wasAtEnd) v.scrollToBottom();\n\n return;\n }\n\n const countChanged = items.length !== virtualizer.count;\n\n // Only count needs explicit update — estimateSize and getItemKey are\n // closures that already reflect the latest currentItems automatically.\n virtualizer.update({ count: items.length });\n\n // When count changed, update() already triggered rebuild + computeVisible().\n // Only force re-emission when count is unchanged (data changed, count same).\n if (!countChanged) {\n // refresh() re-emits with current sizes for stable keys;\n // invalidate() clears position-based measurements when no stable keys.\n if (options.getItemKey) {\n virtualizer.refresh();\n } else {\n virtualizer.invalidate();\n }\n } else if (!options.getItemKey) {\n // Count changed AND no stable keys: position-based measurements are now\n // stale. Clear them so the next render remeasures from fresh estimates.\n virtualizer.invalidate();\n }\n\n if (wasAtEnd) virtualizer.scrollToBottom();\n },\n\n get stickyItems() {\n return virtualizer?.stickyItems ?? [];\n },\n\n subscribe: stateStore.subscribe,\n\n [Symbol.dispose]: _dispose,\n\n get totalSize() {\n return virtualizer?.totalSize ?? 0;\n },\n };\n}\n\n// ─── F5: createVirtualScroller ────────────────────────────────────────────────\n\n/**\n * High-level factory that creates the scroll container and inner list element,\n * appends them to `container`, and returns a fully wired `DomVirtualListController`.\n *\n * @example\n * ```ts\n * const list = createVirtualScroller(document.getElementById('root')!, {\n * render({ items, listEl, recycle }) { … },\n * });\n * list.setItems(data);\n * ```\n */\nexport function createVirtualScroller<T>(\n container: HTMLElement,\n options: VirtualScrollerOptions<T>,\n): DomVirtualListController<T> {\n const scrollEl = document.createElement('div');\n\n scrollEl.style.cssText = options.horizontal\n ? 'overflow: auto hidden; width: 100%; height: 100%;'\n : 'overflow: hidden auto; width: 100%; height: 100%;';\n\n if (options.containerClass) scrollEl.className = options.containerClass;\n\n const listEl = document.createElement('div');\n\n scrollEl.appendChild(listEl);\n container.appendChild(scrollEl);\n\n let ctrl: DomVirtualListController<T>;\n\n try {\n ctrl = createDomVirtualList<T>({\n ...options,\n listElement: listEl,\n scrollElement: scrollEl,\n });\n } catch (e) {\n // Remove the scroll container if construction fails so we don't leak DOM nodes.\n scrollEl.remove();\n throw e;\n }\n\n // Override dispose and [Symbol.dispose] to also remove the scroll container.\n // Capture the original dispose before overwriting so there's no self-reference.\n const innerDispose = ctrl.dispose.bind(ctrl);\n\n return Object.assign(ctrl, {\n dispose() {\n innerDispose();\n scrollEl.remove();\n },\n [Symbol.dispose]() {\n innerDispose();\n scrollEl.remove();\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"dom-virtual-list.js","names":[],"sources":["../src/dom-virtual-list.ts"],"sourcesContent":["import {\n createSnapshotStore,\n DEFAULT_ESTIMATE_SIZE,\n DEFAULT_OVERSCAN,\n type MeasurementCache,\n type Overscan,\n} from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigError, ScrollRangeError } from './errors';\nimport {\n createVirtualizer,\n type ScrollToIndexOptions,\n type VirtualItem,\n type Virtualizer,\n type VirtualizerState,\n type VirtualKey,\n} from './virtualizer';\n\nexport type {\n MeasurementCache,\n Overscan,\n ScrollToIndexOptions,\n VirtualItem,\n Virtualizer,\n VirtualizerState,\n VirtualKey,\n};\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** A `VirtualItem` enriched with the corresponding data record. */\nexport type VirtualRenderItem<T> = VirtualItem & { readonly data: T };\n\n/**\n * Recycle a DOM node by key. If the pool has a live node for `key`, it is\n * returned and reused; otherwise `create()` is called to produce a new one.\n */\nexport type RecycleFn = (key: VirtualKey, create: () => HTMLElement) => HTMLElement;\n\nexport type DomVirtualListRenderArgs<T> = {\n items: Array<VirtualRenderItem<T>>;\n listEl: HTMLElement;\n recycle: RecycleFn;\n /** Sticky items from the underlying virtualizer, enriched with data. */\n stickyItems: Array<VirtualRenderItem<T>>;\n totalSize: number;\n};\n\nexport type StickToBottomOptions = {\n /** Enable/disable the behavior. Default: `true` once this object is provided. */\n enabled?: boolean;\n /**\n * Distance in pixels from the end still considered \"at the end\": the bottom edge in\n * vertical mode, the trailing edge in horizontal mode. Default: `48`.\n */\n threshold?: number;\n};\n\nexport type DomVirtualListOptions<T> = {\n /** Custom teardown that clears listEl. Defaults to `listEl.textContent = ''`. */\n clear?: (listEl: HTMLElement) => void;\n estimateSize?: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n horizontal?: boolean;\n listElement: HTMLElement;\n /** External measurement cache for scroll restoration. */\n measurementCache?: MeasurementCache;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n scrollElement: HTMLElement | Window;\n /**\n * Auto-scroll to the end after `setItems()` whenever the list was already at (or near) the\n * end just before the update: the chat \"stick to bottom on new message\" pattern. Fires on\n * *any* update while at the end, not just growth, so it also follows a streaming last\n * item that grows in place without changing `items.length`. Does nothing while the user\n * has scrolled away from the end. Pass `true` for defaults, or an options object.\n */\n stickToBottom?: boolean | StickToBottomOptions;\n /** Mark items as sticky headers. Receives the item index and the item data. */\n sticky?: (index: number, item: T) => boolean;\n};\n\n/**\n * R11: Controller extends Virtualizer so all methods (scrollToIndex, refresh,\n * scrollToOffset, etc.) are accessible directly on the controller without\n * needing to unwrap an inner virtualizer handle.\n *\n * `prepend` and `update` are omitted: use `setItems()` for item updates and\n * there is no direct `prepend` concept in DomVirtualList.\n */\nexport type DomVirtualListController<T> = Omit<Virtualizer, 'prepend' | 'update'> & {\n setItems: (items: T[]) => void;\n};\n\nexport type VirtualScrollerOptions<T> = Omit<DomVirtualListOptions<T>, 'listElement' | 'scrollElement'> & {\n /** Additional CSS class names on the generated scroll container. */\n containerClass?: string;\n};\n\n// ─── Node pool ────────────────────────────────────────────────────────────────\n\nfunction createNodePool() {\n let live = new Map<VirtualKey, HTMLElement>();\n let nextLive = new Map<VirtualKey, HTMLElement>();\n const stale: HTMLElement[] = [];\n let inCycle = false;\n\n return {\n acquire(key: VirtualKey, create: () => HTMLElement): HTMLElement {\n if (!inCycle) return create();\n\n const existing = live.get(key);\n\n if (existing) {\n nextLive.set(key, existing);\n\n return existing;\n }\n\n const node = stale.pop() ?? create();\n\n nextLive.set(key, node);\n\n return node;\n },\n\n beginCycle(): void {\n nextLive = new Map();\n inCycle = true;\n },\n\n clear(): void {\n for (const node of live.values()) node.remove();\n\n live.clear();\n nextLive.clear();\n stale.length = 0;\n inCycle = false;\n },\n\n endCycle(): void {\n if (!inCycle) return;\n\n inCycle = false;\n\n for (const [key, node] of live) {\n if (!nextLive.has(key)) {\n node.remove();\n stale.push(node);\n }\n }\n\n live = nextLive;\n },\n };\n}\n\n// ─── Implementation ────────────────────────────────────────────────────────────\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n if (typeof options.estimateSize === 'number') requirePositiveNumber(options.estimateSize, 'estimateSize');\n\n if (options.gap !== undefined) requireNonNegativeInteger(options.gap, 'gap');\n\n if (options.overscan !== undefined) validateOverscan(options.overscan);\n\n if (options.stickToBottom !== undefined && typeof options.stickToBottom !== 'boolean') {\n if (options.stickToBottom === null || Array.isArray(options.stickToBottom)) {\n throw new ScrollConfigError('stickToBottom must be a boolean or options object.');\n }\n\n if (options.stickToBottom.threshold !== undefined) {\n requireNonNegativeNumber(options.stickToBottom.threshold, 'stickToBottom.threshold');\n }\n }\n\n let currentItems: T[] = [];\n let disposed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\n const stateStore = createSnapshotStore<VirtualizerState>({ items: [], stickyItems: [], totalSize: 0 });\n\n // Publish state before invoking the render callback.\n function emitState(state: VirtualizerState): void {\n stateStore.publish(state);\n handleChange(state);\n }\n\n // Pool must be declared before virtualizer since handleChange (passed as onChange)\n // is invoked during createVirtualizer initialization via computeVisible.\n const pool = createNodePool();\n\n // Virtualizer is lazily created on the first non-empty setItems call.\n let virtualizer: Virtualizer | null = null;\n\n function resolveKey(index: number): VirtualKey {\n const item = currentItems[index];\n\n if (item !== undefined && options.getItemKey) return options.getItemKey(index, item);\n\n return index;\n }\n\n function resolveEstimate(index: number): number {\n if (options.estimateSize === undefined) return DEFAULT_ESTIMATE_SIZE;\n\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n const item = currentItems[index];\n\n return item !== undefined ? options.estimateSize(index, item) : DEFAULT_ESTIMATE_SIZE;\n }\n\n function applyListSize(totalSize: number): void {\n if (options.horizontal) {\n listEl.style.height = '';\n listEl.style.width = `${totalSize}px`;\n } else {\n listEl.style.height = `${totalSize}px`;\n listEl.style.width = '';\n }\n }\n\n /**\n * R10: Throw rather than silently produce `undefined as T`.\n * This catches bugs where `vi.index` is out of range for `currentItems`.\n */\n function toRenderItem(vi: VirtualItem): VirtualRenderItem<T> {\n const data = currentItems[vi.index];\n\n if (data === undefined) {\n throw new ScrollRangeError(\n `toRenderItem: index ${vi.index} is out of range (currentItems.length=${currentItems.length})`,\n );\n }\n\n return { ...vi, data };\n }\n\n function handleChange(state: VirtualizerState): void {\n applyListSize(state.totalSize);\n\n pool.beginCycle();\n\n // R5: try/finally ensures endCycle() runs even if render() throws, keeping\n // the pool in a consistent state.\n try {\n options.render({\n items: state.items.map(toRenderItem),\n listEl,\n recycle: (key, create) => pool.acquire(key, create),\n stickyItems: state.stickyItems.map(toRenderItem),\n totalSize: state.totalSize,\n });\n } finally {\n pool.endCycle();\n }\n }\n\n function clearAndReset(): void {\n pool.clear();\n\n if (options.clear) {\n options.clear(listEl);\n } else {\n listEl.textContent = '';\n }\n\n listEl.style.height = '';\n listEl.style.width = '';\n listEl.style.position = '';\n listEl.style.contain = '';\n }\n\n const DEFAULT_STICK_THRESHOLD = 48;\n\n function resolveStickToBottom(): { enabled: boolean; threshold: number } {\n const opt = options.stickToBottom;\n\n if (!opt) return { enabled: false, threshold: DEFAULT_STICK_THRESHOLD };\n\n if (opt === true) return { enabled: true, threshold: DEFAULT_STICK_THRESHOLD };\n\n return { enabled: opt.enabled ?? true, threshold: opt.threshold ?? DEFAULT_STICK_THRESHOLD };\n }\n\n function spawnVirtualizer(): Virtualizer {\n const v = createVirtualizer(options.scrollElement, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveKey,\n horizontal: options.horizontal,\n measurementCache: options.measurementCache,\n onChange: emitState,\n overscan: options.overscan ?? DEFAULT_OVERSCAN,\n sticky: options.sticky\n ? (index) => {\n const item = currentItems[index];\n\n return item !== undefined && options.sticky!(index, item);\n }\n : undefined,\n });\n\n virtualizer = v;\n listEl.style.position = 'relative';\n listEl.style.contain = 'layout';\n\n return v;\n }\n\n function _dispose(): void {\n if (disposed) return;\n\n disposed = true;\n stateStore.dispose();\n ac.abort();\n virtualizer?.dispose();\n virtualizer = null;\n clearAndReset();\n }\n\n return {\n // ── Virtualizer passthrough (R11) ──────────────────────────────────────\n get count() {\n return virtualizer?.count ?? 0;\n },\n\n get disposalSignal() {\n return ac.signal;\n },\n\n dispose: _dispose,\n\n get disposed() {\n return disposed;\n },\n\n getSnapshot: stateStore.getSnapshot,\n\n invalidate() {\n if (disposed) return;\n\n virtualizer?.invalidate();\n },\n\n isAtEnd(threshold) {\n return virtualizer?.isAtEnd(threshold) ?? true;\n },\n\n get isScrolling() {\n return virtualizer?.isScrolling ?? false;\n },\n\n get items() {\n return virtualizer?.items ?? [];\n },\n\n measure(index, size) {\n if (disposed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (disposed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (disposed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (disposed) return;\n\n virtualizer?.refresh();\n },\n\n remeasure() {\n if (disposed) return;\n\n virtualizer?.remeasure();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (disposed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (disposed) return;\n\n // Read *before* mutating state: the \"was the list already at the end?\" check must\n // reflect the pre-update layout, not the one `render()` is about to produce below.\n const stick = resolveStickToBottom();\n const wasAtEnd = stick.enabled && (virtualizer?.isAtEnd(stick.threshold) ?? true);\n\n currentItems = items;\n\n if (items.length === 0) {\n virtualizer?.dispose();\n virtualizer = null;\n stateStore.publish({ items: [], stickyItems: [], totalSize: 0 });\n clearAndReset();\n\n return;\n }\n\n if (!virtualizer) {\n const v = spawnVirtualizer();\n\n if (wasAtEnd) v.scrollToBottom();\n\n return;\n }\n\n const countChanged = items.length !== virtualizer.count;\n\n // Only count needs explicit update: estimateSize and getItemKey are\n // closures that already reflect the latest currentItems automatically.\n virtualizer.update({ count: items.length });\n\n // When count changed, update() already triggered rebuild + computeVisible().\n // Only force re-emission when count is unchanged (data changed, count same).\n if (!countChanged) {\n // refresh() re-emits with current sizes for stable keys;\n // invalidate() clears position-based measurements when no stable keys.\n if (options.getItemKey) {\n virtualizer.refresh();\n } else {\n virtualizer.invalidate();\n }\n } else if (!options.getItemKey) {\n // Count changed AND no stable keys: position-based measurements are now\n // stale. Clear them so the next render remeasures from fresh estimates.\n virtualizer.invalidate();\n }\n\n if (wasAtEnd) virtualizer.scrollToBottom();\n },\n\n get stickyItems() {\n return virtualizer?.stickyItems ?? [];\n },\n\n subscribe: stateStore.subscribe,\n\n [Symbol.dispose]: _dispose,\n\n get totalSize() {\n return virtualizer?.totalSize ?? 0;\n },\n };\n}\n\n// ─── F5: createVirtualScroller ────────────────────────────────────────────────\n\n/**\n * High-level factory that creates the scroll container and inner list element,\n * appends them to `container`, and returns a fully wired `DomVirtualListController`.\n *\n * @example\n * ```ts\n * const list = createVirtualScroller(document.getElementById('root')!, {\n * render({ items, listEl, recycle }) { … },\n * });\n * list.setItems(data);\n * ```\n */\nexport function createVirtualScroller<T>(\n container: HTMLElement,\n options: VirtualScrollerOptions<T>,\n): DomVirtualListController<T> {\n const scrollEl = document.createElement('div');\n\n scrollEl.style.cssText = options.horizontal\n ? 'overflow: auto hidden; width: 100%; height: 100%;'\n : 'overflow: hidden auto; width: 100%; height: 100%;';\n\n if (options.containerClass) scrollEl.className = options.containerClass;\n\n const listEl = document.createElement('div');\n\n scrollEl.appendChild(listEl);\n container.appendChild(scrollEl);\n\n let ctrl: DomVirtualListController<T>;\n\n try {\n ctrl = createDomVirtualList<T>({\n ...options,\n listElement: listEl,\n scrollElement: scrollEl,\n });\n } catch (e) {\n // Remove the scroll container if construction fails so we don't leak DOM nodes.\n scrollEl.remove();\n throw e;\n }\n\n // Override dispose and [Symbol.dispose] to also remove the scroll container.\n // Capture the original dispose before overwriting so there's no self-reference.\n const innerDispose = ctrl.dispose.bind(ctrl);\n\n return Object.assign(ctrl, {\n dispose() {\n innerDispose();\n scrollEl.remove();\n },\n [Symbol.dispose]() {\n innerDispose();\n scrollEl.remove();\n },\n });\n}\n"],"mappings":"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"}