@tanstack/hotkeys 0.19.0 → 0.21.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.d.ts +14 -0
- package/dist/_registration.js +33 -2
- package/dist/_store.d.ts +14 -0
- package/dist/_store.js +43 -0
- package/dist/adapter.d.ts +16 -5
- package/dist/adapter.js +16 -24
- package/dist/app-hotkeys.d.ts +19 -4
- package/dist/app-hotkeys.js +62 -12
- 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/hint.d.ts +3 -2
- package/dist/hint.js +3 -1
- package/dist/hotkey-manager.d.ts +4 -12
- package/dist/hotkey-manager.js +35 -41
- 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/match.js +4 -1
- 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/app-hotkeys/SKILL.md +1 -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 };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/_registration.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Merges option layers left to right. A key set to `undefined` keeps the
|
|
4
|
+
* earlier layer's value, so an unset option keeps its default. Getters are
|
|
5
|
+
* read once.
|
|
6
|
+
*/
|
|
7
|
+
export declare function definedOptions<T extends object>(...layers: ReadonlyArray<object | undefined>): T;
|
|
8
|
+
/**
|
|
9
|
+
* Merges options over defaults for an API whose `setOptions` treats
|
|
10
|
+
* `undefined` as "clear". An unset option takes its default, or stays
|
|
11
|
+
* `undefined` to clear an earlier value when it has no default.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withDefaults<T extends object>(defaults: object | undefined, options: T | undefined): T;
|
|
14
|
+
//#endregion
|
package/dist/_registration.js
CHANGED
|
@@ -16,6 +16,37 @@ const defaultHotkeyOptions = {
|
|
|
16
16
|
scope: GLOBAL_SCOPE
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
+
* Merges option layers left to right. A key set to `undefined` keeps the
|
|
20
|
+
* earlier layer's value, so an unset option keeps its default. Getters are
|
|
21
|
+
* read once.
|
|
22
|
+
*/
|
|
23
|
+
function definedOptions(...layers) {
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const layer of layers) {
|
|
26
|
+
if (!layer) continue;
|
|
27
|
+
for (const key of Object.keys(layer)) {
|
|
28
|
+
const value = layer[key];
|
|
29
|
+
if (value !== void 0) result[key] = value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Merges options over defaults for an API whose `setOptions` treats
|
|
36
|
+
* `undefined` as "clear". An unset option takes its default, or stays
|
|
37
|
+
* `undefined` to clear an earlier value when it has no default.
|
|
38
|
+
*/
|
|
39
|
+
function withDefaults(defaults, options) {
|
|
40
|
+
return {
|
|
41
|
+
...options,
|
|
42
|
+
...definedOptions(defaults, options)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Whether a parsed chord has a key. An empty one would match key-less events. */
|
|
46
|
+
function chordHasKey(chord) {
|
|
47
|
+
return chord.code !== void 0 || !!chord.key;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
19
50
|
* Resolves `ignoreInputs: 'auto'` from the hotkey (the first step of a sequence).
|
|
20
51
|
* Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
|
|
21
52
|
*/
|
|
@@ -35,7 +66,7 @@ function getDefaultIgnoreInputs(parsedChord) {
|
|
|
35
66
|
function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
|
|
36
67
|
if (conflictBehavior === "allow") return;
|
|
37
68
|
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.`);
|
|
69
|
+
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
70
|
return;
|
|
40
71
|
}
|
|
41
72
|
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.`);
|
|
@@ -50,4 +81,4 @@ function optionsEqual(left, right) {
|
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
//#endregion
|
|
53
|
-
export { GLOBAL_SCOPE, defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual };
|
|
84
|
+
export { GLOBAL_SCOPE, chordHasKey, defaultHotkeyOptions, definedOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual, withDefaults };
|
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,9 @@
|
|
|
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";
|
|
6
|
+
import { definedOptions, withDefaults } from "./_registration.js";
|
|
4
7
|
//#region src/adapter.d.ts
|
|
5
8
|
/**
|
|
6
9
|
* A hotkey and its callback, with optional per-binding options. Chords accept
|
|
@@ -14,6 +17,11 @@ export type HotkeyDefinition = {
|
|
|
14
17
|
hotkey: RegisterableHotkeySequence;
|
|
15
18
|
callback: HotkeyCallback;
|
|
16
19
|
options?: HotkeySequenceOptions;
|
|
20
|
+
} | {
|
|
21
|
+
/** No hotkey. Registers nothing. */
|
|
22
|
+
hotkey: null | undefined;
|
|
23
|
+
callback: HotkeyCallback;
|
|
24
|
+
options?: HotkeyChordOptions & HotkeySequenceOptions;
|
|
17
25
|
};
|
|
18
26
|
/** Options shared by every definition. Each option applies only to the kinds that support it. */
|
|
19
27
|
export type HotkeyBindingsCommonOptions = HotkeyChordOptions & HotkeySequenceOptions;
|
|
@@ -21,7 +29,8 @@ export type HotkeyBindingsCommonOptions = HotkeyChordOptions & HotkeySequenceOpt
|
|
|
21
29
|
export declare function createHotkeyBindings(): {
|
|
22
30
|
/**
|
|
23
31
|
* Registers new definitions, updates existing ones in place, and removes
|
|
24
|
-
* stale ones. Definitions with
|
|
32
|
+
* stale ones. Definitions with no hotkey (undefined, null, empty string, or
|
|
33
|
+
* empty sequence) or a null target are skipped.
|
|
25
34
|
*/
|
|
26
35
|
update(definitions: ReadonlyArray<HotkeyDefinition>, commonOptions?: HotkeyBindingsCommonOptions): void;
|
|
27
36
|
destroy(): void;
|
|
@@ -77,9 +86,10 @@ export type AdapterHotkeyDefinition<TTarget> = {
|
|
|
77
86
|
} | {
|
|
78
87
|
/**
|
|
79
88
|
* A hotkey whose kind is not known statically, such as user data typed
|
|
80
|
-
* `Hotkey`. Accepts only shared options.
|
|
89
|
+
* `Hotkey`. Accepts only shared options. `undefined`, `null`, and `''`
|
|
90
|
+
* register nothing, for an optional shortcut.
|
|
81
91
|
*/
|
|
82
|
-
hotkey: RegisterableHotkey;
|
|
92
|
+
hotkey: RegisterableHotkey | null | undefined;
|
|
83
93
|
callback: HotkeyCallback;
|
|
84
94
|
options?: AdapterHotkeyOptions<TTarget>;
|
|
85
95
|
};
|
|
@@ -94,7 +104,7 @@ export interface PrepareHotkeyDefinitionsOptions<TTarget> {
|
|
|
94
104
|
/**
|
|
95
105
|
* Resolves a framework target, such as a ref. Return `undefined` for the
|
|
96
106
|
* document or `null` to skip registration until the target exists.
|
|
97
|
-
* Without it, targets pass through
|
|
107
|
+
* Without it, targets pass through, so `null` skips registration.
|
|
98
108
|
*/
|
|
99
109
|
resolveTarget?: (target: TTarget) => Target | null | undefined;
|
|
100
110
|
}
|
|
@@ -160,4 +170,5 @@ export declare function getHotkeysProviderScopeKey(options: {
|
|
|
160
170
|
* Returns a function that releases the activation.
|
|
161
171
|
*/
|
|
162
172
|
export declare function activateHotkeysProviderScope(key: string): () => void;
|
|
163
|
-
//#endregion
|
|
173
|
+
//#endregion
|
|
174
|
+
export { type Store, createAppHotkeysStore, createStore, definedOptions, withDefaults };
|
package/dist/adapter.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
|
-
import { isHotkeySequence, normalizeHotkey } from "./parse.js";
|
|
3
|
-
import { defaultHotkeyOptions } from "./_registration.js";
|
|
2
|
+
import { isHotkeySequence, normalizeHotkey, parseHotkey } from "./parse.js";
|
|
3
|
+
import { chordHasKey, defaultHotkeyOptions, definedOptions, withDefaults } from "./_registration.js";
|
|
4
|
+
import { createStore } from "./_store.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
|
|
@@ -11,24 +13,23 @@ function createHotkeyBindings() {
|
|
|
11
13
|
return {
|
|
12
14
|
/**
|
|
13
15
|
* Registers new definitions, updates existing ones in place, and removes
|
|
14
|
-
* stale ones. Definitions with
|
|
16
|
+
* stale ones. Definitions with no hotkey (undefined, null, empty string, or
|
|
17
|
+
* empty sequence) or a null target are skipped.
|
|
15
18
|
*/
|
|
16
19
|
update(definitions, commonOptions = {}) {
|
|
17
20
|
const prepared = [];
|
|
18
21
|
definitions.forEach((definition, index) => {
|
|
19
|
-
const merged =
|
|
20
|
-
...commonOptions,
|
|
21
|
-
...definition.options
|
|
22
|
-
};
|
|
22
|
+
const merged = definedOptions(commonOptions, definition.options);
|
|
23
23
|
const options = {
|
|
24
24
|
...defaultHotkeyOptions,
|
|
25
25
|
...merged,
|
|
26
|
-
ignoreInputs: merged.ignoreInputs ?? "auto",
|
|
27
26
|
target: "target" in merged ? merged.target ?? null : typeof document === "undefined" ? null : document,
|
|
28
27
|
platform: merged.platform ?? detectPlatform()
|
|
29
28
|
};
|
|
30
|
-
if (!options.target || isHotkeySequence(definition.hotkey) && definition.hotkey.length === 0) return;
|
|
31
|
-
const
|
|
29
|
+
if (!options.target || !definition.hotkey || isHotkeySequence(definition.hotkey) && definition.hotkey.length === 0) return;
|
|
30
|
+
const parsed = parseHotkey(definition.hotkey, options.platform);
|
|
31
|
+
if (!(Array.isArray(parsed) ? parsed : [parsed]).every(chordHasKey)) return;
|
|
32
|
+
const hotkey = normalizeHotkey(parsed, options.platform);
|
|
32
33
|
if (isHotkeySequence(hotkey)) delete options.requireReset;
|
|
33
34
|
else delete options.timeout;
|
|
34
35
|
prepared.push({
|
|
@@ -87,13 +88,8 @@ function prepareHotkeyDefinitions(definitions, options = {}) {
|
|
|
87
88
|
declared = getAppHotkeyRegistrationOptions(appHotkey);
|
|
88
89
|
hotkey = appHotkey.hotkey ?? [];
|
|
89
90
|
}
|
|
90
|
-
const { target, ...merged } =
|
|
91
|
-
|
|
92
|
-
...declared,
|
|
93
|
-
...options.commonOptions,
|
|
94
|
-
...definition.options
|
|
95
|
-
};
|
|
96
|
-
const resolved = target === void 0 ? void 0 : options.resolveTarget ? options.resolveTarget(target) : target ?? void 0;
|
|
91
|
+
const { target, ...merged } = definedOptions(options.defaults, declared, options.commonOptions, definition.options);
|
|
92
|
+
const resolved = target === void 0 ? void 0 : options.resolveTarget ? options.resolveTarget(target) : target;
|
|
97
93
|
return {
|
|
98
94
|
hotkey,
|
|
99
95
|
callback: definition.callback,
|
|
@@ -134,14 +130,10 @@ function subscribePublishedAppHotkeys(onChange) {
|
|
|
134
130
|
function mergeHotkeysProviderDefaults(parent, own, scope) {
|
|
135
131
|
return {
|
|
136
132
|
hotkey: {
|
|
137
|
-
...parent?.hotkey,
|
|
138
|
-
...own?.hotkey,
|
|
133
|
+
...definedOptions(parent?.hotkey, own?.hotkey),
|
|
139
134
|
...scope && { scope }
|
|
140
135
|
},
|
|
141
|
-
hotkeyRecorder:
|
|
142
|
-
...parent?.hotkeyRecorder,
|
|
143
|
-
...own?.hotkeyRecorder
|
|
144
|
-
}
|
|
136
|
+
hotkeyRecorder: definedOptions(parent?.hotkeyRecorder, own?.hotkeyRecorder)
|
|
145
137
|
};
|
|
146
138
|
}
|
|
147
139
|
/**
|
|
@@ -171,4 +163,4 @@ function activateHotkeysProviderScope(key) {
|
|
|
171
163
|
}
|
|
172
164
|
|
|
173
165
|
//#endregion
|
|
174
|
-
export { activateHotkeysProviderScope, createHotkeyBindings, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
|
|
166
|
+
export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, definedOptions, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys, withDefaults };
|
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';
|
|
@@ -135,7 +135,13 @@ export interface AppHotkeysState<T extends AppHotkeys = AppHotkeys> {
|
|
|
135
135
|
}
|
|
136
136
|
/** Options for `AppHotkeysStore`. */
|
|
137
137
|
export interface AppHotkeysStoreOptions<T extends AppHotkeys = AppHotkeys> {
|
|
138
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* The user's saved custom hotkeys. Invalid entries are ignored with a
|
|
140
|
+
* warning. Passing a different value to `setOptions` later, such as data
|
|
141
|
+
* that finished loading or another user's data, replaces the current
|
|
142
|
+
* custom hotkeys. `undefined` leaves them unchanged; a change to `null`
|
|
143
|
+
* clears them.
|
|
144
|
+
*/
|
|
139
145
|
customHotkeys?: CustomHotkeys<T> | null;
|
|
140
146
|
/** Called with the custom hotkeys after every change, ready to save. */
|
|
141
147
|
onCustomHotkeysChange?: (customHotkeys: CustomHotkeys<T>) => void;
|
|
@@ -165,8 +171,17 @@ export declare class AppHotkeysStore<T extends AppHotkeys = AppHotkeys> {
|
|
|
165
171
|
get appHotkeys(): T;
|
|
166
172
|
/** The user's hotkeys that differ from the defaults. */
|
|
167
173
|
get customHotkeys(): CustomHotkeys<T>;
|
|
168
|
-
/**
|
|
169
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Updates options. Providers call it with their props on every update.
|
|
176
|
+
*
|
|
177
|
+
* When `customHotkeys` differs from the value last passed in, it replaces
|
|
178
|
+
* the current custom hotkeys. An equal value, such as the same saved data
|
|
179
|
+
* parsed again, keeps changes the user made since. `undefined` changes
|
|
180
|
+
* nothing, such as while saved data loads; `null` clears them, such as on
|
|
181
|
+
* sign-out. Replacing does not call `onCustomHotkeysChange`, because the
|
|
182
|
+
* data came from the app.
|
|
183
|
+
*/
|
|
184
|
+
setOptions(options: AppHotkeysStoreOptions<T>): void;
|
|
170
185
|
/**
|
|
171
186
|
* Replaces the declarations, for example after hot module replacement.
|
|
172
187
|
* Custom hotkeys for ids that still exist are kept.
|
package/dist/app-hotkeys.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
2
|
import { isHotkeySequence, simplifyHotkey } from "./parse.js";
|
|
3
3
|
import { validateHotkey } from "./validate.js";
|
|
4
|
-
import { areHotkeysEqual } from "./match.js";
|
|
5
4
|
import { GLOBAL_SCOPE } from "./_registration.js";
|
|
5
|
+
import { areHotkeysEqual } from "./match.js";
|
|
6
|
+
import { createStore } from "./_store.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,19 +75,44 @@ 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;
|
|
86
88
|
}
|
|
87
89
|
return result;
|
|
88
90
|
}
|
|
91
|
+
/** Whether two saved values hold the same data. Nothing saved equals `{}`. */
|
|
92
|
+
function sameSavedValue(left, right) {
|
|
93
|
+
if (Object.is(left, right)) return true;
|
|
94
|
+
if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((step, i) => Object.is(step, right[i]));
|
|
95
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
96
|
+
if (left === null || right === null) return false;
|
|
97
|
+
const keys = Object.keys(left);
|
|
98
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameSavedValue(left[key], right[key]));
|
|
99
|
+
}
|
|
100
|
+
/** Copies saved custom hotkeys, so later changes made in place are detected. */
|
|
101
|
+
function copySavedCustomHotkeys(saved) {
|
|
102
|
+
if (typeof saved !== "object" || saved === null) return saved;
|
|
103
|
+
const copy = {};
|
|
104
|
+
for (const [id, value] of Object.entries(saved)) copy[id] = Array.isArray(value) ? [...value] : value;
|
|
105
|
+
return copy;
|
|
106
|
+
}
|
|
107
|
+
/** Whether two saved custom hotkeys objects hold the same data. */
|
|
108
|
+
function sameSavedCustomHotkeys(left, right) {
|
|
109
|
+
return sameSavedValue(left === void 0 ? {} : left, right === void 0 ? {} : right);
|
|
110
|
+
}
|
|
111
|
+
/** Whether two sanitized custom hotkeys objects bind the same hotkeys. */
|
|
112
|
+
function sameCustomHotkeys(left, right, platform) {
|
|
113
|
+
const keys = Object.keys(left);
|
|
114
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && hotkeysEqual(left[key] ?? null, right[key] ?? null, platform));
|
|
115
|
+
}
|
|
89
116
|
/** Whether two app hotkeys can apply at the same time. */
|
|
90
117
|
function scopesOverlap(a, b) {
|
|
91
118
|
const left = a.scope ?? "global";
|
|
@@ -119,11 +146,14 @@ var AppHotkeysStore = class {
|
|
|
119
146
|
store;
|
|
120
147
|
#declarations;
|
|
121
148
|
#options;
|
|
149
|
+
/** The `customHotkeys` option last passed in, compared by value. */
|
|
150
|
+
#savedCustomHotkeys;
|
|
122
151
|
constructor(appHotkeys, options = {}) {
|
|
123
152
|
this.#declarations = appHotkeys;
|
|
124
153
|
this.#options = options;
|
|
154
|
+
this.#savedCustomHotkeys = copySavedCustomHotkeys(options.customHotkeys);
|
|
125
155
|
const customHotkeys = sanitizeCustomHotkeys(appHotkeys, options.customHotkeys);
|
|
126
|
-
this.store =
|
|
156
|
+
this.store = createStore({
|
|
127
157
|
appHotkeys: applyCustomHotkeys(appHotkeys, customHotkeys),
|
|
128
158
|
customHotkeys
|
|
129
159
|
});
|
|
@@ -139,12 +169,31 @@ var AppHotkeysStore = class {
|
|
|
139
169
|
get customHotkeys() {
|
|
140
170
|
return this.store.state.customHotkeys;
|
|
141
171
|
}
|
|
142
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Updates options. Providers call it with their props on every update.
|
|
174
|
+
*
|
|
175
|
+
* When `customHotkeys` differs from the value last passed in, it replaces
|
|
176
|
+
* the current custom hotkeys. An equal value, such as the same saved data
|
|
177
|
+
* parsed again, keeps changes the user made since. `undefined` changes
|
|
178
|
+
* nothing, such as while saved data loads; `null` clears them, such as on
|
|
179
|
+
* sign-out. Replacing does not call `onCustomHotkeysChange`, because the
|
|
180
|
+
* data came from the app.
|
|
181
|
+
*/
|
|
143
182
|
setOptions(options) {
|
|
183
|
+
const { customHotkeys, ...rest } = options;
|
|
144
184
|
this.#options = {
|
|
145
185
|
...this.#options,
|
|
146
|
-
...
|
|
186
|
+
...rest
|
|
147
187
|
};
|
|
188
|
+
if (customHotkeys === void 0) return;
|
|
189
|
+
if (sameSavedCustomHotkeys(customHotkeys, this.#savedCustomHotkeys)) return;
|
|
190
|
+
this.#savedCustomHotkeys = copySavedCustomHotkeys(customHotkeys);
|
|
191
|
+
const next = sanitizeCustomHotkeys(this.#declarations, customHotkeys);
|
|
192
|
+
if (sameCustomHotkeys(next, this.store.state.customHotkeys, this.#platform)) return;
|
|
193
|
+
this.store.setState(() => ({
|
|
194
|
+
appHotkeys: applyCustomHotkeys(this.#declarations, next),
|
|
195
|
+
customHotkeys: next
|
|
196
|
+
}));
|
|
148
197
|
}
|
|
149
198
|
/**
|
|
150
199
|
* Replaces the declarations, for example after hot module replacement.
|
|
@@ -161,7 +210,8 @@ var AppHotkeysStore = class {
|
|
|
161
210
|
}
|
|
162
211
|
/** Effective app hotkey for an id, or `undefined` when it is not declared. */
|
|
163
212
|
get(id) {
|
|
164
|
-
|
|
213
|
+
const { appHotkeys } = this.store.state;
|
|
214
|
+
return Object.hasOwn(appHotkeys, id) ? appHotkeys[id] : void 0;
|
|
165
215
|
}
|
|
166
216
|
/**
|
|
167
217
|
* Other app hotkeys that would collide if `id` used `hotkey`. App hotkeys
|
|
@@ -169,7 +219,7 @@ var AppHotkeysStore = class {
|
|
|
169
219
|
* hotkeys are equal or one sequence starts with the other.
|
|
170
220
|
*/
|
|
171
221
|
findConflicts(id, hotkey) {
|
|
172
|
-
const self = this.
|
|
222
|
+
const self = this.get(id);
|
|
173
223
|
if (!self) return [];
|
|
174
224
|
const candidate = simplifyHotkey(hotkey);
|
|
175
225
|
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 +231,9 @@ var AppHotkeysStore = class {
|
|
|
181
231
|
* @returns Conflicting app hotkeys. When any exist, nothing changes.
|
|
182
232
|
*/
|
|
183
233
|
setCustomHotkey(id, hotkey) {
|
|
184
|
-
const appHotkey = this.
|
|
234
|
+
const appHotkey = this.get(id);
|
|
185
235
|
if (!appHotkey) {
|
|
186
|
-
console.warn(`Cannot set a custom hotkey for unknown app hotkey '${id}'`);
|
|
236
|
+
if (process.env.NODE_ENV !== "production") console.warn(`Cannot set a custom hotkey for unknown app hotkey '${id}'`);
|
|
187
237
|
return [];
|
|
188
238
|
}
|
|
189
239
|
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
|
*/
|