keystra 0.0.0-stage → 0.1.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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +177 -2
  4. package/dist/chunk-2BLQUBG7.js +816 -0
  5. package/dist/chunk-77GGU3AX.js +135 -0
  6. package/dist/chunk-KDOJARJD.js +29 -0
  7. package/dist/chunk-LYYUOTBP.js +125 -0
  8. package/dist/chunk-S5YVXIMA.js +61 -0
  9. package/dist/chunk-SSIKGDD4.js +255 -0
  10. package/dist/chunk-U7YK6PPT.js +1729 -0
  11. package/dist/chunk-XFNYIPOW.js +253 -0
  12. package/dist/chunk-ZOJRXXQN.js +308 -0
  13. package/dist/core.cjs +2861 -0
  14. package/dist/core.d.cts +41 -0
  15. package/dist/core.d.ts +41 -0
  16. package/dist/core.js +7 -0
  17. package/dist/engine-COPfXXTp.d.ts +229 -0
  18. package/dist/engine-DcbkGRgz.d.cts +229 -0
  19. package/dist/focus.cjs +374 -0
  20. package/dist/focus.d.cts +74 -0
  21. package/dist/focus.d.ts +74 -0
  22. package/dist/focus.js +2 -0
  23. package/dist/index-IZkcWAXP.d.cts +395 -0
  24. package/dist/index-IZkcWAXP.d.ts +395 -0
  25. package/dist/index.cjs +3745 -0
  26. package/dist/index.d.cts +9 -0
  27. package/dist/index.d.ts +9 -0
  28. package/dist/index.global.js +1 -0
  29. package/dist/index.js +9 -0
  30. package/dist/layout-C2-n8aUI.d.ts +17 -0
  31. package/dist/layout-CBzD-87D.d.cts +17 -0
  32. package/dist/navigation.cjs +293 -0
  33. package/dist/navigation.d.cts +33 -0
  34. package/dist/navigation.d.ts +33 -0
  35. package/dist/navigation.js +2 -0
  36. package/dist/patterns.cjs +3505 -0
  37. package/dist/patterns.d.cts +12 -0
  38. package/dist/patterns.d.ts +12 -0
  39. package/dist/patterns.js +7 -0
  40. package/dist/roving-focus-CT7Dm6di.d.ts +63 -0
  41. package/dist/roving-focus-ZQ0v7hLe.d.cts +63 -0
  42. package/dist/selection.cjs +266 -0
  43. package/dist/selection.d.cts +43 -0
  44. package/dist/selection.d.ts +43 -0
  45. package/dist/selection.js +2 -0
  46. package/dist/utils-CkveVeIJ.d.cts +18 -0
  47. package/dist/utils-JmPZTlys.d.ts +18 -0
  48. package/dist/utils.cjs +195 -0
  49. package/dist/utils.d.cts +3 -0
  50. package/dist/utils.d.ts +3 -0
  51. package/dist/utils.js +2 -0
  52. package/package.json +147 -4
