vlist 1.9.0 → 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.
- package/README.github.md +104 -97
- package/README.md +46 -33
- package/dist/constants.d.ts +11 -6
- package/dist/constants.js +83 -0
- package/dist/core/create.d.ts +10 -0
- package/dist/core/create.js +740 -0
- package/dist/core/dom.d.ts +8 -0
- package/dist/core/dom.js +47 -0
- package/dist/core/hooks.d.ts +16 -0
- package/dist/core/hooks.js +67 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +13 -0
- package/dist/core/pipeline.d.ts +51 -0
- package/dist/core/pipeline.js +307 -0
- package/dist/core/pool.d.ts +9 -0
- package/dist/core/pool.js +42 -0
- package/dist/core/scroll.d.ts +32 -0
- package/dist/core/scroll.js +137 -0
- package/dist/core/sizes.d.ts +8 -0
- package/dist/core/sizes.js +6 -0
- package/dist/core/state.d.ts +47 -0
- package/dist/core/state.js +56 -0
- package/dist/core/types.d.ts +187 -0
- package/dist/core/types.js +7 -0
- package/dist/{builder → core}/velocity.d.ts +1 -1
- package/dist/core/velocity.js +33 -0
- package/dist/events/emitter.js +60 -0
- package/dist/events/index.js +6 -0
- package/dist/index.d.ts +28 -19
- package/dist/index.js +28 -1
- package/dist/internals.d.ts +11 -7
- package/dist/internals.js +60 -1
- package/dist/plugins/a11y/index.d.ts +2 -0
- package/dist/plugins/a11y/index.js +1 -0
- package/dist/plugins/a11y/plugin.d.ts +13 -0
- package/dist/plugins/a11y/plugin.js +259 -0
- package/dist/{features → plugins}/async/index.d.ts +1 -1
- package/dist/plugins/async/index.js +12 -0
- package/dist/{features → plugins}/async/manager.d.ts +5 -1
- package/dist/plugins/async/manager.js +568 -0
- package/dist/plugins/async/placeholder.js +154 -0
- package/dist/plugins/async/plugin.d.ts +48 -0
- package/dist/plugins/async/plugin.js +311 -0
- package/dist/plugins/async/sparse.js +540 -0
- package/dist/plugins/autosize/index.d.ts +5 -0
- package/dist/plugins/autosize/index.js +4 -0
- package/dist/plugins/autosize/plugin.d.ts +19 -0
- package/dist/plugins/autosize/plugin.js +185 -0
- package/dist/plugins/grid/index.d.ts +7 -0
- package/dist/plugins/grid/index.js +5 -0
- package/dist/plugins/grid/layout.js +275 -0
- package/dist/plugins/grid/plugin.d.ts +23 -0
- package/dist/plugins/grid/plugin.js +347 -0
- package/dist/plugins/grid/renderer.js +525 -0
- package/dist/plugins/grid/types.js +11 -0
- package/dist/plugins/groups/async-bridge.js +246 -0
- package/dist/{features → plugins}/groups/index.d.ts +1 -1
- package/dist/plugins/groups/index.js +13 -0
- package/dist/plugins/groups/layout.js +294 -0
- package/dist/plugins/groups/plugin.d.ts +22 -0
- package/dist/plugins/groups/plugin.js +571 -0
- package/dist/plugins/groups/sticky.js +255 -0
- package/dist/plugins/groups/types.js +12 -0
- package/dist/plugins/masonry/index.d.ts +8 -0
- package/dist/plugins/masonry/index.js +6 -0
- package/dist/plugins/masonry/layout.js +261 -0
- package/dist/plugins/masonry/plugin.d.ts +32 -0
- package/dist/plugins/masonry/plugin.js +381 -0
- package/dist/plugins/masonry/renderer.js +354 -0
- package/dist/plugins/masonry/types.js +9 -0
- package/dist/plugins/page/index.d.ts +5 -0
- package/dist/plugins/page/index.js +5 -0
- package/dist/plugins/page/plugin.d.ts +21 -0
- package/dist/plugins/page/plugin.js +166 -0
- package/dist/plugins/scale/index.d.ts +5 -0
- package/dist/plugins/scale/index.js +4 -0
- package/dist/plugins/scale/plugin.d.ts +24 -0
- package/dist/plugins/scale/plugin.js +507 -0
- package/dist/plugins/scrollbar/controller.js +574 -0
- package/dist/plugins/scrollbar/index.d.ts +7 -0
- package/dist/plugins/scrollbar/index.js +6 -0
- package/dist/plugins/scrollbar/plugin.d.ts +20 -0
- package/dist/plugins/scrollbar/plugin.js +93 -0
- package/dist/plugins/scrollbar/scrollbar.js +556 -0
- package/dist/plugins/selection/index.d.ts +6 -0
- package/dist/plugins/selection/index.js +7 -0
- package/dist/plugins/selection/plugin.d.ts +16 -0
- package/dist/plugins/selection/plugin.js +601 -0
- package/dist/{features → plugins}/selection/state.d.ts +8 -0
- package/dist/plugins/selection/state.js +332 -0
- package/dist/plugins/snapshots/index.d.ts +5 -0
- package/dist/plugins/snapshots/index.js +5 -0
- package/dist/plugins/snapshots/plugin.d.ts +17 -0
- package/dist/plugins/snapshots/plugin.js +301 -0
- package/dist/plugins/sortable/index.d.ts +6 -0
- package/dist/plugins/sortable/index.js +6 -0
- package/dist/plugins/sortable/plugin.d.ts +34 -0
- package/dist/plugins/sortable/plugin.js +753 -0
- package/dist/plugins/table/header.js +501 -0
- package/dist/{features → plugins}/table/index.d.ts +1 -1
- package/dist/plugins/table/index.js +12 -0
- package/dist/plugins/table/layout.js +211 -0
- package/dist/plugins/table/plugin.d.ts +20 -0
- package/dist/plugins/table/plugin.js +391 -0
- package/dist/plugins/table/renderer.js +625 -0
- package/dist/plugins/table/types.js +12 -0
- package/dist/plugins/transition/index.d.ts +5 -0
- package/dist/plugins/transition/index.js +5 -0
- package/dist/plugins/transition/plugin.d.ts +22 -0
- package/dist/plugins/transition/plugin.js +405 -0
- package/dist/rendering/aria.js +23 -0
- package/dist/rendering/index.js +18 -0
- package/dist/rendering/measured.js +98 -0
- package/dist/rendering/renderer.js +586 -0
- package/dist/rendering/scale.js +267 -0
- package/dist/rendering/scroll.js +71 -0
- package/dist/rendering/sizes.js +193 -0
- package/dist/rendering/sort.js +65 -0
- package/dist/rendering/viewport.js +268 -0
- package/dist/size.json +1 -1
- package/dist/types.js +5 -0
- package/dist/utils/padding.d.ts +2 -4
- package/dist/utils/padding.js +49 -0
- package/dist/utils/stats.js +124 -0
- package/dist/vlist-grid.css +1 -1
- package/dist/vlist-masonry.css +1 -1
- package/dist/vlist-table.css +1 -1
- package/dist/vlist.css +1 -1
- package/package.json +9 -4
- package/dist/builder/a11y.d.ts +0 -16
- package/dist/builder/api.d.ts +0 -21
- package/dist/builder/context.d.ts +0 -36
- package/dist/builder/core.d.ts +0 -16
- package/dist/builder/data.d.ts +0 -71
- package/dist/builder/dom.d.ts +0 -15
- package/dist/builder/index.d.ts +0 -25
- package/dist/builder/materialize.d.ts +0 -166
- package/dist/builder/pool.d.ts +0 -10
- package/dist/builder/range.d.ts +0 -10
- package/dist/builder/scroll.d.ts +0 -24
- package/dist/builder/types.d.ts +0 -512
- package/dist/features/async/feature.d.ts +0 -72
- package/dist/features/autosize/feature.d.ts +0 -34
- package/dist/features/autosize/index.d.ts +0 -2
- package/dist/features/grid/feature.d.ts +0 -48
- package/dist/features/grid/index.d.ts +0 -9
- package/dist/features/groups/feature.d.ts +0 -75
- package/dist/features/masonry/feature.d.ts +0 -45
- package/dist/features/masonry/index.d.ts +0 -9
- package/dist/features/page/feature.d.ts +0 -109
- package/dist/features/page/index.d.ts +0 -9
- package/dist/features/scale/feature.d.ts +0 -42
- package/dist/features/scale/index.d.ts +0 -10
- package/dist/features/scrollbar/feature.d.ts +0 -81
- package/dist/features/scrollbar/index.d.ts +0 -8
- package/dist/features/selection/feature.d.ts +0 -91
- package/dist/features/selection/index.d.ts +0 -7
- package/dist/features/snapshots/feature.d.ts +0 -79
- package/dist/features/snapshots/index.d.ts +0 -9
- package/dist/features/sortable/feature.d.ts +0 -101
- package/dist/features/sortable/index.d.ts +0 -6
- package/dist/features/table/feature.d.ts +0 -67
- package/dist/features/transition/feature.d.ts +0 -30
- package/dist/features/transition/index.d.ts +0 -9
- /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
- /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
- /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
- /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/grid/types.d.ts +0 -0
- /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
- /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
- /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
- /package/dist/{features → plugins}/groups/types.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
- /package/dist/{features → plugins}/table/header.d.ts +0 -0
- /package/dist/{features → plugins}/table/layout.d.ts +0 -0
- /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/table/types.d.ts +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Grid Plugin
|
|
3
|
+
*/
|
|
4
|
+
export { grid, type GridPluginConfig } from "./plugin";
|
|
5
|
+
export { createGridLayout, type GridConfigWithGroups, } from "./layout";
|
|
6
|
+
export type { GridLayout, GridPosition, ItemRange, } from "./types";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Grid Plugin
|
|
3
|
+
*
|
|
4
|
+
* Switches from list layout to a 2D grid with configurable columns and gap.
|
|
5
|
+
* Priority 10 — runs before selection (50) so layout is ready for other plugins.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Replaces the default 1D render pipeline with a grid-aware render
|
|
9
|
+
* - Size cache operates in ROW space (each row has height = itemHeight + gap)
|
|
10
|
+
* - Visible range is calculated in row space, then expanded to flat item indices
|
|
11
|
+
* - Items are positioned with translate(colOffset, rowOffset)
|
|
12
|
+
*
|
|
13
|
+
* Restrictions:
|
|
14
|
+
* - Cannot be combined with masonry or table plugins
|
|
15
|
+
*/
|
|
16
|
+
import type { VListItem } from "../../types";
|
|
17
|
+
import type { VListPlugin } from "../../core/types";
|
|
18
|
+
export interface GridPluginConfig {
|
|
19
|
+
columns: number;
|
|
20
|
+
gap?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function grid<T extends VListItem = VListItem>(config: GridPluginConfig): VListPlugin<T>;
|
|
23
|
+
//# sourceMappingURL=plugin.d.ts.map
|