@vielzeug/virtualit 2.0.0 → 3.0.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@vielzeug/virtualit)](https://www.npmjs.com/package/@vielzeug/virtualit) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- **Virtualit** renders only the items visible in the viewport plus a configurable overscan buffer. It uses a `ResizeObserver` for automatic container remeasurement and a passive `scroll` listener to keep the visible window in sync — no framework required.
7
+ **Virtualit** renders only the items visible in the viewport plus a configurable overscan buffer. It uses a `ResizeObserver` for container size changes and a passive `scroll` listener to keep the visible window in sync — no framework required.
8
8
 
9
9
  ## Installation
10
10
 
@@ -30,7 +30,7 @@ const virt = createVirtualizer(scrollEl, {
30
30
 
31
31
  for (const item of virtualItems) {
32
32
  const row = document.createElement('div');
33
- row.style.cssText = `position:absolute;top:${item.top}px;left:0;right:0;height:${item.height}px;`;
33
+ row.style.cssText = `position:absolute;top:${item.start}px;left:0;right:0;height:${item.size}px;`;
34
34
  row.textContent = items[item.index].label;
35
35
  list.appendChild(row);
36
36
  }
@@ -44,11 +44,14 @@ virt.destroy();
44
44
  ## Features
45
45
 
46
46
  - ✅ **Framework-agnostic** — callback-based `onChange`; works with React, Vue, Svelte, Lit, or vanilla DOM
47
- - ✅ **Fixed and variable heights** — pass a number or a per-index estimator; call `measureElement()` for exact heights
48
- - ✅ **Batched measurements** — `measureElement()` calls within a single tick are coalesced into one rebuild via `queueMicrotask`
47
+ - ✅ **Fixed and variable sizes** — pass a number or a per-index estimator; call `measure()` for exact size capture
48
+ - ✅ **Batched measurements** — measurement calls within a single tick are coalesced into one rebuild via `queueMicrotask`
49
+ - ✅ **Stable-key reflow** — `refresh()` rebuilds offsets after reorder/filter changes without discarding measured sizes
49
50
  - ✅ **Skipped re-renders** — `onChange` is not fired when a scroll event doesn't cross an item boundary
50
- - ✅ **Programmatic scrolling** — `scrollToIndex()` with `start`, `end`, `center`, and `auto` alignment; `scrollToOffset()` for pixel-level control; both support `behavior: 'smooth'`
51
- - ✅ **Reactive count and density** — `count` and `estimateSize` setters rebuild and re-render automatically
51
+ - ✅ **Programmatic scrolling** — `scrollToIndex()` with `start`, `end`, `center`, and `auto` alignment; `scrollToOffset()` for pixel-level control; both support `behavior: 'smooth'`; variable-height lists use current estimates until rows are measured
52
+ - ✅ **Atomic updates** — `update()` can change count, estimator, overscan, callbacks, and scroll behavior config together
53
+ - ✅ **Horizontal and window targets** — supports horizontal virtualization and `window` as scroll target
54
+ - ✅ **Asymmetric overscan and gaps** — control start/end overscan independently and set inter-item spacing
52
55
  - ✅ **Typed Float64Array offsets** — dense contiguous buffer for cache-friendly binary search
53
56
  - ✅ **Disposable** — implements `[Symbol.dispose]` for `using` declarations
54
57
  - ✅ **Zero dependencies**
@@ -67,14 +70,15 @@ const list = document.getElementById('list')!;
67
70
  const virt = createVirtualizer(scrollEl, {
68
71
  count: 10_000,
69
72
  estimateSize: 36,
70
- overscan: 3,
73
+ overscan: { start: 3, end: 3 },
71
74
  onChange: (virtualItems, totalSize) => {
72
75
  spacer.style.height = `${totalSize}px`;
73
76
  list.innerHTML = '';
74
77
 
75
78
  for (const item of virtualItems) {
76
79
  const el = document.createElement('div');
77
- el.style.cssText = `position:absolute;top:${item.top}px;`;
80
+ el.dataset.index = String(item.index);
81
+ el.style.cssText = `position:absolute;top:${item.start}px;`;
78
82
  el.textContent = `Row ${item.index}`;
79
83
  list.appendChild(el);
80
84
  }
@@ -94,7 +98,7 @@ const virt = createVirtualizer(scrollEl, {
94
98
  // After rendering, report the actual measured heights
95
99
  for (const item of virtualItems) {
96
100
  const el = list.querySelector(`[data-index="${item.index}"]`) as HTMLElement | null;
97
- if (el) virt.measureElement(item.index, el.offsetHeight);
101
+ if (el) virt.measure(item.index, el.offsetHeight);
98
102
  }
99
103
  },
100
104
  });
@@ -113,14 +117,21 @@ virt.scrollToIndex(50, { align: 'start', behavior: 'smooth' });
113
117
  virt.scrollToOffset(1440, { behavior: 'smooth' });
114
118
  ```
115
119
 
120
+ For variable-height lists, `scrollToIndex()` uses the current estimate/measured cache. If row sizes changed materially, call `invalidate()` and then scroll again.
121
+
122
+ If the same logical rows were reordered or filtered while keeping stable keys, call `refresh()` to rebuild offsets without dropping measured sizes.
123
+
116
124
  ### Updating the List
117
125
 
118
126
  ```ts
119
- // Append more items — setter rebuilds and re-renders automatically
120
- virt.count = newItems.length;
127
+ // Append more items
128
+ virt.update({ count: newItems.length });
121
129
 
122
130
  // Switch row density (e.g. compact ↔ comfortable view)
123
- virt.estimateSize = isDense ? 32 : 48;
131
+ virt.update({ estimateSize: isDense ? 32 : 48 });
132
+
133
+ // Rebuild after reordering stable-key rows
134
+ virt.refresh();
124
135
 
125
136
  // Recompute after a font swap or layout shift
126
137
  virt.invalidate();
@@ -140,47 +151,55 @@ virt.invalidate();
140
151
  ### Package Exports
141
152
 
142
153
  ```ts
143
- export { Virtualizer, createVirtualizer } from '@vielzeug/virtualit';
144
- export type { ScrollToIndexOptions, VirtualItem, VirtualizerOptions } from '@vielzeug/virtualit';
154
+ export { createVirtualizer } from '@vielzeug/virtualit';
155
+ export type {
156
+ Overscan,
157
+ ScrollToIndexOptions,
158
+ VirtualItem,
159
+ Virtualizer,
160
+ VirtualizerOptions,
161
+ VirtualizerUpdateOptions,
162
+ } from '@vielzeug/virtualit';
145
163
  ```
146
164
 
147
165
  ### `createVirtualizer(el, options)`
148
166
 
149
167
  Creates and immediately attaches a `Virtualizer` to `el`.
150
168
 
151
- | Parameter | Type | Description |
152
- | ---------------------- | --------------------------------------------------- | ------------------------------------------------------ |
153
- | `el` | `HTMLElement` | The scroll container to observe |
154
- | `options.count` | `number` | Total number of items |
155
- | `options.estimateSize` | `number \| (i: number) => number` | Row height estimate. Default: `36` |
156
- | `options.overscan` | `number` | Items to render beyond the viewport edge. Default: `3` |
157
- | `options.onChange` | `(items: VirtualItem[], totalSize: number) => void` | Called whenever the visible range changes |
169
+ - `el`: `HTMLElement | Window` — the scroll target to observe.
170
+ - `options.count`: `number` total number of items.
171
+ - `options.estimateSize`: `number | (i: number) => number` row size estimate (default: `36`).
172
+ - `options.gap`: `number` gap in px inserted between items.
173
+ - `options.overscan`: `{ start?: number; end?: number }` items rendered beyond viewport edge (default: `{ start: 3, end: 3 }`).
174
+ - `options.onChange`: `(items: VirtualItem[], totalSize: number) => void` called whenever the visible range changes.
158
175
 
159
176
  Returns a `Virtualizer` instance.
160
177
 
161
178
  ### `Virtualizer`
162
179
 
163
- | Member | Type | Description |
164
- | --------------------------- | ----------------------------------------- | -------------------------------------------------- |
165
- | `count` | `get/set number` | Item count; setting rebuilds and re-renders |
166
- | `estimateSize` | `set number \| (i) => number` | Update the size estimator; clears measured heights |
167
- | `attach(el)` | `(el: HTMLElement) => void` | Attach (or re-attach) to a scroll container |
168
- | `destroy()` | `() => void` | Remove all listeners; idempotent |
169
- | `[Symbol.dispose]()` | | Delegates to `destroy()` |
170
- | `getVirtualItems()` | `() => VirtualItem[]` | Currently rendered items |
171
- | `getTotalSize()` | `() => number` | Total scrollable height in px |
172
- | `measureElement(i, h)` | `(index: number, height: number) => void` | Record an exact item height; batched per microtask |
173
- | `scrollToIndex(i, opts?)` | `(index, ScrollToIndexOptions) => void` | Scroll to an item |
174
- | `scrollToOffset(px, opts?)` | `(offset, { behavior? }) => void` | Scroll to a pixel offset |
175
- | `invalidate()` | `() => void` | Clear all measured heights and re-render |
180
+ - `count`: `readonly number` — current item count.
181
+ - `estimateSize`: `readonly number | (i) => number` — active estimator.
182
+ - `update(next)`: `(next: VirtualizerUpdateOptions) => void` atomically update options.
183
+ - `destroy()`: `() => void` remove all listeners; idempotent.
184
+ - `[Symbol.dispose]()` delegates to `destroy()`.
185
+ - `items`: `readonly VirtualItem[]` currently rendered items.
186
+ - `totalSize`: `readonly number` total scrollable size in px.
187
+ - `scrollOffset`: `readonly number` current scroll offset.
188
+ - `isScrolling`: `readonly boolean` true while in active scrolling window.
189
+ - `measure(i, h)`: `(index: number, size: number) => void` record exact item size; batched per microtask.
190
+ - `refresh()`: `() => void` rebuild offsets and re-render while keeping measured sizes.
191
+ - `scrollToIndex(i, opts?)`: `(index, ScrollToIndexOptions) => void` scroll to an item.
192
+ - `scrollToOffset(px, opts?)`: `(offset, { behavior? }) => void` scroll to a pixel offset.
193
+ - `invalidate()`: `() => void` — clear measured sizes and re-render.
176
194
 
177
195
  ### `VirtualItem`
178
196
 
179
197
  ```ts
180
198
  interface VirtualItem {
181
- index: number; // Position in the full list
182
- top: number; // Pixel offset from the top of the scroll area
183
- height: number; // Measured or estimated height
199
+ index: number;
200
+ start: number;
201
+ end: number;
202
+ size: number;
184
203
  }
185
204
  ```
186
205
 
@@ -188,11 +207,9 @@ interface VirtualItem {
188
207
 
189
208
  Full docs at **[vielzeug.dev/virtualit](https://vielzeug.dev/virtualit)**
190
209
 
191
- | | |
192
- |---|---|
193
- | [Usage Guide](https://vielzeug.dev/virtualit/usage) | Fixed/variable heights, overscan, scrolling |
194
- | [API Reference](https://vielzeug.dev/virtualit/api) | Complete type signatures |
195
- | [Examples](https://vielzeug.dev/virtualit/examples) | Real-world virtual list patterns |
210
+ - [Usage Guide](https://vielzeug.dev/virtualit/usage) — fixed/variable heights, overscan, scrolling.
211
+ - [API Reference](https://vielzeug.dev/virtualit/api) — complete type signatures.
212
+ - [Examples](https://vielzeug.dev/virtualit/examples) real-world virtual list patterns.
196
213
 
197
214
  ## License
198
215
 
package/dist/dom/dom.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require(`../virtualit.cjs`);function t(t){let n=[],r=null,i=null,a=null,o=e=>typeof t.estimateSize==`number`?t.estimateSize:t.estimateSize(e,n[e]),s=()=>{!r||!a||(r.style.height=`${a.getTotalSize()}px`,r.style.position=`relative`,r.style.contain=`layout`)},c=()=>{r&&(t.clear(r),r.style.height=``,r.style.position=``,r.style.contain=``)},l=()=>{let l=t.getScrollElement(),u=t.getListElement();if(!l||!u||n.length===0){a?.destroy(),a=null,r=u,i=l,c();return}let d=i!==l||r!==u;r=u,i=l,!a||d?(a?.destroy(),a=e.createVirtualizer(i,{count:n.length,estimateSize:o,onChange:e=>{r&&t.render({items:n,listEl:r,virtualItems:e})},overscan:t.overscan??3})):(a.count=n.length,a.invalidate()),s()};return{destroy(){a?.destroy(),a=null,c()},scrollToIndex(e,t){a?.scrollToIndex(e,t)},update(e,o){if(n=e,!o||n.length===0){a?.destroy(),a=null,r=t.getListElement(),i=t.getScrollElement(),c();return}l()}}}exports.createDomVirtualList=t;
1
+ const e=require(`../virtualit.cjs`);var t=36,n=3;function r(r){let i=[],a=!0,o=null,s=null,c=null,l=0,u=e=>{let t=i[e];return t&&r.getItemKey?r.getItemKey(e,t):`${l}:${e}`},d=e=>{if(typeof r.estimateSize==`number`)return r.estimateSize;let n=i[e];return n?r.estimateSize(e,n):t},f=(e=o)=>{e&&(r.clear?r.clear(e):e.textContent=``,e.style.height=``,e.style.width=``,e.style.position=``,e.style.contain=``)},p=e=>{o&&(r.horizontal?(o.style.width=`${e}px`,o.style.height=``):(o.style.height=`${e}px`,o.style.width=``))},m=(e,t)=>{o&&(p(t),r.render({items:i,listEl:o,totalSize:t,virtualItems:e}))},h=t=>{let l=r.getScrollElement(),p=r.getListElement(),h=o;if(!a||!l||!p||i.length===0){c?.destroy(),c=null,o=p,s=l,f();return}let g=s!==l||o!==p;if(o=p,s=l,!c||g){let t=!!c&&g;c?.destroy(),t&&f(h),c=e.createVirtualizer(s,{count:i.length,estimateSize:d,gap:r.gap,getItemKey:u,horizontal:r.horizontal,onChange:(e,t)=>m(e,t),overscan:r.overscan??{end:n,start:n}}),o.style.position=`relative`,o.style.contain=`layout`;return}if(c.update({count:i.length,estimateSize:d,gap:r.gap,getItemKey:u,overscan:r.overscan}),t===`invalidate`){c.invalidate();return}t===`refresh`&&c.refresh()};return{destroy(){c?.destroy(),c=null,f()},invalidate(){c?.invalidate()},measure(e,t){c?.measure(e,t)},scrollToIndex(e,t){c?.scrollToIndex(e,t)},setActive(e){a=e,h(null)},setItems(e){if(i=e,r.getItemKey){h(`refresh`);return}l+=1,h(`invalidate`)}}}exports.createDomVirtualList=r;
2
2
  //# sourceMappingURL=dom.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dom.cjs","names":[],"sources":["../../src/dom/dom.ts"],"sourcesContent":["import { type ScrollToIndexOptions, type VirtualItem, type Virtualizer, createVirtualizer } from '../virtualit';\n\nexport * from '../virtualit';\n\nexport type DomVirtualListRenderArgs<T> = {\n items: T[];\n listEl: HTMLElement;\n virtualItems: VirtualItem[];\n};\n\nexport type DomVirtualListOptions<T> = {\n clear: (listEl: HTMLElement) => void;\n estimateSize: number | ((index: number, item: T) => number);\n getListElement: () => HTMLElement | null;\n getScrollElement: () => HTMLElement | null;\n overscan?: number;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n};\n\nexport type DomVirtualListController<T> = {\n destroy: () => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n update: (items: T[], enabled: boolean) => void;\n};\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n let currentItems: T[] = [];\n let listElRef: HTMLElement | null = null;\n let scrollElRef: HTMLElement | null = null;\n let virtualizer: Virtualizer | null = null;\n\n const resolveEstimate = (index: number): number => {\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n return options.estimateSize(index, currentItems[index]!);\n };\n\n const applyListStyles = () => {\n if (!listElRef || !virtualizer) return;\n\n listElRef.style.height = `${virtualizer.getTotalSize()}px`;\n listElRef.style.position = 'relative';\n listElRef.style.contain = 'layout';\n };\n\n const clearAndReset = () => {\n if (!listElRef) return;\n\n options.clear(listElRef);\n listElRef.style.height = '';\n listElRef.style.position = '';\n listElRef.style.contain = '';\n };\n\n const ensureVirtualizer = () => {\n const nextScroll = options.getScrollElement();\n const nextList = options.getListElement();\n\n if (!nextScroll || !nextList || currentItems.length === 0) {\n virtualizer?.destroy();\n virtualizer = null;\n listElRef = nextList;\n scrollElRef = nextScroll;\n clearAndReset();\n\n return;\n }\n\n const targetChanged = scrollElRef !== nextScroll || listElRef !== nextList;\n\n listElRef = nextList;\n scrollElRef = nextScroll;\n\n if (!virtualizer || targetChanged) {\n virtualizer?.destroy();\n virtualizer = createVirtualizer(scrollElRef, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n onChange: (virtualItems) => {\n if (!listElRef) return;\n\n options.render({ items: currentItems, listEl: listElRef, virtualItems });\n },\n overscan: options.overscan ?? 3,\n });\n } else {\n virtualizer.count = currentItems.length;\n virtualizer.invalidate();\n }\n\n applyListStyles();\n };\n\n return {\n destroy() {\n virtualizer?.destroy();\n virtualizer = null;\n clearAndReset();\n },\n scrollToIndex(index, scrollOptions) {\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n update(items, enabled) {\n currentItems = items;\n\n if (!enabled || currentItems.length === 0) {\n virtualizer?.destroy();\n virtualizer = null;\n listElRef = options.getListElement();\n scrollElRef = options.getScrollElement();\n clearAndReset();\n\n return;\n }\n\n ensureVirtualizer();\n },\n };\n}\n"],"mappings":"oCAyBA,SAAgB,EAAwB,EAAgE,CACtG,IAAI,EAAoB,EAAE,CACtB,EAAgC,KAChC,EAAkC,KAClC,EAAkC,KAEhC,EAAmB,GACnB,OAAO,EAAQ,cAAiB,SAAiB,EAAQ,aAEtD,EAAQ,aAAa,EAAO,EAAa,GAAQ,CAGpD,MAAwB,CACxB,CAAC,GAAa,CAAC,IAEnB,EAAU,MAAM,OAAS,GAAG,EAAY,cAAc,CAAC,IACvD,EAAU,MAAM,SAAW,WAC3B,EAAU,MAAM,QAAU,WAGtB,MAAsB,CACrB,IAEL,EAAQ,MAAM,EAAU,CACxB,EAAU,MAAM,OAAS,GACzB,EAAU,MAAM,SAAW,GAC3B,EAAU,MAAM,QAAU,KAGtB,MAA0B,CAC9B,IAAM,EAAa,EAAQ,kBAAkB,CACvC,EAAW,EAAQ,gBAAgB,CAEzC,GAAI,CAAC,GAAc,CAAC,GAAY,EAAa,SAAW,EAAG,CACzD,GAAa,SAAS,CACtB,EAAc,KACd,EAAY,EACZ,EAAc,EACd,GAAe,CAEf,OAGF,IAAM,EAAgB,IAAgB,GAAc,IAAc,EAElE,EAAY,EACZ,EAAc,EAEV,CAAC,GAAe,GAClB,GAAa,SAAS,CACtB,EAAc,EAAA,kBAAkB,EAAa,CAC3C,MAAO,EAAa,OACpB,aAAc,EACd,SAAW,GAAiB,CACrB,GAEL,EAAQ,OAAO,CAAE,MAAO,EAAc,OAAQ,EAAW,eAAc,CAAC,EAE1E,SAAU,EAAQ,UAAY,EAC/B,CAAC,GAEF,EAAY,MAAQ,EAAa,OACjC,EAAY,YAAY,EAG1B,GAAiB,EAGnB,MAAO,CACL,SAAU,CACR,GAAa,SAAS,CACtB,EAAc,KACd,GAAe,EAEjB,cAAc,EAAO,EAAe,CAClC,GAAa,cAAc,EAAO,EAAc,EAElD,OAAO,EAAO,EAAS,CAGrB,GAFA,EAAe,EAEX,CAAC,GAAW,EAAa,SAAW,EAAG,CACzC,GAAa,SAAS,CACtB,EAAc,KACd,EAAY,EAAQ,gBAAgB,CACpC,EAAc,EAAQ,kBAAkB,CACxC,GAAe,CAEf,OAGF,GAAmB,EAEtB"}
1
+ {"version":3,"file":"dom.cjs","names":[],"sources":["../../src/dom/dom.ts"],"sourcesContent":["import {\n type Overscan,\n type ScrollToIndexOptions,\n type VirtualItem,\n type VirtualKey,\n type Virtualizer,\n createVirtualizer,\n} from '../virtualit';\n\nexport * from '../virtualit';\n\nconst DEFAULT_ESTIMATE_SIZE = 36;\nconst DEFAULT_OVERSCAN = 3;\n\nexport type DomVirtualListRenderArgs<T> = {\n items: T[];\n listEl: HTMLElement;\n totalSize: number;\n virtualItems: VirtualItem[];\n};\n\nexport type DomVirtualListOptions<T> = {\n clear?: (listEl: HTMLElement) => void;\n estimateSize: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n getListElement: () => HTMLElement | null;\n getScrollElement: () => HTMLElement | Window | null;\n horizontal?: boolean;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n};\n\nexport type DomVirtualListController<T> = {\n destroy: () => void;\n invalidate: () => void;\n measure: (index: number, size: number) => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n setActive: (active: boolean) => void;\n setItems: (items: T[]) => void;\n};\n\ntype MeasurementSyncMode = 'invalidate' | 'refresh' | null;\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n let currentItems: T[] = [];\n let isActive = true;\n let listElRef: HTMLElement | null = null;\n let scrollElRef: HTMLElement | Window | null = null;\n let virtualizer: Virtualizer | null = null;\n let itemKeyRevision = 0;\n\n const resolveItemKey = (index: number): VirtualKey => {\n const item = currentItems[index];\n\n if (item && options.getItemKey) return options.getItemKey(index, item);\n\n return `${itemKeyRevision}:${index}`;\n };\n\n const resolveEstimate = (index: number): number => {\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n const item = currentItems[index];\n\n if (!item) return DEFAULT_ESTIMATE_SIZE;\n\n return options.estimateSize(index, item);\n };\n\n const clearAndReset = (listEl: HTMLElement | null = listElRef) => {\n if (!listEl) return;\n\n if (options.clear) options.clear(listEl);\n else listEl.textContent = '';\n\n listEl.style.height = '';\n listEl.style.width = '';\n listEl.style.position = '';\n listEl.style.contain = '';\n };\n\n const applyListSize = (totalSize: number) => {\n if (!listElRef) return;\n\n if (options.horizontal) {\n listElRef.style.width = `${totalSize}px`;\n listElRef.style.height = '';\n } else {\n listElRef.style.height = `${totalSize}px`;\n listElRef.style.width = '';\n }\n };\n\n const renderFromChange = (virtualItems: VirtualItem[], totalSize: number) => {\n if (!listElRef) return;\n\n applyListSize(totalSize);\n options.render({\n items: currentItems,\n listEl: listElRef,\n totalSize,\n virtualItems,\n });\n };\n\n const syncVirtualizer = (measurementSync: MeasurementSyncMode) => {\n const nextScroll = options.getScrollElement();\n const nextList = options.getListElement();\n const previousList = listElRef;\n\n if (!isActive || !nextScroll || !nextList || currentItems.length === 0) {\n virtualizer?.destroy();\n virtualizer = null;\n listElRef = nextList;\n scrollElRef = nextScroll;\n clearAndReset();\n\n return;\n }\n\n const targetChanged = scrollElRef !== nextScroll || listElRef !== nextList;\n\n listElRef = nextList;\n scrollElRef = nextScroll;\n\n if (!virtualizer || targetChanged) {\n const shouldClearForTargetSwap = !!virtualizer && targetChanged;\n\n virtualizer?.destroy();\n\n if (shouldClearForTargetSwap) clearAndReset(previousList);\n\n virtualizer = createVirtualizer(scrollElRef, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveItemKey,\n horizontal: options.horizontal,\n onChange: (virtualItems, totalSize) => renderFromChange(virtualItems, totalSize),\n overscan: options.overscan ?? { end: DEFAULT_OVERSCAN, start: DEFAULT_OVERSCAN },\n });\n\n listElRef.style.position = 'relative';\n listElRef.style.contain = 'layout';\n\n return;\n }\n\n virtualizer.update({\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveItemKey,\n overscan: options.overscan,\n });\n\n if (measurementSync === 'invalidate') {\n virtualizer.invalidate();\n\n return;\n }\n\n if (measurementSync === 'refresh') virtualizer.refresh();\n };\n\n return {\n destroy() {\n virtualizer?.destroy();\n virtualizer = null;\n clearAndReset();\n },\n invalidate() {\n virtualizer?.invalidate();\n },\n measure(index, size) {\n virtualizer?.measure(index, size);\n },\n scrollToIndex(index, scrollOptions) {\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n setActive(active) {\n isActive = active;\n syncVirtualizer(null);\n },\n setItems(items) {\n currentItems = items;\n\n if (options.getItemKey) {\n syncVirtualizer('refresh');\n\n return;\n }\n\n itemKeyRevision += 1;\n syncVirtualizer('invalidate');\n },\n };\n}\n"],"mappings":"oCAWA,IAAM,EAAwB,GACxB,EAAmB,EAgCzB,SAAgB,EAAwB,EAAgE,CACtG,IAAI,EAAoB,CAAC,EACrB,EAAW,GACX,EAAgC,KAChC,EAA2C,KAC3C,EAAkC,KAClC,EAAkB,EAEhB,EAAkB,GAA8B,CACpD,IAAM,EAAO,EAAa,GAI1B,OAFI,GAAQ,EAAQ,WAAmB,EAAQ,WAAW,EAAO,CAAI,EAE9D,GAAG,EAAgB,GAAG,GAC/B,EAEM,EAAmB,GAA0B,CACjD,GAAI,OAAO,EAAQ,cAAiB,SAAU,OAAO,EAAQ,aAE7D,IAAM,EAAO,EAAa,GAI1B,OAFK,EAEE,EAAQ,aAAa,EAAO,CAAI,EAFrB,CAGpB,EAEM,GAAiB,EAA6B,IAAc,CAC3D,IAED,EAAQ,MAAO,EAAQ,MAAM,CAAM,EAClC,EAAO,YAAc,GAE1B,EAAO,MAAM,OAAS,GACtB,EAAO,MAAM,MAAQ,GACrB,EAAO,MAAM,SAAW,GACxB,EAAO,MAAM,QAAU,GACzB,EAEM,EAAiB,GAAsB,CACtC,IAED,EAAQ,YACV,EAAU,MAAM,MAAQ,GAAG,EAAU,IACrC,EAAU,MAAM,OAAS,KAEzB,EAAU,MAAM,OAAS,GAAG,EAAU,IACtC,EAAU,MAAM,MAAQ,IAE5B,EAEM,GAAoB,EAA6B,IAAsB,CACtE,IAEL,EAAc,CAAS,EACvB,EAAQ,OAAO,CACb,MAAO,EACP,OAAQ,EACR,YACA,cACF,CAAC,EACH,EAEM,EAAmB,GAAyC,CAChE,IAAM,EAAa,EAAQ,iBAAiB,EACtC,EAAW,EAAQ,eAAe,EAClC,EAAe,EAErB,GAAI,CAAC,GAAY,CAAC,GAAc,CAAC,GAAY,EAAa,SAAW,EAAG,CACtE,GAAa,QAAQ,EACrB,EAAc,KACd,EAAY,EACZ,EAAc,EACd,EAAc,EAEd,MACF,CAEA,IAAM,EAAgB,IAAgB,GAAc,IAAc,EAKlE,GAHA,EAAY,EACZ,EAAc,EAEV,CAAC,GAAe,EAAe,CACjC,IAAM,EAA2B,CAAC,CAAC,GAAe,EAElD,GAAa,QAAQ,EAEjB,GAA0B,EAAc,CAAY,EAExD,EAAc,EAAA,kBAAkB,EAAa,CAC3C,MAAO,EAAa,OACpB,aAAc,EACd,IAAK,EAAQ,IACb,WAAY,EACZ,WAAY,EAAQ,WACpB,UAAW,EAAc,IAAc,EAAiB,EAAc,CAAS,EAC/E,SAAU,EAAQ,UAAY,CAAE,IAAK,EAAkB,MAAO,CAAiB,CACjF,CAAC,EAED,EAAU,MAAM,SAAW,WAC3B,EAAU,MAAM,QAAU,SAE1B,MACF,CAUA,GARA,EAAY,OAAO,CACjB,MAAO,EAAa,OACpB,aAAc,EACd,IAAK,EAAQ,IACb,WAAY,EACZ,SAAU,EAAQ,QACpB,CAAC,EAEG,IAAoB,aAAc,CACpC,EAAY,WAAW,EAEvB,MACF,CAEI,IAAoB,WAAW,EAAY,QAAQ,CACzD,EAEA,MAAO,CACL,SAAU,CACR,GAAa,QAAQ,EACrB,EAAc,KACd,EAAc,CAChB,EACA,YAAa,CACX,GAAa,WAAW,CAC1B,EACA,QAAQ,EAAO,EAAM,CACnB,GAAa,QAAQ,EAAO,CAAI,CAClC,EACA,cAAc,EAAO,EAAe,CAClC,GAAa,cAAc,EAAO,CAAa,CACjD,EACA,UAAU,EAAQ,CAChB,EAAW,EACX,EAAgB,IAAI,CACtB,EACA,SAAS,EAAO,CAGd,GAFA,EAAe,EAEX,EAAQ,WAAY,CACtB,EAAgB,SAAS,EAEzB,MACF,CAEA,GAAmB,EACnB,EAAgB,YAAY,CAC9B,CACF,CACF"}
package/dist/dom/dom.d.ts CHANGED
@@ -1,22 +1,29 @@
1
- import { type ScrollToIndexOptions, type VirtualItem } from '../virtualit';
1
+ import { type Overscan, type ScrollToIndexOptions, type VirtualItem, type VirtualKey } from '../virtualit';
2
2
  export * from '../virtualit';
3
3
  export type DomVirtualListRenderArgs<T> = {
4
4
  items: T[];
5
5
  listEl: HTMLElement;
6
+ totalSize: number;
6
7
  virtualItems: VirtualItem[];
7
8
  };
8
9
  export type DomVirtualListOptions<T> = {
9
- clear: (listEl: HTMLElement) => void;
10
+ clear?: (listEl: HTMLElement) => void;
10
11
  estimateSize: number | ((index: number, item: T) => number);
12
+ gap?: number;
13
+ getItemKey?: (index: number, item: T) => VirtualKey;
11
14
  getListElement: () => HTMLElement | null;
12
- getScrollElement: () => HTMLElement | null;
13
- overscan?: number;
15
+ getScrollElement: () => HTMLElement | Window | null;
16
+ horizontal?: boolean;
17
+ overscan?: Overscan;
14
18
  render: (args: DomVirtualListRenderArgs<T>) => void;
15
19
  };
16
20
  export type DomVirtualListController<T> = {
17
21
  destroy: () => void;
22
+ invalidate: () => void;
23
+ measure: (index: number, size: number) => void;
18
24
  scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;
19
- update: (items: T[], enabled: boolean) => void;
25
+ setActive: (active: boolean) => void;
26
+ setItems: (items: T[]) => void;
20
27
  };
21
28
  export declare function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T>;
22
29
  //# sourceMappingURL=dom.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/dom/dom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,oBAAoB,EAAE,KAAK,WAAW,EAAuC,MAAM,cAAc,CAAC;AAEhH,cAAc,cAAc,CAAC;AAE7B,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAAI;IACxC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACrC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC;IAC5D,cAAc,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACzC,gBAAgB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CACrD,CAAC;AAEF,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAAI;IACxC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACvE,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,wBAAwB,CAAC,CAAC,CAAC,CA6FtG"}
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/dom/dom.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,oBAAoB,EACzB,KAAK,WAAW,EAChB,KAAK,UAAU,EAGhB,MAAM,cAAc,CAAC;AAEtB,cAAc,cAAc,CAAC;AAK7B,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAAI;IACxC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACtC,YAAY,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,UAAU,CAAC;IACpD,cAAc,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACzC,gBAAgB,EAAE,MAAM,WAAW,GAAG,MAAM,GAAG,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CACrD,CAAC;AAEF,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAAI;IACxC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACvE,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;CAChC,CAAC;AAIF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,wBAAwB,CAAC,CAAC,CAAC,CA0JtG"}
package/dist/dom/dom.js CHANGED
@@ -1,47 +1,86 @@
1
1
  import { createVirtualizer as e } from "../virtualit.js";
2
2
  //#region src/dom/dom.ts
3
- function t(t) {
4
- let n = [], r = null, i = null, a = null, o = (e) => typeof t.estimateSize == "number" ? t.estimateSize : t.estimateSize(e, n[e]), s = () => {
5
- !r || !a || (r.style.height = `${a.getTotalSize()}px`, r.style.position = "relative", r.style.contain = "layout");
6
- }, c = () => {
7
- r && (t.clear(r), r.style.height = "", r.style.position = "", r.style.contain = "");
8
- }, l = () => {
9
- let l = t.getScrollElement(), u = t.getListElement();
10
- if (!l || !u || n.length === 0) {
11
- a?.destroy(), a = null, r = u, i = l, c();
3
+ var t = 36, n = 3;
4
+ function r(r) {
5
+ let i = [], a = !0, o = null, s = null, c = null, l = 0, u = (e) => {
6
+ let t = i[e];
7
+ return t && r.getItemKey ? r.getItemKey(e, t) : `${l}:${e}`;
8
+ }, d = (e) => {
9
+ if (typeof r.estimateSize == "number") return r.estimateSize;
10
+ let n = i[e];
11
+ return n ? r.estimateSize(e, n) : t;
12
+ }, f = (e = o) => {
13
+ e && (r.clear ? r.clear(e) : e.textContent = "", e.style.height = "", e.style.width = "", e.style.position = "", e.style.contain = "");
14
+ }, p = (e) => {
15
+ o && (r.horizontal ? (o.style.width = `${e}px`, o.style.height = "") : (o.style.height = `${e}px`, o.style.width = ""));
16
+ }, m = (e, t) => {
17
+ o && (p(t), r.render({
18
+ items: i,
19
+ listEl: o,
20
+ totalSize: t,
21
+ virtualItems: e
22
+ }));
23
+ }, h = (t) => {
24
+ let l = r.getScrollElement(), p = r.getListElement(), h = o;
25
+ if (!a || !l || !p || i.length === 0) {
26
+ c?.destroy(), c = null, o = p, s = l, f();
12
27
  return;
13
28
  }
14
- let d = i !== l || r !== u;
15
- r = u, i = l, !a || d ? (a?.destroy(), a = e(i, {
16
- count: n.length,
17
- estimateSize: o,
18
- onChange: (e) => {
19
- r && t.render({
20
- items: n,
21
- listEl: r,
22
- virtualItems: e
23
- });
24
- },
25
- overscan: t.overscan ?? 3
26
- })) : (a.count = n.length, a.invalidate()), s();
29
+ let g = s !== l || o !== p;
30
+ if (o = p, s = l, !c || g) {
31
+ let t = !!c && g;
32
+ c?.destroy(), t && f(h), c = e(s, {
33
+ count: i.length,
34
+ estimateSize: d,
35
+ gap: r.gap,
36
+ getItemKey: u,
37
+ horizontal: r.horizontal,
38
+ onChange: (e, t) => m(e, t),
39
+ overscan: r.overscan ?? {
40
+ end: n,
41
+ start: n
42
+ }
43
+ }), o.style.position = "relative", o.style.contain = "layout";
44
+ return;
45
+ }
46
+ if (c.update({
47
+ count: i.length,
48
+ estimateSize: d,
49
+ gap: r.gap,
50
+ getItemKey: u,
51
+ overscan: r.overscan
52
+ }), t === "invalidate") {
53
+ c.invalidate();
54
+ return;
55
+ }
56
+ t === "refresh" && c.refresh();
27
57
  };
28
58
  return {
29
59
  destroy() {
30
- a?.destroy(), a = null, c();
60
+ c?.destroy(), c = null, f();
61
+ },
62
+ invalidate() {
63
+ c?.invalidate();
64
+ },
65
+ measure(e, t) {
66
+ c?.measure(e, t);
31
67
  },
32
68
  scrollToIndex(e, t) {
33
- a?.scrollToIndex(e, t);
69
+ c?.scrollToIndex(e, t);
70
+ },
71
+ setActive(e) {
72
+ a = e, h(null);
34
73
  },
35
- update(e, o) {
36
- if (n = e, !o || n.length === 0) {
37
- a?.destroy(), a = null, r = t.getListElement(), i = t.getScrollElement(), c();
74
+ setItems(e) {
75
+ if (i = e, r.getItemKey) {
76
+ h("refresh");
38
77
  return;
39
78
  }
40
- l();
79
+ l += 1, h("invalidate");
41
80
  }
42
81
  };
43
82
  }
44
83
  //#endregion
45
- export { t as createDomVirtualList };
84
+ export { r as createDomVirtualList };
46
85
 
47
86
  //# sourceMappingURL=dom.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dom.js","names":[],"sources":["../../src/dom/dom.ts"],"sourcesContent":["import { type ScrollToIndexOptions, type VirtualItem, type Virtualizer, createVirtualizer } from '../virtualit';\n\nexport * from '../virtualit';\n\nexport type DomVirtualListRenderArgs<T> = {\n items: T[];\n listEl: HTMLElement;\n virtualItems: VirtualItem[];\n};\n\nexport type DomVirtualListOptions<T> = {\n clear: (listEl: HTMLElement) => void;\n estimateSize: number | ((index: number, item: T) => number);\n getListElement: () => HTMLElement | null;\n getScrollElement: () => HTMLElement | null;\n overscan?: number;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n};\n\nexport type DomVirtualListController<T> = {\n destroy: () => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n update: (items: T[], enabled: boolean) => void;\n};\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n let currentItems: T[] = [];\n let listElRef: HTMLElement | null = null;\n let scrollElRef: HTMLElement | null = null;\n let virtualizer: Virtualizer | null = null;\n\n const resolveEstimate = (index: number): number => {\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n return options.estimateSize(index, currentItems[index]!);\n };\n\n const applyListStyles = () => {\n if (!listElRef || !virtualizer) return;\n\n listElRef.style.height = `${virtualizer.getTotalSize()}px`;\n listElRef.style.position = 'relative';\n listElRef.style.contain = 'layout';\n };\n\n const clearAndReset = () => {\n if (!listElRef) return;\n\n options.clear(listElRef);\n listElRef.style.height = '';\n listElRef.style.position = '';\n listElRef.style.contain = '';\n };\n\n const ensureVirtualizer = () => {\n const nextScroll = options.getScrollElement();\n const nextList = options.getListElement();\n\n if (!nextScroll || !nextList || currentItems.length === 0) {\n virtualizer?.destroy();\n virtualizer = null;\n listElRef = nextList;\n scrollElRef = nextScroll;\n clearAndReset();\n\n return;\n }\n\n const targetChanged = scrollElRef !== nextScroll || listElRef !== nextList;\n\n listElRef = nextList;\n scrollElRef = nextScroll;\n\n if (!virtualizer || targetChanged) {\n virtualizer?.destroy();\n virtualizer = createVirtualizer(scrollElRef, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n onChange: (virtualItems) => {\n if (!listElRef) return;\n\n options.render({ items: currentItems, listEl: listElRef, virtualItems });\n },\n overscan: options.overscan ?? 3,\n });\n } else {\n virtualizer.count = currentItems.length;\n virtualizer.invalidate();\n }\n\n applyListStyles();\n };\n\n return {\n destroy() {\n virtualizer?.destroy();\n virtualizer = null;\n clearAndReset();\n },\n scrollToIndex(index, scrollOptions) {\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n update(items, enabled) {\n currentItems = items;\n\n if (!enabled || currentItems.length === 0) {\n virtualizer?.destroy();\n virtualizer = null;\n listElRef = options.getListElement();\n scrollElRef = options.getScrollElement();\n clearAndReset();\n\n return;\n }\n\n ensureVirtualizer();\n },\n };\n}\n"],"mappings":";;AAyBA,SAAgB,EAAwB,GAAgE;CACtG,IAAI,IAAoB,EAAE,EACtB,IAAgC,MAChC,IAAkC,MAClC,IAAkC,MAEhC,KAAmB,MACnB,OAAO,EAAQ,gBAAiB,WAAiB,EAAQ,eAEtD,EAAQ,aAAa,GAAO,EAAa,GAAQ,EAGpD,UAAwB;AACxB,GAAC,KAAa,CAAC,MAEnB,EAAU,MAAM,SAAS,GAAG,EAAY,cAAc,CAAC,KACvD,EAAU,MAAM,WAAW,YAC3B,EAAU,MAAM,UAAU;IAGtB,UAAsB;AACrB,QAEL,EAAQ,MAAM,EAAU,EACxB,EAAU,MAAM,SAAS,IACzB,EAAU,MAAM,WAAW,IAC3B,EAAU,MAAM,UAAU;IAGtB,UAA0B;EAC9B,IAAM,IAAa,EAAQ,kBAAkB,EACvC,IAAW,EAAQ,gBAAgB;AAEzC,MAAI,CAAC,KAAc,CAAC,KAAY,EAAa,WAAW,GAAG;AAKzD,GAJA,GAAa,SAAS,EACtB,IAAc,MACd,IAAY,GACZ,IAAc,GACd,GAAe;AAEf;;EAGF,IAAM,IAAgB,MAAgB,KAAc,MAAc;AAsBlE,EApBA,IAAY,GACZ,IAAc,GAEV,CAAC,KAAe,KAClB,GAAa,SAAS,EACtB,IAAc,EAAkB,GAAa;GAC3C,OAAO,EAAa;GACpB,cAAc;GACd,WAAW,MAAiB;AACrB,SAEL,EAAQ,OAAO;KAAE,OAAO;KAAc,QAAQ;KAAW;KAAc,CAAC;;GAE1E,UAAU,EAAQ,YAAY;GAC/B,CAAC,KAEF,EAAY,QAAQ,EAAa,QACjC,EAAY,YAAY,GAG1B,GAAiB;;AAGnB,QAAO;EACL,UAAU;AAGR,GAFA,GAAa,SAAS,EACtB,IAAc,MACd,GAAe;;EAEjB,cAAc,GAAO,GAAe;AAClC,MAAa,cAAc,GAAO,EAAc;;EAElD,OAAO,GAAO,GAAS;AAGrB,OAFA,IAAe,GAEX,CAAC,KAAW,EAAa,WAAW,GAAG;AAKzC,IAJA,GAAa,SAAS,EACtB,IAAc,MACd,IAAY,EAAQ,gBAAgB,EACpC,IAAc,EAAQ,kBAAkB,EACxC,GAAe;AAEf;;AAGF,MAAmB;;EAEtB"}
1
+ {"version":3,"file":"dom.js","names":[],"sources":["../../src/dom/dom.ts"],"sourcesContent":["import {\n type Overscan,\n type ScrollToIndexOptions,\n type VirtualItem,\n type VirtualKey,\n type Virtualizer,\n createVirtualizer,\n} from '../virtualit';\n\nexport * from '../virtualit';\n\nconst DEFAULT_ESTIMATE_SIZE = 36;\nconst DEFAULT_OVERSCAN = 3;\n\nexport type DomVirtualListRenderArgs<T> = {\n items: T[];\n listEl: HTMLElement;\n totalSize: number;\n virtualItems: VirtualItem[];\n};\n\nexport type DomVirtualListOptions<T> = {\n clear?: (listEl: HTMLElement) => void;\n estimateSize: number | ((index: number, item: T) => number);\n gap?: number;\n getItemKey?: (index: number, item: T) => VirtualKey;\n getListElement: () => HTMLElement | null;\n getScrollElement: () => HTMLElement | Window | null;\n horizontal?: boolean;\n overscan?: Overscan;\n render: (args: DomVirtualListRenderArgs<T>) => void;\n};\n\nexport type DomVirtualListController<T> = {\n destroy: () => void;\n invalidate: () => void;\n measure: (index: number, size: number) => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n setActive: (active: boolean) => void;\n setItems: (items: T[]) => void;\n};\n\ntype MeasurementSyncMode = 'invalidate' | 'refresh' | null;\n\nexport function createDomVirtualList<T>(options: DomVirtualListOptions<T>): DomVirtualListController<T> {\n let currentItems: T[] = [];\n let isActive = true;\n let listElRef: HTMLElement | null = null;\n let scrollElRef: HTMLElement | Window | null = null;\n let virtualizer: Virtualizer | null = null;\n let itemKeyRevision = 0;\n\n const resolveItemKey = (index: number): VirtualKey => {\n const item = currentItems[index];\n\n if (item && options.getItemKey) return options.getItemKey(index, item);\n\n return `${itemKeyRevision}:${index}`;\n };\n\n const resolveEstimate = (index: number): number => {\n if (typeof options.estimateSize === 'number') return options.estimateSize;\n\n const item = currentItems[index];\n\n if (!item) return DEFAULT_ESTIMATE_SIZE;\n\n return options.estimateSize(index, item);\n };\n\n const clearAndReset = (listEl: HTMLElement | null = listElRef) => {\n if (!listEl) return;\n\n if (options.clear) options.clear(listEl);\n else listEl.textContent = '';\n\n listEl.style.height = '';\n listEl.style.width = '';\n listEl.style.position = '';\n listEl.style.contain = '';\n };\n\n const applyListSize = (totalSize: number) => {\n if (!listElRef) return;\n\n if (options.horizontal) {\n listElRef.style.width = `${totalSize}px`;\n listElRef.style.height = '';\n } else {\n listElRef.style.height = `${totalSize}px`;\n listElRef.style.width = '';\n }\n };\n\n const renderFromChange = (virtualItems: VirtualItem[], totalSize: number) => {\n if (!listElRef) return;\n\n applyListSize(totalSize);\n options.render({\n items: currentItems,\n listEl: listElRef,\n totalSize,\n virtualItems,\n });\n };\n\n const syncVirtualizer = (measurementSync: MeasurementSyncMode) => {\n const nextScroll = options.getScrollElement();\n const nextList = options.getListElement();\n const previousList = listElRef;\n\n if (!isActive || !nextScroll || !nextList || currentItems.length === 0) {\n virtualizer?.destroy();\n virtualizer = null;\n listElRef = nextList;\n scrollElRef = nextScroll;\n clearAndReset();\n\n return;\n }\n\n const targetChanged = scrollElRef !== nextScroll || listElRef !== nextList;\n\n listElRef = nextList;\n scrollElRef = nextScroll;\n\n if (!virtualizer || targetChanged) {\n const shouldClearForTargetSwap = !!virtualizer && targetChanged;\n\n virtualizer?.destroy();\n\n if (shouldClearForTargetSwap) clearAndReset(previousList);\n\n virtualizer = createVirtualizer(scrollElRef, {\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveItemKey,\n horizontal: options.horizontal,\n onChange: (virtualItems, totalSize) => renderFromChange(virtualItems, totalSize),\n overscan: options.overscan ?? { end: DEFAULT_OVERSCAN, start: DEFAULT_OVERSCAN },\n });\n\n listElRef.style.position = 'relative';\n listElRef.style.contain = 'layout';\n\n return;\n }\n\n virtualizer.update({\n count: currentItems.length,\n estimateSize: resolveEstimate,\n gap: options.gap,\n getItemKey: resolveItemKey,\n overscan: options.overscan,\n });\n\n if (measurementSync === 'invalidate') {\n virtualizer.invalidate();\n\n return;\n }\n\n if (measurementSync === 'refresh') virtualizer.refresh();\n };\n\n return {\n destroy() {\n virtualizer?.destroy();\n virtualizer = null;\n clearAndReset();\n },\n invalidate() {\n virtualizer?.invalidate();\n },\n measure(index, size) {\n virtualizer?.measure(index, size);\n },\n scrollToIndex(index, scrollOptions) {\n virtualizer?.scrollToIndex(index, scrollOptions);\n },\n setActive(active) {\n isActive = active;\n syncVirtualizer(null);\n },\n setItems(items) {\n currentItems = items;\n\n if (options.getItemKey) {\n syncVirtualizer('refresh');\n\n return;\n }\n\n itemKeyRevision += 1;\n syncVirtualizer('invalidate');\n },\n };\n}\n"],"mappings":";;AAWA,IAAM,IAAwB,IACxB,IAAmB;AAgCzB,SAAgB,EAAwB,GAAgE;CACtG,IAAI,IAAoB,CAAC,GACrB,IAAW,IACX,IAAgC,MAChC,IAA2C,MAC3C,IAAkC,MAClC,IAAkB,GAEhB,KAAkB,MAA8B;EACpD,IAAM,IAAO,EAAa;EAI1B,OAFI,KAAQ,EAAQ,aAAmB,EAAQ,WAAW,GAAO,CAAI,IAE9D,GAAG,EAAgB,GAAG;CAC/B,GAEM,KAAmB,MAA0B;EACjD,IAAI,OAAO,EAAQ,gBAAiB,UAAU,OAAO,EAAQ;EAE7D,IAAM,IAAO,EAAa;EAI1B,OAFK,IAEE,EAAQ,aAAa,GAAO,CAAI,IAFrB;CAGpB,GAEM,KAAiB,IAA6B,MAAc;EAC3D,MAED,EAAQ,QAAO,EAAQ,MAAM,CAAM,IAClC,EAAO,cAAc,IAE1B,EAAO,MAAM,SAAS,IACtB,EAAO,MAAM,QAAQ,IACrB,EAAO,MAAM,WAAW,IACxB,EAAO,MAAM,UAAU;CACzB,GAEM,KAAiB,MAAsB;EACtC,MAED,EAAQ,cACV,EAAU,MAAM,QAAQ,GAAG,EAAU,KACrC,EAAU,MAAM,SAAS,OAEzB,EAAU,MAAM,SAAS,GAAG,EAAU,KACtC,EAAU,MAAM,QAAQ;CAE5B,GAEM,KAAoB,GAA6B,MAAsB;EACtE,MAEL,EAAc,CAAS,GACvB,EAAQ,OAAO;GACb,OAAO;GACP,QAAQ;GACR;GACA;EACF,CAAC;CACH,GAEM,KAAmB,MAAyC;EAChE,IAAM,IAAa,EAAQ,iBAAiB,GACtC,IAAW,EAAQ,eAAe,GAClC,IAAe;EAErB,IAAI,CAAC,KAAY,CAAC,KAAc,CAAC,KAAY,EAAa,WAAW,GAAG;GAKtE,AAJA,GAAa,QAAQ,GACrB,IAAc,MACd,IAAY,GACZ,IAAc,GACd,EAAc;GAEd;EACF;EAEA,IAAM,IAAgB,MAAgB,KAAc,MAAc;EAKlE,IAHA,IAAY,GACZ,IAAc,GAEV,CAAC,KAAe,GAAe;GACjC,IAAM,IAA2B,CAAC,CAAC,KAAe;GAiBlD,AAfA,GAAa,QAAQ,GAEjB,KAA0B,EAAc,CAAY,GAExD,IAAc,EAAkB,GAAa;IAC3C,OAAO,EAAa;IACpB,cAAc;IACd,KAAK,EAAQ;IACb,YAAY;IACZ,YAAY,EAAQ;IACpB,WAAW,GAAc,MAAc,EAAiB,GAAc,CAAS;IAC/E,UAAU,EAAQ,YAAY;KAAE,KAAK;KAAkB,OAAO;IAAiB;GACjF,CAAC,GAED,EAAU,MAAM,WAAW,YAC3B,EAAU,MAAM,UAAU;GAE1B;EACF;EAUA,IARA,EAAY,OAAO;GACjB,OAAO,EAAa;GACpB,cAAc;GACd,KAAK,EAAQ;GACb,YAAY;GACZ,UAAU,EAAQ;EACpB,CAAC,GAEG,MAAoB,cAAc;GACpC,EAAY,WAAW;GAEvB;EACF;EAEA,AAAI,MAAoB,aAAW,EAAY,QAAQ;CACzD;CAEA,OAAO;EACL,UAAU;GAGR,AAFA,GAAa,QAAQ,GACrB,IAAc,MACd,EAAc;EAChB;EACA,aAAa;GACX,GAAa,WAAW;EAC1B;EACA,QAAQ,GAAO,GAAM;GACnB,GAAa,QAAQ,GAAO,CAAI;EAClC;EACA,cAAc,GAAO,GAAe;GAClC,GAAa,cAAc,GAAO,CAAa;EACjD;EACA,UAAU,GAAQ;GAEhB,AADA,IAAW,GACX,EAAgB,IAAI;EACtB;EACA,SAAS,GAAO;GAGd,IAFA,IAAe,GAEX,EAAQ,YAAY;IACtB,EAAgB,SAAS;IAEzB;GACF;GAGA,AADA,KAAmB,GACnB,EAAgB,YAAY;EAC9B;CACF;AACF"}
package/dist/dom.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./virtualit.cjs`),t=require(`./dom/dom.cjs`);exports.Virtualizer=e.Virtualizer,exports.createDomVirtualList=t.createDomVirtualList,exports.createVirtualizer=e.createVirtualizer;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./virtualit.cjs`),t=require(`./dom/dom.cjs`);exports.createDomVirtualList=t.createDomVirtualList,exports.createVirtualizer=e.createVirtualizer;
package/dist/dom.js CHANGED
@@ -1,3 +1,3 @@
1
- import { Virtualizer as e, createVirtualizer as t } from "./virtualit.js";
2
- import { createDomVirtualList as n } from "./dom/dom.js";
3
- export { e as Virtualizer, n as createDomVirtualList, t as createVirtualizer };
1
+ import { createVirtualizer as e } from "./virtualit.js";
2
+ import { createDomVirtualList as t } from "./dom/dom.js";
3
+ export { t as createDomVirtualList, e as createVirtualizer };
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./virtualit.cjs`);exports.Virtualizer=e.Virtualizer,exports.createVirtualizer=e.createVirtualizer;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./virtualit.cjs`);exports.createVirtualizer=e.createVirtualizer;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { Virtualizer as e, createVirtualizer as t } from "./virtualit.js";
2
- export { e as Virtualizer, t as createVirtualizer };
1
+ import { createVirtualizer as e } from "./virtualit.js";
2
+ export { e as createVirtualizer };
@@ -1,2 +1,2 @@
1
- var e=class{_count;_estimateSizeFn;overscan;onChange;measuredHeights=new Map;virtualItems=[];totalSize=0;scrollOffsets=new Float64Array;containerHeight=0;scrollTop=0;prevRenderStart=-1;prevRenderEnd=-1;attachedEl=null;resizeObserver=null;scrollHandler=null;pendingBuild=!1;constructor(e){this._count=e.count;let t=e.estimateSize??36;this._estimateSizeFn=typeof t==`number`?()=>t:t,this.overscan=e.overscan??3,this.onChange=e.onChange,this.buildOffsets()}get count(){return this._count}set count(e){this._count=e,this.buildOffsets(),this.attachedEl&&this.computeVisible()}set estimateSize(e){this._estimateSizeFn=typeof e==`number`?()=>e:e,this.measuredHeights.clear(),this.buildOffsets(),this.attachedEl&&this.computeVisible()}attach(e){this.teardown(),this.attachedEl=e,this.containerHeight=e.clientHeight,this.scrollTop=e.scrollTop,this.scrollHandler=()=>{this.scrollTop=e.scrollTop,this.computeVisible()},e.addEventListener(`scroll`,this.scrollHandler,{passive:!0}),this.resizeObserver=new ResizeObserver(()=>{this.containerHeight=e.clientHeight,this.computeVisible()}),this.resizeObserver.observe(e),this.computeVisible()}destroy(){this.teardown()}[Symbol.dispose](){this.destroy()}getVirtualItems(){return this.virtualItems}getTotalSize(){return this.totalSize}measureElement(e,t){this.heightAt(e)!==t&&(this.measuredHeights.set(e,t),this.pendingBuild||(this.pendingBuild=!0,queueMicrotask(()=>{this.pendingBuild=!1,this.buildOffsets(),this.attachedEl&&this.computeVisible()})))}scrollToIndex(e,t={}){let n=this.attachedEl;if(!n)return;let r=Math.max(0,Math.min(e,this._count-1)),i=t.align??`auto`,a=t.behavior??`auto`,o=this.offsetAt(r),s=this.heightAt(r),c;if(i===`start`)c=o;else if(i===`end`)c=o+s-this.containerHeight;else if(i===`center`)c=o-(this.containerHeight-s)/2;else{let e=n.scrollTop,t=e+this.containerHeight;if(o>=e&&o+s<=t)return;c=o<e?o:o+s-this.containerHeight}n.scrollTo({behavior:a,top:Math.max(0,c)})}scrollToOffset(e,t={}){this.attachedEl?.scrollTo({behavior:t.behavior??`auto`,top:Math.max(0,e)})}invalidate(){this.measuredHeights.clear(),this.buildOffsets(),this.attachedEl&&this.computeVisible()}teardown(){this.scrollHandler&&this.attachedEl&&(this.attachedEl.removeEventListener(`scroll`,this.scrollHandler),this.scrollHandler=null),this.resizeObserver?.disconnect(),this.resizeObserver=null,this.attachedEl=null}heightAt(e){return this.measuredHeights.get(e)??this._estimateSizeFn(e)}offsetAt(e){return this.scrollOffsets[e]??0}buildOffsets(){this.prevRenderStart=-1,this.prevRenderEnd=-1;let e=new Float64Array(this._count+1);e[0]=0;for(let t=0;t<this._count;t++)e[t+1]=e[t]+this.heightAt(t);this.scrollOffsets=e,this.totalSize=e[this._count]??0}computeVisible(){let e=this.scrollTop,t=e+this.containerHeight,n=0,r=this._count-1;for(;n<r;){let t=n+r>>1;this.scrollOffsets[t+1]<=e?n=t+1:r=t}let i=n,a=i,o=this._count-1;for(;a<o;){let e=a+o+1>>1;this.scrollOffsets[e]<t?a=e:o=e-1}let s=a,c=Math.max(0,i-this.overscan),l=Math.min(this._count-1,s+this.overscan);if(c===this.prevRenderStart&&l===this.prevRenderEnd)return;this.prevRenderStart=c,this.prevRenderEnd=l;let u=[];for(let e=c;e<=l;e++)u.push({height:this.heightAt(e),index:e,top:this.scrollOffsets[e]});this.virtualItems=u,this.onChange?.(u,this.totalSize)}};function t(t,n){let r=new e(n);return r.attach(t),r}exports.Virtualizer=e,exports.createVirtualizer=t;
1
+ var e=36,t=3,n=120;function r(e,t=0){return Number.isFinite(e)?Math.max(0,Math.floor(e)):t}function i(e,t){return!Number.isFinite(e)||e<=0?t:e}function a(t){return typeof t==`number`?i(t,e):typeof t==`function`?t:e}function o(t){return typeof t==`number`?()=>t:n=>i(t(n),e)}function s(e){return{end:r(e?.end??t),start:r(e?.start??t)}}function c(e){return`innerHeight`in e&&`innerWidth`in e&&!(`clientHeight`in e)}function l(e,t){return c(e)?{attach(t,n){return e.addEventListener(`scroll`,t,{passive:!0}),e.addEventListener(`resize`,n,{passive:!0}),()=>{e.removeEventListener(`scroll`,t),e.removeEventListener(`resize`,n)}},readOffset:t?()=>e.scrollX:()=>e.scrollY,readViewportSize:t?()=>e.innerWidth:()=>e.innerHeight,writeOffset:(n,r)=>{e.scrollTo(t?{behavior:r,left:n}:{behavior:r,top:n})}}:{attach(t,n){let r=new ResizeObserver(n);return e.addEventListener(`scroll`,t,{passive:!0}),r.observe(e),()=>{e.removeEventListener(`scroll`,t),r.disconnect()}},readOffset:t?()=>e.scrollLeft:()=>e.scrollTop,readViewportSize:t?()=>e.clientWidth:()=>e.clientHeight,writeOffset:(n,r)=>{e.scrollTo(t?{behavior:r,left:n}:{behavior:r,top:n})}}}function u(e,t){let c=l(e,!!t.horizontal),u=r(t.count),d=a(t.estimateSize),f=o(d),p=r(t.gap??0),m=t.getItemKey??(e=>e),h=s(t.overscan),g=r(t.scrollEndDelay??n,n),_=t.onChange,v=t.onScrollEnd,y=t.onScrollingChange,b=[],x=0,S=0,C=!1,w=new Map,T=new Float64Array(u+1),E=c.readViewportSize(),D=-1,O=-1,k=-1,A=!1,j=!1,M=null;function N(e){return m(e)}function P(e){return w.get(N(e))??f(e)}function F(e){return T[e]??0}function I(e){return F(e)+P(e)}function L(e){let t=Number.isFinite(e)?e:0,n=Math.max(0,x-E);return Math.min(n,Math.max(0,t))}function R(){M&&=(clearTimeout(M),null)}function z(e){C!==e&&(C=e,y?.(e))}function B(){R(),M=setTimeout(()=>{M=null,z(!1),v?.(S)},g)}function V(){S=L(c.readOffset()),z(!0),B(),Y()}function H(){E=c.readViewportSize(),Y()}function U(e){let t=0,n=u-1;for(;t<n;){let r=t+n>>1;I(r)<=e?t=r+1:n=r}return t}function W(e,t){let n=t,r=u-1;for(;n<r;){let t=n+r+1>>1;F(t)<e?n=t:r=t-1}return n}function G(e){let t=F(e),n=P(e);return{end:t+n,index:e,size:n,start:t}}function K(e){b=e,_?.(e,x)}function q(){let e=c.readOffset(),t=L(e);t!==S&&(S=t,e!==t&&c.writeOffset(t,`auto`))}function J(){D=-1,O=-1;let e=new Float64Array(u+1);for(let t=0;t<u;t++)e[t+1]=e[t]+P(t)+(t<u-1?p:0);T=e,x=e[u]??0}function Y(){if(j)return;if(q(),u===0||E<=0){(D!==-1||k!==x)&&(D=-1,O=-1,k=x,K([]));return}let e=S,t=e+E,n=U(e),r=W(t,n),i=Math.max(0,n-h.start),a=Math.min(u-1,r+h.end);if(i===D&&a===O&&k===x)return;D=i,O=a,k=x;let o=[];for(let e=i;e<=a;e++)o.push(G(e));K(o)}function X(e){let t=!1,i=!1;if(`count`in e&&e.count!==void 0){let n=r(e.count);n!==u&&(u=n,t=!0,i=!0)}if(`estimateSize`in e&&e.estimateSize!==void 0){let n=a(e.estimateSize);n!==d&&(d=n,f=o(d),w.clear(),t=!0,i=!0)}if(`gap`in e&&e.gap!==void 0){let n=r(e.gap);n!==p&&(p=n,t=!0,i=!0)}if(`getItemKey`in e&&e.getItemKey&&e.getItemKey!==m&&(m=e.getItemKey,w.clear(),t=!0,i=!0),`overscan`in e){let t=s(e.overscan);(t.start!==h.start||t.end!==h.end)&&(h=t,i=!0)}if(`scrollEndDelay`in e&&e.scrollEndDelay!==void 0){let t=r(e.scrollEndDelay,n);t!==g&&(g=t)}`onChange`in e&&(_=e.onChange),`onScrollEnd`in e&&(v=e.onScrollEnd),`onScrollingChange`in e&&(y=e.onScrollingChange),t&&J(),(i||t)&&Y()}function Z(e){j||X(e)}function Q(e,t){if(j||!Number.isFinite(e))return;let n=Math.floor(e);if(n<0||n>=u)return;let r=i(t,-1);if(r<=0)return;let a=N(n);w.get(a)!==r&&(w.set(a,r),!A&&(A=!0,queueMicrotask(()=>{A=!1,J(),Y()})))}function ee(){j||(w.clear(),J(),Y())}function te(){j||(J(),Y())}function ne(e,t={}){if(j||u<=0)return;let n=Math.max(0,Math.min(Number.isFinite(e)?Math.floor(e):0,u-1)),r=t.align??`auto`,i=t.behavior??`auto`,a=F(n),o=P(n),s=a+o,l;if(r===`start`)l=a;else if(r===`end`)l=s-E;else if(r===`center`)l=a-(E-o)/2;else{let e=S,t=e+E;if(a>=e&&s<=t)return;l=a<e?a:s-E}c.writeOffset(L(l),i)}function re(e,t={}){j||c.writeOffset(L(e),t.behavior??`auto`)}function $(){j||(j=!0,R(),ie())}J(),t.initialOffset!==void 0&&c.writeOffset(L(t.initialOffset),`auto`),S=L(c.readOffset());let ie=c.attach(V,H);return Y(),{get count(){return u},destroy:$,invalidate:ee,get isScrolling(){return C},get items(){return b},measure:Q,refresh:te,get scrollOffset(){return S},scrollToIndex:ne,scrollToOffset:re,[Symbol.dispose](){$()},get totalSize(){return x},update:Z}}exports.createVirtualizer=u;
2
2
  //# sourceMappingURL=virtualit.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"virtualit.cjs","names":[],"sources":["../src/virtualit.ts"],"sourcesContent":["/**\n * @vielzeug/virtualit — Lightweight virtual list / infinite-scroll engine.\n *\n * Framework-agnostic: works with any DOM rendering layer.\n * Uses a `ResizeObserver` to re-measure the scroll container and a\n * `scroll` listener to update the visible window.\n */\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface VirtualItem {\n /** Original item index in the full list */\n index: number;\n /** Pixel offset from the top of the virtual scroll area */\n top: number;\n /** Measured (or estimated) pixel height for this item */\n height: number;\n}\n\nexport interface VirtualizerOptions {\n /** Total number of items. */\n count: number;\n /**\n * Either a fixed row height or a per-index estimator function.\n * Defaults to 36px.\n */\n estimateSize?: number | ((index: number) => number);\n /**\n * Number of items to render outside the visible viewport on each side.\n * Higher values reduce blank-flash during fast scroll at the cost of more DOM nodes.\n * Defaults to 3.\n */\n overscan?: number;\n /**\n * Called whenever the visible range changes. Trigger your re-render here.\n */\n onChange?: (items: VirtualItem[], totalSize: number) => void;\n}\n\nexport interface ScrollToIndexOptions {\n /** 'start' | 'end' | 'center' | 'auto'. Defaults to 'auto'. */\n align?: 'start' | 'end' | 'center' | 'auto';\n /** Scroll behaviour. Defaults to 'auto'. */\n behavior?: ScrollBehavior;\n}\n\n// ─── Virtualizer ──────────────────────────────────────────────────────────────\n\nexport class Virtualizer {\n // mutable options\n private _count: number;\n private _estimateSizeFn: (index: number) => number;\n private overscan: number;\n private onChange: ((items: VirtualItem[], totalSize: number) => void) | undefined;\n\n // internal state\n private measuredHeights: Map<number, number> = new Map();\n private virtualItems: VirtualItem[] = [];\n private totalSize = 0;\n private scrollOffsets: Float64Array = new Float64Array(0); // prefix-sum cache\n private containerHeight = 0;\n private scrollTop = 0;\n\n // render range cache — reset in buildOffsets() so layout changes always re-render\n private prevRenderStart = -1;\n private prevRenderEnd = -1;\n\n // cleanup handles\n private attachedEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private scrollHandler: (() => void) | null = null;\n\n // batching flag for measureElement\n private pendingBuild = false;\n\n constructor(options: VirtualizerOptions) {\n this._count = options.count;\n\n const est = options.estimateSize ?? 36;\n\n this._estimateSizeFn = typeof est === 'number' ? () => est : est;\n this.overscan = options.overscan ?? 3;\n this.onChange = options.onChange;\n // Build the offset table eagerly; computeVisible is deferred to attach()\n // so the first onChange call always has a real containerHeight.\n this.buildOffsets();\n }\n\n // ─── Public API ───────────────────────────────────────────────────────────\n\n get count(): number {\n return this._count;\n }\n\n /** Setting count automatically rebuilds offsets and triggers a re-render. */\n set count(value: number) {\n this._count = value;\n this.buildOffsets();\n\n if (this.attachedEl) this.computeVisible();\n }\n\n /**\n * Update the size estimator. Clears all measured heights and re-renders.\n * Useful when switching between row density modes (e.g. compact ↔ comfortable).\n */\n set estimateSize(fn: number | ((index: number) => number)) {\n this._estimateSizeFn = typeof fn === 'number' ? () => fn : fn;\n this.measuredHeights.clear();\n this.buildOffsets();\n\n if (this.attachedEl) this.computeVisible();\n }\n\n /** Start observing the scroll container. */\n attach(el: HTMLElement): void {\n this.teardown();\n\n this.attachedEl = el;\n this.containerHeight = el.clientHeight;\n this.scrollTop = el.scrollTop;\n\n this.scrollHandler = () => {\n this.scrollTop = el.scrollTop;\n this.computeVisible();\n };\n el.addEventListener('scroll', this.scrollHandler, { passive: true });\n\n this.resizeObserver = new ResizeObserver(() => {\n this.containerHeight = el.clientHeight;\n // The offset table depends only on item heights, not container height —\n // no need to rebuild it here, only recompute the visible window.\n this.computeVisible();\n });\n this.resizeObserver.observe(el);\n\n this.computeVisible();\n }\n\n /** Stop observing and remove all listeners. */\n destroy(): void {\n this.teardown();\n }\n\n /** Supports the Explicit Resource Management `using` keyword. */\n [Symbol.dispose](): void {\n this.destroy();\n }\n\n /** Returns the currently visible virtual items. */\n getVirtualItems(): VirtualItem[] {\n return this.virtualItems;\n }\n\n /** Total pixel height of the entire list (set as the spacer height). */\n getTotalSize(): number {\n return this.totalSize;\n }\n\n /**\n * Record a measured height for a rendered item (for variable-height lists).\n *\n * Measurements are batched via microtask — safe to call for every item in a\n * render loop without incurring O(n²) rebuilds.\n */\n measureElement(index: number, height: number): void {\n if (this.heightAt(index) === height) return;\n\n this.measuredHeights.set(index, height);\n\n if (!this.pendingBuild) {\n this.pendingBuild = true;\n queueMicrotask(() => {\n this.pendingBuild = false;\n this.buildOffsets();\n\n if (this.attachedEl) this.computeVisible();\n });\n }\n }\n\n /** Programmatically scroll to a specific index. */\n scrollToIndex(index: number, options: ScrollToIndexOptions = {}): void {\n const el = this.attachedEl;\n\n if (!el) return;\n\n const clampedIndex = Math.max(0, Math.min(index, this._count - 1));\n const align = options.align ?? 'auto';\n const behavior = options.behavior ?? 'auto';\n const itemTop = this.offsetAt(clampedIndex);\n const itemHeight = this.heightAt(clampedIndex);\n\n let targetScrollTop: number;\n\n if (align === 'start') {\n targetScrollTop = itemTop;\n } else if (align === 'end') {\n targetScrollTop = itemTop + itemHeight - this.containerHeight;\n } else if (align === 'center') {\n targetScrollTop = itemTop - (this.containerHeight - itemHeight) / 2;\n } else {\n // auto: scroll only if not already visible\n const visibleStart = el.scrollTop;\n const visibleEnd = visibleStart + this.containerHeight;\n\n if (itemTop >= visibleStart && itemTop + itemHeight <= visibleEnd) return;\n\n targetScrollTop = itemTop < visibleStart ? itemTop : itemTop + itemHeight - this.containerHeight;\n }\n\n el.scrollTo({ behavior, top: Math.max(0, targetScrollTop) });\n }\n\n /** Programmatically scroll to a specific pixel offset. */\n scrollToOffset(offset: number, options: { behavior?: ScrollBehavior } = {}): void {\n this.attachedEl?.scrollTo({ behavior: options.behavior ?? 'auto', top: Math.max(0, offset) });\n }\n\n /**\n * Invalidate all item measurements. Call after a font load or layout shift\n * that changes item heights.\n */\n invalidate(): void {\n this.measuredHeights.clear();\n this.buildOffsets();\n\n if (this.attachedEl) this.computeVisible();\n }\n\n // ─── Private Helpers ──────────────────────────────────────────────────────\n\n private teardown(): void {\n if (this.scrollHandler && this.attachedEl) {\n this.attachedEl.removeEventListener('scroll', this.scrollHandler);\n this.scrollHandler = null;\n }\n\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.attachedEl = null;\n }\n\n private heightAt(index: number): number {\n return this.measuredHeights.get(index) ?? this._estimateSizeFn(index);\n }\n\n private offsetAt(index: number): number {\n return this.scrollOffsets[index] ?? 0;\n }\n\n private buildOffsets(): void {\n // Invalidate the render range cache: item positions may shift even when the\n // visible index range stays the same (e.g. an item above grew taller).\n this.prevRenderStart = -1;\n this.prevRenderEnd = -1;\n\n const offsets = new Float64Array(this._count + 1);\n\n offsets[0] = 0;\n for (let i = 0; i < this._count; i++) {\n offsets[i + 1] = offsets[i] + this.heightAt(i);\n }\n this.scrollOffsets = offsets;\n this.totalSize = offsets[this._count] ?? 0;\n }\n\n private computeVisible(): void {\n const start = this.scrollTop;\n const end = start + this.containerHeight;\n\n // Binary search for the first visible index\n let lo = 0;\n let hi = this._count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n\n if (this.scrollOffsets[mid + 1] <= start) lo = mid + 1;\n else hi = mid;\n }\n\n const firstVisible = lo;\n\n // Binary search for the last visible index\n let lo2 = firstVisible;\n let hi2 = this._count - 1;\n\n while (lo2 < hi2) {\n const mid = (lo2 + hi2 + 1) >> 1;\n\n if (this.scrollOffsets[mid] < end) lo2 = mid;\n else hi2 = mid - 1;\n }\n\n const lastVisible = lo2;\n const renderStart = Math.max(0, firstVisible - this.overscan);\n const renderEnd = Math.min(this._count - 1, lastVisible + this.overscan);\n\n // Skip re-render when the range is unchanged (e.g. a sub-pixel scroll that\n // doesn't cross an item boundary). The cache is reset in buildOffsets() so\n // any layout change always produces at least one render.\n if (renderStart === this.prevRenderStart && renderEnd === this.prevRenderEnd) return;\n\n this.prevRenderStart = renderStart;\n this.prevRenderEnd = renderEnd;\n\n const items: VirtualItem[] = [];\n\n for (let i = renderStart; i <= renderEnd; i++) {\n items.push({ height: this.heightAt(i), index: i, top: this.scrollOffsets[i] });\n }\n\n this.virtualItems = items;\n this.onChange?.(items, this.totalSize);\n }\n}\n\n// ─── Convenience factory ──────────────────────────────────────────────────────\n\n/**\n * Creates and immediately attaches a `Virtualizer` to the given scroll container.\n *\n * @example\n * ```ts\n * import { createVirtualizer } from '@vielzeug/virtualit';\n *\n * const virt = createVirtualizer(scrollContainerEl, {\n * count: items.length,\n * estimateSize: 36,\n * onChange: (virtualItems, totalSize) => {\n * // update your rendered list\n * },\n * });\n *\n * // Later:\n * virt.destroy();\n *\n * // Or, with the Explicit Resource Management proposal:\n * {\n * using virt = createVirtualizer(scrollContainerEl, { ... });\n * } // virt.destroy() called automatically\n * ```\n */\nexport function createVirtualizer(el: HTMLElement, options: VirtualizerOptions): Virtualizer {\n const v = new Virtualizer(options);\n\n v.attach(el);\n\n return v;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"virtualit.cjs","names":[],"sources":["../src/virtualit.ts"],"sourcesContent":["export type VirtualKey = number | string;\n\nexport interface VirtualItem {\n index: number;\n start: number;\n end: number;\n size: number;\n}\n\nexport type Overscan = { end?: number; start?: number };\n\nexport interface VirtualizerOptions {\n count: number;\n estimateSize?: number | ((index: number) => number);\n gap?: number;\n getItemKey?: (index: number) => VirtualKey;\n horizontal?: boolean;\n initialOffset?: number;\n onChange?: (items: VirtualItem[], totalSize: number) => void;\n onScrollEnd?: (offset: number) => void;\n onScrollingChange?: (isScrolling: boolean) => void;\n overscan?: Overscan;\n scrollEndDelay?: number;\n}\n\nexport type VirtualizerUpdateOptions = {\n count?: number;\n estimateSize?: number | ((index: number) => number);\n gap?: number;\n getItemKey?: (index: number) => VirtualKey;\n onChange?: (items: VirtualItem[], totalSize: number) => void;\n onScrollEnd?: (offset: number) => void;\n onScrollingChange?: (isScrolling: boolean) => void;\n overscan?: Overscan;\n scrollEndDelay?: number;\n};\n\nexport interface ScrollToIndexOptions {\n align?: 'start' | 'end' | 'center' | 'auto';\n behavior?: ScrollBehavior;\n}\n\nexport interface Virtualizer {\n readonly count: number;\n readonly isScrolling: boolean;\n readonly items: VirtualItem[];\n readonly scrollOffset: number;\n readonly totalSize: number;\n destroy: () => void;\n invalidate: () => void;\n measure: (index: number, size: number) => void;\n refresh: () => void;\n scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;\n scrollToOffset: (offset: number, options?: { behavior?: ScrollBehavior }) => void;\n update: (next: VirtualizerUpdateOptions) => void;\n [Symbol.dispose]: () => void;\n}\n\nconst DEFAULT_ESTIMATE_SIZE = 36;\nconst DEFAULT_OVERSCAN = 3;\nconst DEFAULT_SCROLL_END_DELAY = 120;\n\ntype ScrollTarget = HTMLElement | Window;\n\ntype AxisIO = {\n attach: (onScroll: () => void, onResize: () => void) => () => void;\n readOffset: () => number;\n readViewportSize: () => number;\n writeOffset: (offset: number, behavior: ScrollBehavior) => void;\n};\n\nfunction toNonNegativeInt(value: number, fallback = 0): number {\n if (!Number.isFinite(value)) return fallback;\n\n return Math.max(0, Math.floor(value));\n}\n\nfunction toPositiveNumber(value: number, fallback: number): number {\n if (!Number.isFinite(value) || value <= 0) return fallback;\n\n return value;\n}\n\nfunction normalizeEstimate(estimate: VirtualizerOptions['estimateSize']): number | ((index: number) => number) {\n if (typeof estimate === 'number') return toPositiveNumber(estimate, DEFAULT_ESTIMATE_SIZE);\n\n if (typeof estimate === 'function') return estimate;\n\n return DEFAULT_ESTIMATE_SIZE;\n}\n\nfunction createEstimateFn(estimate: number | ((index: number) => number)): (index: number) => number {\n if (typeof estimate === 'number') {\n return () => estimate;\n }\n\n return (index: number) => toPositiveNumber(estimate(index), DEFAULT_ESTIMATE_SIZE);\n}\n\nfunction normalizeOverscan(overscan: Overscan | undefined): { end: number; start: number } {\n return {\n end: toNonNegativeInt(overscan?.end ?? DEFAULT_OVERSCAN),\n start: toNonNegativeInt(overscan?.start ?? DEFAULT_OVERSCAN),\n };\n}\n\nfunction isWindowTarget(target: ScrollTarget): target is Window {\n return 'innerHeight' in target && 'innerWidth' in target && !('clientHeight' in target);\n}\n\nfunction createAxisIO(target: ScrollTarget, horizontal: boolean): AxisIO {\n const isWindow = isWindowTarget(target);\n\n if (isWindow) {\n return {\n attach(onScroll, onResize) {\n target.addEventListener('scroll', onScroll, { passive: true });\n target.addEventListener('resize', onResize, { passive: true });\n\n return () => {\n target.removeEventListener('scroll', onScroll);\n target.removeEventListener('resize', onResize);\n };\n },\n readOffset: horizontal ? () => target.scrollX : () => target.scrollY,\n readViewportSize: horizontal ? () => target.innerWidth : () => target.innerHeight,\n writeOffset: (offset, behavior) => {\n target.scrollTo(horizontal ? { behavior, left: offset } : { behavior, top: offset });\n },\n };\n }\n\n return {\n attach(onScroll, onResize) {\n const resizeObserver = new ResizeObserver(onResize);\n\n target.addEventListener('scroll', onScroll, { passive: true });\n resizeObserver.observe(target);\n\n return () => {\n target.removeEventListener('scroll', onScroll);\n resizeObserver.disconnect();\n };\n },\n readOffset: horizontal ? () => target.scrollLeft : () => target.scrollTop,\n readViewportSize: horizontal ? () => target.clientWidth : () => target.clientHeight,\n writeOffset: (offset, behavior) => {\n target.scrollTo(horizontal ? { behavior, left: offset } : { behavior, top: offset });\n },\n };\n}\n\nexport function createVirtualizer(target: ScrollTarget, options: VirtualizerOptions): Virtualizer {\n const axis = createAxisIO(target, !!options.horizontal);\n\n let count = toNonNegativeInt(options.count);\n let estimateSize = normalizeEstimate(options.estimateSize);\n let estimateFn = createEstimateFn(estimateSize);\n let gap = toNonNegativeInt(options.gap ?? 0);\n let getItemKey = options.getItemKey ?? ((index: number) => index);\n let overscan = normalizeOverscan(options.overscan);\n let scrollEndDelay = toNonNegativeInt(options.scrollEndDelay ?? DEFAULT_SCROLL_END_DELAY, DEFAULT_SCROLL_END_DELAY);\n let onChange = options.onChange;\n let onScrollEnd = options.onScrollEnd;\n let onScrollingChange = options.onScrollingChange;\n\n let items: VirtualItem[] = [];\n let totalSize = 0;\n let scrollOffset = 0;\n let isScrolling = false;\n const measuredByKey = new Map<VirtualKey, number>();\n let offsets = new Float64Array(count + 1);\n let viewportSize = axis.readViewportSize();\n let prevRenderStart = -1;\n let prevRenderEnd = -1;\n let prevTotalSize = -1;\n let pendingBuild = false;\n let destroyed = false;\n let scrollEndTimer: ReturnType<typeof setTimeout> | null = null;\n\n function keyAt(index: number): VirtualKey {\n return getItemKey(index);\n }\n\n function sizeAt(index: number): number {\n return measuredByKey.get(keyAt(index)) ?? estimateFn(index);\n }\n\n function startAt(index: number): number {\n return offsets[index] ?? 0;\n }\n\n function endAt(index: number): number {\n return startAt(index) + sizeAt(index);\n }\n\n function clampScrollOffset(offset: number): number {\n const safe = Number.isFinite(offset) ? offset : 0;\n const maxOffset = Math.max(0, totalSize - viewportSize);\n\n return Math.min(maxOffset, Math.max(0, safe));\n }\n\n function clearScrollEndTimer(): void {\n if (!scrollEndTimer) return;\n\n clearTimeout(scrollEndTimer);\n scrollEndTimer = null;\n }\n\n function setScrolling(next: boolean): void {\n if (isScrolling === next) return;\n\n isScrolling = next;\n onScrollingChange?.(next);\n }\n\n function scheduleScrollEnd(): void {\n clearScrollEndTimer();\n\n scrollEndTimer = setTimeout(() => {\n scrollEndTimer = null;\n setScrolling(false);\n onScrollEnd?.(scrollOffset);\n }, scrollEndDelay);\n }\n\n function onScroll(): void {\n scrollOffset = clampScrollOffset(axis.readOffset());\n setScrolling(true);\n scheduleScrollEnd();\n computeVisible();\n }\n\n function onResize(): void {\n viewportSize = axis.readViewportSize();\n computeVisible();\n }\n\n function findFirstVisible(offsetStart: number): number {\n let lo = 0;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n\n if (endAt(mid) <= offsetStart) lo = mid + 1;\n else hi = mid;\n }\n\n return lo;\n }\n\n function findLastVisible(offsetEnd: number, firstVisible: number): number {\n let lo = firstVisible;\n let hi = count - 1;\n\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n\n if (startAt(mid) < offsetEnd) lo = mid;\n else hi = mid - 1;\n }\n\n return lo;\n }\n\n function itemFromIndex(index: number): VirtualItem {\n const start = startAt(index);\n const size = sizeAt(index);\n\n return {\n end: start + size,\n index,\n size,\n start,\n };\n }\n\n function emit(nextItems: VirtualItem[]): void {\n items = nextItems;\n onChange?.(nextItems, totalSize);\n }\n\n function syncScrollOffset(): void {\n const rawOffset = axis.readOffset();\n const nextOffset = clampScrollOffset(rawOffset);\n\n if (nextOffset === scrollOffset) return;\n\n scrollOffset = nextOffset;\n\n if (rawOffset !== nextOffset) axis.writeOffset(nextOffset, 'auto');\n }\n\n function rebuildOffsets(): void {\n prevRenderStart = -1;\n prevRenderEnd = -1;\n\n const next = new Float64Array(count + 1);\n\n for (let i = 0; i < count; i++) {\n next[i + 1] = next[i] + sizeAt(i) + (i < count - 1 ? gap : 0);\n }\n\n offsets = next;\n totalSize = next[count] ?? 0;\n }\n\n function computeVisible(): void {\n if (destroyed) return;\n\n syncScrollOffset();\n\n if (count === 0 || viewportSize <= 0) {\n if (prevRenderStart !== -1 || prevTotalSize !== totalSize) {\n prevRenderStart = -1;\n prevRenderEnd = -1;\n prevTotalSize = totalSize;\n emit([]);\n }\n\n return;\n }\n\n const start = scrollOffset;\n const end = start + viewportSize;\n const firstVisible = findFirstVisible(start);\n const lastVisible = findLastVisible(end, firstVisible);\n const renderStart = Math.max(0, firstVisible - overscan.start);\n const renderEnd = Math.min(count - 1, lastVisible + overscan.end);\n\n if (renderStart === prevRenderStart && renderEnd === prevRenderEnd && prevTotalSize === totalSize) return;\n\n prevRenderStart = renderStart;\n prevRenderEnd = renderEnd;\n prevTotalSize = totalSize;\n\n const nextItems: VirtualItem[] = [];\n\n for (let i = renderStart; i <= renderEnd; i++) {\n nextItems.push(itemFromIndex(i));\n }\n\n emit(nextItems);\n }\n\n function applyOptions(next: VirtualizerUpdateOptions): void {\n let needsRebuild = false;\n let needsCompute = false;\n\n if ('count' in next && next.count !== undefined) {\n const nextCount = toNonNegativeInt(next.count);\n\n if (nextCount !== count) {\n count = nextCount;\n needsRebuild = true;\n needsCompute = true;\n }\n }\n\n if ('estimateSize' in next && next.estimateSize !== undefined) {\n const normalizedEstimate = normalizeEstimate(next.estimateSize);\n\n if (normalizedEstimate !== estimateSize) {\n estimateSize = normalizedEstimate;\n estimateFn = createEstimateFn(estimateSize);\n measuredByKey.clear();\n needsRebuild = true;\n needsCompute = true;\n }\n }\n\n if ('gap' in next && next.gap !== undefined) {\n const nextGap = toNonNegativeInt(next.gap);\n\n if (nextGap !== gap) {\n gap = nextGap;\n needsRebuild = true;\n needsCompute = true;\n }\n }\n\n if ('getItemKey' in next && next.getItemKey) {\n if (next.getItemKey !== getItemKey) {\n getItemKey = next.getItemKey;\n measuredByKey.clear();\n needsRebuild = true;\n needsCompute = true;\n }\n }\n\n if ('overscan' in next) {\n const nextOverscan = normalizeOverscan(next.overscan);\n\n if (nextOverscan.start !== overscan.start || nextOverscan.end !== overscan.end) {\n overscan = nextOverscan;\n needsCompute = true;\n }\n }\n\n if ('scrollEndDelay' in next && next.scrollEndDelay !== undefined) {\n const nextScrollEndDelay = toNonNegativeInt(next.scrollEndDelay, DEFAULT_SCROLL_END_DELAY);\n\n if (nextScrollEndDelay !== scrollEndDelay) scrollEndDelay = nextScrollEndDelay;\n }\n\n if ('onChange' in next) onChange = next.onChange;\n\n if ('onScrollEnd' in next) onScrollEnd = next.onScrollEnd;\n\n if ('onScrollingChange' in next) onScrollingChange = next.onScrollingChange;\n\n if (needsRebuild) rebuildOffsets();\n\n if (needsCompute || needsRebuild) computeVisible();\n }\n\n function update(next: VirtualizerUpdateOptions): void {\n if (destroyed) return;\n\n applyOptions(next);\n }\n\n function measure(index: number, size: number): void {\n if (destroyed) return;\n\n if (!Number.isFinite(index)) return;\n\n const safeIndex = Math.floor(index);\n\n if (safeIndex < 0 || safeIndex >= count) return;\n\n const safeSize = toPositiveNumber(size, -1);\n\n if (safeSize <= 0) return;\n\n const key = keyAt(safeIndex);\n\n if (measuredByKey.get(key) === safeSize) return;\n\n measuredByKey.set(key, safeSize);\n\n if (pendingBuild) return;\n\n pendingBuild = true;\n queueMicrotask(() => {\n pendingBuild = false;\n rebuildOffsets();\n computeVisible();\n });\n }\n\n function invalidate(): void {\n if (destroyed) return;\n\n measuredByKey.clear();\n rebuildOffsets();\n computeVisible();\n }\n\n function refresh(): void {\n if (destroyed) return;\n\n rebuildOffsets();\n computeVisible();\n }\n\n function scrollToIndex(index: number, options: ScrollToIndexOptions = {}): void {\n if (destroyed || count <= 0) return;\n\n const safeIndex = Number.isFinite(index) ? Math.floor(index) : 0;\n const clampedIndex = Math.max(0, Math.min(safeIndex, count - 1));\n const align = options.align ?? 'auto';\n const behavior = options.behavior ?? 'auto';\n const itemStart = startAt(clampedIndex);\n const itemSize = sizeAt(clampedIndex);\n const itemEnd = itemStart + itemSize;\n\n let targetOffset: number;\n\n if (align === 'start') {\n targetOffset = itemStart;\n } else if (align === 'end') {\n targetOffset = itemEnd - viewportSize;\n } else if (align === 'center') {\n targetOffset = itemStart - (viewportSize - itemSize) / 2;\n } else {\n const visibleStart = scrollOffset;\n const visibleEnd = visibleStart + viewportSize;\n\n if (itemStart >= visibleStart && itemEnd <= visibleEnd) return;\n\n targetOffset = itemStart < visibleStart ? itemStart : itemEnd - viewportSize;\n }\n\n axis.writeOffset(clampScrollOffset(targetOffset), behavior);\n }\n\n function scrollToOffset(offset: number, options: { behavior?: ScrollBehavior } = {}): void {\n if (destroyed) return;\n\n axis.writeOffset(clampScrollOffset(offset), options.behavior ?? 'auto');\n }\n\n function destroy(): void {\n if (destroyed) return;\n\n destroyed = true;\n clearScrollEndTimer();\n detachTarget();\n }\n\n rebuildOffsets();\n\n if (options.initialOffset !== undefined) {\n axis.writeOffset(clampScrollOffset(options.initialOffset), 'auto');\n }\n\n scrollOffset = clampScrollOffset(axis.readOffset());\n\n const detachTarget = axis.attach(onScroll, onResize);\n\n computeVisible();\n\n return {\n get count() {\n return count;\n },\n destroy,\n invalidate,\n get isScrolling() {\n return isScrolling;\n },\n get items() {\n return items;\n },\n measure,\n refresh,\n get scrollOffset() {\n return scrollOffset;\n },\n scrollToIndex,\n scrollToOffset,\n [Symbol.dispose]() {\n destroy();\n },\n get totalSize() {\n return totalSize;\n },\n update,\n };\n}\n"],"mappings":"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"}