@vielzeug/scroll 2.0.2 → 2.1.1

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.
Files changed (48) hide show
  1. package/README.md +182 -7
  2. package/dist/_sticky.cjs +2 -0
  3. package/dist/_sticky.cjs.map +1 -0
  4. package/dist/_sticky.d.ts +22 -0
  5. package/dist/_sticky.d.ts.map +1 -0
  6. package/dist/_sticky.js +2 -0
  7. package/dist/_sticky.js.map +1 -0
  8. package/dist/dom-virtual-list.cjs +1 -1
  9. package/dist/dom-virtual-list.cjs.map +1 -1
  10. package/dist/dom-virtual-list.d.ts +5 -0
  11. package/dist/dom-virtual-list.d.ts.map +1 -1
  12. package/dist/dom-virtual-list.js +1 -1
  13. package/dist/dom-virtual-list.js.map +1 -1
  14. package/dist/grid-virtualizer.cjs +1 -1
  15. package/dist/grid-virtualizer.cjs.map +1 -1
  16. package/dist/grid-virtualizer.d.ts +6 -0
  17. package/dist/grid-virtualizer.d.ts.map +1 -1
  18. package/dist/grid-virtualizer.js +1 -1
  19. package/dist/grid-virtualizer.js.map +1 -1
  20. package/dist/grouped-virtualizer.cjs +1 -1
  21. package/dist/grouped-virtualizer.cjs.map +1 -1
  22. package/dist/grouped-virtualizer.d.ts +3 -0
  23. package/dist/grouped-virtualizer.d.ts.map +1 -1
  24. package/dist/grouped-virtualizer.js +1 -1
  25. package/dist/grouped-virtualizer.js.map +1 -1
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.ts +0 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/scroll.cjs +1 -1
  31. package/dist/scroll.cjs.map +1 -1
  32. package/dist/scroll.iife.js +1 -1
  33. package/dist/scroll.iife.js.map +1 -1
  34. package/dist/scroll.js +1 -1
  35. package/dist/scroll.js.map +1 -1
  36. package/dist/virtualizer.cjs +1 -1
  37. package/dist/virtualizer.cjs.map +1 -1
  38. package/dist/virtualizer.d.ts +9 -0
  39. package/dist/virtualizer.d.ts.map +1 -1
  40. package/dist/virtualizer.js +1 -1
  41. package/dist/virtualizer.js.map +1 -1
  42. package/package.json +7 -2
  43. package/dist/reactive.cjs +0 -2
  44. package/dist/reactive.cjs.map +0 -1
  45. package/dist/reactive.d.ts +0 -49
  46. package/dist/reactive.d.ts.map +0 -1
  47. package/dist/reactive.js +0 -2
  48. package/dist/reactive.js.map +0 -1
package/README.md CHANGED
@@ -9,15 +9,44 @@
9
9
 
10
10
  **Package:** `@vielzeug/scroll`  ·  **Category:** UI Performance
11
11
 
12
- **Key exports:** `createVirtualizer`, `createDomVirtualList`, `createVirtualScroller`, `createGroupedVirtualizer`, `createGridVirtualizer`, `createReactiveVirtualizer`
12
+ **Key exports:** `createVirtualizer`, `createDomVirtualList`, `createVirtualScroller`, `createGroupedVirtualizer`, `createGridVirtualizer`
13
13
 
14
- **When to use:** Render only visible rows in large lists. Supports fixed heights, variable heights, sticky headers, grouped sections, grid virtualization, programmatic scrolling, and reactive signal integration.
14
+ **When to use:** Render only visible rows in large lists. Supports fixed heights, variable heights, sticky headers, grouped sections, grid virtualization, programmatic scrolling, and optional reactive signal integration.
15
15
 
