vlist 1.9.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.github.md +104 -97
  2. package/README.md +46 -33
  3. package/dist/constants.d.ts +11 -6
  4. package/dist/constants.js +83 -0
  5. package/dist/core/create.d.ts +10 -0
  6. package/dist/core/create.js +740 -0
  7. package/dist/core/dom.d.ts +8 -0
  8. package/dist/core/dom.js +47 -0
  9. package/dist/core/hooks.d.ts +16 -0
  10. package/dist/core/hooks.js +67 -0
  11. package/dist/core/index.d.ts +17 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/pipeline.d.ts +51 -0
  14. package/dist/core/pipeline.js +307 -0
  15. package/dist/core/pool.d.ts +9 -0
  16. package/dist/core/pool.js +42 -0
  17. package/dist/core/scroll.d.ts +32 -0
  18. package/dist/core/scroll.js +137 -0
  19. package/dist/core/sizes.d.ts +8 -0
  20. package/dist/core/sizes.js +6 -0
  21. package/dist/core/state.d.ts +47 -0
  22. package/dist/core/state.js +56 -0
  23. package/dist/core/types.d.ts +187 -0
  24. package/dist/core/types.js +7 -0
  25. package/dist/{builder → core}/velocity.d.ts +1 -1
  26. package/dist/core/velocity.js +33 -0
  27. package/dist/events/emitter.js +60 -0
  28. package/dist/events/index.js +6 -0
  29. package/dist/index.d.ts +28 -19
  30. package/dist/index.js +28 -1
  31. package/dist/internals.d.ts +11 -7
  32. package/dist/internals.js +60 -1
  33. package/dist/plugins/a11y/index.d.ts +2 -0
  34. package/dist/plugins/a11y/index.js +1 -0
  35. package/dist/plugins/a11y/plugin.d.ts +13 -0
  36. package/dist/plugins/a11y/plugin.js +259 -0
  37. package/dist/{features → plugins}/async/index.d.ts +1 -1
  38. package/dist/plugins/async/index.js +12 -0
  39. package/dist/{features → plugins}/async/manager.d.ts +5 -1
  40. package/dist/plugins/async/manager.js +568 -0
  41. package/dist/plugins/async/placeholder.js +154 -0
  42. package/dist/plugins/async/plugin.d.ts +48 -0
  43. package/dist/plugins/async/plugin.js +311 -0
  44. package/dist/plugins/async/sparse.js +540 -0
  45. package/dist/plugins/autosize/index.d.ts +5 -0
  46. package/dist/plugins/autosize/index.js +4 -0
  47. package/dist/plugins/autosize/plugin.d.ts +19 -0
  48. package/dist/plugins/autosize/plugin.js +185 -0
  49. package/dist/plugins/grid/index.d.ts +7 -0
  50. package/dist/plugins/grid/index.js +5 -0
  51. package/dist/plugins/grid/layout.js +275 -0
  52. package/dist/plugins/grid/plugin.d.ts +23 -0
  53. package/dist/plugins/grid/plugin.js +347 -0
  54. package/dist/plugins/grid/renderer.js +525 -0
  55. package/dist/plugins/grid/types.js +11 -0
  56. package/dist/plugins/groups/async-bridge.js +246 -0
  57. package/dist/{features → plugins}/groups/index.d.ts +1 -1
  58. package/dist/plugins/groups/index.js +13 -0
  59. package/dist/plugins/groups/layout.js +294 -0
  60. package/dist/plugins/groups/plugin.d.ts +22 -0
  61. package/dist/plugins/groups/plugin.js +571 -0
  62. package/dist/plugins/groups/sticky.js +255 -0
  63. package/dist/plugins/groups/types.js +12 -0
  64. package/dist/plugins/masonry/index.d.ts +8 -0
  65. package/dist/plugins/masonry/index.js +6 -0
  66. package/dist/plugins/masonry/layout.js +261 -0
  67. package/dist/plugins/masonry/plugin.d.ts +32 -0
  68. package/dist/plugins/masonry/plugin.js +381 -0
  69. package/dist/plugins/masonry/renderer.js +354 -0
  70. package/dist/plugins/masonry/types.js +9 -0
  71. package/dist/plugins/page/index.d.ts +5 -0
  72. package/dist/plugins/page/index.js +5 -0
  73. package/dist/plugins/page/plugin.d.ts +21 -0
  74. package/dist/plugins/page/plugin.js +166 -0
  75. package/dist/plugins/scale/index.d.ts +5 -0
  76. package/dist/plugins/scale/index.js +4 -0
  77. package/dist/plugins/scale/plugin.d.ts +24 -0
  78. package/dist/plugins/scale/plugin.js +507 -0
  79. package/dist/plugins/scrollbar/controller.js +574 -0
  80. package/dist/plugins/scrollbar/index.d.ts +7 -0
  81. package/dist/plugins/scrollbar/index.js +6 -0
  82. package/dist/plugins/scrollbar/plugin.d.ts +20 -0
  83. package/dist/plugins/scrollbar/plugin.js +93 -0
  84. package/dist/plugins/scrollbar/scrollbar.js +556 -0
  85. package/dist/plugins/selection/index.d.ts +6 -0
  86. package/dist/plugins/selection/index.js +7 -0
  87. package/dist/plugins/selection/plugin.d.ts +16 -0
  88. package/dist/plugins/selection/plugin.js +601 -0
  89. package/dist/{features → plugins}/selection/state.d.ts +8 -0
  90. package/dist/plugins/selection/state.js +332 -0
  91. package/dist/plugins/snapshots/index.d.ts +5 -0
  92. package/dist/plugins/snapshots/index.js +5 -0
  93. package/dist/plugins/snapshots/plugin.d.ts +17 -0
  94. package/dist/plugins/snapshots/plugin.js +301 -0
  95. package/dist/plugins/sortable/index.d.ts +6 -0
  96. package/dist/plugins/sortable/index.js +6 -0
  97. package/dist/plugins/sortable/plugin.d.ts +34 -0
  98. package/dist/plugins/sortable/plugin.js +753 -0
  99. package/dist/plugins/table/header.js +501 -0
  100. package/dist/{features → plugins}/table/index.d.ts +1 -1
  101. package/dist/plugins/table/index.js +12 -0
  102. package/dist/plugins/table/layout.js +211 -0
  103. package/dist/plugins/table/plugin.d.ts +20 -0
  104. package/dist/plugins/table/plugin.js +391 -0
  105. package/dist/plugins/table/renderer.js +625 -0
  106. package/dist/plugins/table/types.js +12 -0
  107. package/dist/plugins/transition/index.d.ts +5 -0
  108. package/dist/plugins/transition/index.js +5 -0
  109. package/dist/plugins/transition/plugin.d.ts +22 -0
  110. package/dist/plugins/transition/plugin.js +405 -0
  111. package/dist/rendering/aria.js +23 -0
  112. package/dist/rendering/index.js +18 -0
  113. package/dist/rendering/measured.js +98 -0
  114. package/dist/rendering/renderer.js +586 -0
  115. package/dist/rendering/scale.js +267 -0
  116. package/dist/rendering/scroll.js +71 -0
  117. package/dist/rendering/sizes.js +193 -0
  118. package/dist/rendering/sort.js +65 -0
  119. package/dist/rendering/viewport.js +268 -0
  120. package/dist/size.json +1 -1
  121. package/dist/types.js +5 -0
  122. package/dist/utils/padding.d.ts +2 -4
  123. package/dist/utils/padding.js +49 -0
  124. package/dist/utils/stats.js +124 -0
  125. package/dist/vlist-grid.css +1 -1
  126. package/dist/vlist-masonry.css +1 -1
  127. package/dist/vlist-table.css +1 -1
  128. package/dist/vlist.css +1 -1
  129. package/package.json +9 -4
  130. package/dist/builder/a11y.d.ts +0 -16
  131. package/dist/builder/api.d.ts +0 -21
  132. package/dist/builder/context.d.ts +0 -36
  133. package/dist/builder/core.d.ts +0 -16
  134. package/dist/builder/data.d.ts +0 -71
  135. package/dist/builder/dom.d.ts +0 -15
  136. package/dist/builder/index.d.ts +0 -25
  137. package/dist/builder/materialize.d.ts +0 -166
  138. package/dist/builder/pool.d.ts +0 -10
  139. package/dist/builder/range.d.ts +0 -10
  140. package/dist/builder/scroll.d.ts +0 -24
  141. package/dist/builder/types.d.ts +0 -512
  142. package/dist/features/async/feature.d.ts +0 -72
  143. package/dist/features/autosize/feature.d.ts +0 -34
  144. package/dist/features/autosize/index.d.ts +0 -2
  145. package/dist/features/grid/feature.d.ts +0 -48
  146. package/dist/features/grid/index.d.ts +0 -9
  147. package/dist/features/groups/feature.d.ts +0 -75
  148. package/dist/features/masonry/feature.d.ts +0 -45
  149. package/dist/features/masonry/index.d.ts +0 -9
  150. package/dist/features/page/feature.d.ts +0 -109
  151. package/dist/features/page/index.d.ts +0 -9
  152. package/dist/features/scale/feature.d.ts +0 -42
  153. package/dist/features/scale/index.d.ts +0 -10
  154. package/dist/features/scrollbar/feature.d.ts +0 -81
  155. package/dist/features/scrollbar/index.d.ts +0 -8
  156. package/dist/features/selection/feature.d.ts +0 -91
  157. package/dist/features/selection/index.d.ts +0 -7
  158. package/dist/features/snapshots/feature.d.ts +0 -79
  159. package/dist/features/snapshots/index.d.ts +0 -9
  160. package/dist/features/sortable/feature.d.ts +0 -101
  161. package/dist/features/sortable/index.d.ts +0 -6
  162. package/dist/features/table/feature.d.ts +0 -67
  163. package/dist/features/transition/feature.d.ts +0 -30
  164. package/dist/features/transition/index.d.ts +0 -9
  165. /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
  166. /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
  167. /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
  168. /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
  169. /package/dist/{features → plugins}/grid/types.d.ts +0 -0
  170. /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
  171. /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
  172. /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
  173. /package/dist/{features → plugins}/groups/types.d.ts +0 -0
  174. /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
  175. /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
  176. /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
  177. /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
  178. /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
  179. /package/dist/{features → plugins}/table/header.d.ts +0 -0
  180. /package/dist/{features → plugins}/table/layout.d.ts +0 -0
  181. /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
  182. /package/dist/{features → plugins}/table/types.d.ts +0 -0