@@ -0,0 +1,1729 @@
1
+ import { createSequentialStrategy, createDirectionalStrategy, createGridStrategy } from './chunk-XFNYIPOW.js';
2
+ import { FocusManager } from './chunk-ZOJRXXQN.js';
3
+ import { SelectionManager } from './chunk-SSIKGDD4.js';
4
+ import { LayoutCache, Emitter } from './chunk-S5YVXIMA.js';
5
+ import { queryItems, isDisabled, isHidden, getText, resolveElement, resolveEventTarget, contains, isEditableTarget } from './chunk-77GGU3AX.js';
6
+
7
+ // src/core/default-keys.ts
8
+ var NAVIGATION = {
9
+ first: ["Home"],
10
+ last: ["End"],
11
+ pageUp: ["PageUp"],
12
+ pageDown: ["PageDown"],
13
+ escape: ["Escape"],
14
+ selectAll: ["Ctrl+A", "Meta+A"]
15
+ };
16
+ var VERTICAL_KEYS = {
17
+ up: ["ArrowUp"],
18
+ down: ["ArrowDown"],
19
+ next: ["ArrowDown"],
20
+ previous: ["ArrowUp"],
21
+ toggle: ["Space"],
22
+ select: ["Enter", "Space"],
23
+ ...NAVIGATION
24
+ };
25
+ var HORIZONTAL_KEYS = {
26
+ left: ["ArrowLeft"],
27
+ right: ["ArrowRight"],
28
+ next: ["ArrowRight"],
29
+ previous: ["ArrowLeft"],
30
+ toggle: ["Space"],
31
+ select: ["Enter", "Space"],
32
+ ...NAVIGATION
33
+ };
34
+ var ROVING_KEYS = {
35
+ up: ["ArrowUp"],
36
+ down: ["ArrowDown"],
37
+ left: ["ArrowLeft"],
38
+ right: ["ArrowRight"],
39
+ next: ["ArrowDown", "ArrowRight"],
40
+ previous: ["ArrowUp", "ArrowLeft"],
41
+ toggle: ["Space"],
42
+ select: ["Enter", "Space"],
43
+ ...NAVIGATION
44
+ };
45
+ var TABS_KEYS_HORIZONTAL = {
46
+ left: ["ArrowLeft"],
47
+ right: ["ArrowRight"],
48
+ next: ["ArrowRight"],
49
+ previous: ["ArrowLeft"],
50
+ first: ["Home"],
51
+ last: ["End"],
52
+ select: ["Enter", "Space"],
53
+ escape: ["Escape"]
54
+ };
55
+ var TABS_KEYS_VERTICAL = {
56
+ up: ["ArrowUp"],
57
+ down: ["ArrowDown"],
58
+ next: ["ArrowDown"],
59
+ previous: ["ArrowUp"],
60
+ first: ["Home"],
61
+ last: ["End"],
62
+ select: ["Enter", "Space"],
63
+ escape: ["Escape"]
64
+ };
65
+ var GRID_KEYS = {
66
+ up: ["ArrowUp"],
67
+ down: ["ArrowDown"],
68
+ left: ["ArrowLeft"],
69
+ right: ["ArrowRight"],
70
+ next: ["ArrowDown"],
71
+ previous: ["ArrowUp"],
72
+ first: ["Home"],
73
+ last: ["End"],
74
+ pageUp: ["PageUp"],
75
+ pageDown: ["PageDown"],
76
+ select: ["Enter", "Space"],
77
+ escape: ["Escape"]
78
+ };
79
+ var MENU_KEYS = {
80
+ up: ["ArrowUp"],
81
+ down: ["ArrowDown"],
82
+ next: ["ArrowDown"],
83
+ previous: ["ArrowUp"],
84
+ first: ["Home"],
85
+ last: ["End"],
86
+ select: ["Enter", "Space"],
87
+ escape: ["Escape"]
88
+ };
89
+ var TREE_KEYS = {
90
+ up: ["ArrowUp"],
91
+ down: ["ArrowDown"],
92
+ next: ["ArrowDown"],
93
+ previous: ["ArrowUp"],
94
+ expand: ["ArrowRight"],
95
+ collapse: ["ArrowLeft"],
96
+ first: ["Home"],
97
+ last: ["End"],
98
+ select: ["Enter", "Space"],
99
+ escape: ["Escape"]
100
+ };
101
+ var LIST_KEYS = {
102
+ up: ["ArrowUp"],
103
+ down: ["ArrowDown"],
104
+ next: ["ArrowDown"],
105
+ previous: ["ArrowUp"],
106
+ first: ["Home"],
107
+ last: ["End"],
108
+ pageUp: ["PageUp"],
109
+ pageDown: ["PageDown"],
110
+ toggle: ["Space"],
111
+ select: ["Enter"],
112
+ selectAll: ["Ctrl+A", "Meta+A"],
113
+ escape: ["Escape"]
114
+ };
115
+ var COMMAND_PALETTE_KEYS = {
116
+ up: ["ArrowUp"],
117
+ down: ["ArrowDown"],
118
+ next: ["ArrowDown"],
119
+ previous: ["ArrowUp"],
120
+ pageUp: ["PageUp"],
121
+ pageDown: ["PageDown"],
122
+ select: ["Enter"],
123
+ escape: ["Escape"]
124
+ };
125
+ function defaultKeysFor(mode) {
126
+ switch (mode) {
127
+ case "horizontal":
128
+ return HORIZONTAL_KEYS;
129
+ case "grid":
130
+ case "directional":
131
+ return GRID_KEYS;
132
+ case "sequential":
133
+ case "vertical":
134
+ case "custom":
135
+ default:
136
+ return VERTICAL_KEYS;
137
+ }
138
+ }
139
+ function mergeKeys(defaults, user, useDefaults) {
140
+ if (!useDefaults) return { ...user ?? {} };
141
+ const merged = { ...defaults };
142
+ if (user) {
143
+ for (const action of Object.keys(user)) {
144
+ const value = user[action];
145
+ if (value === void 0) continue;
146
+ if (value === false || value === null) delete merged[action];
147
+ else merged[action] = value;
148
+ }
149
+ }
150
+ return merged;
151
+ }
152
+
153
+ // src/core/event-manager.ts
154
+ var EventManager = class {
155
+ constructor() {
156
+ this.records = [];
157
+ }
158
+ add(target, type, listener, options) {
159
+ if (!target || typeof target.addEventListener !== "function") return () => {
160
+ };
161
+ target.addEventListener(type, listener, options);
162
+ const record = { target, type, listener, options: options ?? false };
163
+ this.records.push(record);
164
+ return () => this.removeRecord(record);
165
+ }
166
+ removeRecord(record) {
167
+ const index = this.records.indexOf(record);
168
+ if (index === -1) return;
169
+ this.records.splice(index, 1);
170
+ record.target.removeEventListener(record.type, record.listener, record.options);
171
+ }
172
+ dispose() {
173
+ for (let i = this.records.length - 1; i >= 0; i--) {
174
+ const record = this.records[i];
175
+ record.target.removeEventListener(record.type, record.listener, record.options);
176
+ }
177
+ this.records.length = 0;
178
+ }
179
+ get count() {
180
+ return this.records.length;
181
+ }
182
+ };
183
+ function createRafThrottle(callback) {
184
+ let queued = false;
185
+ const raf = typeof globalThis.requestAnimationFrame === "function" ? globalThis.requestAnimationFrame.bind(globalThis) : (fn) => setTimeout(fn, 16);
186
+ const cancelRaf = typeof globalThis.cancelAnimationFrame === "function" ? globalThis.cancelAnimationFrame.bind(globalThis) : (id) => clearTimeout(id);
187
+ let handle = null;
188
+ return {
189
+ schedule() {
190
+ if (queued) return;
191
+ queued = true;
192
+ handle = raf(() => {
193
+ queued = false;
194
+ handle = null;
195
+ callback();
196
+ });
197
+ },
198
+ cancel() {
199
+ if (handle !== null) cancelRaf(handle);
200
+ handle = null;
201
+ queued = false;
202
+ }
203
+ };
204
+ }
205
+
206
+ // src/core/item-collection.ts
207
+ var groups = /* @__PURE__ */ new Map();
208
+ function claimGroup(container) {
209
+ if (container) groups.set(container, (groups.get(container) ?? 0) + 1);
210
+ }
211
+ function releaseGroup(container) {
212
+ if (!container) return;
213
+ const count = groups.get(container) ?? 0;
214
+ if (count <= 1) groups.delete(container);
215
+ else groups.set(container, count - 1);
216
+ }
217
+ function groupsInside(container) {
218
+ const inside = [];
219
+ if (!container) return inside;
220
+ for (const group of groups.keys()) {
221
+ if (group !== container && container.contains(group)) inside.push(group);
222
+ }
223
+ return inside;
224
+ }
225
+ var ItemCollection = class {
226
+ constructor(container, options, onInvalidate) {
227
+ this.container = container;
228
+ this.options = options;
229
+ this.onInvalidate = onInvalidate;
230
+ this.list = [];
231
+ this.extra = [];
232
+ this.removed = /* @__PURE__ */ new WeakSet();
233
+ this.explicit = null;
234
+ this.built = false;
235
+ this.observer = null;
236
+ this.observerKey = "";
237
+ this.layout = new LayoutCache();
238
+ this._version = 0;
239
+ claimGroup(container);
240
+ }
241
+ get version() {
242
+ return this._version;
243
+ }
244
+ get items() {
245
+ if (!this.built) this.build();
246
+ return this.list;
247
+ }
248
+ get size() {
249
+ return this.items.length;
250
+ }
251
+ setContainer(container) {
252
+ if (this.container === container) return;
253
+ releaseGroup(this.container);
254
+ claimGroup(container);
255
+ this.container = container;
256
+ this.invalidate();
257
+ }
258
+ setItems(elements) {
259
+ this.explicit = elements ? elements.slice() : null;
260
+ this.invalidate();
261
+ }
262
+ add(element) {
263
+ const additions = Array.isArray(element) ? element : [element];
264
+ for (const item of additions) {
265
+ if (!item || this.extra.includes(item)) continue;
266
+ this.removed.delete(item);
267
+ this.extra.push(item);
268
+ }
269
+ this.invalidate();
270
+ }
271
+ remove(element) {
272
+ const removals = Array.isArray(element) ? element : [element];
273
+ for (const item of removals) {
274
+ const index = this.extra.indexOf(item);
275
+ if (index !== -1) this.extra.splice(index, 1);
276
+ this.removed.add(item);
277
+ }
278
+ this.invalidate();
279
+ }
280
+ invalidate() {
281
+ this.built = false;
282
+ this.onInvalidate?.();
283
+ }
284
+ refresh() {
285
+ this.build();
286
+ return this.list;
287
+ }
288
+ revalidate() {
289
+ if (this.observer) return;
290
+ const before = this.list;
291
+ this.build();
292
+ if (this.list !== before) this.onInvalidate?.();
293
+ }
294
+ indexOf(element) {
295
+ if (!element) return -1;
296
+ return this.items.indexOf(element);
297
+ }
298
+ at(index) {
299
+ return this.items[index] ?? null;
300
+ }
301
+ startObserving() {
302
+ if (!this.options.observe) {
303
+ this.stopObserving();
304
+ return;
305
+ }
306
+ const key = `${this.container ? "c" : "n"}:${JSON.stringify(this.options.observe)}`;
307
+ if (this.observer && this.observerKey === key) return;
308
+ this.stopObserving();
309
+ if (!this.container || typeof MutationObserver !== "function") return;
310
+ const init = typeof this.options.observe === "object" ? this.options.observe : { childList: true, subtree: true, attributes: true, attributeFilter: ["disabled", "aria-disabled", "hidden", "style", "class"] };
311
+ this.observer = new MutationObserver(() => this.invalidate());
312
+ this.observer.observe(this.container, init);
313
+ this.observerKey = key;
314
+ }
315
+ stopObserving() {
316
+ if (this.observer) {
317
+ this.observer.disconnect();
318
+ this.observer = null;
319
+ this.observerKey = "";
320
+ }
321
+ }
322
+ dispose() {
323
+ this.stopObserving();
324
+ releaseGroup(this.container);
325
+ this.list = [];
326
+ this.extra = [];
327
+ this.removed = /* @__PURE__ */ new WeakSet();
328
+ this.explicit = null;
329
+ this.built = false;
330
+ this.container = null;
331
+ this.onInvalidate = void 0;
332
+ }
333
+ build() {
334
+ const { selector, disabledSelector, skipHidden, filter } = this.options;
335
+ let source;
336
+ let nested = [];
337
+ if (this.explicit) {
338
+ source = this.explicit.slice();
339
+ } else {
340
+ source = queryItems(this.container, selector);
341
+ nested = groupsInside(this.container);
342
+ if (this.extra.length > 0) {
343
+ const merged = source.slice();
344
+ for (const item of this.extra) if (!merged.includes(item)) merged.push(item);
345
+ source = sortInDomOrder(merged);
346
+ }
347
+ }
348
+ const result = [];
349
+ for (let i = 0; i < source.length; i++) {
350
+ const element = source[i];
351
+ if (!element || this.removed.has(element)) continue;
352
+ if (nested.length > 0 && nested.some((group) => group !== element && group.contains(element))) continue;
353
+ if (disabledSelector && isDisabled(element, disabledSelector)) continue;
354
+ if (skipHidden && isHidden(element)) continue;
355
+ if (filter && !filter(element, result.length)) continue;
356
+ result.push(element);
357
+ }
358
+ if (sameList(this.list, result)) {
359
+ this.built = true;
360
+ this.startObserving();
361
+ return;
362
+ }
363
+ this._version++;
364
+ this.list = result;
365
+ this.built = true;
366
+ this.startObserving();
367
+ }
368
+ };
369
+ function sameList(a, b) {
370
+ if (a.length !== b.length) return false;
371
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
372
+ return true;
373
+ }
374
+ function sortInDomOrder(elements) {
375
+ if (elements.length < 2) return elements;
376
+ if (typeof document === "undefined" || elements[0]?.compareDocumentPosition == null) return elements;
377
+ return elements.slice().sort((a, b) => {
378
+ if (a === b) return 0;
379
+ const position = a.compareDocumentPosition(b);
380
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
381
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
382
+ return 0;
383
+ });
384
+ }
385
+ function createRectOf(collection) {
386
+ return (element) => collection.layout.rect(element, collection.version);
387
+ }
388
+
389
+ // src/core/keyboard.ts
390
+ var KEY_ALIASES = {
391
+ spacebar: "Space",
392
+ " ": "Space",
393
+ esc: "Escape",
394
+ del: "Delete",
395
+ left: "ArrowLeft",
396
+ right: "ArrowRight",
397
+ up: "ArrowUp",
398
+ down: "ArrowDown",
399
+ multiply: "*",
400
+ add: "+",
401
+ subtract: "-",
402
+ divide: "/",
403
+ decimal: ".",
404
+ windows: "Meta",
405
+ cmd: "Meta",
406
+ command: "Meta",
407
+ option: "Alt",
408
+ control: "Control",
409
+ return: "Enter",
410
+ cr: "Enter",
411
+ pageup: "PageUp",
412
+ pagedown: "PageDown",
413
+ "page up": "PageUp",
414
+ "page down": "PageDown"
415
+ };
416
+ var CANONICAL = /* @__PURE__ */ new Set([
417
+ "arrowup",
418
+ "arrowdown",
419
+ "arrowleft",
420
+ "arrowright",
421
+ "enter",
422
+ "escape",
423
+ "tab",
424
+ "space",
425
+ "home",
426
+ "end",
427
+ "pageup",
428
+ "pagedown",
429
+ "backspace",
430
+ "delete",
431
+ "insert",
432
+ "shift",
433
+ "control",
434
+ "alt",
435
+ "meta",
436
+ "capslock",
437
+ "contextmenu",
438
+ "f1",
439
+ "f2",
440
+ "f3",
441
+ "f4",
442
+ "f5",
443
+ "f6",
444
+ "f7",
445
+ "f8",
446
+ "f9",
447
+ "f10",
448
+ "f11",
449
+ "f12",
450
+ "f13",
451
+ "f14",
452
+ "f15",
453
+ "f16",
454
+ "f17",
455
+ "f18",
456
+ "f19",
457
+ "f20",
458
+ "f21",
459
+ "f22",
460
+ "f23",
461
+ "f24",
462
+ "printscreen",
463
+ "scrolllock",
464
+ "pause",
465
+ "unidentified"
466
+ ]);
467
+ function normalizeKeyName(key) {
468
+ if (!key) return "";
469
+ const lower = key.toLowerCase();
470
+ const alias = KEY_ALIASES[lower];
471
+ if (alias !== void 0) return alias;
472
+ if (key.length === 1) return key;
473
+ if (CANONICAL.has(lower)) return canonicalCase(lower);
474
+ return key;
475
+ }
476
+ function canonicalCase(lower) {
477
+ switch (lower) {
478
+ case "arrowup":
479
+ return "ArrowUp";
480
+ case "arrowdown":
481
+ return "ArrowDown";
482
+ case "arrowleft":
483
+ return "ArrowLeft";
484
+ case "arrowright":
485
+ return "ArrowRight";
486
+ case "pageup":
487
+ return "PageUp";
488
+ case "pagedown":
489
+ return "PageDown";
490
+ case "capslock":
491
+ return "CapsLock";
492
+ case "contextmenu":
493
+ return "ContextMenu";
494
+ case "printscreen":
495
+ return "PrintScreen";
496
+ case "scrolllock":
497
+ return "ScrollLock";
498
+ case "unidentified":
499
+ return "Unidentified";
500
+ default:
501
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
502
+ }
503
+ }
504
+ var NON_PRINTABLE = /* @__PURE__ */ new Set([
505
+ "Shift",
506
+ "Control",
507
+ "Alt",
508
+ "Meta",
509
+ "CapsLock",
510
+ "Tab",
511
+ "Enter",
512
+ "Escape",
513
+ "Backspace",
514
+ "Delete",
515
+ "Insert",
516
+ "Home",
517
+ "End",
518
+ "PageUp",
519
+ "PageDown",
520
+ "ArrowUp",
521
+ "ArrowDown",
522
+ "ArrowLeft",
523
+ "ArrowRight",
524
+ "ContextMenu",
525
+ "PrintScreen",
526
+ "ScrollLock",
527
+ "Pause",
528
+ "Unidentified"
529
+ ]);
530
+ function isPrintableKey(key) {
531
+ if (!key) return false;
532
+ if (key.length === 1) return true;
533
+ if (NON_PRINTABLE.has(key)) return false;
534
+ if (/^F\d{1,2}$/.test(key)) return false;
535
+ if (/^(Numpad|NumLock)/.test(key)) return false;
536
+ return false;
537
+ }
538
+ function isModifierKey(key) {
539
+ return key === "Shift" || key === "Control" || key === "Alt" || key === "Meta";
540
+ }
541
+ function normalizeKeyEvent(event) {
542
+ let key = normalizeKeyName(event.key ?? "");
543
+ if (key === "Unidentified" || key === "") {
544
+ const fromCode = codeToKey(event.code);
545
+ if (fromCode) key = fromCode;
546
+ }
547
+ const ctrl = event.ctrlKey === true;
548
+ const alt = event.altKey === true;
549
+ const shift = event.shiftKey === true;
550
+ const meta = event.metaKey === true;
551
+ return {
552
+ key,
553
+ code: event.code ?? "",
554
+ ctrl,
555
+ alt,
556
+ shift,
557
+ meta,
558
+ isPlain: !ctrl && !alt && !meta,
559
+ isPrintable: isPrintableKey(key) && !ctrl && !alt && !meta,
560
+ repeat: event.repeat === true,
561
+ shiftKey: shift,
562
+ originalEvent: event
563
+ };
564
+ }
565
+ function codeToKey(code) {
566
+ if (!code) return "";
567
+ if (code.startsWith("Key")) return code.slice(3);
568
+ if (code.startsWith("Digit")) return code.slice(5);
569
+ if (code.startsWith("Arrow")) return code;
570
+ if (code === "NumpadEnter") return "Enter";
571
+ if (code.startsWith("Numpad")) {
572
+ const rest = code.slice(6);
573
+ if (/^\d$/.test(rest)) return rest;
574
+ if (rest === "Add") return "+";
575
+ if (rest === "Subtract") return "-";
576
+ if (rest === "Multiply") return "*";
577
+ if (rest === "Divide") return "/";
578
+ if (rest === "Decimal") return ".";
579
+ return `Numpad${rest}`;
580
+ }
581
+ switch (code) {
582
+ case "Space":
583
+ return "Space";
584
+ case "Enter":
585
+ case "NumpadEnter":
586
+ return "Enter";
587
+ case "Escape":
588
+ return "Escape";
589
+ case "Tab":
590
+ return "Tab";
591
+ case "Backspace":
592
+ return "Backspace";
593
+ case "Delete":
594
+ return "Delete";
595
+ case "Minus":
596
+ return "-";
597
+ case "Equal":
598
+ return "=";
599
+ case "Comma":
600
+ return ",";
601
+ case "Period":
602
+ return ".";
603
+ case "Slash":
604
+ return "/";
605
+ case "Backslash":
606
+ return "\\";
607
+ case "Semicolon":
608
+ return ";";
609
+ case "Quote":
610
+ return "'";
611
+ case "BracketLeft":
612
+ return "[";
613
+ case "BracketRight":
614
+ return "]";
615
+ case "Backquote":
616
+ return "`";
617
+ default:
618
+ return "";
619
+ }
620
+ }
621
+ function formatKeyCombo(key, modifiers = {}) {
622
+ const parts = [];
623
+ if (modifiers.ctrl) parts.push("Ctrl");
624
+ if (modifiers.alt) parts.push("Alt");
625
+ if (modifiers.shift) parts.push("Shift");
626
+ if (modifiers.meta) parts.push("Meta");
627
+ if (key) parts.push(key);
628
+ return parts.join("+");
629
+ }
630
+ function describeEvent(event) {
631
+ const normalized = normalizeKeyEvent(event);
632
+ return formatKeyCombo(normalized.key, normalized);
633
+ }
634
+
635
+ // src/core/key-matcher.ts
636
+ var MODIFIER_ALIASES = {
637
+ ctrl: "ctrl",
638
+ ctl: "ctrl",
639
+ control: "ctrl",
640
+ alt: "alt",
641
+ option: "alt",
642
+ opt: "alt",
643
+ shift: "shift",
644
+ meta: "meta",
645
+ cmd: "meta",
646
+ command: "meta",
647
+ super: "meta",
648
+ win: "meta",
649
+ windows: "meta"
650
+ };
651
+ function tokenize(chord) {
652
+ const tokens = [];
653
+ let current = "";
654
+ let quoted = false;
655
+ for (let i = 0; i < chord.length; i++) {
656
+ const char = chord[i];
657
+ if (char === '"' || char === "'") {
658
+ quoted = !quoted;
659
+ continue;
660
+ }
661
+ if (char === "+" && !quoted) {
662
+ if (i === chord.length - 1) {
663
+ tokens.push("+");
664
+ current = "";
665
+ continue;
666
+ }
667
+ if (current !== "") tokens.push(current);
668
+ current = "";
669
+ continue;
670
+ }
671
+ current += char;
672
+ }
673
+ if (current !== "") tokens.push(current);
674
+ return tokens;
675
+ }
676
+ function parseKeyBinding(binding) {
677
+ if (binding == null) return null;
678
+ let key = null;
679
+ let code = null;
680
+ let ctrl = false;
681
+ let alt = false;
682
+ let shift = false;
683
+ let meta = false;
684
+ let allowWithoutModifiers = false;
685
+ if (typeof binding === "object") {
686
+ if (binding.key) key = normalizeKeyName(String(binding.key));
687
+ if (binding.code) code = String(binding.code);
688
+ ctrl = binding.ctrl === true;
689
+ alt = binding.alt === true;
690
+ shift = binding.shift === true;
691
+ meta = binding.meta === true;
692
+ allowWithoutModifiers = binding.allowWithoutModifiers === true;
693
+ if (!key && !code) return null;
694
+ } else {
695
+ const raw = String(binding);
696
+ const trimmed = raw.trim();
697
+ if (trimmed === "" && raw !== " ") return null;
698
+ const chord = raw === " " ? " " : trimmed;
699
+ if (!chord) return null;
700
+ const tokens = tokenize(chord);
701
+ if (tokens.length === 0) return null;
702
+ for (let i = 0; i < tokens.length; i++) {
703
+ const token = tokens[i];
704
+ const modifier = MODIFIER_ALIASES[token.toLowerCase()];
705
+ if (modifier && i < tokens.length - 1) {
706
+ if (modifier === "ctrl") ctrl = true;
707
+ else if (modifier === "alt") alt = true;
708
+ else if (modifier === "shift") shift = true;
709
+ else meta = true;
710
+ continue;
711
+ }
712
+ key = normalizeKeyName(token);
713
+ }
714
+ if (!key) return null;
715
+ if (shift && key.length === 1) key = key.toLowerCase();
716
+ }
717
+ return {
718
+ key,
719
+ code,
720
+ ctrl,
721
+ alt,
722
+ shift,
723
+ meta,
724
+ allowWithoutModifiers,
725
+ id: bindingId(key, code, ctrl, alt, shift, meta)
726
+ };
727
+ }
728
+ function bindingId(key, code, ctrl, alt, shift, meta) {
729
+ const flags = (ctrl ? "c" : "") + (alt ? "a" : "") + (shift ? "s" : "") + (meta ? "m" : "");
730
+ const main = code ? `#${code.toLowerCase()}` : (key ?? "").toLowerCase();
731
+ return flags ? `${flags}:${main}` : main;
732
+ }
733
+ function toArray(input) {
734
+ if (input == null || input === false) return [];
735
+ return Array.isArray(input) ? input : [input];
736
+ }
737
+ var KeyMatcher = class {
738
+ constructor(map = {}, sequences = []) {
739
+ this.byId = /* @__PURE__ */ new Map();
740
+ this.loose = [];
741
+ this.actions = /* @__PURE__ */ new Set();
742
+ this.sequences = [];
743
+ this.sequenceIndex = 0;
744
+ this.sequenceOwner = null;
745
+ this.sequenceTimer = null;
746
+ this.sequenceCandidates = [];
747
+ this.loadMap(map);
748
+ this.loadSequences(sequences);
749
+ }
750
+ loadMap(map) {
751
+ this.byId.clear();
752
+ this.loose.length = 0;
753
+ this.actions.clear();
754
+ for (const action of Object.keys(map)) {
755
+ for (const raw of toArray(map[action])) {
756
+ const binding = parseKeyBinding(raw);
757
+ if (!binding) continue;
758
+ this.actions.add(action);
759
+ let entry = this.byId.get(binding.id);
760
+ if (!entry || entry.binding.id !== binding.id) {
761
+ entry = { actions: [], binding };
762
+ this.byId.set(binding.id, entry);
763
+ if (binding.allowWithoutModifiers || binding.code) this.loose.push(entry);
764
+ }
765
+ if (entry.actions.indexOf(action) === -1) entry.actions.push(action);
766
+ }
767
+ }
768
+ }
769
+ loadSequences(sequences) {
770
+ this.resetSequence();
771
+ this.sequences.length = 0;
772
+ for (const options of sequences) {
773
+ const normalized = normalizeSequence(options);
774
+ if (!normalized) continue;
775
+ const steps = [];
776
+ let valid = true;
777
+ for (const raw of normalized.binding) {
778
+ const parsed = parseKeyBinding(raw);
779
+ if (!parsed) {
780
+ valid = false;
781
+ break;
782
+ }
783
+ steps.push(parsed);
784
+ }
785
+ if (!valid) continue;
786
+ this.sequences.push({
787
+ steps,
788
+ action: normalized.action,
789
+ timeout: normalized.timeout ?? 1e3,
790
+ preventDefault: normalized.preventDefault !== false
791
+ });
792
+ }
793
+ }
794
+ has(action) {
795
+ return this.actions.has(action);
796
+ }
797
+ get boundActions() {
798
+ return Array.from(this.actions);
799
+ }
800
+ get size() {
801
+ return this.byId.size + this.sequences.length;
802
+ }
803
+ match(event, owner) {
804
+ if (this.sequences.length > 0) {
805
+ const sequence = this.matchSequence(event, owner);
806
+ if (sequence) return sequence;
807
+ }
808
+ const ids = candidateIds(event);
809
+ for (let i = 0; i < ids.length; i++) {
810
+ const entry = this.byId.get(ids[i]);
811
+ if (entry && matchesBinding(entry.binding, event)) {
812
+ return { actions: entry.actions, binding: entry.binding, fromSequence: false, complete: true, preventDefault: true };
813
+ }
814
+ }
815
+ for (let i = 0; i < this.loose.length; i++) {
816
+ const entry = this.loose[i];
817
+ if (matchesBinding(entry.binding, event)) {
818
+ return { actions: entry.actions, binding: entry.binding, fromSequence: false, complete: true, preventDefault: true };
819
+ }
820
+ }
821
+ return null;
822
+ }
823
+ resetSequence() {
824
+ this.sequenceIndex = 0;
825
+ this.sequenceCandidates = [];
826
+ this.clearSequenceTimer();
827
+ }
828
+ dispose() {
829
+ this.resetSequence();
830
+ this.byId.clear();
831
+ this.loose.length = 0;
832
+ this.actions.clear();
833
+ this.sequences.length = 0;
834
+ }
835
+ matchSequence(event, owner) {
836
+ if (owner && this.sequenceOwner && this.sequenceOwner !== owner) this.resetSequence();
837
+ if (owner) this.sequenceOwner = owner;
838
+ const pending = this.sequenceCandidates.length > 0;
839
+ const step = this.sequenceIndex;
840
+ const pool = pending ? this.sequenceCandidates : this.sequences;
841
+ const fitting = pool.filter((sequence) => {
842
+ const binding = sequence.steps[step];
843
+ return binding !== void 0 && matchesBinding(binding, event);
844
+ });
845
+ if (fitting.length === 0) {
846
+ if (!pending) return null;
847
+ this.resetSequence();
848
+ return this.matchSequence(event, owner);
849
+ }
850
+ const done = fitting.find((sequence) => sequence.steps.length === step + 1);
851
+ const lead = done ?? fitting[0];
852
+ const result = {
853
+ actions: [lead.action],
854
+ binding: lead.steps[step],
855
+ fromSequence: true,
856
+ complete: done !== void 0,
857
+ preventDefault: lead.preventDefault
858
+ };
859
+ if (done) {
860
+ this.resetSequence();
861
+ } else {
862
+ this.sequenceCandidates = fitting;
863
+ this.sequenceIndex = step + 1;
864
+ this.armSequenceTimer(lead.timeout);
865
+ }
866
+ return result;
867
+ }
868
+ armSequenceTimer(timeout) {
869
+ this.clearSequenceTimer();
870
+ if (typeof setTimeout !== "function") return;
871
+ this.sequenceTimer = setTimeout(() => this.resetSequence(), timeout);
872
+ }
873
+ clearSequenceTimer() {
874
+ if (this.sequenceTimer !== null) {
875
+ clearTimeout(this.sequenceTimer);
876
+ this.sequenceTimer = null;
877
+ }
878
+ }
879
+ };
880
+ var SHIFT_DISTINCT_KEYS = /* @__PURE__ */ new Set(["tab"]);
881
+ function shiftDistinct(key) {
882
+ return SHIFT_DISTINCT_KEYS.has(key.toLowerCase());
883
+ }
884
+ function makeId(key, ctrl, alt, shift, meta) {
885
+ const flags = (ctrl ? "c" : "") + (alt ? "a" : "") + (shift ? "s" : "") + (meta ? "m" : "");
886
+ return flags ? `${flags}:${key}` : key;
887
+ }
888
+ function candidateIds(event) {
889
+ const { ctrl, alt, shift, meta } = event;
890
+ const key = event.key;
891
+ const lower = key.toLowerCase();
892
+ const ids = [makeId(lower, ctrl, alt, shift, meta)];
893
+ const singleChar = key.length === 1;
894
+ const noShift = shift && (singleChar || !shiftDistinct(lower)) ? makeId(lower, ctrl, alt, false, meta) : null;
895
+ if (noShift) ids.push(noShift);
896
+ if (singleChar && lower !== key) {
897
+ ids.push(makeId(key, ctrl, alt, shift, meta));
898
+ if (noShift) ids.push(makeId(key, ctrl, alt, false, meta));
899
+ }
900
+ return ids;
901
+ }
902
+ function matchesBinding(binding, event) {
903
+ if (binding.code) {
904
+ if (binding.code.toLowerCase() !== event.code.toLowerCase()) return false;
905
+ } else if (binding.key) {
906
+ if (!keyEquals(binding.key, event.key)) return false;
907
+ } else {
908
+ return false;
909
+ }
910
+ if (binding.allowWithoutModifiers && !event.ctrl && !event.alt && !event.meta) return true;
911
+ if (binding.ctrl !== event.ctrl) return false;
912
+ if (binding.alt !== event.alt) return false;
913
+ if (binding.meta !== event.meta) return false;
914
+ if (binding.shift !== event.shift) {
915
+ const named = binding.key ?? "";
916
+ const singleChar = named.length === 1;
917
+ if (event.shift && !binding.shift) {
918
+ if (shiftDistinct(named)) return false;
919
+ if (/^[a-z]$/i.test(named) && (event.ctrl || event.alt || event.meta)) return false;
920
+ } else {
921
+ if (!singleChar) return false;
922
+ }
923
+ }
924
+ return true;
925
+ }
926
+ function keyEquals(bindingKey, eventKey) {
927
+ if (bindingKey === eventKey) return true;
928
+ return bindingKey.toLowerCase() === eventKey.toLowerCase();
929
+ }
930
+ function normalizeSequence(input) {
931
+ if (!input || !input.binding || input.binding.length < 2 || !input.action) return null;
932
+ return input;
933
+ }
934
+ function createKeyMatcher(map = {}, sequences = []) {
935
+ return new KeyMatcher(map, sequences);
936
+ }
937
+
938
+ // src/core/typeahead.ts
939
+ function createTypeahead(options = {}) {
940
+ const resetDelay = options.resetDelay ?? 600;
941
+ const getTextFor = options.getText ?? ((element) => getText(element));
942
+ let buffer = "";
943
+ let timer = null;
944
+ let lastMatch = -1;
945
+ function clearTimer() {
946
+ if (timer !== null) {
947
+ clearTimeout(timer);
948
+ timer = null;
949
+ }
950
+ }
951
+ function reset() {
952
+ buffer = "";
953
+ lastMatch = -1;
954
+ clearTimer();
955
+ }
956
+ return {
957
+ get query() {
958
+ return buffer;
959
+ },
960
+ reset,
961
+ dispose: reset,
962
+ handle(character, items, currentIndex) {
963
+ if (!character) return null;
964
+ const total = items.length;
965
+ if (total === 0) return null;
966
+ const previousBuffer = buffer;
967
+ buffer += character.toLowerCase();
968
+ if (buffer.length > 12) buffer = buffer.slice(-12);
969
+ clearTimer();
970
+ if (typeof setTimeout === "function") timer = setTimeout(reset, resetDelay);
971
+ const repeating = buffer.length > 1 && previousBuffer.length === 1 && buffer === previousBuffer + previousBuffer;
972
+ let query = repeating ? previousBuffer : buffer;
973
+ const collect = (prefix) => {
974
+ const found = [];
975
+ for (let i = 0; i < total; i++) {
976
+ const text = getTextFor(items[i]);
977
+ if (text && text.toLowerCase().startsWith(prefix)) found.push(i);
978
+ }
979
+ return found;
980
+ };
981
+ let matches = collect(query);
982
+ if (matches.length === 0 && query.length > 1) {
983
+ query = character.toLowerCase();
984
+ buffer = query;
985
+ matches = collect(query);
986
+ }
987
+ if (matches.length === 0) {
988
+ options.onMatch?.(null, query);
989
+ return null;
990
+ }
991
+ let pick;
992
+ if (repeating) {
993
+ const position = matches.indexOf(lastMatch);
994
+ pick = matches[(position + 1) % matches.length];
995
+ } else if (query === previousBuffer && previousBuffer.length > 0) {
996
+ const position = matches.indexOf(lastMatch);
997
+ pick = matches[(position + 1) % matches.length];
998
+ } else {
999
+ pick = matches[0];
1000
+ for (let i = 0; i < matches.length; i++) {
1001
+ if (matches[i] > currentIndex) {
1002
+ pick = matches[i];
1003
+ break;
1004
+ }
1005
+ }
1006
+ }
1007
+ lastMatch = pick;
1008
+ const element = items[pick] ?? null;
1009
+ options.onMatch?.(element, query);
1010
+ return element;
1011
+ }
1012
+ };
1013
+ }
1014
+
1015
+ // src/core/engine.ts
1016
+ var DEFAULT_ITEM_SELECTOR = "[data-keystra-item]";
1017
+ var DEFAULT_DISABLED_SELECTOR = "[disabled],[aria-disabled='true']";
1018
+ var DEFAULT_PAGE_SIZE = 10;
1019
+ var MOVEMENT_ACTIONS = /* @__PURE__ */ new Set([
1020
+ "next",
1021
+ "previous",
1022
+ "up",
1023
+ "down",
1024
+ "left",
1025
+ "right",
1026
+ "first",
1027
+ "last",
1028
+ "pageUp",
1029
+ "pageDown"
1030
+ ]);
1031
+ function resolveOptions(options = {}, overrides = {}) {
1032
+ const mode = overrides.mode ?? options.mode ?? "vertical";
1033
+ const container = overrides.container ?? resolveElement(options.container) ?? null;
1034
+ const keys = overrides.keys ?? mergeKeys(
1035
+ overrides.defaultKeys ?? defaultKeysFor(mode),
1036
+ options.keys,
1037
+ options.useDefaultKeys !== false
1038
+ );
1039
+ const userStrategy = options.strategy ?? null;
1040
+ const listenTarget = overrides.listenTarget ?? resolveEventTarget(options.target) ?? container ?? (typeof document !== "undefined" ? document : null);
1041
+ const resolved = {
1042
+ container,
1043
+ pinnedTarget: options.target !== void 0 && options.target !== null,
1044
+ listenTarget,
1045
+ selector: options.items ?? options.itemSelector ?? DEFAULT_ITEM_SELECTOR,
1046
+ disabledSelector: options.disabledSelector === false || options.disabledSelector === null ? null : options.disabledSelector ?? DEFAULT_DISABLED_SELECTOR,
1047
+ skipHidden: options.skipHidden !== false,
1048
+ filter: options.filter ?? null,
1049
+ mode,
1050
+ loop: overrides.loop ?? options.loop === true,
1051
+ columns: overrides.columns ?? options.columns ?? 0,
1052
+ pageSize: options.pageSize ?? DEFAULT_PAGE_SIZE,
1053
+ strategy: userStrategy ?? overrides.strategy ?? strategyForMode(mode),
1054
+ userStrategy,
1055
+ onBeforeNavigate: options.onBeforeNavigate ?? null,
1056
+ keys,
1057
+ defaultKeys: overrides.defaultKeys ?? null,
1058
+ userKeys: options.keys ?? null,
1059
+ useDefaultKeys: options.useDefaultKeys !== false,
1060
+ sequences: options.sequences ?? [],
1061
+ typeahead: normalizeTypeahead(options.typeahead),
1062
+ roving: normalizeRoving(options.roving),
1063
+ activeDescendant: options.activeDescendant === true || options.activeDescendant !== void 0 && options.activeDescendant !== false && options.activeDescendant !== null,
1064
+ descendantOwner: options.activeDescendant === void 0 || options.activeDescendant === false || options.activeDescendant === null ? null : options.activeDescendant === true ? () => resolveActiveDescendantOwner(container, listenTarget) : () => resolveElement(options.activeDescendant, container?.ownerDocument ?? null),
1065
+ autoFocus: normalizeAutoFocus(options.autoFocus),
1066
+ selectionOptions: normalizeSelection(options.selection),
1067
+ observe: options.observe ?? false,
1068
+ onlyWhenFocusedInside: options.onlyWhenFocusedInside !== false,
1069
+ when: options.when ?? null,
1070
+ ignoreEditable: options.ignoreEditable !== false,
1071
+ editableSelector: options.editableSelector ?? null,
1072
+ stopPropagation: options.stopPropagation !== false,
1073
+ preventDefault: options.preventDefault !== false,
1074
+ capture: options.capture === true,
1075
+ pointerSync: options.pointerSync === true,
1076
+ handlers: {
1077
+ onFocus: options.onFocus,
1078
+ onBlur: options.onBlur,
1079
+ onSelect: options.onSelect,
1080
+ onAction: options.onAction,
1081
+ onEscape: options.onEscape,
1082
+ onRefresh: options.onRefresh
1083
+ }
1084
+ };
1085
+ return resolved;
1086
+ }
1087
+ function strategyForMode(mode) {
1088
+ switch (mode) {
1089
+ case "horizontal":
1090
+ return createSequentialStrategy("horizontal");
1091
+ case "grid":
1092
+ return createGridStrategy();
1093
+ case "directional":
1094
+ return createDirectionalStrategy();
1095
+ case "custom":
1096
+ return () => null;
1097
+ case "sequential":
1098
+ case "vertical":
1099
+ default:
1100
+ return createSequentialStrategy("vertical");
1101
+ }
1102
+ }
1103
+ function normalizeTypeahead(input) {
1104
+ if (!input) return null;
1105
+ if (input === true) return { enabled: true };
1106
+ return input.enabled === false ? null : { ...input, enabled: true };
1107
+ }
1108
+ function resolveActiveDescendantOwner(container, listenTarget) {
1109
+ if (listenTarget && typeof listenTarget.nodeType === "number") {
1110
+ const element = listenTarget;
1111
+ if (!container || contains(container, element) || element === container) return element;
1112
+ }
1113
+ if (typeof document === "undefined") return null;
1114
+ const active = document.activeElement;
1115
+ if (active && container && contains(container, active)) return active;
1116
+ return null;
1117
+ }
1118
+ function normalizeRoving(input) {
1119
+ if (input === false) return { enabled: false };
1120
+ if (input === true || input === void 0) return { enabled: true };
1121
+ return input;
1122
+ }
1123
+ function normalizeAutoFocus(input) {
1124
+ if (!input) return { enabled: false };
1125
+ if (input === true) return { enabled: true };
1126
+ return input;
1127
+ }
1128
+ function normalizeSelection(input) {
1129
+ if (!input) return null;
1130
+ if (input === true) return { enabled: true, mode: "single" };
1131
+ return input.enabled === false ? null : { enabled: true, ...input };
1132
+ }
1133
+ var NavigationController = class {
1134
+ constructor(options = {}, overrides = {}) {
1135
+ this.events = new EventManager();
1136
+ this.emitter = new Emitter();
1137
+ this.keydownDisposer = null;
1138
+ this.auxDisposers = [];
1139
+ this.syncPending = true;
1140
+ this.destroyed = false;
1141
+ this.resolved = resolveOptions(options, overrides);
1142
+ this.collection = new ItemCollection(
1143
+ this.resolved.container,
1144
+ {
1145
+ selector: this.resolved.selector,
1146
+ disabledSelector: this.resolved.disabledSelector,
1147
+ skipHidden: this.resolved.skipHidden,
1148
+ filter: this.resolved.filter,
1149
+ observe: this.resolved.observe
1150
+ },
1151
+ () => {
1152
+ this.syncPending = true;
1153
+ }
1154
+ );
1155
+ this.focus = new FocusManager({
1156
+ roving: this.resolved.roving,
1157
+ virtual: this.resolved.activeDescendant,
1158
+ descendantOwner: this.resolved.descendantOwner,
1159
+ disabledSelector: this.resolved.disabledSelector,
1160
+ skipHidden: this.resolved.skipHidden,
1161
+ onFocus: (element, index, action) => {
1162
+ const selection = this.resolved.selectionOptions;
1163
+ if (selection?.followFocus && selection.mode !== "multiple") {
1164
+ this.selectionManager?.select(index);
1165
+ }
1166
+ this.resolved.handlers.onFocus?.(element, index);
1167
+ this.emitter.emit("focus", { element, index, action });
1168
+ },
1169
+ onBlur: (element, index) => {
1170
+ this.resolved.handlers.onBlur?.(element, index);
1171
+ this.emitter.emit("blur", { element, index });
1172
+ }
1173
+ });
1174
+ if (options.elements) this.collection.setItems(options.elements);
1175
+ this.matcher = new KeyMatcher(this.resolved.keys, this.resolved.sequences ?? []);
1176
+ this.typeaheadController = this.resolved.typeahead ? createTypeahead(this.resolved.typeahead) : null;
1177
+ this.selectionManager = this.resolved.selectionOptions ? new SelectionManager(this.resolved.selectionOptions) : null;
1178
+ this.selectionManager?.setDiffHandler((selected, added, removed) => {
1179
+ this.emitter.emit("selectionchange", { selected, added, removed });
1180
+ });
1181
+ this.selectionManager?.bindItems(() => this.collection.items);
1182
+ if (options.enabled !== false) this.attach();
1183
+ if (this.resolved.autoFocus.enabled) this.applyAutoFocus();
1184
+ }
1185
+ get container() {
1186
+ return this.resolved.container;
1187
+ }
1188
+ get items() {
1189
+ this.sync();
1190
+ return this.collection.items;
1191
+ }
1192
+ get activeElement() {
1193
+ this.sync();
1194
+ return this.focus.element;
1195
+ }
1196
+ get activeIndex() {
1197
+ this.sync();
1198
+ return this.focus.focusedIndex;
1199
+ }
1200
+ get currentIndex() {
1201
+ this.sync();
1202
+ return this.focus.index;
1203
+ }
1204
+ get mode() {
1205
+ return this.resolved.mode;
1206
+ }
1207
+ get loop() {
1208
+ return this.resolved.loop;
1209
+ }
1210
+ get enabled() {
1211
+ return this.keydownDisposer !== null;
1212
+ }
1213
+ get selection() {
1214
+ this.sync();
1215
+ return this.selectionManager;
1216
+ }
1217
+ next(count = 1) {
1218
+ return this.step(count, "next");
1219
+ }
1220
+ previous(count = 1) {
1221
+ return this.step(-count, "previous");
1222
+ }
1223
+ up() {
1224
+ return this.move("up");
1225
+ }
1226
+ down() {
1227
+ return this.move("down");
1228
+ }
1229
+ left() {
1230
+ return this.move("left");
1231
+ }
1232
+ right() {
1233
+ return this.move("right");
1234
+ }
1235
+ move(direction) {
1236
+ this.collection.revalidate();
1237
+ this.sync();
1238
+ const index = this.resolveNavigation(direction, direction, null);
1239
+ if (index === null) return null;
1240
+ const focused = this.focus.focusIndex(index, direction);
1241
+ if (focused) this.afterMove(index, direction, null);
1242
+ return focused;
1243
+ }
1244
+ first() {
1245
+ this.collection.revalidate();
1246
+ this.sync();
1247
+ if (this.collection.items.length === 0) return null;
1248
+ const focused = this.focus.focusIndex(0, "first");
1249
+ if (focused) this.afterMove(0, "first", null);
1250
+ return focused;
1251
+ }
1252
+ last() {
1253
+ this.collection.revalidate();
1254
+ this.sync();
1255
+ const total = this.collection.items.length;
1256
+ if (total === 0) return null;
1257
+ const focused = this.focus.focusIndex(total - 1, "last");
1258
+ if (focused) this.afterMove(total - 1, "last", null);
1259
+ return focused;
1260
+ }
1261
+ focusFirst() {
1262
+ return this.first();
1263
+ }
1264
+ focusLast() {
1265
+ return this.last();
1266
+ }
1267
+ pageUp() {
1268
+ return this.step(-this.resolved.pageSize, "pageUp");
1269
+ }
1270
+ pageDown() {
1271
+ return this.step(this.resolved.pageSize, "pageDown");
1272
+ }
1273
+ focusIndex(index) {
1274
+ this.collection.revalidate();
1275
+ this.sync();
1276
+ return this.focus.focusIndex(index, null);
1277
+ }
1278
+ setActiveIndex(index, options = {}) {
1279
+ this.collection.revalidate();
1280
+ this.sync();
1281
+ if (options.focus === false) {
1282
+ this.focus.setActiveIndex(index);
1283
+ return this.collection.items[index] ?? null;
1284
+ }
1285
+ return this.focus.focusIndex(index, null);
1286
+ }
1287
+ focusElement(element) {
1288
+ this.sync();
1289
+ return this.focus.focusElement(element ?? null);
1290
+ }
1291
+ restoreFocus() {
1292
+ return this.focus.restore();
1293
+ }
1294
+ rememberFocus() {
1295
+ if (typeof document === "undefined") return;
1296
+ const active = document.activeElement;
1297
+ if (!active || active === document.body || active === document.documentElement) return;
1298
+ if (this.resolved.container && contains(this.resolved.container, active)) return;
1299
+ this.focus.remember(active);
1300
+ }
1301
+ select(elementOrIndex) {
1302
+ this.sync();
1303
+ const index = this.resolveIndex(elementOrIndex);
1304
+ const element = index >= 0 ? this.collection.items[index] : void 0;
1305
+ if (!element) return;
1306
+ this.applySelection(index, false);
1307
+ this.resolved.handlers.onSelect?.(element, index);
1308
+ this.emitter.emit("select", { element, index });
1309
+ this.emitAction("select", null, element, index);
1310
+ }
1311
+ perform(action, event = null) {
1312
+ if (this.destroyed) return false;
1313
+ return this.runAction(action, event, null);
1314
+ }
1315
+ setContainer(container) {
1316
+ const next = resolveElement(container, this.resolved.container?.ownerDocument ?? null);
1317
+ if (next === this.resolved.container) return;
1318
+ const attached = this.keydownDisposer !== null;
1319
+ if (attached) this.detach();
1320
+ this.resolved.container = next;
1321
+ if (!this.resolved.pinnedTarget) {
1322
+ this.resolved.listenTarget = next ?? (typeof document !== "undefined" ? document : null);
1323
+ }
1324
+ this.collection.setContainer(next);
1325
+ if (attached) this.attach();
1326
+ this.syncPending = true;
1327
+ this.emitter.emit("refresh", { items: this.collection.items });
1328
+ }
1329
+ refresh() {
1330
+ const items = this.collection.refresh();
1331
+ this.sync(true);
1332
+ this.resolved.handlers.onRefresh?.(items);
1333
+ this.emitter.emit("refresh", { items });
1334
+ return items;
1335
+ }
1336
+ add(element) {
1337
+ this.collection.add(element);
1338
+ }
1339
+ remove(element) {
1340
+ this.collection.remove(element);
1341
+ }
1342
+ setItems(elements) {
1343
+ this.collection.setItems(elements);
1344
+ }
1345
+ enable() {
1346
+ if (this.destroyed || this.enabled) return;
1347
+ this.attach();
1348
+ }
1349
+ disable() {
1350
+ this.detach();
1351
+ this.matcher.resetSequence();
1352
+ this.typeaheadController?.reset();
1353
+ }
1354
+ update(options) {
1355
+ if (this.destroyed) return;
1356
+ if (options.container !== void 0) this.setContainer(options.container);
1357
+ if (options.mode !== void 0 && options.mode !== this.resolved.mode) {
1358
+ this.resolved.mode = options.mode;
1359
+ if (!this.resolved.userStrategy) {
1360
+ this.resolved.strategy = strategyForMode(options.mode);
1361
+ }
1362
+ }
1363
+ if (options.loop !== void 0) this.resolved.loop = options.loop === true;
1364
+ if (options.columns !== void 0) this.resolved.columns = options.columns ?? 0;
1365
+ if (options.pageSize !== void 0) this.resolved.pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
1366
+ if (options.keys !== void 0) this.resolved.userKeys = options.keys;
1367
+ if (options.keys !== void 0 || options.mode !== void 0) {
1368
+ this.resolved.keys = mergeKeys(
1369
+ this.resolved.defaultKeys ?? defaultKeysFor(this.resolved.mode),
1370
+ this.resolved.userKeys,
1371
+ this.resolved.useDefaultKeys
1372
+ );
1373
+ this.matcher.loadMap(this.resolved.keys);
1374
+ }
1375
+ if (options.enabled === true) this.enable();
1376
+ else if (options.enabled === false) this.disable();
1377
+ this.syncPending = true;
1378
+ }
1379
+ on(event, handler) {
1380
+ return this.emitter.on(event, handler);
1381
+ }
1382
+ off(event, handler) {
1383
+ this.emitter.off(event, handler);
1384
+ }
1385
+ destroy() {
1386
+ if (this.destroyed) return;
1387
+ this.destroyed = true;
1388
+ this.detach();
1389
+ this.events.dispose();
1390
+ this.collection.dispose();
1391
+ this.matcher.dispose();
1392
+ this.typeaheadController?.dispose();
1393
+ this.typeaheadController = null;
1394
+ this.selectionManager?.dispose();
1395
+ this.selectionManager = null;
1396
+ this.focus.dispose();
1397
+ this.emitter.emit("destroy", {});
1398
+ this.emitter.clear();
1399
+ }
1400
+ attach() {
1401
+ const target = this.resolved.listenTarget;
1402
+ if (!target || this.keydownDisposer) return;
1403
+ const listenerOptions = { capture: this.resolved.capture, passive: false };
1404
+ this.keydownDisposer = this.events.add(target, "keydown", (event) => this.onKeydown(event), listenerOptions);
1405
+ this.auxDisposers.push(
1406
+ this.events.add(target, "focusin", (event) => {
1407
+ this.focus.observe(event.target, this.resolved.container);
1408
+ })
1409
+ );
1410
+ if (this.resolved.pointerSync && this.resolved.container) {
1411
+ this.auxDisposers.push(
1412
+ this.events.add(this.resolved.container, "click", (event) => this.onPointer(event)),
1413
+ this.events.add(this.resolved.container, "pointerover", (event) => this.onPointerOver(event))
1414
+ );
1415
+ }
1416
+ if (this.resolved.observe) this.collection.startObserving();
1417
+ }
1418
+ detach() {
1419
+ if (this.keydownDisposer) {
1420
+ this.keydownDisposer();
1421
+ this.keydownDisposer = null;
1422
+ }
1423
+ for (const dispose of this.auxDisposers) dispose();
1424
+ this.auxDisposers.length = 0;
1425
+ this.collection.stopObserving();
1426
+ }
1427
+ applyAutoFocus() {
1428
+ const { target } = this.resolved.autoFocus;
1429
+ this.sync(true);
1430
+ const items = this.collection.items;
1431
+ if (items.length === 0) return;
1432
+ let index = 0;
1433
+ if (target === "last") index = items.length - 1;
1434
+ else if (typeof target === "function") index = target(items);
1435
+ this.focus.focusIndex(clampIndex(index, items.length, false), null);
1436
+ }
1437
+ onKeydown(event) {
1438
+ if (this.destroyed) return;
1439
+ if (this.resolved.when && !this.resolved.when()) return;
1440
+ this.collection.revalidate();
1441
+ const normalized = normalizeKeyEvent(event);
1442
+ const match = this.matcher.match(normalized, this);
1443
+ if (this.reservedTarget(event) && !(match?.complete ?? false)) return;
1444
+ if (!match) {
1445
+ if (this.handleTypeahead(normalized, event)) return;
1446
+ if (!isModifierKey(normalized.key)) this.emitter.emit("keydown", { event, handled: false });
1447
+ return;
1448
+ }
1449
+ if (!match.complete) {
1450
+ if (match.preventDefault && this.resolved.preventDefault) event.preventDefault();
1451
+ return;
1452
+ }
1453
+ if (this.reservedTarget(event)) return;
1454
+ if (this.resolved.onlyWhenFocusedInside && !this.focusIsInside()) return;
1455
+ const actions = match.actions;
1456
+ for (let i = 0; i < actions.length; i++) {
1457
+ if (this.runAction(actions[i], event, match)) {
1458
+ if (this.resolved.preventDefault) event.preventDefault();
1459
+ if (this.resolved.stopPropagation) event.stopPropagation();
1460
+ this.emitter.emit("keydown", { event, handled: true });
1461
+ return;
1462
+ }
1463
+ }
1464
+ this.emitter.emit("keydown", { event, handled: false });
1465
+ }
1466
+ reservedTarget(event) {
1467
+ if (!this.resolved.ignoreEditable) return false;
1468
+ if (event.target === this.resolved.listenTarget) return false;
1469
+ return isEditableTarget(event.target, this.resolved.editableSelector);
1470
+ }
1471
+ focusIsInside() {
1472
+ if (typeof document === "undefined") return true;
1473
+ const active = document.activeElement;
1474
+ const container = this.resolved.container;
1475
+ const nothingFocused = !active || active === document.body || active === document.documentElement;
1476
+ if (nothingFocused) {
1477
+ return this.focus.element !== null || container === null;
1478
+ }
1479
+ if (!container) return true;
1480
+ if (contains(container, active)) return true;
1481
+ return this.focus.element !== null && contains(container, this.focus.element);
1482
+ }
1483
+ handleTypeahead(normalized, event) {
1484
+ const typeahead = this.typeaheadController;
1485
+ if (!typeahead) return false;
1486
+ if (!normalized.isPrintable || normalized.key.length !== 1 || normalized.repeat) return false;
1487
+ this.sync();
1488
+ const items = this.collection.items;
1489
+ if (items.length === 0) return false;
1490
+ const match = typeahead.handle(normalized.key, items, this.focus.index);
1491
+ if (!match) return false;
1492
+ const index = items.indexOf(match);
1493
+ if (index === -1) return false;
1494
+ const focused = this.focus.focusIndex(index, "typeahead");
1495
+ if (!focused) return false;
1496
+ this.afterMove(index, "typeahead", event);
1497
+ if (this.resolved.preventDefault) event.preventDefault();
1498
+ if (this.resolved.stopPropagation) event.stopPropagation();
1499
+ this.emitter.emit("keydown", { event, handled: true });
1500
+ return true;
1501
+ }
1502
+ runAction(action, event, match) {
1503
+ const fromSequence = match?.fromSequence === true;
1504
+ this.sync();
1505
+ const items = this.collection.items;
1506
+ const index = this.focus.index;
1507
+ const element = index >= 0 ? items[index] ?? null : null;
1508
+ if (MOVEMENT_ACTIONS.has(action)) {
1509
+ if (items.length === 0) return false;
1510
+ return this.runMovement(action, event, items, fromSequence);
1511
+ }
1512
+ switch (action) {
1513
+ case "select": {
1514
+ if (index < 0) return false;
1515
+ const item = items[index];
1516
+ if (this.resolved.selectionOptions?.selectOnActivate !== false) this.applySelection(index, false);
1517
+ this.resolved.handlers.onSelect?.(item, index);
1518
+ this.emitter.emit("select", { element: item, index });
1519
+ this.emitAction(action, event, item, index, fromSequence);
1520
+ return this.selectionManager !== null || this.isHeard(action) || !activatesNatively(item);
1521
+ }
1522
+ case "toggle": {
1523
+ const mode = this.selectionManager?.mode;
1524
+ if (!this.selectionManager || mode === "single" || mode === "none") return false;
1525
+ if (index < 0) return false;
1526
+ this.selectionManager.toggle(index);
1527
+ this.emitAction(action, event, items[index], index, fromSequence);
1528
+ return true;
1529
+ }
1530
+ case "selectAll": {
1531
+ if (!this.selectionManager) return false;
1532
+ this.selectionManager.selectAll();
1533
+ this.emitAction(action, event, element, index, fromSequence);
1534
+ return true;
1535
+ }
1536
+ case "escape": {
1537
+ this.typeaheadController?.reset();
1538
+ this.matcher.resetSequence();
1539
+ this.emitAction(action, event, element, index, fromSequence);
1540
+ this.resolved.handlers.onEscape?.(event);
1541
+ return this.isHeard(action);
1542
+ }
1543
+ default: {
1544
+ this.emitAction(action, event, element, index, fromSequence);
1545
+ return true;
1546
+ }
1547
+ }
1548
+ }
1549
+ runMovement(action, event, items, fromSequence = false) {
1550
+ const direction = action === "up" || action === "down" || action === "left" || action === "right" ? action : null;
1551
+ let target;
1552
+ switch (action) {
1553
+ case "first":
1554
+ target = this.resolveNavigation(action, null, event) ?? 0;
1555
+ break;
1556
+ case "last":
1557
+ target = this.resolveNavigation(action, null, event) ?? items.length - 1;
1558
+ break;
1559
+ case "pageUp":
1560
+ case "pageDown": {
1561
+ const delta = (action === "pageDown" ? 1 : -1) * this.resolved.pageSize;
1562
+ const current = this.focus.index;
1563
+ const raw = current < 0 ? delta > 0 ? 0 : items.length - 1 : current + delta;
1564
+ target = clampIndex(raw, items.length, this.resolved.loop);
1565
+ break;
1566
+ }
1567
+ default: {
1568
+ const resolved = this.resolveNavigation(action, direction, event);
1569
+ if (resolved === null) return false;
1570
+ target = resolved;
1571
+ }
1572
+ }
1573
+ const focused = this.focus.focusIndex(target, action);
1574
+ if (!focused) return false;
1575
+ this.afterMove(target, action, event, fromSequence);
1576
+ return true;
1577
+ }
1578
+ isHeard(action) {
1579
+ const { onSelect, onEscape, onAction } = this.resolved.handlers;
1580
+ if (onAction || this.ownsAction(action)) return true;
1581
+ return action === "escape" ? onEscape !== void 0 : onSelect !== void 0;
1582
+ }
1583
+ ownsAction(_action) {
1584
+ return false;
1585
+ }
1586
+ emitAction(action, event, element, index, fromSequence = false) {
1587
+ this.resolved.handlers.onAction?.(action, event, element, index);
1588
+ this.emitter.emit("action", { action, event, element, index, fromSequence });
1589
+ }
1590
+ afterMove(index, action, event, fromSequence = false) {
1591
+ this.emitAction(action, event, this.collection.items[index] ?? null, index, fromSequence);
1592
+ const selection = this.resolved.selectionOptions;
1593
+ if (!selection) return;
1594
+ if (!selection.followFocus && selection.mode === "range" && selection.shiftExtendsRange !== false && event?.shiftKey) {
1595
+ this.extendRangeTo(index);
1596
+ }
1597
+ }
1598
+ resolveNavigation(action, direction, event) {
1599
+ const items = this.collection.items;
1600
+ if (items.length === 0) return null;
1601
+ const context = this.createContext(action, direction, event);
1602
+ if (this.resolved.onBeforeNavigate) {
1603
+ const override = this.resolved.onBeforeNavigate(context);
1604
+ if (typeof override === "number" && override >= 0 && override < items.length) return override;
1605
+ }
1606
+ const result = this.resolved.strategy(context);
1607
+ if (typeof result !== "number" || result < 0) return null;
1608
+ return result >= items.length ? items.length - 1 : result;
1609
+ }
1610
+ createContext(action, direction, event) {
1611
+ return {
1612
+ items: this.collection.items,
1613
+ container: this.resolved.container,
1614
+ index: this.focus.index,
1615
+ direction,
1616
+ action,
1617
+ event,
1618
+ shiftKey: event?.shiftKey === true,
1619
+ altKey: event?.altKey === true,
1620
+ ctrlKey: event?.ctrlKey === true,
1621
+ metaKey: event?.metaKey === true,
1622
+ columns: this.resolved.columns,
1623
+ loop: this.resolved.loop,
1624
+ version: this.collection.version,
1625
+ rectOf: createRectOf(this.collection)
1626
+ };
1627
+ }
1628
+ step(delta, action) {
1629
+ this.collection.revalidate();
1630
+ this.focus.sync(this.collection.items, this.resolved.container);
1631
+ this.sync();
1632
+ const total = this.collection.items.length;
1633
+ if (total === 0 || delta === 0) return null;
1634
+ const current = this.focus.index;
1635
+ const raw = current < 0 ? delta > 0 ? 0 : total - 1 : current + delta;
1636
+ const target = clampIndex(raw, total, this.resolved.loop);
1637
+ const focused = this.focus.focusIndex(target, action);
1638
+ if (focused && action) this.afterMove(target, action, null);
1639
+ return focused;
1640
+ }
1641
+ resolveIndex(elementOrIndex) {
1642
+ if (elementOrIndex === void 0) return this.focus.index;
1643
+ if (typeof elementOrIndex === "number") return elementOrIndex;
1644
+ return this.collection.indexOf(elementOrIndex);
1645
+ }
1646
+ applySelection(index, extend = false) {
1647
+ const manager = this.selectionManager;
1648
+ if (!manager) return;
1649
+ manager.select(index, extend);
1650
+ }
1651
+ extendRangeTo(index) {
1652
+ const manager = this.selectionManager;
1653
+ if (!manager) return;
1654
+ const items = this.collection.items;
1655
+ const anchor = manager.anchor;
1656
+ const from = anchor ? items.indexOf(anchor) : index;
1657
+ if (from < 0) return;
1658
+ manager.selectRange(from, index);
1659
+ }
1660
+ onPointer(event) {
1661
+ const item = this.itemFromEvent(event);
1662
+ if (!item) return;
1663
+ const index = this.collection.indexOf(item);
1664
+ if (index === -1) return;
1665
+ this.focus.focusIndex(index, "select");
1666
+ const manager = this.selectionManager;
1667
+ if (!manager) return;
1668
+ const mode = manager.mode;
1669
+ if (mode === "none") return;
1670
+ if (mode === "multiple") {
1671
+ if (event.ctrlKey || event.metaKey) manager.toggle(index);
1672
+ else manager.select(index);
1673
+ return;
1674
+ }
1675
+ if (mode === "range" && event.shiftKey) {
1676
+ this.extendRangeTo(index);
1677
+ return;
1678
+ }
1679
+ this.applySelection(index, false);
1680
+ }
1681
+ onPointerOver(event) {
1682
+ const item = this.itemFromEvent(event);
1683
+ if (item) this.focus.observe(item, this.resolved.container);
1684
+ }
1685
+ itemFromEvent(event) {
1686
+ const target = event.target;
1687
+ if (!target) return null;
1688
+ this.collection.revalidate();
1689
+ this.sync();
1690
+ const items = this.collection.items;
1691
+ const selector = this.resolved.selector;
1692
+ if (typeof selector === "string" && typeof target.closest === "function") {
1693
+ try {
1694
+ const closest = target.closest(selector);
1695
+ if (closest && items.indexOf(closest) !== -1) return closest;
1696
+ } catch {
1697
+ }
1698
+ }
1699
+ let node = target;
1700
+ while (node) {
1701
+ if (items.indexOf(node) !== -1) return node;
1702
+ node = node.parentElement;
1703
+ }
1704
+ return null;
1705
+ }
1706
+ sync(force = false) {
1707
+ if (this.destroyed) return;
1708
+ if (!force && !this.syncPending) return;
1709
+ this.syncPending = false;
1710
+ const items = this.collection.items;
1711
+ this.focus.sync(items, this.resolved.container);
1712
+ this.selectionManager?.sync(items);
1713
+ }
1714
+ };
1715
+ function activatesNatively(element) {
1716
+ return element.matches("a[href],button,summary,input,select,textarea");
1717
+ }
1718
+ function clampIndex(index, total, loop) {
1719
+ if (total === 0) return -1;
1720
+ if (loop) return (index % total + total) % total;
1721
+ if (index < 0) return 0;
1722
+ if (index >= total) return total - 1;
1723
+ return index;
1724
+ }
1725
+ function createKeystra(options = {}) {
1726
+ return new NavigationController(options);
1727
+ }
1728
+
1729
+ export { COMMAND_PALETTE_KEYS, EventManager, GRID_KEYS, HORIZONTAL_KEYS, ItemCollection, KeyMatcher, LIST_KEYS, MENU_KEYS, NavigationController, ROVING_KEYS, TABS_KEYS_HORIZONTAL, TABS_KEYS_VERTICAL, TREE_KEYS, VERTICAL_KEYS, codeToKey, createKeyMatcher, createKeystra, createRafThrottle, createRectOf, createTypeahead, defaultKeysFor, describeEvent, formatKeyCombo, isModifierKey, isPrintableKey, keyEquals, mergeKeys, normalizeKeyEvent, normalizeKeyName, parseKeyBinding, resolveOptions };