16
16
  **Related:** [@vielzeug/dnd](https://vielzeug.dev/dnd/) · [@vielzeug/ore](https://vielzeug.dev/ore/) · [@vielzeug/refine](https://vielzeug.dev/refine/)
17
17
 
18
18
  </details>
19
19
 
20
- `@vielzeug/scroll` is part of Vielzeug and ships as a TypeScript package with ESM+CJS output. The only dependency is `@vielzeug/ripple`, used by the optional reactive integration.
20
+ `@vielzeug/scroll` is part of Vielzeug and ships as a TypeScript package with ESM+CJS output. The only dependency is `@vielzeug/ripple`, used by the optional reactive signal integration.
21
+
22
+ ## Choosing a Factory
23
+
24
+ Each factory serves a specific use case. Pick the one that matches your needs:
25
+
26
+ | Factory | Use Case | Benefits | Trade-offs |
27
+ |---------|----------|----------|-----------|
28
+ | **`createVirtualizer`** | Low-level scroll optimization with manual DOM | Full control, minimal overhead | You manage layout and DOM updates |
29
+ | **`createDomVirtualList`** | Rendering a data-bound list with automatic cleanup | Auto height management, item recycling, stick-to-bottom | Less control over container |
30
+ | **`createVirtualScroller`** | Quick setup: auto-creates scroll + list containers | Minimal setup, self-contained | Less flexibility over structure |
31
+ | **`createGroupedVirtualizer`** | Lists with grouped sections and sticky headers | Automatic sticky headers, section navigation | Not suitable for flat lists |
32
+ | **`createGridVirtualizer`** | 2D grids (spreadsheets, photo galleries) | Row + column virtualization, automatic cell layout | More complex to render |
33
+
34
+ ### Quick Decision Tree
35
+
36
+ ```
37
+ Do you need a 2D grid (rows AND columns)?
38
+ ├─ YES → createGridVirtualizer
39
+ └─ NO
40
+ Do your items have sections with headers?
41
+ ├─ YES → createGroupedVirtualizer
42
+ └─ NO
43
+ Do you want scroll + list containers auto-created?
44
+ ├─ YES → createVirtualScroller
45
+ └─ NO
46
+ Do you have data items that need recycling?
47
+ ├─ YES → createDomVirtualList
48
+ └─ NO → createVirtualizer (full control)
49
+ ```
21
50
 
22
51
  ## Installation
23
52
 
@@ -160,17 +189,161 @@ grid.scrollToCell(500, 10, { rowAlign: 'center', colAlign: 'start' });
160
189
  grid.dispose();
161
190
  ```
162
191
 
192
+ ## Measurement Methods
193
+
194
+ Items can have variable heights. Measure them once, and the offset table updates automatically.
195
+
196
+ ### `measure(index, size)`
197
+ Measure a single item. Use when one item's size changes (e.g., image loaded).
198
+ ```ts
199
+ virt.measure(42, 120); // Item 42 is now 120px tall
200
+ ```
201
+
202
+ ### `measureBatch(entries)`
203
+ Measure multiple items in one operation. Coalesces updates into a single rebuild.
204
+ ```ts
205
+ virt.measureBatch([
206
+ { index: 10, size: 150 },
207
+ { index: 11, size: 140 },
208
+ { index: 12, size: 160 },
209
+ ]);
210
+ ```
211
+
212
+ ### `measureEl(index, el)`
213
+ Auto-observe an element's size with `ResizeObserver`. Useful for dynamic content (videos, expanding text).
214
+ ```ts
215
+ const disconnect = virt.measureEl(42, videoElement);
216
+ // Later:
217
+ disconnect(); // Stop observing
218
+ ```
219
+
220
+ ## Migration Guide
221
+
222
+ ### Switching from `createVirtualizer` to `createDomVirtualList`
223
+
224
+ If you're manually managing a list and want item recycling + auto-height:
225
+
226
+ ```ts
227
+ // Before:
228
+ const virt = createVirtualizer(scrollEl, {
229
+ count: items.length,
230
+ onChange: ({ items: renderItems, totalSize }) => {
231
+ listEl.style.height = `${totalSize}px`;
232
+ // manual DOM updates
233
+ },
234
+ });
235
+
236
+ // After:
237
+ const virt = createDomVirtualList({
238
+ items,
239
+ scrollElement: scrollEl,
240
+ listElement: listEl,
241
+ render: ({ items: renderItems, recycle }) => {
242
+ // recycled DOM updates
243
+ },
244
+ });
245
+ virt.setItems(newItems); // Auto-rebuilds
246
+ ```
247
+
248
+ ### Switching from flat list to grouped
249
+
250
+ When your data gains structure (sections with headers):
251
+
252
+ ```ts
253
+ // Before:
254
+ const virt = createVirtualizer(scrollEl, { count: items.length });
255
+
256
+ // After:
257
+ const virt = createGroupedVirtualizer(scrollEl, {
258
+ sections: [
259
+ { label: 'Section A', items: itemsA },
260
+ { label: 'Section B', items: itemsB },
261
+ ],
262
+ });
263
+ ```
264
+
265
+ Changes needed in your render function:
266
+ - Receive `headers` array in addition to `items`
267
+ - Render headers with `.start`, `.size`, `.label`
268
+ - Render items with `.data` field containing the item
269
+
270
+ ## Keyboard Navigation
271
+
272
+ Enable keyboard-based scrolling with the `keyboardScroll` option:
273
+
274
+ ```ts
275
+ const virt = createVirtualizer(scrollEl, {
276
+ count: 1000,
277
+ estimateSize: 40,
278
+ keyboardScroll: true,
279
+ });
280
+ ```
281
+
282
+ Supported keys:
283
+ - **Arrow Up/Down** (or Left/Right for horizontal lists) — Scroll by one estimated item size
284
+ - **Page Up/Down** — Scroll by ~80% of viewport height (configurable with Page Down/Up key modifiers)
285
+ - **Home** — Jump to the start
286
+ - **End** — Jump to the end
287
+
288
+ **Requirements:**
289
+ - The scroll container (or a descendant) must have focus for keyboard events to fire
290
+ - Works with all factories: `createVirtualizer`, `createDomVirtualList`, `createGroupedVirtualizer`, `createGridVirtualizer`
291
+ - Grid virtualization supports separate row/column scrolling (arrows navigate rows or columns independently)
292
+
293
+ ## Auto-Measurement
294
+
295
+ For dynamic or user-generated content with variable sizes, enable `autoMeasure` to automatically measure visible items:
296
+
297
+ ```ts
298
+ const virt = createVirtualizer(scrollEl, {
299
+ count: 1000,
300
+ estimateSize: 40,
301
+ autoMeasure: true,
302
+ });
303
+ ```
304
+
305
+ **How it works:**
306
+ - Each rendered item is automatically measured via `ResizeObserver`
307
+ - Measurements are cached and the virtualizer recomputes layout in real time
308
+ - Useful for content that grows/shrinks (expanding text, loading spinners, videos)
309
+
310
+ **Requirements:**
311
+ - Every rendered item must have a `data-vz-key` attribute set to the item's key:
312
+ ```ts
313
+ const virt = createVirtualizer(scrollEl, {
314
+ count: items.length,
315
+ getItemKey: (i) => items[i].id,
316
+ onChange: ({ items: renderItems, totalSize }) => {
317
+ listEl.style.height = `${totalSize}px`;
318
+ for (const item of renderItems) {
319
+ const el = document.createElement('div');
320
+ el.setAttribute('data-vz-key', items[item.index].id); // <-- Required
321
+ el.textContent = items[item.index].text;
322
+ listEl.appendChild(el);
323
+ }
324
+ },
325
+ });
326
+ ```
327
+ - Must use a DOM scroll target (not `Window`)
328
+ - For more control, use `measureEl()` manually instead
329
+
330
+ **Caveats:**
331
+ - `autoMeasure` queries the DOM every render cycle; avoid with very large visible windows (100+ items)
332
+ - Elements are looked up in the scroll target; ensure elements are direct/indirect children
333
+ - ResizeObserver cleanup is automatic on `dispose()`
334
+
163
335
  ## Reactive Integration
164
336
 
165
- `createReactiveVirtualizer` wraps the core virtualizer and exposes state as a `Signal<VirtualizerState>` from `@vielzeug/ripple`:
337
+ Any virtualizer can emit state to a reactive `Signal` from `@vielzeug/ripple` by providing a `signal` option:
166
338
 
167
339
  ```ts
168
- import { createReactiveVirtualizer } from '@vielzeug/scroll';
169
- import { effect } from '@vielzeug/ripple';
340
+ import { createVirtualizer } from '@vielzeug/scroll';
341
+ import { signal, effect } from '@vielzeug/ripple';
170
342
 
171
- const virt = createReactiveVirtualizer(scrollEl, {
343
+ const virt = createVirtualizer(scrollEl, {
172
344
  count: 1000,
173
345
  estimateSize: 40,
346
+ signal: (init) => signal(init), // Create and emit to a signal
174
347
  });
175
348
 
176
349
  effect(() => {
@@ -182,6 +355,8 @@ effect(() => {
182
355
  virt.dispose();
183
356
  ```
184
357
 
358
+ The `signal` option works with all factories (`createDomVirtualList`, `createGroupedVirtualizer`, `createGridVirtualizer`, etc.) and works alongside the `onChange` callback if provided.
359
+
185
360
  ## Documentation
186
361
 
187
362
  - [Overview](https://vielzeug.dev/scroll/)
@@ -0,0 +1,2 @@
1
+ function e(e){if(!e.stickyFn||e.count===0||e.scrollOffset<=0)return[];let t=-1,n=0,r=e.count-1;for(;n<=r;){let i=n+r>>1;e.startAt(i)<e.scrollOffset?(t=i,n=i+1):r=i-1}if(t<0)return[];let i=-1;for(let n=t;n>=0;n--)if(e.stickyFn(n)){i=n;break}if(i===-1)return[];let a=e.sizeAt(i),o=1/0;for(let t=i+1;t<e.count;t++)if(e.stickyFn(t)){o=e.startAt(t);break}let s=Math.min(e.scrollOffset,o-a);return[{end:s+a,index:i,size:a,start:s}]}exports.computeStickyItems=e;
2
+ //# sourceMappingURL=_sticky.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_sticky.cjs","names":[],"sources":["../src/_sticky.ts"],"sourcesContent":["/**\n * Shared sticky item computation logic.\n * Used by virtualizer and grid-virtualizer for finding and pinning sticky items.\n */\n\nimport type { VirtualItem } from './_axis1d';\n\nexport interface StickyComputeContext {\n count: number;\n scrollOffset: number;\n sizeAt: (index: number) => number;\n startAt: (index: number) => number;\n stickyFn: ((index: number) => boolean) | null;\n}\n\n/**\n * Compute the sticky item(s) for a 1D scroll axis.\n * Returns the single sticky item that should be pinned at the viewport start,\n * accounting for the next sticky item's position.\n *\n * @param ctx Context with functions to query item positions and sizes\n * @returns Array containing at most one sticky item (pinned to viewport)\n */\nexport function computeStickyItems(ctx: StickyComputeContext): VirtualItem[] {\n if (!ctx.stickyFn || ctx.count === 0 || ctx.scrollOffset <= 0) return [];\n\n // Binary search to find the last item that starts before the viewport\n let lastAbove = -1;\n let lo = 0;\n let hi = ctx.count - 1;\n\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n\n if (ctx.startAt(mid) < ctx.scrollOffset) {\n lastAbove = mid;\n lo = mid + 1;\n } else {\n hi = mid - 1;\n }\n }\n\n if (lastAbove < 0) return [];\n\n // Scan backwards from lastAbove to find the most recent sticky item\n let activeIdx = -1;\n\n for (let i = lastAbove; i >= 0; i--) {\n if (ctx.stickyFn(i)) {\n activeIdx = i;\n break;\n }\n }\n\n if (activeIdx === -1) return [];\n\n // Find the next sticky item to determine how far we can pin\n const activeSize = ctx.sizeAt(activeIdx);\n let nextStickyStart = Infinity;\n\n for (let i = activeIdx + 1; i < ctx.count; i++) {\n if (ctx.stickyFn(i)) {\n nextStickyStart = ctx.startAt(i);\n break;\n }\n }\n\n // Pin the active sticky item, but don't let it push past the next sticky item\n const pinnedStart = Math.min(ctx.scrollOffset, nextStickyStart - activeSize);\n\n return [{ end: pinnedStart + activeSize, index: activeIdx, size: activeSize, start: pinnedStart }];\n}\n"],"mappings":"AAuBA,SAAgB,EAAmB,EAA0C,CAC3E,GAAI,CAAC,EAAI,UAAY,EAAI,QAAU,GAAK,EAAI,cAAgB,EAAG,MAAO,CAAC,EAGvE,IAAI,EAAY,GACZ,EAAK,EACL,EAAK,EAAI,MAAQ,EAErB,KAAO,GAAM,GAAI,CACf,IAAM,EAAO,EAAK,GAAO,EAErB,EAAI,QAAQ,CAAG,EAAI,EAAI,cACzB,EAAY,EACZ,EAAK,EAAM,GAEX,EAAK,EAAM,CAEf,CAEA,GAAI,EAAY,EAAG,MAAO,CAAC,EAG3B,IAAI,EAAY,GAEhB,IAAK,IAAI,EAAI,EAAW,GAAK,EAAG,IAC9B,GAAI,EAAI,SAAS,CAAC,EAAG,CACnB,EAAY,EACZ,KACF,CAGF,GAAI,IAAc,GAAI,MAAO,CAAC,EAG9B,IAAM,EAAa,EAAI,OAAO,CAAS,EACnC,EAAkB,IAEtB,IAAK,IAAI,EAAI,EAAY,EAAG,EAAI,EAAI,MAAO,IACzC,GAAI,EAAI,SAAS,CAAC,EAAG,CACnB,EAAkB,EAAI,QAAQ,CAAC,EAC/B,KACF,CAIF,IAAM,EAAc,KAAK,IAAI,EAAI,aAAc,EAAkB,CAAU,EAE3E,MAAO,CAAC,CAAE,IAAK,EAAc,EAAY,MAAO,EAAW,KAAM,EAAY,MAAO,CAAY,CAAC,CACnG"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared sticky item computation logic.
3
+ * Used by virtualizer and grid-virtualizer for finding and pinning sticky items.
4
+ */
5
+ import type { VirtualItem } from './_axis1d';
6
+ export interface StickyComputeContext {
7
+ count: number;
8
+ scrollOffset: number;
9
+ sizeAt: (index: number) => number;
10
+ startAt: (index: number) => number;
11
+ stickyFn: ((index: number) => boolean) | null;
12
+ }
13
+ /**
14
+ * Compute the sticky item(s) for a 1D scroll axis.
15
+ * Returns the single sticky item that should be pinned at the viewport start,
16
+ * accounting for the next sticky item's position.
17
+ *
18
+ * @param ctx Context with functions to query item positions and sizes
19
+ * @returns Array containing at most one sticky item (pinned to viewport)
20
+ */
21
+ export declare function computeStickyItems(ctx: StickyComputeContext): VirtualItem[];
22
+ //# sourceMappingURL=_sticky.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_sticky.d.ts","sourceRoot":"","sources":["../src/_sticky.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,oBAAoB,GAAG,WAAW,EAAE,CAgD3E"}
@@ -0,0 +1,2 @@
1
+ function e(e){if(!e.stickyFn||e.count===0||e.scrollOffset<=0)return[];let t=-1,n=0,r=e.count-1;for(;n<=r;){let i=n+r>>1;e.startAt(i)<e.scrollOffset?(t=i,n=i+1):r=i-1}if(t<0)return[];let i=-1;for(let n=t;n>=0;n--)if(e.stickyFn(n)){i=n;break}if(i===-1)return[];let a=e.sizeAt(i),o=1/0;for(let t=i+1;t<e.count;t++)if(e.stickyFn(t)){o=e.startAt(t);break}let s=Math.min(e.scrollOffset,o-a);return[{end:s+a,index:i,size:a,start:s}]}export{e as computeStickyItems};
2
+ //# sourceMappingURL=_sticky.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_sticky.js","names":[],"sources":["../src/_sticky.ts"],"sourcesContent":["/**\n * Shared sticky item computation logic.\n * Used by virtualizer and grid-virtualizer for finding and pinning sticky items.\n */\n\nimport type { VirtualItem } from './_axis1d';\n\nexport interface StickyComputeContext {\n count: number;\n scrollOffset: number;\n sizeAt: (index: number) => number;\n startAt: (index: number) => number;\n stickyFn: ((index: number) => boolean) | null;\n}\n\n/**\n * Compute the sticky item(s) for a 1D scroll axis.\n * Returns the single sticky item that should be pinned at the viewport start,\n * accounting for the next sticky item's position.\n *\n * @param ctx Context with functions to query item positions and sizes\n * @returns Array containing at most one sticky item (pinned to viewport)\n */\nexport function computeStickyItems(ctx: StickyComputeContext): VirtualItem[] {\n if (!ctx.stickyFn || ctx.count === 0 || ctx.scrollOffset <= 0) return [];\n\n // Binary search to find the last item that starts before the viewport\n let lastAbove = -1;\n let lo = 0;\n let hi = ctx.count - 1;\n\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n\n if (ctx.startAt(mid) < ctx.scrollOffset) {\n lastAbove = mid;\n lo = mid + 1;\n } else {\n hi = mid - 1;\n }\n }\n\n if (lastAbove < 0) return [];\n\n // Scan backwards from lastAbove to find the most recent sticky item\n let activeIdx = -1;\n\n for (let i = lastAbove; i >= 0; i--) {\n if (ctx.stickyFn(i)) {\n activeIdx = i;\n break;\n }\n }\n\n if (activeIdx === -1) return [];\n\n // Find the next sticky item to determine how far we can pin\n const activeSize = ctx.sizeAt(activeIdx);\n let nextStickyStart = Infinity;\n\n for (let i = activeIdx + 1; i < ctx.count; i++) {\n if (ctx.stickyFn(i)) {\n nextStickyStart = ctx.startAt(i);\n break;\n }\n }\n\n // Pin the active sticky item, but don't let it push past the next sticky item\n const pinnedStart = Math.min(ctx.scrollOffset, nextStickyStart - activeSize);\n\n return [{ end: pinnedStart + activeSize, index: activeIdx, size: activeSize, start: pinnedStart }];\n}\n"],"mappings":"AAuBA,SAAgB,EAAmB,EAA0C,CAC3E,GAAI,CAAC,EAAI,UAAY,EAAI,QAAU,GAAK,EAAI,cAAgB,EAAG,MAAO,CAAC,EAGvE,IAAI,EAAY,GACZ,EAAK,EACL,EAAK,EAAI,MAAQ,EAErB,KAAO,GAAM,GAAI,CACf,IAAM,EAAO,EAAK,GAAO,EAErB,EAAI,QAAQ,CAAG,EAAI,EAAI,cACzB,EAAY,EACZ,EAAK,EAAM,GAEX,EAAK,EAAM,CAEf,CAEA,GAAI,EAAY,EAAG,MAAO,CAAC,EAG3B,IAAI,EAAY,GAEhB,IAAK,IAAI,EAAI,EAAW,GAAK,EAAG,IAC9B,GAAI,EAAI,SAAS,CAAC,EAAG,CACnB,EAAY,EACZ,KACF,CAGF,GAAI,IAAc,GAAI,MAAO,CAAC,EAG9B,IAAM,EAAa,EAAI,OAAO,CAAS,EACnC,EAAkB,IAEtB,IAAK,IAAI,EAAI,EAAY,EAAG,EAAI,EAAI,MAAO,IACzC,GAAI,EAAI,SAAS,CAAC,EAAG,CACnB,EAAkB,EAAI,QAAQ,CAAC,EAC/B,KACF,CAIF,IAAM,EAAc,KAAK,IAAI,EAAI,aAAc,EAAkB,CAAU,EAE3E,MAAO,CAAC,CAAE,IAAK,EAAc,EAAY,MAAO,EAAW,KAAM,EAAY,MAAO,CAAY,CAAC,CACnG"}
@@ -1,2 +1,2 @@
1
- require("./_utils.cjs");const e=require("./errors.cjs"),t=require("./_validation.cjs"),n=require("./virtualizer.cjs");function r(){let e=new Map,t=new Map,n=[],r=!1;return{acquire(i,a){if(!r)return a();let o=e.get(i);if(o)return t.set(i,o),o;let s=n.pop()??a();return t.set(i,s),s},beginCycle(){t=new Map,r=!0},clear(){for(let t of e.values())t.remove();e.clear(),t.clear(),n.length=0,r=!1},endCycle(){if(r){r=!1;for(let[r,i]of e)t.has(r)||(i.remove(),n.push(i));e=t}}}}function i(i){if(typeof i.estimateSize==`number`&&t.requirePositiveNumber(i.estimateSize,`estimateSize`),i.gap!==void 0&&t.requireNonNegativeInteger(i.gap,`gap`),i.overscan!==void 0&&t.validateOverscan(i.overscan),i.stickToBottom!==void 0&&typeof i.stickToBottom!=`boolean`){if(i.stickToBottom===null||Array.isArray(i.stickToBottom))throw new e.ScrollConfigurationError(`stickToBottom must be a boolean or options object.`);i.stickToBottom.threshold!==void 0&&t.requireNonNegativeNumber(i.stickToBottom.threshold,`stickToBottom.threshold`)}let a=[],o=!1,s=new AbortController,c=i.listElement,l=r(),u=null;function d(e){let t=a[e];return t!==void 0&&i.getItemKey?i.getItemKey(e,t):e}function f(e){if(i.estimateSize===void 0)return 36;if(typeof i.estimateSize==`number`)return i.estimateSize;let t=a[e];return t===void 0?36:i.estimateSize(e,t)}function p(e){i.horizontal?(c.style.height=``,c.style.width=`${e}px`):(c.style.height=`${e}px`,c.style.width=``)}function m(t){let n=a[t.index];if(n===void 0)throw new e.ScrollRangeError(`toRenderItem: index ${t.index} is out of range (currentItems.length=${a.length})`);return{...t,data:n}}function h(e){p(e.totalSize),l.beginCycle();try{i.render({items:e.items.map(m),listEl:c,recycle:(e,t)=>l.acquire(e,t),stickyItems:e.stickyItems.map(m),totalSize:e.totalSize})}finally{l.endCycle()}}function g(){l.clear(),i.clear?i.clear(c):c.textContent=``,c.style.height=``,c.style.width=``,c.style.position=``,c.style.contain=``}function _(){let e=i.stickToBottom;return e?e===!0?{enabled:!0,threshold:48}:{enabled:e.enabled??!0,threshold:e.threshold??48}:{enabled:!1,threshold:48}}function v(){let e=n.createVirtualizer(i.scrollElement,{count:a.length,estimateSize:f,gap:i.gap,getItemKey:d,horizontal:i.horizontal,measurementCache:i.measurementCache,onChange:h,overscan:i.overscan??3,sticky:i.sticky?e=>{let t=a[e];return t!==void 0&&i.sticky(e,t)}:void 0});return u=e,c.style.position=`relative`,c.style.contain=`layout`,e}function y(){o||(o=!0,s.abort(),u?.dispose(),u=null,g())}return{get count(){return u?.count??0},get disposalSignal(){return s.signal},dispose:y,get disposed(){return o},invalidate(){o||u?.invalidate()},isAtEnd(e){return u?.isAtEnd(e)??!0},get isScrolling(){return u?.isScrolling??!1},get items(){return u?.items??[]},measure(e,t){o||u?.measure(e,t)},measureBatch(e){o||u?.measureBatch(e)},measureEl(e,t){return o?()=>{}:u?.measureEl(e,t)??(()=>{})},refresh(){o||u?.refresh()},get scrollOffset(){return u?.scrollOffset??0},scrollToBottom(e){o||u?.scrollToBottom(e)},scrollToIndex(e,t){o||u?.scrollToIndex(e,t)},scrollToOffset(e,t){o||u?.scrollToOffset(e,t)},scrollToTop(e){o||u?.scrollToTop(e)},setItems(e){if(o)return;let t=_(),n=t.enabled&&(u?.isAtEnd(t.threshold)??!0);if(a=e,e.length===0){u?.dispose(),u=null,g();return}if(!u){let e=v();n&&e.scrollToBottom();return}let r=e.length!==u.count;u.update({count:e.length}),r?i.getItemKey||u.invalidate():i.getItemKey?u.refresh():u.invalidate(),n&&u.scrollToBottom()},get stickyItems(){return u?.stickyItems??[]},[Symbol.dispose]:y,get totalSize(){return u?.totalSize??0}}}function a(e,t){let n=document.createElement(`div`);n.style.cssText=t.horizontal?`overflow: auto hidden; width: 100%; height: 100%;`:`overflow: hidden auto; width: 100%; height: 100%;`,t.containerClass&&(n.className=t.containerClass);let r=document.createElement(`div`);n.appendChild(r),e.appendChild(n);let a;try{a=i({...t,listElement:r,scrollElement:n})}catch(e){throw n.remove(),e}let o=a.dispose.bind(a);return Object.assign(a,{dispose(){o(),n.remove()},[Symbol.dispose](){o(),n.remove()}})}exports.createDomVirtualList=i,exports.createVirtualScroller=a;
1
+ require("./_utils.cjs");const e=require("./errors.cjs"),t=require("./_validation.cjs"),n=require("./virtualizer.cjs");function r(){let e=new Map,t=new Map,n=[],r=!1;return{acquire(i,a){if(!r)return a();let o=e.get(i);if(o)return t.set(i,o),o;let s=n.pop()??a();return t.set(i,s),s},beginCycle(){t=new Map,r=!0},clear(){for(let t of e.values())t.remove();e.clear(),t.clear(),n.length=0,r=!1},endCycle(){if(r){r=!1;for(let[r,i]of e)t.has(r)||(i.remove(),n.push(i));e=t}}}}function i(i){if(typeof i.estimateSize==`number`&&t.requirePositiveNumber(i.estimateSize,`estimateSize`),i.gap!==void 0&&t.requireNonNegativeInteger(i.gap,`gap`),i.overscan!==void 0&&t.validateOverscan(i.overscan),i.stickToBottom!==void 0&&typeof i.stickToBottom!=`boolean`){if(i.stickToBottom===null||Array.isArray(i.stickToBottom))throw new e.ScrollConfigurationError(`stickToBottom must be a boolean or options object.`);i.stickToBottom.threshold!==void 0&&t.requireNonNegativeNumber(i.stickToBottom.threshold,`stickToBottom.threshold`)}let a=[],o=!1,s=new AbortController,c=i.listElement,l=null;i.signal&&(l=i.signal({items:[],stickyItems:[],totalSize:0}));function u(e){l&&(l.value=e),_(e)}let d=r(),f=null;function p(e){let t=a[e];return t!==void 0&&i.getItemKey?i.getItemKey(e,t):e}function m(e){if(i.estimateSize===void 0)return 36;if(typeof i.estimateSize==`number`)return i.estimateSize;let t=a[e];return t===void 0?36:i.estimateSize(e,t)}function h(e){i.horizontal?(c.style.height=``,c.style.width=`${e}px`):(c.style.height=`${e}px`,c.style.width=``)}function g(t){let n=a[t.index];if(n===void 0)throw new e.ScrollRangeError(`toRenderItem: index ${t.index} is out of range (currentItems.length=${a.length})`);return{...t,data:n}}function _(e){h(e.totalSize),d.beginCycle();try{i.render({items:e.items.map(g),listEl:c,recycle:(e,t)=>d.acquire(e,t),stickyItems:e.stickyItems.map(g),totalSize:e.totalSize})}finally{d.endCycle()}}function v(){d.clear(),i.clear?i.clear(c):c.textContent=``,c.style.height=``,c.style.width=``,c.style.position=``,c.style.contain=``}function y(){let e=i.stickToBottom;return e?e===!0?{enabled:!0,threshold:48}:{enabled:e.enabled??!0,threshold:e.threshold??48}:{enabled:!1,threshold:48}}function b(){let e=n.createVirtualizer(i.scrollElement,{count:a.length,estimateSize:m,gap:i.gap,getItemKey:p,horizontal:i.horizontal,keyboardScroll:i.keyboardScroll,measurementCache:i.measurementCache,onChange:u,overscan:i.overscan??3,sticky:i.sticky?e=>{let t=a[e];return t!==void 0&&i.sticky(e,t)}:void 0});return f=e,c.style.position=`relative`,c.style.contain=`layout`,e}function x(){o||(o=!0,s.abort(),f?.dispose(),f=null,v())}return{get count(){return f?.count??0},get disposalSignal(){return s.signal},dispose:x,get disposed(){return o},invalidate(){o||f?.invalidate()},isAtEnd(e){return f?.isAtEnd(e)??!0},get isScrolling(){return f?.isScrolling??!1},get items(){return f?.items??[]},measure(e,t){o||f?.measure(e,t)},measureBatch(e){o||f?.measureBatch(e)},measureEl(e,t){return o?()=>{}:f?.measureEl(e,t)??(()=>{})},refresh(){o||f?.refresh()},get scrollOffset(){return f?.scrollOffset??0},scrollToBottom(e){o||f?.scrollToBottom(e)},scrollToIndex(e,t){o||f?.scrollToIndex(e,t)},scrollToOffset(e,t){o||f?.scrollToOffset(e,t)},scrollToTop(e){o||f?.scrollToTop(e)},setItems(e){if(o)return;let t=y(),n=t.enabled&&(f?.isAtEnd(t.threshold)??!0);if(a=e,e.length===0){f?.dispose(),f=null,v();return}if(!f){let e=b();n&&e.scrollToBottom();return}let r=e.length!==f.count;f.update({count:e.length}),r?i.getItemKey||f.invalidate():i.getItemKey?f.refresh():f.invalidate(),n&&f.scrollToBottom()},get stickyItems(){return f?.stickyItems??[]},[Symbol.dispose]:x,get totalSize(){return f?.totalSize??0}}}function a(e,t){let n=document.createElement(`div`);n.style.cssText=t.horizontal?`overflow: auto hidden; width: 100%; height: 100%;`:`overflow: hidden auto; width: 100%; height: 100%;`,t.containerClass&&(n.className=t.containerClass);let r=document.createElement(`div`);n.appendChild(r),e.appendChild(n);let a;try{a=i({...t,listElement:r,scrollElement:n})}catch(e){throw n.remove(),e}let o=a.dispose.bind(a);return Object.assign(a,{dispose(){o(),n.remove()},[Symbol.dispose](){o(),n.remove()}})}exports.createDomVirtualList=i,exports.createVirtualScroller=a;
2
2
  //# sourceMappingURL=dom-virtual-list.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dom-virtual-list.cjs","names":[],"sources":["../src/dom-virtual-list.ts"],"sourcesContent":["import { DEFAULT_ESTIMATE_SIZE, DEFAULT_OVERSCAN, type MeasurementCache, type Overscan } from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigurationError, 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 ScrollConfigurationError('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 isDestroyed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\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: handleChange,\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 (isDestroyed) return;\n\n isDestroyed = true;\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 isDestroyed;\n },\n\n invalidate() {\n if (isDestroyed) 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 (isDestroyed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (isDestroyed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (isDestroyed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (isDestroyed) return;\n\n virtualizer?.refresh();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (isDestroyed) 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 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 [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":"sHAqGA,SAAS,GAAiB,CACxB,IAAI,EAAO,IAAI,IACX,EAAW,IAAI,IACb,EAAuB,CAAC,EAC1B,EAAU,GAEd,MAAO,CACL,QAAQ,EAAiB,EAAwC,CAC/D,GAAI,CAAC,EAAS,OAAO,EAAO,EAE5B,IAAM,EAAW,EAAK,IAAI,CAAG,EAE7B,GAAI,EAGF,OAFA,EAAS,IAAI,EAAK,CAAQ,EAEnB,EAGT,IAAM,EAAO,EAAM,IAAI,GAAK,EAAO,EAInC,OAFA,EAAS,IAAI,EAAK,CAAI,EAEf,CACT,EAEA,YAAmB,CACjB,EAAW,IAAI,IACf,EAAU,EACZ,EAEA,OAAc,CACZ,IAAK,IAAM,KAAQ,EAAK,OAAO,EAAG,EAAK,OAAO,EAE9C,EAAK,MAAM,EACX,EAAS,MAAM,EACf,EAAM,OAAS,EACf,EAAU,EACZ,EAEA,UAAiB,CACV,KAEL,GAAU,GAEV,IAAK,GAAM,CAAC,EAAK,KAAS,EACnB,EAAS,IAAI,CAAG,IACnB,EAAK,OAAO,EACZ,EAAM,KAAK,CAAI,GAInB,EAAO,CATG,CAUZ,CACF,CACF,CAIA,SAAgB,EAAwB,EAAgE,CAOtG,GANI,OAAO,EAAQ,cAAiB,UAAU,EAAA,sBAAsB,EAAQ,aAAc,cAAc,EAEpG,EAAQ,MAAQ,IAAA,IAAW,EAAA,0BAA0B,EAAQ,IAAK,KAAK,EAEvE,EAAQ,WAAa,IAAA,IAAW,EAAA,iBAAiB,EAAQ,QAAQ,EAEjE,EAAQ,gBAAkB,IAAA,IAAa,OAAO,EAAQ,eAAkB,UAAW,CACrF,GAAI,EAAQ,gBAAkB,MAAQ,MAAM,QAAQ,EAAQ,aAAa,EACvE,MAAM,IAAI,EAAA,yBAAyB,oDAAoD,EAGrF,EAAQ,cAAc,YAAc,IAAA,IACtC,EAAA,yBAAyB,EAAQ,cAAc,UAAW,yBAAyB,CAEvF,CAEA,IAAI,EAAoB,CAAC,EACrB,EAAc,GACZ,EAAK,IAAI,gBACT,EAAS,EAAQ,YAIjB,EAAO,EAAe,EAGxB,EAAkC,KAEtC,SAAS,EAAW,EAA2B,CAC7C,IAAM,EAAO,EAAa,GAI1B,OAFI,IAAS,IAAA,IAAa,EAAQ,WAAmB,EAAQ,WAAW,EAAO,CAAI,EAE5E,CACT,CAEA,SAAS,EAAgB,EAAuB,CAC9C,GAAI,EAAQ,eAAiB,IAAA,GAAW,MAAA,IAExC,GAAI,OAAO,EAAQ,cAAiB,SAAU,OAAO,EAAQ,aAE7D,IAAM,EAAO,EAAa,GAE1B,OAAO,IAAS,IAAA,GAA4C,GAAhC,EAAQ,aAAa,EAAO,CAAI,CAC9D,CAEA,SAAS,EAAc,EAAyB,CAC1C,EAAQ,YACV,EAAO,MAAM,OAAS,GACtB,EAAO,MAAM,MAAQ,GAAG,EAAU,MAElC,EAAO,MAAM,OAAS,GAAG,EAAU,IACnC,EAAO,MAAM,MAAQ,GAEzB,CAMA,SAAS,EAAa,EAAuC,CAC3D,IAAM,EAAO,EAAa,EAAG,OAE7B,GAAI,IAAS,IAAA,GACX,MAAM,IAAI,EAAA,iBACR,uBAAuB,EAAG,MAAM,wCAAwC,EAAa,OAAO,EAC9F,EAGF,MAAO,CAAE,GAAG,EAAI,MAAK,CACvB,CAEA,SAAS,EAAa,EAA+B,CACnD,EAAc,EAAM,SAAS,EAE7B,EAAK,WAAW,EAIhB,GAAI,CACF,EAAQ,OAAO,CACb,MAAO,EAAM,MAAM,IAAI,CAAY,EACnC,SACA,SAAU,EAAK,IAAW,EAAK,QAAQ,EAAK,CAAM,EAClD,YAAa,EAAM,YAAY,IAAI,CAAY,EAC/C,UAAW,EAAM,SACnB,CAAC,CACH,QAAU,CACR,EAAK,SAAS,CAChB,CACF,CAEA,SAAS,GAAsB,CAC7B,EAAK,MAAM,EAEP,EAAQ,MACV,EAAQ,MAAM,CAAM,EAEpB,EAAO,YAAc,GAGvB,EAAO,MAAM,OAAS,GACtB,EAAO,MAAM,MAAQ,GACrB,EAAO,MAAM,SAAW,GACxB,EAAO,MAAM,QAAU,EACzB,CAIA,SAAS,GAAgE,CACvE,IAAM,EAAM,EAAQ,cAMpB,OAJK,EAED,IAAQ,GAAa,CAAE,QAAS,GAAM,UAAW,EAAwB,EAEtE,CAAE,QAAS,EAAI,SAAW,GAAM,UAAW,EAAI,WAAa,EAAwB,EAJ1E,CAAE,QAAS,GAAO,UAAW,EAAwB,CAKxE,CAEA,SAAS,GAAgC,CACvC,IAAM,EAAI,EAAA,kBAAkB,EAAQ,cAAe,CACjD,MAAO,EAAa,OACpB,aAAc,EACd,IAAK,EAAQ,IACb,WAAY,EACZ,WAAY,EAAQ,WACpB,iBAAkB,EAAQ,iBAC1B,SAAU,EACV,SAAU,EAAQ,UAAA,EAClB,OAAQ,EAAQ,OACX,GAAU,CACT,IAAM,EAAO,EAAa,GAE1B,OAAO,IAAS,IAAA,IAAa,EAAQ,OAAQ,EAAO,CAAI,CAC1D,EACA,IAAA,EACN,CAAC,EAMD,MAJA,GAAc,EACd,EAAO,MAAM,SAAW,WACxB,EAAO,MAAM,QAAU,SAEhB,CACT,CAEA,SAAS,GAAiB,CACpB,IAEJ,EAAc,GACd,EAAG,MAAM,EACT,GAAa,QAAQ,EACrB,EAAc,KACd,EAAc,EAChB,CAEA,MAAO,CAEL,IAAI,OAAQ,CACV,OAAO,GAAa,OAAS,CAC/B,EAEA,IAAI,gBAAiB,CACnB,OAAO,EAAG,MACZ,EAEA,QAAS,EAET,IAAI,UAAW,CACb,OAAO,CACT,EAEA,YAAa,CACP,GAEJ,GAAa,WAAW,CAC1B,EAEA,QAAQ,EAAW,CACjB,OAAO,GAAa,QAAQ,CAAS,GAAK,EAC5C,EAEA,IAAI,aAAc,CAChB,OAAO,GAAa,aAAe,EACrC,EAEA,IAAI,OAAQ,CACV,OAAO,GAAa,OAAS,CAAC,CAChC,EAEA,QAAQ,EAAO,EAAM,CACf,GAEJ,GAAa,QAAQ,EAAO,CAAI,CAClC,EAEA,aAAa,EAAS,CAChB,GAEJ,GAAa,aAAa,CAAO,CACnC,EAEA,UAAU,EAAO,EAAI,CAGnB,OAFI,MAA0B,CAAC,EAExB,GAAa,UAAU,EAAO,CAAE,QAAY,CAAC,EACtD,EAEA,SAAU,CACJ,GAEJ,GAAa,QAAQ,CACvB,EAEA,IAAI,cAAe,CACjB,OAAO,GAAa,cAAgB,CACtC,EAEA,eAAe,EAAe,CACxB,GAEJ,GAAa,eAAe,CAAa,CAC3C,EAEA,cAAc,EAAO,EAAe,CAC9B,GAEJ,GAAa,cAAc,EAAO,CAAa,CACjD,EAEA,eAAe,EAAQ,EAAe,CAChC,GAEJ,GAAa,eAAe,EAAQ,CAAa,CACnD,EAEA,YAAY,EAAe,CACrB,GAEJ,GAAa,YAAY,CAAa,CACxC,EAGA,SAAS,EAAO,CACd,GAAI,EAAa,OAIjB,IAAM,EAAQ,EAAqB,EAC7B,EAAW,EAAM,UAAY,GAAa,QAAQ,EAAM,SAAS,GAAK,IAI5E,GAFA,EAAe,EAEX,EAAM,SAAW,EAAG,CACtB,GAAa,QAAQ,EACrB,EAAc,KACd,EAAc,EAEd,MACF,CAEA,GAAI,CAAC,EAAa,CAChB,IAAM,EAAI,EAAiB,EAEvB,GAAU,EAAE,eAAe,EAE/B,MACF,CAEA,IAAM,EAAe,EAAM,SAAW,EAAY,MAIlD,EAAY,OAAO,CAAE,MAAO,EAAM,MAAO,CAAC,EAIrC,EAQO,EAAQ,YAGlB,EAAY,WAAW,EARnB,EAAQ,WACV,EAAY,QAAQ,EAEpB,EAAY,WAAW,EAQvB,GAAU,EAAY,eAAe,CAC3C,EAEA,IAAI,aAAc,CAChB,OAAO,GAAa,aAAe,CAAC,CACtC,GAEC,OAAO,SAAU,EAElB,IAAI,WAAY,CACd,OAAO,GAAa,WAAa,CACnC,CACF,CACF,CAgBA,SAAgB,EACd,EACA,EAC6B,CAC7B,IAAM,EAAW,SAAS,cAAc,KAAK,EAE7C,EAAS,MAAM,QAAU,EAAQ,WAC7B,oDACA,oDAEA,EAAQ,iBAAgB,EAAS,UAAY,EAAQ,gBAEzD,IAAM,EAAS,SAAS,cAAc,KAAK,EAE3C,EAAS,YAAY,CAAM,EAC3B,EAAU,YAAY,CAAQ,EAE9B,IAAI,EAEJ,GAAI,CACF,EAAO,EAAwB,CAC7B,GAAG,EACH,YAAa,EACb,cAAe,CACjB,CAAC,CACH,OAAS,EAAG,CAGV,MADA,EAAS,OAAO,EACV,CACR,CAIA,IAAM,EAAe,EAAK,QAAQ,KAAK,CAAI,EAE3C,OAAO,OAAO,OAAO,EAAM,CACzB,SAAU,CACR,EAAa,EACb,EAAS,OAAO,CAClB,EACA,CAAC,OAAO,UAAW,CACjB,EAAa,EACb,EAAS,OAAO,CAClB,CACF,CAAC,CACH"}
1
+ {"version":3,"file":"dom-virtual-list.cjs","names":[],"sources":["../src/dom-virtual-list.ts"],"sourcesContent":["import type { Signal } from '@vielzeug/ripple';\n\nimport { DEFAULT_ESTIMATE_SIZE, DEFAULT_OVERSCAN, type MeasurementCache, type Overscan } from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigurationError, 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 /** Enable keyboard navigation (Arrow/Page/Home/End keys). */\n keyboardScroll?: 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 /** Optional signal factory for reactive state. */\n signal?: (init: VirtualizerState) => Signal<VirtualizerState>;\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 ScrollConfigurationError('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 isDestroyed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\n\n // Optional signal for reactive state\n let stateSignal: Signal<VirtualizerState> | null = null;\n if (options.signal) {\n const initialState: VirtualizerState = { items: [], stickyItems: [], totalSize: 0 };\n stateSignal = options.signal(initialState);\n }\n\n // Helper to emit state to both callback and signal\n function emitState(state: VirtualizerState): void {\n if (stateSignal) stateSignal.value = 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 keyboardScroll: options.keyboardScroll,\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 (isDestroyed) return;\n\n isDestroyed = true;\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 isDestroyed;\n },\n\n invalidate() {\n if (isDestroyed) 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 (isDestroyed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (isDestroyed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (isDestroyed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (isDestroyed) return;\n\n virtualizer?.refresh();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (isDestroyed) 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 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 [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,3 +1,4 @@
1
+ import type { Signal } from '@vielzeug/ripple';
1
2
  import { type MeasurementCache, type Overscan } from './_utils';
2
3
  import { type ScrollToIndexOptions, type VirtualItem, type Virtualizer, type VirtualizerState, type VirtualKey } from './virtualizer';
3
4
  export type { MeasurementCache, Overscan, ScrollToIndexOptions, VirtualItem, Virtualizer, VirtualizerState, VirtualKey, };
@@ -34,6 +35,8 @@ export type DomVirtualListOptions<T> = {
34
35
  gap?: number;
35
36
  getItemKey?: (index: number, item: T) => VirtualKey;
36
37
  horizontal?: boolean;
38
+ /** Enable keyboard navigation (Arrow/Page/Home/End keys). */
39
+ keyboardScroll?: boolean;
37
40
  listElement: HTMLElement;
38
41
  /** External measurement cache for scroll restoration. */
39
42
  measurementCache?: MeasurementCache;
@@ -50,6 +53,8 @@ export type DomVirtualListOptions<T> = {
50
53
  stickToBottom?: boolean | StickToBottomOptions;
51
54
  /** Mark items as sticky headers. Receives the item index and the item data. */
52
55
  sticky?: (index: number, item: T) => boolean;
56
+ /** Optional signal factory for reactive state. */
57
+ signal?: (init: VirtualizerState) => Signal<VirtualizerState>;
53
58
  };
54
59
  /**
55
60
  * R11: Controller extends Virtualizer so all methods (scrollToIndex, refresh,
@@ -1 +1 @@
1
- {"version":3,"file":"dom-virtual-list.d.ts","sourceRoot":"","sources":["../src/dom-virtual-list.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dom-virtual-list.d.ts","sourceRoot":"","sources":["../src/dom-virtual-list.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import"./_utils.js";import{ScrollConfigurationError as e,ScrollRangeError as t}from"./errors.js";import{requireNonNegativeInteger as n,requireNonNegativeNumber as r,requirePositiveNumber as i,validateOverscan as a}from"./_validation.js";import{createVirtualizer as o}from"./virtualizer.js";function s(){let e=new Map,t=new Map,n=[],r=!1;return{acquire(i,a){if(!r)return a();let o=e.get(i);if(o)return t.set(i,o),o;let s=n.pop()??a();return t.set(i,s),s},beginCycle(){t=new Map,r=!0},clear(){for(let t of e.values())t.remove();e.clear(),t.clear(),n.length=0,r=!1},endCycle(){if(r){r=!1;for(let[r,i]of e)t.has(r)||(i.remove(),n.push(i));e=t}}}}function c(c){if(typeof c.estimateSize==`number`&&i(c.estimateSize,`estimateSize`),c.gap!==void 0&&n(c.gap,`gap`),c.overscan!==void 0&&a(c.overscan),c.stickToBottom!==void 0&&typeof c.stickToBottom!=`boolean`){if(c.stickToBottom===null||Array.isArray(c.stickToBottom))throw new e(`stickToBottom must be a boolean or options object.`);c.stickToBottom.threshold!==void 0&&r(c.stickToBottom.threshold,`stickToBottom.threshold`)}let l=[],u=!1,d=new AbortController,f=c.listElement,p=s(),m=null;function h(e){let t=l[e];return t!==void 0&&c.getItemKey?c.getItemKey(e,t):e}function g(e){if(c.estimateSize===void 0)return 36;if(typeof c.estimateSize==`number`)return c.estimateSize;let t=l[e];return t===void 0?36:c.estimateSize(e,t)}function _(e){c.horizontal?(f.style.height=``,f.style.width=`${e}px`):(f.style.height=`${e}px`,f.style.width=``)}function v(e){let n=l[e.index];if(n===void 0)throw new t(`toRenderItem: index ${e.index} is out of range (currentItems.length=${l.length})`);return{...e,data:n}}function y(e){_(e.totalSize),p.beginCycle();try{c.render({items:e.items.map(v),listEl:f,recycle:(e,t)=>p.acquire(e,t),stickyItems:e.stickyItems.map(v),totalSize:e.totalSize})}finally{p.endCycle()}}function b(){p.clear(),c.clear?c.clear(f):f.textContent=``,f.style.height=``,f.style.width=``,f.style.position=``,f.style.contain=``}function x(){let e=c.stickToBottom;return e?e===!0?{enabled:!0,threshold:48}:{enabled:e.enabled??!0,threshold:e.threshold??48}:{enabled:!1,threshold:48}}function S(){let e=o(c.scrollElement,{count:l.length,estimateSize:g,gap:c.gap,getItemKey:h,horizontal:c.horizontal,measurementCache:c.measurementCache,onChange:y,overscan:c.overscan??3,sticky:c.sticky?e=>{let t=l[e];return t!==void 0&&c.sticky(e,t)}:void 0});return m=e,f.style.position=`relative`,f.style.contain=`layout`,e}function C(){u||(u=!0,d.abort(),m?.dispose(),m=null,b())}return{get count(){return m?.count??0},get disposalSignal(){return d.signal},dispose:C,get disposed(){return u},invalidate(){u||m?.invalidate()},isAtEnd(e){return m?.isAtEnd(e)??!0},get isScrolling(){return m?.isScrolling??!1},get items(){return m?.items??[]},measure(e,t){u||m?.measure(e,t)},measureBatch(e){u||m?.measureBatch(e)},measureEl(e,t){return u?()=>{}:m?.measureEl(e,t)??(()=>{})},refresh(){u||m?.refresh()},get scrollOffset(){return m?.scrollOffset??0},scrollToBottom(e){u||m?.scrollToBottom(e)},scrollToIndex(e,t){u||m?.scrollToIndex(e,t)},scrollToOffset(e,t){u||m?.scrollToOffset(e,t)},scrollToTop(e){u||m?.scrollToTop(e)},setItems(e){if(u)return;let t=x(),n=t.enabled&&(m?.isAtEnd(t.threshold)??!0);if(l=e,e.length===0){m?.dispose(),m=null,b();return}if(!m){let e=S();n&&e.scrollToBottom();return}let r=e.length!==m.count;m.update({count:e.length}),r?c.getItemKey||m.invalidate():c.getItemKey?m.refresh():m.invalidate(),n&&m.scrollToBottom()},get stickyItems(){return m?.stickyItems??[]},[Symbol.dispose]:C,get totalSize(){return m?.totalSize??0}}}function l(e,t){let n=document.createElement(`div`);n.style.cssText=t.horizontal?`overflow: auto hidden; width: 100%; height: 100%;`:`overflow: hidden auto; width: 100%; height: 100%;`,t.containerClass&&(n.className=t.containerClass);let r=document.createElement(`div`);n.appendChild(r),e.appendChild(n);let i;try{i=c({...t,listElement:r,scrollElement:n})}catch(e){throw n.remove(),e}let a=i.dispose.bind(i);return Object.assign(i,{dispose(){a(),n.remove()},[Symbol.dispose](){a(),n.remove()}})}export{c as createDomVirtualList,l as createVirtualScroller};
1
+ import"./_utils.js";import{ScrollConfigurationError as e,ScrollRangeError as t}from"./errors.js";import{requireNonNegativeInteger as n,requireNonNegativeNumber as r,requirePositiveNumber as i,validateOverscan as a}from"./_validation.js";import{createVirtualizer as o}from"./virtualizer.js";function s(){let e=new Map,t=new Map,n=[],r=!1;return{acquire(i,a){if(!r)return a();let o=e.get(i);if(o)return t.set(i,o),o;let s=n.pop()??a();return t.set(i,s),s},beginCycle(){t=new Map,r=!0},clear(){for(let t of e.values())t.remove();e.clear(),t.clear(),n.length=0,r=!1},endCycle(){if(r){r=!1;for(let[r,i]of e)t.has(r)||(i.remove(),n.push(i));e=t}}}}function c(c){if(typeof c.estimateSize==`number`&&i(c.estimateSize,`estimateSize`),c.gap!==void 0&&n(c.gap,`gap`),c.overscan!==void 0&&a(c.overscan),c.stickToBottom!==void 0&&typeof c.stickToBottom!=`boolean`){if(c.stickToBottom===null||Array.isArray(c.stickToBottom))throw new e(`stickToBottom must be a boolean or options object.`);c.stickToBottom.threshold!==void 0&&r(c.stickToBottom.threshold,`stickToBottom.threshold`)}let l=[],u=!1,d=new AbortController,f=c.listElement,p=null;c.signal&&(p=c.signal({items:[],stickyItems:[],totalSize:0}));function m(e){p&&(p.value=e),x(e)}let h=s(),g=null;function _(e){let t=l[e];return t!==void 0&&c.getItemKey?c.getItemKey(e,t):e}function v(e){if(c.estimateSize===void 0)return 36;if(typeof c.estimateSize==`number`)return c.estimateSize;let t=l[e];return t===void 0?36:c.estimateSize(e,t)}function y(e){c.horizontal?(f.style.height=``,f.style.width=`${e}px`):(f.style.height=`${e}px`,f.style.width=``)}function b(e){let n=l[e.index];if(n===void 0)throw new t(`toRenderItem: index ${e.index} is out of range (currentItems.length=${l.length})`);return{...e,data:n}}function x(e){y(e.totalSize),h.beginCycle();try{c.render({items:e.items.map(b),listEl:f,recycle:(e,t)=>h.acquire(e,t),stickyItems:e.stickyItems.map(b),totalSize:e.totalSize})}finally{h.endCycle()}}function S(){h.clear(),c.clear?c.clear(f):f.textContent=``,f.style.height=``,f.style.width=``,f.style.position=``,f.style.contain=``}function C(){let e=c.stickToBottom;return e?e===!0?{enabled:!0,threshold:48}:{enabled:e.enabled??!0,threshold:e.threshold??48}:{enabled:!1,threshold:48}}function w(){let e=o(c.scrollElement,{count:l.length,estimateSize:v,gap:c.gap,getItemKey:_,horizontal:c.horizontal,keyboardScroll:c.keyboardScroll,measurementCache:c.measurementCache,onChange:m,overscan:c.overscan??3,sticky:c.sticky?e=>{let t=l[e];return t!==void 0&&c.sticky(e,t)}:void 0});return g=e,f.style.position=`relative`,f.style.contain=`layout`,e}function T(){u||(u=!0,d.abort(),g?.dispose(),g=null,S())}return{get count(){return g?.count??0},get disposalSignal(){return d.signal},dispose:T,get disposed(){return u},invalidate(){u||g?.invalidate()},isAtEnd(e){return g?.isAtEnd(e)??!0},get isScrolling(){return g?.isScrolling??!1},get items(){return g?.items??[]},measure(e,t){u||g?.measure(e,t)},measureBatch(e){u||g?.measureBatch(e)},measureEl(e,t){return u?()=>{}:g?.measureEl(e,t)??(()=>{})},refresh(){u||g?.refresh()},get scrollOffset(){return g?.scrollOffset??0},scrollToBottom(e){u||g?.scrollToBottom(e)},scrollToIndex(e,t){u||g?.scrollToIndex(e,t)},scrollToOffset(e,t){u||g?.scrollToOffset(e,t)},scrollToTop(e){u||g?.scrollToTop(e)},setItems(e){if(u)return;let t=C(),n=t.enabled&&(g?.isAtEnd(t.threshold)??!0);if(l=e,e.length===0){g?.dispose(),g=null,S();return}if(!g){let e=w();n&&e.scrollToBottom();return}let r=e.length!==g.count;g.update({count:e.length}),r?c.getItemKey||g.invalidate():c.getItemKey?g.refresh():g.invalidate(),n&&g.scrollToBottom()},get stickyItems(){return g?.stickyItems??[]},[Symbol.dispose]:T,get totalSize(){return g?.totalSize??0}}}function l(e,t){let n=document.createElement(`div`);n.style.cssText=t.horizontal?`overflow: auto hidden; width: 100%; height: 100%;`:`overflow: hidden auto; width: 100%; height: 100%;`,t.containerClass&&(n.className=t.containerClass);let r=document.createElement(`div`);n.appendChild(r),e.appendChild(n);let i;try{i=c({...t,listElement:r,scrollElement:n})}catch(e){throw n.remove(),e}let a=i.dispose.bind(i);return Object.assign(i,{dispose(){a(),n.remove()},[Symbol.dispose](){a(),n.remove()}})}export{c as createDomVirtualList,l as createVirtualScroller};
2
2
  //# sourceMappingURL=dom-virtual-list.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dom-virtual-list.js","names":[],"sources":["../src/dom-virtual-list.ts"],"sourcesContent":["import { DEFAULT_ESTIMATE_SIZE, DEFAULT_OVERSCAN, type MeasurementCache, type Overscan } from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigurationError, 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 ScrollConfigurationError('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 isDestroyed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\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: handleChange,\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 (isDestroyed) return;\n\n isDestroyed = true;\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 isDestroyed;\n },\n\n invalidate() {\n if (isDestroyed) 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 (isDestroyed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (isDestroyed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (isDestroyed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (isDestroyed) return;\n\n virtualizer?.refresh();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (isDestroyed) 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 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 [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 type { Signal } from '@vielzeug/ripple';\n\nimport { DEFAULT_ESTIMATE_SIZE, DEFAULT_OVERSCAN, type MeasurementCache, type Overscan } from './_utils';\nimport {\n requireNonNegativeInteger,\n requireNonNegativeNumber,\n requirePositiveNumber,\n validateOverscan,\n} from './_validation';\nimport { ScrollConfigurationError, 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 /** Enable keyboard navigation (Arrow/Page/Home/End keys). */\n keyboardScroll?: 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 /** Optional signal factory for reactive state. */\n signal?: (init: VirtualizerState) => Signal<VirtualizerState>;\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 ScrollConfigurationError('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 isDestroyed = false;\n const ac = new AbortController();\n const listEl = options.listElement;\n\n // Optional signal for reactive state\n let stateSignal: Signal<VirtualizerState> | null = null;\n if (options.signal) {\n const initialState: VirtualizerState = { items: [], stickyItems: [], totalSize: 0 };\n stateSignal = options.signal(initialState);\n }\n\n // Helper to emit state to both callback and signal\n function emitState(state: VirtualizerState): void {\n if (stateSignal) stateSignal.value = 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 keyboardScroll: options.keyboardScroll,\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 (isDestroyed) return;\n\n isDestroyed = true;\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 isDestroyed;\n },\n\n invalidate() {\n if (isDestroyed) 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 (isDestroyed) return;\n\n virtualizer?.measure(index, size);\n },\n\n measureBatch(entries) {\n if (isDestroyed) return;\n\n virtualizer?.measureBatch(entries);\n },\n\n measureEl(index, el) {\n if (isDestroyed) return () => {};\n\n return virtualizer?.measureEl(index, el) ?? (() => {});\n },\n\n refresh() {\n if (isDestroyed) return;\n\n virtualizer?.refresh();\n },\n\n get scrollOffset() {\n return virtualizer?.scrollOffset ?? 0;\n },\n\n scrollToBottom(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToBottom(scrollOptions);\n },\n\n scrollToIndex(index, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n\n scrollToOffset(offset, scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToOffset(offset, scrollOptions);\n },\n\n scrollToTop(scrollOptions) {\n if (isDestroyed) return;\n\n virtualizer?.scrollToTop(scrollOptions);\n },\n\n // ── DomVirtualList-specific ────────────────────────────────────────────\n setItems(items) {\n if (isDestroyed) 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 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 [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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"}