@tanstack/hotkeys 0.18.0 → 0.20.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.
- package/dist/_app-hotkeys-factory.d.ts +5 -0
- package/dist/_app-hotkeys-factory.js +13 -0
- package/dist/_event-target.js +202 -124
- package/dist/_keyboard-event.js +13 -5
- package/dist/_match.js +23 -1
- package/dist/_named-keys.js +21 -1
- package/dist/_registration.js +1 -1
- package/dist/_store.d.ts +14 -0
- package/dist/_store.js +43 -0
- package/dist/adapter.d.ts +4 -1
- package/dist/adapter.js +3 -1
- package/dist/app-hotkeys.d.ts +1 -1
- package/dist/app-hotkeys.js +12 -9
- package/dist/constants.d.ts +8 -7
- package/dist/constants.js +20 -158
- package/dist/format.d.ts +16 -3
- package/dist/format.js +19 -14
- package/dist/hotkey-manager.d.ts +1 -10
- package/dist/hotkey-manager.js +15 -36
- package/dist/hotkey-recorder.d.ts +1 -1
- package/dist/hotkey-recorder.js +6 -8
- package/dist/key-state-tracker.d.ts +1 -1
- package/dist/key-state-tracker.js +2 -2
- package/dist/validate.d.ts +2 -2
- package/dist/validate.js +8 -11
- package/dist/vendor/@tanstack/store/dist/store.d.ts +23 -0
- package/dist/vendor/@tanstack/store/dist/types.d.ts +11 -0
- package/package.json +5 -2
- package/skills/hotkeys-core/SKILL.md +2 -1
package/dist/app-hotkeys.js
CHANGED
|
@@ -2,9 +2,10 @@ import { detectPlatform } from "./platform.js";
|
|
|
2
2
|
import { isHotkeySequence, simplifyHotkey } from "./parse.js";
|
|
3
3
|
import { validateHotkey } from "./validate.js";
|
|
4
4
|
import { areHotkeysEqual } from "./match.js";
|
|
5
|
+
import { createStore } from "./_store.js";
|
|
5
6
|
import { GLOBAL_SCOPE } from "./_registration.js";
|
|
6
7
|
import { getHotkeyManager } from "./hotkey-manager.js";
|
|
7
|
-
import {
|
|
8
|
+
import { setAppHotkeysStoreFactory } from "./_app-hotkeys-factory.js";
|
|
8
9
|
|
|
9
10
|
//#region src/app-hotkeys.ts
|
|
10
11
|
/**
|
|
@@ -22,6 +23,7 @@ import { Store } from "@tanstack/store";
|
|
|
22
23
|
* ```
|
|
23
24
|
*/
|
|
24
25
|
function defineAppHotkeys(definitions) {
|
|
26
|
+
setAppHotkeysStoreFactory((appHotkeys, options) => new AppHotkeysStore(appHotkeys, options));
|
|
25
27
|
const result = {};
|
|
26
28
|
for (const [id, input] of Object.entries(definitions)) {
|
|
27
29
|
const hotkey = input.hotkey === null ? null : simplifyHotkey(input.hotkey);
|
|
@@ -73,13 +75,13 @@ function sanitizeCustomHotkeys(appHotkeys, saved) {
|
|
|
73
75
|
if (typeof saved !== "object" || saved === null) return result;
|
|
74
76
|
for (const [id, value] of Object.entries(saved)) {
|
|
75
77
|
if (value === void 0) continue;
|
|
76
|
-
if (!(id
|
|
77
|
-
console.warn(`Ignoring custom hotkey for unknown app hotkey '${id}'`);
|
|
78
|
+
if (!Object.hasOwn(appHotkeys, id)) {
|
|
79
|
+
if (process.env.NODE_ENV !== "production") console.warn(`Ignoring custom hotkey for unknown app hotkey '${id}'`);
|
|
78
80
|
continue;
|
|
79
81
|
}
|
|
80
82
|
const hotkey = parseCustomHotkey(value);
|
|
81
83
|
if (hotkey === void 0) {
|
|
82
|
-
console.warn(`Ignoring invalid custom hotkey for app hotkey '${id}':`, value);
|
|
84
|
+
if (process.env.NODE_ENV !== "production") console.warn(`Ignoring invalid custom hotkey for app hotkey '${id}':`, value);
|
|
83
85
|
continue;
|
|
84
86
|
}
|
|
85
87
|
result[id] = hotkey;
|
|
@@ -123,7 +125,7 @@ var AppHotkeysStore = class {
|
|
|
123
125
|
this.#declarations = appHotkeys;
|
|
124
126
|
this.#options = options;
|
|
125
127
|
const customHotkeys = sanitizeCustomHotkeys(appHotkeys, options.customHotkeys);
|
|
126
|
-
this.store =
|
|
128
|
+
this.store = createStore({
|
|
127
129
|
appHotkeys: applyCustomHotkeys(appHotkeys, customHotkeys),
|
|
128
130
|
customHotkeys
|
|
129
131
|
});
|
|
@@ -161,7 +163,8 @@ var AppHotkeysStore = class {
|
|
|
161
163
|
}
|
|
162
164
|
/** Effective app hotkey for an id, or `undefined` when it is not declared. */
|
|
163
165
|
get(id) {
|
|
164
|
-
|
|
166
|
+
const { appHotkeys } = this.store.state;
|
|
167
|
+
return Object.hasOwn(appHotkeys, id) ? appHotkeys[id] : void 0;
|
|
165
168
|
}
|
|
166
169
|
/**
|
|
167
170
|
* Other app hotkeys that would collide if `id` used `hotkey`. App hotkeys
|
|
@@ -169,7 +172,7 @@ var AppHotkeysStore = class {
|
|
|
169
172
|
* hotkeys are equal or one sequence starts with the other.
|
|
170
173
|
*/
|
|
171
174
|
findConflicts(id, hotkey) {
|
|
172
|
-
const self = this.
|
|
175
|
+
const self = this.get(id);
|
|
173
176
|
if (!self) return [];
|
|
174
177
|
const candidate = simplifyHotkey(hotkey);
|
|
175
178
|
return Object.values(this.store.state.appHotkeys).filter((other) => other.id !== id && other.hotkey !== null && scopesOverlap(self, other) && hotkeysOverlap(candidate, other.hotkey, this.#platform));
|
|
@@ -181,9 +184,9 @@ var AppHotkeysStore = class {
|
|
|
181
184
|
* @returns Conflicting app hotkeys. When any exist, nothing changes.
|
|
182
185
|
*/
|
|
183
186
|
setCustomHotkey(id, hotkey) {
|
|
184
|
-
const appHotkey = this.
|
|
187
|
+
const appHotkey = this.get(id);
|
|
185
188
|
if (!appHotkey) {
|
|
186
|
-
console.warn(`Cannot set a custom hotkey for unknown app hotkey '${id}'`);
|
|
189
|
+
if (process.env.NODE_ENV !== "production") console.warn(`Cannot set a custom hotkey for unknown app hotkey '${id}'`);
|
|
187
190
|
return [];
|
|
188
191
|
}
|
|
189
192
|
const next = hotkey === null ? null : simplifyHotkey(hotkey);
|
package/dist/constants.d.ts
CHANGED
|
@@ -107,9 +107,9 @@ export declare const PUNCTUATION_CODE_MAP: Record<string, string>;
|
|
|
107
107
|
/**
|
|
108
108
|
* Set of all valid non-modifier keys.
|
|
109
109
|
*
|
|
110
|
-
* This is the union of all key type sets (letters, numbers, function keys,
|
|
111
|
-
* editing,
|
|
112
|
-
*
|
|
110
|
+
* This is the union of all key type sets (letters, numbers, function keys,
|
|
111
|
+
* navigation, editing, punctuation, and other named keys): every key with
|
|
112
|
+
* type-safe autocomplete support.
|
|
113
113
|
*
|
|
114
114
|
* @see {@link LETTER_KEYS}
|
|
115
115
|
* @see {@link NUMBER_KEYS}
|
|
@@ -122,16 +122,17 @@ export declare const ALL_KEYS: Set<"CapsLock" | "ContextMenu" | "Convert" | "Kan
|
|
|
122
122
|
/** Whether a key is a single Unicode letter, such as `'a'` or `'é'`. */
|
|
123
123
|
export declare function isSingleLetterKey(key: string): boolean;
|
|
124
124
|
/**
|
|
125
|
-
* Normalizes a key name
|
|
125
|
+
* Normalizes a key name from a `KeyboardEvent` or a hotkey: single letters
|
|
126
|
+
* become uppercase, and the space character becomes `Space`. Other names are
|
|
127
|
+
* returned as written, because key names are case-sensitive.
|
|
126
128
|
*
|
|
127
|
-
* @param key - The key name to normalize
|
|
129
|
+
* @param key - The key name to normalize
|
|
128
130
|
* @returns The canonical key name
|
|
129
131
|
*
|
|
130
132
|
* @example
|
|
131
133
|
* ```ts
|
|
132
|
-
* normalizeKeyName('esc') // 'Escape'
|
|
133
134
|
* normalizeKeyName('a') // 'A'
|
|
134
|
-
* normalizeKeyName('
|
|
135
|
+
* normalizeKeyName(' ') // 'Space'
|
|
135
136
|
* normalizeKeyName('ArrowUp') // 'ArrowUp' (already canonical)
|
|
136
137
|
* ```
|
|
137
138
|
*/
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LOGICAL_ONLY_NAMED_KEYS, SHARED_NAMED_KEYS } from "./_named-keys.js";
|
|
1
|
+
import { EDITING_KEY_NAMES, LOGICAL_ONLY_NAMED_KEYS, NAVIGATION_KEY_NAMES, SHARED_NAMED_KEYS } from "./_named-keys.js";
|
|
2
2
|
|
|
3
3
|
//#region src/constants.ts
|
|
4
4
|
/**
|
|
@@ -80,34 +80,7 @@ const MODIFIER_ALIASES = {
|
|
|
80
80
|
* Used for validation and type checking. Letter keys are matched case-insensitively
|
|
81
81
|
* in hotkey matching, but normalized to uppercase in canonical form.
|
|
82
82
|
*/
|
|
83
|
-
const LETTER_KEYS = /* @__PURE__ */ new Set(
|
|
84
|
-
"A",
|
|
85
|
-
"B",
|
|
86
|
-
"C",
|
|
87
|
-
"D",
|
|
88
|
-
"E",
|
|
89
|
-
"F",
|
|
90
|
-
"G",
|
|
91
|
-
"H",
|
|
92
|
-
"I",
|
|
93
|
-
"J",
|
|
94
|
-
"K",
|
|
95
|
-
"L",
|
|
96
|
-
"M",
|
|
97
|
-
"N",
|
|
98
|
-
"O",
|
|
99
|
-
"P",
|
|
100
|
-
"Q",
|
|
101
|
-
"R",
|
|
102
|
-
"S",
|
|
103
|
-
"T",
|
|
104
|
-
"U",
|
|
105
|
-
"V",
|
|
106
|
-
"W",
|
|
107
|
-
"X",
|
|
108
|
-
"Y",
|
|
109
|
-
"Z"
|
|
110
|
-
]);
|
|
83
|
+
const LETTER_KEYS = /* @__PURE__ */ new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
|
111
84
|
/**
|
|
112
85
|
* Set of all valid number keys (0-9).
|
|
113
86
|
*
|
|
@@ -115,50 +88,14 @@ const LETTER_KEYS = /* @__PURE__ */ new Set([
|
|
|
115
88
|
* so they're excluded from Shift-based hotkey combinations to avoid
|
|
116
89
|
* layout-dependent behavior.
|
|
117
90
|
*/
|
|
118
|
-
const NUMBER_KEYS = /* @__PURE__ */ new Set(
|
|
119
|
-
"0",
|
|
120
|
-
"1",
|
|
121
|
-
"2",
|
|
122
|
-
"3",
|
|
123
|
-
"4",
|
|
124
|
-
"5",
|
|
125
|
-
"6",
|
|
126
|
-
"7",
|
|
127
|
-
"8",
|
|
128
|
-
"9"
|
|
129
|
-
]);
|
|
91
|
+
const NUMBER_KEYS = /* @__PURE__ */ new Set("0123456789");
|
|
130
92
|
/**
|
|
131
93
|
* Set of all valid function keys (F1-F24).
|
|
132
94
|
*
|
|
133
95
|
* Function keys are commonly used for system shortcuts (e.g., F12 for DevTools,
|
|
134
96
|
* Alt+F4 to close windows) and application-specific commands.
|
|
135
97
|
*/
|
|
136
|
-
const FUNCTION_KEYS = /* @__PURE__ */ new Set(
|
|
137
|
-
"F1",
|
|
138
|
-
"F2",
|
|
139
|
-
"F3",
|
|
140
|
-
"F4",
|
|
141
|
-
"F5",
|
|
142
|
-
"F6",
|
|
143
|
-
"F7",
|
|
144
|
-
"F8",
|
|
145
|
-
"F9",
|
|
146
|
-
"F10",
|
|
147
|
-
"F11",
|
|
148
|
-
"F12",
|
|
149
|
-
"F13",
|
|
150
|
-
"F14",
|
|
151
|
-
"F15",
|
|
152
|
-
"F16",
|
|
153
|
-
"F17",
|
|
154
|
-
"F18",
|
|
155
|
-
"F19",
|
|
156
|
-
"F20",
|
|
157
|
-
"F21",
|
|
158
|
-
"F22",
|
|
159
|
-
"F23",
|
|
160
|
-
"F24"
|
|
161
|
-
]);
|
|
98
|
+
const FUNCTION_KEYS = /* @__PURE__ */ new Set(Array.from({ length: 24 }, (_, i) => `F${i + 1}`));
|
|
162
99
|
/**
|
|
163
100
|
* Set of all valid navigation keys for cursor movement and document navigation.
|
|
164
101
|
*
|
|
@@ -166,16 +103,7 @@ const FUNCTION_KEYS = /* @__PURE__ */ new Set([
|
|
|
166
103
|
* These keys are commonly combined with modifiers for selection (Shift+ArrowUp) or
|
|
167
104
|
* navigation shortcuts (Alt+ArrowLeft for back).
|
|
168
105
|
*/
|
|
169
|
-
const NAVIGATION_KEYS = /* @__PURE__ */ new Set(
|
|
170
|
-
"ArrowUp",
|
|
171
|
-
"ArrowDown",
|
|
172
|
-
"ArrowLeft",
|
|
173
|
-
"ArrowRight",
|
|
174
|
-
"Home",
|
|
175
|
-
"End",
|
|
176
|
-
"PageUp",
|
|
177
|
-
"PageDown"
|
|
178
|
-
]);
|
|
106
|
+
const NAVIGATION_KEYS = /* @__PURE__ */ new Set(NAVIGATION_KEY_NAMES);
|
|
179
107
|
/**
|
|
180
108
|
* Set of all valid editing and special keys.
|
|
181
109
|
*
|
|
@@ -183,14 +111,7 @@ const NAVIGATION_KEYS = /* @__PURE__ */ new Set([
|
|
|
183
111
|
* special actions (Escape, Space). These keys are frequently combined with modifiers
|
|
184
112
|
* for editor shortcuts (Mod+Enter to submit, Shift+Tab to go back).
|
|
185
113
|
*/
|
|
186
|
-
const EDITING_KEYS = /* @__PURE__ */ new Set(
|
|
187
|
-
"Enter",
|
|
188
|
-
"Escape",
|
|
189
|
-
"Space",
|
|
190
|
-
"Tab",
|
|
191
|
-
"Backspace",
|
|
192
|
-
"Delete"
|
|
193
|
-
]);
|
|
114
|
+
const EDITING_KEYS = /* @__PURE__ */ new Set(EDITING_KEY_NAMES);
|
|
194
115
|
/**
|
|
195
116
|
* Set of all valid punctuation keys commonly used in keyboard shortcuts.
|
|
196
117
|
*
|
|
@@ -203,40 +124,7 @@ const EDITING_KEYS = /* @__PURE__ */ new Set([
|
|
|
203
124
|
* Note: Punctuation keys are affected by Shift (Shift+',' → '<' on US layout),
|
|
204
125
|
* so they're excluded from Shift-based hotkey combinations to avoid layout-dependent behavior.
|
|
205
126
|
*/
|
|
206
|
-
const PUNCTUATION_KEYS = /* @__PURE__ */ new Set([
|
|
207
|
-
"/",
|
|
208
|
-
"[",
|
|
209
|
-
"]",
|
|
210
|
-
"\\",
|
|
211
|
-
"=",
|
|
212
|
-
"-",
|
|
213
|
-
",",
|
|
214
|
-
".",
|
|
215
|
-
";",
|
|
216
|
-
"`",
|
|
217
|
-
"'",
|
|
218
|
-
"+",
|
|
219
|
-
"?",
|
|
220
|
-
"!",
|
|
221
|
-
"@",
|
|
222
|
-
"#",
|
|
223
|
-
"$",
|
|
224
|
-
"%",
|
|
225
|
-
"^",
|
|
226
|
-
"&",
|
|
227
|
-
"*",
|
|
228
|
-
"(",
|
|
229
|
-
")",
|
|
230
|
-
"_",
|
|
231
|
-
"{",
|
|
232
|
-
"}",
|
|
233
|
-
"|",
|
|
234
|
-
":",
|
|
235
|
-
"\"",
|
|
236
|
-
"<",
|
|
237
|
-
">",
|
|
238
|
-
"~"
|
|
239
|
-
]);
|
|
127
|
+
const PUNCTUATION_KEYS = /* @__PURE__ */ new Set("/[]\\=-,.;`'+?!@#$%^&*()_{}|:\"<>~");
|
|
240
128
|
/**
|
|
241
129
|
* Maps `KeyboardEvent.code` values for punctuation keys to their canonical characters.
|
|
242
130
|
*
|
|
@@ -259,12 +147,15 @@ const PUNCTUATION_CODE_MAP = {
|
|
|
259
147
|
Semicolon: ";",
|
|
260
148
|
Slash: "/"
|
|
261
149
|
};
|
|
150
|
+
function union(...groups) {
|
|
151
|
+
return new Set(groups.flatMap((group) => [...group]));
|
|
152
|
+
}
|
|
262
153
|
/**
|
|
263
154
|
* Set of all valid non-modifier keys.
|
|
264
155
|
*
|
|
265
|
-
* This is the union of all key type sets (letters, numbers, function keys,
|
|
266
|
-
* editing,
|
|
267
|
-
*
|
|
156
|
+
* This is the union of all key type sets (letters, numbers, function keys,
|
|
157
|
+
* navigation, editing, punctuation, and other named keys): every key with
|
|
158
|
+
* type-safe autocomplete support.
|
|
268
159
|
*
|
|
269
160
|
* @see {@link LETTER_KEYS}
|
|
270
161
|
* @see {@link NUMBER_KEYS}
|
|
@@ -273,52 +164,23 @@ const PUNCTUATION_CODE_MAP = {
|
|
|
273
164
|
* @see {@link EDITING_KEYS}
|
|
274
165
|
* @see {@link PUNCTUATION_KEYS}
|
|
275
166
|
*/
|
|
276
|
-
const ALL_KEYS = /* @__PURE__ */
|
|
277
|
-
...LETTER_KEYS,
|
|
278
|
-
...NUMBER_KEYS,
|
|
279
|
-
...FUNCTION_KEYS,
|
|
280
|
-
...NAVIGATION_KEYS,
|
|
281
|
-
...EDITING_KEYS,
|
|
282
|
-
...PUNCTUATION_KEYS,
|
|
283
|
-
...SHARED_NAMED_KEYS,
|
|
284
|
-
...LOGICAL_ONLY_NAMED_KEYS
|
|
285
|
-
]);
|
|
286
|
-
/**
|
|
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.
|
|
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
|
-
]);
|
|
167
|
+
const ALL_KEYS = /* @__PURE__ */ union(LETTER_KEYS, NUMBER_KEYS, FUNCTION_KEYS, NAVIGATION_KEYS, EDITING_KEYS, PUNCTUATION_KEYS, SHARED_NAMED_KEYS, LOGICAL_ONLY_NAMED_KEYS);
|
|
307
168
|
/** Whether a key is a single Unicode letter, such as `'a'` or `'é'`. */
|
|
308
169
|
function isSingleLetterKey(key) {
|
|
309
170
|
return /^\p{Letter}$/u.test(key);
|
|
310
171
|
}
|
|
311
172
|
/**
|
|
312
|
-
* Normalizes a key name
|
|
173
|
+
* Normalizes a key name from a `KeyboardEvent` or a hotkey: single letters
|
|
174
|
+
* become uppercase, and the space character becomes `Space`. Other names are
|
|
175
|
+
* returned as written, because key names are case-sensitive.
|
|
313
176
|
*
|
|
314
|
-
* @param key - The key name to normalize
|
|
177
|
+
* @param key - The key name to normalize
|
|
315
178
|
* @returns The canonical key name
|
|
316
179
|
*
|
|
317
180
|
* @example
|
|
318
181
|
* ```ts
|
|
319
|
-
* normalizeKeyName('esc') // 'Escape'
|
|
320
182
|
* normalizeKeyName('a') // 'A'
|
|
321
|
-
* normalizeKeyName('
|
|
183
|
+
* normalizeKeyName(' ') // 'Space'
|
|
322
184
|
* normalizeKeyName('ArrowUp') // 'ArrowUp' (already canonical)
|
|
323
185
|
* ```
|
|
324
186
|
*/
|
|
@@ -328,7 +190,7 @@ function normalizeKeyName(key) {
|
|
|
328
190
|
const upper = key.toUpperCase();
|
|
329
191
|
return upper.length === 1 ? upper : key;
|
|
330
192
|
}
|
|
331
|
-
return
|
|
193
|
+
return key === " " ? "Space" : key;
|
|
332
194
|
}
|
|
333
195
|
|
|
334
196
|
//#endregion
|
package/dist/format.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CanonicalModifier } from "./key.types.js";
|
|
1
2
|
import { ParsedHotkeyChord, RegisterableHotkeyChord } from "./hotkey.types.js";
|
|
2
3
|
//#region src/format.d.ts
|
|
3
4
|
/**
|
|
@@ -17,8 +18,11 @@ import { ParsedHotkeyChord, RegisterableHotkeyChord } from "./hotkey.types.js";
|
|
|
17
18
|
export declare function formatHotkey(parsed: ParsedHotkeyChord): string;
|
|
18
19
|
export declare function formatHotkey(parsed: ReadonlyArray<ParsedHotkeyChord>): Array<string>;
|
|
19
20
|
export declare function formatHotkey(parsed: ParsedHotkeyChord | ReadonlyArray<ParsedHotkeyChord>): string | Array<string>;
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* A single chord to display: a hotkey string, which includes single keys, a
|
|
23
|
+
* modifier on its own, a raw object such as `{ key }`, or a parsed hotkey.
|
|
24
|
+
*/
|
|
25
|
+
export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkeyChord | CanonicalModifier;
|
|
22
26
|
/** A hotkey to display: a chord or a sequence of chords. */
|
|
23
27
|
export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChord>;
|
|
24
28
|
/**
|
|
@@ -35,7 +39,10 @@ export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChor
|
|
|
35
39
|
* Sequence steps are formatted individually and joined with `sequenceSeparator`
|
|
36
40
|
* (a space by default).
|
|
37
41
|
*
|
|
38
|
-
*
|
|
42
|
+
* A key reported by a `KeyboardEvent`, such as a held key from
|
|
43
|
+
* `KeyStateTracker`, can be outside the key types: pass it as `{ key }`.
|
|
44
|
+
*
|
|
45
|
+
* @param hotkey - A logical or physical hotkey string, raw object, ParsedHotkeyChord, or sequence
|
|
39
46
|
* @param options - Formatting options
|
|
40
47
|
* @returns Display text, or individual key labels when parts is true (one array per step for sequences)
|
|
41
48
|
*
|
|
@@ -50,6 +57,12 @@ export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChor
|
|
|
50
57
|
* formatForDisplay('Escape')
|
|
51
58
|
* // Returns: 'Esc' (on all platforms)
|
|
52
59
|
*
|
|
60
|
+
* formatForDisplay('Shift', { platform: 'mac' })
|
|
61
|
+
* // Returns: '⇧'
|
|
62
|
+
*
|
|
63
|
+
* formatForDisplay({ key: heldKey })
|
|
64
|
+
* // A key from a KeyboardEvent, such as a held key
|
|
65
|
+
*
|
|
53
66
|
* formatForDisplay(['G', 'H'])
|
|
54
67
|
* // Returns: 'G H'
|
|
55
68
|
* ```
|
package/dist/format.js
CHANGED
|
@@ -45,20 +45,25 @@ function formatChordParts(hotkey, options) {
|
|
|
45
45
|
};
|
|
46
46
|
segments.sort((a, b) => modifierIndex(a) - modifierIndex(b));
|
|
47
47
|
}
|
|
48
|
-
return segments.map((segment) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
return segments.map((segment) => formatKey(segment, platform, modifierSymbols, keySymbols, options));
|
|
49
|
+
}
|
|
50
|
+
/** Own properties only, so names such as `toString` never read Object.prototype. */
|
|
51
|
+
function lookup(table, name) {
|
|
52
|
+
return table && Object.hasOwn(table, name) ? table[name] : void 0;
|
|
53
|
+
}
|
|
54
|
+
function formatKey(segment, platform, modifierSymbols, keySymbols, options) {
|
|
55
|
+
const modifier = modifierAlias(segment);
|
|
56
|
+
if (modifier) return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
|
|
57
|
+
const code = physicalCodeOf(segment);
|
|
58
|
+
const label = lookup(options.keyLabels, code ?? segment);
|
|
59
|
+
if (label !== void 0) return label;
|
|
60
|
+
const numpad = code?.startsWith("Numpad") ?? false;
|
|
61
|
+
let displayKey = code ?? segment;
|
|
62
|
+
const mappedKey = code ? options.layoutMap?.get(code) : void 0;
|
|
63
|
+
if (mappedKey) displayKey = normalizeKeyName(mappedKey);
|
|
64
|
+
else if (code) displayKey = numpad ? code.slice(6) : codeToLogicalKey(code) ?? code;
|
|
65
|
+
const display = lookup(keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS, displayKey) ?? displayKey;
|
|
66
|
+
return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
//#endregion
|
package/dist/hotkey-manager.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Store } from "./_store.js";
|
|
1
2
|
import { ConflictBehavior, HotkeyCallback, HotkeyCallbackContextFor, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkeyChord, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
|
|
2
3
|
import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
|
|
3
|
-
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/hotkey-manager.d.ts
|
|
5
5
|
/** An element, document, or window that hotkeys can listen on. */
|
|
6
6
|
export type Target = HTMLElement | Document | Window;
|
|
@@ -366,15 +366,6 @@ export declare class HotkeyManager {
|
|
|
366
366
|
* Resets the progress of every in-progress sequence.
|
|
367
367
|
*/
|
|
368
368
|
resetSequenceProgress(): void;
|
|
369
|
-
/**
|
|
370
|
-
* Triggers a registration's callback programmatically from devtools.
|
|
371
|
-
* Creates a synthetic KeyboardEvent from the chord (or a sequence's last step)
|
|
372
|
-
* and invokes the callback.
|
|
373
|
-
*
|
|
374
|
-
* @param id - The registration ID to trigger
|
|
375
|
-
* @returns True if the registration was found and triggered
|
|
376
|
-
*/
|
|
377
|
-
triggerRegistration(id: string): boolean;
|
|
378
369
|
/**
|
|
379
370
|
* Gets the number of registered hotkeys, chords and sequences.
|
|
380
371
|
*/
|
package/dist/hotkey-manager.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { normalizeKeyName } from "./constants.js";
|
|
2
2
|
import { detectPlatform } from "./platform.js";
|
|
3
|
-
import { keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
3
|
+
import { isKeyNameShape, keysEqual, normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
4
4
|
import { isHotkeySequence, isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
|
|
5
5
|
import { formatHotkey } from "./format.js";
|
|
6
6
|
import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
|
|
7
7
|
import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
|
|
8
|
-
import { getTargetDocument,
|
|
8
|
+
import { getTargetDocument, isDocumentLevel, shouldIgnoreInputEvent, targetHasFocus } from "./_event-target.js";
|
|
9
|
+
import { createStore } from "./_store.js";
|
|
9
10
|
import { TargetListeners } from "./_target-listeners.js";
|
|
10
11
|
import { managerRef } from "./_manager-ref.js";
|
|
11
12
|
import { isRecordingEvent } from "./_recording-guard.js";
|
|
12
13
|
import { GLOBAL_SCOPE, defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
|
|
13
|
-
import { Store } from "@tanstack/store";
|
|
14
14
|
|
|
15
15
|
//#region src/hotkey-manager.ts
|
|
16
16
|
/** Whether a registration can fire now. Without a manager, scopes don't apply. */
|
|
@@ -127,14 +127,14 @@ var HotkeyManager = class HotkeyManager {
|
|
|
127
127
|
* }
|
|
128
128
|
* ```
|
|
129
129
|
*/
|
|
130
|
-
registrations =
|
|
130
|
+
registrations = createStore(/* @__PURE__ */ new Map());
|
|
131
131
|
/**
|
|
132
132
|
* The app hotkeys store published by a framework provider, so tools such as
|
|
133
133
|
* devtools can list declared hotkeys that are not registered.
|
|
134
134
|
*/
|
|
135
|
-
appHotkeys =
|
|
135
|
+
appHotkeys = createStore(null);
|
|
136
136
|
/** Active scopes. Subscribe to show which hotkeys are suspended. */
|
|
137
|
-
scopes =
|
|
137
|
+
scopes = createStore({
|
|
138
138
|
active: [],
|
|
139
139
|
exclusive: null,
|
|
140
140
|
allowed: [],
|
|
@@ -194,7 +194,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
194
194
|
if (scope === "global") return () => {};
|
|
195
195
|
const { exclusive } = options;
|
|
196
196
|
const allow = exclusive ? exclusive === true ? [] : exclusive.allow : null;
|
|
197
|
-
if (allow && [...this.#scopeActivations].some((other) => other.allow && other.scope === scope)) console.warn(`Exclusive hotkey scope '${scope}' is already active. Hotkeys from both activations will fire; give each a unique scope.`);
|
|
197
|
+
if (process.env.NODE_ENV !== "production" && allow && [...this.#scopeActivations].some((other) => other.allow && other.scope === scope)) console.warn(`Exclusive hotkey scope '${scope}' is already active. Hotkeys from both activations will fire; give each a unique scope.`);
|
|
198
198
|
const activation = {
|
|
199
199
|
scope,
|
|
200
200
|
parent: options.parent ?? "global",
|
|
@@ -286,7 +286,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
286
286
|
#handledByLiveHotkey(event, eventType) {
|
|
287
287
|
const path = event.composedPath();
|
|
288
288
|
for (const registration of this.registrations.state.values()) {
|
|
289
|
-
if (!this.#isLive(registration) || registration.options.eventType !== eventType || !path.includes(registration.target) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, registration.target
|
|
289
|
+
if (!this.#isLive(registration) || registration.options.eventType !== eventType || !path.includes(registration.target) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, registration.target)) continue;
|
|
290
290
|
if ((registration.kind === "chord" ? [registration.parsedChord] : [registration.parsedSequence[registration.currentIndex], registration.parsedSequence[0]]).some((step) => matchKeyboardEvent(event, step, registration.options.platform).matched)) return true;
|
|
291
291
|
}
|
|
292
292
|
return false;
|
|
@@ -353,6 +353,10 @@ var HotkeyManager = class HotkeyManager {
|
|
|
353
353
|
parsedChord
|
|
354
354
|
};
|
|
355
355
|
}
|
|
356
|
+
if (process.env.NODE_ENV !== "production") {
|
|
357
|
+
const steps = registration.kind === "sequence" ? registration.parsedSequence : [registration.parsedChord];
|
|
358
|
+
for (const { key } of steps) if (key !== void 0 && !isKeyNameShape(key)) console.warn(`Unknown key '${key}' in '${formatForMessage(registration.hotkey)}'. Key names are case-sensitive, such as 'Escape' and 'F1', so this hotkey may never fire.`);
|
|
359
|
+
}
|
|
356
360
|
const conflicting = this.#findConflictingRegistration(registration);
|
|
357
361
|
if (conflicting) handleConflict(conflicting.id, formatForMessage(registration.hotkey), options.conflictBehavior ?? "warn", (conflictId) => this.#unregister(conflictId));
|
|
358
362
|
this.registrations.setState((prev) => new Map(prev).set(id, registration));
|
|
@@ -477,7 +481,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
477
481
|
let reserved = false;
|
|
478
482
|
for (const registration of registrations) {
|
|
479
483
|
const live = this.#isLive(registration);
|
|
480
|
-
if (!live && registration.options.preventDefault !== "always" || registration.options.eventType !== eventType ||
|
|
484
|
+
if (!live && registration.options.preventDefault !== "always" || registration.options.eventType !== eventType || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target)) continue;
|
|
481
485
|
const result = matchKeyboardEvent(event, registration.parsedChord, registration.options.platform);
|
|
482
486
|
if (!result.matched) continue;
|
|
483
487
|
if (!live) {
|
|
@@ -533,7 +537,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
533
537
|
for (const registration of this.registrations.state.values()) {
|
|
534
538
|
if (registration.kind !== "sequence" || registration.currentIndex === 0 || !this.#isLive(registration) || registration.options.eventType !== eventType || now - registration.lastKeyTime > (registration.options.timeout ?? 1e3)) continue;
|
|
535
539
|
const target = registration.target;
|
|
536
|
-
if (path.includes(target
|
|
540
|
+
if (path.includes(isDocumentLevel(target) ? getTargetDocument(target) : target) && !(ignoresInputs(registration) && shouldIgnoreInputEvent(event, target)) && matchKeyboardEvent(event, registration.parsedSequence[registration.currentIndex], registration.options.platform).matched) return true;
|
|
537
541
|
}
|
|
538
542
|
return false;
|
|
539
543
|
}
|
|
@@ -556,7 +560,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
556
560
|
const candidates = /* @__PURE__ */ new Map();
|
|
557
561
|
let bestScore = 0;
|
|
558
562
|
for (const registration of registrations) {
|
|
559
|
-
if (!
|
|
563
|
+
if (!this.#isLive(registration) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target) || registration.options.eventType !== eventType) continue;
|
|
560
564
|
const timeout = registration.options.timeout ?? 1e3;
|
|
561
565
|
if (registration.currentIndex > 0 && now - registration.lastKeyTime > timeout) {
|
|
562
566
|
registration.currentIndex = 0;
|
|
@@ -662,31 +666,6 @@ var HotkeyManager = class HotkeyManager {
|
|
|
662
666
|
this.#notify();
|
|
663
667
|
}
|
|
664
668
|
/**
|
|
665
|
-
* Triggers a registration's callback programmatically from devtools.
|
|
666
|
-
* Creates a synthetic KeyboardEvent from the chord (or a sequence's last step)
|
|
667
|
-
* and invokes the callback.
|
|
668
|
-
*
|
|
669
|
-
* @param id - The registration ID to trigger
|
|
670
|
-
* @returns True if the registration was found and triggered
|
|
671
|
-
*/
|
|
672
|
-
triggerRegistration(id) {
|
|
673
|
-
const registration = this.registrations.state.get(id);
|
|
674
|
-
if (!registration) return false;
|
|
675
|
-
const context = this.#callbackContext(registration);
|
|
676
|
-
const parsed = registration.kind === "chord" ? registration.parsedChord : registration.parsedSequence[registration.parsedSequence.length - 1];
|
|
677
|
-
const syntheticEvent = new KeyboardEvent(registration.options.eventType ?? "keydown", {
|
|
678
|
-
...parsed.code !== void 0 ? { code: parsed.code } : { key: parsed.key },
|
|
679
|
-
ctrlKey: parsed.ctrl,
|
|
680
|
-
shiftKey: parsed.shift,
|
|
681
|
-
altKey: parsed.alt,
|
|
682
|
-
metaKey: parsed.meta,
|
|
683
|
-
bubbles: true,
|
|
684
|
-
cancelable: true
|
|
685
|
-
});
|
|
686
|
-
this.#fire(registration, syntheticEvent, context);
|
|
687
|
-
return true;
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
669
|
* Gets the number of registered hotkeys, chords and sequences.
|
|
691
670
|
*/
|
|
692
671
|
getRegistrationCount() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Store } from "./_store.js";
|
|
1
2
|
import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkeyChord } from "./hotkey.types.js";
|
|
2
3
|
import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.js";
|
|
3
|
-
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/hotkey-recorder.d.ts
|
|
5
5
|
/** Event property used to record a binding. Physical codes are the default. */
|
|
6
6
|
export type RecorderKeyMode = 'code' | 'key';
|
package/dist/hotkey-recorder.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
|
-
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
2
|
+
import { normalizeKeyboardEvent, splitChordParts } from "./_keyboard-event.js";
|
|
3
3
|
import { isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
|
|
4
|
-
import { validateHotkey } from "./validate.js";
|
|
5
4
|
import { isTypingEvent } from "./_event-target.js";
|
|
5
|
+
import { createStore } from "./_store.js";
|
|
6
6
|
import { beginRecording, captureRecordingEvent, endRecording } from "./_recording-guard.js";
|
|
7
7
|
import { findHotkeyConflicts } from "./conflicts.js";
|
|
8
8
|
import { chordRejection, hotkeyChordFromKeydown } from "./_recorder-chord.js";
|
|
9
|
-
import { Store } from "@tanstack/store";
|
|
10
9
|
|
|
11
10
|
//#region src/hotkey-recorder.ts
|
|
12
11
|
const initialState = {
|
|
@@ -49,7 +48,7 @@ const initialState = {
|
|
|
49
48
|
* ```
|
|
50
49
|
*/
|
|
51
50
|
var HotkeyRecorder = class {
|
|
52
|
-
store =
|
|
51
|
+
store = createStore(initialState);
|
|
53
52
|
#keydownHandler = null;
|
|
54
53
|
#optionsSource;
|
|
55
54
|
#optionOverrides = {};
|
|
@@ -101,7 +100,7 @@ var HotkeyRecorder = class {
|
|
|
101
100
|
* and records chords according to `maxSteps`.
|
|
102
101
|
*/
|
|
103
102
|
start() {
|
|
104
|
-
if (this.#keydownHandler) return;
|
|
103
|
+
if (this.#keydownHandler || typeof document === "undefined") return;
|
|
105
104
|
this.#clearIdleTimer();
|
|
106
105
|
this.#events = [];
|
|
107
106
|
beginRecording(this);
|
|
@@ -156,11 +155,10 @@ var HotkeyRecorder = class {
|
|
|
156
155
|
}
|
|
157
156
|
const chord = hotkeyChordFromKeydown(event, platform, options.recordBy);
|
|
158
157
|
if (chord === null) return;
|
|
159
|
-
|
|
160
|
-
if (!validation.valid) {
|
|
158
|
+
if (splitChordParts(chord).some((part) => !part.trim())) {
|
|
161
159
|
options.onReject?.({
|
|
162
160
|
reason: "invalid",
|
|
163
|
-
message:
|
|
161
|
+
message: "Whitespace keys cannot be recorded",
|
|
164
162
|
hotkey: chord
|
|
165
163
|
});
|
|
166
164
|
return;
|