@tanstack/hotkeys 0.19.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/_keyboard-event.js +13 -5
- 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 +10 -31
- package/dist/hotkey-recorder.d.ts +1 -1
- package/dist/hotkey-recorder.js +5 -7
- 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
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { AppHotkeys, AppHotkeysStore, AppHotkeysStoreOptions } from "./app-hotkeys.js";
|
|
2
|
+
//#region src/_app-hotkeys-factory.d.ts
|
|
3
|
+
/** Creates the store for app hotkeys from `defineAppHotkeys`. */
|
|
4
|
+
export declare function createAppHotkeysStore<T extends AppHotkeys>(appHotkeys: T, options: AppHotkeysStoreOptions<T>): AppHotkeysStore<T>;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/_app-hotkeys-factory.ts
|
|
2
|
+
let factory;
|
|
3
|
+
function setAppHotkeysStoreFactory(create) {
|
|
4
|
+
factory = create;
|
|
5
|
+
}
|
|
6
|
+
/** Creates the store for app hotkeys from `defineAppHotkeys`. */
|
|
7
|
+
function createAppHotkeysStore(appHotkeys, options) {
|
|
8
|
+
if (!factory) throw new Error("[hotkeys] App hotkeys must come from defineAppHotkeys.");
|
|
9
|
+
return factory(appHotkeys, options);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { createAppHotkeysStore, setAppHotkeysStoreFactory };
|
package/dist/_keyboard-event.js
CHANGED
|
@@ -34,9 +34,8 @@ function codeToLogicalKey(code) {
|
|
|
34
34
|
if (/^Key[A-Z]$/i.test(code)) return code.slice(3).toUpperCase();
|
|
35
35
|
if (/^Digit[0-9]$/i.test(code)) return code.slice(5);
|
|
36
36
|
if (/^Numpad[0-9]$/i.test(code)) return code.slice(6);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return {
|
|
37
|
+
if (Object.hasOwn(PUNCTUATION_CODE_MAP, code)) return PUNCTUATION_CODE_MAP[code];
|
|
38
|
+
const named = {
|
|
40
39
|
NumpadAdd: "+",
|
|
41
40
|
NumpadSubtract: "-",
|
|
42
41
|
NumpadMultiply: "*",
|
|
@@ -44,7 +43,8 @@ function codeToLogicalKey(code) {
|
|
|
44
43
|
NumpadDecimal: ".",
|
|
45
44
|
NumpadEnter: "Enter",
|
|
46
45
|
Space: "Space"
|
|
47
|
-
}
|
|
46
|
+
};
|
|
47
|
+
return Object.hasOwn(named, code) ? named[code] : void 0;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Whether AltGr changed what a key typed, as in AltGr+Q typing `@`. False when
|
|
@@ -102,6 +102,14 @@ function keysEqual(left, right) {
|
|
|
102
102
|
function assertLogicalKey(key) {
|
|
103
103
|
if (/^\[.*\]$/s.test(key.trim())) throw new Error("Logical key must not contain a bracketed physical code. Use the code field instead.");
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether a key is written like a key the library knows: one character,
|
|
107
|
+
* F1-F24, or a capitalized name such as `Escape`. Key names are
|
|
108
|
+
* case-sensitive, so `escape` and `f1` don't qualify.
|
|
109
|
+
*/
|
|
110
|
+
function isKeyNameShape(key) {
|
|
111
|
+
return /^(?:.|F(?:[1-9]|1\d|2[0-4])|[A-Z][a-z][A-Za-z\d]*)$/su.test(key);
|
|
112
|
+
}
|
|
105
113
|
|
|
106
114
|
//#endregion
|
|
107
|
-
export { altGraphTypedCharacter, assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts };
|
|
115
|
+
export { altGraphTypedCharacter, assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isKeyNameShape, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts };
|
package/dist/_named-keys.js
CHANGED
|
@@ -60,6 +60,26 @@ const LOGICAL_ONLY_NAMED_KEYS = [
|
|
|
60
60
|
"Hankaku",
|
|
61
61
|
"ZenkakuHankaku"
|
|
62
62
|
];
|
|
63
|
+
/** Arrow, Home/End, and PageUp/PageDown keys. */
|
|
64
|
+
const NAVIGATION_KEY_NAMES = [
|
|
65
|
+
"ArrowUp",
|
|
66
|
+
"ArrowDown",
|
|
67
|
+
"ArrowLeft",
|
|
68
|
+
"ArrowRight",
|
|
69
|
+
"Home",
|
|
70
|
+
"End",
|
|
71
|
+
"PageUp",
|
|
72
|
+
"PageDown"
|
|
73
|
+
];
|
|
74
|
+
/** Editing and special keys. */
|
|
75
|
+
const EDITING_KEY_NAMES = [
|
|
76
|
+
"Enter",
|
|
77
|
+
"Escape",
|
|
78
|
+
"Space",
|
|
79
|
+
"Tab",
|
|
80
|
+
"Backspace",
|
|
81
|
+
"Delete"
|
|
82
|
+
];
|
|
63
83
|
|
|
64
84
|
//#endregion
|
|
65
|
-
export { LOGICAL_ONLY_NAMED_KEYS, SHARED_NAMED_KEYS };
|
|
85
|
+
export { EDITING_KEY_NAMES, LOGICAL_ONLY_NAMED_KEYS, NAVIGATION_KEY_NAMES, SHARED_NAMED_KEYS };
|
package/dist/_registration.js
CHANGED
|
@@ -35,7 +35,7 @@ function getDefaultIgnoreInputs(parsedChord) {
|
|
|
35
35
|
function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
|
|
36
36
|
if (conflictBehavior === "allow") return;
|
|
37
37
|
if (conflictBehavior === "warn") {
|
|
38
|
-
console.warn(`'${keyDisplay}' is already registered. Multiple handlers may be triggered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to suppress this warning.`);
|
|
38
|
+
if (process.env.NODE_ENV !== "production") console.warn(`'${keyDisplay}' is already registered. Multiple handlers may be triggered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to suppress this warning.`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
if (conflictBehavior === "error") throw new Error(`'${keyDisplay}' is already registered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to allow multiple registrations.`);
|
package/dist/_store.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Store as Store$1 } from "./vendor/@tanstack/store/dist/store.js";
|
|
2
|
+
//#region src/_store.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The part of TanStack Store's `Store` that TanStack Hotkeys uses. Its stores
|
|
5
|
+
* work with TanStack Store's framework adapters, such as `useSelector` from
|
|
6
|
+
* `@tanstack/react-store`.
|
|
7
|
+
*/
|
|
8
|
+
export type Store<T> = Pick<Store$1<T>, 'state' | 'get' | 'setState' | 'subscribe'>;
|
|
9
|
+
/**
|
|
10
|
+
* Creates a store. Listeners run synchronously after each change, and an
|
|
11
|
+
* update to an equal value (by `Object.is`) notifies no one.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createStore<T>(initialState: T): Store<T>;
|
|
14
|
+
//#endregion
|
package/dist/_store.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/_store.ts
|
|
2
|
+
const queue = /* @__PURE__ */ new Set();
|
|
3
|
+
function notify(listeners) {
|
|
4
|
+
for (const listener of listeners) queue.add(listener);
|
|
5
|
+
for (const listener of queue) {
|
|
6
|
+
queue.delete(listener);
|
|
7
|
+
listener();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Creates a store. Listeners run synchronously after each change, and an
|
|
12
|
+
* update to an equal value (by `Object.is`) notifies no one.
|
|
13
|
+
*/
|
|
14
|
+
function createStore(initialState) {
|
|
15
|
+
let state = initialState;
|
|
16
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
17
|
+
return {
|
|
18
|
+
get state() {
|
|
19
|
+
return state;
|
|
20
|
+
},
|
|
21
|
+
get: () => state,
|
|
22
|
+
setState(updater) {
|
|
23
|
+
const next = updater(state);
|
|
24
|
+
if (Object.is(state, next)) return;
|
|
25
|
+
state = next;
|
|
26
|
+
notify(listeners);
|
|
27
|
+
},
|
|
28
|
+
subscribe(observerOrFn) {
|
|
29
|
+
const listener = () => {
|
|
30
|
+
if (typeof observerOrFn === "function") observerOrFn(state);
|
|
31
|
+
else observerOrFn.next?.(state);
|
|
32
|
+
};
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
return { unsubscribe() {
|
|
35
|
+
listeners.delete(listener);
|
|
36
|
+
queue.delete(listener);
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { createStore };
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Store, createStore } from "./_store.js";
|
|
1
2
|
import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
|
|
2
3
|
import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
|
|
3
4
|
import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
|
|
5
|
+
import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
|
|
4
6
|
//#region src/adapter.d.ts
|
|
5
7
|
/**
|
|
6
8
|
* A hotkey and its callback, with optional per-binding options. Chords accept
|
|
@@ -160,4 +162,5 @@ export declare function getHotkeysProviderScopeKey(options: {
|
|
|
160
162
|
* Returns a function that releases the activation.
|
|
161
163
|
*/
|
|
162
164
|
export declare function activateHotkeysProviderScope(key: string): () => void;
|
|
163
|
-
//#endregion
|
|
165
|
+
//#endregion
|
|
166
|
+
export { type Store, createAppHotkeysStore, createStore };
|
package/dist/adapter.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
2
|
import { isHotkeySequence, normalizeHotkey } from "./parse.js";
|
|
3
|
+
import { createStore } from "./_store.js";
|
|
3
4
|
import { defaultHotkeyOptions } from "./_registration.js";
|
|
4
5
|
import { getHotkeyManager } from "./hotkey-manager.js";
|
|
6
|
+
import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
|
|
5
7
|
import { getAppHotkeyRegistrationOptions, isAppHotkey, resolveAppHotkey } from "./app-hotkeys.js";
|
|
6
8
|
|
|
7
9
|
//#region src/adapter.ts
|
|
@@ -171,4 +173,4 @@ function activateHotkeysProviderScope(key) {
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
//#endregion
|
|
174
|
-
export { activateHotkeysProviderScope, createHotkeyBindings, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
|
|
176
|
+
export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
|
package/dist/app-hotkeys.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Store } from "./_store.js";
|
|
1
2
|
import { Hotkey, HotkeyChord, HotkeyMeta, HotkeySequence } from "./hotkey.types.js";
|
|
2
3
|
import { HotkeyChordOptions, HotkeyRegistrationView, HotkeySequenceOptions } from "./hotkey-manager.js";
|
|
3
|
-
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/app-hotkeys.d.ts
|
|
5
5
|
type Platform = 'mac' | 'windows' | 'linux';
|
|
6
6
|
type DeclaredOptionKeys = 'capture' | 'conflictBehavior' | 'eventType' | 'ignoreInputs' | 'preventDefault' | 'stopPropagation';
|
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
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",
|
|
@@ -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));
|
|
@@ -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 = {};
|
|
@@ -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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createStore } from "./_store.js";
|
|
3
3
|
|
|
4
4
|
//#region src/key-state-tracker.ts
|
|
5
5
|
/**
|
|
@@ -51,7 +51,7 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
51
51
|
* The TanStack Store instance containing the tracker state.
|
|
52
52
|
* Use this to subscribe to state changes or access current state.
|
|
53
53
|
*/
|
|
54
|
-
store =
|
|
54
|
+
store = createStore(getDefaultKeyStateTrackerState());
|
|
55
55
|
#heldEntries = /* @__PURE__ */ new Map();
|
|
56
56
|
/** Window listeners as `[type, listener, capture]`, removed together on destroy. */
|
|
57
57
|
#listeners = [];
|
package/dist/validate.d.ts
CHANGED
|
@@ -42,8 +42,8 @@ export declare function validateHotkey(hotkey: ValidatableHotkey): ValidationRes
|
|
|
42
42
|
*/
|
|
43
43
|
export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
|
|
44
44
|
/**
|
|
45
|
-
* Validates a hotkey and logs warnings to the console.
|
|
46
|
-
* Useful for development-time feedback.
|
|
45
|
+
* Validates a hotkey and logs errors and warnings to the console.
|
|
46
|
+
* Useful for development-time feedback. Logs nothing in production builds.
|
|
47
47
|
*
|
|
48
48
|
* @param hotkey - The hotkey string to validate
|
|
49
49
|
* @returns True if the hotkey is valid (may still have warnings)
|
package/dist/validate.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
|
|
1
|
+
import { isKeyNameShape, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
|
|
3
2
|
|
|
4
3
|
//#region src/validate.ts
|
|
5
4
|
/**
|
|
@@ -62,17 +61,13 @@ function validateChord(hotkey) {
|
|
|
62
61
|
const keyPart = parts[parts.length - 1];
|
|
63
62
|
for (const modifier of modifierParts) if (!modifierAlias(modifier)) errors.push(`Unknown modifier: '${modifier}'`);
|
|
64
63
|
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 (!
|
|
64
|
+
if (physicalCodeOf(keyPart) === void 0 && !isKeyNameShape(keyPart)) warnings.push(`Unknown key: '${keyPart}'. Key names are case-sensitive, such as 'Escape' and 'F1'.`);
|
|
66
65
|
return {
|
|
67
66
|
valid: errors.length === 0,
|
|
68
67
|
warnings,
|
|
69
68
|
errors
|
|
70
69
|
};
|
|
71
70
|
}
|
|
72
|
-
/** Physical codes, single characters, and named keys are known. */
|
|
73
|
-
function isKnownKey(key) {
|
|
74
|
-
return physicalCodeOf(key) !== void 0 || Array.from(key).length === 1 || ALL_KEYS.has(key);
|
|
75
|
-
}
|
|
76
71
|
/**
|
|
77
72
|
* Validates a hotkey and throws an error if invalid.
|
|
78
73
|
* Useful for development-time validation.
|
|
@@ -91,8 +86,8 @@ function assertValidHotkey(hotkey) {
|
|
|
91
86
|
if (!result.valid) throw new Error(`Invalid hotkey '${String(hotkey)}': ${result.errors.join(", ")}`);
|
|
92
87
|
}
|
|
93
88
|
/**
|
|
94
|
-
* Validates a hotkey and logs warnings to the console.
|
|
95
|
-
* Useful for development-time feedback.
|
|
89
|
+
* Validates a hotkey and logs errors and warnings to the console.
|
|
90
|
+
* Useful for development-time feedback. Logs nothing in production builds.
|
|
96
91
|
*
|
|
97
92
|
* @param hotkey - The hotkey string to validate
|
|
98
93
|
* @returns True if the hotkey is valid (may still have warnings)
|
|
@@ -106,8 +101,10 @@ function assertValidHotkey(hotkey) {
|
|
|
106
101
|
*/
|
|
107
102
|
function checkHotkey(hotkey) {
|
|
108
103
|
const result = validateHotkey(hotkey);
|
|
109
|
-
if (
|
|
110
|
-
|
|
104
|
+
if (process.env.NODE_ENV !== "production") {
|
|
105
|
+
if (result.errors.length > 0) console.error(`Hotkey '${String(hotkey)}' errors:`, result.errors.join("; "));
|
|
106
|
+
if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}' warnings:`, result.warnings.join("; "));
|
|
107
|
+
}
|
|
111
108
|
return result.valid;
|
|
112
109
|
}
|
|
113
110
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Observer, Subscription } from "./types.js";
|
|
2
|
+
//#region ../../node_modules/.pnpm/@tanstack+store@0.11.2/node_modules/@tanstack/store/dist/store.d.ts
|
|
3
|
+
//#region src/store.d.ts
|
|
4
|
+
type StoreAction = (...args: Array<any>) => any;
|
|
5
|
+
type StoreActionMap = Record<string, StoreAction>;
|
|
6
|
+
type StoreActionsFactory<T, TActions extends StoreActionMap> = (store: {
|
|
7
|
+
setState: Store<T>['setState'];
|
|
8
|
+
get: Store<T>['get'];
|
|
9
|
+
}) => TActions;
|
|
10
|
+
type NonFunction<T> = T extends ((...args: Array<any>) => any) ? never : T;
|
|
11
|
+
declare class Store<T, TActions extends StoreActionMap = never> {
|
|
12
|
+
private atom;
|
|
13
|
+
readonly actions: TActions;
|
|
14
|
+
constructor(getValue: (prev?: NoInfer<T>) => T);
|
|
15
|
+
constructor(initialValue: T);
|
|
16
|
+
constructor(initialValue: NonFunction<T>, actionsFactory: StoreActionsFactory<T, TActions>);
|
|
17
|
+
setState(updater: (prev: T) => T): void;
|
|
18
|
+
get state(): T;
|
|
19
|
+
get(): T;
|
|
20
|
+
subscribe(observerOrFn: Observer<T> | ((value: T) => void)): Subscription;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { Store, StoreAction, StoreActionMap, StoreActionsFactory };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region ../../node_modules/.pnpm/@tanstack+store@0.11.2/node_modules/@tanstack/store/dist/types.d.ts
|
|
2
|
+
type Observer<T> = {
|
|
3
|
+
next?: (value: T) => void;
|
|
4
|
+
error?: (err: unknown) => void;
|
|
5
|
+
complete?: () => void;
|
|
6
|
+
};
|
|
7
|
+
interface Subscription {
|
|
8
|
+
unsubscribe: () => void;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { Observer, Subscription };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/hotkeys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,9 +39,12 @@
|
|
|
39
39
|
"dist",
|
|
40
40
|
"skills"
|
|
41
41
|
],
|
|
42
|
-
"
|
|
42
|
+
"devDependencies": {
|
|
43
43
|
"@tanstack/store": "^0.11.2"
|
|
44
44
|
},
|
|
45
|
+
"inlinedDependencies": {
|
|
46
|
+
"@tanstack/store": "0.11.2"
|
|
47
|
+
},
|
|
45
48
|
"scripts": {
|
|
46
49
|
"clean": "premove ./build ./dist",
|
|
47
50
|
"lint": "eslint ./src",
|
|
@@ -33,7 +33,7 @@ The library owns the keyboard. Register every shortcut with your framework adapt
|
|
|
33
33
|
| Instead of | Use |
|
|
34
34
|
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
35
|
| A `keydown` listener that checks `event.key`, `metaKey`, or `ctrlKey` | A registered hotkey such as `'Mod+S'`. Inside a widget's own key handler, `matchesKeyboardEvent(event, 'Mod+Enter')`. |
|
|
36
|
-
| `isMac` checks and `⌘`/`Ctrl` label maps | `Mod` in the hotkey,
|
|
36
|
+
| `isMac` checks and `⌘`/`Ctrl` label maps | `Mod` in the hotkey, `formatForDisplay(hotkey)` for labels, or `formatForDisplay({ key })` for held keys |
|
|
37
37
|
| Skipping events from text fields, calling `preventDefault` | The defaults: single keys and Shift/Alt chords are skipped while typing, `Mod` chords and `Escape` still fire, and matched keys are prevented |
|
|
38
38
|
| A shortcut registry, service, context, or wrapper hook | The adapter API, called where each action lives. For one list of every shortcut, load `@tanstack/hotkeys#app-hotkeys`. |
|
|
39
39
|
| A "shortcuts enabled" flag or topmost-dialog tracking | Scopes, with `exclusive` for dialogs |
|
|
@@ -59,6 +59,7 @@ When nesting scopes, stacking dialogs, or keeping some hotkeys working during an
|
|
|
59
59
|
- **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog. Add `preventDefault: 'always'` to keep the action blocked while the hotkey can't fire, whether it's disabled or its scope is off. Skip it for hotkeys turned off to give the key back to the browser, such as `Mod+C`, `Mod+Z`, `Tab`, or single keys behind a user's off switch. Don't keep the hotkey enabled and return early from the callback; the hotkey would also stay `active` in app hotkey lists and show its hint.
|
|
60
60
|
- **Modifiers match exactly.** `ArrowDown` never fires while Shift is held, so register `Shift+ArrowDown` instead of checking `event.shiftKey`. Shift that a symbol needs is the exception: `?` matches Shift+/.
|
|
61
61
|
- **Chords repeat while held.** Set `requireReset: true` on toggles, or holding the keys flickers them.
|
|
62
|
+
- **Key names are case-sensitive.** Write `Escape`, `ArrowUp`, `F1`, and `+` as the types spell them; `escape`, `Esc`, `f1`, and `Plus` never match. Only letters match in any case. Hotkeys from plain JavaScript or saved data skip the types, so check them with `validateHotkey`.
|
|
62
63
|
- **Letters follow the typed character.** For position-based controls such as WASD, use physical bindings such as `'[KeyW]'`. Keep letters for mnemonic commands such as `Mod+S`.
|
|
63
64
|
- **Each open exclusive scope needs its own name.** A reusable dialog with a fixed scope name breaks when one dialog opens another: both share one scope and both handle `Escape`. Build the name per instance.
|
|
64
65
|
- **A chord on a sequence's first key runs every time the sequence starts.** With `G` and `['G', 'G']`, the first `G` runs the chord. Later keys belong to the sequence. To show "waiting for the next key", read `matchedStepCount` from the adapter's registrations list.
|