@tanstack/hotkeys 0.8.0 → 0.10.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 (109) hide show
  1. package/README.md +15 -1
  2. package/dist/{manager.utils.js → _event-target.js} +2 -41
  3. package/dist/_keyboard-event.js +75 -0
  4. package/dist/_match.js +48 -0
  5. package/dist/_named-keys.d.ts +6 -0
  6. package/dist/_named-keys.js +65 -0
  7. package/dist/_recorder-chord.js +42 -0
  8. package/dist/_recording-guard.js +55 -0
  9. package/dist/_registration.js +49 -0
  10. package/dist/conflicts.d.ts +36 -0
  11. package/dist/conflicts.js +54 -0
  12. package/dist/constants.d.ts +16 -144
  13. package/dist/constants.js +55 -172
  14. package/dist/display-labels.d.ts +90 -0
  15. package/dist/display-labels.js +112 -0
  16. package/dist/format.d.ts +48 -12
  17. package/dist/format.js +35 -39
  18. package/dist/hint.d.ts +20 -0
  19. package/dist/hint.js +23 -0
  20. package/dist/hotkey-manager.d.ts +14 -12
  21. package/dist/hotkey-manager.js +70 -21
  22. package/dist/hotkey-recorder.d.ts +8 -8
  23. package/dist/hotkey-recorder.js +74 -15
  24. package/dist/hotkey-sequence-recorder.d.ts +13 -9
  25. package/dist/hotkey-sequence-recorder.js +90 -28
  26. package/dist/{hotkey.d.ts → hotkey.types.d.ts} +58 -126
  27. package/dist/index.d.ts +14 -9
  28. package/dist/index.js +10 -6
  29. package/dist/key-state-tracker.d.ts +4 -7
  30. package/dist/key-state-tracker.js +30 -27
  31. package/dist/key.types.d.ts +79 -0
  32. package/dist/match.d.ts +24 -11
  33. package/dist/match.js +19 -30
  34. package/dist/parse.d.ts +18 -15
  35. package/dist/parse.js +39 -22
  36. package/dist/platform.d.ts +41 -0
  37. package/dist/platform.js +52 -0
  38. package/dist/recorder-options.d.ts +31 -0
  39. package/dist/sequence-manager.d.ts +27 -15
  40. package/dist/sequence-manager.js +61 -30
  41. package/dist/validate.d.ts +16 -8
  42. package/dist/validate.js +8 -13
  43. package/package.json +8 -13
  44. package/dist/constants.cjs +0 -535
  45. package/dist/constants.cjs.map +0 -1
  46. package/dist/constants.d.cts +0 -290
  47. package/dist/constants.js.map +0 -1
  48. package/dist/format.cjs +0 -91
  49. package/dist/format.cjs.map +0 -1
  50. package/dist/format.d.cts +0 -60
  51. package/dist/format.js.map +0 -1
  52. package/dist/hotkey-manager.cjs +0 -383
  53. package/dist/hotkey-manager.cjs.map +0 -1
  54. package/dist/hotkey-manager.d.cts +0 -230
  55. package/dist/hotkey-manager.js.map +0 -1
  56. package/dist/hotkey-recorder.cjs +0 -179
  57. package/dist/hotkey-recorder.cjs.map +0 -1
  58. package/dist/hotkey-recorder.d.cts +0 -116
  59. package/dist/hotkey-recorder.js.map +0 -1
  60. package/dist/hotkey-sequence-recorder.cjs +0 -192
  61. package/dist/hotkey-sequence-recorder.cjs.map +0 -1
  62. package/dist/hotkey-sequence-recorder.d.cts +0 -79
  63. package/dist/hotkey-sequence-recorder.js.map +0 -1
  64. package/dist/hotkey.d.cts +0 -301
  65. package/dist/index.cjs +0 -63
  66. package/dist/index.d.cts +0 -13
  67. package/dist/key-state-tracker.cjs +0 -197
  68. package/dist/key-state-tracker.cjs.map +0 -1
  69. package/dist/key-state-tracker.d.cts +0 -107
  70. package/dist/key-state-tracker.js.map +0 -1
  71. package/dist/manager.utils.cjs +0 -115
  72. package/dist/manager.utils.cjs.map +0 -1
  73. package/dist/manager.utils.d.cts +0 -13
  74. package/dist/manager.utils.d.ts +0 -13
  75. package/dist/manager.utils.js.map +0 -1
  76. package/dist/match.cjs +0 -153
  77. package/dist/match.cjs.map +0 -1
  78. package/dist/match.d.cts +0 -85
  79. package/dist/match.js.map +0 -1
  80. package/dist/parse.cjs +0 -228
  81. package/dist/parse.cjs.map +0 -1
  82. package/dist/parse.d.cts +0 -128
  83. package/dist/parse.js.map +0 -1
  84. package/dist/recorder-chord.cjs +0 -18
  85. package/dist/recorder-chord.cjs.map +0 -1
  86. package/dist/recorder-chord.js +0 -18
  87. package/dist/recorder-chord.js.map +0 -1
  88. package/dist/sequence-manager.cjs +0 -415
  89. package/dist/sequence-manager.cjs.map +0 -1
  90. package/dist/sequence-manager.d.cts +0 -157
  91. package/dist/sequence-manager.js.map +0 -1
  92. package/dist/validate.cjs +0 -116
  93. package/dist/validate.cjs.map +0 -1
  94. package/dist/validate.d.cts +0 -56
  95. package/dist/validate.js.map +0 -1
  96. package/src/constants.ts +0 -603
  97. package/src/format.ts +0 -142
  98. package/src/hotkey-manager.ts +0 -759
  99. package/src/hotkey-recorder.ts +0 -272
  100. package/src/hotkey-sequence-recorder.ts +0 -328
  101. package/src/hotkey.ts +0 -436
  102. package/src/index.ts +0 -11
  103. package/src/key-state-tracker.ts +0 -249
  104. package/src/manager.utils.ts +0 -231
  105. package/src/match.ts +0 -259
  106. package/src/parse.ts +0 -328
  107. package/src/recorder-chord.ts +0 -25
  108. package/src/sequence-manager.ts +0 -734
  109. package/src/validate.ts +0 -171