@@ -0,0 +1,601 @@
1
+ /**
2
+ * vlist v2 — Selection Plugin
3
+ *
4
+ * Manages selection state, click/keyboard handlers, ARIA attributes.
5
+ * Adapted from v1 withSelection feature + a11y.ts to the v2 plugin interface.
6
+ */
7
+ import { createSelectionState, moveFocus, getSelectedArray, claimPlaceholderSelection, } from "./state";
8
+ import { PLACEHOLDER_ID_PREFIX } from "../../constants";
9
+ // =============================================================================
10
+ // Factory
11
+ // =============================================================================
12
+ const focusPreventScroll = { preventScroll: true };
13
+ export function selection(config) {
14
+ const mode = config?.mode ?? "single";
15
+ const followFocus = config?.followFocus ?? false;
16
+ const focusOnClick = config?.focusOnClick ?? false;
17
+ let state;
18
+ let getItem;
19
+ let forceRender;
20
+ let emitter;
21
+ let dom;
22
+ let sizeCache;
23
+ let engineState;
24
+ let scrollTo;
25
+ let lastSelectedIndex = -1;
26
+ const selectedItemCache = new Map();
27
+ let l2dFn = null;
28
+ let d2lFn = null;
29
+ let isGHFn = null;
30
+ let sivFn = null;
31
+ let getTotalFn;
32
+ let resolved = false;
33
+ function resolveOnce(ctx) {
34
+ if (resolved)
35
+ return;
36
+ resolved = true;
37
+ l2dFn = ctx.getMethod("_layoutToDataIndex") ?? null;
38
+ d2lFn = ctx.getMethod("_dataToLayoutIndex") ?? null;
39
+ isGHFn = ctx.getMethod("_isGroupHeader") ?? null;
40
+ sivFn = ctx.getMethod("_scrollItemIntoView") ?? null;
41
+ const gl = ctx.getMethod("getGroupLayout");
42
+ if (gl) {
43
+ const layout = gl();
44
+ getTotalFn = () => layout.totalEntries;
45
+ }
46
+ }
47
+ const toDataIndex = (layoutIdx) => l2dFn ? l2dFn(layoutIdx) : layoutIdx;
48
+ const getItemAtLayout = (layoutIdx) => {
49
+ const di = toDataIndex(layoutIdx);
50
+ return di >= 0 ? getItem(di) : undefined;
51
+ };
52
+ const skipHeaders = (from, dir, total) => {
53
+ if (!isGHFn)
54
+ return from;
55
+ let i = from;
56
+ while (i >= 0 && i < total) {
57
+ if (!isGHFn(i))
58
+ return i;
59
+ i += dir;
60
+ }
61
+ i = from - dir;
62
+ while (i >= 0 && i < total) {
63
+ if (!isGHFn(i))
64
+ return i;
65
+ i -= dir;
66
+ }
67
+ return from;
68
+ };
69
+ // ── Selection mutations (keep Set + item cache in sync) ────────
70
+ function doSelect(id, item) {
71
+ if (mode === "single") {
72
+ state.selected.clear();
73
+ selectedItemCache.clear();
74
+ }
75
+ state.selected.add(id);
76
+ if (item)
77
+ selectedItemCache.set(id, item);
78
+ }
79
+ function doToggle(id, item) {
80
+ if (state.selected.has(id)) {
81
+ state.selected.delete(id);
82
+ selectedItemCache.delete(id);
83
+ }
84
+ else {
85
+ doSelect(id, item);
86
+ }
87
+ }
88
+ function doClear() {
89
+ state.selected.clear();
90
+ selectedItemCache.clear();
91
+ }
92
+ function doDeselect(id) {
93
+ state.selected.delete(id);
94
+ selectedItemCache.delete(id);
95
+ }
96
+ function doSelectRange(from, to) {
97
+ const start = Math.min(from, to);
98
+ const end = Math.max(from, to);
99
+ for (let i = start; i <= end; i++) {
100
+ const item = getItem(i);
101
+ if (item) {
102
+ state.selected.add(item.id);
103
+ selectedItemCache.set(item.id, item);
104
+ }
105
+ }
106
+ }
107
+ function doSelectAll() {
108
+ const total = engineState.totalItems;
109
+ for (let i = 0; i < total; i++) {
110
+ const item = getItem(i);
111
+ if (item) {
112
+ state.selected.add(item.id);
113
+ selectedItemCache.set(item.id, item);
114
+ }
115
+ }
116
+ }
117
+ function collectSelectedItems() {
118
+ if (state.selected.size === 0)
119
+ return [];
120
+ if (selectedItemCache.size >= state.selected.size) {
121
+ const result = [];
122
+ for (const id of state.selected) {
123
+ const item = selectedItemCache.get(id);
124
+ if (item)
125
+ result.push(item);
126
+ }
127
+ if (result.length === state.selected.size)
128
+ return result;
129
+ }
130
+ const result = [];
131
+ const remaining = new Set(state.selected);
132
+ const total = engineState.totalItems;
133
+ for (let i = 0; i < total && remaining.size > 0; i++) {
134
+ const item = getItem(i);
135
+ if (item && remaining.has(item.id)) {
136
+ result.push(item);
137
+ selectedItemCache.set(item.id, item);
138
+ remaining.delete(item.id);
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ function emitSelectionChange() {
144
+ forceRender();
145
+ emitter.emit("selection:change", {
146
+ selected: getSelectedArray(state.selected),
147
+ items: collectSelectedItems(),
148
+ });
149
+ }
150
+ let hitItem = null;
151
+ let hitIndex = -1;
152
+ return {
153
+ name: "selection",
154
+ priority: 50,
155
+ setup(ctx) {
156
+ state = createSelectionState(config?.initial);
157
+ getItem = ctx.getItem.bind(ctx);
158
+ forceRender = ctx.forceRender.bind(ctx);
159
+ emitter = ctx.emitter;
160
+ dom = ctx.dom;
161
+ const resolvedConfig = ctx.config;
162
+ sizeCache = ctx.sizeCache;
163
+ engineState = ctx.getState();
164
+ scrollTo = ctx.scrollTo.bind(ctx);
165
+ getTotalFn = () => engineState.totalItems;
166
+ if (mode === "none") {
167
+ ctx.registerMethod("select", () => { });
168
+ ctx.registerMethod("deselect", () => { });
169
+ ctx.registerMethod("toggleSelect", () => { });
170
+ ctx.registerMethod("selectAll", () => { });
171
+ ctx.registerMethod("clearSelection", () => { });
172
+ ctx.registerMethod("getSelected", () => []);
173
+ ctx.registerMethod("getSelectedItems", () => []);
174
+ ctx.registerMethod("selectNext", () => { });
175
+ ctx.registerMethod("selectPrevious", () => { });
176
+ ctx.registerMethod("_seedSelection", () => { });
177
+ ctx.registerMethod("_getFocusedId", () => undefined);
178
+ ctx.registerMethod("_focusById", () => { });
179
+ return;
180
+ }
181
+ const classPrefix = resolvedConfig.classPrefix;
182
+ ctx.setItemStateFn((index, is) => {
183
+ resolveOnce(ctx);
184
+ if (state.selected.size > 0) {
185
+ const di = toDataIndex(index);
186
+ const item = di >= 0 ? getItem(di) : undefined;
187
+ const id = item?.id;
188
+ if (id !== undefined) {
189
+ if (state.selected.has(id)) {
190
+ is.selected = true;
191
+ }
192
+ else if (claimPlaceholderSelection(state.selected, di, id)) {
193
+ is.selected = true;
194
+ selectedItemCache.delete(PLACEHOLDER_ID_PREFIX + di);
195
+ selectedItemCache.set(id, item);
196
+ }
197
+ else {
198
+ is.selected = false;
199
+ }
200
+ }
201
+ else {
202
+ is.selected = false;
203
+ }
204
+ }
205
+ else {
206
+ is.selected = false;
207
+ }
208
+ is.focused = state.focusVisible && state.focusedIndex === index;
209
+ });
210
+ ctx.registerMethod("_getSelectedIds", () => state.selected);
211
+ ctx.registerMethod("_getFocusedIndex", () => state.focusVisible ? state.focusedIndex : -1);
212
+ dom.root.classList.add(`${classPrefix}--selectable`);
213
+ // ── Helpers ────────────────────────────────────────────────
214
+ const findItemFromEvent = (event) => {
215
+ hitItem = null;
216
+ hitIndex = -1;
217
+ const el = event.target.closest("[data-index]");
218
+ if (!el)
219
+ return false;
220
+ const layoutIdx = parseInt(el.dataset.index ?? "-1", 10);
221
+ if (layoutIdx < 0)
222
+ return false;
223
+ const di = toDataIndex(layoutIdx);
224
+ if (di < 0)
225
+ return false;
226
+ const item = getItem(di);
227
+ if (!item)
228
+ return false;
229
+ hitItem = item;
230
+ hitIndex = layoutIdx;
231
+ return true;
232
+ };
233
+ const scrollFocusIntoView = (index) => {
234
+ if (index < 0)
235
+ return;
236
+ if (sivFn) {
237
+ sivFn(index);
238
+ return;
239
+ }
240
+ const nav = ctx.getNavConfig();
241
+ const ci = nav.scrollIndex ? nav.scrollIndex(index) : index;
242
+ const offset = sizeCache.getOffset(ci);
243
+ const size = sizeCache.getSize(ci);
244
+ const cs = engineState.containerSize;
245
+ const sp = engineState.scrollPosition;
246
+ if (offset < sp) {
247
+ scrollTo(offset);
248
+ }
249
+ else if (offset + size > sp + cs) {
250
+ scrollTo(offset - cs + size);
251
+ }
252
+ };
253
+ // ── Focus In/Out ──────────────────────────────────────────
254
+ const onFocusIn = () => {
255
+ if (engineState.destroyed)
256
+ return;
257
+ resolveOnce(ctx);
258
+ if (!dom.content.matches(":focus-visible") && !dom.root.matches(":focus-visible"))
259
+ return;
260
+ const t = getTotalFn();
261
+ if (t === 0)
262
+ return;
263
+ let tgt = state.focusedIndex >= 0 ? Math.min(state.focusedIndex, t - 1) : 0;
264
+ tgt = skipHeaders(tgt, 1, t);
265
+ state.focusedIndex = tgt;
266
+ state.focusVisible = true;
267
+ dom.content.setAttribute("aria-activedescendant", `${classPrefix}-item-${tgt}`);
268
+ scrollFocusIntoView(tgt);
269
+ forceRender();
270
+ };
271
+ dom.root.addEventListener("focusin", onFocusIn);
272
+ const onFocusOut = (e) => {
273
+ if (engineState.destroyed)
274
+ return;
275
+ const rel = e.relatedTarget;
276
+ if (rel && dom.root.contains(rel))
277
+ return;
278
+ state.focusVisible = false;
279
+ forceRender();
280
+ dom.content.removeAttribute("aria-activedescendant");
281
+ };
282
+ dom.root.addEventListener("focusout", onFocusOut);
283
+ ctx.registerDestroyHandler(() => {
284
+ dom.root.removeEventListener("focusin", onFocusIn);
285
+ dom.root.removeEventListener("focusout", onFocusOut);
286
+ });
287
+ // ── Click handler ─────────────────────────────────────────
288
+ ctx.registerClickHandler((event) => {
289
+ resolveOnce(ctx);
290
+ if (!findItemFromEvent(event))
291
+ return;
292
+ if (mode === "multiple" && event.shiftKey && state.focusedIndex >= 0) {
293
+ const anchor = lastSelectedIndex >= 0 ? lastSelectedIndex : state.focusedIndex;
294
+ const anchorData = toDataIndex(anchor);
295
+ const hitData = toDataIndex(hitIndex);
296
+ if (anchorData >= 0 && hitData >= 0) {
297
+ doSelectRange(anchorData, hitData);
298
+ }
299
+ state.focusedIndex = hitIndex;
300
+ state.focusVisible = focusOnClick;
301
+ lastSelectedIndex = hitIndex;
302
+ emitSelectionChange();
303
+ return;
304
+ }
305
+ state.focusedIndex = hitIndex;
306
+ state.focusVisible = focusOnClick;
307
+ lastSelectedIndex = hitIndex;
308
+ dom.content.focus(focusPreventScroll);
309
+ doToggle(hitItem.id, hitItem);
310
+ emitSelectionChange();
311
+ });
312
+ // ── Keyboard handler ──────────────────────────────────────
313
+ if (resolvedConfig.interactive) {
314
+ ctx.registerKeydownHandler((event) => {
315
+ resolveOnce(ctx);
316
+ const total = getTotalFn();
317
+ if (total === 0)
318
+ return;
319
+ const nav = ctx.getNavConfig();
320
+ const prevFocus = state.focusedIndex;
321
+ let handled = false;
322
+ let selectionChanged = false;
323
+ if (nav.navigate && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "PageUp" || event.key === "PageDown" || event.key === "Home" || event.key === "End")) {
324
+ const next = nav.navigate(state.focusedIndex, event.key, total);
325
+ if (next !== state.focusedIndex) {
326
+ state.focusedIndex = Math.max(0, Math.min(next, total - 1));
327
+ }
328
+ state.focusVisible = true;
329
+ handled = true;
330
+ }
331
+ else
332
+ switch (event.key) {
333
+ case "ArrowUp": {
334
+ const ud = nav.ud || 1;
335
+ const lr = nav.lr;
336
+ const hz = resolvedConfig.horizontal;
337
+ if (hz && !lr)
338
+ break;
339
+ moveFocus(state, -(hz ? lr : ud), total, resolvedConfig.reverse);
340
+ state.focusVisible = true;
341
+ handled = true;
342
+ break;
343
+ }
344
+ case "ArrowDown": {
345
+ const ud = nav.ud || 1;
346
+ const lr = nav.lr;
347
+ const hz = resolvedConfig.horizontal;
348
+ if (hz && !lr)
349
+ break;
350
+ moveFocus(state, hz ? lr : ud, total, resolvedConfig.reverse);
351
+ state.focusVisible = true;
352
+ handled = true;
353
+ break;
354
+ }
355
+ case "ArrowLeft": {
356
+ const ud = nav.ud || 1;
357
+ const lr = nav.lr;
358
+ const hz = resolvedConfig.horizontal;
359
+ if (!hz && !lr)
360
+ break;
361
+ moveFocus(state, -(hz ? ud : lr), total, resolvedConfig.reverse);
362
+ state.focusVisible = true;
363
+ handled = true;
364
+ break;
365
+ }
366
+ case "ArrowRight": {
367
+ const ud = nav.ud || 1;
368
+ const lr = nav.lr;
369
+ const hz = resolvedConfig.horizontal;
370
+ if (!hz && !lr)
371
+ break;
372
+ moveFocus(state, hz ? ud : lr, total, resolvedConfig.reverse);
373
+ state.focusVisible = true;
374
+ handled = true;
375
+ break;
376
+ }
377
+ case "Home":
378
+ if (total > 0)
379
+ state.focusedIndex = 0;
380
+ state.focusVisible = true;
381
+ handled = true;
382
+ break;
383
+ case "End":
384
+ if (total > 0)
385
+ state.focusedIndex = total - 1;
386
+ state.focusVisible = true;
387
+ handled = true;
388
+ break;
389
+ case "PageUp":
390
+ case "PageDown": {
391
+ const ud = nav.ud || 1;
392
+ const idx = Math.max(0, state.focusedIndex);
393
+ const si = nav.scrollIndex ? nav.scrollIndex(idx) : idx;
394
+ const rowH = sizeCache.getSize(si);
395
+ const cs = engineState.containerSize;
396
+ const visRows = rowH > 0 ? Math.max(1, Math.floor(cs / rowH)) : 10;
397
+ const pageSize = visRows * ud;
398
+ if (l2dFn && d2lFn) {
399
+ const curData = l2dFn(state.focusedIndex);
400
+ const step = event.key === "PageUp" ? -pageSize : pageSize;
401
+ const maxData = engineState.totalItems - 1;
402
+ state.focusedIndex = d2lFn(Math.max(0, Math.min(maxData, curData + step)));
403
+ }
404
+ else {
405
+ moveFocus(state, event.key === "PageUp" ? -pageSize : pageSize, total);
406
+ }
407
+ state.focusVisible = true;
408
+ handled = true;
409
+ break;
410
+ }
411
+ case " ":
412
+ case "Enter":
413
+ if (event.key === " " && event.shiftKey && mode === "multiple" && state.focusedIndex >= 0) {
414
+ if (lastSelectedIndex >= 0) {
415
+ const fromData = toDataIndex(lastSelectedIndex);
416
+ const toData = toDataIndex(state.focusedIndex);
417
+ if (fromData >= 0 && toData >= 0) {
418
+ doSelectRange(fromData, toData);
419
+ }
420
+ }
421
+ state.focusVisible = true;
422
+ selectionChanged = true;
423
+ handled = true;
424
+ break;
425
+ }
426
+ if (state.focusedIndex >= 0) {
427
+ const item = getItemAtLayout(state.focusedIndex);
428
+ if (item) {
429
+ doToggle(item.id, item);
430
+ lastSelectedIndex = state.focusedIndex;
431
+ }
432
+ state.focusVisible = true;
433
+ selectionChanged = true;
434
+ handled = true;
435
+ }
436
+ break;
437
+ case "a":
438
+ if ((event.ctrlKey || event.metaKey) && mode === "multiple") {
439
+ if (state.selected.size === engineState.totalItems) {
440
+ doClear();
441
+ }
442
+ else {
443
+ doSelectAll();
444
+ }
445
+ state.focusVisible = true;
446
+ selectionChanged = true;
447
+ handled = true;
448
+ }
449
+ break;
450
+ case "Delete":
451
+ case "Backspace":
452
+ if (state.selected.size > 0) {
453
+ emitter.emit("delete", {
454
+ selected: getSelectedArray(state.selected),
455
+ items: collectSelectedItems(),
456
+ });
457
+ handled = true;
458
+ }
459
+ break;
460
+ }
461
+ // Skip group headers before shift-selection uses focusedIndex
462
+ if (state.focusedIndex !== prevFocus && isGHFn) {
463
+ const dir = state.focusedIndex > prevFocus ? 1 : -1;
464
+ state.focusedIndex = skipHeaders(state.focusedIndex, dir, total);
465
+ }
466
+ const focusMoved = state.focusedIndex !== prevFocus;
467
+ // Shift+movement: extend selection
468
+ if (event.shiftKey && mode === "multiple" && !selectionChanged && focusMoved) {
469
+ const isArrow = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "ArrowLeft" || event.key === "ArrowRight";
470
+ if (isArrow) {
471
+ const destItem = getItemAtLayout(state.focusedIndex);
472
+ if (destItem)
473
+ doToggle(destItem.id, destItem);
474
+ lastSelectedIndex = state.focusedIndex;
475
+ selectionChanged = true;
476
+ }
477
+ const isCtrlHomeEnd = (event.ctrlKey || event.metaKey)
478
+ && (event.key === "Home" || event.key === "End");
479
+ if (isCtrlHomeEnd) {
480
+ const fromData = toDataIndex(prevFocus >= 0 ? prevFocus : state.focusedIndex);
481
+ const toData = toDataIndex(state.focusedIndex);
482
+ if (fromData >= 0 && toData >= 0) {
483
+ doSelectRange(fromData, toData);
484
+ }
485
+ lastSelectedIndex = state.focusedIndex;
486
+ selectionChanged = true;
487
+ }
488
+ }
489
+ // Follow focus: auto-select on movement
490
+ if (followFocus && mode === "single" && !selectionChanged && focusMoved && state.focusedIndex >= 0) {
491
+ const item = getItemAtLayout(state.focusedIndex);
492
+ if (item)
493
+ doSelect(item.id, item);
494
+ selectionChanged = true;
495
+ }
496
+ if (handled) {
497
+ event.preventDefault();
498
+ if (focusMoved && state.focusedIndex >= 0) {
499
+ scrollFocusIntoView(state.focusedIndex);
500
+ dom.content.setAttribute("aria-activedescendant", `${classPrefix}-item-${state.focusedIndex}`);
501
+ }
502
+ if (selectionChanged) {
503
+ emitSelectionChange();
504
+ }
505
+ else if (focusMoved) {
506
+ forceRender();
507
+ if (state.focusedIndex >= 0) {
508
+ const item = getItemAtLayout(state.focusedIndex);
509
+ if (item) {
510
+ emitter.emit("focus:change", { id: item.id, index: state.focusedIndex });
511
+ }
512
+ }
513
+ }
514
+ }
515
+ });
516
+ }
517
+ // ── Public methods ────────────────────────────────────────
518
+ ctx.registerMethod("select", (...ids) => {
519
+ for (const id of ids)
520
+ doSelect(id);
521
+ emitSelectionChange();
522
+ });
523
+ ctx.registerMethod("deselect", (...ids) => {
524
+ for (const id of ids)
525
+ doDeselect(id);
526
+ emitSelectionChange();
527
+ });
528
+ ctx.registerMethod("toggleSelect", (id) => {
529
+ doToggle(id);
530
+ emitSelectionChange();
531
+ });
532
+ ctx.registerMethod("selectAll", () => {
533
+ if (mode !== "multiple")
534
+ return;
535
+ doSelectAll();
536
+ emitSelectionChange();
537
+ });
538
+ ctx.registerMethod("clearSelection", () => {
539
+ doClear();
540
+ emitSelectionChange();
541
+ });
542
+ ctx.registerMethod("getSelected", () => {
543
+ return getSelectedArray(state.selected);
544
+ });
545
+ ctx.registerMethod("getSelectedItems", () => {
546
+ return collectSelectedItems();
547
+ });
548
+ ctx.registerMethod("selectNext", () => {
549
+ resolveOnce(ctx);
550
+ const total = getTotalFn();
551
+ if (total === 0)
552
+ return;
553
+ moveFocus(state, 1, total, resolvedConfig.reverse);
554
+ if (isGHFn)
555
+ state.focusedIndex = skipHeaders(state.focusedIndex, 1, total);
556
+ const item = getItemAtLayout(state.focusedIndex);
557
+ if (item)
558
+ doSelect(item.id, item);
559
+ emitSelectionChange();
560
+ });
561
+ ctx.registerMethod("selectPrevious", () => {
562
+ resolveOnce(ctx);
563
+ const total = getTotalFn();
564
+ if (total === 0)
565
+ return;
566
+ moveFocus(state, -1, total, resolvedConfig.reverse);
567
+ if (isGHFn)
568
+ state.focusedIndex = skipHeaders(state.focusedIndex, -1, total);
569
+ const item = getItemAtLayout(state.focusedIndex);
570
+ if (item)
571
+ doSelect(item.id, item);
572
+ emitSelectionChange();
573
+ });
574
+ // ── Internal methods (used by snapshots, sortable) ────────
575
+ ctx.registerMethod("_seedSelection", (ids) => {
576
+ for (const id of ids)
577
+ state.selected.add(id);
578
+ });
579
+ ctx.registerMethod("_getFocusedId", () => {
580
+ if (state.focusedIndex < 0)
581
+ return undefined;
582
+ return getItemAtLayout(state.focusedIndex)?.id;
583
+ });
584
+ ctx.registerMethod("_focusById", (id) => {
585
+ const total = engineState.totalItems;
586
+ for (let i = 0; i < total; i++) {
587
+ const item = getItem(i);
588
+ if (item && item.id === id) {
589
+ const layoutIdx = d2lFn ? d2lFn(i) : i;
590
+ state.focusedIndex = layoutIdx;
591
+ emitter.emit("focus:change", { id, index: layoutIdx });
592
+ return;
593
+ }
594
+ }
595
+ });
596
+ },
597
+ destroy() {
598
+ selectedItemCache.clear();
599
+ },
600
+ };
601
+ }
@@ -112,4 +112,12 @@ export declare const selectFocused: <T extends VListItem>(state: SelectionState,
112
112
  * Pure function - returns new state
113
113
  */
114
114
  export declare const selectRange: <T extends VListItem>(state: SelectionState, items: T[], fromIndex: number, toIndex: number, mode: SelectionMode) => SelectionState;
115
+ export declare function selectOne(selected: Set<string | number>, id: string | number, mode: SelectionMode): void;
116
+ export declare function toggleOne(selected: Set<string | number>, id: string | number, mode: SelectionMode): void;
117
+ export declare function selectAllItems<T extends VListItem>(selected: Set<string | number>, items: readonly T[]): void;
118
+ export declare function selectRangeMut<T extends VListItem>(selected: Set<string | number>, items: readonly T[], fromIndex: number, toIndex: number): void;
119
+ export declare function moveFocus(state: SelectionState, delta: number, totalItems: number, reverse?: boolean): void;
120
+ export declare function getSelectedArray(selected: Set<string | number>): Array<string | number>;
121
+ export { getSelectedItems as getSelectedItemsImmutable };
122
+ export declare function getSelectedItemsMut<T extends VListItem>(selected: Set<string | number>, items: readonly T[]): T[];
115
123
  //# sourceMappingURL=state.d.ts.map