vlist 1.9.1 → 2.0.0

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 (182) hide show
  1. package/README.github.md +104 -97
  2. package/README.md +46 -33
  3. package/dist/constants.d.ts +11 -6
  4. package/dist/constants.js +83 -0
  5. package/dist/core/create.d.ts +10 -0
  6. package/dist/core/create.js +740 -0
  7. package/dist/core/dom.d.ts +8 -0
  8. package/dist/core/dom.js +47 -0
  9. package/dist/core/hooks.d.ts +16 -0
  10. package/dist/core/hooks.js +67 -0
  11. package/dist/core/index.d.ts +17 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/pipeline.d.ts +51 -0
  14. package/dist/core/pipeline.js +307 -0
  15. package/dist/core/pool.d.ts +9 -0
  16. package/dist/core/pool.js +42 -0
  17. package/dist/core/scroll.d.ts +32 -0
  18. package/dist/core/scroll.js +137 -0
  19. package/dist/core/sizes.d.ts +8 -0
  20. package/dist/core/sizes.js +6 -0
  21. package/dist/core/state.d.ts +47 -0
  22. package/dist/core/state.js +56 -0
  23. package/dist/core/types.d.ts +187 -0
  24. package/dist/core/types.js +7 -0
  25. package/dist/{builder → core}/velocity.d.ts +1 -1
  26. package/dist/core/velocity.js +33 -0
  27. package/dist/events/emitter.js +60 -0
  28. package/dist/events/index.js +6 -0
  29. package/dist/index.d.ts +28 -19
  30. package/dist/index.js +28 -1
  31. package/dist/internals.d.ts +11 -7
  32. package/dist/internals.js +60 -1
  33. package/dist/plugins/a11y/index.d.ts +2 -0
  34. package/dist/plugins/a11y/index.js +1 -0
  35. package/dist/plugins/a11y/plugin.d.ts +13 -0
  36. package/dist/plugins/a11y/plugin.js +259 -0
  37. package/dist/{features → plugins}/async/index.d.ts +1 -1
  38. package/dist/plugins/async/index.js +12 -0
  39. package/dist/{features → plugins}/async/manager.d.ts +5 -1
  40. package/dist/plugins/async/manager.js +568 -0
  41. package/dist/plugins/async/placeholder.js +154 -0
  42. package/dist/plugins/async/plugin.d.ts +48 -0
  43. package/dist/plugins/async/plugin.js +311 -0
  44. package/dist/plugins/async/sparse.js +540 -0
  45. package/dist/plugins/autosize/index.d.ts +5 -0
  46. package/dist/plugins/autosize/index.js +4 -0
  47. package/dist/plugins/autosize/plugin.d.ts +19 -0
  48. package/dist/plugins/autosize/plugin.js +185 -0
  49. package/dist/plugins/grid/index.d.ts +7 -0
  50. package/dist/plugins/grid/index.js +5 -0
  51. package/dist/plugins/grid/layout.js +275 -0
  52. package/dist/plugins/grid/plugin.d.ts +23 -0
  53. package/dist/plugins/grid/plugin.js +347 -0
  54. package/dist/plugins/grid/renderer.js +525 -0
  55. package/dist/plugins/grid/types.js +11 -0
  56. package/dist/plugins/groups/async-bridge.js +246 -0
  57. package/dist/{features → plugins}/groups/index.d.ts +1 -1
  58. package/dist/plugins/groups/index.js +13 -0
  59. package/dist/plugins/groups/layout.js +294 -0
  60. package/dist/plugins/groups/plugin.d.ts +22 -0
  61. package/dist/plugins/groups/plugin.js +571 -0
  62. package/dist/plugins/groups/sticky.js +255 -0
  63. package/dist/plugins/groups/types.js +12 -0
  64. package/dist/plugins/masonry/index.d.ts +8 -0
  65. package/dist/plugins/masonry/index.js +6 -0
  66. package/dist/plugins/masonry/layout.js +261 -0
  67. package/dist/plugins/masonry/plugin.d.ts +32 -0
  68. package/dist/plugins/masonry/plugin.js +381 -0
  69. package/dist/plugins/masonry/renderer.js +354 -0
  70. package/dist/plugins/masonry/types.js +9 -0
  71. package/dist/plugins/page/index.d.ts +5 -0
  72. package/dist/plugins/page/index.js +5 -0
  73. package/dist/plugins/page/plugin.d.ts +21 -0
  74. package/dist/plugins/page/plugin.js +166 -0
  75. package/dist/plugins/scale/index.d.ts +5 -0
  76. package/dist/plugins/scale/index.js +4 -0
  77. package/dist/plugins/scale/plugin.d.ts +24 -0
  78. package/dist/plugins/scale/plugin.js +507 -0
  79. package/dist/plugins/scrollbar/controller.js +574 -0
  80. package/dist/plugins/scrollbar/index.d.ts +7 -0
  81. package/dist/plugins/scrollbar/index.js +6 -0
  82. package/dist/plugins/scrollbar/plugin.d.ts +20 -0
  83. package/dist/plugins/scrollbar/plugin.js +93 -0
  84. package/dist/plugins/scrollbar/scrollbar.js +556 -0
  85. package/dist/plugins/selection/index.d.ts +6 -0
  86. package/dist/plugins/selection/index.js +7 -0
  87. package/dist/plugins/selection/plugin.d.ts +16 -0
  88. package/dist/plugins/selection/plugin.js +601 -0
  89. package/dist/{features → plugins}/selection/state.d.ts +8 -0
  90. package/dist/plugins/selection/state.js +332 -0
  91. package/dist/plugins/snapshots/index.d.ts +5 -0
  92. package/dist/plugins/snapshots/index.js +5 -0
  93. package/dist/plugins/snapshots/plugin.d.ts +17 -0
  94. package/dist/plugins/snapshots/plugin.js +301 -0
  95. package/dist/plugins/sortable/index.d.ts +6 -0
  96. package/dist/plugins/sortable/index.js +6 -0
  97. package/dist/plugins/sortable/plugin.d.ts +34 -0
  98. package/dist/plugins/sortable/plugin.js +753 -0
  99. package/dist/plugins/table/header.js +501 -0
  100. package/dist/{features → plugins}/table/index.d.ts +1 -1
  101. package/dist/plugins/table/index.js +12 -0
  102. package/dist/plugins/table/layout.js +211 -0
  103. package/dist/plugins/table/plugin.d.ts +20 -0
  104. package/dist/plugins/table/plugin.js +391 -0
  105. package/dist/plugins/table/renderer.js +625 -0
  106. package/dist/plugins/table/types.js +12 -0
  107. package/dist/plugins/transition/index.d.ts +5 -0
  108. package/dist/plugins/transition/index.js +5 -0
  109. package/dist/plugins/transition/plugin.d.ts +22 -0
  110. package/dist/plugins/transition/plugin.js +405 -0
  111. package/dist/rendering/aria.js +23 -0
  112. package/dist/rendering/index.js +18 -0
  113. package/dist/rendering/measured.js +98 -0
  114. package/dist/rendering/renderer.js +586 -0
  115. package/dist/rendering/scale.js +267 -0
  116. package/dist/rendering/scroll.js +71 -0
  117. package/dist/rendering/sizes.js +193 -0
  118. package/dist/rendering/sort.js +65 -0
  119. package/dist/rendering/viewport.js +268 -0
  120. package/dist/size.json +1 -1
  121. package/dist/types.js +5 -0
  122. package/dist/utils/padding.d.ts +2 -4
  123. package/dist/utils/padding.js +49 -0
  124. package/dist/utils/stats.js +124 -0
  125. package/dist/vlist-grid.css +1 -1
  126. package/dist/vlist-masonry.css +1 -1
  127. package/dist/vlist-table.css +1 -1
  128. package/dist/vlist.css +1 -1
  129. package/package.json +9 -4
  130. package/dist/builder/a11y.d.ts +0 -16
  131. package/dist/builder/api.d.ts +0 -21
  132. package/dist/builder/context.d.ts +0 -36
  133. package/dist/builder/core.d.ts +0 -16
  134. package/dist/builder/data.d.ts +0 -71
  135. package/dist/builder/dom.d.ts +0 -15
  136. package/dist/builder/index.d.ts +0 -25
  137. package/dist/builder/materialize.d.ts +0 -166
  138. package/dist/builder/pool.d.ts +0 -10
  139. package/dist/builder/range.d.ts +0 -10
  140. package/dist/builder/scroll.d.ts +0 -24
  141. package/dist/builder/types.d.ts +0 -512
  142. package/dist/features/async/feature.d.ts +0 -72
  143. package/dist/features/autosize/feature.d.ts +0 -34
  144. package/dist/features/autosize/index.d.ts +0 -2
  145. package/dist/features/grid/feature.d.ts +0 -48
  146. package/dist/features/grid/index.d.ts +0 -9
  147. package/dist/features/groups/feature.d.ts +0 -75
  148. package/dist/features/masonry/feature.d.ts +0 -45
  149. package/dist/features/masonry/index.d.ts +0 -9
  150. package/dist/features/page/feature.d.ts +0 -109
  151. package/dist/features/page/index.d.ts +0 -9
  152. package/dist/features/scale/feature.d.ts +0 -42
  153. package/dist/features/scale/index.d.ts +0 -10
  154. package/dist/features/scrollbar/feature.d.ts +0 -81
  155. package/dist/features/scrollbar/index.d.ts +0 -8
  156. package/dist/features/selection/feature.d.ts +0 -91
  157. package/dist/features/selection/index.d.ts +0 -7
  158. package/dist/features/snapshots/feature.d.ts +0 -79
  159. package/dist/features/snapshots/index.d.ts +0 -9
  160. package/dist/features/sortable/feature.d.ts +0 -101
  161. package/dist/features/sortable/index.d.ts +0 -6
  162. package/dist/features/table/feature.d.ts +0 -67
  163. package/dist/features/transition/feature.d.ts +0 -30
  164. package/dist/features/transition/index.d.ts +0 -9
  165. /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
  166. /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
  167. /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
  168. /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
  169. /package/dist/{features → plugins}/grid/types.d.ts +0 -0
  170. /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
  171. /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
  172. /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
  173. /package/dist/{features → plugins}/groups/types.d.ts +0 -0
  174. /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
  175. /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
  176. /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
  177. /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
  178. /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
  179. /package/dist/{features → plugins}/table/header.d.ts +0 -0
  180. /package/dist/{features → plugins}/table/layout.d.ts +0 -0
  181. /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
  182. /package/dist/{features → plugins}/table/types.d.ts +0 -0
