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,347 @@
|
|
|
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 { createGridLayout } from "./layout";
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Reusable state singleton — no allocation per frame
|
|
19
|
+
// =============================================================================
|
|
20
|
+
const itemState = { selected: false, focused: false };
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Factory
|
|
23
|
+
// =============================================================================
|
|
24
|
+
export function grid(config) {
|
|
25
|
+
if (!config.columns || config.columns < 1) {
|
|
26
|
+
throw new Error("[vlist] grid: columns must be >= 1");
|
|
27
|
+
}
|
|
28
|
+
let layout;
|
|
29
|
+
let sizeCache;
|
|
30
|
+
let engineState;
|
|
31
|
+
let pool;
|
|
32
|
+
let contentElement;
|
|
33
|
+
let template;
|
|
34
|
+
let getItem;
|
|
35
|
+
let horizontal;
|
|
36
|
+
let classPrefix;
|
|
37
|
+
let overscan;
|
|
38
|
+
let resolveItemState = null;
|
|
39
|
+
const rendered = new Map();
|
|
40
|
+
let containerWidth = 0;
|
|
41
|
+
// Mutable range objects — reused across frames
|
|
42
|
+
let lastScrollPosition = -1;
|
|
43
|
+
let lastContainerSize = -1;
|
|
44
|
+
let forceNextRender = true;
|
|
45
|
+
function getRowCount() {
|
|
46
|
+
return layout.getTotalRows(engineState.totalItems);
|
|
47
|
+
}
|
|
48
|
+
function buildTransform(itemIndex) {
|
|
49
|
+
const row = layout.getRow(itemIndex);
|
|
50
|
+
const col = layout.getCol(itemIndex);
|
|
51
|
+
const x = layout.getColumnOffset(col, containerWidth);
|
|
52
|
+
const y = sizeCache.getOffset(row);
|
|
53
|
+
if (horizontal) {
|
|
54
|
+
return `translate(${Math.round(y)}px, ${Math.round(x)}px)`;
|
|
55
|
+
}
|
|
56
|
+
return `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
|
|
57
|
+
}
|
|
58
|
+
function applySizeStyles(element, itemIndex) {
|
|
59
|
+
const row = layout.getRow(itemIndex);
|
|
60
|
+
const colWidth = layout.getColumnWidth(containerWidth);
|
|
61
|
+
const rowHeight = sizeCache.getSize(row) - layout.gap;
|
|
62
|
+
if (horizontal) {
|
|
63
|
+
element.style.width = `${rowHeight}px`;
|
|
64
|
+
element.style.height = `${colWidth}px`;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
element.style.width = `${colWidth}px`;
|
|
68
|
+
element.style.height = `${rowHeight}px`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function applyTemplate(el, item, index, isf) {
|
|
72
|
+
if (isf)
|
|
73
|
+
isf(index, itemState);
|
|
74
|
+
else {
|
|
75
|
+
itemState.selected = false;
|
|
76
|
+
itemState.focused = false;
|
|
77
|
+
}
|
|
78
|
+
const result = template(item, index, itemState);
|
|
79
|
+
if (typeof result === "string") {
|
|
80
|
+
el.innerHTML = result;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
el.innerHTML = "";
|
|
84
|
+
el.appendChild(result);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function gridRenderIfNeeded() {
|
|
88
|
+
if (engineState.destroyed)
|
|
89
|
+
return;
|
|
90
|
+
const scrollPos = engineState.scrollPosition;
|
|
91
|
+
const cs = engineState.containerSize;
|
|
92
|
+
if (!forceNextRender && scrollPos === lastScrollPosition && cs === lastContainerSize) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
lastScrollPosition = scrollPos;
|
|
96
|
+
lastContainerSize = cs;
|
|
97
|
+
forceNextRender = false;
|
|
98
|
+
const totalRows = getRowCount();
|
|
99
|
+
if (cs <= 0 || totalRows === 0)
|
|
100
|
+
return;
|
|
101
|
+
// Visible row range
|
|
102
|
+
let visStart = sizeCache.indexAtOffset(scrollPos);
|
|
103
|
+
let visEnd = sizeCache.indexAtOffset(scrollPos + cs);
|
|
104
|
+
if (visEnd < totalRows - 1)
|
|
105
|
+
visEnd++;
|
|
106
|
+
visStart = Math.max(0, visStart);
|
|
107
|
+
visEnd = Math.min(totalRows - 1, Math.max(0, visEnd));
|
|
108
|
+
// Overscan in row space
|
|
109
|
+
const renderStart = Math.max(0, visStart - overscan);
|
|
110
|
+
const renderEnd = Math.min(totalRows - 1, visEnd + overscan);
|
|
111
|
+
// Range-unchanged fast path
|
|
112
|
+
if (renderStart === engineState.prevRangeStart && renderEnd === engineState.prevRangeEnd && !engineState.renderPending) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Convert row range → flat item range
|
|
116
|
+
const itemRange = layout.getItemRange(renderStart, renderEnd, engineState.totalItems);
|
|
117
|
+
// Release items outside the new range
|
|
118
|
+
rendered.forEach((tracked, idx) => {
|
|
119
|
+
if (idx < itemRange.start || idx > itemRange.end) {
|
|
120
|
+
tracked.el.remove();
|
|
121
|
+
pool.release(tracked.el);
|
|
122
|
+
rendered.delete(idx);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Render items in range
|
|
126
|
+
const gridItemClass = `${classPrefix}-item ${classPrefix}-grid-item`;
|
|
127
|
+
const isf = resolveItemState?.() ?? null;
|
|
128
|
+
const selClass = isf ? `${classPrefix}-item--selected` : "";
|
|
129
|
+
const focClass = isf ? `${classPrefix}-item--focused` : "";
|
|
130
|
+
for (let i = itemRange.start; i <= itemRange.end; i++) {
|
|
131
|
+
const item = getItem(i);
|
|
132
|
+
if (!item)
|
|
133
|
+
continue;
|
|
134
|
+
let tracked = rendered.get(i);
|
|
135
|
+
if (tracked === undefined) {
|
|
136
|
+
const el = pool.acquire();
|
|
137
|
+
el.className = gridItemClass;
|
|
138
|
+
el.setAttribute("data-index", String(i));
|
|
139
|
+
el.setAttribute("data-id", String(item.id));
|
|
140
|
+
applyTemplate(el, item, i, isf);
|
|
141
|
+
tracked = { el, lastId: item.id };
|
|
142
|
+
rendered.set(i, tracked);
|
|
143
|
+
contentElement.appendChild(el);
|
|
144
|
+
}
|
|
145
|
+
else if (tracked.lastId !== item.id) {
|
|
146
|
+
tracked.el.setAttribute("data-id", String(item.id));
|
|
147
|
+
tracked.lastId = item.id;
|
|
148
|
+
applyTemplate(tracked.el, item, i, isf);
|
|
149
|
+
}
|
|
150
|
+
if (isf) {
|
|
151
|
+
isf(i, itemState);
|
|
152
|
+
tracked.el.classList.toggle(selClass, itemState.selected);
|
|
153
|
+
tracked.el.classList.toggle(focClass, itemState.focused);
|
|
154
|
+
if (itemState.selected)
|
|
155
|
+
tracked.el.setAttribute("aria-selected", "true");
|
|
156
|
+
else
|
|
157
|
+
tracked.el.removeAttribute("aria-selected");
|
|
158
|
+
}
|
|
159
|
+
applySizeStyles(tracked.el, i);
|
|
160
|
+
tracked.el.style.transform = buildTransform(i);
|
|
161
|
+
}
|
|
162
|
+
// Update content size for scrollbar
|
|
163
|
+
const totalSize = sizeCache.getTotalSize();
|
|
164
|
+
contentElement.style[horizontal ? "width" : "height"] = totalSize + "px";
|
|
165
|
+
// Update engine state for other hooks/plugins
|
|
166
|
+
engineState.prevRangeStart = renderStart;
|
|
167
|
+
engineState.prevRangeEnd = renderEnd;
|
|
168
|
+
engineState.renderPending = false;
|
|
169
|
+
// Fill EngineState buffers for plugins that read them
|
|
170
|
+
const count = itemRange.end - itemRange.start + 1;
|
|
171
|
+
engineState.visibleCount = Math.min(count, engineState.capacity);
|
|
172
|
+
engineState.startIndex = itemRange.start;
|
|
173
|
+
for (let i = 0; i < engineState.visibleCount; i++) {
|
|
174
|
+
const idx = itemRange.start + i;
|
|
175
|
+
const row = layout.getRow(idx);
|
|
176
|
+
engineState.visibleIndices[i] = idx;
|
|
177
|
+
engineState.visibleOffsets[i] = sizeCache.getOffset(row);
|
|
178
|
+
engineState.visibleSizes[i] = sizeCache.getSize(row);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function gridForceRender() {
|
|
182
|
+
if (engineState.destroyed)
|
|
183
|
+
return;
|
|
184
|
+
sizeCache.rebuild(getRowCount());
|
|
185
|
+
engineState.prevRangeStart = -1;
|
|
186
|
+
engineState.prevRangeEnd = -1;
|
|
187
|
+
engineState.renderPending = true;
|
|
188
|
+
forceNextRender = true;
|
|
189
|
+
gridRenderIfNeeded();
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
name: "grid",
|
|
193
|
+
priority: 10,
|
|
194
|
+
conflicts: ["masonry", "table"],
|
|
195
|
+
setup(ctx) {
|
|
196
|
+
const gap = config.gap ?? 0;
|
|
197
|
+
layout = createGridLayout({ columns: config.columns, gap });
|
|
198
|
+
sizeCache = ctx.sizeCache;
|
|
199
|
+
engineState = ctx.getState();
|
|
200
|
+
pool = ctx.pool;
|
|
201
|
+
contentElement = ctx.dom.content;
|
|
202
|
+
template = ctx.template;
|
|
203
|
+
horizontal = ctx.config.horizontal;
|
|
204
|
+
classPrefix = ctx.config.classPrefix;
|
|
205
|
+
overscan = ctx.config.overscan;
|
|
206
|
+
getItem = ctx.getItem.bind(ctx);
|
|
207
|
+
resolveItemState = () => ctx.getItemStateFn();
|
|
208
|
+
// Initialize container width
|
|
209
|
+
containerWidth = engineState.crossSize;
|
|
210
|
+
// Size cache in ROW space: each row = itemHeight + gap
|
|
211
|
+
// Inject grid context into dynamic height functions
|
|
212
|
+
const rawSpec = ctx.rawSizeSpec;
|
|
213
|
+
let baseRowSize;
|
|
214
|
+
if (typeof rawSpec === "function") {
|
|
215
|
+
const colWidth = layout.getColumnWidth(containerWidth);
|
|
216
|
+
const gridCtx = { columnWidth: colWidth, columns: config.columns, gap };
|
|
217
|
+
baseRowSize = rawSpec(0, gridCtx);
|
|
218
|
+
ctx.setSizeConfig((rowIndex) => {
|
|
219
|
+
gridCtx.columnWidth = layout.getColumnWidth(containerWidth);
|
|
220
|
+
const firstItem = rowIndex * config.columns;
|
|
221
|
+
return rawSpec(firstItem, gridCtx) + gap;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
baseRowSize = rawSpec;
|
|
226
|
+
if (gap > 0) {
|
|
227
|
+
ctx.setSizeConfig(baseRowSize + gap);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Size cache must have rowCount entries, not totalItems.
|
|
231
|
+
// setSizeConfig creates with state.totalItems — fix immediately.
|
|
232
|
+
sizeCache.rebuild(getRowCount());
|
|
233
|
+
// Fix trailing gap: last row's cached size includes gap that
|
|
234
|
+
// shouldn't add empty space at the bottom.
|
|
235
|
+
if (gap > 0) {
|
|
236
|
+
const origGetTotalSize = sizeCache.getTotalSize;
|
|
237
|
+
sizeCache.getTotalSize = () => {
|
|
238
|
+
const t = origGetTotalSize();
|
|
239
|
+
return t > 0 ? t - gap : 0;
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Virtual total = row count (not item count)
|
|
243
|
+
ctx.setVirtualTotalFn(() => getRowCount());
|
|
244
|
+
// Add CSS class
|
|
245
|
+
ctx.dom.root.classList.add(`${classPrefix}--grid`);
|
|
246
|
+
// Replace render pipeline
|
|
247
|
+
ctx.setRenderFn(gridRenderIfNeeded, gridForceRender);
|
|
248
|
+
// ── Public methods ─────────────────────────────────────────
|
|
249
|
+
ctx.registerMethod("getGridLayout", () => layout);
|
|
250
|
+
ctx.registerMethod("updateGrid", (newConfig) => {
|
|
251
|
+
if (newConfig.columns !== undefined) {
|
|
252
|
+
if (!Number.isInteger(newConfig.columns) || newConfig.columns < 1) {
|
|
253
|
+
throw new Error("[vlist] updateGrid: columns must be >= 1");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (newConfig.gap !== undefined && newConfig.gap < 0) {
|
|
257
|
+
throw new Error("[vlist] updateGrid: gap must be >= 0");
|
|
258
|
+
}
|
|
259
|
+
layout.update(newConfig);
|
|
260
|
+
if (newConfig.gap !== undefined || newConfig.columns !== undefined) {
|
|
261
|
+
const newGap = layout.gap;
|
|
262
|
+
if (typeof rawSpec === "function") {
|
|
263
|
+
const gridCtx = { columnWidth: layout.getColumnWidth(containerWidth), columns: layout.columns, gap: newGap };
|
|
264
|
+
ctx.setSizeConfig((rowIndex) => {
|
|
265
|
+
gridCtx.columnWidth = layout.getColumnWidth(containerWidth);
|
|
266
|
+
const firstItem = rowIndex * layout.columns;
|
|
267
|
+
return rawSpec(firstItem, gridCtx) + newGap;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
ctx.setSizeConfig(baseRowSize + newGap);
|
|
272
|
+
}
|
|
273
|
+
sizeCache.rebuild(getRowCount());
|
|
274
|
+
}
|
|
275
|
+
if (newConfig.columns !== undefined) {
|
|
276
|
+
ctx.setNavConfig({ ud: layout.columns });
|
|
277
|
+
}
|
|
278
|
+
containerWidth = engineState.crossSize;
|
|
279
|
+
gridForceRender();
|
|
280
|
+
});
|
|
281
|
+
// Override scrollToIndex: item index → row index
|
|
282
|
+
ctx.registerMethod("scrollToIndex", (index, alignOrOptions = "start") => {
|
|
283
|
+
const rowIndex = layout.getRow(index);
|
|
284
|
+
const totalRows = getRowCount();
|
|
285
|
+
if (totalRows === 0)
|
|
286
|
+
return;
|
|
287
|
+
const safeRow = Math.max(0, Math.min(rowIndex, totalRows - 1));
|
|
288
|
+
const offset = sizeCache.getOffset(safeRow);
|
|
289
|
+
const rowHeight = sizeCache.getSize(safeRow);
|
|
290
|
+
const cs = engineState.containerSize;
|
|
291
|
+
const totalSize = sizeCache.getTotalSize();
|
|
292
|
+
const maxScroll = Math.max(0, totalSize - cs);
|
|
293
|
+
const align = typeof alignOrOptions === "string" ? alignOrOptions : (alignOrOptions.align ?? "start");
|
|
294
|
+
const behavior = typeof alignOrOptions === "object" ? alignOrOptions.behavior : undefined;
|
|
295
|
+
const duration = typeof alignOrOptions === "object" ? alignOrOptions.duration : undefined;
|
|
296
|
+
let pos;
|
|
297
|
+
switch (align) {
|
|
298
|
+
case "center":
|
|
299
|
+
pos = offset - (cs - rowHeight) / 2;
|
|
300
|
+
break;
|
|
301
|
+
case "end":
|
|
302
|
+
pos = offset - cs + rowHeight;
|
|
303
|
+
break;
|
|
304
|
+
default:
|
|
305
|
+
pos = offset;
|
|
306
|
+
}
|
|
307
|
+
pos = Math.max(0, Math.min(pos, maxScroll));
|
|
308
|
+
if (behavior === "smooth" && duration && duration > 0) {
|
|
309
|
+
ctx.smoothScrollTo(pos, duration);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
ctx.scrollTo(pos);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
// ── 2D keyboard navigation ─────────────────────────────────
|
|
316
|
+
ctx.setNavConfig({
|
|
317
|
+
total: () => engineState.totalItems,
|
|
318
|
+
ud: config.columns,
|
|
319
|
+
lr: 1,
|
|
320
|
+
scrollIndex: (itemIndex) => layout.getRow(itemIndex),
|
|
321
|
+
});
|
|
322
|
+
// ── Cleanup ────────────────────────────────────────────────
|
|
323
|
+
ctx.registerDestroyHandler(() => {
|
|
324
|
+
for (const [, tracked] of rendered) {
|
|
325
|
+
tracked.el.remove();
|
|
326
|
+
}
|
|
327
|
+
rendered.clear();
|
|
328
|
+
ctx.dom.root.classList.remove(`${classPrefix}--grid`);
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
hooks: {
|
|
332
|
+
onResize(_width, _height) {
|
|
333
|
+
const newCross = engineState.crossSize;
|
|
334
|
+
if (Math.abs(newCross - containerWidth) < 1)
|
|
335
|
+
return;
|
|
336
|
+
containerWidth = newCross;
|
|
337
|
+
rendered.forEach((tracked, index) => {
|
|
338
|
+
applySizeStyles(tracked.el, index);
|
|
339
|
+
tracked.el.style.transform = buildTransform(index);
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
destroy() {
|
|
344
|
+
rendered.clear();
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|