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,185 +0,0 @@
1
- /**
2
- * vlist v2 — Autosize Plugin
3
- *
4
- * Enables dynamic item measurement via ResizeObserver for items with
5
- * unknown sizes. Items are rendered without an explicit main-axis size,
6
- * measured once by ResizeObserver, then pinned to their measured size.
7
- *
8
- * Priority 5 — runs before grid/masonry (10) so the measured cache
9
- * is in place before layout plugins consume it.
10
- *
11
- * Requires: `item.estimatedHeight` or `item.estimatedWidth` in config
12
- */
13
- // =============================================================================
14
- // Factory
15
- // =============================================================================
16
- export function autosize(config) {
17
- let gap = config?.gap ?? 0;
18
- let observer = null;
19
- let storedCtx = null;
20
- let engineState;
21
- let hz;
22
- let sizeProp;
23
- let estimatedSize;
24
- const measuredSizes = new Map();
25
- const elementToIndex = new WeakMap();
26
- let pendingScrollDelta = 0;
27
- let pendingContentSizeUpdate = false;
28
- const BOTTOM_THRESHOLD = 2;
29
- function sizeFn(index) {
30
- return measuredSizes.get(index) ?? estimatedSize;
31
- }
32
- function isAtBottom() {
33
- const viewport = storedCtx.dom.viewport;
34
- const maxScroll = viewport.scrollHeight - viewport.clientHeight;
35
- return maxScroll > 0 && engineState.scrollPosition >= maxScroll - BOTTOM_THRESHOLD;
36
- }
37
- function snapToBottom() {
38
- const viewport = storedCtx.dom.viewport;
39
- void viewport.scrollHeight;
40
- const maxScroll = viewport.scrollHeight - viewport.clientHeight;
41
- if (maxScroll > engineState.scrollPosition) {
42
- storedCtx.scrollTo(maxScroll);
43
- }
44
- }
45
- function updateContentSize() {
46
- storedCtx.updateContentSize(storedCtx.sizeCache.getTotalSize());
47
- }
48
- return {
49
- name: "autosize",
50
- priority: 5,
51
- setup(ctx) {
52
- storedCtx = ctx;
53
- engineState = ctx.getState();
54
- hz = ctx.config.horizontal;
55
- sizeProp = hz ? "width" : "height";
56
- if (gap === 0)
57
- gap = ctx.config.gap;
58
- // Read estimated size from the current sizeCache before replacing it.
59
- // The initial cache already has gap baked in — read the raw spec size.
60
- estimatedSize = typeof ctx.rawSizeSpec === "function"
61
- ? ctx.rawSizeSpec(0) + gap
62
- : ctx.rawSizeSpec + gap;
63
- // Replace the fixed sizeCache with a variable one backed by measurements
64
- ctx.setSizeConfig(sizeFn);
65
- if (gap > 0) {
66
- const orig = ctx.sizeCache.getTotalSize;
67
- ctx.sizeCache.getTotalSize = () => {
68
- const t = orig();
69
- return t > 0 ? t - gap : 0;
70
- };
71
- }
72
- // ResizeObserver for measuring items
73
- observer = new ResizeObserver((entries) => {
74
- if (engineState.destroyed || !storedCtx)
75
- return;
76
- let hasNewMeasurements = false;
77
- const firstVisible = engineState.startIndex;
78
- for (const entry of entries) {
79
- const el = entry.target;
80
- const index = elementToIndex.get(el);
81
- if (index === undefined)
82
- continue;
83
- // Verify element wasn't recycled to a different item
84
- if (el.getAttribute("data-index") !== String(index)) {
85
- observer.unobserve(el);
86
- continue;
87
- }
88
- if (measuredSizes.has(index))
89
- continue;
90
- const boxSize = entry.borderBoxSize[0];
91
- if (!boxSize)
92
- continue;
93
- const newSize = hz ? boxSize.inlineSize : boxSize.blockSize;
94
- if (newSize <= 0)
95
- continue;
96
- const sizeWithGap = newSize + gap;
97
- const oldSize = estimatedSize;
98
- measuredSizes.set(index, sizeWithGap);
99
- hasNewMeasurements = true;
100
- if (index < firstVisible && sizeWithGap !== oldSize) {
101
- pendingScrollDelta += sizeWithGap - oldSize;
102
- }
103
- observer.unobserve(el);
104
- // Pin the element to its measured size
105
- el.style[sizeProp] = `${newSize}px`;
106
- }
107
- if (!hasNewMeasurements)
108
- return;
109
- const atBottom = isAtBottom();
110
- // Rebuild prefix sums with new measurements
111
- ctx.rebuildSizeCache();
112
- // Apply scroll correction for items above viewport
113
- if (pendingScrollDelta) {
114
- ctx.scrollTo(engineState.scrollPosition + pendingScrollDelta);
115
- pendingScrollDelta = 0;
116
- }
117
- const isScrolling = engineState.scrollDirection !== 0;
118
- const nearEnd = engineState.totalItems > 0
119
- && engineState.prevRangeEnd >= engineState.totalItems - 1;
120
- if (atBottom || nearEnd || !isScrolling) {
121
- updateContentSize();
122
- pendingContentSizeUpdate = false;
123
- if (atBottom) {
124
- snapToBottom();
125
- }
126
- }
127
- else {
128
- pendingContentSizeUpdate = true;
129
- }
130
- ctx.forceRender();
131
- });
132
- // Public methods
133
- ctx.registerMethod("isMeasured", (index) => measuredSizes.has(index));
134
- ctx.registerMethod("setMeasuredSize", (index, size) => {
135
- measuredSizes.set(index, size);
136
- });
137
- ctx.registerMethod("getMeasuredCount", () => measuredSizes.size);
138
- // Cleanup
139
- ctx.registerDestroyHandler(() => {
140
- if (observer) {
141
- observer.disconnect();
142
- observer = null;
143
- }
144
- });
145
- },
146
- hooks: {
147
- onCommit(state) {
148
- if (!observer || !storedCtx)
149
- return;
150
- for (let i = 0; i < state.visibleCount; i++) {
151
- const idx = state.visibleIndices[i];
152
- if (measuredSizes.has(idx))
153
- continue;
154
- const el = storedCtx.getRenderedElement(idx);
155
- if (!el)
156
- continue;
157
- // Clear the explicit size set by phase2Commit so
158
- // ResizeObserver can measure the natural content size.
159
- el.style[sizeProp] = "";
160
- elementToIndex.set(el, idx);
161
- observer.observe(el);
162
- }
163
- },
164
- onIdle() {
165
- if (!pendingContentSizeUpdate || !storedCtx)
166
- return;
167
- const atBottom = isAtBottom();
168
- updateContentSize();
169
- pendingContentSizeUpdate = false;
170
- if (atBottom) {
171
- snapToBottom();
172
- storedCtx.forceRender();
173
- }
174
- },
175
- },
176
- destroy() {
177
- if (observer) {
178
- observer.disconnect();
179
- observer = null;
180
- }
181
- measuredSizes.clear();
182
- storedCtx = null;
183
- },
184
- };
185
- }
@@ -1,5 +0,0 @@
1
- /**
2
- * vlist v2 — Grid Plugin
3
- */
4
- export { grid } from "./plugin";
5
- export { createGridLayout, } from "./layout";
@@ -1,275 +0,0 @@
1
- /**
2
- * vlist - Grid Layout
3
- * Pure O(1) calculations for mapping between flat item indices and grid positions.
4
- *
5
- * The grid transforms a flat list into rows:
6
- * Items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
7
- * Grid (columns=4):
8
- * Row 0: [0, 1, 2, 3]
9
- * Row 1: [4, 5, 6, 7]
10
- * Row 2: [8, 9] ← partially filled last row
11
- *
12
- * All operations are O(1) — integer division and modulo only.
13
- */
14
- // =============================================================================
15
- // Factory
16
- // =============================================================================
17
- /**
18
- * Create a GridLayout instance.
19
- *
20
- * @param config - Grid configuration (columns, gap, optional isHeaderFn)
21
- * @returns GridLayout with O(1) mapping functions (or groups-aware if isHeaderFn provided)
22
- */
23
- export const createGridLayout = (config) => {
24
- let columns = Math.max(1, Math.floor(config.columns));
25
- let gap = config.gap ?? 0;
26
- let isHeaderFn = config.isHeaderFn;
27
- // Reusable position object to avoid allocation on hot paths
28
- const reusablePosition = { row: 0, col: 0 };
29
- /**
30
- * Total rows for a given item count.
31
- * When isHeaderFn is provided, headers force new rows.
32
- */
33
- const getTotalRows = (totalItems) => {
34
- if (totalItems <= 0)
35
- return 0;
36
- if (!isHeaderFn) {
37
- return Math.ceil(totalItems / columns);
38
- }
39
- // Groups-aware calculation
40
- let row = 0;
41
- let colInRow = 0;
42
- let headerCount = 0;
43
- for (let i = 0; i < totalItems; i++) {
44
- if (isHeaderFn(i)) {
45
- headerCount++;
46
- // Header: start new row if not at beginning
47
- if (colInRow) {
48
- row++;
49
- colInRow = 0;
50
- }
51
- // Header occupies its own row
52
- row++;
53
- colInRow = 0;
54
- }
55
- else {
56
- // Regular item
57
- colInRow++;
58
- if (colInRow >= columns) {
59
- row++;
60
- colInRow = 0;
61
- }
62
- }
63
- }
64
- // Add final row if there are items in it
65
- if (colInRow) {
66
- row++;
67
- }
68
- return row;
69
- };
70
- /**
71
- * Get row/col position for a flat item index.
72
- * Reuses a single object to reduce GC pressure on scroll hot path.
73
- */
74
- const getPosition = (itemIndex) => {
75
- reusablePosition.row = getRow(itemIndex);
76
- reusablePosition.col = getCol(itemIndex);
77
- return reusablePosition;
78
- };
79
- /**
80
- * Get row index for a flat item index — O(1)
81
- * When isHeaderFn is provided, headers force new rows and span all columns.
82
- */
83
- const getRow = (itemIndex) => {
84
- if (!isHeaderFn) {
85
- return Math.floor(itemIndex / columns);
86
- }
87
- // Groups-aware calculation
88
- let row = 0;
89
- let colInRow = 0;
90
- for (let i = 0; i <= itemIndex; i++) {
91
- const isHeader = isHeaderFn(i);
92
- if (isHeader) {
93
- // Header: start new row if not at beginning
94
- if (colInRow) {
95
- row++;
96
- colInRow = 0;
97
- }
98
- // Header occupies its own row
99
- if (i === itemIndex) {
100
- return row;
101
- }
102
- row++;
103
- colInRow = 0;
104
- }
105
- else {
106
- // Regular item
107
- if (i === itemIndex) {
108
- return row;
109
- }
110
- colInRow++;
111
- if (colInRow >= columns) {
112
- row++;
113
- colInRow = 0;
114
- }
115
- }
116
- }
117
- console.warn(`⚠️ getRow(${itemIndex}) fell through - returning ${row}`);
118
- return row;
119
- };
120
- /**
121
- * Get column index for a flat item index — O(1)
122
- * Headers always return col 0 when isHeaderFn is provided.
123
- */
124
- const getCol = (itemIndex) => {
125
- if (!isHeaderFn) {
126
- return itemIndex % columns;
127
- }
128
- // Headers always at column 0
129
- if (isHeaderFn(itemIndex)) {
130
- return 0;
131
- }
132
- // Calculate column for regular items
133
- let colInRow = 0;
134
- for (let i = 0; i <= itemIndex; i++) {
135
- const isHeader = isHeaderFn(i);
136
- if (isHeader) {
137
- // Header: reset column counter
138
- colInRow = 0;
139
- }
140
- else {
141
- if (i === itemIndex) {
142
- return colInRow;
143
- }
144
- colInRow++;
145
- if (colInRow >= columns) {
146
- colInRow = 0;
147
- }
148
- }
149
- }
150
- return colInRow;
151
- };
152
- /**
153
- * Get the flat item range [start, end] for a range of rows.
154
- *
155
- * rowStart and rowEnd are inclusive row indices.
156
- * The returned end is clamped to totalItems - 1.
157
- * When isHeaderFn is provided, this accounts for headers disrupting the grid flow.
158
- */
159
- const getItemRange = (rowStart, rowEnd, totalItems) => {
160
- if (totalItems <= 0)
161
- return { start: 0, end: -1 };
162
- if (!isHeaderFn) {
163
- // Simple O(1) calculation for regular grids
164
- const start = Math.max(0, rowStart * columns);
165
- const end = Math.min(totalItems - 1, (rowEnd + 1) * columns - 1);
166
- return { start, end };
167
- }
168
- // Groups-aware calculation - find items that fall in the row range
169
- let start = -1;
170
- let end = -1;
171
- let currentRow = 0;
172
- let colInRow = 0;
173
- for (let i = 0; i < totalItems; i++) {
174
- const isHeader = isHeaderFn(i);
175
- if (isHeader) {
176
- // Header: start new row if not at beginning
177
- if (colInRow) {
178
- currentRow++;
179
- colInRow = 0;
180
- }
181
- // Check if this header's row is in range
182
- if (currentRow >= rowStart && currentRow <= rowEnd) {
183
- if (start < 0)
184
- start = i;
185
- end = i;
186
- }
187
- currentRow++;
188
- colInRow = 0;
189
- }
190
- else {
191
- // Regular item
192
- if (currentRow >= rowStart && currentRow <= rowEnd) {
193
- if (start < 0)
194
- start = i;
195
- end = i;
196
- }
197
- colInRow++;
198
- if (colInRow >= columns) {
199
- currentRow++;
200
- colInRow = 0;
201
- }
202
- }
203
- // Early exit if we're past the end row
204
- if (currentRow > rowEnd && !colInRow) {
205
- break;
206
- }
207
- }
208
- // If no items found in range, return empty range
209
- if (start < 0) {
210
- return { start: 0, end: -1 };
211
- }
212
- return { start, end };
213
- };
214
- /**
215
- * Get the flat item index from a row and column.
216
- * Returns -1 if the position is out of bounds.
217
- */
218
- const getItemIndex = (row, col, totalItems) => {
219
- if (col < 0 || col >= columns)
220
- return -1;
221
- const index = row * columns + col;
222
- if (index < 0 || index >= totalItems)
223
- return -1;
224
- return index;
225
- };
226
- /**
227
- * Calculate column width given the container's inner width.
228
- * Distributes gaps evenly: totalGapWidth = (columns - 1) * gap
229
- * columnWidth = (containerWidth - totalGapWidth) / columns
230
- */
231
- const getColumnWidth = (containerWidth) => {
232
- const totalGap = (columns - 1) * gap;
233
- return Math.max(0, (containerWidth - totalGap) / columns);
234
- };
235
- /**
236
- * Calculate the X pixel offset for a given column index.
237
- * offset = col * (columnWidth + gap)
238
- */
239
- const getColumnOffset = (col, containerWidth) => {
240
- const colWidth = getColumnWidth(containerWidth);
241
- return col * (colWidth + gap);
242
- };
243
- /**
244
- * Update grid configuration without recreating the layout.
245
- * This is more efficient than destroying and recreating.
246
- */
247
- const updateConfig = (newConfig) => {
248
- if (newConfig.columns !== undefined) {
249
- columns = Math.max(1, Math.floor(newConfig.columns));
250
- }
251
- if (newConfig.gap !== undefined) {
252
- gap = newConfig.gap;
253
- }
254
- if (newConfig.isHeaderFn !== undefined) {
255
- isHeaderFn = newConfig.isHeaderFn;
256
- }
257
- };
258
- return {
259
- get columns() {
260
- return columns;
261
- },
262
- get gap() {
263
- return gap;
264
- },
265
- update: updateConfig,
266
- getTotalRows,
267
- getPosition,
268
- getRow,
269
- getCol,
270
- getItemRange,
271
- getItemIndex,
272
- getColumnWidth,
273
- getColumnOffset,
274
- };
275
- };