@@ -0,0 +1,354 @@
1
+ /**
2
+ * vlist - Masonry Renderer
3
+ * Renders items in a masonry/Pinterest-style layout with absolute positioning.
4
+ *
5
+ * Unlike grid renderer (which uses row-based positioning), masonry renderer
6
+ * positions each item using its pre-calculated coordinates from the layout phase.
7
+ *
8
+ * Key differences from grid:
9
+ * - Items positioned using cached x/y coordinates (not row/col calculations)
10
+ * - Each item can have different height/width
11
+ * - No row alignment - items flow into shortest column/row
12
+ * - Visibility determined by checking each item's absolute position
13
+ *
14
+ * Performance:
15
+ * - Element pooling avoids createElement cost
16
+ * - Template re-evaluation skipped when item data + state unchanged
17
+ * - O(1) Set-based visibility diffing (not O(n) .some())
18
+ * - Release grace period prevents boundary thrashing (hover blink, transition replay)
19
+ * - Released elements removed from DOM immediately
20
+ */
21
+ import { sortRenderedDOM } from "../../rendering/sort";
22
+ const createElementPool = (maxSize = 200) => {
23
+ const pool = [];
24
+ const acquire = () => {
25
+ const element = pool.pop();
26
+ if (element) {
27
+ return element;
28
+ }
29
+ const newElement = document.createElement("div");
30
+ newElement.setAttribute("role", "option");
31
+ return newElement;
32
+ };
33
+ const release = (element) => {
34
+ // Remove from DOM immediately — prevents blank divs in the container
35
+ element.remove();
36
+ if (pool.length < maxSize) {
37
+ element.className = "";
38
+ element.textContent = "";
39
+ element.removeAttribute("style");
40
+ element.removeAttribute("data-index");
41
+ element.removeAttribute("data-id");
42
+ element.removeAttribute("data-lane");
43
+ pool.push(element);
44
+ }
45
+ };
46
+ const clear = () => {
47
+ pool.length = 0;
48
+ };
49
+ return { acquire, release, clear };
50
+ };
51
+ // =============================================================================
52
+ // Release grace period
53
+ // =============================================================================
54
+ /**
55
+ * Number of render cycles to keep an item alive after it leaves the visible set.
56
+ * Prevents boundary thrashing: items near the overscan edge aren't recycled
57
+ * on small scroll deltas, preserving DOM element hover state and avoiding
58
+ * CSS transition replays.
59
+ */
60
+ const RELEASE_GRACE = 1;
61
+ // =============================================================================
62
+ // Masonry Renderer Factory
63
+ // =============================================================================
64
+ /**
65
+ * Create a masonry renderer for managing DOM elements with absolute positioning.
66
+ *
67
+ * @param itemsContainer - The DOM element that holds rendered items
68
+ * @param template - Item template function
69
+ * @param classPrefix - CSS class prefix
70
+ * @param isHorizontal - Whether layout is horizontal (scrolls right)
71
+ * @param totalItemsGetter - Optional getter for total item count (for aria-setsize)
72
+ * @param ariaIdPrefix - Optional unique prefix for element IDs (for aria-activedescendant)
73
+ */
74
+ export const createMasonryRenderer = (itemsContainer, template, classPrefix, isHorizontal = false, totalItemsGetter, ariaIdPrefix, ariaPosInSetGetter, interactive) => {
75
+ const pool = createElementPool();
76
+ const rendered = new Map();
77
+ // ── Reusable visibleSet — cleared and repopulated each frame (no allocation) ──
78
+ const visibleSet = new Set();
79
+ // ── Frame counter for release grace period ──
80
+ let frameCounter = 0;
81
+ // Track aria-setsize to avoid redundant updates
82
+ let lastAriaSetSize = "";
83
+ let lastAriaTotal = -1;
84
+ // Reusable item state to avoid allocation per render
85
+ const reusableItemState = { selected: false, focused: false };
86
+ const getItemState = (isSelected, isFocused) => {
87
+ reusableItemState.selected = isSelected;
88
+ reusableItemState.focused = isFocused;
89
+ return reusableItemState;
90
+ };
91
+ // Pre-computed class names
92
+ const baseClass = `${classPrefix}-item ${classPrefix}-masonry-item`;
93
+ const groupHeaderClass = `${classPrefix}-group-header`;
94
+ const selectedClass = `${classPrefix}-item--selected`;
95
+ const focusedClass = `${classPrefix}-item--focused`;
96
+ /**
97
+ * Apply template result to element
98
+ */
99
+ const applyTemplate = (element, result) => {
100
+ if (typeof result === "string") {
101
+ element.innerHTML = result;
102
+ }
103
+ else {
104
+ element.replaceChildren(result);
105
+ }
106
+ };
107
+ /**
108
+ * Apply state-dependent classes
109
+ */
110
+ const applyClasses = (element, isSelected, isFocused) => {
111
+ element.classList.toggle(selectedClass, isSelected);
112
+ element.classList.toggle(focusedClass, isFocused);
113
+ };
114
+ const positionElement = (element, placement) => {
115
+ if (isHorizontal) {
116
+ element.style.transform = `translate(${Math.round(placement.y)}px, ${Math.round(placement.x)}px)`;
117
+ }
118
+ else {
119
+ element.style.transform = `translate(${Math.round(placement.x)}px, ${Math.round(placement.y)}px)`;
120
+ }
121
+ };
122
+ /**
123
+ * Apply size styles to an element.
124
+ */
125
+ const applySizeStyles = (element, placement) => {
126
+ if (isHorizontal) {
127
+ element.style.width = `${placement.size}px`;
128
+ element.style.height = `${placement.crossSize}px`;
129
+ }
130
+ else {
131
+ element.style.width = `${placement.crossSize}px`;
132
+ element.style.height = `${placement.size}px`;
133
+ }
134
+ };
135
+ /**
136
+ * Render a single masonry item (new element from pool)
137
+ */
138
+ const renderItem = (itemIndex, item, placement, isSelected, isFocused) => {
139
+ const element = pool.acquire();
140
+ const isGH = item.__groupHeader;
141
+ const state = getItemState(isSelected, isFocused);
142
+ // Group headers get a distinct class and role
143
+ element.className = isGH ? groupHeaderClass : baseClass;
144
+ // Set data attributes
145
+ element.dataset.index = String(itemIndex);
146
+ element.dataset.id = String(item.id);
147
+ element.dataset.lane = String(placement.lane);
148
+ if (isGH) {
149
+ element.setAttribute("role", "presentation");
150
+ element.removeAttribute("aria-selected");
151
+ element.removeAttribute("aria-setsize");
152
+ element.removeAttribute("aria-posinset");
153
+ element.removeAttribute("id");
154
+ }
155
+ else if (interactive !== false) {
156
+ element.setAttribute("role", "option");
157
+ element.ariaSelected = String(isSelected);
158
+ if (ariaIdPrefix) {
159
+ element.id = `${ariaIdPrefix}-item-${itemIndex}`;
160
+ }
161
+ if (totalItemsGetter) {
162
+ const total = totalItemsGetter();
163
+ if (total !== lastAriaTotal) {
164
+ lastAriaTotal = total;
165
+ lastAriaSetSize = String(total);
166
+ }
167
+ element.setAttribute("aria-setsize", lastAriaSetSize);
168
+ const posInSet = ariaPosInSetGetter ? ariaPosInSetGetter(itemIndex) : itemIndex + 1;
169
+ element.setAttribute("aria-posinset", String(posInSet));
170
+ }
171
+ }
172
+ else {
173
+ element.setAttribute("role", "listitem");
174
+ element.removeAttribute("aria-selected");
175
+ if (totalItemsGetter) {
176
+ const total = totalItemsGetter();
177
+ if (total !== lastAriaTotal) {
178
+ lastAriaTotal = total;
179
+ lastAriaSetSize = String(total);
180
+ }
181
+ element.setAttribute("aria-setsize", lastAriaSetSize);
182
+ const posInSet = ariaPosInSetGetter ? ariaPosInSetGetter(itemIndex) : itemIndex + 1;
183
+ element.setAttribute("aria-posinset", String(posInSet));
184
+ }
185
+ }
186
+ // Apply sizing
187
+ applySizeStyles(element, placement);
188
+ // Apply template
189
+ const result = template(item, itemIndex, state);
190
+ applyTemplate(element, result);
191
+ // Apply state classes and position
192
+ applyClasses(element, isSelected, isFocused);
193
+ positionElement(element, placement);
194
+ return {
195
+ element,
196
+ lastItemId: item.id,
197
+ lastSelected: isSelected,
198
+ lastFocused: isFocused,
199
+ lastY: placement.y,
200
+ lastX: placement.x,
201
+ lastSize: placement.size,
202
+ lastCrossSize: placement.crossSize,
203
+ lastSeenFrame: frameCounter,
204
+ };
205
+ };
206
+ /**
207
+ * Render visible items using pre-calculated placements.
208
+ *
209
+ * Performance characteristics:
210
+ * - Uses Set for O(1) visibility check (not O(n) .some())
211
+ * - Skips template re-evaluation when item id + state unchanged
212
+ * - Only updates position when coordinates changed
213
+ * - Release grace period prevents boundary thrashing
214
+ * - Released elements removed from DOM immediately
215
+ */
216
+ const render = (getItem, placements, selectedIds, focusedIndex) => {
217
+ frameCounter++;
218
+ // Repopulate reusable visibleSet — O(k) clear + O(k) add, no allocation
219
+ visibleSet.clear();
220
+ for (let i = 0; i < placements.length; i++) {
221
+ visibleSet.add(placements[i].index);
222
+ }
223
+ // Release items no longer visible, with grace period to prevent
224
+ // boundary thrashing (hover blink, CSS transition replay).
225
+ // Items that just left the visible set keep their DOM element for
226
+ // RELEASE_GRACE extra render cycles — if they re-enter, the same
227
+ // element is reused with :hover state intact.
228
+ for (const [index, tracked] of rendered) {
229
+ if (visibleSet.has(index)) {
230
+ tracked.lastSeenFrame = frameCounter;
231
+ }
232
+ else if (frameCounter - tracked.lastSeenFrame > RELEASE_GRACE) {
233
+ pool.release(tracked.element);
234
+ rendered.delete(index);
235
+ }
236
+ }
237
+ // DocumentFragment for batched DOM insertion of new elements
238
+ // (matches core renderer and grid renderer patterns)
239
+ let fragment = null;
240
+ // Render new or update existing items
241
+ for (let pi = 0; pi < placements.length; pi++) {
242
+ const placement = placements[pi];
243
+ const itemIndex = placement.index;
244
+ const item = getItem(itemIndex);
245
+ if (!item)
246
+ continue;
247
+ const isSelected = selectedIds.has(item.id);
248
+ const isFocused = itemIndex === focusedIndex;
249
+ const existing = rendered.get(itemIndex);
250
+ if (existing) {
251
+ // ── Fast path: skip work when nothing changed ──
252
+ const idChanged = existing.lastItemId !== item.id;
253
+ const selectedChanged = existing.lastSelected !== isSelected;
254
+ const focusedChanged = existing.lastFocused !== isFocused;
255
+ const posChanged = existing.lastY !== placement.y ||
256
+ existing.lastX !== placement.x;
257
+ const sizeChanged = existing.lastSize !== placement.size ||
258
+ existing.lastCrossSize !== placement.crossSize;
259
+ // Template re-evaluation only when item data actually changed
260
+ // (NOT on selection/focus change — that would destroy loaded images)
261
+ if (idChanged) {
262
+ const state = getItemState(isSelected, isFocused);
263
+ const result = template(item, itemIndex, state);
264
+ applyTemplate(existing.element, result);
265
+ // Update data attributes
266
+ existing.element.dataset.id = String(item.id);
267
+ existing.lastItemId = item.id;
268
+ // Refresh aria-posinset when element is reused for a different item
269
+ const isGH = item.__groupHeader;
270
+ if (!isGH) {
271
+ const posInSet = ariaPosInSetGetter ? ariaPosInSetGetter(itemIndex) : itemIndex + 1;
272
+ existing.element.setAttribute("aria-posinset", String(posInSet));
273
+ }
274
+ }
275
+ // Class + aria updates only when selection/focus changed
276
+ if (idChanged || selectedChanged || focusedChanged) {
277
+ applyClasses(existing.element, isSelected, isFocused);
278
+ existing.element.ariaSelected = String(isSelected);
279
+ existing.lastSelected = isSelected;
280
+ existing.lastFocused = isFocused;
281
+ }
282
+ // Size update when cross-axis or main-axis size changed (e.g. container resize)
283
+ if (sizeChanged) {
284
+ applySizeStyles(existing.element, placement);
285
+ existing.lastSize = placement.size;
286
+ existing.lastCrossSize = placement.crossSize;
287
+ }
288
+ // Position update only when coordinates changed
289
+ if (posChanged) {
290
+ positionElement(existing.element, placement);
291
+ existing.lastY = placement.y;
292
+ existing.lastX = placement.x;
293
+ }
294
+ }
295
+ else {
296
+ // Render new item — collect in fragment for batched insertion
297
+ const tracked = renderItem(itemIndex, item, placement, isSelected, isFocused);
298
+ if (!fragment)
299
+ fragment = document.createDocumentFragment();
300
+ fragment.appendChild(tracked.element);
301
+ rendered.set(itemIndex, tracked);
302
+ }
303
+ }
304
+ // Single DOM insertion for all new elements — minimizes reflows
305
+ if (fragment)
306
+ itemsContainer.appendChild(fragment);
307
+ };
308
+ /**
309
+ * Clear all rendered items.
310
+ */
311
+ const clear = () => {
312
+ for (const tracked of rendered.values()) {
313
+ pool.release(tracked.element);
314
+ }
315
+ rendered.clear();
316
+ itemsContainer.innerHTML = "";
317
+ };
318
+ /**
319
+ * Destroy renderer and cleanup.
320
+ */
321
+ const destroy = () => {
322
+ clear();
323
+ pool.clear();
324
+ };
325
+ /**
326
+ * Reorder DOM children so they follow logical data-index order.
327
+ * Called on scroll idle for accessibility — screen readers traverse
328
+ * DOM order, not visual (transform) order. Since items are
329
+ * position:absolute, this has zero visual impact.
330
+ */
331
+ const sortDOM = () => {
332
+ sortRenderedDOM(itemsContainer, rendered.keys(), (key) => rendered.get(key)?.element);
333
+ };
334
+ const updateItemClasses = (index, isSelected, isFocused) => {
335
+ const existing = rendered.get(index);
336
+ if (!existing)
337
+ return;
338
+ const selectedChanged = existing.lastSelected !== isSelected;
339
+ const focusedChanged = existing.lastFocused !== isFocused;
340
+ if (selectedChanged || focusedChanged) {
341
+ applyClasses(existing.element, isSelected, isFocused);
342
+ existing.lastSelected = isSelected;
343
+ existing.lastFocused = isFocused;
344
+ }
345
+ };
346
+ return {
347
+ render,
348
+ getElement: (index) => rendered.get(index)?.element,
349
+ updateItemClasses,
350
+ sortDOM,
351
+ clear,
352
+ destroy,
353
+ };
354
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * vlist - Masonry Types
3
+ * Types for masonry/Pinterest-style layout mode
4
+ *
5
+ * Masonry layout arranges items in columns (vertical) or rows (horizontal)
6
+ * where items flow into the shortest column/row, creating a packed layout
7
+ * with no alignment across the cross-axis.
8
+ */
9
+ export {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * vlist/page - Window Scroll Mode
3
+ */
4
+ export { page, type PagePluginConfig } from "./plugin";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * vlist/page - Window Scroll Mode
3
+ */
4
+ // v2 plugin
5
+ export { page } from "./plugin";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * vlist v2 — Page (Window Scroll) Plugin
3
+ *
4
+ * Redirects scroll from the viewport to the window, enabling
5
+ * the list to scroll with the page. Useful for infinite feeds,
6
+ * full-page lists, and document-integrated virtual scrolling.
7
+ *
8
+ * Priority: 5 (runs early, before plugins that depend on scroll)
9
+ */
10
+ import type { VListItem } from "../../types";
11
+ import type { VListPlugin } from "../../core/types";
12
+ export interface PagePluginConfig {
13
+ scrollPadding?: {
14
+ top?: number | (() => number);
15
+ bottom?: number | (() => number);
16
+ left?: number | (() => number);
17
+ right?: number | (() => number);
18
+ };
19
+ }
20
+ export declare function page<T extends VListItem = VListItem>(config?: PagePluginConfig): VListPlugin<T>;
21
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1,166 @@
1
+ /**
2
+ * vlist v2 — Page (Window Scroll) Plugin
3
+ *
4
+ * Redirects scroll from the viewport to the window, enabling
5
+ * the list to scroll with the page. Useful for infinite feeds,
6
+ * full-page lists, and document-integrated virtual scrolling.
7
+ *
8
+ * Priority: 5 (runs early, before plugins that depend on scroll)
9
+ */
10
+ // =============================================================================
11
+ // Helpers
12
+ // =============================================================================
13
+ const resolvePad = (v) => v == null ? 0 : typeof v === "function" ? v() : v;
14
+ // =============================================================================
15
+ // Factory
16
+ // =============================================================================
17
+ export function page(config) {
18
+ const scrollPadding = config?.scrollPadding;
19
+ let cleanupResize;
20
+ let cleanupScroll;
21
+ return {
22
+ name: "page",
23
+ priority: 5,
24
+ setup(ctx) {
25
+ const { dom, sizeCache, config: cfg, emitter } = ctx;
26
+ const hz = cfg.horizontal;
27
+ const win = window;
28
+ const state = ctx.getState();
29
+ // ── 1. Disable default viewport scroll & resize ────────────
30
+ ctx.disableDefaultScroll();
31
+ ctx.disableDefaultResize();
32
+ // ── 2. Modify DOM for window scroll ────────────────────────
33
+ dom.root.style.overflow = "visible";
34
+ dom.root.style.height = "auto";
35
+ if (hz) {
36
+ dom.viewport.style.overflowX = "visible";
37
+ dom.viewport.style.overflowY = "visible";
38
+ }
39
+ else {
40
+ dom.viewport.style.overflow = "visible";
41
+ }
42
+ dom.viewport.classList.remove(`${cfg.classPrefix}-viewport--custom-scrollbar`);
43
+ // ── 3. Override scroll position get/set ─────────────────────
44
+ ctx.setScrollFns(() => {
45
+ const rect = dom.viewport.getBoundingClientRect();
46
+ return Math.max(0, hz ? -rect.left : -rect.top);
47
+ }, (pos) => {
48
+ const rect = dom.viewport.getBoundingClientRect();
49
+ const target = (hz ? rect.left + win.scrollX : rect.top + win.scrollY) + pos;
50
+ if (hz) {
51
+ win.scrollTo({ left: target, top: win.scrollY, behavior: "instant" });
52
+ }
53
+ else {
54
+ win.scrollTo({ left: win.scrollX, top: target, behavior: "instant" });
55
+ }
56
+ });
57
+ // ── 4. Set container size from window ──────────────────────
58
+ state.containerSize = hz ? win.innerWidth : win.innerHeight;
59
+ state.crossSize = hz ? win.innerHeight : win.innerWidth;
60
+ // ── 5. Window scroll listener ──────────────────────────────
61
+ let idleTimer = null;
62
+ const onWindowScroll = () => {
63
+ const rect = dom.viewport.getBoundingClientRect();
64
+ const pos = Math.max(0, hz ? -rect.left : -rect.top);
65
+ state.prevScrollPosition = state.scrollPosition;
66
+ state.scrollPosition = pos;
67
+ state.scrollDirection = pos > state.prevScrollPosition ? 1
68
+ : pos < state.prevScrollPosition ? -1 : 0;
69
+ ctx.onScrollFrame();
70
+ if (idleTimer !== null)
71
+ clearTimeout(idleTimer);
72
+ idleTimer = setTimeout(() => {
73
+ idleTimer = null;
74
+ ctx.onScrollIdle();
75
+ }, 150);
76
+ };
77
+ win.addEventListener("scroll", onWindowScroll, { passive: true });
78
+ cleanupScroll = () => win.removeEventListener("scroll", onWindowScroll);
79
+ // ── 6. Window resize listener ──────────────────────────────
80
+ let prevW = win.innerWidth;
81
+ let prevH = win.innerHeight;
82
+ const onWindowResize = () => {
83
+ const w = win.innerWidth;
84
+ const h = win.innerHeight;
85
+ const sizeDelta = hz ? Math.abs(w - prevW) : Math.abs(h - prevH);
86
+ if (sizeDelta < 1)
87
+ return;
88
+ prevW = w;
89
+ prevH = h;
90
+ state.containerSize = hz ? w : h;
91
+ state.crossSize = hz ? h : w;
92
+ ctx.forceRender();
93
+ emitter.emit("resize", { width: w, height: h });
94
+ };
95
+ win.addEventListener("resize", onWindowResize);
96
+ cleanupResize = () => win.removeEventListener("resize", onWindowResize);
97
+ // ── 7. Scroll padding (scrollToIndex adjustments) ──────────
98
+ if (scrollPadding) {
99
+ ctx.setScrollToPosFn((index, sc, containerSize, totalItems, align) => {
100
+ const startPad = resolvePad(hz ? scrollPadding.left : scrollPadding.top);
101
+ const endPad = resolvePad(hz ? scrollPadding.right : scrollPadding.bottom);
102
+ if (totalItems === 0)
103
+ return 0;
104
+ const clamped = Math.max(0, Math.min(index, totalItems - 1));
105
+ const offset = sc.getOffset(clamped);
106
+ const itemSize = sc.getSize(clamped);
107
+ const totalSize = sc.getTotalSize();
108
+ const maxScroll = Math.max(0, totalSize - containerSize + endPad);
109
+ let pos;
110
+ switch (align) {
111
+ case "center":
112
+ pos = offset - startPad - (containerSize - startPad - endPad - itemSize) / 2;
113
+ break;
114
+ case "end":
115
+ pos = offset - containerSize + itemSize + endPad;
116
+ break;
117
+ default:
118
+ pos = offset - startPad;
119
+ }
120
+ return Math.max(-startPad, Math.min(pos, maxScroll));
121
+ });
122
+ ctx.registerMethod("_scrollItemIntoView", (index) => {
123
+ const containerSize = hz ? win.innerWidth : win.innerHeight;
124
+ const startPad = resolvePad(hz ? scrollPadding.left : scrollPadding.top);
125
+ const endPad = resolvePad(hz ? scrollPadding.right : scrollPadding.bottom);
126
+ const rect = dom.viewport.getBoundingClientRect();
127
+ const domScroll = hz ? win.scrollX : win.scrollY;
128
+ const listScreenPos = hz ? rect.left : rect.top;
129
+ const listDocPos = listScreenPos + domScroll;
130
+ const itemOffset = sizeCache.getOffset(index);
131
+ const itemSize = sizeCache.getSize(index);
132
+ const itemScreenStart = listScreenPos + itemOffset;
133
+ const safeEnd = containerSize - endPad;
134
+ let newTarget = domScroll;
135
+ if (itemScreenStart < startPad) {
136
+ newTarget = listDocPos + itemOffset - startPad;
137
+ }
138
+ else if (itemScreenStart + itemSize > safeEnd) {
139
+ newTarget = listDocPos + itemOffset + itemSize - safeEnd;
140
+ }
141
+ if (newTarget !== domScroll) {
142
+ if (hz) {
143
+ win.scrollTo({ left: newTarget, top: win.scrollY, behavior: "instant" });
144
+ }
145
+ else {
146
+ win.scrollTo({ left: win.scrollX, top: newTarget, behavior: "instant" });
147
+ }
148
+ }
149
+ });
150
+ }
151
+ // ── 8. Register cleanup ────────────────────────────────────
152
+ ctx.registerDestroyHandler(() => {
153
+ cleanupScroll?.();
154
+ cleanupResize?.();
155
+ if (idleTimer !== null)
156
+ clearTimeout(idleTimer);
157
+ });
158
+ },
159
+ destroy() {
160
+ cleanupScroll?.();
161
+ cleanupResize?.();
162
+ cleanupScroll = undefined;
163
+ cleanupResize = undefined;
164
+ },
165
+ };
166
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * vlist v2 — Scale Plugin
3
+ */
4
+ export { scale, type ScalePluginConfig } from "./plugin";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * vlist v2 — Scale Plugin
3
+ */
4
+ export { scale } from "./plugin";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * vlist v2 — Scale Plugin
3
+ *
4
+ * Enables support for 1M+ item lists by compressing the scroll space
5
+ * when total content size exceeds the browser's ~16.7M pixel limit.
6
+ *
7
+ * Priority 20 — runs after layout plugins (10), before selection (50).
8
+ *
9
+ * When compressed:
10
+ * - Native scroll is disabled (overflow: hidden)
11
+ * - Custom wheel handler with lerp smooth scroll
12
+ * - Custom touch handler with momentum
13
+ * - Items positioned relative to viewport via onCalculate hook
14
+ * - Fallback scrollbar created if no scrollbar plugin present
15
+ *
16
+ * Uses v1's rendering/scale.ts pure math functions directly.
17
+ */
18
+ import type { VListItem } from "../../types";
19
+ import type { VListPlugin } from "../../core/types";
20
+ export interface ScalePluginConfig {
21
+ force?: boolean;
22
+ }
23
+ export declare function scale<T extends VListItem = VListItem>(config?: ScalePluginConfig): VListPlugin<T>;
24
+ //# sourceMappingURL=plugin.d.ts.map