package/README.md CHANGED
@@ -1,5 +1,19 @@
1
1
  <div align="center">
2
- <img src="./media/header_hotkeys.png" alt="TanStack Hotkeys" />
2
+ <picture>
3
+ <source
4
+ media="(prefers-color-scheme: dark)"
5
+ srcset="https://tanstack.com/api/readme/hotkeys.png?theme=dark"
6
+ />
7
+ <source
8
+ media="(prefers-color-scheme: light)"
9
+ srcset="https://tanstack.com/api/readme/hotkeys.png"
10
+ />
11
+ <img
12
+ src="https://tanstack.com/api/readme/hotkeys.png"
13
+ alt="TanStack Hotkeys"
14
+ width="900"
15
+ />
16
+ </picture>
3
17
  </div>
4
18
 
5
19
  <br />
@@ -1,25 +1,4 @@
1
- //#region src/manager.utils.ts
2
- /**
3
- * Default options for hotkey/sequence registration.
4
- * Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).
5
- */
6
- const defaultHotkeyOptions = {
7
- preventDefault: true,
8
- stopPropagation: true,
9
- eventType: "keydown",
10
- enabled: true,
11
- ignoreInputs: true,
12
- conflictBehavior: "warn"
13
- };
14
- /**
15
- * Computes the default ignoreInputs value based on the hotkey.
16
- * Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
17
- */
18
- function getDefaultIgnoreInputs(parsedHotkey) {
19
- if (parsedHotkey.ctrl || parsedHotkey.meta) return false;
20
- if (parsedHotkey.key === "Escape") return false;
21
- return true;
22
- }
1
+ //#region src/_event-target.ts
23
2
  /**
24
3
  * Checks if an element is an input-like element that should be ignored for hotkeys.
25
4
  *
@@ -86,24 +65,6 @@ function isEventForTarget(event, target) {
86
65
  }
87
66
  return false;
88
67
  }
89
- /**
90
- * Handles conflicts between registrations based on conflict behavior.
91
- *
92
- * @param conflictingId - The ID of the conflicting registration
93
- * @param keyDisplay - Display string for the conflicting key/sequence (for error messages)
94
- * @param conflictBehavior - How to handle the conflict
95
- * @param unregister - Function to unregister by ID
96
- */
97
- function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
98
- if (conflictBehavior === "allow") return;
99
- if (conflictBehavior === "warn") {
100
- console.warn(`'${keyDisplay}' is already registered. Multiple handlers will be triggered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to suppress this warning.`);
101
- return;
102
- }
103
- if (conflictBehavior === "error") throw new Error(`'${keyDisplay}' is already registered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to allow multiple registrations.`);
104
- unregister(conflictingId);
105
- }
106
68
 
