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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Async Group Bridge
|
|
3
|
+
* Virtual group layer that bridges withAsync's sparse data manager with
|
|
4
|
+
* withGroups' layout system.
|
|
5
|
+
*
|
|
6
|
+
* Instead of physically inserting header pseudo-items into the data array
|
|
7
|
+
* (which is impossible with sparse storage), this bridge:
|
|
8
|
+
* - Observes items as they load via onItemsLoaded callbacks
|
|
9
|
+
* - Discovers group boundaries incrementally from loaded data
|
|
10
|
+
* - Maps between "data indices" (what the async manager knows) and
|
|
11
|
+
* "layout indices" (what the renderer sees, including header slots)
|
|
12
|
+
* - Provides header/item resolution at render time
|
|
13
|
+
*
|
|
14
|
+
* Group boundaries are only computed for contiguous loaded ranges.
|
|
15
|
+
* Unloaded gaps have no headers — placeholders render as regular items.
|
|
16
|
+
* When gaps fill in, boundaries are recomputed.
|
|
17
|
+
*/
|
|
18
|
+
import { findGroupByDataIndex, findGroupByLayoutIndex } from "./layout";
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Factory
|
|
21
|
+
// =============================================================================
|
|
22
|
+
const EMPTY_GROUP = {
|
|
23
|
+
key: "",
|
|
24
|
+
groupIndex: 0,
|
|
25
|
+
headerLayoutIndex: 0,
|
|
26
|
+
firstDataIndex: 0,
|
|
27
|
+
count: 0,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Create an async group bridge.
|
|
31
|
+
*
|
|
32
|
+
* The bridge discovers group boundaries incrementally as items load from
|
|
33
|
+
* the async adapter. It maintains a mapping between data indices (sparse
|
|
34
|
+
* storage) and layout indices (data items + group headers).
|
|
35
|
+
*
|
|
36
|
+
* @param config - Bridge configuration
|
|
37
|
+
* @param getItem - Item accessor from the async data manager
|
|
38
|
+
* @param isItemLoaded - Check if an item at index is loaded (not placeholder)
|
|
39
|
+
*/
|
|
40
|
+
export const createAsyncGroupBridge = (config, _getItem, _isItemLoaded) => {
|
|
41
|
+
let groups = [];
|
|
42
|
+
let dataTotal = 0;
|
|
43
|
+
let totalEntries = 0;
|
|
44
|
+
// Track group key for each loaded data index.
|
|
45
|
+
// This survives across onItemsLoaded calls so we can detect
|
|
46
|
+
// cross-page group boundaries.
|
|
47
|
+
const groupKeyByIndex = new Map();
|
|
48
|
+
/**
|
|
49
|
+
* Rebuild group boundaries from all known group keys.
|
|
50
|
+
*
|
|
51
|
+
* Scans from index 0 to dataTotal-1. For loaded items, uses the cached
|
|
52
|
+
* group key. For unloaded items, skips them — no header is inserted at
|
|
53
|
+
* load/unload boundaries.
|
|
54
|
+
*
|
|
55
|
+
* This runs on each onItemsLoaded. It's O(dataTotal) in theory but
|
|
56
|
+
* only does map lookups per index — fast in practice.
|
|
57
|
+
*/
|
|
58
|
+
const rebuildGroups = () => {
|
|
59
|
+
groups = [];
|
|
60
|
+
if (dataTotal === 0 || groupKeyByIndex.size === 0) {
|
|
61
|
+
totalEntries = dataTotal;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let currentKey = null;
|
|
65
|
+
let groupStart = -1;
|
|
66
|
+
// Running header count — each discovered group adds 1 header
|
|
67
|
+
let headerCount = 0;
|
|
68
|
+
for (let i = 0; i < dataTotal; i++) {
|
|
69
|
+
const key = groupKeyByIndex.get(i);
|
|
70
|
+
if (key === undefined) {
|
|
71
|
+
// Unloaded item — close any open group
|
|
72
|
+
if (currentKey !== null && groupStart >= 0) {
|
|
73
|
+
const count = i - groupStart;
|
|
74
|
+
groups.push({
|
|
75
|
+
key: currentKey,
|
|
76
|
+
groupIndex: groups.length,
|
|
77
|
+
headerLayoutIndex: groupStart + headerCount,
|
|
78
|
+
firstDataIndex: groupStart,
|
|
79
|
+
count,
|
|
80
|
+
});
|
|
81
|
+
headerCount++;
|
|
82
|
+
currentKey = null;
|
|
83
|
+
groupStart = -1;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (key !== currentKey) {
|
|
88
|
+
// Close previous group if any
|
|
89
|
+
if (currentKey !== null && groupStart >= 0) {
|
|
90
|
+
const count = i - groupStart;
|
|
91
|
+
groups.push({
|
|
92
|
+
key: currentKey,
|
|
93
|
+
groupIndex: groups.length,
|
|
94
|
+
headerLayoutIndex: groupStart + headerCount,
|
|
95
|
+
firstDataIndex: groupStart,
|
|
96
|
+
count,
|
|
97
|
+
});
|
|
98
|
+
headerCount++;
|
|
99
|
+
}
|
|
100
|
+
// Start new group
|
|
101
|
+
currentKey = key;
|
|
102
|
+
groupStart = i;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Close last open group
|
|
106
|
+
if (currentKey !== null && groupStart >= 0) {
|
|
107
|
+
const count = dataTotal - groupStart;
|
|
108
|
+
groups.push({
|
|
109
|
+
key: currentKey,
|
|
110
|
+
groupIndex: groups.length,
|
|
111
|
+
headerLayoutIndex: groupStart + headerCount,
|
|
112
|
+
firstDataIndex: groupStart,
|
|
113
|
+
count,
|
|
114
|
+
});
|
|
115
|
+
headerCount++;
|
|
116
|
+
}
|
|
117
|
+
totalEntries = dataTotal + headerCount;
|
|
118
|
+
};
|
|
119
|
+
// ── Public API ──
|
|
120
|
+
const onItemsLoaded = (items, offset, total) => {
|
|
121
|
+
dataTotal = total;
|
|
122
|
+
// Cache group keys for loaded items
|
|
123
|
+
for (let i = 0; i < items.length; i++) {
|
|
124
|
+
const dataIndex = offset + i;
|
|
125
|
+
const item = items[i];
|
|
126
|
+
if (item !== undefined) {
|
|
127
|
+
const key = config.getGroupForIndex(dataIndex, item);
|
|
128
|
+
groupKeyByIndex.set(dataIndex, key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
rebuildGroups();
|
|
132
|
+
};
|
|
133
|
+
const isHeader = (layoutIndex) => {
|
|
134
|
+
if (groups.length === 0)
|
|
135
|
+
return false;
|
|
136
|
+
const gi = findGroupByLayoutIndex(groups, layoutIndex);
|
|
137
|
+
const group = groups[gi];
|
|
138
|
+
return layoutIndex === group.headerLayoutIndex;
|
|
139
|
+
};
|
|
140
|
+
const getHeaderItem = (layoutIndex) => {
|
|
141
|
+
if (groups.length === 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
const gi = findGroupByLayoutIndex(groups, layoutIndex);
|
|
144
|
+
const group = groups[gi];
|
|
145
|
+
if (layoutIndex !== group.headerLayoutIndex)
|
|
146
|
+
return undefined;
|
|
147
|
+
return {
|
|
148
|
+
id: `__group_header_${group.groupIndex}`,
|
|
149
|
+
__groupHeader: true,
|
|
150
|
+
groupKey: group.key,
|
|
151
|
+
groupIndex: group.groupIndex,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const layoutToDataIndex = (layoutIndex) => {
|
|
155
|
+
if (groups.length === 0)
|
|
156
|
+
return layoutIndex;
|
|
157
|
+
const gi = findGroupByLayoutIndex(groups, layoutIndex);
|
|
158
|
+
const group = groups[gi];
|
|
159
|
+
if (layoutIndex === group.headerLayoutIndex) {
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
|
162
|
+
const offsetInGroup = layoutIndex - group.headerLayoutIndex - 1;
|
|
163
|
+
return group.firstDataIndex + offsetInGroup;
|
|
164
|
+
};
|
|
165
|
+
const dataToLayoutIndex = (dataIndex) => {
|
|
166
|
+
if (groups.length === 0)
|
|
167
|
+
return dataIndex;
|
|
168
|
+
const gi = findGroupByDataIndex(groups, dataIndex);
|
|
169
|
+
const group = groups[gi];
|
|
170
|
+
const offsetInGroup = dataIndex - group.firstDataIndex;
|
|
171
|
+
return group.headerLayoutIndex + 1 + offsetInGroup;
|
|
172
|
+
};
|
|
173
|
+
const getHeaderHeight = (_groupIndex) => config.headerHeight;
|
|
174
|
+
const getGroupAtLayoutIndex = (layoutIndex) => {
|
|
175
|
+
if (groups.length === 0)
|
|
176
|
+
return EMPTY_GROUP;
|
|
177
|
+
const gi = findGroupByLayoutIndex(groups, layoutIndex);
|
|
178
|
+
return groups[gi];
|
|
179
|
+
};
|
|
180
|
+
const getGroupAtDataIndex = (dataIndex) => {
|
|
181
|
+
if (groups.length === 0)
|
|
182
|
+
return EMPTY_GROUP;
|
|
183
|
+
const gi = findGroupByDataIndex(groups, dataIndex);
|
|
184
|
+
return groups[gi];
|
|
185
|
+
};
|
|
186
|
+
const insertAt = (dataIndex, item) => {
|
|
187
|
+
const newMap = new Map();
|
|
188
|
+
for (const [idx, key] of groupKeyByIndex) {
|
|
189
|
+
if (idx < dataIndex)
|
|
190
|
+
newMap.set(idx, key);
|
|
191
|
+
else
|
|
192
|
+
newMap.set(idx + 1, key);
|
|
193
|
+
}
|
|
194
|
+
groupKeyByIndex.clear();
|
|
195
|
+
for (const [idx, key] of newMap)
|
|
196
|
+
groupKeyByIndex.set(idx, key);
|
|
197
|
+
const key = config.getGroupForIndex(dataIndex, item);
|
|
198
|
+
groupKeyByIndex.set(dataIndex, key);
|
|
199
|
+
dataTotal++;
|
|
200
|
+
rebuildGroups();
|
|
201
|
+
};
|
|
202
|
+
const removeAt = (dataIndex) => {
|
|
203
|
+
// Remove the key at dataIndex and shift all subsequent keys down by 1
|
|
204
|
+
const newMap = new Map();
|
|
205
|
+
for (const [idx, key] of groupKeyByIndex) {
|
|
206
|
+
if (idx < dataIndex)
|
|
207
|
+
newMap.set(idx, key);
|
|
208
|
+
else if (idx > dataIndex)
|
|
209
|
+
newMap.set(idx - 1, key);
|
|
210
|
+
// idx === dataIndex is dropped
|
|
211
|
+
}
|
|
212
|
+
groupKeyByIndex.clear();
|
|
213
|
+
for (const [idx, key] of newMap)
|
|
214
|
+
groupKeyByIndex.set(idx, key);
|
|
215
|
+
dataTotal--;
|
|
216
|
+
rebuildGroups();
|
|
217
|
+
};
|
|
218
|
+
const reset = () => {
|
|
219
|
+
groups = [];
|
|
220
|
+
dataTotal = 0;
|
|
221
|
+
totalEntries = 0;
|
|
222
|
+
groupKeyByIndex.clear();
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
onItemsLoaded,
|
|
226
|
+
get totalEntries() {
|
|
227
|
+
return totalEntries;
|
|
228
|
+
},
|
|
229
|
+
get groupCount() {
|
|
230
|
+
return groups.length;
|
|
231
|
+
},
|
|
232
|
+
get groups() {
|
|
233
|
+
return groups;
|
|
234
|
+
},
|
|
235
|
+
isHeader,
|
|
236
|
+
getHeaderItem,
|
|
237
|
+
layoutToDataIndex,
|
|
238
|
+
dataToLayoutIndex,
|
|
239
|
+
getHeaderHeight,
|
|
240
|
+
getGroupAtLayoutIndex,
|
|
241
|
+
getGroupAtDataIndex,
|
|
242
|
+
insertAt,
|
|
243
|
+
removeAt,
|
|
244
|
+
reset,
|
|
245
|
+
};
|
|
246
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* vlist - Groups Domain
|
|
3
3
|
* Sticky headers and grouped lists
|
|
4
4
|
*/
|
|
5
|
-
export {
|
|
5
|
+
export { groups, type GroupsPluginConfig } from "./plugin";
|
|
6
6
|
export type { GroupsConfig, GroupBoundary, LayoutEntry, GroupHeaderItem, GroupLayout, StickyHeader, } from "./types";
|
|
7
7
|
export { isGroupHeader } from "./types";
|
|
8
8
|
export { createGroupLayout, buildLayoutItems, createGroupedSizeFn, } from "./layout";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Groups Domain
|
|
3
|
+
* Sticky headers and grouped lists
|
|
4
|
+
*/
|
|
5
|
+
// v2 Plugin
|
|
6
|
+
export { groups } from "./plugin";
|
|
7
|
+
export { isGroupHeader } from "./types";
|
|
8
|
+
// Layout
|
|
9
|
+
export { createGroupLayout, buildLayoutItems, createGroupedSizeFn, } from "./layout";
|
|
10
|
+
// Sticky Header
|
|
11
|
+
export { createStickyHeader } from "./sticky";
|
|
12
|
+
// Async Bridge
|
|
13
|
+
export { createAsyncGroupBridge } from "./async-bridge";
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist - Group Layout
|
|
3
|
+
* Computes group boundaries and maps between data indices and layout indices.
|
|
4
|
+
*
|
|
5
|
+
* The layout transforms a flat items array into a "layout" that includes
|
|
6
|
+
* group header pseudo-items interspersed at group boundaries:
|
|
7
|
+
*
|
|
8
|
+
* Data: [item0, item1, item2, item3, item4, item5]
|
|
9
|
+
* Groups: [ A, A, A, B, B, C ]
|
|
10
|
+
* Layout: [headerA, item0, item1, item2, headerB, item3, item4, headerC, item5]
|
|
11
|
+
* Index: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
|
|
12
|
+
*
|
|
13
|
+
* All lookups are O(log g) where g = number of groups, using binary search
|
|
14
|
+
* on the sorted group boundaries array.
|
|
15
|
+
*/
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Binary Search Helpers
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Find the last group whose headerLayoutIndex <= layoutIndex.
|
|
21
|
+
* Returns the group's index in the groups array.
|
|
22
|
+
*/
|
|
23
|
+
export const findGroupByLayoutIndex = (groups, layoutIndex) => {
|
|
24
|
+
let lo = 0;
|
|
25
|
+
let hi = groups.length - 1;
|
|
26
|
+
while (lo < hi) {
|
|
27
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
28
|
+
if (groups[mid].headerLayoutIndex <= layoutIndex) {
|
|
29
|
+
lo = mid;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
hi = mid - 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return lo;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Find the last group whose firstDataIndex <= dataIndex.
|
|
39
|
+
* Returns the group's index in the groups array.
|
|
40
|
+
*/
|
|
41
|
+
export const findGroupByDataIndex = (groups, dataIndex) => {
|
|
42
|
+
let lo = 0;
|
|
43
|
+
let hi = groups.length - 1;
|
|
44
|
+
while (lo < hi) {
|
|
45
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
46
|
+
if (groups[mid].firstDataIndex <= dataIndex) {
|
|
47
|
+
lo = mid;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
hi = mid - 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return lo;
|
|
54
|
+
};
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Layout Builder
|
|
57
|
+
// =============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Build the groups array from the items using getGroupForIndex.
|
|
60
|
+
*
|
|
61
|
+
* Items MUST be pre-sorted by group — a new group boundary is created
|
|
62
|
+
* whenever getGroupForIndex returns a different value than the previous call.
|
|
63
|
+
*/
|
|
64
|
+
const buildGroups = (itemCount, getGroupForIndex, getItem) => {
|
|
65
|
+
if (itemCount === 0)
|
|
66
|
+
return [];
|
|
67
|
+
const groups = [];
|
|
68
|
+
let currentKey = null;
|
|
69
|
+
let groupStart = 0;
|
|
70
|
+
let headerLayoutIndex = 0;
|
|
71
|
+
for (let i = 0; i < itemCount; i++) {
|
|
72
|
+
const item = getItem?.(i);
|
|
73
|
+
// Skip unloaded items — they extend the current group without
|
|
74
|
+
// creating boundaries. This avoids "Unknown" headers for placeholders.
|
|
75
|
+
if (!item)
|
|
76
|
+
continue;
|
|
77
|
+
const key = getGroupForIndex(i, item);
|
|
78
|
+
if (currentKey === null) {
|
|
79
|
+
// First loaded item — start first group
|
|
80
|
+
currentKey = key;
|
|
81
|
+
groupStart = 0;
|
|
82
|
+
// All items before this one (unloaded) belong to this group
|
|
83
|
+
}
|
|
84
|
+
else if (key !== currentKey) {
|
|
85
|
+
const count = i - groupStart;
|
|
86
|
+
groups.push({
|
|
87
|
+
key: currentKey,
|
|
88
|
+
groupIndex: groups.length,
|
|
89
|
+
headerLayoutIndex,
|
|
90
|
+
firstDataIndex: groupStart,
|
|
91
|
+
count,
|
|
92
|
+
});
|
|
93
|
+
headerLayoutIndex = headerLayoutIndex + 1 + count;
|
|
94
|
+
currentKey = key;
|
|
95
|
+
groupStart = i;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (currentKey !== null) {
|
|
99
|
+
// Close the last group — includes all remaining unloaded items
|
|
100
|
+
groups.push({
|
|
101
|
+
key: currentKey,
|
|
102
|
+
groupIndex: groups.length,
|
|
103
|
+
headerLayoutIndex,
|
|
104
|
+
firstDataIndex: groupStart,
|
|
105
|
+
count: itemCount - groupStart,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return groups;
|
|
109
|
+
};
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// Layout Items Builder
|
|
112
|
+
// =============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* Build the transformed layout items array with header pseudo-items inserted
|
|
115
|
+
* at group boundaries.
|
|
116
|
+
*
|
|
117
|
+
* @param items - Original data items
|
|
118
|
+
* @param groups - Computed group boundaries
|
|
119
|
+
* @returns Array of items and header pseudo-items in layout order
|
|
120
|
+
*/
|
|
121
|
+
export const buildLayoutItems = (items, groups) => {
|
|
122
|
+
if (items.length === 0 || groups.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const totalEntries = items.length + groups.length;
|
|
125
|
+
const result = new Array(totalEntries);
|
|
126
|
+
let layoutIdx = 0;
|
|
127
|
+
for (const group of groups) {
|
|
128
|
+
// Insert header pseudo-item
|
|
129
|
+
result[layoutIdx] = {
|
|
130
|
+
id: `__group_header_${group.groupIndex}`,
|
|
131
|
+
__groupHeader: true,
|
|
132
|
+
groupKey: group.key,
|
|
133
|
+
groupIndex: group.groupIndex,
|
|
134
|
+
};
|
|
135
|
+
layoutIdx++;
|
|
136
|
+
// Insert data items for this group
|
|
137
|
+
for (let i = 0; i < group.count; i++) {
|
|
138
|
+
result[layoutIdx] = items[group.firstDataIndex + i];
|
|
139
|
+
layoutIdx++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Height Function Builder
|
|
146
|
+
// =============================================================================
|
|
147
|
+
/**
|
|
148
|
+
* Create a size function for the layout (items + headers).
|
|
149
|
+
*
|
|
150
|
+
* Maps layout indices to sizes, accounting for both group headers and data items.
|
|
151
|
+
*
|
|
152
|
+
* @param layout - The group layout instance
|
|
153
|
+
* @param itemSize - Original item size config (number or function)
|
|
154
|
+
* @returns A size function (layoutIndex) => number suitable for SizeCache
|
|
155
|
+
*/
|
|
156
|
+
export const createGroupedSizeFn = (layout, itemSize, sticky = false) => {
|
|
157
|
+
const getItemSize = typeof itemSize === "number"
|
|
158
|
+
? (_dataIndex) => itemSize
|
|
159
|
+
: itemSize;
|
|
160
|
+
return (layoutIndex) => {
|
|
161
|
+
const entry = layout.getEntry(layoutIndex);
|
|
162
|
+
if (entry.type === "header") {
|
|
163
|
+
// When sticky headers are active the first group's inline header
|
|
164
|
+
// is redundant (the sticky header already shows it). Collapse it
|
|
165
|
+
// to 0 so it occupies no space in the layout.
|
|
166
|
+
if (sticky && entry.group.groupIndex === 0)
|
|
167
|
+
return 0;
|
|
168
|
+
return layout.getHeaderHeight(entry.group.groupIndex);
|
|
169
|
+
}
|
|
170
|
+
return getItemSize(entry.dataIndex);
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// GroupLayout Factory
|
|
175
|
+
// =============================================================================
|
|
176
|
+
/**
|
|
177
|
+
* Create a GroupLayout instance.
|
|
178
|
+
*
|
|
179
|
+
* The layout computes group boundaries from items and provides efficient
|
|
180
|
+
* O(log g) mappings between data indices and layout indices.
|
|
181
|
+
*
|
|
182
|
+
* @param itemCount - Number of data items
|
|
183
|
+
* @param config - Groups configuration
|
|
184
|
+
*/
|
|
185
|
+
export const createGroupLayout = (itemCount, config, getItem) => {
|
|
186
|
+
let groups = buildGroups(itemCount, config.getGroupForIndex, getItem);
|
|
187
|
+
let totalEntries = itemCount + groups.length;
|
|
188
|
+
// Pre-compute header sizes — resolve from height, width, or legacy headerHeight
|
|
189
|
+
const headerHeightConfig = config.header?.height ?? config.header?.width ?? config.headerHeight;
|
|
190
|
+
const getHeaderHeight = typeof headerHeightConfig === "number"
|
|
191
|
+
? (_groupIndex) => headerHeightConfig
|
|
192
|
+
: (groupIndex) => {
|
|
193
|
+
const group = groups[groupIndex];
|
|
194
|
+
if (!group)
|
|
195
|
+
return 0;
|
|
196
|
+
return headerHeightConfig(group.key, groupIndex);
|
|
197
|
+
};
|
|
198
|
+
// =========================================================================
|
|
199
|
+
// Public API
|
|
200
|
+
// =========================================================================
|
|
201
|
+
const getEntry = (layoutIndex) => {
|
|
202
|
+
if (groups.length === 0) {
|
|
203
|
+
// Fallback: shouldn't happen if totalEntries > 0
|
|
204
|
+
return {
|
|
205
|
+
type: "item",
|
|
206
|
+
dataIndex: layoutIndex,
|
|
207
|
+
group: {
|
|
208
|
+
key: "",
|
|
209
|
+
groupIndex: 0,
|
|
210
|
+
headerLayoutIndex: 0,
|
|
211
|
+
firstDataIndex: 0,
|
|
212
|
+
count: 0,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const gi = findGroupByLayoutIndex(groups, layoutIndex);
|
|
217
|
+
const group = groups[gi];
|
|
218
|
+
if (layoutIndex === group.headerLayoutIndex) {
|
|
219
|
+
return { type: "header", group };
|
|
220
|
+
}
|
|
221
|
+
// It's a data item within this group
|
|
222
|
+
const offsetInGroup = layoutIndex - group.headerLayoutIndex - 1;
|
|
223
|
+
const dataIndex = group.firstDataIndex + offsetInGroup;
|
|
224
|
+
return { type: "item", dataIndex, group };
|
|
225
|
+
};
|
|
226
|
+
const layoutToDataIndex = (layoutIndex) => {
|
|
227
|
+
if (groups.length === 0)
|
|
228
|
+
return layoutIndex;
|
|
229
|
+
const gi = findGroupByLayoutIndex(groups, layoutIndex);
|
|
230
|
+
const group = groups[gi];
|
|
231
|
+
if (layoutIndex === group.headerLayoutIndex) {
|
|
232
|
+
return -1; // It's a header
|
|
233
|
+
}
|
|
234
|
+
const offsetInGroup = layoutIndex - group.headerLayoutIndex - 1;
|
|
235
|
+
return group.firstDataIndex + offsetInGroup;
|
|
236
|
+
};
|
|
237
|
+
const dataToLayoutIndex = (dataIndex) => {
|
|
238
|
+
if (groups.length === 0)
|
|
239
|
+
return dataIndex;
|
|
240
|
+
const gi = findGroupByDataIndex(groups, dataIndex);
|
|
241
|
+
const group = groups[gi];
|
|
242
|
+
// Layout index = header layout index + 1 (skip header) + offset within group
|
|
243
|
+
const offsetInGroup = dataIndex - group.firstDataIndex;
|
|
244
|
+
return group.headerLayoutIndex + 1 + offsetInGroup;
|
|
245
|
+
};
|
|
246
|
+
const getGroupAtLayoutIndex = (layoutIndex) => {
|
|
247
|
+
if (groups.length === 0) {
|
|
248
|
+
return {
|
|
249
|
+
key: "",
|
|
250
|
+
groupIndex: 0,
|
|
251
|
+
headerLayoutIndex: 0,
|
|
252
|
+
firstDataIndex: 0,
|
|
253
|
+
count: 0,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const gi = findGroupByLayoutIndex(groups, layoutIndex);
|
|
257
|
+
return groups[gi];
|
|
258
|
+
};
|
|
259
|
+
const getGroupAtDataIndex = (dataIndex) => {
|
|
260
|
+
if (groups.length === 0) {
|
|
261
|
+
return {
|
|
262
|
+
key: "",
|
|
263
|
+
groupIndex: 0,
|
|
264
|
+
headerLayoutIndex: 0,
|
|
265
|
+
firstDataIndex: 0,
|
|
266
|
+
count: 0,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const gi = findGroupByDataIndex(groups, dataIndex);
|
|
270
|
+
return groups[gi];
|
|
271
|
+
};
|
|
272
|
+
const rebuild = (newItemCount, newGetItem) => {
|
|
273
|
+
groups = buildGroups(newItemCount, config.getGroupForIndex, newGetItem ?? getItem);
|
|
274
|
+
totalEntries = newItemCount + groups.length;
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
get totalEntries() {
|
|
278
|
+
return totalEntries;
|
|
279
|
+
},
|
|
280
|
+
get groupCount() {
|
|
281
|
+
return groups.length;
|
|
282
|
+
},
|
|
283
|
+
get groups() {
|
|
284
|
+
return groups;
|
|
285
|
+
},
|
|
286
|
+
getEntry,
|
|
287
|
+
layoutToDataIndex,
|
|
288
|
+
dataToLayoutIndex,
|
|
289
|
+
getGroupAtLayoutIndex,
|
|
290
|
+
getGroupAtDataIndex,
|
|
291
|
+
getHeaderHeight,
|
|
292
|
+
rebuild,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vlist v2 — Groups Plugin
|
|
3
|
+
*
|
|
4
|
+
* Adds grouped lists with sticky headers.
|
|
5
|
+
* Priority 10 — runs before selection (50).
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Transforms items list: inserts group header pseudo-items at group boundaries
|
|
9
|
+
* - Replaces size function: headers use header height, items use item height
|
|
10
|
+
* - Replaces render pipeline: handles grouped layout rendering
|
|
11
|
+
* - Sticky header: floating header that updates on scroll
|
|
12
|
+
* - CSS class: adds .vlist--grouped to root
|
|
13
|
+
*
|
|
14
|
+
* Restrictions:
|
|
15
|
+
* - Items must be pre-sorted by group
|
|
16
|
+
*/
|
|
17
|
+
import type { VListItem, GroupsConfig } from "../../types";
|
|
18
|
+
import type { VListPlugin } from "../../core/types";
|
|
19
|
+
export interface GroupsPluginConfig extends GroupsConfig {
|
|
20
|
+
}
|
|
21
|
+
export declare function groups<T extends VListItem = VListItem>(config: GroupsPluginConfig): VListPlugin<T>;
|
|
22
|
+
//# sourceMappingURL=plugin.d.ts.map
|