@remcostoeten/use-shortcut 1.3.0 → 2.0.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/CHANGELOG.md +8 -0
- package/README.md +33 -313
- package/dist/cli/index.mjs +88 -216
- package/dist/index.d.mts +39 -68
- package/dist/index.d.ts +39 -68
- package/dist/index.js +350 -361
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +351 -354
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/src/__tests__/features.test.ts +43 -12
- package/src/builder.ts +37 -476
- package/src/constants.ts +59 -13
- package/src/formatter.ts +37 -22
- package/src/hook.ts +150 -31
- package/src/index.ts +1 -12
- package/src/parser.ts +6 -3
- package/src/runtime/binding.ts +136 -0
- package/src/runtime/conflicts.ts +43 -0
- package/src/runtime/debug.ts +6 -0
- package/src/runtime/guards.ts +82 -0
- package/src/runtime/keys.ts +63 -0
- package/src/runtime/listener.ts +142 -0
- package/src/runtime/recording.ts +39 -0
- package/src/runtime/types.ts +48 -0
- package/src/types.ts +16 -19
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import { useRef, useMemo, useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
// src/constants.ts
|
|
4
|
-
var
|
|
4
|
+
var OS = {
|
|
5
5
|
MAC: "mac",
|
|
6
6
|
WINDOWS: "windows",
|
|
7
7
|
LINUX: "linux"
|
|
8
8
|
};
|
|
9
|
+
var Platform = OS;
|
|
10
|
+
var _cachedPlatform = null;
|
|
9
11
|
function detectPlatform() {
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (_cachedPlatform) return _cachedPlatform;
|
|
13
|
+
if (typeof navigator === "undefined") {
|
|
14
|
+
_cachedPlatform = OS.WINDOWS;
|
|
15
|
+
return _cachedPlatform;
|
|
16
|
+
}
|
|
17
|
+
const uaPlatform = navigator.userAgentData?.platform?.toLowerCase();
|
|
18
|
+
const platform = (uaPlatform ?? navigator.platform ?? navigator.userAgent ?? "").toLowerCase();
|
|
19
|
+
if (platform.includes("mac") || platform.includes("iphone") || platform.includes("ipad") || platform.includes("ipod")) {
|
|
20
|
+
_cachedPlatform = OS.MAC;
|
|
21
|
+
return _cachedPlatform;
|
|
22
|
+
}
|
|
23
|
+
if (platform.includes("linux") || platform.includes("android")) {
|
|
24
|
+
_cachedPlatform = OS.LINUX;
|
|
25
|
+
return _cachedPlatform;
|
|
26
|
+
}
|
|
27
|
+
if (platform.includes("win")) {
|
|
28
|
+
_cachedPlatform = OS.WINDOWS;
|
|
29
|
+
return _cachedPlatform;
|
|
30
|
+
}
|
|
31
|
+
_cachedPlatform = OS.WINDOWS;
|
|
32
|
+
return _cachedPlatform;
|
|
15
33
|
}
|
|
16
34
|
var ModifierKey = {
|
|
17
35
|
META: "meta",
|
|
@@ -81,19 +99,19 @@ var SpecialKeyMap = {
|
|
|
81
99
|
closebracket: "]"
|
|
82
100
|
};
|
|
83
101
|
var ModifierDisplaySymbols = {
|
|
84
|
-
[
|
|
102
|
+
[OS.MAC]: {
|
|
85
103
|
[ModifierKey.META]: "\u2318",
|
|
86
104
|
[ModifierKey.CTRL]: "\u2303",
|
|
87
105
|
[ModifierKey.ALT]: "\u2325",
|
|
88
106
|
[ModifierKey.SHIFT]: "\u21E7"
|
|
89
107
|
},
|
|
90
|
-
[
|
|
108
|
+
[OS.WINDOWS]: {
|
|
91
109
|
[ModifierKey.META]: "Ctrl",
|
|
92
110
|
[ModifierKey.CTRL]: "Ctrl",
|
|
93
111
|
[ModifierKey.ALT]: "Alt",
|
|
94
112
|
[ModifierKey.SHIFT]: "Shift"
|
|
95
113
|
},
|
|
96
|
-
[
|
|
114
|
+
[OS.LINUX]: {
|
|
97
115
|
[ModifierKey.META]: "Super",
|
|
98
116
|
[ModifierKey.CTRL]: "Ctrl",
|
|
99
117
|
[ModifierKey.ALT]: "Alt",
|
|
@@ -101,12 +119,15 @@ var ModifierDisplaySymbols = {
|
|
|
101
119
|
}
|
|
102
120
|
};
|
|
103
121
|
var ModifierDisplayOrder = {
|
|
104
|
-
[
|
|
105
|
-
[
|
|
106
|
-
[
|
|
122
|
+
[OS.MAC]: [ModifierKey.CTRL, ModifierKey.ALT, ModifierKey.SHIFT, ModifierKey.META],
|
|
123
|
+
[OS.WINDOWS]: [ModifierKey.META, ModifierKey.ALT, ModifierKey.SHIFT, ModifierKey.CTRL],
|
|
124
|
+
[OS.LINUX]: [ModifierKey.META, ModifierKey.ALT, ModifierKey.SHIFT, ModifierKey.CTRL]
|
|
107
125
|
};
|
|
108
126
|
|
|
109
127
|
// src/parser.ts
|
|
128
|
+
function _normalizeKeyToken(key) {
|
|
129
|
+
return key === " " ? "space" : key.toLowerCase();
|
|
130
|
+
}
|
|
110
131
|
function parseShortcut(shortcut) {
|
|
111
132
|
const platform = detectPlatform();
|
|
112
133
|
const normalized = shortcut.toLowerCase().trim();
|
|
@@ -158,9 +179,9 @@ function getModifiersFromEvent(event) {
|
|
|
158
179
|
}
|
|
159
180
|
function matchesShortcut(event, parsed) {
|
|
160
181
|
const eventModifiers = getModifiersFromEvent(event);
|
|
161
|
-
const eventKey = event.key
|
|
182
|
+
const eventKey = _normalizeKeyToken(event.key);
|
|
162
183
|
const modifiersMatch = eventModifiers.meta === parsed.modifiers.meta && eventModifiers.ctrl === parsed.modifiers.ctrl && eventModifiers.alt === parsed.modifiers.alt && eventModifiers.shift === parsed.modifiers.shift;
|
|
163
|
-
const keyMatches = eventKey === parsed.key
|
|
184
|
+
const keyMatches = eventKey === _normalizeKeyToken(parsed.key);
|
|
164
185
|
return modifiersMatch && keyMatches;
|
|
165
186
|
}
|
|
166
187
|
function matchesAnyShortcut(event, parsedShortcuts) {
|
|
@@ -168,6 +189,34 @@ function matchesAnyShortcut(event, parsedShortcuts) {
|
|
|
168
189
|
}
|
|
169
190
|
|
|
170
191
|
// src/formatter.ts
|
|
192
|
+
var _BASE_DISPLAY_NAMES = {
|
|
193
|
+
ArrowUp: "\u2191",
|
|
194
|
+
ArrowDown: "\u2193",
|
|
195
|
+
ArrowLeft: "\u2190",
|
|
196
|
+
ArrowRight: "\u2192",
|
|
197
|
+
Home: "Home",
|
|
198
|
+
End: "End",
|
|
199
|
+
PageUp: "PgUp",
|
|
200
|
+
PageDown: "PgDn"
|
|
201
|
+
};
|
|
202
|
+
var _MAC_DISPLAY_NAMES = {
|
|
203
|
+
..._BASE_DISPLAY_NAMES,
|
|
204
|
+
Enter: "\u21A9",
|
|
205
|
+
Tab: "\u21E5",
|
|
206
|
+
Escape: "\u238B",
|
|
207
|
+
Backspace: "\u232B",
|
|
208
|
+
Delete: "\u2326",
|
|
209
|
+
" ": "\u2423"
|
|
210
|
+
};
|
|
211
|
+
var _NON_MAC_DISPLAY_NAMES = {
|
|
212
|
+
..._BASE_DISPLAY_NAMES,
|
|
213
|
+
Enter: "Enter",
|
|
214
|
+
Tab: "Tab",
|
|
215
|
+
Escape: "Esc",
|
|
216
|
+
Backspace: "Backspace",
|
|
217
|
+
Delete: "Del",
|
|
218
|
+
" ": "Space"
|
|
219
|
+
};
|
|
171
220
|
function formatShortcut(shortcut, platform) {
|
|
172
221
|
const targetPlatform = platform ?? detectPlatform();
|
|
173
222
|
const parsed = parseShortcut(shortcut);
|
|
@@ -179,151 +228,87 @@ function formatShortcut(shortcut, platform) {
|
|
|
179
228
|
parts.push(symbols[modifier]);
|
|
180
229
|
}
|
|
181
230
|
}
|
|
182
|
-
const displayKey =
|
|
231
|
+
const displayKey = _formatKey(parsed.key, targetPlatform);
|
|
183
232
|
parts.push(displayKey);
|
|
184
|
-
const separator = targetPlatform ===
|
|
233
|
+
const separator = targetPlatform === OS.MAC ? "" : "+";
|
|
185
234
|
return parts.join(separator);
|
|
186
235
|
}
|
|
187
|
-
function
|
|
188
|
-
const displayNames =
|
|
189
|
-
ArrowUp: "\u2191",
|
|
190
|
-
ArrowDown: "\u2193",
|
|
191
|
-
ArrowLeft: "\u2190",
|
|
192
|
-
ArrowRight: "\u2192",
|
|
193
|
-
Enter: platform === Platform.MAC ? "\u21A9" : "Enter",
|
|
194
|
-
Tab: platform === Platform.MAC ? "\u21E5" : "Tab",
|
|
195
|
-
Escape: platform === Platform.MAC ? "\u238B" : "Esc",
|
|
196
|
-
Backspace: platform === Platform.MAC ? "\u232B" : "Backspace",
|
|
197
|
-
Delete: platform === Platform.MAC ? "\u2326" : "Del",
|
|
198
|
-
" ": platform === Platform.MAC ? "\u2423" : "Space",
|
|
199
|
-
Home: "Home",
|
|
200
|
-
End: "End",
|
|
201
|
-
PageUp: "PgUp",
|
|
202
|
-
PageDown: "PgDn"
|
|
203
|
-
};
|
|
236
|
+
function _formatKey(key, platform) {
|
|
237
|
+
const displayNames = platform === OS.MAC ? _MAC_DISPLAY_NAMES : _NON_MAC_DISPLAY_NAMES;
|
|
204
238
|
return displayNames[key] || key.toUpperCase();
|
|
205
239
|
}
|
|
206
|
-
function getModifierSymbols(platform) {
|
|
207
|
-
const targetPlatform = platform ?? detectPlatform();
|
|
208
|
-
return ModifierDisplaySymbols[targetPlatform];
|
|
209
|
-
}
|
|
210
240
|
|
|
211
|
-
// src/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
input: (e) => {
|
|
216
|
-
const target = e.target;
|
|
217
|
-
return IGNORED_TAGS.has(target.tagName);
|
|
218
|
-
},
|
|
219
|
-
editable: (e) => {
|
|
220
|
-
const target = e.target;
|
|
221
|
-
return target.isContentEditable;
|
|
222
|
-
},
|
|
223
|
-
typing: (e) => {
|
|
224
|
-
const target = e.target;
|
|
225
|
-
return IGNORED_TAGS.has(target.tagName) || target.isContentEditable;
|
|
226
|
-
},
|
|
227
|
-
modal: () => {
|
|
228
|
-
return document.querySelector('[data-modal="true"], [role="dialog"]') !== null;
|
|
229
|
-
},
|
|
230
|
-
disabled: (e) => {
|
|
231
|
-
const target = e.target;
|
|
232
|
-
return target.hasAttribute("disabled") || target.getAttribute("aria-disabled") === "true";
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
function shouldExcept(event, except) {
|
|
236
|
-
if (!except) return false;
|
|
237
|
-
if (typeof except === "function") {
|
|
238
|
-
return except(event);
|
|
239
|
-
}
|
|
240
|
-
if (Array.isArray(except)) {
|
|
241
|
-
return except.some((preset) => EXCEPT_PREDICATES[preset]?.(event));
|
|
242
|
-
}
|
|
243
|
-
return EXCEPT_PREDICATES[except]?.(event) ?? false;
|
|
244
|
-
}
|
|
245
|
-
function normalizeScopes(scopes) {
|
|
246
|
-
if (!scopes) return [];
|
|
247
|
-
return (Array.isArray(scopes) ? scopes : [scopes]).map((scope) => scope.trim()).filter(Boolean);
|
|
248
|
-
}
|
|
249
|
-
function scopeMatch(requiredScopes, activeScopes) {
|
|
250
|
-
if (requiredScopes.size === 0) return true;
|
|
251
|
-
for (const required of requiredScopes) {
|
|
252
|
-
if (activeScopes.has(required)) return true;
|
|
241
|
+
// src/runtime/debug.ts
|
|
242
|
+
function _debugLog(debug, ...args) {
|
|
243
|
+
if (debug) {
|
|
244
|
+
console.log("[useShortcut]", ...args);
|
|
253
245
|
}
|
|
254
|
-
return false;
|
|
255
246
|
}
|
|
256
|
-
|
|
247
|
+
|
|
248
|
+
// src/runtime/keys.ts
|
|
249
|
+
function _getActiveModifierTokens(modifiers) {
|
|
257
250
|
const platform = detectPlatform();
|
|
258
251
|
const order = ModifierDisplayOrder[platform];
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (key === ModifierKey.
|
|
262
|
-
if (key === ModifierKey.
|
|
263
|
-
if (key === ModifierKey.
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (key === ModifierKey.ALT) return "alt";
|
|
268
|
-
if (key === ModifierKey.SHIFT) return "shift";
|
|
269
|
-
if (key === ModifierKey.META) return "cmd";
|
|
270
|
-
return "";
|
|
271
|
-
});
|
|
252
|
+
const tokens = [];
|
|
253
|
+
for (const key of order) {
|
|
254
|
+
if (key === ModifierKey.CTRL && modifiers.ctrl) tokens.push("ctrl");
|
|
255
|
+
if (key === ModifierKey.ALT && modifiers.alt) tokens.push("alt");
|
|
256
|
+
if (key === ModifierKey.SHIFT && modifiers.shift) tokens.push("shift");
|
|
257
|
+
if (key === ModifierKey.META && modifiers.cmd) tokens.push("cmd");
|
|
258
|
+
}
|
|
259
|
+
return tokens;
|
|
272
260
|
}
|
|
273
|
-
function
|
|
274
|
-
const tokens =
|
|
261
|
+
function _buildComboString(modifiers, key) {
|
|
262
|
+
const tokens = _getActiveModifierTokens(modifiers);
|
|
275
263
|
return [...tokens, key].join("+");
|
|
276
264
|
}
|
|
277
|
-
function
|
|
265
|
+
function _formatSequenceDisplay(steps) {
|
|
278
266
|
return steps.map((step) => formatShortcut(step)).join(" then ");
|
|
279
267
|
}
|
|
280
|
-
function
|
|
281
|
-
if (debug) {
|
|
282
|
-
console.log("[useShortcut]", ...args);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
function canonicalizeParsed(parsed) {
|
|
268
|
+
function _canonicalizeParsed(parsed) {
|
|
286
269
|
const modifiers = [];
|
|
287
270
|
if (parsed.modifiers.ctrl) modifiers.push("ctrl");
|
|
288
271
|
if (parsed.modifiers.alt) modifiers.push("alt");
|
|
289
272
|
if (parsed.modifiers.shift) modifiers.push("shift");
|
|
290
273
|
if (parsed.modifiers.meta) modifiers.push("cmd");
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
function isPureModifier(event) {
|
|
294
|
-
const key = event.key.toLowerCase();
|
|
295
|
-
return key === "shift" || key === "control" || key === "alt" || key === "meta";
|
|
274
|
+
const key = parsed.key === " " || parsed.key === "Spacebar" ? "space" : parsed.key.toLowerCase();
|
|
275
|
+
return [...modifiers, key].join("+");
|
|
296
276
|
}
|
|
297
|
-
function
|
|
298
|
-
const platform = detectPlatform();
|
|
299
|
-
const symbols = ModifierDisplaySymbols[platform];
|
|
277
|
+
function _eventToCombo(event) {
|
|
300
278
|
const modifiers = [];
|
|
301
|
-
if (event.ctrlKey) modifiers.push(
|
|
279
|
+
if (event.ctrlKey) modifiers.push("ctrl");
|
|
302
280
|
if (event.altKey) modifiers.push("alt");
|
|
303
281
|
if (event.shiftKey) modifiers.push("shift");
|
|
304
282
|
if (event.metaKey) modifiers.push("cmd");
|
|
305
|
-
const key = event.key
|
|
283
|
+
const key = event.key === " " || event.key === "Spacebar" ? "space" : event.key.toLowerCase();
|
|
306
284
|
return [...modifiers, key].join("+");
|
|
307
285
|
}
|
|
308
|
-
function
|
|
286
|
+
function _eventToMatchStep(event) {
|
|
287
|
+
return _eventToCombo(event);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/runtime/conflicts.ts
|
|
291
|
+
function _isPrefix(a, b) {
|
|
309
292
|
if (a.length > b.length) return false;
|
|
310
293
|
for (let i = 0; i < a.length; i += 1) {
|
|
311
|
-
if (
|
|
294
|
+
if (a[i] !== b[i]) {
|
|
312
295
|
return false;
|
|
313
296
|
}
|
|
314
297
|
}
|
|
315
298
|
return true;
|
|
316
299
|
}
|
|
317
|
-
function
|
|
318
|
-
const
|
|
319
|
-
const
|
|
300
|
+
function _detectConflict(newSteps, existingSteps) {
|
|
301
|
+
const newCanonicalSteps = newSteps.map(_canonicalizeParsed);
|
|
302
|
+
const existingCanonicalSteps = existingSteps.map(_canonicalizeParsed);
|
|
303
|
+
const newCombo = newCanonicalSteps.join(" ");
|
|
304
|
+
const existingCombo = existingCanonicalSteps.join(" ");
|
|
320
305
|
if (newCombo === existingCombo) return "exact";
|
|
321
|
-
if (
|
|
306
|
+
if (_isPrefix(newCanonicalSteps, existingCanonicalSteps) || _isPrefix(existingCanonicalSteps, newCanonicalSteps)) {
|
|
322
307
|
return "sequence-prefix";
|
|
323
308
|
}
|
|
324
309
|
return null;
|
|
325
310
|
}
|
|
326
|
-
function
|
|
311
|
+
function _emitConflict(registry, conflict) {
|
|
327
312
|
const conflictWarnings = registry.options.conflictWarnings ?? true;
|
|
328
313
|
if (registry.options.onConflict) {
|
|
329
314
|
registry.options.onConflict(conflict);
|
|
@@ -334,37 +319,195 @@ function emitConflict(registry, conflict) {
|
|
|
334
319
|
`[useShortcut] Conflict detected (${conflict.reason}) between "${conflict.combo}" and "${conflict.existingCombo}"`
|
|
335
320
|
);
|
|
336
321
|
}
|
|
337
|
-
|
|
322
|
+
|
|
323
|
+
// src/runtime/guards.ts
|
|
324
|
+
var _IGNORED_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT"]);
|
|
325
|
+
var _EXCEPT_PREDICATES = {
|
|
326
|
+
input: (e) => {
|
|
327
|
+
if (!(e.target instanceof HTMLElement)) return false;
|
|
328
|
+
const target = e.target;
|
|
329
|
+
return _IGNORED_TAGS.has(target.tagName);
|
|
330
|
+
},
|
|
331
|
+
editable: (e) => {
|
|
332
|
+
if (!(e.target instanceof HTMLElement)) return false;
|
|
333
|
+
const target = e.target;
|
|
334
|
+
return target.isContentEditable;
|
|
335
|
+
},
|
|
336
|
+
typing: (e) => {
|
|
337
|
+
if (!(e.target instanceof HTMLElement)) return false;
|
|
338
|
+
const target = e.target;
|
|
339
|
+
return _IGNORED_TAGS.has(target.tagName) || target.isContentEditable;
|
|
340
|
+
},
|
|
341
|
+
modal: () => {
|
|
342
|
+
if (typeof document === "undefined" || typeof document.querySelector !== "function")
|
|
343
|
+
return false;
|
|
344
|
+
return document.querySelector('[data-modal="true"], [role="dialog"]') !== null;
|
|
345
|
+
},
|
|
346
|
+
disabled: (e) => {
|
|
347
|
+
if (!(e.target instanceof HTMLElement)) return false;
|
|
348
|
+
const target = e.target;
|
|
349
|
+
return target.hasAttribute("disabled") || target.getAttribute("aria-disabled") === "true";
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
function _shouldExcept(event, except) {
|
|
353
|
+
if (!except) return false;
|
|
354
|
+
if (typeof except === "function") {
|
|
355
|
+
return except(event);
|
|
356
|
+
}
|
|
357
|
+
if (Array.isArray(except)) {
|
|
358
|
+
return except.some((preset) => _EXCEPT_PREDICATES[preset]?.(event));
|
|
359
|
+
}
|
|
360
|
+
return _EXCEPT_PREDICATES[except]?.(event) ?? false;
|
|
361
|
+
}
|
|
362
|
+
function _normalizeScopes(scopes) {
|
|
363
|
+
if (!scopes) return [];
|
|
364
|
+
return (Array.isArray(scopes) ? scopes : [scopes]).map((scope) => scope.trim()).filter(Boolean);
|
|
365
|
+
}
|
|
366
|
+
function _scopeMatch(requiredScopes, activeScopes) {
|
|
367
|
+
if (requiredScopes.size === 0) return true;
|
|
368
|
+
for (const required of requiredScopes) {
|
|
369
|
+
if (activeScopes.has(required)) return true;
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
function _isPureModifier(event) {
|
|
374
|
+
const key = event.key.toLowerCase();
|
|
375
|
+
return key === "shift" || key === "control" || key === "alt" || key === "meta";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/runtime/listener.ts
|
|
379
|
+
function _sortEntries(entries) {
|
|
380
|
+
if (entries.length <= 1) return entries;
|
|
338
381
|
return [...entries].sort((a, b) => {
|
|
339
382
|
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
340
383
|
return a.id - b.id;
|
|
341
384
|
});
|
|
342
385
|
}
|
|
343
|
-
function
|
|
386
|
+
function _dispatchRegistryEvent(registry, event) {
|
|
387
|
+
const runtimeOptions = registry.options;
|
|
388
|
+
if (runtimeOptions.disabled) return;
|
|
389
|
+
if (runtimeOptions.eventFilter && !runtimeOptions.eventFilter(event)) return;
|
|
390
|
+
const candidateCombos = /* @__PURE__ */ new Set();
|
|
391
|
+
const firstStepCombos = registry.firstStepIndex.get(_eventToMatchStep(event));
|
|
392
|
+
if (firstStepCombos) {
|
|
393
|
+
for (const combo of firstStepCombos) candidateCombos.add(combo);
|
|
394
|
+
}
|
|
395
|
+
for (const combo of registry.activeSequenceCombos) {
|
|
396
|
+
candidateCombos.add(combo);
|
|
397
|
+
}
|
|
398
|
+
for (const combo of candidateCombos) {
|
|
399
|
+
const comboEntries = registry.listeners.get(combo);
|
|
400
|
+
if (!comboEntries) continue;
|
|
401
|
+
const orderedEntries = _sortEntries(comboEntries);
|
|
402
|
+
for (const item of orderedEntries) {
|
|
403
|
+
if (!item.isEnabled) continue;
|
|
404
|
+
if (!_scopeMatch(item.scopes, registry.activeScopes)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (runtimeOptions.ignoreInputs !== false && !item.except) {
|
|
408
|
+
const targetEl = event.target;
|
|
409
|
+
if (targetEl && (_IGNORED_TAGS.has(targetEl.tagName) || targetEl.isContentEditable)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (_shouldExcept(event, item.except)) {
|
|
414
|
+
_debugLog(runtimeOptions.debug, "Skipped due to except condition:", combo);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const expected = item.parsedSteps[item.progress];
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
if (item.progress > 0 && now - item.lastMatchedAt > item.sequenceTimeout) {
|
|
420
|
+
item.progress = 0;
|
|
421
|
+
}
|
|
422
|
+
let matched = false;
|
|
423
|
+
if (matchesShortcut(event, expected)) {
|
|
424
|
+
item.progress += 1;
|
|
425
|
+
item.lastMatchedAt = now;
|
|
426
|
+
if (item.progress === item.parsedSteps.length) {
|
|
427
|
+
matched = true;
|
|
428
|
+
item.progress = 0;
|
|
429
|
+
}
|
|
430
|
+
} else if (item.progress > 0 && matchesShortcut(event, item.parsedSteps[0])) {
|
|
431
|
+
item.progress = 1;
|
|
432
|
+
item.lastMatchedAt = now;
|
|
433
|
+
} else {
|
|
434
|
+
item.progress = 0;
|
|
435
|
+
}
|
|
436
|
+
for (const cb of item.attemptCallbacks) {
|
|
437
|
+
cb(matched, event);
|
|
438
|
+
}
|
|
439
|
+
if (!matched) continue;
|
|
440
|
+
_debugLog(runtimeOptions.debug, "MATCHED:", combo);
|
|
441
|
+
if (item.preventDefault) {
|
|
442
|
+
event.preventDefault();
|
|
443
|
+
}
|
|
444
|
+
if (item.stopPropagation) {
|
|
445
|
+
event.stopPropagation();
|
|
446
|
+
}
|
|
447
|
+
const executeHandler = () => item.userHandler(event);
|
|
448
|
+
if (item.delay > 0) {
|
|
449
|
+
_debugLog(runtimeOptions.debug, "Delaying execution by", item.delay, "ms");
|
|
450
|
+
setTimeout(executeHandler, item.delay);
|
|
451
|
+
} else {
|
|
452
|
+
executeHandler();
|
|
453
|
+
}
|
|
454
|
+
if (item.stopOnMatch) {
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (comboEntries.some((entry) => entry.progress > 0)) {
|
|
459
|
+
registry.activeSequenceCombos.add(combo);
|
|
460
|
+
} else {
|
|
461
|
+
registry.activeSequenceCombos.delete(combo);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function _attachRegistryListener(registry) {
|
|
466
|
+
if (registry.listener) return;
|
|
467
|
+
const target = registry.options.target ?? (typeof window !== "undefined" ? window : null);
|
|
468
|
+
if (!target) return;
|
|
469
|
+
const eventType = registry.options.eventType ?? "keydown";
|
|
470
|
+
const listener = (event) => _dispatchRegistryEvent(registry, event);
|
|
471
|
+
target.addEventListener(eventType, listener);
|
|
472
|
+
registry.listener = listener;
|
|
473
|
+
registry.listenerTarget = target;
|
|
474
|
+
registry.listenerEventType = eventType;
|
|
475
|
+
_debugLog(registry.options.debug, "Listener attached");
|
|
476
|
+
}
|
|
477
|
+
function _detachRegistryListener(registry) {
|
|
478
|
+
if (!registry.listener || !registry.listenerTarget) return;
|
|
479
|
+
registry.listenerTarget.removeEventListener(registry.listenerEventType, registry.listener);
|
|
480
|
+
registry.listener = null;
|
|
481
|
+
registry.listenerTarget = null;
|
|
482
|
+
_debugLog(registry.options.debug, "Listener detached");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/runtime/binding.ts
|
|
486
|
+
function _createBinding(state, handler, handlerOptions = {}, registry) {
|
|
344
487
|
const { options, except: stateExcept } = state;
|
|
345
488
|
const rawSteps = state.steps;
|
|
346
489
|
if (rawSteps.length === 0) {
|
|
347
490
|
throw new Error("[useShortcut] No key specified. Use .key() to set the action key.");
|
|
348
491
|
}
|
|
349
492
|
const parsedSteps = rawSteps.map((step) => parseShortcut(step));
|
|
350
|
-
const combo = parsedSteps.map(
|
|
351
|
-
const display =
|
|
493
|
+
const combo = parsedSteps.map(_canonicalizeParsed).join(" ");
|
|
494
|
+
const display = _formatSequenceDisplay(rawSteps);
|
|
352
495
|
const debug = options.debug ?? false;
|
|
353
496
|
const except = stateExcept ?? handlerOptions.except;
|
|
354
|
-
for (const [existingCombo,
|
|
355
|
-
for (const existing of
|
|
497
|
+
for (const [existingCombo, entries] of registry.listeners.entries()) {
|
|
498
|
+
for (const existing of entries) {
|
|
356
499
|
if (existingCombo === combo) continue;
|
|
357
|
-
const reason =
|
|
500
|
+
const reason = _detectConflict(parsedSteps, existing.parsedSteps);
|
|
358
501
|
if (!reason) continue;
|
|
359
|
-
|
|
502
|
+
_emitConflict(registry, { combo, existingCombo, reason });
|
|
360
503
|
}
|
|
361
504
|
}
|
|
362
505
|
const isEnabled = !handlerOptions.disabled && !options.disabled;
|
|
363
506
|
const delay = handlerOptions.delay ?? options.delay ?? 0;
|
|
364
507
|
const sequenceTimeout = handlerOptions.sequenceTimeout ?? options.sequenceTimeout ?? 800;
|
|
365
|
-
const requiredScopes = new Set(
|
|
508
|
+
const requiredScopes = new Set(_normalizeScopes(state.scopes ?? handlerOptions.scopes));
|
|
366
509
|
const attemptCallbacks = /* @__PURE__ */ new Set();
|
|
367
|
-
|
|
510
|
+
_debugLog(debug, "Registering:", combo, "\u2192", display, {
|
|
368
511
|
parsedSteps,
|
|
369
512
|
except: !!except,
|
|
370
513
|
scopes: [...requiredScopes]
|
|
@@ -386,97 +529,41 @@ function createBinding(state, handler, handlerOptions = {}, registry) {
|
|
|
386
529
|
stopOnMatch: handlerOptions.stopOnMatch ?? false,
|
|
387
530
|
priority: handlerOptions.priority ?? 0
|
|
388
531
|
};
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
for (const item of orderedEntries) {
|
|
401
|
-
if (!item.isEnabled) continue;
|
|
402
|
-
if (!scopeMatch(item.scopes, registry.activeScopes)) {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (runtimeOptions.ignoreInputs !== false && !item.except) {
|
|
406
|
-
const targetEl = event.target;
|
|
407
|
-
if (targetEl && (IGNORED_TAGS.has(targetEl.tagName) || targetEl.isContentEditable)) {
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (shouldExcept(event, item.except)) {
|
|
412
|
-
debugLog(debug, "Skipped due to except condition:", combo);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
const expected = item.parsedSteps[item.progress];
|
|
416
|
-
const now = Date.now();
|
|
417
|
-
if (item.progress > 0 && now - item.lastMatchedAt > item.sequenceTimeout) {
|
|
418
|
-
item.progress = 0;
|
|
419
|
-
}
|
|
420
|
-
let matched = false;
|
|
421
|
-
if (matchesShortcut(event, expected)) {
|
|
422
|
-
item.progress += 1;
|
|
423
|
-
item.lastMatchedAt = now;
|
|
424
|
-
if (item.progress === item.parsedSteps.length) {
|
|
425
|
-
matched = true;
|
|
426
|
-
item.progress = 0;
|
|
427
|
-
}
|
|
428
|
-
} else if (item.progress > 0 && matchesShortcut(event, item.parsedSteps[0])) {
|
|
429
|
-
item.progress = 1;
|
|
430
|
-
item.lastMatchedAt = now;
|
|
431
|
-
} else {
|
|
432
|
-
item.progress = 0;
|
|
433
|
-
}
|
|
434
|
-
item.attemptCallbacks.forEach((cb) => cb(matched, event));
|
|
435
|
-
if (!matched) continue;
|
|
436
|
-
debugLog(debug, "MATCHED:", combo, "\u2192", display);
|
|
437
|
-
if (item.preventDefault) {
|
|
438
|
-
event.preventDefault();
|
|
439
|
-
}
|
|
440
|
-
if (item.stopPropagation) {
|
|
441
|
-
event.stopPropagation();
|
|
442
|
-
}
|
|
443
|
-
const executeHandler = () => item.userHandler(event);
|
|
444
|
-
if (item.delay > 0) {
|
|
445
|
-
debugLog(debug, "Delaying execution by", item.delay, "ms");
|
|
446
|
-
setTimeout(executeHandler, item.delay);
|
|
447
|
-
} else {
|
|
448
|
-
executeHandler();
|
|
449
|
-
}
|
|
450
|
-
if (item.stopOnMatch) {
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
if (target) {
|
|
456
|
-
target.addEventListener(eventType, listener);
|
|
457
|
-
debugLog(debug, "Listener attached for:", combo);
|
|
532
|
+
const comboEntries = registry.listeners.get(combo);
|
|
533
|
+
if (comboEntries) {
|
|
534
|
+
comboEntries.push(entry);
|
|
535
|
+
} else {
|
|
536
|
+
registry.listeners.set(combo, [entry]);
|
|
537
|
+
const firstStep = _canonicalizeParsed(parsedSteps[0]);
|
|
538
|
+
const indexedCombos = registry.firstStepIndex.get(firstStep);
|
|
539
|
+
if (indexedCombos) {
|
|
540
|
+
indexedCombos.add(combo);
|
|
541
|
+
} else {
|
|
542
|
+
registry.firstStepIndex.set(firstStep, /* @__PURE__ */ new Set([combo]));
|
|
458
543
|
}
|
|
459
|
-
const unbind = () => {
|
|
460
|
-
if (target) {
|
|
461
|
-
target.removeEventListener(eventType, listener);
|
|
462
|
-
registry.listeners.delete(combo);
|
|
463
|
-
debugLog(debug, "Unregistered:", combo);
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
comboListener = {
|
|
467
|
-
listener,
|
|
468
|
-
entries: [],
|
|
469
|
-
unbind
|
|
470
|
-
};
|
|
471
|
-
registry.listeners.set(combo, comboListener);
|
|
472
544
|
}
|
|
473
|
-
|
|
545
|
+
_attachRegistryListener(registry);
|
|
474
546
|
const unbindEntry = () => {
|
|
475
|
-
const
|
|
476
|
-
if (!
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
547
|
+
const currentEntries = registry.listeners.get(combo);
|
|
548
|
+
if (!currentEntries) return;
|
|
549
|
+
const nextEntries = currentEntries.filter((item) => item.id !== entry.id);
|
|
550
|
+
if (nextEntries.length === 0) {
|
|
551
|
+
registry.listeners.delete(combo);
|
|
552
|
+
registry.activeSequenceCombos.delete(combo);
|
|
553
|
+
const firstStep = _canonicalizeParsed(parsedSteps[0]);
|
|
554
|
+
const indexedCombos = registry.firstStepIndex.get(firstStep);
|
|
555
|
+
if (indexedCombos) {
|
|
556
|
+
indexedCombos.delete(combo);
|
|
557
|
+
if (indexedCombos.size === 0) {
|
|
558
|
+
registry.firstStepIndex.delete(firstStep);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
_debugLog(debug, "Unregistered:", combo);
|
|
562
|
+
} else {
|
|
563
|
+
registry.listeners.set(combo, nextEntries);
|
|
564
|
+
}
|
|
565
|
+
if (registry.listeners.size === 0) {
|
|
566
|
+
_detachRegistryListener(registry);
|
|
480
567
|
}
|
|
481
568
|
};
|
|
482
569
|
return {
|
|
@@ -499,7 +586,9 @@ function createBinding(state, handler, handlerOptions = {}, registry) {
|
|
|
499
586
|
}
|
|
500
587
|
};
|
|
501
588
|
}
|
|
502
|
-
|
|
589
|
+
|
|
590
|
+
// src/runtime/recording.ts
|
|
591
|
+
function _createRecorder(options) {
|
|
503
592
|
return (recordingOptions = {}) => {
|
|
504
593
|
return new Promise((resolve, reject) => {
|
|
505
594
|
const target = recordingOptions.target ?? options.target ?? (typeof window !== "undefined" ? window : null);
|
|
@@ -511,13 +600,13 @@ function createRecorder(options) {
|
|
|
511
600
|
let timeout;
|
|
512
601
|
const listener = (event) => {
|
|
513
602
|
const keyboardEvent = event;
|
|
514
|
-
if (
|
|
603
|
+
if (_isPureModifier(keyboardEvent)) return;
|
|
515
604
|
keyboardEvent.preventDefault();
|
|
516
605
|
target.removeEventListener(eventType, listener);
|
|
517
606
|
if (timeout) clearTimeout(timeout);
|
|
518
|
-
resolve(
|
|
607
|
+
resolve(_eventToCombo(keyboardEvent));
|
|
519
608
|
};
|
|
520
|
-
target.addEventListener(eventType, listener
|
|
609
|
+
target.addEventListener(eventType, listener);
|
|
521
610
|
const timeoutMs = recordingOptions.timeoutMs;
|
|
522
611
|
if (timeoutMs && timeoutMs > 0) {
|
|
523
612
|
timeout = setTimeout(() => {
|
|
@@ -528,43 +617,51 @@ function createRecorder(options) {
|
|
|
528
617
|
});
|
|
529
618
|
};
|
|
530
619
|
}
|
|
531
|
-
|
|
620
|
+
|
|
621
|
+
// src/builder.ts
|
|
622
|
+
var _MODIFIER_KEYS = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "cmd", "mod"]);
|
|
623
|
+
function _createShortcutBuilder(options = {}) {
|
|
532
624
|
const registry = {
|
|
533
625
|
listeners: /* @__PURE__ */ new Map(),
|
|
626
|
+
firstStepIndex: /* @__PURE__ */ new Map(),
|
|
627
|
+
activeSequenceCombos: /* @__PURE__ */ new Set(),
|
|
534
628
|
options,
|
|
535
|
-
activeScopes: new Set(
|
|
536
|
-
nextId: 1
|
|
629
|
+
activeScopes: new Set(_normalizeScopes(options.activeScopes)),
|
|
630
|
+
nextId: 1,
|
|
631
|
+
listener: null,
|
|
632
|
+
listenerTarget: null,
|
|
633
|
+
listenerEventType: options.eventType ?? "keydown"
|
|
537
634
|
};
|
|
538
|
-
|
|
539
|
-
function
|
|
635
|
+
_debugLog(options.debug, "Builder created with options:", options);
|
|
636
|
+
function _createProxy(currentState) {
|
|
540
637
|
return new Proxy({}, {
|
|
541
638
|
get(_, prop) {
|
|
542
639
|
if (prop === "__debug") {
|
|
543
640
|
return currentState.options.debug;
|
|
544
641
|
}
|
|
545
|
-
if (
|
|
642
|
+
if (_MODIFIER_KEYS.has(prop)) {
|
|
546
643
|
const platform = detectPlatform();
|
|
547
644
|
const modKey = prop === "mod" ? platform === Platform.MAC ? "cmd" : "ctrl" : prop;
|
|
548
645
|
const newState = {
|
|
549
646
|
...currentState,
|
|
550
647
|
modifiers: { ...currentState.modifiers, [modKey]: true }
|
|
551
648
|
};
|
|
552
|
-
|
|
553
|
-
return
|
|
649
|
+
_debugLog(currentState.options.debug, `Chain: +${prop} \u2192`, newState.modifiers);
|
|
650
|
+
return _createProxy(newState);
|
|
554
651
|
}
|
|
555
652
|
if (prop === "in") {
|
|
556
653
|
return (scopes) => {
|
|
557
|
-
const nextScopes = [...
|
|
654
|
+
const nextScopes = [..._normalizeScopes(currentState.scopes), ..._normalizeScopes(scopes)];
|
|
558
655
|
const newState = {
|
|
559
656
|
...currentState,
|
|
560
657
|
scopes: nextScopes
|
|
561
658
|
};
|
|
562
|
-
return
|
|
659
|
+
return _createProxy(newState);
|
|
563
660
|
};
|
|
564
661
|
}
|
|
565
662
|
if (prop === "setScopes") {
|
|
566
663
|
return (scopes) => {
|
|
567
|
-
registry.activeScopes = new Set(
|
|
664
|
+
registry.activeScopes = new Set(_normalizeScopes(scopes));
|
|
568
665
|
};
|
|
569
666
|
}
|
|
570
667
|
if (prop === "enableScope") {
|
|
@@ -586,18 +683,18 @@ function createShortcutBuilder(options = {}) {
|
|
|
586
683
|
return (scope) => registry.activeScopes.has(scope);
|
|
587
684
|
}
|
|
588
685
|
if (prop === "record") {
|
|
589
|
-
return
|
|
686
|
+
return _createRecorder(registry.options);
|
|
590
687
|
}
|
|
591
688
|
if (prop === "key") {
|
|
592
689
|
return (key) => {
|
|
593
|
-
const nextStep =
|
|
690
|
+
const nextStep = _buildComboString(currentState.modifiers, key);
|
|
594
691
|
const newState = {
|
|
595
692
|
...currentState,
|
|
596
693
|
modifiers: {},
|
|
597
694
|
steps: [...currentState.steps, nextStep]
|
|
598
695
|
};
|
|
599
|
-
|
|
600
|
-
return
|
|
696
|
+
_debugLog(currentState.options.debug, `Chain: .key("${key}")`);
|
|
697
|
+
return _createProxy(newState);
|
|
601
698
|
};
|
|
602
699
|
}
|
|
603
700
|
if (prop === "then") {
|
|
@@ -610,8 +707,8 @@ function createShortcutBuilder(options = {}) {
|
|
|
610
707
|
...currentState,
|
|
611
708
|
steps: [...currentState.steps, nextStep]
|
|
612
709
|
};
|
|
613
|
-
|
|
614
|
-
return
|
|
710
|
+
_debugLog(currentState.options.debug, `Chain: .then("${nextStep}")`);
|
|
711
|
+
return _createProxy(newState);
|
|
615
712
|
};
|
|
616
713
|
}
|
|
617
714
|
if (prop === "except") {
|
|
@@ -620,19 +717,19 @@ function createShortcutBuilder(options = {}) {
|
|
|
620
717
|
...currentState,
|
|
621
718
|
except: condition
|
|
622
719
|
};
|
|
623
|
-
|
|
624
|
-
return
|
|
720
|
+
_debugLog(currentState.options.debug, "Chain: .except()", condition);
|
|
721
|
+
return _createProxy(newState);
|
|
625
722
|
};
|
|
626
723
|
}
|
|
627
724
|
if (prop === "on") {
|
|
628
725
|
return (handler, handlerOptions) => {
|
|
629
|
-
return
|
|
726
|
+
return _createBinding(currentState, handler, handlerOptions, registry);
|
|
630
727
|
};
|
|
631
728
|
}
|
|
632
729
|
if (prop === "handle") {
|
|
633
730
|
return (opts) => {
|
|
634
731
|
const { handler, ...rest } = opts;
|
|
635
|
-
return
|
|
732
|
+
return _createBinding(currentState, handler, rest, registry);
|
|
636
733
|
};
|
|
637
734
|
}
|
|
638
735
|
return void 0;
|
|
@@ -645,79 +742,17 @@ function createShortcutBuilder(options = {}) {
|
|
|
645
742
|
options
|
|
646
743
|
};
|
|
647
744
|
return {
|
|
648
|
-
builder:
|
|
745
|
+
builder: _createProxy(initialState),
|
|
649
746
|
registry
|
|
650
747
|
};
|
|
651
748
|
}
|
|
652
749
|
|
|
653
750
|
// src/hook.ts
|
|
654
|
-
function normalizeShortcutMapKeys(keys) {
|
|
655
|
-
if (Array.isArray(keys)) {
|
|
656
|
-
return keys.map((key) => key.trim()).filter(Boolean);
|
|
657
|
-
}
|
|
658
|
-
const trimmed = keys.trim();
|
|
659
|
-
if (!trimmed) return [];
|
|
660
|
-
if (trimmed.includes(" then ")) {
|
|
661
|
-
return trimmed.split(/\s+then\s+/i).map((key) => key.trim()).filter(Boolean);
|
|
662
|
-
}
|
|
663
|
-
if (trimmed.includes(" ") && !trimmed.includes("+")) {
|
|
664
|
-
return trimmed.split(/\s+/).map((key) => key.trim()).filter(Boolean);
|
|
665
|
-
}
|
|
666
|
-
return [trimmed];
|
|
667
|
-
}
|
|
668
|
-
function applyStep(builder, step) {
|
|
669
|
-
const tokens = step.toLowerCase().split("+").map((token) => token.trim()).filter(Boolean);
|
|
670
|
-
if (tokens.length === 0) {
|
|
671
|
-
throw new Error("[useShortcutMap] Invalid step: empty shortcut step");
|
|
672
|
-
}
|
|
673
|
-
const key = tokens.pop();
|
|
674
|
-
let chain = builder;
|
|
675
|
-
for (const token of tokens) {
|
|
676
|
-
if (token === "ctrl" || token === "control") {
|
|
677
|
-
chain = chain.ctrl;
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
if (token === "shift") {
|
|
681
|
-
chain = chain.shift;
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
if (token === "alt" || token === "option") {
|
|
685
|
-
chain = chain.alt;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
if (token === "cmd" || token === "command" || token === "meta") {
|
|
689
|
-
chain = chain.cmd;
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
if (token === "mod") {
|
|
693
|
-
chain = chain.mod;
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
throw new Error(`[useShortcutMap] Unsupported modifier token "${token}" in step "${step}"`);
|
|
697
|
-
}
|
|
698
|
-
return chain.key(key);
|
|
699
|
-
}
|
|
700
|
-
function registerShortcutMap(builder, shortcutMap) {
|
|
701
|
-
const results = {};
|
|
702
|
-
for (const id of Object.keys(shortcutMap)) {
|
|
703
|
-
const entry = shortcutMap[id];
|
|
704
|
-
const steps = normalizeShortcutMapKeys(entry.keys);
|
|
705
|
-
if (steps.length === 0) {
|
|
706
|
-
throw new Error(`[useShortcutMap] Shortcut "${String(id)}" has no key steps`);
|
|
707
|
-
}
|
|
708
|
-
let chain = applyStep(builder, steps[0]);
|
|
709
|
-
for (const step of steps.slice(1)) {
|
|
710
|
-
chain = chain.then(step);
|
|
711
|
-
}
|
|
712
|
-
results[id] = chain.on(entry.handler, entry.options);
|
|
713
|
-
}
|
|
714
|
-
return results;
|
|
715
|
-
}
|
|
716
751
|
function useShortcut(options = {}) {
|
|
717
752
|
const optionsRef = useRef(options);
|
|
718
753
|
optionsRef.current = options;
|
|
719
754
|
const { builder, registry } = useMemo(() => {
|
|
720
|
-
return
|
|
755
|
+
return _createShortcutBuilder(optionsRef.current);
|
|
721
756
|
}, []);
|
|
722
757
|
useEffect(() => {
|
|
723
758
|
registry.options = optionsRef.current;
|
|
@@ -725,60 +760,22 @@ function useShortcut(options = {}) {
|
|
|
725
760
|
const scopes = Array.isArray(optionsRef.current.activeScopes) ? optionsRef.current.activeScopes : [optionsRef.current.activeScopes];
|
|
726
761
|
registry.activeScopes = new Set(scopes.map((scope) => scope.trim()).filter(Boolean));
|
|
727
762
|
}
|
|
728
|
-
});
|
|
763
|
+
}, [registry, options]);
|
|
729
764
|
useEffect(() => {
|
|
730
765
|
return () => {
|
|
731
|
-
registry.listeners.forEach((entry) => entry.unbind());
|
|
732
766
|
registry.listeners.clear();
|
|
767
|
+
registry.firstStepIndex.clear();
|
|
768
|
+
registry.activeSequenceCombos.clear();
|
|
769
|
+
if (registry.listener && registry.listenerTarget) {
|
|
770
|
+
registry.listenerTarget.removeEventListener(registry.listenerEventType, registry.listener);
|
|
771
|
+
registry.listener = null;
|
|
772
|
+
registry.listenerTarget = null;
|
|
773
|
+
}
|
|
733
774
|
};
|
|
734
775
|
}, [registry]);
|
|
735
776
|
return builder;
|
|
736
777
|
}
|
|
737
|
-
function useShortcutMap(shortcutMap, options = {}) {
|
|
738
|
-
const $ = useShortcut(options);
|
|
739
|
-
return registerShortcutMap($, shortcutMap);
|
|
740
|
-
}
|
|
741
|
-
function createShortcut(options = {}) {
|
|
742
|
-
const { builder } = createShortcutBuilder(options);
|
|
743
|
-
return builder;
|
|
744
|
-
}
|
|
745
|
-
function createShortcutMap(shortcutMap, options = {}) {
|
|
746
|
-
const builder = createShortcut(options);
|
|
747
|
-
return registerShortcutMap(builder, shortcutMap);
|
|
748
|
-
}
|
|
749
|
-
function createShortcutGroup() {
|
|
750
|
-
const results = [];
|
|
751
|
-
return {
|
|
752
|
-
add: (...entries) => {
|
|
753
|
-
results.push(...entries);
|
|
754
|
-
},
|
|
755
|
-
addMany: (entries) => {
|
|
756
|
-
if (Array.isArray(entries)) {
|
|
757
|
-
results.push(...entries);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
results.push(...Object.values(entries));
|
|
761
|
-
},
|
|
762
|
-
unbindAll: () => {
|
|
763
|
-
for (const entry of results) {
|
|
764
|
-
entry.unbind();
|
|
765
|
-
}
|
|
766
|
-
results.length = 0;
|
|
767
|
-
},
|
|
768
|
-
clear: () => {
|
|
769
|
-
results.length = 0;
|
|
770
|
-
},
|
|
771
|
-
getResults: () => [...results]
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
function useShortcutGroup() {
|
|
775
|
-
const groupRef = useRef(null);
|
|
776
|
-
if (!groupRef.current) {
|
|
777
|
-
groupRef.current = createShortcutGroup();
|
|
778
|
-
}
|
|
779
|
-
return groupRef.current;
|
|
780
|
-
}
|
|
781
778
|
|
|
782
|
-
export { ModifierAliases, ModifierDisplayOrder, ModifierDisplaySymbols, ModifierKey, Platform, SpecialKeyMap,
|
|
779
|
+
export { ModifierAliases, ModifierDisplayOrder, ModifierDisplaySymbols, ModifierKey, Platform, SpecialKeyMap, detectPlatform, formatShortcut, matchesAnyShortcut, matchesShortcut, parseShortcut, parseShortcuts, useShortcut };
|
|
783
780
|
//# sourceMappingURL=index.mjs.map
|
|
784
781
|
//# sourceMappingURL=index.mjs.map
|