107
69
  //#endregion
108
- export { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, isEventForTarget, isInputElement, shouldIgnoreInputEvent };
109
- //# sourceMappingURL=manager.utils.js.map
70
+ export { getActiveElementForListenerTarget, isEventForTarget, isInputElement, shouldIgnoreInputEvent };
@@ -0,0 +1,75 @@
1
+ import { PUNCTUATION_CODE_MAP, normalizeKeyName } from "./constants.js";
2
+
3
+ //#region src/_keyboard-event.ts
4
+ /** Normalizes Unicode key names and modifier flags while retaining physical code and IME state. */
5
+ function normalizeKeyboardEvent(event, platform) {
6
+ let altGraph = false;
7
+ try {
8
+ altGraph = platform !== "mac" && typeof event.getModifierState === "function" && event.getModifierState("AltGraph");
9
+ } catch {}
10
+ const key = (event.key || "").normalize("NFC");
11
+ return {
12
+ key: normalizeKeyName(key),
13
+ code: event.code || "",
14
+ ctrl: Boolean(event.ctrlKey),
15
+ shift: Boolean(event.shiftKey),
16
+ alt: Boolean(event.altKey),
17
+ meta: Boolean(event.metaKey),
18
+ altGraph,
19
+ location: event.location,
20
+ isComposing: Boolean(event.isComposing) || key === "Process" || key === "Unidentified"
21
+ };
22
+ }
23
+ /**
24
+ * Maps familiar positions to compatibility fallback keys for the logical matcher.
25
+ * This assumes conventional positions, not the active layout; never use it to
26
+ * turn recorded physical bindings into logical bindings or display labels.
27
+ */
28
+ function codeToLogicalKey(code) {
29
+ if (/^Key[A-Z]$/i.test(code)) return code.slice(3).toUpperCase();
30
+ if (/^Digit[0-9]$/i.test(code)) return code.slice(5);
31
+ if (/^Numpad[0-9]$/i.test(code)) return code.slice(6);
32
+ const punctuation = PUNCTUATION_CODE_MAP[code];
33
+ if (punctuation) return punctuation;
34
+ return {
35
+ NumpadAdd: "+",
36
+ NumpadSubtract: "-",
37
+ NumpadMultiply: "*",
38
+ NumpadDivide: "/",
39
+ NumpadDecimal: ".",
40
+ NumpadEnter: "Enter",
41
+ Space: "Space"
42
+ }[code];
43
+ }
44
+ /** Tests for one Unicode code point, including characters represented by a surrogate pair. */
45
+ function isPrintableKey(key) {
46
+ return key !== "" && Array.from(key).length === 1;
47
+ }
48
+ /** Splits modifier tokens while retaining a trailing literal plus: `Control++` → `['Control', '+']`. */
49
+ function splitHotkeyParts(hotkey) {
50
+ if (!hotkey.endsWith("+")) return hotkey.split("+");
51
+ const parts = hotkey.slice(0, -1).split("+");
52
+ if (parts.at(-1) === "") parts.pop();
53
+ parts.push("+");
54
+ return parts;
55
+ }
56
+ /** Identifies ASCII letters whose layout-produced value takes precedence over positional fallbacks. */
57
+ function isAsciiLetter(key) {
58
+ return /^[A-Za-z]$/.test(key);
59
+ }
60
+ /** Identifies a single Latin-script character, including accented letters. */
61
+ function isLatinLetter(key) {
62
+ return /^\p{Script_Extensions=Latin}$/u.test(key);
63
+ }
64
+ /** Compares letters without case, preserving accents and avoiding expansions such as ß → SS. */
65
+ function keysEqual(left, right) {
66
+ if (/^\p{Letter}$/u.test(left) && /^\p{Letter}$/u.test(right)) return left.localeCompare(right, void 0, { sensitivity: "accent" }) === 0;
67
+ return left === right;
68
+ }
69
+ /** Rejects reserved physical-token syntax in logical object fields while allowing Unicode characters. */
70
+ function assertLogicalKey(key) {
71
+ if (/^\[.*\]$/s.test(key.trim())) throw new Error("Logical key must not contain a bracketed physical code. Use the code field instead.");
72
+ }
73
+
74
+ //#endregion
75
+ export { assertLogicalKey, codeToLogicalKey, isAsciiLetter, isLatinLetter, isPrintableKey, keysEqual, normalizeKeyboardEvent, splitHotkeyParts };
package/dist/_match.js ADDED
@@ -0,0 +1,48 @@
1
+ import { detectPlatform } from "./platform.js";
2
+ import { codeToLogicalKey, isAsciiLetter, isLatinLetter, isPrintableKey, keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
3
+ import { parseHotkey } from "./parse.js";
4
+
5
+ //#region src/_match.ts
6
+ /** Matches an event and reports the winning candidate and release identity. */
7
+ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
8
+ const parsed = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : hotkey;
9
+ const normalized = normalizeKeyboardEvent(event, platform);
10
+ const noMatch = {
11
+ matched: false,
12
+ score: 0
13
+ };
14
+ if (normalized.isComposing && (parsed.code !== void 0 || isPrintableKey(parsed.key))) return noMatch;
15
+ if (normalized.altGraph && (parsed.ctrl || parsed.alt)) return noMatch;
16
+ if (parsed.code !== void 0) {
17
+ if (!modifiersMatch(normalized, parsed, false)) return noMatch;
18
+ return normalized.code === parsed.code ? matched("code", 3) : noMatch;
19
+ }
20
+ const direct = keysEqual(normalized.key, parsed.key);
21
+ const implicitShift = direct && isPrintableKey(normalized.key) && !/^\p{Letter}$/u.test(normalized.key) && !parsed.shift;
22
+ if (direct && modifiersMatch(normalized, parsed, implicitShift)) return matched("key", implicitShift ? 2 : 3);
23
+ if (isAsciiLetter(normalized.key) || isLatinLetter(normalized.key) && normalized.code.startsWith("Key") && !normalized.alt) return noMatch;
24
+ if (!modifiersMatch(normalized, parsed, false)) return noMatch;
25
+ const fallbackKey = codeToLogicalKey(normalized.code);
26
+ return fallbackKey !== void 0 && keysEqual(fallbackKey, parsed.key) ? matched("fallback", 1) : noMatch;
27
+ function matched(source, score) {
28
+ return {
29
+ matched: true,
30
+ score,
31
+ source,
32
+ identity: {
33
+ key: normalized.key,
34
+ code: normalized.code
35
+ }
36
+ };
37
+ }
38
+ }
39
+ /** Compares modifiers, excluding synthetic AltGr flags and optionally allowing Shift used to type a glyph. */
40
+ function modifiersMatch(event, parsed, allowImplicitShift) {
41
+ const ctrl = event.altGraph ? false : event.ctrl;
42
+ const alt = event.altGraph ? false : event.alt;
43
+ if (ctrl !== parsed.ctrl || alt !== parsed.alt || event.meta !== parsed.meta) return false;
44
+ return event.shift === parsed.shift || allowImplicitShift && event.shift;
45
+ }
46
+
47
+ //#endregion
48
+ export { matchKeyboardEvent };
@@ -0,0 +1,6 @@
1
+ //#region src/_named-keys.d.ts
2
+ /** Names defined for both KeyboardEvent.key and KeyboardEvent.code, beyond the basic key groups. */
3
+ export declare const SHARED_NAMED_KEYS: readonly ["CapsLock", "ContextMenu", "Convert", "KanaMode", "NonConvert", "Help", "Insert", "NumLock", "Fn", "FnLock", "PrintScreen", "ScrollLock", "Pause", "BrowserBack", "BrowserFavorites", "BrowserForward", "BrowserHome", "BrowserRefresh", "BrowserSearch", "BrowserStop", "Eject", "LaunchMail", "MediaPlayPause", "MediaStop", "MediaTrackNext", "MediaTrackPrevious", "Power", "AudioVolumeDown", "AudioVolumeMute", "AudioVolumeUp", "WakeUp", "Hyper", "Super", "Again", "Copy", "Cut", "Find", "Open", "Paste", "Props", "Select", "Undo", "Hiragana", "Katakana"];
4
+ /** Logical counterparts whose physical positions use different names (for example Sleep and Lang1). */
5
+ export declare const LOGICAL_ONLY_NAMED_KEYS: readonly ["Clear", "Cancel", "Standby", "LaunchApplication1", "LaunchApplication2", "LaunchMediaPlayer", "HangulMode", "HanjaMode", "Zenkaku", "Hankaku", "ZenkakuHankaku"];
6
+ //#endregion
@@ -0,0 +1,65 @@
1
+ //#region src/_named-keys.ts
2
+ /** Names defined for both KeyboardEvent.key and KeyboardEvent.code, beyond the basic key groups. */
3
+ const SHARED_NAMED_KEYS = [
4
+ "CapsLock",
5
+ "ContextMenu",
6
+ "Convert",
7
+ "KanaMode",
8
+ "NonConvert",
9
+ "Help",
10
+ "Insert",
11
+ "NumLock",
12
+ "Fn",
13
+ "FnLock",
14
+ "PrintScreen",
15
+ "ScrollLock",
16
+ "Pause",
17
+ "BrowserBack",
18
+ "BrowserFavorites",
19
+ "BrowserForward",
20
+ "BrowserHome",
21
+ "BrowserRefresh",
22
+ "BrowserSearch",
23
+ "BrowserStop",
24
+ "Eject",
25
+ "LaunchMail",
26
+ "MediaPlayPause",
27
+ "MediaStop",
28
+ "MediaTrackNext",
29
+ "MediaTrackPrevious",
30
+ "Power",
31
+ "AudioVolumeDown",
32
+ "AudioVolumeMute",
33
+ "AudioVolumeUp",
34
+ "WakeUp",
35
+ "Hyper",
36
+ "Super",
37
+ "Again",
38
+ "Copy",
39
+ "Cut",
40
+ "Find",
41
+ "Open",
42
+ "Paste",
43
+ "Props",
44
+ "Select",
45
+ "Undo",
46
+ "Hiragana",
47
+ "Katakana"
48
+ ];
49
+ /** Logical counterparts whose physical positions use different names (for example Sleep and Lang1). */
50
+ const LOGICAL_ONLY_NAMED_KEYS = [
51
+ "Clear",
52
+ "Cancel",
53
+ "Standby",
54
+ "LaunchApplication1",
55
+ "LaunchApplication2",
56
+ "LaunchMediaPlayer",
57
+ "HangulMode",
58
+ "HanjaMode",
59
+ "Zenkaku",
60
+ "Hankaku",
61
+ "ZenkakuHankaku"
62
+ ];
63
+
64
+ //#endregion
65
+ export { LOGICAL_ONLY_NAMED_KEYS, SHARED_NAMED_KEYS };
@@ -0,0 +1,42 @@
1
+ import { normalizeKeyName } from "./constants.js";
2
+ import { normalizeKeyboardEvent } from "./_keyboard-event.js";
3
+ import { isModifierKey, normalizeHotkeyFromParsed, parseKeyboardEvent } from "./parse.js";
4
+
5
+ //#region src/_recorder-chord.ts
6
+ /**
7
+ * Converts a keydown to a replayable chord, preserving physical codes by default.
8
+ * Returns null for composition, modifier-only input, or unusable physical input.
9
+ * Key mode retains the produced character; neither mode guesses layout letters.
10
+ */
11
+ function hotkeyChordFromKeydown(event, platform, recordBy = "code") {
12
+ const normalized = normalizeKeyboardEvent(event, platform);
13
+ if (normalized.isComposing || isModifierKey(normalizeKeyName(event.key)) || event.key === "AltGraph") return null;
14
+ if (recordBy === "code" && (!/^[A-Za-z][A-Za-z0-9]*$/.test(event.code) || event.code === "Unidentified" || normalized.altGraph)) return null;
15
+ const parsed = parseKeyboardEvent(event, platform);
16
+ if (recordBy === "code") {
17
+ const { key: _key, code: _code, ...modifiers } = parsed;
18
+ return normalizeHotkeyFromParsed({
19
+ ...modifiers,
20
+ code: event.code
21
+ }, platform);
22
+ }
23
+ return parsed.key ? normalizeHotkeyFromParsed(parsed, platform) : null;
24
+ }
25
+ /**
26
+ * Explains why a physical chord cannot be recorded before syntax/policy checks.
27
+ * Logical mode deliberately accepts produced characters instead of requiring a code.
28
+ */
29
+ function chordRejection(event, options) {
30
+ if (options.recordBy === "key") return void 0;
31
+ if (normalizeKeyboardEvent(event, options.platform).altGraph) return {
32
+ reason: "alt-graph",
33
+ message: "AltGraph character entry cannot be recorded as a physical shortcut. Use key mode for characters."
34
+ };
35
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(event.code) || event.code === "Unidentified") return {
36
+ reason: "missing-code",
37
+ message: "This event has no usable physical key code."
38
+ };
39
+ }
40
+
41
+ //#endregion
42
+ export { chordRejection, hotkeyChordFromKeydown };
@@ -0,0 +1,55 @@
1
+ import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
+
3
+ //#region src/_recording-guard.ts
4
+ const sessions = /* @__PURE__ */ new Set();
5
+ const captured = /* @__PURE__ */ new WeakSet();
6
+ const pending = /* @__PURE__ */ new Map();
7
+ let listening = false;
8
+ const identity = (event) => event.code || `key:${event.key}`;
9
+ /** Consumes recorded key releases and removes guards for completed chords. */
10
+ function release(event) {
11
+ if (pending.delete(identity(event))) captured.add(event);
12
+ if (MODIFIER_KEYS.has(normalizeKeyName(event.key))) {
13
+ for (const [id, key] of pending) if (!MODIFIER_KEYS.has(key)) pending.delete(id);
14
+ }
15
+ cleanup();
16
+ }
17
+ /** Clears pending releases when focus loss prevents the browser from delivering them. */
18
+ function blur() {
19
+ pending.clear();
20
+ cleanup();
21
+ }
22
+ /** Detaches shared listeners once neither active sessions nor pending releases need them. */
23
+ function cleanup() {
24
+ if (listening && sessions.size === 0 && pending.size === 0) {
25
+ document.removeEventListener("keyup", release, true);
26
+ window.removeEventListener("blur", blur);
27
+ listening = false;
28
+ }
29
+ }
30
+ /** Pauses application shortcut dispatch and starts tracking releases for this recorder. */
31
+ function beginRecording(owner) {
32
+ sessions.add(owner);
33
+ if (!listening && typeof document !== "undefined") {
34
+ document.addEventListener("keyup", release, true);
35
+ window.addEventListener("blur", blur);
36
+ listening = true;
37
+ }
38
+ }
39
+ /** Ends a session while retaining guards for keys still held after commit or cancellation. */
40
+ function endRecording(owner) {
41
+ sessions.delete(owner);
42
+ cleanup();
43
+ }
44
+ /** Marks a consumed keydown and guards subsequent repeats and its release. */
45
+ function captureRecordingEvent(event) {
46
+ captured.add(event);
47
+ pending.set(identity(event), normalizeKeyName(event.key));
48
+ }
49
+ /** Whether managers should suppress callbacks for this recording session or captured chord. */
50
+ function isRecordingEvent(event) {
51
+ return sessions.size > 0 || captured.has(event) || pending.has(identity(event));
52
+ }
53
+
54
+ //#endregion
55
+ export { beginRecording, captureRecordingEvent, endRecording, isRecordingEvent };
@@ -0,0 +1,49 @@
1
+ //#region src/_registration.ts
2
+ /**
3
+ * Default options for hotkey/sequence registration.
4
+ * Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).
5
+ */
6
+ const defaultHotkeyOptions = {
7
+ preventDefault: true,
8
+ stopPropagation: true,
9
+ eventType: "keydown",
10
+ enabled: true,
11
+ ignoreInputs: true,
12
+ conflictBehavior: "warn"
13
+ };
14
+ /**
15
+ * Computes the default ignoreInputs value based on the hotkey.
16
+ * Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
17
+ */
18
+ function getDefaultIgnoreInputs(parsedHotkey) {
19
+ if (parsedHotkey.ctrl || parsedHotkey.meta) return false;
20
+ if (parsedHotkey.key === "Escape" || parsedHotkey.code === "Escape") return false;
21
+ return true;
22
+ }
23
+ /**
24
+ * Handles conflicts between registrations based on conflict behavior.
25
+ *
26
+ * @param conflictingId - The ID of the conflicting registration
27
+ * @param keyDisplay - Display string for the conflicting key/sequence (for error messages)
28
+ * @param conflictBehavior - How to handle the conflict
29
+ * @param unregister - Function to unregister by ID
30
+ */
31
+ function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
32
+ if (conflictBehavior === "allow") return;
33
+ if (conflictBehavior === "warn") {
34
+ console.warn(`'${keyDisplay}' is already registered. Multiple handlers will be triggered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to suppress this warning.`);
35
+ return;
36
+ }
37
+ if (conflictBehavior === "error") throw new Error(`'${keyDisplay}' is already registered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to allow multiple registrations.`);
38
+ unregister(conflictingId);
39
+ }
40
+ /** Compare updates without publishing unchanged options, including fresh metadata objects. */
41
+ function optionsEqual(left, right) {
42
+ const a = left;
43
+ const b = right;
44
+ const keys = Object.keys(a);
45
+ return keys.length === Object.keys(b).length && keys.every((key) => key === "meta" && a[key] && b[key] ? optionsEqual(a[key], b[key]) : Object.is(a[key], b[key]));
46
+ }
47
+
48
+ //#endregion
49
+ export { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual };
@@ -0,0 +1,36 @@
1
+ import { RegisterableHotkey } from "./hotkey.types.js";
2
+ import { HotkeyRegistrationView } from "./hotkey-manager.js";
3
+ import { SequenceRegistrationView } from "./sequence-manager.js";
4
+ //#region src/conflicts.d.ts
5
+ export type HotkeyConflict = {
6
+ type: 'hotkey';
7
+ registration: HotkeyRegistrationView;
8
+ } | {
9
+ type: 'sequence';
10
+ registration: SequenceRegistrationView;
11
+ };
12
+ export interface HotkeyConflictOptions {
13
+ /** Intended registration target. Defaults to document; disjoint targets are excluded. */
14
+ target?: HTMLElement | Document | Window;
15
+ /** Check all targets instead of overlapping targets. Default: overlapping. */
16
+ scope?: 'overlapping' | 'all';
17
+ /** Intended event type. Default: keydown. */
18
+ eventType?: 'keydown' | 'keyup';
19
+ /** Include disabled registrations. Default: false. */
20
+ includeDisabled?: boolean;
21
+ /** IDs of registrations being edited. */
22
+ excludeIds?: ReadonlyArray<string>;
23
+ /** Additional app-specific exclusions, such as all instances of one action. */
24
+ exclude?: (registration: HotkeyRegistrationView | SequenceRegistrationView) => boolean;
25
+ /** Platform used to resolve Mod in the candidate. Registrations retain their own platform. */
26
+ platform?: 'mac' | 'windows' | 'linux';
27
+ /** Source events allow detecting physical/logical overlap on the recorded layout. */
28
+ events?: ReadonlyArray<KeyboardEvent>;
29
+ }
30
+ /**
31
+ * Find equivalent bindings and sequence-prefix conflicts in the live registry.
32
+ * With source events, also checks observed logical/physical overlap. Does not
33
+ * discover unmounted handlers, external listeners, or other keyboard layouts.
34
+ */
35
+ export declare function findHotkeyConflicts(candidate: RegisterableHotkey | ReadonlyArray<RegisterableHotkey>, options?: HotkeyConflictOptions): Array<HotkeyConflict>;
36
+ //#endregion
@@ -0,0 +1,54 @@
1
+ import { detectPlatform } from "./platform.js";
2
+ import { keysEqual } from "./_keyboard-event.js";
3
+ import { parseRegisterableHotkey } from "./parse.js";
4
+ import { matchesKeyboardEvent } from "./match.js";
5
+ import { getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
6
+ import { getSequenceManager } from "./sequence-manager.js";
7
+
8
+ //#region src/conflicts.ts
9
+ /** Whether two targets share a document and one contains the other (or is its window). */
10
+ function targetsOverlap(a, b) {
11
+ if (a === b) return true;
12
+ const documentOf = (target) => {
13
+ if ("document" in target) return target.document;
14
+ if (target.nodeType === 9) return target;
15
+ return target.ownerDocument;
16
+ };
17
+ if (documentOf(a) !== documentOf(b)) return false;
18
+ if (!("contains" in a) || !("contains" in b)) return true;
19
+ return a.contains(b) || b.contains(a);
20
+ }
21
+ /**
22
+ * Find equivalent bindings and sequence-prefix conflicts in the live registry.
23
+ * With source events, also checks observed logical/physical overlap. Does not
24
+ * discover unmounted handlers, external listeners, or other keyboard layouts.
25
+ */
26
+ function findHotkeyConflicts(candidate, options = {}) {
27
+ const steps = Array.isArray(candidate) ? candidate : [candidate];
28
+ if (steps.length === 0) return [];
29
+ const platform = options.platform ?? detectPlatform();
30
+ const parsed = steps.map((step) => parseRegisterableHotkey(step, platform));
31
+ const target = options.target ?? (typeof document === "undefined" ? void 0 : document);
32
+ const conflicts = [];
33
+ const matches = (registration, bindings) => {
34
+ if (!options.includeDisabled && registration.options.enabled === false || (registration.options.eventType ?? "keydown") !== (options.eventType ?? "keydown") || options.excludeIds?.includes(registration.id) || options.exclude?.(registration) || options.scope !== "all" && target && !targetsOverlap(target, registration.target)) return false;
35
+ return parsed.slice(0, Math.min(parsed.length, bindings.length)).every((a, index) => {
36
+ const b = parseRegisterableHotkey(bindings[index], registration.options.platform);
37
+ const same = a.ctrl === b.ctrl && a.alt === b.alt && a.shift === b.shift && a.meta === b.meta && (a.code !== void 0 || b.code !== void 0 ? a.code === b.code : keysEqual(a.key, b.key));
38
+ const event = options.events?.[index];
39
+ return same || event !== void 0 && matchesKeyboardEvent(event, a, platform) && matchesKeyboardEvent(event, b, registration.options.platform);
40
+ });
41
+ };
42
+ for (const registration of getHotkeyManager().registrations.state.values()) if (matches(registration, [registration.hotkey])) conflicts.push({
43
+ type: "hotkey",
44
+ registration: toHotkeyRegistrationView(registration)
45
+ });
46
+ for (const registration of getSequenceManager().registrations.state.values()) if (matches(registration, registration.sequence)) conflicts.push({
47
+ type: "sequence",
48
+ registration
49
+ });
50
+ return conflicts;
51
+ }
52
+
53
+ //#endregion
54
+ export { findHotkeyConflicts };