@tanstack/hotkeys 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_match.js +2 -0
- package/dist/adapter.d.ts +108 -19
- package/dist/adapter.js +90 -65
- package/dist/app-hotkeys.d.ts +239 -0
- package/dist/app-hotkeys.js +295 -0
- package/dist/conflicts.d.ts +5 -9
- package/dist/conflicts.js +6 -11
- package/dist/format.d.ts +19 -24
- package/dist/format.js +19 -30
- package/dist/hint.d.ts +1 -0
- package/dist/hint.js +5 -2
- package/dist/hotkey-manager.d.ts +110 -59
- package/dist/hotkey-manager.js +254 -151
- package/dist/hotkey-recorder.d.ts +66 -42
- package/dist/hotkey-recorder.js +148 -107
- package/dist/hotkey.types.d.ts +26 -12
- package/dist/index.d.ts +10 -11
- package/dist/index.js +5 -6
- package/dist/match.d.ts +38 -5
- package/dist/match.js +72 -4
- package/dist/parse.d.ts +20 -8
- package/dist/parse.js +15 -5
- package/dist/recorder-options.d.ts +6 -3
- package/dist/validate.d.ts +10 -4
- package/dist/validate.js +26 -4
- package/package.json +1 -1
- package/dist/hotkey-sequence-recorder.d.ts +0 -84
- package/dist/hotkey-sequence-recorder.js +0 -259
- package/dist/sequence-manager.d.ts +0 -169
- package/dist/sequence-manager.js +0 -443
package/dist/hotkey-manager.js
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
import { normalizeKeyName } from "./constants.js";
|
|
2
2
|
import { detectPlatform } from "./platform.js";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
4
|
+
import { isHotkeySequence, isModifierKey, parseHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
|
|
4
5
|
import { formatHotkey } from "./format.js";
|
|
5
6
|
import { matchKeyboardEvent } from "./_match.js";
|
|
6
|
-
import { areHotkeysEqual } from "./match.js";
|
|
7
|
+
import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
|
|
7
8
|
import { isRecordingEvent } from "./_recording-guard.js";
|
|
8
9
|
import { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
|
|
9
10
|
import { isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
|
|
10
11
|
import { Store } from "@tanstack/store";
|
|
11
12
|
|
|
12
13
|
//#region src/hotkey-manager.ts
|
|
13
|
-
/**
|
|
14
|
-
* Creates a public view from an internal registration,
|
|
15
|
-
* stripping the callback function.
|
|
16
|
-
*/
|
|
14
|
+
/** Creates a public view from an internal registration, stripping the callback. */
|
|
17
15
|
function toHotkeyRegistrationView(reg) {
|
|
18
|
-
|
|
19
|
-
hotkey: reg.hotkey,
|
|
16
|
+
const base = {
|
|
20
17
|
id: reg.id,
|
|
21
|
-
options: reg.options,
|
|
22
|
-
parsedHotkey: reg.parsedHotkey,
|
|
23
18
|
target: reg.target,
|
|
24
19
|
triggerCount: reg.triggerCount,
|
|
25
20
|
hasFired: reg.hasFired
|
|
26
21
|
};
|
|
22
|
+
if (reg.kind === "chord") return {
|
|
23
|
+
...base,
|
|
24
|
+
kind: "chord",
|
|
25
|
+
hotkey: reg.hotkey,
|
|
26
|
+
parsedHotkey: reg.parsedHotkey,
|
|
27
|
+
options: reg.options
|
|
28
|
+
};
|
|
29
|
+
const length = reg.parsedSequence.length;
|
|
30
|
+
const inProgress = reg.currentIndex > 0 && reg.currentIndex < length;
|
|
31
|
+
return {
|
|
32
|
+
...base,
|
|
33
|
+
kind: "sequence",
|
|
34
|
+
hotkey: reg.hotkey,
|
|
35
|
+
parsedSequence: reg.parsedSequence,
|
|
36
|
+
options: reg.options,
|
|
37
|
+
matchedStepCount: inProgress ? reg.currentIndex : 0,
|
|
38
|
+
partialMatchLastKeyTime: inProgress ? reg.lastKeyTime : 0
|
|
39
|
+
};
|
|
27
40
|
}
|
|
28
41
|
let registrationIdCounter = 0;
|
|
29
42
|
/**
|
|
@@ -32,29 +45,55 @@ let registrationIdCounter = 0;
|
|
|
32
45
|
function generateId() {
|
|
33
46
|
return `hotkey_${++registrationIdCounter}`;
|
|
34
47
|
}
|
|
48
|
+
/** Drops options that do not apply to a registration's kind. */
|
|
49
|
+
function optionsForKind(kind, options) {
|
|
50
|
+
if (kind === "chord") {
|
|
51
|
+
const { timeout: _timeout, ...rest } = options;
|
|
52
|
+
return rest;
|
|
53
|
+
}
|
|
54
|
+
const { requireReset: _requireReset, ...rest } = options;
|
|
55
|
+
return rest;
|
|
56
|
+
}
|
|
57
|
+
/** Steps with Mod already resolved for the registration's platform. */
|
|
58
|
+
function resolvedSteps(registration) {
|
|
59
|
+
return registration.kind === "chord" ? [registration.parsedHotkey] : registration.parsedSequence;
|
|
60
|
+
}
|
|
61
|
+
function sameResolvedSteps(left, right) {
|
|
62
|
+
return left.length === right.length && left.every((step, index) => areHotkeysEqual(step, right[index]));
|
|
63
|
+
}
|
|
64
|
+
function formatForMessage(hotkey) {
|
|
65
|
+
return isHotkeySequence(hotkey) ? hotkey.join(" ") : hotkey;
|
|
66
|
+
}
|
|
35
67
|
/**
|
|
36
|
-
* Singleton manager for hotkey registrations.
|
|
68
|
+
* Singleton manager for hotkey registrations, including sequences.
|
|
37
69
|
*
|
|
38
|
-
* This class provides a centralized way to register and manage keyboard
|
|
39
|
-
* It
|
|
40
|
-
* are registered.
|
|
70
|
+
* This class provides a centralized way to register and manage keyboard
|
|
71
|
+
* hotkeys. It attaches one keydown and one keyup listener per target,
|
|
72
|
+
* regardless of how many hotkeys are registered.
|
|
73
|
+
*
|
|
74
|
+
* Sequences (Vim-style hotkeys such as `['G', 'G']`) match when every step is
|
|
75
|
+
* pressed within the timeout. Modifier-only events (Shift, Control, Alt, or
|
|
76
|
+
* Meta alone) neither advance nor reset a sequence, so chained modifier chords
|
|
77
|
+
* such as `['Shift+R', 'Shift+T']` work.
|
|
41
78
|
*
|
|
42
79
|
* @example
|
|
43
80
|
* ```ts
|
|
44
81
|
* const manager = HotkeyManager.getInstance()
|
|
45
82
|
*
|
|
46
|
-
* const
|
|
83
|
+
* const save = manager.register('Mod+S', (event, context) => {
|
|
47
84
|
* console.log('Save triggered!')
|
|
48
85
|
* })
|
|
86
|
+
* const top = manager.register(['G', 'G'], () => scrollToTop(), { timeout: 500 })
|
|
49
87
|
*
|
|
50
88
|
* // Later, to unregister:
|
|
51
|
-
* unregister()
|
|
89
|
+
* save.unregister()
|
|
90
|
+
* top.unregister()
|
|
52
91
|
* ```
|
|
53
92
|
*/
|
|
54
93
|
var HotkeyManager = class HotkeyManager {
|
|
55
94
|
static #instance = null;
|
|
56
95
|
/**
|
|
57
|
-
* The TanStack Store containing all hotkey registrations.
|
|
96
|
+
* The TanStack Store containing all hotkey registrations, chords and sequences.
|
|
58
97
|
* Use this to subscribe to registration changes or access current registrations.
|
|
59
98
|
*
|
|
60
99
|
* @example
|
|
@@ -73,6 +112,11 @@ var HotkeyManager = class HotkeyManager {
|
|
|
73
112
|
* ```
|
|
74
113
|
*/
|
|
75
114
|
registrations = new Store(/* @__PURE__ */ new Map());
|
|
115
|
+
/**
|
|
116
|
+
* The app hotkeys store published by a framework provider, so tools such as
|
|
117
|
+
* devtools can list declared hotkeys that are not registered.
|
|
118
|
+
*/
|
|
119
|
+
appHotkeys = new Store(null);
|
|
76
120
|
#platform;
|
|
77
121
|
#targetListeners = /* @__PURE__ */ new Map();
|
|
78
122
|
#targetRegistrations = /* @__PURE__ */ new Map();
|
|
@@ -95,32 +139,13 @@ var HotkeyManager = class HotkeyManager {
|
|
|
95
139
|
HotkeyManager.#instance = null;
|
|
96
140
|
}
|
|
97
141
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* @param callback - The function to call when the hotkey is pressed
|
|
106
|
-
* @param options - Options for the hotkey behavior
|
|
107
|
-
* @returns A handle for managing the registration
|
|
108
|
-
*
|
|
109
|
-
* @example
|
|
110
|
-
* ```ts
|
|
111
|
-
* const handle = manager.register('Mod+S', callback)
|
|
112
|
-
*
|
|
113
|
-
* // Update callback without re-registering (avoids stale closures)
|
|
114
|
-
* handle.callback = newCallback
|
|
115
|
-
*
|
|
116
|
-
* // Update options
|
|
117
|
-
* handle.setOptions({ enabled: false })
|
|
118
|
-
*
|
|
119
|
-
* // Unregister when done
|
|
120
|
-
* handle.unregister()
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
|
-
register(hotkey, callback, options = {}) {
|
|
142
|
+
/** Notifies subscribers after in-place updates such as counts and sequence progress. */
|
|
143
|
+
#notify() {
|
|
144
|
+
this.registrations.setState((prev) => new Map(prev));
|
|
145
|
+
}
|
|
146
|
+
register(input, callback, options = {}) {
|
|
147
|
+
const hotkey = simplifyHotkey(input);
|
|
148
|
+
if (isHotkeySequence(hotkey) && hotkey.length === 0) throw new Error("Sequence must contain at least one hotkey");
|
|
124
149
|
if (typeof document === "undefined" && !options.target) {
|
|
125
150
|
let currentCallback = callback;
|
|
126
151
|
return {
|
|
@@ -140,29 +165,51 @@ var HotkeyManager = class HotkeyManager {
|
|
|
140
165
|
}
|
|
141
166
|
const id = generateId();
|
|
142
167
|
const platform = options.platform ?? this.#platform;
|
|
143
|
-
const parsedHotkey = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : rawHotkeyToParsedHotkey(hotkey, platform);
|
|
144
|
-
const hotkeyStr = typeof hotkey === "string" ? hotkey : formatHotkey(parsedHotkey);
|
|
145
168
|
const target = options.target ?? document;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
let registration;
|
|
170
|
+
if (isHotkeySequence(hotkey)) {
|
|
171
|
+
const parsedSequence = hotkey.map((step) => parseHotkey(step, platform));
|
|
172
|
+
registration = {
|
|
173
|
+
kind: "sequence",
|
|
174
|
+
id,
|
|
175
|
+
hotkey,
|
|
176
|
+
parsedSequence,
|
|
177
|
+
callback,
|
|
178
|
+
options: {
|
|
179
|
+
...defaultHotkeyOptions,
|
|
180
|
+
timeout: DEFAULT_SEQUENCE_TIMEOUT,
|
|
181
|
+
...optionsForKind("sequence", options),
|
|
182
|
+
platform,
|
|
183
|
+
ignoreInputs: options.ignoreInputs ?? getDefaultIgnoreInputs(parsedSequence[0])
|
|
184
|
+
},
|
|
185
|
+
target,
|
|
186
|
+
currentIndex: 0,
|
|
187
|
+
lastKeyTime: 0,
|
|
188
|
+
triggerCount: 0,
|
|
189
|
+
hasFired: false
|
|
190
|
+
};
|
|
191
|
+
} else {
|
|
192
|
+
const parsedHotkey = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : rawHotkeyToParsedHotkey(hotkey, platform);
|
|
193
|
+
registration = {
|
|
194
|
+
kind: "chord",
|
|
195
|
+
id,
|
|
196
|
+
hotkey: typeof hotkey === "string" ? hotkey : formatHotkey(parsedHotkey),
|
|
197
|
+
parsedHotkey,
|
|
198
|
+
callback,
|
|
199
|
+
options: {
|
|
200
|
+
...defaultHotkeyOptions,
|
|
201
|
+
requireReset: false,
|
|
202
|
+
...optionsForKind("chord", options),
|
|
203
|
+
platform,
|
|
204
|
+
ignoreInputs: options.ignoreInputs ?? getDefaultIgnoreInputs(parsedHotkey)
|
|
205
|
+
},
|
|
206
|
+
target,
|
|
207
|
+
hasFired: false,
|
|
208
|
+
triggerCount: 0
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const conflicting = this.#findConflictingRegistration(registration);
|
|
212
|
+
if (conflicting) handleConflict(conflicting.id, formatForMessage(registration.hotkey), options.conflictBehavior ?? "warn", (conflictId) => this.#unregister(conflictId));
|
|
166
213
|
this.registrations.setState((prev) => new Map(prev).set(id, registration));
|
|
167
214
|
if (!this.#targetRegistrations.has(target)) this.#targetRegistrations.set(target, /* @__PURE__ */ new Set());
|
|
168
215
|
this.#targetRegistrations.get(target).add(id);
|
|
@@ -185,21 +232,16 @@ var HotkeyManager = class HotkeyManager {
|
|
|
185
232
|
setOptions: (newOptions) => {
|
|
186
233
|
manager.registrations.setState((prev) => {
|
|
187
234
|
const reg = prev.get(id);
|
|
188
|
-
if (reg
|
|
235
|
+
if (!reg) return prev;
|
|
236
|
+
const options = {
|
|
189
237
|
...reg.options,
|
|
190
|
-
...newOptions
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
...newOptions
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
return next;
|
|
201
|
-
}
|
|
202
|
-
return prev;
|
|
238
|
+
...optionsForKind(reg.kind, newOptions)
|
|
239
|
+
};
|
|
240
|
+
if (optionsEqual(reg.options, options)) return prev;
|
|
241
|
+
return new Map(prev).set(id, {
|
|
242
|
+
...reg,
|
|
243
|
+
options
|
|
244
|
+
});
|
|
203
245
|
});
|
|
204
246
|
},
|
|
205
247
|
get isActive() {
|
|
@@ -231,13 +273,13 @@ var HotkeyManager = class HotkeyManager {
|
|
|
231
273
|
#ensureListenersForTarget(target) {
|
|
232
274
|
if (typeof document === "undefined") return;
|
|
233
275
|
if (this.#targetListeners.has(target)) return;
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
target.addEventListener("keydown",
|
|
237
|
-
target.addEventListener("keyup",
|
|
276
|
+
const keydown = (event) => this.#processTargetEvent(event, target, "keydown");
|
|
277
|
+
const keyup = (event) => this.#processTargetEvent(event, target, "keyup");
|
|
278
|
+
target.addEventListener("keydown", keydown);
|
|
279
|
+
target.addEventListener("keyup", keyup);
|
|
238
280
|
this.#targetListeners.set(target, {
|
|
239
|
-
keydown
|
|
240
|
-
keyup
|
|
281
|
+
keydown,
|
|
282
|
+
keyup
|
|
241
283
|
});
|
|
242
284
|
}
|
|
243
285
|
/**
|
|
@@ -252,94 +294,146 @@ var HotkeyManager = class HotkeyManager {
|
|
|
252
294
|
this.#targetListeners.delete(target);
|
|
253
295
|
this.#targetRegistrations.delete(target);
|
|
254
296
|
}
|
|
297
|
+
/** Registrations of one kind on a target, in registration order. */
|
|
298
|
+
#registrationsOn(target, kind) {
|
|
299
|
+
const result = [];
|
|
300
|
+
for (const id of this.#targetRegistrations.get(target) ?? []) {
|
|
301
|
+
const registration = this.registrations.state.get(id);
|
|
302
|
+
if (registration?.kind === kind) result.push(registration);
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
255
306
|
/**
|
|
256
307
|
* Processes keyboard events for a specific target and event type.
|
|
308
|
+
* Chords and sequences match independently; both may fire for one event.
|
|
257
309
|
*/
|
|
258
310
|
#processTargetEvent(event, target, eventType) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
311
|
+
this.#processChords(event, target, eventType);
|
|
312
|
+
this.#processSequences(event, target, eventType);
|
|
313
|
+
}
|
|
314
|
+
#processChords(event, target, eventType) {
|
|
315
|
+
const registrations = this.#registrationsOn(target, "chord");
|
|
316
|
+
if (eventType === "keyup") {
|
|
317
|
+
for (const registration of registrations) if (registration.hasFired && this.#shouldResetRegistration(registration, event)) {
|
|
262
318
|
registration.hasFired = false;
|
|
263
319
|
registration.activeMatch = void 0;
|
|
264
320
|
}
|
|
265
321
|
}
|
|
266
322
|
if (isRecordingEvent(event)) return;
|
|
267
|
-
const targetRegs = this.#targetRegistrations.get(target);
|
|
268
|
-
if (!targetRegs) return;
|
|
269
323
|
const matches = /* @__PURE__ */ new Map();
|
|
270
324
|
let bestScore = 0;
|
|
271
|
-
for (const
|
|
272
|
-
|
|
273
|
-
if (!registration || !registration.options.enabled || registration.options.eventType !== eventType || !isEventForTarget(event, target) || registration.options.ignoreInputs !== false && shouldIgnoreInputEvent(event, target, registration.target)) continue;
|
|
325
|
+
for (const registration of registrations) {
|
|
326
|
+
if (!registration.options.enabled || registration.options.eventType !== eventType || !isEventForTarget(event, target) || registration.options.ignoreInputs !== false && shouldIgnoreInputEvent(event, target, registration.target)) continue;
|
|
274
327
|
const result = matchKeyboardEvent(event, registration.parsedHotkey, registration.options.platform);
|
|
275
328
|
if (result.matched) {
|
|
276
|
-
matches.set(id, result);
|
|
329
|
+
matches.set(registration.id, result);
|
|
277
330
|
bestScore = Math.max(bestScore, result.score);
|
|
278
331
|
}
|
|
279
332
|
}
|
|
280
|
-
for (const
|
|
281
|
-
const
|
|
282
|
-
if (!
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (shouldIgnoreInputEvent(event, target, registration.target)) continue;
|
|
333
|
+
for (const registration of registrations) {
|
|
334
|
+
const match = matches.get(registration.id);
|
|
335
|
+
if (!match?.matched || match.score !== bestScore) continue;
|
|
336
|
+
if (eventType === "keyup") {
|
|
337
|
+
this.#executeCallback(registration, event);
|
|
338
|
+
continue;
|
|
287
339
|
}
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
340
|
+
if (registration.options.preventDefault) event.preventDefault();
|
|
341
|
+
if (registration.options.stopPropagation) event.stopPropagation();
|
|
342
|
+
if (!registration.options.requireReset || !registration.hasFired) {
|
|
343
|
+
this.#executeCallback(registration, event);
|
|
344
|
+
if (registration.options.requireReset) {
|
|
345
|
+
registration.hasFired = true;
|
|
346
|
+
registration.activeMatch = match.identity;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
#processSequences(event, target, eventType) {
|
|
352
|
+
const registrations = this.#registrationsOn(target, "sequence");
|
|
353
|
+
if (registrations.length === 0) return;
|
|
354
|
+
if (isRecordingEvent(event)) {
|
|
355
|
+
let changed = false;
|
|
356
|
+
for (const registration of registrations) if (registration.currentIndex > 0) {
|
|
357
|
+
registration.currentIndex = 0;
|
|
358
|
+
changed = true;
|
|
359
|
+
}
|
|
360
|
+
if (changed) this.#notify();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (event.type === "keydown" && event.repeat || normalizeKeyboardEvent(event).isComposing) return;
|
|
364
|
+
if (isModifierKey(normalizeKeyName(event.key))) return;
|
|
365
|
+
const now = Date.now();
|
|
366
|
+
let changed = false;
|
|
367
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
368
|
+
let bestScore = 0;
|
|
369
|
+
for (const registration of registrations) {
|
|
370
|
+
if (!isEventForTarget(event, target) || !registration.options.enabled || registration.options.ignoreInputs !== false && shouldIgnoreInputEvent(event, target, registration.target) || registration.options.eventType !== eventType) continue;
|
|
371
|
+
const timeout = registration.options.timeout ?? 1e3;
|
|
372
|
+
if (registration.currentIndex > 0 && now - registration.lastKeyTime > timeout) {
|
|
373
|
+
registration.currentIndex = 0;
|
|
374
|
+
changed = true;
|
|
375
|
+
}
|
|
376
|
+
const expected = registration.parsedSequence[registration.currentIndex];
|
|
377
|
+
if (!expected) continue;
|
|
378
|
+
let match = matchKeyboardEvent(event, expected, registration.options.platform);
|
|
379
|
+
let nextIndex = registration.currentIndex + 1;
|
|
380
|
+
if (!match.matched && registration.currentIndex > 0) {
|
|
381
|
+
match = matchKeyboardEvent(event, registration.parsedSequence[0], registration.options.platform);
|
|
382
|
+
nextIndex = 1;
|
|
383
|
+
}
|
|
384
|
+
candidates.set(registration.id, {
|
|
385
|
+
score: match.score,
|
|
386
|
+
nextIndex
|
|
387
|
+
});
|
|
388
|
+
bestScore = Math.max(bestScore, match.score);
|
|
389
|
+
}
|
|
390
|
+
const completed = [];
|
|
391
|
+
for (const registration of registrations) {
|
|
392
|
+
const candidate = candidates.get(registration.id);
|
|
393
|
+
if (!candidate) continue;
|
|
394
|
+
if (candidate.score > 0 && candidate.score === bestScore) {
|
|
395
|
+
registration.lastKeyTime = now;
|
|
396
|
+
registration.currentIndex = candidate.nextIndex;
|
|
397
|
+
changed = true;
|
|
398
|
+
if (registration.currentIndex >= registration.parsedSequence.length) {
|
|
292
399
|
if (registration.options.preventDefault) event.preventDefault();
|
|
293
400
|
if (registration.options.stopPropagation) event.stopPropagation();
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (registration.options.requireReset) {
|
|
297
|
-
registration.hasFired = true;
|
|
298
|
-
registration.activeMatch = match.identity;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
401
|
+
registration.currentIndex = 0;
|
|
402
|
+
completed.push(registration);
|
|
301
403
|
}
|
|
302
|
-
} else if (registration.
|
|
303
|
-
|
|
304
|
-
|
|
404
|
+
} else if (registration.currentIndex > 0) {
|
|
405
|
+
registration.currentIndex = 0;
|
|
406
|
+
changed = true;
|
|
305
407
|
}
|
|
306
408
|
}
|
|
409
|
+
if (changed && completed.length === 0) this.#notify();
|
|
410
|
+
for (const registration of completed) this.#executeCallback(registration, event);
|
|
307
411
|
}
|
|
308
412
|
/**
|
|
309
413
|
* Executes a hotkey callback with proper event handling.
|
|
310
414
|
*/
|
|
311
|
-
#
|
|
415
|
+
#executeCallback(registration, event) {
|
|
312
416
|
if (registration.options.preventDefault) event.preventDefault();
|
|
313
417
|
if (registration.options.stopPropagation) event.stopPropagation();
|
|
314
418
|
registration.triggerCount++;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
parsedHotkey: registration.parsedHotkey
|
|
319
|
-
};
|
|
320
|
-
registration.callback(event, context);
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Creates a keydown handler for a specific target.
|
|
324
|
-
*/
|
|
325
|
-
#createTargetKeyDownHandler(target) {
|
|
326
|
-
return (event) => {
|
|
327
|
-
this.#processTargetEvent(event, target, "keydown");
|
|
328
|
-
};
|
|
419
|
+
if (registration.kind === "sequence") registration.hasFired = true;
|
|
420
|
+
this.#notify();
|
|
421
|
+
registration.callback(event, this.#callbackContext(registration));
|
|
329
422
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return (event) => {
|
|
335
|
-
this.#processTargetEvent(event, target, "keyup");
|
|
423
|
+
#callbackContext(registration) {
|
|
424
|
+
return {
|
|
425
|
+
hotkey: registration.hotkey,
|
|
426
|
+
parsedHotkey: registration.kind === "chord" ? registration.parsedHotkey : registration.parsedSequence[registration.parsedSequence.length - 1]
|
|
336
427
|
};
|
|
337
428
|
}
|
|
338
429
|
/**
|
|
339
|
-
* Finds an existing registration
|
|
430
|
+
* Finds an existing registration on the same target whose steps resolve to
|
|
431
|
+
* the same identity. Each side keeps its own platform, so `Mod+S` on macOS
|
|
432
|
+
* and `Mod+S` on Windows are different bindings.
|
|
340
433
|
*/
|
|
341
|
-
#findConflictingRegistration(
|
|
342
|
-
|
|
434
|
+
#findConflictingRegistration(candidate) {
|
|
435
|
+
const steps = resolvedSteps(candidate);
|
|
436
|
+
for (const registration of this.registrations.state.values()) if (registration.target === candidate.target && sameResolvedSteps(resolvedSteps(registration), steps)) return registration;
|
|
343
437
|
return null;
|
|
344
438
|
}
|
|
345
439
|
/**
|
|
@@ -362,8 +456,19 @@ var HotkeyManager = class HotkeyManager {
|
|
|
362
456
|
return false;
|
|
363
457
|
}
|
|
364
458
|
/**
|
|
459
|
+
* Resets the progress of every in-progress sequence.
|
|
460
|
+
*/
|
|
461
|
+
resetSequenceProgress() {
|
|
462
|
+
for (const registration of this.registrations.state.values()) if (registration.kind === "sequence") {
|
|
463
|
+
registration.currentIndex = 0;
|
|
464
|
+
registration.lastKeyTime = 0;
|
|
465
|
+
}
|
|
466
|
+
this.#notify();
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
365
469
|
* Triggers a registration's callback programmatically from devtools.
|
|
366
|
-
* Creates a synthetic KeyboardEvent
|
|
470
|
+
* Creates a synthetic KeyboardEvent from the chord (or a sequence's last step)
|
|
471
|
+
* and invokes the callback.
|
|
367
472
|
*
|
|
368
473
|
* @param id - The registration ID to trigger
|
|
369
474
|
* @returns True if the registration was found and triggered
|
|
@@ -371,7 +476,8 @@ var HotkeyManager = class HotkeyManager {
|
|
|
371
476
|
triggerRegistration(id) {
|
|
372
477
|
const registration = this.registrations.state.get(id);
|
|
373
478
|
if (!registration) return false;
|
|
374
|
-
const
|
|
479
|
+
const context = this.#callbackContext(registration);
|
|
480
|
+
const parsed = context.parsedHotkey;
|
|
375
481
|
const syntheticEvent = new KeyboardEvent(registration.options.eventType ?? "keydown", {
|
|
376
482
|
...parsed.code !== void 0 ? { code: parsed.code } : { key: parsed.key },
|
|
377
483
|
ctrlKey: parsed.ctrl,
|
|
@@ -382,15 +488,13 @@ var HotkeyManager = class HotkeyManager {
|
|
|
382
488
|
cancelable: true
|
|
383
489
|
});
|
|
384
490
|
registration.triggerCount++;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
parsedHotkey: registration.parsedHotkey
|
|
389
|
-
});
|
|
491
|
+
if (registration.kind === "sequence") registration.hasFired = true;
|
|
492
|
+
this.#notify();
|
|
493
|
+
registration.callback(syntheticEvent, context);
|
|
390
494
|
return true;
|
|
391
495
|
}
|
|
392
496
|
/**
|
|
393
|
-
* Gets the number of registered hotkeys.
|
|
497
|
+
* Gets the number of registered hotkeys, chords and sequences.
|
|
394
498
|
*/
|
|
395
499
|
getRegistrationCount() {
|
|
396
500
|
return this.registrations.state.size;
|
|
@@ -398,14 +502,12 @@ var HotkeyManager = class HotkeyManager {
|
|
|
398
502
|
/**
|
|
399
503
|
* Checks if a specific hotkey is registered.
|
|
400
504
|
*
|
|
401
|
-
* @param hotkey - The
|
|
505
|
+
* @param hotkey - The chord or sequence to check
|
|
402
506
|
* @param target - Optional target element to match (if provided, both hotkey and target must match)
|
|
403
507
|
* @returns True if a matching registration exists
|
|
404
508
|
*/
|
|
405
509
|
isRegistered(hotkey, target) {
|
|
406
|
-
for (const registration of this.registrations.state.values()) if (areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform))
|
|
407
|
-
if (target === void 0 || registration.target === target) return true;
|
|
408
|
-
}
|
|
510
|
+
for (const registration of this.registrations.state.values()) if (areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) && (target === void 0 || registration.target === target)) return true;
|
|
409
511
|
return false;
|
|
410
512
|
}
|
|
411
513
|
/**
|
|
@@ -414,6 +516,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
414
516
|
destroy() {
|
|
415
517
|
for (const target of this.#targetListeners.keys()) this.#removeListenersForTarget(target);
|
|
416
518
|
this.registrations.setState(() => /* @__PURE__ */ new Map());
|
|
519
|
+
this.appHotkeys.setState(() => null);
|
|
417
520
|
this.#targetListeners.clear();
|
|
418
521
|
this.#targetRegistrations.clear();
|
|
419
522
|
}
|