@tanstack/hotkeys 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_keyboard-event.js +18 -2
- package/dist/_manager-ref.js +10 -0
- package/dist/_match.js +9 -1
- package/dist/_recorder-chord.js +7 -3
- package/dist/_target-listeners.js +3 -2
- package/dist/conflicts.js +6 -5
- package/dist/constants.d.ts +1 -24
- package/dist/constants.js +21 -83
- package/dist/format.js +7 -14
- package/dist/hotkey-manager.js +73 -63
- package/dist/hotkey-recorder.d.ts +32 -2
- package/dist/hotkey.types.d.ts +13 -14
- package/dist/index.d.ts +1 -2
- package/dist/key-state-tracker.js +36 -31
- package/dist/match.js +5 -22
- package/dist/parse.d.ts +7 -6
- package/dist/parse.js +30 -54
- package/dist/validate.js +7 -23
- package/package.json +1 -1
- package/dist/recorder-options.d.ts +0 -34
package/dist/_keyboard-event.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PUNCTUATION_CODE_MAP, normalizeKeyName } from "./constants.js";
|
|
1
|
+
import { MODIFIER_ALIASES, PUNCTUATION_CODE_MAP, normalizeKeyName } from "./constants.js";
|
|
2
2
|
|
|
3
3
|
//#region src/_keyboard-event.ts
|
|
4
4
|
/** Normalizes Unicode key names and modifier flags while retaining physical code and IME state. */
|
|
@@ -45,6 +45,22 @@ function codeToLogicalKey(code) {
|
|
|
45
45
|
function isPrintableKey(key) {
|
|
46
46
|
return key !== "" && Array.from(key).length === 1;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves a modifier token or alias case-insensitively: `ctrl` → `Control`,
|
|
50
|
+
* `cmd` → `Meta`, `mod` → `Mod`. Returns undefined for anything else.
|
|
51
|
+
*/
|
|
52
|
+
function modifierAlias(token) {
|
|
53
|
+
const lower = token.toLowerCase();
|
|
54
|
+
return Object.hasOwn(MODIFIER_ALIASES, lower) ? MODIFIER_ALIASES[lower] : void 0;
|
|
55
|
+
}
|
|
56
|
+
/** Whether a string has the shape of a `KeyboardEvent.code`, such as `KeyS`. */
|
|
57
|
+
function isCodeName(code) {
|
|
58
|
+
return /^[A-Za-z][A-Za-z0-9]*$/.test(code);
|
|
59
|
+
}
|
|
60
|
+
/** The code inside a physical token: `[KeyS]` → `KeyS`. Undefined for anything else. */
|
|
61
|
+
function physicalCodeOf(token) {
|
|
62
|
+
return token.startsWith("[") && token.endsWith("]") && isCodeName(token.slice(1, -1)) ? token.slice(1, -1) : void 0;
|
|
63
|
+
}
|
|
48
64
|
/** Splits modifier tokens while retaining a trailing literal plus: `Control++` → `['Control', '+']`. */
|
|
49
65
|
function splitHotkeyParts(hotkey) {
|
|
50
66
|
if (!hotkey.endsWith("+")) return hotkey.split("+");
|
|
@@ -72,4 +88,4 @@ function assertLogicalKey(key) {
|
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
//#endregion
|
|
75
|
-
export { assertLogicalKey, codeToLogicalKey, isAsciiLetter, isLatinLetter, isPrintableKey, keysEqual, normalizeKeyboardEvent, splitHotkeyParts };
|
|
91
|
+
export { assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitHotkeyParts };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/_manager-ref.ts
|
|
2
|
+
/**
|
|
3
|
+
* The HotkeyManager singleton, once created. Code that only reads the live
|
|
4
|
+
* registry, such as conflict checks, reads this instead of importing the
|
|
5
|
+
* manager, so a recorder bundle does not include hotkey dispatch.
|
|
6
|
+
*/
|
|
7
|
+
const managerRef = { current: null };
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
export { managerRef };
|
package/dist/_match.js
CHANGED
|
@@ -45,6 +45,14 @@ function modifiersMatch(event, parsed, allowImplicitShift) {
|
|
|
45
45
|
if (ctrl !== parsed.ctrl || alt !== parsed.alt || event.meta !== parsed.meta) return false;
|
|
46
46
|
return event.shift === parsed.shift || allowImplicitShift && event.shift;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether two parsed chords have the same identity: equal modifiers, and the
|
|
50
|
+
* same physical code or (case-insensitively) the same logical key. A code and
|
|
51
|
+
* a key never match, even when one keyboard event could produce both.
|
|
52
|
+
*/
|
|
53
|
+
function parsedHotkeysEqual(a, b) {
|
|
54
|
+
return 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));
|
|
55
|
+
}
|
|
48
56
|
|
|
49
57
|
//#endregion
|
|
50
|
-
export { matchKeyboardEvent };
|
|
58
|
+
export { matchKeyboardEvent, parsedHotkeysEqual };
|
package/dist/_recorder-chord.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeKeyName } from "./constants.js";
|
|
2
|
-
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
2
|
+
import { isCodeName, normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
3
3
|
import { isModifierKey, normalizeHotkeyFromParsed, parseKeyboardEvent } from "./parse.js";
|
|
4
4
|
|
|
5
5
|
//#region src/_recorder-chord.ts
|
|
@@ -11,7 +11,7 @@ import { isModifierKey, normalizeHotkeyFromParsed, parseKeyboardEvent } from "./
|
|
|
11
11
|
function hotkeyChordFromKeydown(event, platform, recordBy = "code") {
|
|
12
12
|
const normalized = normalizeKeyboardEvent(event, platform);
|
|
13
13
|
if (normalized.isComposing || isModifierKey(normalizeKeyName(event.key)) || event.key === "AltGraph") return null;
|
|
14
|
-
if (recordBy === "code" && (
|
|
14
|
+
if (recordBy === "code" && (!hasUsableCode(event) || normalized.altGraph)) return null;
|
|
15
15
|
const parsed = parseKeyboardEvent(event, platform);
|
|
16
16
|
if (recordBy === "code") {
|
|
17
17
|
const { key: _key, code: _code, ...modifiers } = parsed;
|
|
@@ -32,11 +32,15 @@ function chordRejection(event, options) {
|
|
|
32
32
|
reason: "alt-graph",
|
|
33
33
|
message: "AltGraph character entry cannot be recorded as a physical shortcut. Use key mode for characters."
|
|
34
34
|
};
|
|
35
|
-
if (
|
|
35
|
+
if (!hasUsableCode(event)) return {
|
|
36
36
|
reason: "missing-code",
|
|
37
37
|
message: "This event has no usable physical key code."
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
/** Whether the event reports a real physical key code to record. */
|
|
41
|
+
function hasUsableCode(event) {
|
|
42
|
+
return isCodeName(event.code) && event.code !== "Unidentified";
|
|
43
|
+
}
|
|
40
44
|
|
|
41
45
|
//#endregion
|
|
42
46
|
export { chordRejection, hotkeyChordFromKeydown };
|
|
@@ -20,7 +20,8 @@ var TargetListeners = class {
|
|
|
20
20
|
const listener = (eventType) => (rawEvent) => {
|
|
21
21
|
const event = rawEvent;
|
|
22
22
|
let processed = this.#processed.get(event);
|
|
23
|
-
|
|
23
|
+
const first = !processed || processed.groups.has(ids);
|
|
24
|
+
if (!processed || first) {
|
|
24
25
|
processed = {
|
|
25
26
|
ids: /* @__PURE__ */ new Set(),
|
|
26
27
|
groups: /* @__PURE__ */ new Set()
|
|
@@ -33,7 +34,7 @@ var TargetListeners = class {
|
|
|
33
34
|
processed.ids.add(registrationId);
|
|
34
35
|
pending.add(registrationId);
|
|
35
36
|
}
|
|
36
|
-
this.dispatch(event, target, eventType, capture, pending);
|
|
37
|
+
this.dispatch(event, target, eventType, capture, pending, first);
|
|
37
38
|
};
|
|
38
39
|
group = {
|
|
39
40
|
ids,
|
package/dist/conflicts.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
|
-
import { keysEqual } from "./_keyboard-event.js";
|
|
3
2
|
import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
|
|
3
|
+
import { parsedHotkeysEqual } from "./_match.js";
|
|
4
4
|
import { matchesKeyboardEvent } from "./match.js";
|
|
5
5
|
import { getTargetDocument } from "./_event-target.js";
|
|
6
|
-
import {
|
|
6
|
+
import { managerRef } from "./_manager-ref.js";
|
|
7
|
+
import { toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
7
8
|
|
|
8
9
|
//#region src/conflicts.ts
|
|
9
10
|
/** Whether two targets share a document and one contains the other (or is its window). */
|
|
@@ -29,12 +30,12 @@ function findHotkeyConflicts(candidate, options = {}) {
|
|
|
29
30
|
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;
|
|
30
31
|
return parsed.slice(0, Math.min(parsed.length, bindings.length)).every((a, index) => {
|
|
31
32
|
const b = parseRegisterableHotkey(bindings[index], registration.options.platform);
|
|
32
|
-
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));
|
|
33
33
|
const event = options.events?.[index];
|
|
34
|
-
return
|
|
34
|
+
return parsedHotkeysEqual(a, b) || event !== void 0 && matchesKeyboardEvent(event, a, platform) && matchesKeyboardEvent(event, b, registration.options.platform);
|
|
35
35
|
});
|
|
36
36
|
};
|
|
37
|
-
|
|
37
|
+
const registrations = managerRef.current?.registrations.state.values() ?? [];
|
|
38
|
+
for (const registration of registrations) {
|
|
38
39
|
const view = toHotkeyRegistrationView(registration);
|
|
39
40
|
if (matches(view, view.kind === "sequence" ? view.hotkey : [view.hotkey])) conflicts.push({ registration: view });
|
|
40
41
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -119,30 +119,7 @@ export declare const PUNCTUATION_CODE_MAP: Record<string, string>;
|
|
|
119
119
|
* @see {@link PUNCTUATION_KEYS}
|
|
120
120
|
*/
|
|
121
121
|
export declare const ALL_KEYS: Set<"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" | "Clear" | "Cancel" | "Standby" | "LaunchApplication1" | "LaunchApplication2" | "LaunchMediaPlayer" | "HangulMode" | "HanjaMode" | "Zenkaku" | "Hankaku" | "ZenkakuHankaku" | LetterKey | NumberKey | FunctionKey | NavigationKey | EditingKey | PunctuationKey>;
|
|
122
|
-
/**
|
|
123
|
-
* Normalizes a key name to its canonical form.
|
|
124
|
-
*
|
|
125
|
-
* Converts various key name formats (aliases, case variations) into the standard
|
|
126
|
-
* canonical names used throughout the library. This enables a more forgiving API
|
|
127
|
-
* where users can write keys in different ways and still get correct behavior.
|
|
128
|
-
*
|
|
129
|
-
* Normalization rules:
|
|
130
|
-
* 1. Check aliases first (e.g., 'Esc' → 'Escape', 'Del' → 'Delete')
|
|
131
|
-
* 2. Single letters → uppercase (e.g., 'a' → 'A', 's' → 'S')
|
|
132
|
-
* 3. Function keys → uppercase (e.g., 'f1' → 'F1', 'F12' → 'F12')
|
|
133
|
-
* 4. Other keys → returned as-is (already canonical or unknown)
|
|
134
|
-
*
|
|
135
|
-
* @param key - The key name to normalize (can be an alias, lowercase, etc.)
|
|
136
|
-
* @returns The canonical key name
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* ```ts
|
|
140
|
-
* normalizeKeyName('esc') // 'Escape'
|
|
141
|
-
* normalizeKeyName('a') // 'A'
|
|
142
|
-
* normalizeKeyName('f1') // 'F1'
|
|
143
|
-
* normalizeKeyName('ArrowUp') // 'ArrowUp' (already canonical)
|
|
144
|
-
* ```
|
|
145
|
-
*/
|
|
122
|
+
/** Whether a key is a single Unicode letter, such as `'a'` or `'é'`. */
|
|
146
123
|
export declare function isSingleLetterKey(key: string): boolean;
|
|
147
124
|
/**
|
|
148
125
|
* Normalizes a key name to its canonical form.
|
package/dist/constants.js
CHANGED
|
@@ -28,7 +28,7 @@ const MODIFIER_ORDER = [
|
|
|
28
28
|
* a modifier is released so we can clear non-modifier keys whose keyup events
|
|
29
29
|
* may have been swallowed by the OS (e.g. macOS Cmd+key combos).
|
|
30
30
|
*/
|
|
31
|
-
const MODIFIER_KEYS = new Set(MODIFIER_ORDER);
|
|
31
|
+
const MODIFIER_KEYS = /* @__PURE__ */ new Set(MODIFIER_ORDER);
|
|
32
32
|
/**
|
|
33
33
|
* Maps modifier key aliases to their canonical form or platform-adaptive 'Mod'.
|
|
34
34
|
*
|
|
@@ -283,89 +283,28 @@ const ALL_KEYS = /* @__PURE__ */ new Set([
|
|
|
283
283
|
...SHARED_NAMED_KEYS,
|
|
284
284
|
...LOGICAL_ONLY_NAMED_KEYS
|
|
285
285
|
]);
|
|
286
|
-
const CANONICAL_KEY_NAMES = new Map(Array.from(ALL_KEYS, (key) => [key.toLowerCase(), key]));
|
|
287
286
|
/**
|
|
288
|
-
* Maps key
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* they'll all normalize to 'Escape'.
|
|
293
|
-
*
|
|
294
|
-
* This map is used internally by `normalizeKeyName()` to convert user input
|
|
295
|
-
* into the canonical key names used throughout the library.
|
|
296
|
-
*
|
|
297
|
-
* @remarks Case-insensitive lookups are supported via lowercase variants
|
|
298
|
-
*
|
|
299
|
-
* @example
|
|
300
|
-
* ```ts
|
|
301
|
-
* KEY_ALIASES['Esc'] // 'Escape'
|
|
302
|
-
* KEY_ALIASES['Del'] // 'Delete'
|
|
303
|
-
* KEY_ALIASES['Up'] // 'ArrowUp'
|
|
304
|
-
* ```
|
|
305
|
-
*/
|
|
306
|
-
const KEY_ALIASES = {
|
|
307
|
-
Plus: "+",
|
|
308
|
-
plus: "+",
|
|
309
|
-
Esc: "Escape",
|
|
310
|
-
esc: "Escape",
|
|
311
|
-
escape: "Escape",
|
|
312
|
-
Return: "Enter",
|
|
313
|
-
return: "Enter",
|
|
314
|
-
enter: "Enter",
|
|
315
|
-
" ": "Space",
|
|
316
|
-
space: "Space",
|
|
317
|
-
Spacebar: "Space",
|
|
318
|
-
spacebar: "Space",
|
|
319
|
-
tab: "Tab",
|
|
320
|
-
backspace: "Backspace",
|
|
321
|
-
Del: "Delete",
|
|
322
|
-
del: "Delete",
|
|
323
|
-
delete: "Delete",
|
|
324
|
-
Up: "ArrowUp",
|
|
325
|
-
up: "ArrowUp",
|
|
326
|
-
arrowup: "ArrowUp",
|
|
327
|
-
Down: "ArrowDown",
|
|
328
|
-
down: "ArrowDown",
|
|
329
|
-
arrowdown: "ArrowDown",
|
|
330
|
-
Left: "ArrowLeft",
|
|
331
|
-
left: "ArrowLeft",
|
|
332
|
-
arrowleft: "ArrowLeft",
|
|
333
|
-
Right: "ArrowRight",
|
|
334
|
-
right: "ArrowRight",
|
|
335
|
-
arrowright: "ArrowRight",
|
|
336
|
-
home: "Home",
|
|
337
|
-
end: "End",
|
|
338
|
-
pageup: "PageUp",
|
|
339
|
-
pagedown: "PageDown",
|
|
340
|
-
PgUp: "PageUp",
|
|
341
|
-
PgDn: "PageDown",
|
|
342
|
-
pgup: "PageUp",
|
|
343
|
-
pgdn: "PageDown"
|
|
344
|
-
};
|
|
345
|
-
/**
|
|
346
|
-
* Normalizes a key name to its canonical form.
|
|
347
|
-
*
|
|
348
|
-
* Converts various key name formats (aliases, case variations) into the standard
|
|
349
|
-
* canonical names used throughout the library. This enables a more forgiving API
|
|
350
|
-
* where users can write keys in different ways and still get correct behavior.
|
|
351
|
-
*
|
|
352
|
-
* Normalization rules:
|
|
353
|
-
* 1. Check aliases first (e.g., 'Esc' → 'Escape', 'Del' → 'Delete')
|
|
354
|
-
* 2. Single letters → uppercase (e.g., 'a' → 'A', 's' → 'S')
|
|
355
|
-
* 3. Function keys → uppercase (e.g., 'f1' → 'F1', 'F12' → 'F12')
|
|
356
|
-
* 4. Other keys → returned as-is (already canonical or unknown)
|
|
357
|
-
*
|
|
358
|
-
* @param key - The key name to normalize (can be an alias, lowercase, etc.)
|
|
359
|
-
* @returns The canonical key name
|
|
360
|
-
*
|
|
361
|
-
* @example
|
|
362
|
-
* ```ts
|
|
363
|
-
* normalizeKeyName('esc') // 'Escape'
|
|
364
|
-
* normalizeKeyName('a') // 'A'
|
|
365
|
-
* normalizeKeyName('f1') // 'F1'
|
|
366
|
-
* normalizeKeyName('ArrowUp') // 'ArrowUp' (already canonical)
|
|
367
|
-
* ```
|
|
287
|
+
* Maps lowercase key names and aliases to their canonical form, so lookups
|
|
288
|
+
* are case-insensitive: `'esc'`, `'Esc'`, and `'ESCAPE'` all become `'Escape'`.
|
|
289
|
+
* Canonical names come from {@link ALL_KEYS}, so validation and normalization
|
|
290
|
+
* share one spelling for every key.
|
|
368
291
|
*/
|
|
292
|
+
const CANONICAL_KEY_NAMES = /* @__PURE__ */ new Map([
|
|
293
|
+
...Array.from(ALL_KEYS, (key) => [key.toLowerCase(), key]),
|
|
294
|
+
["plus", "+"],
|
|
295
|
+
["esc", "Escape"],
|
|
296
|
+
["return", "Enter"],
|
|
297
|
+
[" ", "Space"],
|
|
298
|
+
["spacebar", "Space"],
|
|
299
|
+
["del", "Delete"],
|
|
300
|
+
["up", "ArrowUp"],
|
|
301
|
+
["down", "ArrowDown"],
|
|
302
|
+
["left", "ArrowLeft"],
|
|
303
|
+
["right", "ArrowRight"],
|
|
304
|
+
["pgup", "PageUp"],
|
|
305
|
+
["pgdn", "PageDown"]
|
|
306
|
+
]);
|
|
307
|
+
/** Whether a key is a single Unicode letter, such as `'a'` or `'é'`. */
|
|
369
308
|
function isSingleLetterKey(key) {
|
|
370
309
|
return /^\p{Letter}$/u.test(key);
|
|
371
310
|
}
|
|
@@ -385,7 +324,6 @@ function isSingleLetterKey(key) {
|
|
|
385
324
|
*/
|
|
386
325
|
function normalizeKeyName(key) {
|
|
387
326
|
if (!key) return "";
|
|
388
|
-
if (key in KEY_ALIASES) return KEY_ALIASES[key];
|
|
389
327
|
if (isSingleLetterKey(key)) {
|
|
390
328
|
const upper = key.toUpperCase();
|
|
391
329
|
return upper.length === 1 ? upper : key;
|
package/dist/format.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MODIFIER_ORDER, normalizeKeyName } from "./constants.js";
|
|
2
2
|
import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
|
|
3
3
|
import { detectPlatform } from "./platform.js";
|
|
4
|
-
import { assertLogicalKey, splitHotkeyParts } from "./_keyboard-event.js";
|
|
5
|
-
import {
|
|
4
|
+
import { assertLogicalKey, codeToLogicalKey, modifierAlias, physicalCodeOf, splitHotkeyParts } from "./_keyboard-event.js";
|
|
5
|
+
import { normalizeHotkeyFromParsed, normalizeRegisterableHotkey } from "./parse.js";
|
|
6
6
|
|
|
7
7
|
//#region src/format.ts
|
|
8
8
|
/**
|
|
@@ -55,23 +55,16 @@ function formatChordParts(hotkey, options) {
|
|
|
55
55
|
segments.sort((a, b) => modifierIndex(a) - modifierIndex(b));
|
|
56
56
|
}
|
|
57
57
|
return segments.map((segment) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const code = /^\[([A-Za-z][A-Za-z0-9]*)\]$/.exec(segment)?.[1];
|
|
58
|
+
const modifier = modifierAlias(segment);
|
|
59
|
+
if (modifier) return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
|
|
60
|
+
const code = physicalCodeOf(segment);
|
|
63
61
|
const label = options.keyLabels?.[code ?? segment];
|
|
64
62
|
if (label !== void 0) return label;
|
|
65
63
|
const numpad = code?.startsWith("Numpad") ?? false;
|
|
66
64
|
let displayKey = code ?? segment;
|
|
67
65
|
const mappedKey = code ? options.layoutMap?.get(code) : void 0;
|
|
68
66
|
if (mappedKey) displayKey = normalizeKeyName(mappedKey);
|
|
69
|
-
else if (code)
|
|
70
|
-
if (/^Key[A-Z]$/.test(code)) displayKey = code.slice(3);
|
|
71
|
-
else if (/^Digit[0-9]$/.test(code)) displayKey = code.slice(5);
|
|
72
|
-
else if (numpad) displayKey = code.slice(6);
|
|
73
|
-
else displayKey = PUNCTUATION_CODE_MAP[code] ?? code;
|
|
74
|
-
}
|
|
67
|
+
else if (code) displayKey = numpad ? code.slice(6) : codeToLogicalKey(code) ?? code;
|
|
75
68
|
const display = (keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS)[displayKey] ?? displayKey;
|
|
76
69
|
return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
|
|
77
70
|
});
|
package/dist/hotkey-manager.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { normalizeKeyName } from "./constants.js";
|
|
2
2
|
import { detectPlatform } from "./platform.js";
|
|
3
|
-
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
3
|
+
import { keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
4
4
|
import { isHotkeySequence, isModifierKey, parseHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
|
|
5
5
|
import { formatHotkey } from "./format.js";
|
|
6
|
-
import { matchKeyboardEvent } from "./_match.js";
|
|
6
|
+
import { matchKeyboardEvent, parsedHotkeysEqual } from "./_match.js";
|
|
7
7
|
import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
|
|
8
|
-
import { isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
|
|
8
|
+
import { getTargetDocument, isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
|
|
9
9
|
import { TargetListeners } from "./_target-listeners.js";
|
|
10
|
+
import { managerRef } from "./_manager-ref.js";
|
|
10
11
|
import { isRecordingEvent } from "./_recording-guard.js";
|
|
11
12
|
import { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
|
|
12
13
|
import { Store } from "@tanstack/store";
|
|
@@ -65,7 +66,7 @@ function ignoresInputs(registration) {
|
|
|
65
66
|
return ignoreInputs === "auto" ? getDefaultIgnoreInputs(resolvedSteps(registration)[0]) : ignoreInputs;
|
|
66
67
|
}
|
|
67
68
|
function sameResolvedSteps(left, right) {
|
|
68
|
-
return left.length === right.length && left.every((step, index) =>
|
|
69
|
+
return left.length === right.length && left.every((step, index) => parsedHotkeysEqual(step, right[index]));
|
|
69
70
|
}
|
|
70
71
|
function formatForMessage(hotkey) {
|
|
71
72
|
return isHotkeySequence(hotkey) ? hotkey.join(" ") : hotkey;
|
|
@@ -97,7 +98,6 @@ function formatForMessage(hotkey) {
|
|
|
97
98
|
* ```
|
|
98
99
|
*/
|
|
99
100
|
var HotkeyManager = class HotkeyManager {
|
|
100
|
-
static #instance = null;
|
|
101
101
|
/**
|
|
102
102
|
* The TanStack Store containing all hotkey registrations, chords and sequences.
|
|
103
103
|
* Use this to subscribe to registration changes or access current registrations.
|
|
@@ -125,6 +125,10 @@ var HotkeyManager = class HotkeyManager {
|
|
|
125
125
|
appHotkeys = new Store(null);
|
|
126
126
|
#platform;
|
|
127
127
|
#listeners = new TargetListeners((...args) => this.#processTargetEvent(...args));
|
|
128
|
+
/** Events that continue a sequence in progress, decided before any listener runs. */
|
|
129
|
+
#sequenceOwned = /* @__PURE__ */ new WeakMap();
|
|
130
|
+
/** Physical keys whose keydown a sequence owned, so their repeats and keyup are owned too. */
|
|
131
|
+
#sequenceOwnedKeys = /* @__PURE__ */ new Set();
|
|
128
132
|
constructor() {
|
|
129
133
|
this.#platform = detectPlatform();
|
|
130
134
|
}
|
|
@@ -132,17 +136,14 @@ var HotkeyManager = class HotkeyManager {
|
|
|
132
136
|
* Gets the singleton instance of HotkeyManager.
|
|
133
137
|
*/
|
|
134
138
|
static getInstance() {
|
|
135
|
-
|
|
136
|
-
return HotkeyManager.#instance;
|
|
139
|
+
return managerRef.current ??= new HotkeyManager();
|
|
137
140
|
}
|
|
138
141
|
/**
|
|
139
142
|
* Resets the singleton instance. Useful for testing.
|
|
140
143
|
*/
|
|
141
144
|
static resetInstance() {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
HotkeyManager.#instance = null;
|
|
145
|
-
}
|
|
145
|
+
managerRef.current?.destroy();
|
|
146
|
+
managerRef.current = null;
|
|
146
147
|
}
|
|
147
148
|
/** Notifies subscribers after in-place updates such as counts and sequence progress. */
|
|
148
149
|
#notify() {
|
|
@@ -171,44 +172,37 @@ var HotkeyManager = class HotkeyManager {
|
|
|
171
172
|
const id = generateId();
|
|
172
173
|
const platform = options.platform ?? this.#platform;
|
|
173
174
|
const target = options.target ?? document;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
kind: "sequence",
|
|
175
|
+
const sequence = isHotkeySequence(hotkey);
|
|
176
|
+
const common = {
|
|
177
177
|
id,
|
|
178
|
-
hotkey,
|
|
179
|
-
parsedSequence: hotkey.map((step) => parseHotkey(step, platform)),
|
|
180
178
|
callback,
|
|
181
179
|
options: {
|
|
182
180
|
...defaultHotkeyOptions,
|
|
183
|
-
timeout: DEFAULT_SEQUENCE_TIMEOUT,
|
|
184
|
-
...optionsForKind("sequence", options),
|
|
181
|
+
...sequence ? { timeout: DEFAULT_SEQUENCE_TIMEOUT } : { requireReset: false },
|
|
182
|
+
...optionsForKind(sequence ? "sequence" : "chord", options),
|
|
185
183
|
platform,
|
|
186
184
|
ignoreInputs: options.ignoreInputs ?? "auto"
|
|
187
185
|
},
|
|
188
186
|
target,
|
|
189
|
-
currentIndex: 0,
|
|
190
|
-
lastKeyTime: 0,
|
|
191
187
|
triggerCount: 0,
|
|
192
188
|
hasFired: false
|
|
193
189
|
};
|
|
190
|
+
let registration;
|
|
191
|
+
if (sequence) registration = {
|
|
192
|
+
...common,
|
|
193
|
+
kind: "sequence",
|
|
194
|
+
hotkey,
|
|
195
|
+
parsedSequence: hotkey.map((step) => parseHotkey(step, platform)),
|
|
196
|
+
currentIndex: 0,
|
|
197
|
+
lastKeyTime: 0
|
|
198
|
+
};
|
|
194
199
|
else {
|
|
195
200
|
const parsedHotkey = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : rawHotkeyToParsedHotkey(hotkey, platform);
|
|
196
201
|
registration = {
|
|
202
|
+
...common,
|
|
197
203
|
kind: "chord",
|
|
198
|
-
id,
|
|
199
204
|
hotkey: typeof hotkey === "string" ? hotkey : formatHotkey(parsedHotkey),
|
|
200
|
-
parsedHotkey
|
|
201
|
-
callback,
|
|
202
|
-
options: {
|
|
203
|
-
...defaultHotkeyOptions,
|
|
204
|
-
requireReset: false,
|
|
205
|
-
...optionsForKind("chord", options),
|
|
206
|
-
platform,
|
|
207
|
-
ignoreInputs: options.ignoreInputs ?? "auto"
|
|
208
|
-
},
|
|
209
|
-
target,
|
|
210
|
-
hasFired: false,
|
|
211
|
-
triggerCount: 0
|
|
205
|
+
parsedHotkey
|
|
212
206
|
};
|
|
213
207
|
}
|
|
214
208
|
const conflicting = this.#findConflictingRegistration(registration);
|
|
@@ -285,9 +279,11 @@ var HotkeyManager = class HotkeyManager {
|
|
|
285
279
|
}
|
|
286
280
|
/**
|
|
287
281
|
* Processes keyboard events for a specific target and event type.
|
|
288
|
-
*
|
|
282
|
+
* A key that continues a sequence in progress belongs to that sequence, so
|
|
283
|
+
* chords skip it. Chords and first steps of sequences may both fire.
|
|
289
284
|
*/
|
|
290
|
-
#processTargetEvent(event, target, eventType, capture, ids) {
|
|
285
|
+
#processTargetEvent(event, target, eventType, capture, ids, first) {
|
|
286
|
+
if (first) this.#sequenceOwned.set(event, this.#isOwnedBySequence(event, eventType));
|
|
291
287
|
this.#processChords(event, target, eventType, capture, this.#registrationsIn(ids, capture, "chord"));
|
|
292
288
|
this.#processSequences(event, target, eventType, capture, this.#registrationsIn(ids, capture, "sequence"));
|
|
293
289
|
}
|
|
@@ -298,7 +294,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
298
294
|
registration.activeMatch = void 0;
|
|
299
295
|
}
|
|
300
296
|
}
|
|
301
|
-
if (isRecordingEvent(event)) return;
|
|
297
|
+
if (isRecordingEvent(event) || this.#sequenceOwned.get(event)) return;
|
|
302
298
|
const matches = /* @__PURE__ */ new Map();
|
|
303
299
|
let bestScore = 0;
|
|
304
300
|
for (const registration of registrations) {
|
|
@@ -314,21 +310,48 @@ var HotkeyManager = class HotkeyManager {
|
|
|
314
310
|
if (!match?.matched || match.score !== bestScore) continue;
|
|
315
311
|
const registration = this.#currentIn(snapshot, capture);
|
|
316
312
|
if (!registration) continue;
|
|
313
|
+
this.#consume(registration, event);
|
|
317
314
|
if (eventType === "keyup") {
|
|
318
|
-
this.#
|
|
315
|
+
this.#fire(registration, event);
|
|
319
316
|
continue;
|
|
320
317
|
}
|
|
321
|
-
if (registration.options.preventDefault) event.preventDefault();
|
|
322
|
-
if (registration.options.stopPropagation) event.stopPropagation();
|
|
323
318
|
if (!registration.options.requireReset || !registration.hasFired) {
|
|
324
319
|
if (registration.options.requireReset) {
|
|
325
320
|
registration.hasFired = true;
|
|
326
321
|
registration.activeMatch = match.identity;
|
|
327
322
|
}
|
|
328
|
-
this.#
|
|
323
|
+
this.#fire(registration, event);
|
|
329
324
|
}
|
|
330
325
|
}
|
|
331
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Whether the event continues or completes a sequence in progress whose
|
|
329
|
+
* target the event reaches. The repeats and keyup of a keydown that a
|
|
330
|
+
* sequence owns belong to the sequence too.
|
|
331
|
+
*/
|
|
332
|
+
#isOwnedBySequence(event, eventType) {
|
|
333
|
+
const key = event.code || normalizeKeyName(event.key);
|
|
334
|
+
if (eventType === "keyup") {
|
|
335
|
+
const ownedKeydown = this.#sequenceOwnedKeys.delete(key);
|
|
336
|
+
return this.#continuesSequence(event, eventType) || ownedKeydown;
|
|
337
|
+
}
|
|
338
|
+
if (event.repeat) return this.#sequenceOwnedKeys.has(key);
|
|
339
|
+
const owned = this.#continuesSequence(event, eventType);
|
|
340
|
+
if (owned) this.#sequenceOwnedKeys.add(key);
|
|
341
|
+
else this.#sequenceOwnedKeys.delete(key);
|
|
342
|
+
return owned;
|
|
343
|
+
}
|
|
344
|
+
#continuesSequence(event, eventType) {
|
|
345
|
+
if (isRecordingEvent(event) || normalizeKeyboardEvent(event).isComposing || isModifierKey(normalizeKeyName(event.key))) return false;
|
|
346
|
+
const path = event.composedPath();
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
for (const registration of this.registrations.state.values()) {
|
|
349
|
+
if (registration.kind !== "sequence" || registration.currentIndex === 0 || !registration.options.enabled || registration.options.eventType !== eventType || now - registration.lastKeyTime > (registration.options.timeout ?? 1e3)) continue;
|
|
350
|
+
const target = registration.target;
|
|
351
|
+
if (path.includes(target.nodeType === 1 ? target : getTargetDocument(target)) && !(ignoresInputs(registration) && shouldIgnoreInputEvent(event, target, target)) && matchKeyboardEvent(event, registration.parsedSequence[registration.currentIndex], registration.options.platform).matched) return true;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
332
355
|
#processSequences(event, target, eventType, capture, registrations) {
|
|
333
356
|
if (registrations.length === 0) return;
|
|
334
357
|
if (isRecordingEvent(event)) {
|
|
@@ -376,8 +399,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
376
399
|
registration.currentIndex = candidate.nextIndex;
|
|
377
400
|
changed = true;
|
|
378
401
|
if (registration.currentIndex >= registration.parsedSequence.length) {
|
|
379
|
-
|
|
380
|
-
if (registration.options.stopPropagation) event.stopPropagation();
|
|
402
|
+
this.#consume(registration, event);
|
|
381
403
|
registration.currentIndex = 0;
|
|
382
404
|
completed.push(registration);
|
|
383
405
|
}
|
|
@@ -389,19 +411,20 @@ var HotkeyManager = class HotkeyManager {
|
|
|
389
411
|
if (changed && completed.length === 0) this.#notify();
|
|
390
412
|
for (const snapshot of completed) {
|
|
391
413
|
const registration = this.#currentIn(snapshot, capture);
|
|
392
|
-
if (registration) this.#
|
|
414
|
+
if (registration) this.#fire(registration, event);
|
|
393
415
|
}
|
|
394
416
|
}
|
|
395
|
-
/**
|
|
396
|
-
|
|
397
|
-
*/
|
|
398
|
-
#executeCallback(registration, event) {
|
|
417
|
+
/** Applies a matched registration's preventDefault and stopPropagation options. */
|
|
418
|
+
#consume(registration, event) {
|
|
399
419
|
if (registration.options.preventDefault) event.preventDefault();
|
|
400
420
|
if (registration.options.stopPropagation) event.stopPropagation();
|
|
421
|
+
}
|
|
422
|
+
/** Counts a trigger, notifies subscribers (such as devtools), and runs the callback. */
|
|
423
|
+
#fire(registration, event, context = this.#callbackContext(registration)) {
|
|
401
424
|
registration.triggerCount++;
|
|
402
425
|
if (registration.kind === "sequence") registration.hasFired = true;
|
|
403
426
|
this.#notify();
|
|
404
|
-
registration.callback(event,
|
|
427
|
+
registration.callback(event, context);
|
|
405
428
|
}
|
|
406
429
|
#callbackContext(registration) {
|
|
407
430
|
return {
|
|
@@ -426,17 +449,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
426
449
|
const parsed = registration.parsedHotkey;
|
|
427
450
|
const releasedKey = normalizeKeyName(event.key);
|
|
428
451
|
if (registration.activeMatch?.code && event.code === registration.activeMatch.code) return true;
|
|
429
|
-
|
|
430
|
-
if (event.code === parsed.code) return true;
|
|
431
|
-
} else {
|
|
432
|
-
const parsedKeyNormalized = parsed.key.length === 1 ? parsed.key.toUpperCase() : parsed.key;
|
|
433
|
-
if ((releasedKey.length === 1 ? releasedKey.toUpperCase() : releasedKey) === parsedKeyNormalized) return true;
|
|
434
|
-
}
|
|
435
|
-
if (parsed.ctrl && releasedKey === "Control") return true;
|
|
436
|
-
if (parsed.shift && releasedKey === "Shift") return true;
|
|
437
|
-
if (parsed.alt && releasedKey === "Alt") return true;
|
|
438
|
-
if (parsed.meta && releasedKey === "Meta") return true;
|
|
439
|
-
return false;
|
|
452
|
+
return (parsed.code !== void 0 ? event.code === parsed.code : keysEqual(releasedKey, parsed.key)) || parsed.modifiers.includes(releasedKey);
|
|
440
453
|
}
|
|
441
454
|
/**
|
|
442
455
|
* Resets the progress of every in-progress sequence.
|
|
@@ -470,10 +483,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
470
483
|
bubbles: true,
|
|
471
484
|
cancelable: true
|
|
472
485
|
});
|
|
473
|
-
registration
|
|
474
|
-
if (registration.kind === "sequence") registration.hasFired = true;
|
|
475
|
-
this.#notify();
|
|
476
|
-
registration.callback(syntheticEvent, context);
|
|
486
|
+
this.#fire(registration, syntheticEvent, context);
|
|
477
487
|
return true;
|
|
478
488
|
}
|
|
479
489
|
/**
|
|
@@ -1,7 +1,37 @@
|
|
|
1
|
-
import { Hotkey, HotkeyChord, HotkeySequence } from "./hotkey.types.js";
|
|
2
|
-
import {
|
|
1
|
+
import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkey } from "./hotkey.types.js";
|
|
2
|
+
import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.js";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/hotkey-recorder.d.ts
|
|
5
|
+
/** Event property used to record a binding. Physical codes are the default. */
|
|
6
|
+
export type RecorderKeyMode = 'code' | 'key';
|
|
7
|
+
export interface RecorderRejection {
|
|
8
|
+
reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation';
|
|
9
|
+
message: string;
|
|
10
|
+
/** The rejected chord or sequence, when one was recorded. */
|
|
11
|
+
hotkey?: Hotkey;
|
|
12
|
+
conflicts?: Array<HotkeyConflict>;
|
|
13
|
+
}
|
|
14
|
+
export interface RecorderOptions {
|
|
15
|
+
/** Physical code recording is the default. Choose key to record logical characters. */
|
|
16
|
+
recordBy?: RecorderKeyMode;
|
|
17
|
+
/** Platform used for Mod and AltGraph handling. Defaults to platform detection. */
|
|
18
|
+
platform?: 'mac' | 'windows' | 'linux';
|
|
19
|
+
/** Opt in to live-registry conflicts, including sequence prefixes. Default: false. */
|
|
20
|
+
detectConflicts?: boolean | HotkeyConflictOptions;
|
|
21
|
+
/** Rejected candidates leave recording active. */
|
|
22
|
+
onReject?: (rejection: RecorderRejection) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Validation context for a recorded chord or sequence. */
|
|
25
|
+
export interface HotkeyRecorderValidationContext {
|
|
26
|
+
/** The event that recorded the last chord. */
|
|
27
|
+
event: KeyboardEvent;
|
|
28
|
+
/** The last chord, parsed. */
|
|
29
|
+
parsedHotkey: ParsedHotkey;
|
|
30
|
+
/** The event that recorded each step. */
|
|
31
|
+
events: ReadonlyArray<KeyboardEvent>;
|
|
32
|
+
/** Every step, parsed. */
|
|
33
|
+
parsedSequence: ReadonlyArray<ParsedHotkey>;
|
|
34
|
+
}
|
|
5
35
|
/**
|
|
6
36
|
* State interface for the HotkeyRecorder.
|
|
7
37
|
*/
|
package/dist/hotkey.types.d.ts
CHANGED
|
@@ -127,6 +127,19 @@ export type ParsedHotkey = ParsedModifiers & ({
|
|
|
127
127
|
code: string;
|
|
128
128
|
key?: never;
|
|
129
129
|
});
|
|
130
|
+
/** Modifier flags shared by logical-key and physical-code registrations. */
|
|
131
|
+
export interface RawModifiers {
|
|
132
|
+
/** Platform-adaptive modifier: Command on macOS, Control on Windows/Linux. Defaults to false. */
|
|
133
|
+
mod?: boolean;
|
|
134
|
+
/** Whether the Control key is required. Defaults to false. */
|
|
135
|
+
ctrl?: boolean;
|
|
136
|
+
/** Whether the Shift key is required. Defaults to false. */
|
|
137
|
+
shift?: boolean;
|
|
138
|
+
/** Whether the Alt key is required. Defaults to false. */
|
|
139
|
+
alt?: boolean;
|
|
140
|
+
/** Whether the Meta (Command) key is required. Defaults to false. */
|
|
141
|
+
meta?: boolean;
|
|
142
|
+
}
|
|
130
143
|
/**
|
|
131
144
|
* A raw hotkey object for programmatic registration.
|
|
132
145
|
*
|
|
@@ -149,20 +162,6 @@ export type ParsedHotkey = ParsedModifiers & ({
|
|
|
149
162
|
* useHotkey({ code: 'NumpadAdd', mod: true }, handler) // Physical numpad plus
|
|
150
163
|
* ```
|
|
151
164
|
*/
|
|
152
|
-
/** Modifier flags shared by logical-key and physical-code registrations. */
|
|
153
|
-
export interface RawModifiers {
|
|
154
|
-
/** The non-modifier key (e.g., 'S', 'Escape', 'F1'). */
|
|
155
|
-
/** Platform-adaptive modifier: Command on macOS, Control on Windows/Linux. Defaults to false. */
|
|
156
|
-
mod?: boolean;
|
|
157
|
-
/** Whether the Control key is required. Defaults to false. */
|
|
158
|
-
ctrl?: boolean;
|
|
159
|
-
/** Whether the Shift key is required. Defaults to false. */
|
|
160
|
-
shift?: boolean;
|
|
161
|
-
/** Whether the Alt key is required. Defaults to false. */
|
|
162
|
-
alt?: boolean;
|
|
163
|
-
/** Whether the Meta (Command) key is required. Defaults to false. */
|
|
164
|
-
meta?: boolean;
|
|
165
|
-
}
|
|
166
165
|
export type RawHotkey = ({
|
|
167
166
|
key: LogicalKey | (string & {});
|
|
168
167
|
code?: never;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,5 @@ import { CreateHotkeyHandlerOptions, DEFAULT_SEQUENCE_TIMEOUT, KeyboardEventMatc
|
|
|
12
12
|
import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
|
|
13
13
|
import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./conflicts.js";
|
|
14
14
|
import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
|
|
15
|
-
import { HotkeyRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./recorder
|
|
16
|
-
import { HotkeyChordRecorderOptions, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeySequenceRecorderOptions } from "./hotkey-recorder.js";
|
|
15
|
+
import { HotkeyChordRecorderOptions, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeySequenceRecorderOptions, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./hotkey-recorder.js";
|
|
17
16
|
export { ALL_KEYS, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkey, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyChord, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequence, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkey, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkey, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisteredAppHotkeys, SingleModifierHotkey, Target, ThreeModifierHotkey, TwoModifierHotkey, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, provideAppHotkeys, publishAppHotkeysStore, rawHotkeyToParsedHotkey, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
|
|
@@ -53,10 +53,8 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
53
53
|
*/
|
|
54
54
|
store = new Store(getDefaultKeyStateTrackerState());
|
|
55
55
|
#heldEntries = /* @__PURE__ */ new Map();
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
#mouseListener = null;
|
|
59
|
-
#blurListener = null;
|
|
56
|
+
/** Window listeners as `[type, listener, capture]`, removed together on destroy. */
|
|
57
|
+
#listeners = [];
|
|
60
58
|
constructor() {
|
|
61
59
|
this.#setupListeners();
|
|
62
60
|
}
|
|
@@ -81,7 +79,7 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
81
79
|
*/
|
|
82
80
|
#setupListeners() {
|
|
83
81
|
if (typeof document === "undefined") return;
|
|
84
|
-
|
|
82
|
+
const onKeydown = (event) => {
|
|
85
83
|
let changed = this.#reconcileModifiers(event);
|
|
86
84
|
const key = normalizeKeyName(event.key);
|
|
87
85
|
const identity = event.code || `key:${key}`;
|
|
@@ -94,7 +92,7 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
94
92
|
}
|
|
95
93
|
if (changed) this.#syncState();
|
|
96
94
|
};
|
|
97
|
-
|
|
95
|
+
const onKeyup = (event) => {
|
|
98
96
|
const key = normalizeKeyName(event.key);
|
|
99
97
|
const identity = event.code || `key:${key}`;
|
|
100
98
|
this.#heldEntries.delete(identity);
|
|
@@ -107,20 +105,43 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
107
105
|
}
|
|
108
106
|
this.#syncState();
|
|
109
107
|
};
|
|
110
|
-
|
|
108
|
+
const onMouse = (event) => {
|
|
111
109
|
if (this.#reconcileModifiers(event)) this.#syncState();
|
|
112
110
|
};
|
|
113
|
-
|
|
111
|
+
const onBlur = () => {
|
|
114
112
|
if (this.#heldEntries.size > 0) {
|
|
115
113
|
this.#heldEntries.clear();
|
|
116
114
|
this.#syncState();
|
|
117
115
|
}
|
|
118
116
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
this.#listeners = [
|
|
118
|
+
[
|
|
119
|
+
"keydown",
|
|
120
|
+
onKeydown,
|
|
121
|
+
true
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
"keyup",
|
|
125
|
+
onKeyup,
|
|
126
|
+
true
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"mousemove",
|
|
130
|
+
onMouse,
|
|
131
|
+
true
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"mousedown",
|
|
135
|
+
onMouse,
|
|
136
|
+
true
|
|
137
|
+
],
|
|
138
|
+
[
|
|
139
|
+
"blur",
|
|
140
|
+
onBlur,
|
|
141
|
+
false
|
|
142
|
+
]
|
|
143
|
+
];
|
|
144
|
+
for (const [type, listener, capture] of this.#listeners) window.addEventListener(type, listener, capture);
|
|
124
145
|
}
|
|
125
146
|
/**
|
|
126
147
|
* Removes modifiers whose release was missed without a window blur.
|
|
@@ -153,24 +174,8 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
153
174
|
* Removes the keyboard event listeners.
|
|
154
175
|
*/
|
|
155
176
|
#removeListeners() {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
window.removeEventListener("keydown", this.#keydownListener, true);
|
|
159
|
-
this.#keydownListener = null;
|
|
160
|
-
}
|
|
161
|
-
if (this.#keyupListener) {
|
|
162
|
-
window.removeEventListener("keyup", this.#keyupListener, true);
|
|
163
|
-
this.#keyupListener = null;
|
|
164
|
-
}
|
|
165
|
-
if (this.#mouseListener) {
|
|
166
|
-
window.removeEventListener("mousemove", this.#mouseListener, true);
|
|
167
|
-
window.removeEventListener("mousedown", this.#mouseListener, true);
|
|
168
|
-
this.#mouseListener = null;
|
|
169
|
-
}
|
|
170
|
-
if (this.#blurListener) {
|
|
171
|
-
window.removeEventListener("blur", this.#blurListener);
|
|
172
|
-
this.#blurListener = null;
|
|
173
|
-
}
|
|
177
|
+
for (const [type, listener, capture] of this.#listeners) window.removeEventListener(type, listener, capture);
|
|
178
|
+
this.#listeners = [];
|
|
174
179
|
}
|
|
175
180
|
/**
|
|
176
181
|
* Gets an array of currently held key names.
|
package/dist/match.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { normalizeKeyName } from "./constants.js";
|
|
2
2
|
import { detectPlatform } from "./platform.js";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
4
4
|
import { isHotkeySequence, isModifierKey, parseHotkey, parseRegisterableHotkey } from "./parse.js";
|
|
5
|
-
import {
|
|
5
|
+
import { formatHotkey } from "./format.js";
|
|
6
|
+
import { matchKeyboardEvent, parsedHotkeysEqual } from "./_match.js";
|
|
6
7
|
|
|
7
8
|
//#region src/match.ts
|
|
8
9
|
/**
|
|
@@ -58,7 +59,7 @@ function matchesKeyboardEvent(event, hotkey, platform = detectPlatform()) {
|
|
|
58
59
|
function createHotkeyHandler(hotkey, callback, options = {}) {
|
|
59
60
|
const { preventDefault = true, stopPropagation = true, platform } = options;
|
|
60
61
|
const resolvedPlatform = platform ?? detectPlatform();
|
|
61
|
-
const hotkeyString = typeof hotkey === "string" ? hotkey :
|
|
62
|
+
const hotkeyString = typeof hotkey === "string" ? hotkey : formatHotkey(hotkey);
|
|
62
63
|
const parsed = typeof hotkey === "string" ? parseHotkey(hotkey, resolvedPlatform) : hotkey;
|
|
63
64
|
const context = {
|
|
64
65
|
hotkey: hotkeyString,
|
|
@@ -114,19 +115,6 @@ function createMultiHotkeyHandler(handlers, options = {}) {
|
|
|
114
115
|
};
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
117
|
-
* Formats a ParsedHotkey back to a hotkey string.
|
|
118
|
-
* Used internally to provide the hotkey string in callback context.
|
|
119
|
-
*/
|
|
120
|
-
function formatParsedHotkey(parsed) {
|
|
121
|
-
const parts = [];
|
|
122
|
-
if (parsed.ctrl) parts.push("Control");
|
|
123
|
-
if (parsed.alt) parts.push("Alt");
|
|
124
|
-
if (parsed.shift) parts.push("Shift");
|
|
125
|
-
if (parsed.meta) parts.push("Meta");
|
|
126
|
-
parts.push(parsed.code !== void 0 ? `[${parsed.code}]` : parsed.key);
|
|
127
|
-
return parts.join("+");
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
118
|
* Compares normalized hotkey identity, including all four modifiers.
|
|
131
119
|
* Sequences are equal when every step is equal; a one-step sequence equals its chord.
|
|
132
120
|
* Aliases resolve on the supplied platform. A physical code and a logical key
|
|
@@ -135,12 +123,7 @@ function formatParsedHotkey(parsed) {
|
|
|
135
123
|
function areHotkeysEqual(left, right, platform = detectPlatform()) {
|
|
136
124
|
const a = isHotkeySequence(left) ? left : [left];
|
|
137
125
|
const b = isHotkeySequence(right) ? right : [right];
|
|
138
|
-
return a.length === b.length && a.every((step, index) =>
|
|
139
|
-
}
|
|
140
|
-
function areChordsEqual(left, right, platform) {
|
|
141
|
-
const a = parseRegisterableHotkey(left, platform);
|
|
142
|
-
const b = parseRegisterableHotkey(right, platform);
|
|
143
|
-
return 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));
|
|
126
|
+
return a.length === b.length && a.every((step, index) => parsedHotkeysEqual(parseRegisterableHotkey(step, platform), parseRegisterableHotkey(b[index], platform)));
|
|
144
127
|
}
|
|
145
128
|
/** Default timeout between sequence steps (in milliseconds). */
|
|
146
129
|
const DEFAULT_SEQUENCE_TIMEOUT = 1e3;
|
package/dist/parse.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Key } from "./key.types.js";
|
|
2
2
|
import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkey, RawHotkey, RegisterableHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
|
|
3
|
-
import { MODIFIER_ALIASES } from "./constants.js";
|
|
4
3
|
//#region src/parse.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
|
|
@@ -49,6 +48,12 @@ export declare function parseHotkey(hotkey: HotkeyChord | (string & {}), platfor
|
|
|
49
48
|
* ```
|
|
50
49
|
*/
|
|
51
50
|
export declare function rawHotkeyToParsedHotkey(raw: RawHotkey, platform?: 'mac' | 'windows' | 'linux'): ParsedHotkey;
|
|
51
|
+
/**
|
|
52
|
+
* Same canonical string as {@link normalizeHotkey}, but from an already-parsed
|
|
53
|
+
* hotkey: `Mod` first when the platform allows, then `Alt`, then `Shift`;
|
|
54
|
+
* otherwise full `Control+Alt+Shift+Meta` order.
|
|
55
|
+
*/
|
|
56
|
+
export declare function normalizeHotkeyFromParsed(parsed: ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
|
|
52
57
|
/**
|
|
53
58
|
* Normalizes a hotkey string to its canonical form.
|
|
54
59
|
*
|
|
@@ -70,10 +75,6 @@ export declare function rawHotkeyToParsedHotkey(raw: RawHotkey, platform?: 'mac'
|
|
|
70
75
|
* ```
|
|
71
76
|
*/
|
|
72
77
|
export declare function normalizeHotkey(hotkey: Key | (string & {}), platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
|
|
73
|
-
/**
|
|
74
|
-
* Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
|
|
75
|
-
*/
|
|
76
|
-
export declare function normalizeHotkeyFromParsed(parsed: ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
|
|
77
78
|
/**
|
|
78
79
|
* Normalizes a chord (string or {@link RawHotkey} object) or a sequence to canonical form.
|
|
79
80
|
* Each sequence step is normalized, and a one-step sequence becomes its chord.
|
|
@@ -90,7 +91,7 @@ export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkey,
|
|
|
90
91
|
* @param key - Key name or alias (e.g. from a hotkey string or `event.key`)
|
|
91
92
|
* @returns True if the string is a recognized modifier
|
|
92
93
|
*/
|
|
93
|
-
export declare function isModifierKey(key: Key | (string & {})):
|
|
94
|
+
export declare function isModifierKey(key: Key | (string & {})): boolean;
|
|
94
95
|
/**
|
|
95
96
|
* Parses a KeyboardEvent into a ParsedHotkey object.
|
|
96
97
|
*
|
package/dist/parse.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MODIFIER_ORDER, normalizeKeyName } from "./constants.js";
|
|
2
2
|
import { detectPlatform, resolveModifier } from "./platform.js";
|
|
3
|
-
import { assertLogicalKey, normalizeKeyboardEvent, splitHotkeyParts } from "./_keyboard-event.js";
|
|
3
|
+
import { assertLogicalKey, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitHotkeyParts } from "./_keyboard-event.js";
|
|
4
4
|
|
|
5
5
|
//#region src/parse.ts
|
|
6
|
+
/** Builds a ParsedHotkey, listing held modifiers in canonical order. */
|
|
7
|
+
function toParsedHotkey(identity, ctrl, shift, alt, meta) {
|
|
8
|
+
const held = {
|
|
9
|
+
Control: ctrl,
|
|
10
|
+
Alt: alt,
|
|
11
|
+
Shift: shift,
|
|
12
|
+
Meta: meta
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
...identity,
|
|
16
|
+
ctrl,
|
|
17
|
+
shift,
|
|
18
|
+
alt,
|
|
19
|
+
meta,
|
|
20
|
+
modifiers: MODIFIER_ORDER.filter((modifier) => held[modifier])
|
|
21
|
+
};
|
|
22
|
+
}
|
|
6
23
|
/**
|
|
7
24
|
* Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
|
|
8
25
|
* does not narrow readonly arrays.
|
|
@@ -35,7 +52,7 @@ function parseHotkey(hotkey, platform = detectPlatform()) {
|
|
|
35
52
|
const part = parts[i].trim();
|
|
36
53
|
if (i === parts.length - 1) key = normalizeKeyName(part);
|
|
37
54
|
else {
|
|
38
|
-
const alias =
|
|
55
|
+
const alias = modifierAlias(part);
|
|
39
56
|
if (alias) {
|
|
40
57
|
const resolved = resolveModifier(alias, platform);
|
|
41
58
|
modifiers.add(resolved);
|
|
@@ -43,15 +60,8 @@ function parseHotkey(hotkey, platform = detectPlatform()) {
|
|
|
43
60
|
}
|
|
44
61
|
}
|
|
45
62
|
if (!key && parts.length > 0) key = normalizeKeyName(parts[parts.length - 1].trim());
|
|
46
|
-
const code =
|
|
47
|
-
return {
|
|
48
|
-
...code !== void 0 ? { code } : { key },
|
|
49
|
-
ctrl: modifiers.has("Control"),
|
|
50
|
-
shift: modifiers.has("Shift"),
|
|
51
|
-
alt: modifiers.has("Alt"),
|
|
52
|
-
meta: modifiers.has("Meta"),
|
|
53
|
-
modifiers: MODIFIER_ORDER.filter((m) => modifiers.has(m))
|
|
54
|
-
};
|
|
63
|
+
const code = physicalCodeOf(key);
|
|
64
|
+
return toParsedHotkey(code !== void 0 ? { code } : { key }, modifiers.has("Control"), modifiers.has("Shift"), modifiers.has("Alt"), modifiers.has("Meta"));
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
57
67
|
* Converts a RawHotkey object to a ParsedHotkey.
|
|
@@ -84,30 +94,14 @@ function rawHotkeyToParsedHotkey(raw, platform = detectPlatform()) {
|
|
|
84
94
|
if (resolveModifier("Mod", platform) === "Control") ctrl = true;
|
|
85
95
|
else meta = true;
|
|
86
96
|
}
|
|
87
|
-
|
|
88
|
-
switch (m) {
|
|
89
|
-
case "Control": return ctrl;
|
|
90
|
-
case "Shift": return shift;
|
|
91
|
-
case "Alt": return alt;
|
|
92
|
-
case "Meta": return meta;
|
|
93
|
-
default: return false;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
return {
|
|
97
|
-
...raw.code !== void 0 ? { code: raw.code } : { key: normalizeKeyName(raw.key) },
|
|
98
|
-
ctrl,
|
|
99
|
-
shift,
|
|
100
|
-
alt,
|
|
101
|
-
meta,
|
|
102
|
-
modifiers
|
|
103
|
-
};
|
|
97
|
+
return toParsedHotkey(raw.code !== void 0 ? { code: raw.code } : { key: normalizeKeyName(raw.key) }, ctrl, shift, alt, meta);
|
|
104
98
|
}
|
|
105
99
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
100
|
+
* Same canonical string as {@link normalizeHotkey}, but from an already-parsed
|
|
101
|
+
* hotkey: `Mod` first when the platform allows, then `Alt`, then `Shift`;
|
|
108
102
|
* otherwise full `Control+Alt+Shift+Meta` order.
|
|
109
103
|
*/
|
|
110
|
-
function
|
|
104
|
+
function normalizeHotkeyFromParsed(parsed, platform = detectPlatform()) {
|
|
111
105
|
if (parsed.code === void 0) assertLogicalKey(parsed.key);
|
|
112
106
|
const canUseMod = platform === "mac" ? parsed.meta && !parsed.ctrl : parsed.ctrl && !parsed.meta;
|
|
113
107
|
const parts = [];
|
|
@@ -140,13 +134,7 @@ function normalizedHotkeyStringFromParsed(parsed, platform) {
|
|
|
140
134
|
* ```
|
|
141
135
|
*/
|
|
142
136
|
function normalizeHotkey(hotkey, platform = detectPlatform()) {
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
|
|
147
|
-
*/
|
|
148
|
-
function normalizeHotkeyFromParsed(parsed, platform = detectPlatform()) {
|
|
149
|
-
return normalizedHotkeyStringFromParsed(parsed, platform);
|
|
137
|
+
return normalizeHotkeyFromParsed(parseHotkey(hotkey, platform), platform);
|
|
150
138
|
}
|
|
151
139
|
function normalizeRegisterableHotkey(hotkey, platform = detectPlatform()) {
|
|
152
140
|
if (isHotkeySequence(hotkey)) {
|
|
@@ -165,7 +153,7 @@ function normalizeRegisterableHotkey(hotkey, platform = detectPlatform()) {
|
|
|
165
153
|
* @returns True if the string is a recognized modifier
|
|
166
154
|
*/
|
|
167
155
|
function isModifierKey(key) {
|
|
168
|
-
return key
|
|
156
|
+
return modifierAlias(key) !== void 0;
|
|
169
157
|
}
|
|
170
158
|
/**
|
|
171
159
|
* Parses a KeyboardEvent into a ParsedHotkey object.
|
|
@@ -190,19 +178,7 @@ function parseKeyboardEvent(event, platform) {
|
|
|
190
178
|
const ctrl = normalized.altGraph ? false : normalized.ctrl;
|
|
191
179
|
const alt = normalized.altGraph ? false : normalized.alt;
|
|
192
180
|
const shift = normalized.shift;
|
|
193
|
-
|
|
194
|
-
if (ctrl) modifiers.push("Control");
|
|
195
|
-
if (alt) modifiers.push("Alt");
|
|
196
|
-
if (shift) modifiers.push("Shift");
|
|
197
|
-
if (normalized.meta) modifiers.push("Meta");
|
|
198
|
-
return {
|
|
199
|
-
key: normalized.key,
|
|
200
|
-
ctrl,
|
|
201
|
-
shift,
|
|
202
|
-
alt,
|
|
203
|
-
meta: normalized.meta,
|
|
204
|
-
modifiers
|
|
205
|
-
};
|
|
181
|
+
return toParsedHotkey({ key: normalized.key }, ctrl, shift, alt, normalized.meta);
|
|
206
182
|
}
|
|
207
183
|
/**
|
|
208
184
|
* Normalizes a keyboard event to the same canonical hotkey string as {@link normalizeHotkey}.
|
|
@@ -211,7 +187,7 @@ function parseKeyboardEvent(event, platform) {
|
|
|
211
187
|
* @param platform - Target platform for `Mod` eligibility
|
|
212
188
|
*/
|
|
213
189
|
function normalizeHotkeyFromEvent(event, platform = detectPlatform()) {
|
|
214
|
-
return
|
|
190
|
+
return normalizeHotkeyFromParsed(parseKeyboardEvent(event, platform), platform);
|
|
215
191
|
}
|
|
216
192
|
/**
|
|
217
193
|
* Checks if a hotkey or ParsedHotkey contains at least one non-modifier key.
|
package/dist/validate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ALL_KEYS,
|
|
2
|
-
import { splitHotkeyParts } from "./_keyboard-event.js";
|
|
1
|
+
import { ALL_KEYS, normalizeKeyName } from "./constants.js";
|
|
2
|
+
import { modifierAlias, physicalCodeOf, splitHotkeyParts } from "./_keyboard-event.js";
|
|
3
3
|
|
|
4
4
|
//#region src/validate.ts
|
|
5
5
|
/**
|
|
@@ -60,34 +60,18 @@ function validateChord(hotkey) {
|
|
|
60
60
|
};
|
|
61
61
|
const modifierParts = parts.slice(0, -1);
|
|
62
62
|
const keyPart = parts[parts.length - 1];
|
|
63
|
-
for (const modifier of modifierParts) if (!(
|
|
64
|
-
if (keyPart.length > 1 && (keyPart.startsWith("[") || keyPart.endsWith("]")) &&
|
|
65
|
-
if (!isKnownKey(normalizeKeyName(keyPart))
|
|
63
|
+
for (const modifier of modifierParts) if (!modifierAlias(modifier)) errors.push(`Unknown modifier: '${modifier}'`);
|
|
64
|
+
if (keyPart.length > 1 && (keyPart.startsWith("[") || keyPart.endsWith("]")) && physicalCodeOf(keyPart) === void 0) errors.push("Invalid physical key: use a bracketed KeyboardEvent.code, such as [KeyS]");
|
|
65
|
+
if (!isKnownKey(normalizeKeyName(keyPart))) warnings.push(`Unknown key: '${keyPart}'. This may still work but won't have type-safe autocomplete.`);
|
|
66
66
|
return {
|
|
67
67
|
valid: errors.length === 0,
|
|
68
68
|
warnings,
|
|
69
69
|
errors
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Checks if a key is in the known keys set.
|
|
74
|
-
*/
|
|
72
|
+
/** Physical codes, single characters, and named keys are known. */
|
|
75
73
|
function isKnownKey(key) {
|
|
76
|
-
|
|
77
|
-
if (Array.from(key).length === 1) return true;
|
|
78
|
-
if (ALL_KEYS.has(key)) return true;
|
|
79
|
-
if (key.length === 1 && ALL_KEYS.has(key.toUpperCase())) return true;
|
|
80
|
-
return key in {
|
|
81
|
-
Esc: true,
|
|
82
|
-
Return: true,
|
|
83
|
-
Space: true,
|
|
84
|
-
" ": true,
|
|
85
|
-
Del: true,
|
|
86
|
-
Up: true,
|
|
87
|
-
Down: true,
|
|
88
|
-
Left: true,
|
|
89
|
-
Right: true
|
|
90
|
-
};
|
|
74
|
+
return physicalCodeOf(key) !== void 0 || Array.from(key).length === 1 || ALL_KEYS.has(key);
|
|
91
75
|
}
|
|
92
76
|
/**
|
|
93
77
|
* Validates a hotkey and throws an error if invalid.
|
package/package.json
CHANGED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Hotkey, ParsedHotkey } from "./hotkey.types.js";
|
|
2
|
-
import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.js";
|
|
3
|
-
//#region src/recorder-options.d.ts
|
|
4
|
-
/** Event property used to record a binding. Physical codes are the default. */
|
|
5
|
-
export type RecorderKeyMode = 'code' | 'key';
|
|
6
|
-
export interface RecorderRejection {
|
|
7
|
-
reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation';
|
|
8
|
-
message: string;
|
|
9
|
-
/** The rejected chord or sequence, when one was recorded. */
|
|
10
|
-
hotkey?: Hotkey;
|
|
11
|
-
conflicts?: Array<HotkeyConflict>;
|
|
12
|
-
}
|
|
13
|
-
export interface RecorderOptions {
|
|
14
|
-
/** Physical code recording is the default. Choose key to record logical characters. */
|
|
15
|
-
recordBy?: RecorderKeyMode;
|
|
16
|
-
/** Platform used for Mod and AltGraph handling. Defaults to platform detection. */
|
|
17
|
-
platform?: 'mac' | 'windows' | 'linux';
|
|
18
|
-
/** Opt in to live-registry conflicts, including sequence prefixes. Default: false. */
|
|
19
|
-
detectConflicts?: boolean | HotkeyConflictOptions;
|
|
20
|
-
/** Rejected candidates leave recording active. */
|
|
21
|
-
onReject?: (rejection: RecorderRejection) => void;
|
|
22
|
-
}
|
|
23
|
-
/** Validation context for a recorded chord or sequence. */
|
|
24
|
-
export interface HotkeyRecorderValidationContext {
|
|
25
|
-
/** The event that recorded the last chord. */
|
|
26
|
-
event: KeyboardEvent;
|
|
27
|
-
/** The last chord, parsed. */
|
|
28
|
-
parsedHotkey: ParsedHotkey;
|
|
29
|
-
/** The event that recorded each step. */
|
|
30
|
-
events: ReadonlyArray<KeyboardEvent>;
|
|
31
|
-
/** Every step, parsed. */
|
|
32
|
-
parsedSequence: ReadonlyArray<ParsedHotkey>;
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|