@tanstack/hotkeys 0.20.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/_registration.d.ts +14 -0
- package/dist/_registration.js +32 -1
- package/dist/adapter.d.ts +13 -5
- package/dist/adapter.js +14 -24
- package/dist/app-hotkeys.d.ts +18 -3
- package/dist/app-hotkeys.js +50 -3
- package/dist/hint.d.ts +3 -2
- package/dist/hint.js +3 -1
- package/dist/hotkey-manager.d.ts +3 -2
- package/dist/hotkey-manager.js +26 -11
- package/dist/match.js +4 -1
- package/package.json +1 -1
- package/skills/app-hotkeys/SKILL.md +1 -2
|
@@ -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
|
*/
|
|
@@ -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/adapter.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, Regist
|
|
|
3
3
|
import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
|
|
4
4
|
import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
|
|
5
5
|
import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
|
|
6
|
+
import { definedOptions, withDefaults } from "./_registration.js";
|
|
6
7
|
//#region src/adapter.d.ts
|
|
7
8
|
/**
|
|
8
9
|
* A hotkey and its callback, with optional per-binding options. Chords accept
|
|
@@ -16,6 +17,11 @@ export type HotkeyDefinition = {
|
|
|
16
17
|
hotkey: RegisterableHotkeySequence;
|
|
17
18
|
callback: HotkeyCallback;
|
|
18
19
|
options?: HotkeySequenceOptions;
|
|
20
|
+
} | {
|
|
21
|
+
/** No hotkey. Registers nothing. */
|
|
22
|
+
hotkey: null | undefined;
|
|
23
|
+
callback: HotkeyCallback;
|
|
24
|
+
options?: HotkeyChordOptions & HotkeySequenceOptions;
|
|
19
25
|
};
|
|
20
26
|
/** Options shared by every definition. Each option applies only to the kinds that support it. */
|
|
21
27
|
export type HotkeyBindingsCommonOptions = HotkeyChordOptions & HotkeySequenceOptions;
|
|
@@ -23,7 +29,8 @@ export type HotkeyBindingsCommonOptions = HotkeyChordOptions & HotkeySequenceOpt
|
|
|
23
29
|
export declare function createHotkeyBindings(): {
|
|
24
30
|
/**
|
|
25
31
|
* Registers new definitions, updates existing ones in place, and removes
|
|
26
|
-
* 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.
|
|
27
34
|
*/
|
|
28
35
|
update(definitions: ReadonlyArray<HotkeyDefinition>, commonOptions?: HotkeyBindingsCommonOptions): void;
|
|
29
36
|
destroy(): void;
|
|
@@ -79,9 +86,10 @@ export type AdapterHotkeyDefinition<TTarget> = {
|
|
|
79
86
|
} | {
|
|
80
87
|
/**
|
|
81
88
|
* A hotkey whose kind is not known statically, such as user data typed
|
|
82
|
-
* `Hotkey`. Accepts only shared options.
|
|
89
|
+
* `Hotkey`. Accepts only shared options. `undefined`, `null`, and `''`
|
|
90
|
+
* register nothing, for an optional shortcut.
|
|
83
91
|
*/
|
|
84
|
-
hotkey: RegisterableHotkey;
|
|
92
|
+
hotkey: RegisterableHotkey | null | undefined;
|
|
85
93
|
callback: HotkeyCallback;
|
|
86
94
|
options?: AdapterHotkeyOptions<TTarget>;
|
|
87
95
|
};
|
|
@@ -96,7 +104,7 @@ export interface PrepareHotkeyDefinitionsOptions<TTarget> {
|
|
|
96
104
|
/**
|
|
97
105
|
* Resolves a framework target, such as a ref. Return `undefined` for the
|
|
98
106
|
* document or `null` to skip registration until the target exists.
|
|
99
|
-
* Without it, targets pass through
|
|
107
|
+
* Without it, targets pass through, so `null` skips registration.
|
|
100
108
|
*/
|
|
101
109
|
resolveTarget?: (target: TTarget) => Target | null | undefined;
|
|
102
110
|
}
|
|
@@ -163,4 +171,4 @@ export declare function getHotkeysProviderScopeKey(options: {
|
|
|
163
171
|
*/
|
|
164
172
|
export declare function activateHotkeysProviderScope(key: string): () => void;
|
|
165
173
|
//#endregion
|
|
166
|
-
export { type Store, createAppHotkeysStore, createStore };
|
|
174
|
+
export { type Store, createAppHotkeysStore, createStore, definedOptions, withDefaults };
|
package/dist/adapter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
|
-
import { isHotkeySequence, normalizeHotkey } from "./parse.js";
|
|
2
|
+
import { isHotkeySequence, normalizeHotkey, parseHotkey } from "./parse.js";
|
|
3
|
+
import { chordHasKey, defaultHotkeyOptions, definedOptions, withDefaults } from "./_registration.js";
|
|
3
4
|
import { createStore } from "./_store.js";
|
|
4
|
-
import { defaultHotkeyOptions } from "./_registration.js";
|
|
5
5
|
import { getHotkeyManager } from "./hotkey-manager.js";
|
|
6
6
|
import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
|
|
7
7
|
import { getAppHotkeyRegistrationOptions, isAppHotkey, resolveAppHotkey } from "./app-hotkeys.js";
|
|
@@ -13,24 +13,23 @@ function createHotkeyBindings() {
|
|
|
13
13
|
return {
|
|
14
14
|
/**
|
|
15
15
|
* Registers new definitions, updates existing ones in place, and removes
|
|
16
|
-
* 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.
|
|
17
18
|
*/
|
|
18
19
|
update(definitions, commonOptions = {}) {
|
|
19
20
|
const prepared = [];
|
|
20
21
|
definitions.forEach((definition, index) => {
|
|
21
|
-
const merged =
|
|
22
|
-
...commonOptions,
|
|
23
|
-
...definition.options
|
|
24
|
-
};
|
|
22
|
+
const merged = definedOptions(commonOptions, definition.options);
|
|
25
23
|
const options = {
|
|
26
24
|
...defaultHotkeyOptions,
|
|
27
25
|
...merged,
|
|
28
|
-
ignoreInputs: merged.ignoreInputs ?? "auto",
|
|
29
26
|
target: "target" in merged ? merged.target ?? null : typeof document === "undefined" ? null : document,
|
|
30
27
|
platform: merged.platform ?? detectPlatform()
|
|
31
28
|
};
|
|
32
|
-
if (!options.target || isHotkeySequence(definition.hotkey) && definition.hotkey.length === 0) return;
|
|
33
|
-
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);
|
|
34
33
|
if (isHotkeySequence(hotkey)) delete options.requireReset;
|
|
35
34
|
else delete options.timeout;
|
|
36
35
|
prepared.push({
|
|
@@ -89,13 +88,8 @@ function prepareHotkeyDefinitions(definitions, options = {}) {
|
|
|
89
88
|
declared = getAppHotkeyRegistrationOptions(appHotkey);
|
|
90
89
|
hotkey = appHotkey.hotkey ?? [];
|
|
91
90
|
}
|
|
92
|
-
const { target, ...merged } =
|
|
93
|
-
|
|
94
|
-
...declared,
|
|
95
|
-
...options.commonOptions,
|
|
96
|
-
...definition.options
|
|
97
|
-
};
|
|
98
|
-
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;
|
|
99
93
|
return {
|
|
100
94
|
hotkey,
|
|
101
95
|
callback: definition.callback,
|
|
@@ -136,14 +130,10 @@ function subscribePublishedAppHotkeys(onChange) {
|
|
|
136
130
|
function mergeHotkeysProviderDefaults(parent, own, scope) {
|
|
137
131
|
return {
|
|
138
132
|
hotkey: {
|
|
139
|
-
...parent?.hotkey,
|
|
140
|
-
...own?.hotkey,
|
|
133
|
+
...definedOptions(parent?.hotkey, own?.hotkey),
|
|
141
134
|
...scope && { scope }
|
|
142
135
|
},
|
|
143
|
-
hotkeyRecorder:
|
|
144
|
-
...parent?.hotkeyRecorder,
|
|
145
|
-
...own?.hotkeyRecorder
|
|
146
|
-
}
|
|
136
|
+
hotkeyRecorder: definedOptions(parent?.hotkeyRecorder, own?.hotkeyRecorder)
|
|
147
137
|
};
|
|
148
138
|
}
|
|
149
139
|
/**
|
|
@@ -173,4 +163,4 @@ function activateHotkeysProviderScope(key) {
|
|
|
173
163
|
}
|
|
174
164
|
|
|
175
165
|
//#endregion
|
|
176
|
-
export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
|
|
166
|
+
export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, definedOptions, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys, withDefaults };
|
package/dist/app-hotkeys.d.ts
CHANGED
|
@@ -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,9 +1,9 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
2
|
import { isHotkeySequence, simplifyHotkey } from "./parse.js";
|
|
3
3
|
import { validateHotkey } from "./validate.js";
|
|
4
|
+
import { GLOBAL_SCOPE } from "./_registration.js";
|
|
4
5
|
import { areHotkeysEqual } from "./match.js";
|
|
5
6
|
import { createStore } from "./_store.js";
|
|
6
|
-
import { GLOBAL_SCOPE } from "./_registration.js";
|
|
7
7
|
import { getHotkeyManager } from "./hotkey-manager.js";
|
|
8
8
|
import { setAppHotkeysStoreFactory } from "./_app-hotkeys-factory.js";
|
|
9
9
|
|
|
@@ -88,6 +88,31 @@ function sanitizeCustomHotkeys(appHotkeys, saved) {
|
|
|
88
88
|
}
|
|
89
89
|
return result;
|
|
90
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
|
+
}
|
|
91
116
|
/** Whether two app hotkeys can apply at the same time. */
|
|
92
117
|
function scopesOverlap(a, b) {
|
|
93
118
|
const left = a.scope ?? "global";
|
|
@@ -121,9 +146,12 @@ var AppHotkeysStore = class {
|
|
|
121
146
|
store;
|
|
122
147
|
#declarations;
|
|
123
148
|
#options;
|
|
149
|
+
/** The `customHotkeys` option last passed in, compared by value. */
|
|
150
|
+
#savedCustomHotkeys;
|
|
124
151
|
constructor(appHotkeys, options = {}) {
|
|
125
152
|
this.#declarations = appHotkeys;
|
|
126
153
|
this.#options = options;
|
|
154
|
+
this.#savedCustomHotkeys = copySavedCustomHotkeys(options.customHotkeys);
|
|
127
155
|
const customHotkeys = sanitizeCustomHotkeys(appHotkeys, options.customHotkeys);
|
|
128
156
|
this.store = createStore({
|
|
129
157
|
appHotkeys: applyCustomHotkeys(appHotkeys, customHotkeys),
|
|
@@ -141,12 +169,31 @@ var AppHotkeysStore = class {
|
|
|
141
169
|
get customHotkeys() {
|
|
142
170
|
return this.store.state.customHotkeys;
|
|
143
171
|
}
|
|
144
|
-
/**
|
|
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
|
+
*/
|
|
145
182
|
setOptions(options) {
|
|
183
|
+
const { customHotkeys, ...rest } = options;
|
|
146
184
|
this.#options = {
|
|
147
185
|
...this.#options,
|
|
148
|
-
...
|
|
186
|
+
...rest
|
|
149
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
|
+
}));
|
|
150
197
|
}
|
|
151
198
|
/**
|
|
152
199
|
* Replaces the declarations, for example after hot module replacement.
|
package/dist/hint.d.ts
CHANGED
|
@@ -11,11 +11,12 @@ export interface HeldModifierOptions {
|
|
|
11
11
|
* By default, any nonempty subset of the binding's modifiers qualifies; an
|
|
12
12
|
* unrelated modifier hides the hint. `exact` requires every binding modifier.
|
|
13
13
|
* Nonmodifier keys are ignored. This does not check registration or focus.
|
|
14
|
-
* A sequence reveals its hint from its first step.
|
|
14
|
+
* A sequence reveals its hint from its first step. Returns false for no
|
|
15
|
+
* hotkey (`undefined`, `null`, or `''`).
|
|
15
16
|
*
|
|
16
17
|
* @example
|
|
17
18
|
* matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
|
|
18
19
|
* matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt', 'Control']) // false
|
|
19
20
|
*/
|
|
20
|
-
export declare function matchesHeldModifiers(hotkey: RegisterableHotkey, heldKeys: ReadonlyArray<string>, options?: HeldModifierOptions): boolean;
|
|
21
|
+
export declare function matchesHeldModifiers(hotkey: RegisterableHotkey | null | undefined, heldKeys: ReadonlyArray<string>, options?: HeldModifierOptions): boolean;
|
|
21
22
|
//#endregion
|
package/dist/hint.js
CHANGED
|
@@ -7,13 +7,15 @@ import { isHotkeySequence, parseHotkey } from "./parse.js";
|
|
|
7
7
|
* By default, any nonempty subset of the binding's modifiers qualifies; an
|
|
8
8
|
* unrelated modifier hides the hint. `exact` requires every binding modifier.
|
|
9
9
|
* Nonmodifier keys are ignored. This does not check registration or focus.
|
|
10
|
-
* A sequence reveals its hint from its first step.
|
|
10
|
+
* A sequence reveals its hint from its first step. Returns false for no
|
|
11
|
+
* hotkey (`undefined`, `null`, or `''`).
|
|
11
12
|
*
|
|
12
13
|
* @example
|
|
13
14
|
* matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
|
|
14
15
|
* matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt', 'Control']) // false
|
|
15
16
|
*/
|
|
16
17
|
function matchesHeldModifiers(hotkey, heldKeys, options = {}) {
|
|
18
|
+
if (!hotkey) return false;
|
|
17
19
|
if (heldKeys.includes("AltGraph")) return false;
|
|
18
20
|
const held = [...new Set(heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)))];
|
|
19
21
|
const first = isHotkeySequence(hotkey) ? hotkey[0] : hotkey;
|
package/dist/hotkey-manager.d.ts
CHANGED
|
@@ -385,10 +385,11 @@ export declare class HotkeyManager {
|
|
|
385
385
|
* only while focus is inside that element. An app hotkey matches
|
|
386
386
|
* registrations by id, so custom hotkeys apply.
|
|
387
387
|
*
|
|
388
|
-
* @param hotkey - A chord, sequence, or app hotkey
|
|
388
|
+
* @param hotkey - A chord, sequence, or app hotkey. No hotkey
|
|
389
|
+
* (`undefined`, `null`, or `''`) returns false.
|
|
389
390
|
* @returns True if a registration that can fire exists
|
|
390
391
|
*/
|
|
391
|
-
hasActiveRegistration(hotkey: RegisterableHotkey | AppHotkey): boolean;
|
|
392
|
+
hasActiveRegistration(hotkey: RegisterableHotkey | AppHotkey | null | undefined): boolean;
|
|
392
393
|
/**
|
|
393
394
|
* Destroys the manager and removes all listeners.
|
|
394
395
|
*/
|
package/dist/hotkey-manager.js
CHANGED
|
@@ -4,13 +4,13 @@ import { isKeyNameShape, keysEqual, normalizeKeyboardEvent } from "./_keyboard-e
|
|
|
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
|
+
import { GLOBAL_SCOPE, chordHasKey, defaultHotkeyOptions, definedOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
|
|
7
8
|
import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
|
|
8
9
|
import { getTargetDocument, isDocumentLevel, shouldIgnoreInputEvent, targetHasFocus } from "./_event-target.js";
|
|
9
10
|
import { createStore } from "./_store.js";
|
|
10
11
|
import { TargetListeners } from "./_target-listeners.js";
|
|
11
12
|
import { managerRef } from "./_manager-ref.js";
|
|
12
13
|
import { isRecordingEvent } from "./_recording-guard.js";
|
|
13
|
-
import { GLOBAL_SCOPE, defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
|
|
14
14
|
|
|
15
15
|
//#region src/hotkey-manager.ts
|
|
16
16
|
/** Whether a registration can fire now. Without a manager, scopes don't apply. */
|
|
@@ -57,6 +57,13 @@ function chordString(chord, parsed) {
|
|
|
57
57
|
function generateId() {
|
|
58
58
|
return `hotkey_${++registrationIdCounter}`;
|
|
59
59
|
}
|
|
60
|
+
/** Defaults for a registration of the given kind. */
|
|
61
|
+
function registrationDefaults(kind) {
|
|
62
|
+
return {
|
|
63
|
+
...defaultHotkeyOptions,
|
|
64
|
+
...kind === "sequence" ? { timeout: DEFAULT_SEQUENCE_TIMEOUT } : { requireReset: false }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
60
67
|
/** Drops options that do not apply to a registration's kind. */
|
|
61
68
|
function optionsForKind(kind, options) {
|
|
62
69
|
if (kind === "chord") {
|
|
@@ -296,6 +303,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
296
303
|
this.registrations.setState((prev) => new Map(prev));
|
|
297
304
|
}
|
|
298
305
|
register(input, callback, options = {}) {
|
|
306
|
+
if (input == null) throw new Error("A hotkey is required. Skip registration when there is no hotkey.");
|
|
299
307
|
const hotkey = simplifyHotkey(input);
|
|
300
308
|
if (isHotkeySequence(hotkey) && hotkey.length === 0) throw new Error("Sequence must contain at least one hotkey");
|
|
301
309
|
if (typeof document === "undefined" && !options.target) {
|
|
@@ -319,15 +327,14 @@ var HotkeyManager = class HotkeyManager {
|
|
|
319
327
|
const platform = options.platform ?? this.#platform;
|
|
320
328
|
const target = options.target ?? document;
|
|
321
329
|
const sequence = isHotkeySequence(hotkey);
|
|
330
|
+
const kind = sequence ? "sequence" : "chord";
|
|
322
331
|
const common = {
|
|
323
332
|
id,
|
|
324
333
|
callback,
|
|
325
334
|
options: {
|
|
326
|
-
...
|
|
327
|
-
...
|
|
328
|
-
|
|
329
|
-
platform,
|
|
330
|
-
ignoreInputs: options.ignoreInputs ?? "auto"
|
|
335
|
+
...registrationDefaults(kind),
|
|
336
|
+
...optionsForKind(kind, definedOptions(options)),
|
|
337
|
+
platform
|
|
331
338
|
},
|
|
332
339
|
target,
|
|
333
340
|
triggerCount: 0,
|
|
@@ -353,8 +360,9 @@ var HotkeyManager = class HotkeyManager {
|
|
|
353
360
|
parsedChord
|
|
354
361
|
};
|
|
355
362
|
}
|
|
363
|
+
const steps = registration.kind === "sequence" ? registration.parsedSequence : [registration.parsedChord];
|
|
364
|
+
if (!steps.every(chordHasKey)) throw new Error(`Hotkey '${formatForMessage(registration.hotkey)}' has no key. Skip registration when there is no hotkey.`);
|
|
356
365
|
if (process.env.NODE_ENV !== "production") {
|
|
357
|
-
const steps = registration.kind === "sequence" ? registration.parsedSequence : [registration.parsedChord];
|
|
358
366
|
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
367
|
}
|
|
360
368
|
const conflicting = this.#findConflictingRegistration(registration);
|
|
@@ -380,10 +388,15 @@ var HotkeyManager = class HotkeyManager {
|
|
|
380
388
|
manager.registrations.setState((prev) => {
|
|
381
389
|
const reg = prev.get(id);
|
|
382
390
|
if (!reg) return prev;
|
|
383
|
-
const
|
|
384
|
-
...reg.
|
|
385
|
-
|
|
391
|
+
const defaults = {
|
|
392
|
+
...registrationDefaults(reg.kind),
|
|
393
|
+
platform: reg.options.platform
|
|
386
394
|
};
|
|
395
|
+
const options = { ...reg.options };
|
|
396
|
+
const entries = Object.entries(optionsForKind(reg.kind, newOptions));
|
|
397
|
+
for (const [key, value] of entries) if (value !== void 0) options[key] = value;
|
|
398
|
+
else if (key in defaults) options[key] = defaults[key];
|
|
399
|
+
else delete options[key];
|
|
387
400
|
if (optionsEqual(reg.options, options)) return prev;
|
|
388
401
|
const capture = !!reg.options.capture;
|
|
389
402
|
if (capture !== !!options.capture) {
|
|
@@ -689,10 +702,12 @@ var HotkeyManager = class HotkeyManager {
|
|
|
689
702
|
* only while focus is inside that element. An app hotkey matches
|
|
690
703
|
* registrations by id, so custom hotkeys apply.
|
|
691
704
|
*
|
|
692
|
-
* @param hotkey - A chord, sequence, or app hotkey
|
|
705
|
+
* @param hotkey - A chord, sequence, or app hotkey. No hotkey
|
|
706
|
+
* (`undefined`, `null`, or `''`) returns false.
|
|
693
707
|
* @returns True if a registration that can fire exists
|
|
694
708
|
*/
|
|
695
709
|
hasActiveRegistration(hotkey) {
|
|
710
|
+
if (!hotkey) return false;
|
|
696
711
|
const id = typeof hotkey === "object" && "~appHotkey" in hotkey ? hotkey.id : void 0;
|
|
697
712
|
for (const registration of this.registrations.state.values()) if (this.#isLive(registration) && targetHasFocus(registration.target) && (id === void 0 ? areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) : registration.options.appHotkeyId === id)) return true;
|
|
698
713
|
return false;
|
package/dist/match.js
CHANGED
|
@@ -4,6 +4,7 @@ import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
|
4
4
|
import { isHotkeySequence, isModifierKey, parseHotkey } from "./parse.js";
|
|
5
5
|
import { formatHotkey } from "./format.js";
|
|
6
6
|
import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
|
|
7
|
+
import { chordHasKey } from "./_registration.js";
|
|
7
8
|
|
|
8
9
|
//#region src/match.ts
|
|
9
10
|
/**
|
|
@@ -36,7 +37,9 @@ import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
|
|
|
36
37
|
* ```
|
|
37
38
|
*/
|
|
38
39
|
function matchesKeyboardEvent(event, chord, platform = detectPlatform()) {
|
|
39
|
-
|
|
40
|
+
const parsed = typeof chord === "string" ? parseHotkey(chord, platform) : chord;
|
|
41
|
+
if (!chordHasKey(parsed)) return false;
|
|
42
|
+
return matchKeyboardEvent(event, parsed, platform).matched;
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* Creates a keyboard event handler that calls the callback when the chord
|
package/package.json
CHANGED
|
@@ -46,12 +46,11 @@ export const appHotkeys = defineAppHotkeys({
|
|
|
46
46
|
})
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Augment `Register` in the adapter module you import from, such as `declare module '@tanstack/react-hotkeys' { interface Register { appHotkeys: typeof appHotkeys } }`, to type ids. Lit, Alpine, and Ember have no provider: call `provideAppHotkeys(appHotkeys, options)` once at startup.
|
|
49
|
+
Augment `Register` in the adapter module you import from, such as `declare module '@tanstack/react-hotkeys' { interface Register { appHotkeys: typeof appHotkeys } }`, to type ids. Lit, Alpine, and Ember have no provider: call `provideAppHotkeys(appHotkeys, options)` once at startup. Pass saved data that loads later to `store.setOptions({ customHotkeys })` on the returned store; providers apply a changed `customHotkeys` prop themselves. `undefined` leaves custom hotkeys unchanged while data loads; `null` clears them, such as on sign-out.
|
|
50
50
|
|
|
51
51
|
## Pitfalls
|
|
52
52
|
|
|
53
53
|
- **Register the app hotkey, not its value.** Pass `appHotkeys['doc.save']`. Passing `.hotkey` registers the declared default, so the user's rebinding never applies, and the list reports the shortcut as `'inactive'`.
|
|
54
|
-
- **The provider reads `customHotkeys` once.** When saved shortcuts load asynchronously, render the provider after they arrive, or key it by user. Change shortcuts afterwards through the adapter's custom hotkeys API, not by updating the prop.
|
|
55
54
|
- **Save what `onCustomHotkeysChange` receives.** It holds only the shortcuts the user changed, so users still get new defaults. Saving every effective hotkey freezes today's defaults into their data.
|
|
56
55
|
- **Ids are a storage contract.** Saved data is keyed by id, and unknown ids are dropped with a warning, so renaming an id discards users' customizations.
|
|
57
56
|
- **`setCustomHotkey` returns conflicts and changes nothing when there are any.** Check the result before reporting success, or reject the key press earlier by calling `findConflicts` in the recorder's `validate`.
|