vlist 2.0.0 → 2.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.
Files changed (77) hide show
  1. package/dist/index.js +1 -28
  2. package/dist/internals.js +1 -60
  3. package/package.json +1 -1
  4. package/dist/constants.js +0 -83
  5. package/dist/core/create.js +0 -740
  6. package/dist/core/dom.js +0 -47
  7. package/dist/core/hooks.js +0 -67
  8. package/dist/core/index.js +0 -13
  9. package/dist/core/pipeline.js +0 -307
  10. package/dist/core/pool.js +0 -42
  11. package/dist/core/scroll.js +0 -137
  12. package/dist/core/sizes.js +0 -6
  13. package/dist/core/state.js +0 -56
  14. package/dist/core/types.js +0 -7
  15. package/dist/core/velocity.js +0 -33
  16. package/dist/events/emitter.js +0 -60
  17. package/dist/events/index.js +0 -6
  18. package/dist/plugins/a11y/index.js +0 -1
  19. package/dist/plugins/a11y/plugin.js +0 -259
  20. package/dist/plugins/async/index.js +0 -12
  21. package/dist/plugins/async/manager.js +0 -568
  22. package/dist/plugins/async/placeholder.js +0 -154
  23. package/dist/plugins/async/plugin.js +0 -311
  24. package/dist/plugins/async/sparse.js +0 -540
  25. package/dist/plugins/autosize/index.js +0 -4
  26. package/dist/plugins/autosize/plugin.js +0 -185
  27. package/dist/plugins/grid/index.js +0 -5
  28. package/dist/plugins/grid/layout.js +0 -275
  29. package/dist/plugins/grid/plugin.js +0 -347
  30. package/dist/plugins/grid/renderer.js +0 -525
  31. package/dist/plugins/grid/types.js +0 -11
  32. package/dist/plugins/groups/async-bridge.js +0 -246
  33. package/dist/plugins/groups/index.js +0 -13
  34. package/dist/plugins/groups/layout.js +0 -294
  35. package/dist/plugins/groups/plugin.js +0 -571
  36. package/dist/plugins/groups/sticky.js +0 -255
  37. package/dist/plugins/groups/types.js +0 -12
  38. package/dist/plugins/masonry/index.js +0 -6
  39. package/dist/plugins/masonry/layout.js +0 -261
  40. package/dist/plugins/masonry/plugin.js +0 -381
  41. package/dist/plugins/masonry/renderer.js +0 -354
  42. package/dist/plugins/masonry/types.js +0 -9
  43. package/dist/plugins/page/index.js +0 -5
  44. package/dist/plugins/page/plugin.js +0 -166
  45. package/dist/plugins/scale/index.js +0 -4
  46. package/dist/plugins/scale/plugin.js +0 -507
  47. package/dist/plugins/scrollbar/controller.js +0 -574
  48. package/dist/plugins/scrollbar/index.js +0 -6
  49. package/dist/plugins/scrollbar/plugin.js +0 -93
  50. package/dist/plugins/scrollbar/scrollbar.js +0 -556
  51. package/dist/plugins/selection/index.js +0 -7
  52. package/dist/plugins/selection/plugin.js +0 -601
  53. package/dist/plugins/selection/state.js +0 -332
  54. package/dist/plugins/snapshots/index.js +0 -5
  55. package/dist/plugins/snapshots/plugin.js +0 -301
  56. package/dist/plugins/sortable/index.js +0 -6
  57. package/dist/plugins/sortable/plugin.js +0 -753
  58. package/dist/plugins/table/header.js +0 -501
  59. package/dist/plugins/table/index.js +0 -12
  60. package/dist/plugins/table/layout.js +0 -211
  61. package/dist/plugins/table/plugin.js +0 -391
  62. package/dist/plugins/table/renderer.js +0 -625
  63. package/dist/plugins/table/types.js +0 -12
  64. package/dist/plugins/transition/index.js +0 -5
  65. package/dist/plugins/transition/plugin.js +0 -405
  66. package/dist/rendering/aria.js +0 -23
  67. package/dist/rendering/index.js +0 -18
  68. package/dist/rendering/measured.js +0 -98
  69. package/dist/rendering/renderer.js +0 -586
  70. package/dist/rendering/scale.js +0 -267
  71. package/dist/rendering/scroll.js +0 -71
  72. package/dist/rendering/sizes.js +0 -193
  73. package/dist/rendering/sort.js +0 -65
  74. package/dist/rendering/viewport.js +0 -268
  75. package/dist/types.js +0 -5
  76. package/dist/utils/padding.js +0 -49
  77. package/dist/utils/stats.js +0 -124
