vlist 2.0.0 → 2.0.2

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 (91) hide show
  1. package/README.github.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/core/dom.d.ts +1 -1
  4. package/dist/core/index.d.ts +1 -1
  5. package/dist/core/pipeline.d.ts +2 -2
  6. package/dist/core/scroll.d.ts +1 -1
  7. package/dist/core/types.d.ts +7 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -28
  10. package/dist/internals.js +1 -60
  11. package/dist/plugins/scrollbar/controller.d.ts +3 -3
  12. package/dist/plugins/scrollbar/scrollbar.d.ts +2 -2
  13. package/dist/rendering/renderer.d.ts +2 -2
  14. package/dist/rendering/viewport.d.ts +1 -1
  15. package/dist/size.json +1 -1
  16. package/dist/types.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/dist/constants.js +0 -83
  19. package/dist/core/create.js +0 -740
  20. package/dist/core/dom.js +0 -47
  21. package/dist/core/hooks.js +0 -67
  22. package/dist/core/index.js +0 -13
  23. package/dist/core/pipeline.js +0 -307
  24. package/dist/core/pool.js +0 -42
  25. package/dist/core/scroll.js +0 -137
  26. package/dist/core/sizes.js +0 -6
  27. package/dist/core/state.js +0 -56
  28. package/dist/core/types.js +0 -7
  29. package/dist/core/velocity.js +0 -33
  30. package/dist/events/emitter.js +0 -60
  31. package/dist/events/index.js +0 -6
  32. package/dist/plugins/a11y/index.js +0 -1
  33. package/dist/plugins/a11y/plugin.js +0 -259
  34. package/dist/plugins/async/index.js +0 -12
  35. package/dist/plugins/async/manager.js +0 -568
  36. package/dist/plugins/async/placeholder.js +0 -154
  37. package/dist/plugins/async/plugin.js +0 -311
  38. package/dist/plugins/async/sparse.js +0 -540
  39. package/dist/plugins/autosize/index.js +0 -4
  40. package/dist/plugins/autosize/plugin.js +0 -185
  41. package/dist/plugins/grid/index.js +0 -5
  42. package/dist/plugins/grid/layout.js +0 -275
  43. package/dist/plugins/grid/plugin.js +0 -347
  44. package/dist/plugins/grid/renderer.js +0 -525
  45. package/dist/plugins/grid/types.js +0 -11
  46. package/dist/plugins/groups/async-bridge.js +0 -246
  47. package/dist/plugins/groups/index.js +0 -13
  48. package/dist/plugins/groups/layout.js +0 -294
  49. package/dist/plugins/groups/plugin.js +0 -571
  50. package/dist/plugins/groups/sticky.js +0 -255
  51. package/dist/plugins/groups/types.js +0 -12
  52. package/dist/plugins/masonry/index.js +0 -6
  53. package/dist/plugins/masonry/layout.js +0 -261
  54. package/dist/plugins/masonry/plugin.js +0 -381
  55. package/dist/plugins/masonry/renderer.js +0 -354
  56. package/dist/plugins/masonry/types.js +0 -9
  57. package/dist/plugins/page/index.js +0 -5
  58. package/dist/plugins/page/plugin.js +0 -166
  59. package/dist/plugins/scale/index.js +0 -4
  60. package/dist/plugins/scale/plugin.js +0 -507
  61. package/dist/plugins/scrollbar/controller.js +0 -574
  62. package/dist/plugins/scrollbar/index.js +0 -6
  63. package/dist/plugins/scrollbar/plugin.js +0 -93
  64. package/dist/plugins/scrollbar/scrollbar.js +0 -556
  65. package/dist/plugins/selection/index.js +0 -7
  66. package/dist/plugins/selection/plugin.js +0 -601
  67. package/dist/plugins/selection/state.js +0 -332
  68. package/dist/plugins/snapshots/index.js +0 -5
  69. package/dist/plugins/snapshots/plugin.js +0 -301
  70. package/dist/plugins/sortable/index.js +0 -6
  71. package/dist/plugins/sortable/plugin.js +0 -753
  72. package/dist/plugins/table/header.js +0 -501
  73. package/dist/plugins/table/index.js +0 -12
  74. package/dist/plugins/table/layout.js +0 -211
  75. package/dist/plugins/table/plugin.js +0 -391
  76. package/dist/plugins/table/renderer.js +0 -625
  77. package/dist/plugins/table/types.js +0 -12
  78. package/dist/plugins/transition/index.js +0 -5
  79. package/dist/plugins/transition/plugin.js +0 -405
  80. package/dist/rendering/aria.js +0 -23
  81. package/dist/rendering/index.js +0 -18
  82. package/dist/rendering/measured.js +0 -98
  83. package/dist/rendering/renderer.js +0 -586
  84. package/dist/rendering/scale.js +0 -267
  85. package/dist/rendering/scroll.js +0 -71
  86. package/dist/rendering/sizes.js +0 -193
  87. package/dist/rendering/sort.js +0 -65
  88. package/dist/rendering/viewport.js +0 -268
  89. package/dist/types.js +0 -5
  90. package/dist/utils/padding.js +0 -49
  91. package/dist/utils/stats.js +0 -124
@@ -1,601 +0,0 @@
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
- }