@@ -1,405 +0,0 @@
1
- /**
2
- * vlist v2 — Transition Plugin
3
- *
4
- * FLIP-based enter/exit animations for insertItem and removeItem.
5
- * Without this plugin, insert/remove are instantaneous.
6
- *
7
- * Priority: 45 (before selection, after most layout plugins)
8
- */
9
- const DEFAULT_DURATION = 200;
10
- const MAX_DURATION = 1000;
11
- const DEFAULT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
12
- function resolveTiming(base, override) {
13
- if (override === false)
14
- return null;
15
- return {
16
- duration: Math.min(override?.duration ?? base.duration, MAX_DURATION),
17
- easing: override?.easing ?? base.easing,
18
- };
19
- }
20
- // =============================================================================
21
- // Factory
22
- // =============================================================================
23
- export function transition(config) {
24
- const baseDuration = config?.duration ?? DEFAULT_DURATION;
25
- const baseEasing = config?.easing ?? DEFAULT_EASING;
26
- const base = { duration: baseDuration, easing: baseEasing };
27
- const removeTiming = resolveTiming(base, config?.remove);
28
- const insertTiming = resolveTiming(base, config?.insert);
29
- let removePending = null;
30
- let addPending = null;
31
- return {
32
- name: "transition",
33
- conflicts: ["grid", "table", "masonry"],
34
- priority: 45,
35
- setup(ctx) {
36
- const { sizeCache: sc, emitter } = ctx;
37
- const cfg = ctx.config;
38
- const dom = ctx.dom;
39
- const state = ctx.getState();
40
- const origin = cfg.reverse ? "bottom center" : "top center";
41
- const prop = cfg.horizontal ? "translateX" : "translateY";
42
- const toLayout = ctx.getMethod("_dataToLayoutIndex") ?? null;
43
- const dataToLayout = (dataIndex) => toLayout ? toLayout(dataIndex) : dataIndex;
44
- const findById = (id) => {
45
- const eid = String(id).replace(/"/g, '\\"');
46
- const el = dom.content.querySelector(`[data-id="${eid}"]`);
47
- const layoutIndex = el ? parseInt(el.dataset.index ?? "-1", 10) : -1;
48
- return { el, layoutIndex };
49
- };
50
- const commitStyles = () => {
51
- const children = dom.content.children;
52
- for (let i = 0; i < children.length; i++) {
53
- children[i].style.transition = "none";
54
- }
55
- dom.content.offsetHeight;
56
- for (let i = 0; i < children.length; i++) {
57
- children[i].style.transition = "";
58
- }
59
- };
60
- // ── Animated removeItem ──────────────────────────────────
61
- if (removeTiming) {
62
- const removeItem = (id) => {
63
- if (removePending)
64
- removePending();
65
- const active = typeof document !== "undefined" ? document.activeElement : null;
66
- const focIdx = active && dom.content.contains(active)
67
- ? parseInt(active.dataset?.index ?? "-1", 10)
68
- : -1;
69
- const found = findById(id);
70
- const layoutIndex = found.layoutIndex;
71
- const removedEl = found.el;
72
- const itemSize = layoutIndex >= 0 ? sc.getSize(layoutIndex) : 0;
73
- const originalOffset = layoutIndex >= 0 ? sc.getOffset(layoutIndex) : 0;
74
- if (!removedEl || layoutIndex < 0) {
75
- const result = ctx.removeItemById(id);
76
- if (result < 0)
77
- return false;
78
- ctx.forceRender();
79
- commitStyles();
80
- emitter.emit("data:change", { type: "remove", id });
81
- if (focIdx >= 0) {
82
- const t = state.totalItems;
83
- if (t > 0)
84
- ctx.getRenderedElement(Math.min(focIdx, t - 1))?.focus();
85
- }
86
- emitter.emit("remove:end", { id });
87
- return true;
88
- }
89
- // FIRST — clone before removal
90
- const exitClone = removedEl.cloneNode(true);
91
- exitClone.style.pointerEvents = "none";
92
- exitClone.style.overflow = "hidden";
93
- exitClone.removeAttribute("data-index");
94
- exitClone.removeAttribute("data-id");
95
- exitClone.removeAttribute("id");
96
- exitClone.removeAttribute("aria-selected");
97
- exitClone.classList.remove(`${cfg.classPrefix}-item--selected`);
98
- const scrollProp = cfg.horizontal ? "scrollLeft" : "scrollTop";
99
- const oldScroll = dom.viewport[scrollProp];
100
- // LAST — remove data + reconcile
101
- const result = ctx.removeItemById(id);
102
- if (result < 0)
103
- return false;
104
- ctx.forceRender();
105
- commitStyles();
106
- emitter.emit("data:change", { type: "remove", id });
107
- const scrollDelta = oldScroll - dom.viewport[scrollProp];
108
- exitClone.style.zIndex = "1";
109
- dom.content.appendChild(exitClone);
110
- const rt = removeTiming;
111
- const animOptions = { duration: rt.duration, easing: rt.easing };
112
- const animations = [];
113
- const cloneStart = Math.round(originalOffset - scrollDelta);
114
- animations.push(exitClone.animate([
115
- { transform: `${prop}(${cloneStart}px) scaleY(1)`, opacity: 1, transformOrigin: origin },
116
- { transform: `${prop}(${Math.round(originalOffset)}px) scaleY(0)`, opacity: 0, transformOrigin: origin },
117
- ], animOptions));
118
- const allOnClamp = scrollDelta > 0;
119
- const itemChildren = dom.content.children;
120
- for (let i = 0; i < itemChildren.length; i++) {
121
- const el = itemChildren[i];
122
- if (el === exitClone)
123
- continue;
124
- const idx = parseInt(el.dataset?.index ?? "-1", 10);
125
- if (idx < 0 || (!allOnClamp && idx < layoutIndex))
126
- continue;
127
- const newOffset = sc.getOffset(idx);
128
- const oldVisual = idx >= layoutIndex
129
- ? Math.round(newOffset + itemSize - scrollDelta)
130
- : Math.round(newOffset - scrollDelta);
131
- const newVisual = Math.round(newOffset);
132
- if (oldVisual === newVisual)
133
- continue;
134
- animations.push(el.animate([
135
- { transform: `${prop}(${oldVisual}px)` },
136
- { transform: `${prop}(${newVisual}px)` },
137
- ], animOptions));
138
- }
139
- let settled = false;
140
- const finalize = () => {
141
- if (settled)
142
- return;
143
- settled = true;
144
- removePending = null;
145
- exitClone.remove();
146
- for (const a of animations) {
147
- if (a.playState !== "finished")
148
- a.cancel();
149
- }
150
- if (focIdx >= 0) {
151
- const t = state.totalItems;
152
- if (t > 0)
153
- ctx.getRenderedElement(Math.min(focIdx, t - 1))?.focus();
154
- }
155
- emitter.emit("remove:end", { id });
156
- };
157
- removePending = finalize;
158
- Promise.all(animations.map(a => a.finished)).then(finalize, finalize);
159
- setTimeout(finalize, rt.duration + 50);
160
- return true;
161
- };
162
- // ── Batch removeItems ──────────────────────────────────
163
- const removeItems = (ids) => {
164
- if (ids.length === 0)
165
- return 0;
166
- if (ids.length === 1)
167
- return removeItem(ids[0]) ? 1 : 0;
168
- if (removePending)
169
- removePending();
170
- if (addPending)
171
- addPending();
172
- const active = typeof document !== "undefined" ? document.activeElement : null;
173
- const focIdx = active && dom.content.contains(active)
174
- ? parseInt(active.dataset?.index ?? "-1", 10)
175
- : -1;
176
- const targets = [];
177
- for (const id of ids) {
178
- const found = findById(id);
179
- targets.push({
180
- id,
181
- layoutIndex: found.layoutIndex,
182
- el: found.el,
183
- size: found.layoutIndex >= 0 ? sc.getSize(found.layoutIndex) : 0,
184
- offset: found.layoutIndex >= 0 ? sc.getOffset(found.layoutIndex) : 0,
185
- });
186
- }
187
- targets.sort((a, b) => b.layoutIndex - a.layoutIndex);
188
- const clones = [];
189
- for (const t of targets) {
190
- if (!t.el || t.layoutIndex < 0)
191
- continue;
192
- const clone = t.el.cloneNode(true);
193
- clone.style.pointerEvents = "none";
194
- clone.style.overflow = "hidden";
195
- clone.removeAttribute("data-index");
196
- clone.removeAttribute("data-id");
197
- clone.removeAttribute("id");
198
- clone.removeAttribute("aria-selected");
199
- clone.classList.remove(`${cfg.classPrefix}-item--selected`);
200
- clones.push({ clone, offset: t.offset, size: t.size, layoutIndex: t.layoutIndex });
201
- }
202
- const oldOffsetById = new Map();
203
- const children = dom.content.children;
204
- for (let i = 0; i < children.length; i++) {
205
- const el = children[i];
206
- const elId = el.dataset?.id;
207
- const idx = parseInt(el.dataset?.index ?? "-1", 10);
208
- if (elId && idx >= 0)
209
- oldOffsetById.set(elId, sc.getOffset(idx));
210
- }
211
- const scrollProp = cfg.horizontal ? "scrollLeft" : "scrollTop";
212
- const oldScroll = dom.viewport[scrollProp];
213
- // Remove all items (descending order preserved)
214
- const removedIds = [];
215
- for (const t of targets) {
216
- const result = ctx.removeItemById(t.id);
217
- if (result >= 0)
218
- removedIds.push(t.id);
219
- }
220
- if (removedIds.length === 0)
221
- return 0;
222
- ctx.forceRender();
223
- commitStyles();
224
- for (const rid of removedIds) {
225
- emitter.emit("data:change", { type: "remove", id: rid });
226
- }
227
- if (clones.length === 0) {
228
- if (focIdx >= 0) {
229
- const t = state.totalItems;
230
- if (t > 0)
231
- ctx.getRenderedElement(Math.min(focIdx, t - 1))?.focus();
232
- }
233
- for (const rid of removedIds)
234
- emitter.emit("remove:end", { id: rid });
235
- return removedIds.length;
236
- }
237
- const scrollDelta = oldScroll - dom.viewport[scrollProp];
238
- const rt = removeTiming;
239
- const animOptions = { duration: rt.duration, easing: rt.easing };
240
- const animations = [];
241
- clones.sort((a, b) => a.layoutIndex - b.layoutIndex);
242
- let removedSizeAbove = 0;
243
- for (const c of clones) {
244
- c.clone.style.zIndex = "1";
245
- dom.content.appendChild(c.clone);
246
- const cloneStart = Math.round(c.offset - scrollDelta);
247
- const shiftedEnd = Math.round(c.offset - removedSizeAbove);
248
- animations.push(c.clone.animate([
249
- { transform: `${prop}(${cloneStart}px) scaleY(1)`, opacity: 1, transformOrigin: origin },
250
- { transform: `${prop}(${shiftedEnd}px) scaleY(0)`, opacity: 0, transformOrigin: origin },
251
- ], animOptions));
252
- removedSizeAbove += c.size;
253
- }
254
- const cloneSet = new Set(clones.map(c => c.clone));
255
- const itemChildren = dom.content.children;
256
- for (let i = 0; i < itemChildren.length; i++) {
257
- const el = itemChildren[i];
258
- if (cloneSet.has(el))
259
- continue;
260
- const elId = el.dataset?.id;
261
- const idx = parseInt(el.dataset?.index ?? "-1", 10);
262
- if (!elId || idx < 0)
263
- continue;
264
- const oldOffset = oldOffsetById.get(elId);
265
- if (oldOffset === undefined)
266
- continue;
267
- const newOffset = sc.getOffset(idx);
268
- const oldVisual = Math.round(oldOffset - scrollDelta);
269
- const newVisual = Math.round(newOffset);
270
- if (oldVisual === newVisual)
271
- continue;
272
- animations.push(el.animate([
273
- { transform: `${prop}(${oldVisual}px)` },
274
- { transform: `${prop}(${newVisual}px)` },
275
- ], animOptions));
276
- }
277
- let settled = false;
278
- const finalize = () => {
279
- if (settled)
280
- return;
281
- settled = true;
282
- removePending = null;
283
- for (const { clone } of clones)
284
- clone.remove();
285
- for (const a of animations) {
286
- if (a.playState !== "finished")
287
- a.cancel();
288
- }
289
- if (focIdx >= 0) {
290
- const t = state.totalItems;
291
- if (t > 0)
292
- ctx.getRenderedElement(Math.min(focIdx, t - 1))?.focus();
293
- }
294
- for (const rid of removedIds)
295
- emitter.emit("remove:end", { id: rid });
296
- };
297
- removePending = finalize;
298
- Promise.all(animations.map(a => a.finished)).then(finalize, finalize);
299
- setTimeout(finalize, rt.duration + 50);
300
- return removedIds.length;
301
- };
302
- ctx.registerMethod("removeItem", removeItem);
303
- ctx.registerMethod("removeItems", removeItems);
304
- }
305
- // ── Animated insertItem ────────────────────────────────────
306
- if (insertTiming) {
307
- const insertItem = (item, index) => {
308
- if (addPending)
309
- addPending();
310
- if (removePending)
311
- removePending();
312
- const insertDataIndex = index ?? 0;
313
- const oldOffsetById = new Map();
314
- const children = dom.content.children;
315
- for (let i = 0; i < children.length; i++) {
316
- const el = children[i];
317
- const id = el.dataset?.id;
318
- const idx = parseInt(el.dataset?.index ?? "-1", 10);
319
- if (id && idx >= 0)
320
- oldOffsetById.set(id, sc.getOffset(idx));
321
- }
322
- const scrollProp = cfg.horizontal ? "scrollLeft" : "scrollTop";
323
- const sizeProp = cfg.horizontal ? "scrollWidth" : "scrollHeight";
324
- const clientProp = cfg.horizontal ? "clientWidth" : "clientHeight";
325
- const oldScroll = dom.viewport[scrollProp];
326
- const oldMaxScroll = dom.viewport[sizeProp] - dom.viewport[clientProp];
327
- const wasAtEnd = oldScroll >= oldMaxScroll - 1;
328
- ctx.insertItemAt(item, insertDataIndex);
329
- ctx.forceRender();
330
- commitStyles();
331
- emitter.emit("data:change", { type: "insert", id: item.id });
332
- let scrollDelta = dom.viewport[scrollProp] - oldScroll;
333
- const postInsertLayoutIndex = dataToLayout(insertDataIndex);
334
- if (cfg.reverse && scrollDelta === 0 && wasAtEnd) {
335
- dom.viewport[scrollProp] = dom.viewport[sizeProp] - dom.viewport[clientProp];
336
- const bumped = dom.viewport[scrollProp];
337
- state.prevScrollPosition = state.scrollPosition;
338
- state.scrollPosition = bumped;
339
- scrollDelta = bumped - oldScroll;
340
- if (scrollDelta > 0) {
341
- ctx.forceRender();
342
- commitStyles();
343
- }
344
- }
345
- const newEl = ctx.getRenderedElement(postInsertLayoutIndex);
346
- const at = insertTiming;
347
- const animOptions = { duration: at.duration, easing: at.easing };
348
- const animations = [];
349
- if (newEl) {
350
- const newOffset = sc.getOffset(postInsertLayoutIndex);
351
- animations.push(newEl.animate([
352
- { transform: `${prop}(${Math.round(newOffset)}px) scaleY(0)`, opacity: 0, transformOrigin: origin },
353
- { transform: `${prop}(${Math.round(newOffset)}px) scaleY(1)`, opacity: 1, transformOrigin: origin },
354
- ], animOptions));
355
- }
356
- const postChildren = dom.content.children;
357
- for (let i = 0; i < postChildren.length; i++) {
358
- const el = postChildren[i];
359
- if (el === newEl)
360
- continue;
361
- const id = el.dataset?.id;
362
- const idx = parseInt(el.dataset?.index ?? "-1", 10);
363
- if (!id || idx < 0)
364
- continue;
365
- const oldOffset = oldOffsetById.get(id);
366
- if (oldOffset === undefined)
367
- continue;
368
- const newOffset = sc.getOffset(idx);
369
- const visualOld = Math.round(oldOffset + scrollDelta);
370
- const visualNew = Math.round(newOffset);
371
- if (visualOld === visualNew)
372
- continue;
373
- animations.push(el.animate([
374
- { transform: `${prop}(${visualOld}px)` },
375
- { transform: `${prop}(${visualNew}px)` },
376
- ], animOptions));
377
- }
378
- if (animations.length === 0)
379
- return;
380
- let settled = false;
381
- const finalize = () => {
382
- if (settled)
383
- return;
384
- settled = true;
385
- addPending = null;
386
- for (const a of animations) {
387
- if (a.playState !== "finished")
388
- a.cancel();
389
- }
390
- };
391
- addPending = finalize;
392
- Promise.all(animations.map(a => a.finished)).then(finalize, finalize);
393
- setTimeout(finalize, at.duration + 50);
394
- };
395
- ctx.registerMethod("insertItem", insertItem);
396
- }
397
- },
398
- destroy() {
399
- if (removePending)
400
- removePending();
401
- if (addPending)
402
- addPending();
403
- },
404
- };
405
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * vlist/rendering -- ARIA position resolvers
3
- *
4
- * Lazy-resolves _getTotal and _layoutToDataIndex from the methods map
5
- * so that aria-setsize uses data total (not layout total including
6
- * group headers) and aria-posinset uses data-space position.
7
- */
8
- export const createAriaResolvers = (methods, fallbackTotal) => {
9
- let gt;
10
- let l2d;
11
- return {
12
- getSetSize: () => {
13
- if (gt === undefined)
14
- gt = methods.get("_getTotal") ?? null;
15
- return gt ? gt() : fallbackTotal();
16
- },
17
- getPosInSet: (layoutIndex) => {
18
- if (l2d === undefined)
19
- l2d = methods.get("_layoutToDataIndex") ?? null;
20
- return l2d ? l2d(layoutIndex) + 1 : layoutIndex + 1;
21
- },
22
- };
23
- };
@@ -1,18 +0,0 @@
1
- /**
2
- * vlist - Rendering Domain
3
- * Rendering, virtualization, and scaling for large datasets
4
- */
5
- // Size Cache (dimension-agnostic for vertical/horizontal scrolling)
6
- export { createSizeCache, countVisibleItems, countItemsFittingFromBottom, getOffsetForVirtualIndex, } from "./sizes";
7
- // Measured Size Cache (auto-measurement for Mode B)
8
- export { createMeasuredSizeCache, } from "./measured";
9
- // DOM Sort (accessibility — reorder children on scroll idle)
10
- export { sortRenderedDOM } from "./sort";
11
- // Renderer
12
- export { createRenderer, createDOMStructure, updateContentHeight, updateContentWidth, resolveContainer, getContainerDimensions, } from "./renderer";
13
- // Viewport Scrolling
14
- export { createViewportState, updateViewportState, updateViewportSize, updateViewportItems, calculateRenderRange, calculateTotalSize, calculateActualSize, calculateItemOffset, calculateScrollToIndex, clampScrollPosition, getScrollDirection, rangesEqual, isInRange, getRangeCount, rangeToIndices, diffRanges, getSimpleCompressionState, simpleVisibleRange, simpleScrollToIndex, NO_COMPRESSION, } from "./viewport";
15
- // Scale (large dataset handling - used by withScale feature)
16
- export { MAX_VIRTUAL_SIZE, getCompressionState, getCompressionState as getCompression, needsCompression, getMaxItemsWithoutCompression, getCompressionInfo, calculateCompressedVisibleRange, calculateCompressedRenderRange, calculateCompressedItemPosition, calculateCompressedScrollToIndex, calculateIndexFromScrollPosition, } from "./scale";
17
- // Smart Edge Scroll (shared by core baseline and selection feature)
18
- export { scrollToFocus, scrollToFocusSimple, } from "./scroll";
@@ -1,98 +0,0 @@
1
- // src/rendering/measured.ts
2
- /**
3
- * vlist - Measured Size Cache
4
- * Auto-measurement support for items with unknown sizes (Mode B)
5
- *
6
- * Wraps the existing variable SizeCache with measurement tracking.
7
- * Once an item is measured, it behaves identically to Mode A (known size).
8
- * Unmeasured items use the estimated size as a fallback.
9
- *
10
- * Fully axis-neutral: works identically for vertical (estimatedHeight)
11
- * and horizontal (estimatedWidth) orientations. This cache stores plain
12
- * numbers representing the main-axis dimension — it never knows whether
13
- * those numbers are heights or widths. The axis-specific translation
14
- * happens in builder/core.ts at the DOM boundary.
15
- *
16
- * Implements the SizeCache interface so all downstream code
17
- * (viewport, scale, features) works unchanged.
18
- */
19
- import { createSizeCache } from "./sizes";
20
- // =============================================================================
21
- // Factory
22
- // =============================================================================
23
- /**
24
- * Create a measured size cache for auto-measurement (Mode B)
25
- *
26
- * Works for both orientations:
27
- * - Vertical: estimatedSize = estimatedHeight, measures block size
28
- * - Horizontal: estimatedSize = estimatedWidth, measures inline size
29
- *
30
- * The cache itself is axis-neutral — it only stores numbers. The caller
31
- * (builder/core.ts) is responsible for reading the correct axis from
32
- * the config and from ResizeObserver entries (blockSize vs inlineSize).
33
- *
34
- * Internally maintains a Map of measured sizes keyed by item index.
35
- * Unmeasured items fall back to the estimated size. The underlying
36
- * prefix-sum array is rebuilt when measurements change.
37
- *
38
- * The size function fed into the variable SizeCache becomes:
39
- * (index) => measuredSizes.has(index) ? measuredSizes.get(index) : estimatedSize
40
- *
41
- * This means all existing viewport, compression, and range calculations
42
- * work unchanged — they only see a SizeCache with variable sizes.
43
- */
44
- export const createMeasuredSizeCache = (estimatedSize, initialTotal) => {
45
- const measuredSizes = new Map();
46
- // Size function: return measured size if available, else estimated
47
- const sizeFn = (index) => measuredSizes.get(index) ?? estimatedSize;
48
- // Create the underlying variable SizeCache with our size function
49
- let inner = createSizeCache(sizeFn, initialTotal);
50
- return {
51
- // ── SizeCache interface ──────────────────────────────────────
52
- getOffset(index) {
53
- return inner.getOffset(index);
54
- },
55
- getSize(index) {
56
- return sizeFn(index);
57
- },
58
- indexAtOffset(offset) {
59
- return inner.indexAtOffset(offset);
60
- },
61
- getTotalSize() {
62
- return inner.getTotalSize();
63
- },
64
- getTotal() {
65
- return inner.getTotal();
66
- },
67
- rebuild(totalItems) {
68
- // Discard measured sizes for indices that no longer exist
69
- if (totalItems < inner.getTotal()) {
70
- for (const index of measuredSizes.keys()) {
71
- if (index >= totalItems)
72
- measuredSizes.delete(index);
73
- }
74
- }
75
- // Rebuild the underlying variable cache with current size function
76
- // We must recreate because createSizeCache captures sizeFn at creation,
77
- // but our sizeFn closes over measuredSizes which is mutable — so a
78
- // rebuild of the inner cache re-evaluates all prefix sums.
79
- inner = createSizeCache(sizeFn, totalItems);
80
- },
81
- isVariable() {
82
- return true;
83
- },
84
- // ── MeasuredSizeCache extensions ─────────────────────────────
85
- setMeasuredSize(index, size) {
86
- measuredSizes.set(index, size);
87
- },
88
- isMeasured(index) {
89
- return measuredSizes.has(index);
90
- },
91
- getEstimatedSize() {
92
- return estimatedSize;
93
- },
94
- measuredCount() {
95
- return measuredSizes.size;
96
- },
97
- };
98
- };