@tanstack/hotkeys 0.7.1 → 0.9.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/README.md +15 -1
- package/dist/{manager.utils.js → _event-target.cjs} +5 -41
- package/dist/_event-target.cjs.map +1 -0
- package/dist/{manager.utils.cjs → _event-target.js} +3 -47
- package/dist/_event-target.js.map +1 -0
- package/dist/_keyboard-event.cjs +83 -0
- package/dist/_keyboard-event.cjs.map +1 -0
- package/dist/_keyboard-event.js +76 -0
- package/dist/_keyboard-event.js.map +1 -0
- package/dist/_match.cjs +49 -0
- package/dist/_match.cjs.map +1 -0
- package/dist/_match.js +49 -0
- package/dist/_match.js.map +1 -0
- package/dist/_named-keys.cjs +68 -0
- package/dist/_named-keys.cjs.map +1 -0
- package/dist/_named-keys.d.cts +8 -0
- package/dist/_named-keys.d.ts +8 -0
- package/dist/_named-keys.js +66 -0
- package/dist/_named-keys.js.map +1 -0
- package/dist/_recorder-chord.cjs +44 -0
- package/dist/_recorder-chord.cjs.map +1 -0
- package/dist/_recorder-chord.js +43 -0
- package/dist/_recorder-chord.js.map +1 -0
- package/dist/_recording-guard.cjs +59 -0
- package/dist/_recording-guard.cjs.map +1 -0
- package/dist/_recording-guard.js +56 -0
- package/dist/_recording-guard.js.map +1 -0
- package/dist/_registration.cjs +54 -0
- package/dist/_registration.cjs.map +1 -0
- package/dist/_registration.js +50 -0
- package/dist/_registration.js.map +1 -0
- package/dist/conflicts.cjs +55 -0
- package/dist/conflicts.cjs.map +1 -0
- package/dist/conflicts.d.cts +39 -0
- package/dist/conflicts.d.ts +39 -0
- package/dist/conflicts.js +55 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/constants.cjs +46 -171
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +4 -129
- package/dist/constants.d.ts +4 -129
- package/dist/constants.js +48 -164
- package/dist/constants.js.map +1 -1
- package/dist/display-labels.cjs +119 -0
- package/dist/display-labels.cjs.map +1 -0
- package/dist/display-labels.d.cts +93 -0
- package/dist/display-labels.d.ts +93 -0
- package/dist/display-labels.js +113 -0
- package/dist/display-labels.js.map +1 -0
- package/dist/format.cjs +30 -36
- package/dist/format.cjs.map +1 -1
- package/dist/format.d.cts +46 -7
- package/dist/format.d.ts +46 -7
- package/dist/format.js +31 -37
- package/dist/format.js.map +1 -1
- package/dist/hint.cjs +24 -0
- package/dist/hint.cjs.map +1 -0
- package/dist/hint.d.cts +23 -0
- package/dist/hint.d.ts +23 -0
- package/dist/hint.js +24 -0
- package/dist/hint.js.map +1 -0
- package/dist/hotkey-manager.cjs +53 -22
- package/dist/hotkey-manager.cjs.map +1 -1
- package/dist/hotkey-manager.d.cts +6 -2
- package/dist/hotkey-manager.d.ts +6 -2
- package/dist/hotkey-manager.js +49 -18
- package/dist/hotkey-manager.js.map +1 -1
- package/dist/hotkey-recorder.cjs +67 -11
- package/dist/hotkey-recorder.cjs.map +1 -1
- package/dist/hotkey-recorder.d.cts +6 -3
- package/dist/hotkey-recorder.d.ts +6 -3
- package/dist/hotkey-recorder.js +66 -10
- package/dist/hotkey-recorder.js.map +1 -1
- package/dist/hotkey-sequence-recorder.cjs +85 -22
- package/dist/hotkey-sequence-recorder.cjs.map +1 -1
- package/dist/hotkey-sequence-recorder.d.cts +9 -2
- package/dist/hotkey-sequence-recorder.d.ts +9 -2
- package/dist/hotkey-sequence-recorder.js +84 -21
- package/dist/hotkey-sequence-recorder.js.map +1 -1
- package/dist/{hotkey.d.ts → hotkey.types.d.cts} +55 -120
- package/dist/{hotkey.d.cts → hotkey.types.d.ts} +55 -120
- package/dist/index.cjs +18 -10
- package/dist/index.d.cts +14 -9
- package/dist/index.d.ts +14 -9
- package/dist/index.js +10 -6
- package/dist/key-state-tracker.cjs +24 -24
- package/dist/key-state-tracker.cjs.map +1 -1
- package/dist/key-state-tracker.js +24 -24
- package/dist/key-state-tracker.js.map +1 -1
- package/dist/key.types.d.cts +82 -0
- package/dist/key.types.d.ts +82 -0
- package/dist/match.cjs +21 -30
- package/dist/match.cjs.map +1 -1
- package/dist/match.d.cts +19 -3
- package/dist/match.d.ts +19 -3
- package/dist/match.js +19 -29
- package/dist/match.js.map +1 -1
- package/dist/parse.cjs +43 -26
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.d.cts +10 -4
- package/dist/parse.d.ts +10 -4
- package/dist/parse.js +35 -19
- package/dist/parse.js.map +1 -1
- package/dist/platform.cjs +55 -0
- package/dist/platform.cjs.map +1 -0
- package/dist/platform.d.cts +44 -0
- package/dist/platform.d.ts +44 -0
- package/dist/platform.js +53 -0
- package/dist/platform.js.map +1 -0
- package/dist/recorder-options.d.cts +34 -0
- package/dist/recorder-options.d.ts +34 -0
- package/dist/sequence-manager.cjs +54 -28
- package/dist/sequence-manager.cjs.map +1 -1
- package/dist/sequence-manager.d.cts +18 -3
- package/dist/sequence-manager.d.ts +18 -3
- package/dist/sequence-manager.js +49 -23
- package/dist/sequence-manager.js.map +1 -1
- package/dist/validate.cjs +6 -10
- package/dist/validate.cjs.map +1 -1
- package/dist/validate.d.cts +13 -2
- package/dist/validate.d.ts +13 -2
- package/dist/validate.js +7 -11
- package/dist/validate.js.map +1 -1
- package/package.json +2 -2
- package/src/{manager.utils.ts → _event-target.ts} +0 -74
- package/src/_keyboard-event.ts +101 -0
- package/src/_match.ts +96 -0
- package/src/_named-keys.ts +62 -0
- package/src/_recorder-chord.ts +76 -0
- package/src/_recording-guard.ts +63 -0
- package/src/_registration.ts +79 -0
- package/src/conflicts.ts +125 -0
- package/src/constants.ts +48 -196
- package/src/display-labels.ts +123 -0
- package/src/format.ts +111 -52
- package/src/hint.ts +42 -0
- package/src/hotkey-manager.ts +95 -40
- package/src/hotkey-recorder.ts +92 -16
- package/src/hotkey-sequence-recorder.ts +110 -28
- package/src/{hotkey.ts → hotkey.types.ts} +57 -224
- package/src/index.ts +11 -5
- package/src/key-state-tracker.ts +36 -24
- package/src/key.types.ts +268 -0
- package/src/match.ts +41 -90
- package/src/parse.ts +67 -26
- package/src/platform.ts +66 -0
- package/src/recorder-options.ts +34 -0
- package/src/sequence-manager.ts +77 -42
- package/src/validate.ts +29 -21
- package/dist/manager.utils.cjs.map +0 -1
- package/dist/manager.utils.d.cts +0 -13
- package/dist/manager.utils.d.ts +0 -13
- package/dist/manager.utils.js.map +0 -1
- package/dist/recorder-chord.cjs +0 -18
- package/dist/recorder-chord.cjs.map +0 -1
- package/dist/recorder-chord.js +0 -18
- package/dist/recorder-chord.js.map +0 -1
- package/src/recorder-chord.ts +0 -25
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/platform.ts
|
|
3
|
+
/**
|
|
4
|
+
* Detects the current platform based on browser navigator properties.
|
|
5
|
+
*
|
|
6
|
+
* Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
|
|
7
|
+
* Control elsewhere) and for platform-specific hotkey formatting.
|
|
8
|
+
*
|
|
9
|
+
* @returns The detected platform: 'mac', 'windows', or 'linux'
|
|
10
|
+
* @remarks Defaults to 'linux' in SSR environments where navigator is undefined
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
|
|
15
|
+
* const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function detectPlatform() {
|
|
19
|
+
if (typeof navigator === "undefined") return "linux";
|
|
20
|
+
const platform = navigator.platform?.toLowerCase() ?? "";
|
|
21
|
+
const userAgent = navigator.userAgent?.toLowerCase() ?? "";
|
|
22
|
+
if (platform.includes("mac") || userAgent.includes("mac")) return "mac";
|
|
23
|
+
if (platform.includes("win") || userAgent.includes("win")) return "windows";
|
|
24
|
+
return "linux";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
|
|
28
|
+
*
|
|
29
|
+
* The 'Mod' token represents the "primary modifier" on each platform:
|
|
30
|
+
* - macOS: 'Meta' (Command key ⌘)
|
|
31
|
+
* - Windows/Linux: 'Control' (Ctrl key)
|
|
32
|
+
*
|
|
33
|
+
* This enables cross-platform hotkey definitions like 'Mod+S' that automatically
|
|
34
|
+
* map to Command+S on Mac and Ctrl+S on Windows/Linux.
|
|
35
|
+
*
|
|
36
|
+
* @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.
|
|
37
|
+
* @param platform - The target platform. Defaults to auto-detection.
|
|
38
|
+
* @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* resolveModifier('Mod', 'mac') // 'Meta'
|
|
43
|
+
* resolveModifier('Mod', 'windows') // 'Control'
|
|
44
|
+
* resolveModifier('Control', 'mac') // 'Control' (unchanged)
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function resolveModifier(modifier, platform = detectPlatform()) {
|
|
48
|
+
if (modifier === "Mod") return platform === "mac" ? "Meta" : "Control";
|
|
49
|
+
return modifier;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
exports.detectPlatform = detectPlatform;
|
|
54
|
+
exports.resolveModifier = resolveModifier;
|
|
55
|
+
//# sourceMappingURL=platform.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.cjs","names":[],"sources":["../src/platform.ts"],"sourcesContent":["import type { CanonicalModifier } from './key.types'\n\n/**\n * Detects the current platform based on browser navigator properties.\n *\n * Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,\n * Control elsewhere) and for platform-specific hotkey formatting.\n *\n * @returns The detected platform: 'mac', 'windows', or 'linux'\n * @remarks Defaults to 'linux' in SSR environments where navigator is undefined\n *\n * @example\n * ```ts\n * const platform = detectPlatform() // 'mac' | 'windows' | 'linux'\n * const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere\n * ```\n */\nexport function detectPlatform(): 'mac' | 'windows' | 'linux' {\n if (typeof navigator === 'undefined') {\n return 'linux' // Default for SSR\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const platform = navigator.platform?.toLowerCase() ?? ''\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const userAgent = navigator.userAgent?.toLowerCase() ?? ''\n\n if (platform.includes('mac') || userAgent.includes('mac')) {\n return 'mac'\n }\n if (platform.includes('win') || userAgent.includes('win')) {\n return 'windows'\n }\n return 'linux'\n}\n\n/**\n * Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.\n *\n * The 'Mod' token represents the \"primary modifier\" on each platform:\n * - macOS: 'Meta' (Command key ⌘)\n * - Windows/Linux: 'Control' (Ctrl key)\n *\n * This enables cross-platform hotkey definitions like 'Mod+S' that automatically\n * map to Command+S on Mac and Ctrl+S on Windows/Linux.\n *\n * @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.\n * @param platform - The target platform. Defaults to auto-detection.\n * @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')\n *\n * @example\n * ```ts\n * resolveModifier('Mod', 'mac') // 'Meta'\n * resolveModifier('Mod', 'windows') // 'Control'\n * resolveModifier('Control', 'mac') // 'Control' (unchanged)\n * ```\n */\nexport function resolveModifier(\n modifier: CanonicalModifier | 'Mod',\n platform: 'mac' | 'windows' | 'linux' = detectPlatform(),\n): CanonicalModifier {\n if (modifier === 'Mod') {\n return platform === 'mac' ? 'Meta' : 'Control'\n }\n return modifier\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAA8C;AAC5D,KAAI,OAAO,cAAc,YACvB,QAAO;CAIT,MAAM,WAAW,UAAU,UAAU,aAAa,IAAI;CAEtD,MAAM,YAAY,UAAU,WAAW,aAAa,IAAI;AAExD,KAAI,SAAS,SAAS,MAAM,IAAI,UAAU,SAAS,MAAM,CACvD,QAAO;AAET,KAAI,SAAS,SAAS,MAAM,IAAI,UAAU,SAAS,MAAM,CACvD,QAAO;AAET,QAAO;;;;;;;;;;;;;;;;;;;;;;;AAwBT,SAAgB,gBACd,UACA,WAAwC,gBAAgB,EACrC;AACnB,KAAI,aAAa,MACf,QAAO,aAAa,QAAQ,SAAS;AAEvC,QAAO"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CanonicalModifier } from "./key.types.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/platform.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detects the current platform based on browser navigator properties.
|
|
6
|
+
*
|
|
7
|
+
* Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
|
|
8
|
+
* Control elsewhere) and for platform-specific hotkey formatting.
|
|
9
|
+
*
|
|
10
|
+
* @returns The detected platform: 'mac', 'windows', or 'linux'
|
|
11
|
+
* @remarks Defaults to 'linux' in SSR environments where navigator is undefined
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
|
|
16
|
+
* const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare function detectPlatform(): 'mac' | 'windows' | 'linux';
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
|
|
22
|
+
*
|
|
23
|
+
* The 'Mod' token represents the "primary modifier" on each platform:
|
|
24
|
+
* - macOS: 'Meta' (Command key ⌘)
|
|
25
|
+
* - Windows/Linux: 'Control' (Ctrl key)
|
|
26
|
+
*
|
|
27
|
+
* This enables cross-platform hotkey definitions like 'Mod+S' that automatically
|
|
28
|
+
* map to Command+S on Mac and Ctrl+S on Windows/Linux.
|
|
29
|
+
*
|
|
30
|
+
* @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.
|
|
31
|
+
* @param platform - The target platform. Defaults to auto-detection.
|
|
32
|
+
* @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* resolveModifier('Mod', 'mac') // 'Meta'
|
|
37
|
+
* resolveModifier('Mod', 'windows') // 'Control'
|
|
38
|
+
* resolveModifier('Control', 'mac') // 'Control' (unchanged)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function resolveModifier(modifier: CanonicalModifier | 'Mod', platform?: 'mac' | 'windows' | 'linux'): CanonicalModifier;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { detectPlatform, resolveModifier };
|
|
44
|
+
//# sourceMappingURL=platform.d.cts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CanonicalModifier } from "./key.types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/platform.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detects the current platform based on browser navigator properties.
|
|
6
|
+
*
|
|
7
|
+
* Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
|
|
8
|
+
* Control elsewhere) and for platform-specific hotkey formatting.
|
|
9
|
+
*
|
|
10
|
+
* @returns The detected platform: 'mac', 'windows', or 'linux'
|
|
11
|
+
* @remarks Defaults to 'linux' in SSR environments where navigator is undefined
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
|
|
16
|
+
* const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare function detectPlatform(): 'mac' | 'windows' | 'linux';
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
|
|
22
|
+
*
|
|
23
|
+
* The 'Mod' token represents the "primary modifier" on each platform:
|
|
24
|
+
* - macOS: 'Meta' (Command key ⌘)
|
|
25
|
+
* - Windows/Linux: 'Control' (Ctrl key)
|
|
26
|
+
*
|
|
27
|
+
* This enables cross-platform hotkey definitions like 'Mod+S' that automatically
|
|
28
|
+
* map to Command+S on Mac and Ctrl+S on Windows/Linux.
|
|
29
|
+
*
|
|
30
|
+
* @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.
|
|
31
|
+
* @param platform - The target platform. Defaults to auto-detection.
|
|
32
|
+
* @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* resolveModifier('Mod', 'mac') // 'Meta'
|
|
37
|
+
* resolveModifier('Mod', 'windows') // 'Control'
|
|
38
|
+
* resolveModifier('Control', 'mac') // 'Control' (unchanged)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function resolveModifier(modifier: CanonicalModifier | 'Mod', platform?: 'mac' | 'windows' | 'linux'): CanonicalModifier;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { detectPlatform, resolveModifier };
|
|
44
|
+
//# sourceMappingURL=platform.d.ts.map
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/platform.ts
|
|
2
|
+
/**
|
|
3
|
+
* Detects the current platform based on browser navigator properties.
|
|
4
|
+
*
|
|
5
|
+
* Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
|
|
6
|
+
* Control elsewhere) and for platform-specific hotkey formatting.
|
|
7
|
+
*
|
|
8
|
+
* @returns The detected platform: 'mac', 'windows', or 'linux'
|
|
9
|
+
* @remarks Defaults to 'linux' in SSR environments where navigator is undefined
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
|
|
14
|
+
* const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
function detectPlatform() {
|
|
18
|
+
if (typeof navigator === "undefined") return "linux";
|
|
19
|
+
const platform = navigator.platform?.toLowerCase() ?? "";
|
|
20
|
+
const userAgent = navigator.userAgent?.toLowerCase() ?? "";
|
|
21
|
+
if (platform.includes("mac") || userAgent.includes("mac")) return "mac";
|
|
22
|
+
if (platform.includes("win") || userAgent.includes("win")) return "windows";
|
|
23
|
+
return "linux";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
|
|
27
|
+
*
|
|
28
|
+
* The 'Mod' token represents the "primary modifier" on each platform:
|
|
29
|
+
* - macOS: 'Meta' (Command key ⌘)
|
|
30
|
+
* - Windows/Linux: 'Control' (Ctrl key)
|
|
31
|
+
*
|
|
32
|
+
* This enables cross-platform hotkey definitions like 'Mod+S' that automatically
|
|
33
|
+
* map to Command+S on Mac and Ctrl+S on Windows/Linux.
|
|
34
|
+
*
|
|
35
|
+
* @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.
|
|
36
|
+
* @param platform - The target platform. Defaults to auto-detection.
|
|
37
|
+
* @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* resolveModifier('Mod', 'mac') // 'Meta'
|
|
42
|
+
* resolveModifier('Mod', 'windows') // 'Control'
|
|
43
|
+
* resolveModifier('Control', 'mac') // 'Control' (unchanged)
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function resolveModifier(modifier, platform = detectPlatform()) {
|
|
47
|
+
if (modifier === "Mod") return platform === "mac" ? "Meta" : "Control";
|
|
48
|
+
return modifier;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { detectPlatform, resolveModifier };
|
|
53
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","names":[],"sources":["../src/platform.ts"],"sourcesContent":["import type { CanonicalModifier } from './key.types'\n\n/**\n * Detects the current platform based on browser navigator properties.\n *\n * Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,\n * Control elsewhere) and for platform-specific hotkey formatting.\n *\n * @returns The detected platform: 'mac', 'windows', or 'linux'\n * @remarks Defaults to 'linux' in SSR environments where navigator is undefined\n *\n * @example\n * ```ts\n * const platform = detectPlatform() // 'mac' | 'windows' | 'linux'\n * const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere\n * ```\n */\nexport function detectPlatform(): 'mac' | 'windows' | 'linux' {\n if (typeof navigator === 'undefined') {\n return 'linux' // Default for SSR\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const platform = navigator.platform?.toLowerCase() ?? ''\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const userAgent = navigator.userAgent?.toLowerCase() ?? ''\n\n if (platform.includes('mac') || userAgent.includes('mac')) {\n return 'mac'\n }\n if (platform.includes('win') || userAgent.includes('win')) {\n return 'windows'\n }\n return 'linux'\n}\n\n/**\n * Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.\n *\n * The 'Mod' token represents the \"primary modifier\" on each platform:\n * - macOS: 'Meta' (Command key ⌘)\n * - Windows/Linux: 'Control' (Ctrl key)\n *\n * This enables cross-platform hotkey definitions like 'Mod+S' that automatically\n * map to Command+S on Mac and Ctrl+S on Windows/Linux.\n *\n * @param modifier - The modifier to resolve. If 'Mod', resolves based on platform.\n * @param platform - The target platform. Defaults to auto-detection.\n * @returns The canonical modifier name ('Control', 'Shift', 'Alt', or 'Meta')\n *\n * @example\n * ```ts\n * resolveModifier('Mod', 'mac') // 'Meta'\n * resolveModifier('Mod', 'windows') // 'Control'\n * resolveModifier('Control', 'mac') // 'Control' (unchanged)\n * ```\n */\nexport function resolveModifier(\n modifier: CanonicalModifier | 'Mod',\n platform: 'mac' | 'windows' | 'linux' = detectPlatform(),\n): CanonicalModifier {\n if (modifier === 'Mod') {\n return platform === 'mac' ? 'Meta' : 'Control'\n }\n return modifier\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAA8C;AAC5D,KAAI,OAAO,cAAc,YACvB,QAAO;CAIT,MAAM,WAAW,UAAU,UAAU,aAAa,IAAI;CAEtD,MAAM,YAAY,UAAU,WAAW,aAAa,IAAI;AAExD,KAAI,SAAS,SAAS,MAAM,IAAI,UAAU,SAAS,MAAM,CACvD,QAAO;AAET,KAAI,SAAS,SAAS,MAAM,IAAI,UAAU,SAAS,MAAM,CACvD,QAAO;AAET,QAAO;;;;;;;;;;;;;;;;;;;;;;;AAwBT,SAAgB,gBACd,UACA,WAAwC,gBAAgB,EACrC;AACnB,KAAI,aAAa,MACf,QAAO,aAAa,QAAQ,SAAS;AAEvC,QAAO"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Hotkey, ParsedHotkey } from "./hotkey.types.cjs";
|
|
2
|
+
import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.cjs";
|
|
3
|
+
|
|
4
|
+
//#region src/recorder-options.d.ts
|
|
5
|
+
/** Event property used to record a binding. Physical codes are the default. */
|
|
6
|
+
type RecorderKeyMode = 'code' | 'key';
|
|
7
|
+
interface RecorderRejection {
|
|
8
|
+
reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation';
|
|
9
|
+
message: string;
|
|
10
|
+
hotkey?: Hotkey;
|
|
11
|
+
sequence?: Array<Hotkey>;
|
|
12
|
+
conflicts?: Array<HotkeyConflict>;
|
|
13
|
+
}
|
|
14
|
+
interface RecorderOptions {
|
|
15
|
+
/** Physical code recording is the default. Choose key to record logical characters. */
|
|
16
|
+
recordBy?: RecorderKeyMode;
|
|
17
|
+
/** Platform used for Mod and AltGraph handling. Defaults to platform detection. */
|
|
18
|
+
platform?: 'mac' | 'windows' | 'linux';
|
|
19
|
+
/** Opt in to live-registry conflicts, including sequence prefixes. Default: false. */
|
|
20
|
+
detectConflicts?: boolean | HotkeyConflictOptions;
|
|
21
|
+
/** Rejected candidates leave recording active. */
|
|
22
|
+
onReject?: (rejection: RecorderRejection) => void;
|
|
23
|
+
}
|
|
24
|
+
interface HotkeyRecorderValidationContext {
|
|
25
|
+
event: KeyboardEvent;
|
|
26
|
+
parsedHotkey: ParsedHotkey;
|
|
27
|
+
}
|
|
28
|
+
interface HotkeySequenceRecorderValidationContext {
|
|
29
|
+
events: ReadonlyArray<KeyboardEvent>;
|
|
30
|
+
parsedSequence: ReadonlyArray<ParsedHotkey>;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { HotkeyRecorderValidationContext, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection };
|
|
34
|
+
//# sourceMappingURL=recorder-options.d.cts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Hotkey, ParsedHotkey } from "./hotkey.types.js";
|
|
2
|
+
import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.js";
|
|
3
|
+
|
|
4
|
+
//#region src/recorder-options.d.ts
|
|
5
|
+
/** Event property used to record a binding. Physical codes are the default. */
|
|
6
|
+
type RecorderKeyMode = 'code' | 'key';
|
|
7
|
+
interface RecorderRejection {
|
|
8
|
+
reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation';
|
|
9
|
+
message: string;
|
|
10
|
+
hotkey?: Hotkey;
|
|
11
|
+
sequence?: Array<Hotkey>;
|
|
12
|
+
conflicts?: Array<HotkeyConflict>;
|
|
13
|
+
}
|
|
14
|
+
interface RecorderOptions {
|
|
15
|
+
/** Physical code recording is the default. Choose key to record logical characters. */
|
|
16
|
+
recordBy?: RecorderKeyMode;
|
|
17
|
+
/** Platform used for Mod and AltGraph handling. Defaults to platform detection. */
|
|
18
|
+
platform?: 'mac' | 'windows' | 'linux';
|
|
19
|
+
/** Opt in to live-registry conflicts, including sequence prefixes. Default: false. */
|
|
20
|
+
detectConflicts?: boolean | HotkeyConflictOptions;
|
|
21
|
+
/** Rejected candidates leave recording active. */
|
|
22
|
+
onReject?: (rejection: RecorderRejection) => void;
|
|
23
|
+
}
|
|
24
|
+
interface HotkeyRecorderValidationContext {
|
|
25
|
+
event: KeyboardEvent;
|
|
26
|
+
parsedHotkey: ParsedHotkey;
|
|
27
|
+
}
|
|
28
|
+
interface HotkeySequenceRecorderValidationContext {
|
|
29
|
+
events: ReadonlyArray<KeyboardEvent>;
|
|
30
|
+
parsedSequence: ReadonlyArray<ParsedHotkey>;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { HotkeyRecorderValidationContext, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection };
|
|
34
|
+
//# sourceMappingURL=recorder-options.d.ts.map
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const require_constants = require('./constants.cjs');
|
|
2
|
+
const require_platform = require('./platform.cjs');
|
|
3
|
+
const require__keyboard_event = require('./_keyboard-event.cjs');
|
|
2
4
|
const require_parse = require('./parse.cjs');
|
|
3
5
|
const require_format = require('./format.cjs');
|
|
6
|
+
const require__match = require('./_match.cjs');
|
|
4
7
|
const require_match = require('./match.cjs');
|
|
5
|
-
const
|
|
8
|
+
const require__recording_guard = require('./_recording-guard.cjs');
|
|
9
|
+
const require__registration = require('./_registration.cjs');
|
|
10
|
+
const require__event_target = require('./_event-target.cjs');
|
|
6
11
|
let _tanstack_store = require("@tanstack/store");
|
|
7
12
|
|
|
8
13
|
//#region src/sequence-manager.ts
|
|
@@ -19,12 +24,6 @@ function generateSequenceId() {
|
|
|
19
24
|
return `sequence_${++sequenceIdCounter}`;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
|
-
* Returns a canonical string for sequence conflict comparison.
|
|
23
|
-
*/
|
|
24
|
-
function sequenceKey(sequence) {
|
|
25
|
-
return sequence.join("|");
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
27
|
* Manages keyboard sequence matching for Vim-style shortcuts.
|
|
29
28
|
*
|
|
30
29
|
* This class allows registering multi-key sequences like 'g g' or 'd d'
|
|
@@ -74,7 +73,7 @@ var SequenceManager = class SequenceManager {
|
|
|
74
73
|
#platform;
|
|
75
74
|
constructor() {
|
|
76
75
|
this.registrations = new _tanstack_store.Store(/* @__PURE__ */ new Map());
|
|
77
|
-
this.#platform =
|
|
76
|
+
this.#platform = require_platform.detectPlatform();
|
|
78
77
|
}
|
|
79
78
|
/**
|
|
80
79
|
* Gets the singleton instance of SequenceManager.
|
|
@@ -110,17 +109,17 @@ var SequenceManager = class SequenceManager {
|
|
|
110
109
|
const parsedSequence = sequence.map((hotkey) => require_parse.parseHotkey(hotkey, platform));
|
|
111
110
|
const target = options.target ?? (typeof document !== "undefined" ? document : {});
|
|
112
111
|
const conflictBehavior = options.conflictBehavior ?? "warn";
|
|
113
|
-
const conflictingRegistration = this.#findConflictingSequence(
|
|
114
|
-
if (conflictingRegistration)
|
|
112
|
+
const conflictingRegistration = this.#findConflictingSequence(parsedSequence, target);
|
|
113
|
+
if (conflictingRegistration) require__registration.handleConflict(conflictingRegistration.id, require_format.formatHotkeySequence(sequence), conflictBehavior, (regId) => this.#unregister(regId));
|
|
115
114
|
const firstStep = parsedSequence[0];
|
|
116
|
-
const resolvedIgnoreInputs = options.ignoreInputs ??
|
|
115
|
+
const resolvedIgnoreInputs = options.ignoreInputs ?? require__registration.getDefaultIgnoreInputs(firstStep);
|
|
117
116
|
const registration = {
|
|
118
117
|
id,
|
|
119
118
|
sequence,
|
|
120
119
|
parsedSequence,
|
|
121
120
|
callback,
|
|
122
121
|
options: {
|
|
123
|
-
...
|
|
122
|
+
...require__registration.defaultHotkeyOptions,
|
|
124
123
|
timeout: DEFAULT_SEQUENCE_TIMEOUT,
|
|
125
124
|
...options,
|
|
126
125
|
platform,
|
|
@@ -154,7 +153,10 @@ var SequenceManager = class SequenceManager {
|
|
|
154
153
|
},
|
|
155
154
|
setOptions: (newOptions) => {
|
|
156
155
|
const reg = manager.#registrations.get(id);
|
|
157
|
-
if (reg
|
|
156
|
+
if (reg && !require__registration.optionsEqual(reg.options, {
|
|
157
|
+
...reg.options,
|
|
158
|
+
...newOptions
|
|
159
|
+
})) {
|
|
158
160
|
reg.options = {
|
|
159
161
|
...reg.options,
|
|
160
162
|
...newOptions
|
|
@@ -233,17 +235,30 @@ var SequenceManager = class SequenceManager {
|
|
|
233
235
|
* Processes keyboard events for a specific target and event type.
|
|
234
236
|
*/
|
|
235
237
|
#processTargetEvent(event, target, eventType) {
|
|
238
|
+
if (require__recording_guard.isRecordingEvent(event)) {
|
|
239
|
+
for (const id of this.#targetRegistrations.get(target) ?? []) {
|
|
240
|
+
const registration = this.#registrations.get(id);
|
|
241
|
+
if (registration && registration.currentIndex > 0) {
|
|
242
|
+
registration.currentIndex = 0;
|
|
243
|
+
this.#syncRegistrationView(registration);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (event.type === "keydown" && event.repeat || require__keyboard_event.normalizeKeyboardEvent(event).isComposing) return;
|
|
236
249
|
if (require_parse.isModifierKey(require_constants.normalizeKeyName(event.key))) return;
|
|
237
250
|
const targetRegs = this.#targetRegistrations.get(target);
|
|
238
251
|
if (!targetRegs) return;
|
|
239
252
|
const now = Date.now();
|
|
253
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
254
|
+
let bestScore = 0;
|
|
240
255
|
registrationIds: for (const id of targetRegs) {
|
|
241
256
|
const registration = this.#registrations.get(id);
|
|
242
257
|
if (!registration) continue;
|
|
243
|
-
if (!
|
|
258
|
+
if (!require__event_target.isEventForTarget(event, target)) continue;
|
|
244
259
|
if (!registration.options.enabled) continue;
|
|
245
260
|
if (registration.options.ignoreInputs !== false) {
|
|
246
|
-
if (
|
|
261
|
+
if (require__event_target.shouldIgnoreInputEvent(event, target, registration.target)) continue registrationIds;
|
|
247
262
|
}
|
|
248
263
|
if (registration.options.eventType !== eventType) continue;
|
|
249
264
|
const timeout = registration.options.timeout ?? 1e3;
|
|
@@ -253,9 +268,24 @@ var SequenceManager = class SequenceManager {
|
|
|
253
268
|
}
|
|
254
269
|
const expectedHotkey = registration.parsedSequence[registration.currentIndex];
|
|
255
270
|
if (!expectedHotkey) continue;
|
|
256
|
-
|
|
271
|
+
let match = require__match.matchKeyboardEvent(event, expectedHotkey, registration.options.platform);
|
|
272
|
+
let nextIndex = registration.currentIndex + 1;
|
|
273
|
+
if (!match.matched && registration.currentIndex > 0) {
|
|
274
|
+
match = require__match.matchKeyboardEvent(event, registration.parsedSequence[0], registration.options.platform);
|
|
275
|
+
nextIndex = 1;
|
|
276
|
+
}
|
|
277
|
+
candidates.set(id, {
|
|
278
|
+
score: match.score,
|
|
279
|
+
nextIndex
|
|
280
|
+
});
|
|
281
|
+
bestScore = Math.max(bestScore, match.score);
|
|
282
|
+
}
|
|
283
|
+
for (const [id, candidate] of candidates) {
|
|
284
|
+
const registration = this.#registrations.get(id);
|
|
285
|
+
if (!registration || !registration.options.enabled) continue;
|
|
286
|
+
if (candidate.score > 0 && candidate.score === bestScore) {
|
|
257
287
|
registration.lastKeyTime = now;
|
|
258
|
-
registration.currentIndex
|
|
288
|
+
registration.currentIndex = candidate.nextIndex;
|
|
259
289
|
if (registration.currentIndex >= registration.parsedSequence.length) {
|
|
260
290
|
if (registration.options.preventDefault) event.preventDefault();
|
|
261
291
|
if (registration.options.stopPropagation) event.stopPropagation();
|
|
@@ -270,21 +300,16 @@ var SequenceManager = class SequenceManager {
|
|
|
270
300
|
registration.callback(event, context);
|
|
271
301
|
} else this.#syncRegistrationView(registration);
|
|
272
302
|
} else if (registration.currentIndex > 0) {
|
|
273
|
-
|
|
274
|
-
if (require_match.matchesKeyboardEvent(event, firstHotkey, registration.options.platform)) {
|
|
275
|
-
registration.currentIndex = 1;
|
|
276
|
-
registration.lastKeyTime = now;
|
|
277
|
-
} else registration.currentIndex = 0;
|
|
303
|
+
registration.currentIndex = 0;
|
|
278
304
|
this.#syncRegistrationView(registration);
|
|
279
305
|
}
|
|
280
306
|
}
|
|
281
307
|
}
|
|
282
308
|
/**
|
|
283
|
-
*
|
|
309
|
+
* Compares resolved step identities without changing the supplied sequence strings.
|
|
284
310
|
*/
|
|
285
|
-
#findConflictingSequence(
|
|
286
|
-
const
|
|
287
|
-
for (const registration of this.#registrations.values()) if (sequenceKey(registration.sequence) === key && registration.target === target) return registration;
|
|
311
|
+
#findConflictingSequence(parsedSequence, target) {
|
|
312
|
+
for (const registration of this.#registrations.values()) if (registration.target === target && registration.parsedSequence.length === parsedSequence.length && parsedSequence.every((step, index) => require_match.areHotkeysEqual(step, registration.parsedSequence[index]))) return registration;
|
|
288
313
|
return null;
|
|
289
314
|
}
|
|
290
315
|
/**
|
|
@@ -310,7 +335,7 @@ var SequenceManager = class SequenceManager {
|
|
|
310
335
|
const lastParsed = registration.parsedSequence[registration.parsedSequence.length - 1];
|
|
311
336
|
if (!lastParsed) return false;
|
|
312
337
|
const syntheticEvent = new KeyboardEvent(registration.options.eventType ?? "keydown", {
|
|
313
|
-
key: lastParsed.key,
|
|
338
|
+
...lastParsed.code !== void 0 ? { code: lastParsed.code } : { key: lastParsed.key },
|
|
314
339
|
ctrlKey: lastParsed.ctrl,
|
|
315
340
|
shiftKey: lastParsed.shift,
|
|
316
341
|
altKey: lastParsed.alt,
|
|
@@ -373,13 +398,14 @@ function getSequenceManager() {
|
|
|
373
398
|
* ```
|
|
374
399
|
*/
|
|
375
400
|
function createSequenceMatcher(sequence, options = {}) {
|
|
376
|
-
const platform = options.platform ??
|
|
401
|
+
const platform = options.platform ?? require_platform.detectPlatform();
|
|
377
402
|
const timeout = options.timeout ?? 1e3;
|
|
378
403
|
const parsedSequence = sequence.map((hotkey) => require_parse.parseHotkey(hotkey, platform));
|
|
379
404
|
let currentIndex = 0;
|
|
380
405
|
let lastKeyTime = 0;
|
|
381
406
|
return {
|
|
382
407
|
match(event) {
|
|
408
|
+
if (event.type === "keydown" && event.repeat || require__keyboard_event.normalizeKeyboardEvent(event, platform).isComposing || require_parse.isModifierKey(require_constants.normalizeKeyName(event.key))) return false;
|
|
383
409
|
const now = Date.now();
|
|
384
410
|
if (currentIndex > 0 && now - lastKeyTime > timeout) currentIndex = 0;
|
|
385
411
|
const expected = parsedSequence[currentIndex];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sequence-manager.cjs","names":["#instance","Store","#platform","detectPlatform","parseHotkey","#findConflictingSequence","formatHotkeySequence","#unregister","getDefaultIgnoreInputs","defaultHotkeyOptions","#registrations","#targetRegistrations","#ensureListenersForTarget","#removeListenersForTarget","#targetListeners","#createTargetKeyDownHandler","#createTargetKeyUpHandler","#processTargetEvent","isModifierKey","normalizeKeyName","isEventForTarget","shouldIgnoreInputEvent","#syncRegistrationView","matchesKeyboardEvent"],"sources":["../src/sequence-manager.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { formatHotkeySequence } from './format'\nimport { detectPlatform, normalizeKeyName } from './constants'\nimport { isModifierKey, parseHotkey } from './parse'\nimport { matchesKeyboardEvent } from './match'\nimport {\n defaultHotkeyOptions,\n getDefaultIgnoreInputs,\n handleConflict,\n isEventForTarget,\n shouldIgnoreInputEvent,\n} from './manager.utils'\nimport type { HotkeyOptions } from './hotkey-manager'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyCallbackContext,\n ParsedHotkey,\n} from './hotkey'\n\ntype Target = HTMLElement | Document | Window\n\n/**\n * Options for hotkey sequence matching.\n * Extends HotkeyOptions but excludes requireReset (not applicable to sequences).\n */\nexport interface SequenceOptions extends Omit<HotkeyOptions, 'requireReset'> {\n /** Timeout between keys in milliseconds. Default: 1000 */\n timeout?: number\n}\n\n/**\n * A sequence of hotkeys for Vim-style shortcuts.\n *\n * Each element is one step (a `Hotkey` string). Steps may include modifiers;\n * the same modifier can appear on consecutive steps (e.g. `Shift+R` then\n * `Shift+T`). Modifier-only key events do not advance or reset matching—see\n * `SequenceManager`.\n *\n * @example\n * ```ts\n * const gotoTop: HotkeySequence = ['G', 'G'] // gg\n * const deleteLine: HotkeySequence = ['D', 'D'] // dd\n * const deleteWord: HotkeySequence = ['D', 'I', 'W'] // diw\n * const chainedShift: HotkeySequence = ['Shift+R', 'Shift+T']\n * ```\n */\nexport type HotkeySequence = Array<Hotkey>\n\n/**\n * Default timeout between keys in a sequence (in milliseconds).\n * Exported for consumers that display sequence state (e.g. devtools).\n */\nexport const DEFAULT_SEQUENCE_TIMEOUT = 1000\n\nlet sequenceIdCounter = 0\n\n/**\n * Generates a unique ID for sequence registrations.\n */\nfunction generateSequenceId(): string {\n return `sequence_${++sequenceIdCounter}`\n}\n\n/**\n * Returns a canonical string for sequence conflict comparison.\n */\nfunction sequenceKey(sequence: HotkeySequence): string {\n return sequence.join('|')\n}\n\n/**\n * View of a sequence registration for devtools display.\n * Progress fields reflect an in-progress match (between first key and completion or timeout).\n */\nexport interface SequenceRegistrationView {\n id: string\n sequence: HotkeySequence\n options: SequenceOptions\n target: Target\n triggerCount: number\n /** Whether this sequence has been triggered at least once. */\n hasFired: boolean\n /** Steps matched in the current attempt (0 when idle or just completed). */\n matchedStepCount: number\n /** `Date.now()` when the last step in the current attempt matched; 0 if none. */\n partialMatchLastKeyTime: number\n}\n\n/**\n * Internal representation of a sequence registration.\n */\ninterface SequenceRegistration {\n id: string\n sequence: HotkeySequence\n parsedSequence: Array<ParsedHotkey>\n callback: HotkeyCallback\n options: SequenceOptions\n target: Target\n currentIndex: number\n lastKeyTime: number\n triggerCount: number\n hasFired: boolean\n}\n\n/**\n * A handle returned from SequenceManager.register() that allows updating\n * the callback and options without re-registering the sequence.\n *\n * @example\n * ```ts\n * const handle = manager.register(['G', 'G'], callback, options)\n *\n * handle.callback = newCallback\n * handle.setOptions({ timeout: 500 })\n * handle.unregister()\n * ```\n */\nexport interface SequenceRegistrationHandle {\n readonly id: string\n readonly isActive: boolean\n callback: HotkeyCallback\n setOptions: (options: Partial<SequenceOptions>) => void\n unregister: () => void\n}\n\n/**\n * Manages keyboard sequence matching for Vim-style shortcuts.\n *\n * This class allows registering multi-key sequences like 'g g' or 'd d'\n * that trigger callbacks when the full sequence is pressed within\n * a configurable timeout.\n *\n * Modifier-only `keydown` / `keyup` events (Shift, Control, Alt, or Meta\n * alone) are not processed: they neither match a step nor reset progress.\n * That supports chained modifier chords (e.g. `Shift+R` then `Shift+T`) when\n * the user presses a modifier alone between steps.\n *\n * @example\n * ```ts\n * const matcher = SequenceManager.getInstance()\n *\n * // Register 'g g' to go to top\n * const unregister = matcher.register(['G', 'G'], (event, context) => {\n * scrollToTop()\n * }, { timeout: 500 })\n *\n * // Later, to unregister:\n * unregister()\n * ```\n */\n/**\n * Builds a devtools view from an internal registration.\n */\nfunction toRegistrationView(\n reg: SequenceRegistration,\n): SequenceRegistrationView {\n const len = reg.parsedSequence.length\n const inProgress = reg.currentIndex > 0 && reg.currentIndex < len\n return {\n id: reg.id,\n sequence: reg.sequence,\n options: reg.options,\n target: reg.target,\n triggerCount: reg.triggerCount,\n hasFired: reg.hasFired,\n matchedStepCount: inProgress ? reg.currentIndex : 0,\n partialMatchLastKeyTime: inProgress ? reg.lastKeyTime : 0,\n }\n}\n\nexport class SequenceManager {\n static #instance: SequenceManager | null = null\n\n /**\n * The TanStack Store containing sequence registration views for devtools.\n * Subscribe to this to observe registration changes.\n */\n readonly registrations: Store<Map<string, SequenceRegistrationView>> =\n new Store(new Map())\n\n #registrations: Map<string, SequenceRegistration> = new Map()\n #targetListeners: Map<\n Target,\n {\n keydown: (event: KeyboardEvent) => void\n keyup: (event: KeyboardEvent) => void\n }\n > = new Map()\n #targetRegistrations: Map<Target, Set<string>> = new Map()\n #platform: 'mac' | 'windows' | 'linux'\n\n private constructor() {\n this.#platform = detectPlatform()\n }\n\n /**\n * Gets the singleton instance of SequenceManager.\n */\n static getInstance(): SequenceManager {\n if (!SequenceManager.#instance) {\n SequenceManager.#instance = new SequenceManager()\n }\n return SequenceManager.#instance\n }\n\n /**\n * Resets the singleton instance. Useful for testing.\n */\n static resetInstance(): void {\n if (SequenceManager.#instance) {\n SequenceManager.#instance.destroy()\n SequenceManager.#instance = null\n }\n }\n\n #syncRegistrationView(registration: SequenceRegistration): void {\n this.registrations.setState((prev) =>\n new Map(prev).set(registration.id, toRegistrationView(registration)),\n )\n }\n\n /**\n * Registers a hotkey sequence handler.\n *\n * @param sequence - Array of hotkey strings that form the sequence\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior\n * @returns A handle to update or unregister the sequence\n */\n register(\n sequence: HotkeySequence,\n callback: HotkeyCallback,\n options: SequenceOptions = {},\n ): SequenceRegistrationHandle {\n if (sequence.length === 0) {\n throw new Error('Sequence must contain at least one hotkey')\n }\n\n const id = generateSequenceId()\n const platform = options.platform ?? this.#platform\n const parsedSequence = sequence.map((hotkey) =>\n parseHotkey(hotkey, platform),\n )\n\n // Resolve target: default to document if not provided or null\n const target: Target =\n options.target ??\n (typeof document !== 'undefined' ? document : ({} as Document))\n\n // Resolve conflict behavior\n const conflictBehavior = options.conflictBehavior ?? 'warn'\n\n // Check for existing registrations with the same sequence and target\n const conflictingRegistration = this.#findConflictingSequence(\n sequence,\n target,\n )\n\n if (conflictingRegistration) {\n handleConflict(\n conflictingRegistration.id,\n formatHotkeySequence(sequence),\n conflictBehavior,\n (regId) => this.#unregister(regId),\n )\n }\n\n const firstStep = parsedSequence[0]!\n const resolvedIgnoreInputs =\n options.ignoreInputs ?? getDefaultIgnoreInputs(firstStep)\n\n const baseOptions = {\n ...defaultHotkeyOptions,\n timeout: DEFAULT_SEQUENCE_TIMEOUT,\n ...options,\n platform,\n ignoreInputs: resolvedIgnoreInputs,\n }\n\n const registration: SequenceRegistration = {\n id,\n sequence,\n parsedSequence,\n callback,\n options: baseOptions,\n target,\n currentIndex: 0,\n lastKeyTime: 0,\n triggerCount: 0,\n hasFired: false,\n }\n\n this.#registrations.set(id, registration)\n this.registrations.setState((prev) =>\n new Map(prev).set(id, toRegistrationView(registration)),\n )\n\n // Track registration for this target\n if (!this.#targetRegistrations.has(target)) {\n this.#targetRegistrations.set(target, new Set())\n }\n this.#targetRegistrations.get(target)!.add(id)\n\n // Ensure listeners are attached for this target\n this.#ensureListenersForTarget(target)\n\n const manager = this\n const handle: SequenceRegistrationHandle = {\n get id() {\n return id\n },\n get isActive() {\n return manager.#registrations.has(id)\n },\n get callback() {\n const reg = manager.#registrations.get(id)\n return reg?.callback ?? callback\n },\n set callback(newCallback: HotkeyCallback) {\n const reg = manager.#registrations.get(id)\n if (reg) {\n reg.callback = newCallback\n }\n },\n setOptions: (newOptions: Partial<SequenceOptions>) => {\n const reg = manager.#registrations.get(id)\n if (reg) {\n reg.options = { ...reg.options, ...newOptions }\n manager.registrations.setState((prev) =>\n new Map(prev).set(id, toRegistrationView(reg)),\n )\n }\n },\n unregister: () => {\n manager.#unregister(id)\n },\n }\n\n return handle\n }\n\n /**\n * Unregisters a sequence by its registration ID.\n */\n #unregister(id: string): void {\n const registration = this.#registrations.get(id)\n if (!registration) {\n return\n }\n\n const target = registration.target\n\n this.#registrations.delete(id)\n this.registrations.setState((prev) => {\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n\n // Remove from target registrations tracking\n const targetRegs = this.#targetRegistrations.get(target)\n if (targetRegs) {\n targetRegs.delete(id)\n if (targetRegs.size === 0) {\n this.#removeListenersForTarget(target)\n }\n }\n }\n\n /**\n * Ensures event listeners are attached for a specific target.\n */\n #ensureListenersForTarget(target: Target): void {\n if (typeof document === 'undefined') {\n return // SSR safety\n }\n\n if (this.#targetListeners.has(target)) {\n return\n }\n\n const keydownHandler = this.#createTargetKeyDownHandler(target)\n const keyupHandler = this.#createTargetKeyUpHandler(target)\n\n target.addEventListener('keydown', keydownHandler as EventListener)\n target.addEventListener('keyup', keyupHandler as EventListener)\n\n this.#targetListeners.set(target, {\n keydown: keydownHandler,\n keyup: keyupHandler,\n })\n }\n\n /**\n * Removes event listeners for a specific target.\n */\n #removeListenersForTarget(target: Target): void {\n if (typeof document === 'undefined') {\n return\n }\n\n const listeners = this.#targetListeners.get(target)\n if (!listeners) {\n return\n }\n\n target.removeEventListener('keydown', listeners.keydown as EventListener)\n target.removeEventListener('keyup', listeners.keyup as EventListener)\n\n this.#targetListeners.delete(target)\n this.#targetRegistrations.delete(target)\n }\n\n /**\n * Creates a keydown handler for a specific target.\n */\n #createTargetKeyDownHandler(target: Target): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n this.#processTargetEvent(event, target, 'keydown')\n }\n }\n\n /**\n * Creates a keyup handler for a specific target.\n */\n #createTargetKeyUpHandler(target: Target): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n this.#processTargetEvent(event, target, 'keyup')\n }\n }\n\n /**\n * Processes keyboard events for a specific target and event type.\n */\n #processTargetEvent(\n event: KeyboardEvent,\n target: Target,\n eventType: 'keydown' | 'keyup',\n ): void {\n // Skip modifier-only events so pressing e.g. Shift before Shift+C does not reset the sequence.\n if (isModifierKey(normalizeKeyName(event.key))) {\n return\n }\n\n const targetRegs = this.#targetRegistrations.get(target)\n if (!targetRegs) {\n return\n }\n\n const now = Date.now()\n\n registrationIds: for (const id of targetRegs) {\n const registration = this.#registrations.get(id)\n if (!registration) {\n continue\n }\n\n if (!isEventForTarget(event, target)) {\n continue\n }\n\n if (!registration.options.enabled) {\n continue\n }\n\n // Check if we should ignore input elements (defaults to true)\n if (registration.options.ignoreInputs !== false) {\n if (shouldIgnoreInputEvent(event, target, registration.target)) {\n continue registrationIds\n }\n }\n\n // Only process registrations that listen for this event type\n if (registration.options.eventType !== eventType) {\n continue\n }\n\n const timeout = registration.options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT\n\n // Check if sequence has timed out\n if (\n registration.currentIndex > 0 &&\n now - registration.lastKeyTime > timeout\n ) {\n registration.currentIndex = 0\n this.#syncRegistrationView(registration)\n }\n\n const expectedHotkey =\n registration.parsedSequence[registration.currentIndex]\n if (!expectedHotkey) {\n continue\n }\n\n if (\n matchesKeyboardEvent(\n event,\n expectedHotkey,\n registration.options.platform,\n )\n ) {\n registration.lastKeyTime = now\n registration.currentIndex++\n\n if (registration.currentIndex >= registration.parsedSequence.length) {\n if (registration.options.preventDefault) {\n event.preventDefault()\n }\n if (registration.options.stopPropagation) {\n event.stopPropagation()\n }\n\n const context: HotkeyCallbackContext = {\n hotkey: registration.sequence.join(' ') as Hotkey,\n parsedHotkey:\n registration.parsedSequence[\n registration.parsedSequence.length - 1\n ]!,\n }\n\n registration.triggerCount++\n registration.hasFired = true\n registration.currentIndex = 0\n\n this.#syncRegistrationView(registration)\n\n registration.callback(event, context)\n } else {\n this.#syncRegistrationView(registration)\n }\n } else if (registration.currentIndex > 0) {\n const firstHotkey = registration.parsedSequence[0]!\n if (\n matchesKeyboardEvent(\n event,\n firstHotkey,\n registration.options.platform,\n )\n ) {\n registration.currentIndex = 1\n registration.lastKeyTime = now\n } else {\n registration.currentIndex = 0\n }\n this.#syncRegistrationView(registration)\n }\n }\n }\n\n /**\n * Finds an existing registration with the same sequence and target.\n */\n #findConflictingSequence(\n sequence: HotkeySequence,\n target: Target,\n ): SequenceRegistration | null {\n const key = sequenceKey(sequence)\n for (const registration of this.#registrations.values()) {\n if (\n sequenceKey(registration.sequence) === key &&\n registration.target === target\n ) {\n return registration\n }\n }\n return null\n }\n\n /**\n * Resets all sequence progress.\n */\n resetAll(): void {\n for (const registration of this.#registrations.values()) {\n registration.currentIndex = 0\n registration.lastKeyTime = 0\n this.#syncRegistrationView(registration)\n }\n }\n\n /**\n * Triggers a sequence's callback programmatically from devtools.\n * Creates a synthetic KeyboardEvent from the last key in the sequence.\n *\n * @param id - The registration ID to trigger\n * @returns True if the registration was found and triggered\n */\n triggerSequence(id: string): boolean {\n const registration = this.#registrations.get(id)\n if (!registration) {\n return false\n }\n\n const lastParsed =\n registration.parsedSequence[registration.parsedSequence.length - 1]\n if (!lastParsed) {\n return false\n }\n\n const syntheticEvent = new KeyboardEvent(\n registration.options.eventType ?? 'keydown',\n {\n key: lastParsed.key,\n ctrlKey: lastParsed.ctrl,\n shiftKey: lastParsed.shift,\n altKey: lastParsed.alt,\n metaKey: lastParsed.meta,\n bubbles: true,\n cancelable: true,\n },\n )\n\n registration.triggerCount++\n registration.hasFired = true\n\n this.#syncRegistrationView(registration)\n\n const context: HotkeyCallbackContext = {\n hotkey: registration.sequence.join(' ') as Hotkey,\n parsedHotkey: lastParsed,\n }\n\n registration.callback(syntheticEvent, context)\n\n return true\n }\n\n /**\n * Gets the number of registered sequences.\n */\n getRegistrationCount(): number {\n return this.#registrations.size\n }\n\n /**\n * Destroys the manager and removes all listeners.\n */\n destroy(): void {\n for (const target of this.#targetListeners.keys()) {\n this.#removeListenersForTarget(target)\n }\n this.#registrations.clear()\n this.registrations.setState(() => new Map())\n }\n}\n\n/**\n * Gets the singleton SequenceManager instance.\n * Convenience function for accessing the manager.\n */\nexport function getSequenceManager(): SequenceManager {\n return SequenceManager.getInstance()\n}\n\n/**\n * Creates a simple sequence matcher for one-off use.\n *\n * This is a low-level helper that does not support ignoreInputs, target,\n * or other HotkeyOptions. Callers must handle input filtering themselves\n * if attaching to document.\n *\n * @param sequence - The sequence of hotkeys to match\n * @param options - Options including timeout\n * @returns An object with match() and reset() methods\n *\n * @example\n * ```ts\n * const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })\n *\n * document.addEventListener('keydown', (event) => {\n * if (matcher.match(event)) {\n * console.log('Sequence matched!')\n * }\n * })\n * ```\n */\nexport function createSequenceMatcher(\n sequence: HotkeySequence,\n options: { timeout?: number; platform?: 'mac' | 'windows' | 'linux' } = {},\n): {\n match: (event: KeyboardEvent) => boolean\n reset: () => void\n getProgress: () => number\n} {\n const platform = options.platform ?? detectPlatform()\n const timeout = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT\n const parsedSequence = sequence.map((hotkey) => parseHotkey(hotkey, platform))\n\n let currentIndex = 0\n let lastKeyTime = 0\n\n return {\n match(event: KeyboardEvent): boolean {\n const now = Date.now()\n\n if (currentIndex > 0 && now - lastKeyTime > timeout) {\n currentIndex = 0\n }\n\n const expected = parsedSequence[currentIndex]\n if (!expected) {\n return false\n }\n\n if (matchesKeyboardEvent(event, expected, platform)) {\n lastKeyTime = now\n currentIndex++\n\n if (currentIndex >= parsedSequence.length) {\n currentIndex = 0\n return true\n }\n } else if (currentIndex > 0) {\n if (matchesKeyboardEvent(event, parsedSequence[0]!, platform)) {\n currentIndex = 1\n lastKeyTime = now\n } else {\n currentIndex = 0\n }\n }\n\n return false\n },\n\n reset(): void {\n currentIndex = 0\n lastKeyTime = 0\n },\n\n getProgress(): number {\n return currentIndex\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sequence-manager.cjs","names":["#instance","Store","#platform","detectPlatform","parseHotkey","#findConflictingSequence","formatHotkeySequence","#unregister","getDefaultIgnoreInputs","defaultHotkeyOptions","#registrations","#targetRegistrations","#ensureListenersForTarget","optionsEqual","#removeListenersForTarget","#targetListeners","#createTargetKeyDownHandler","#createTargetKeyUpHandler","#processTargetEvent","isRecordingEvent","#syncRegistrationView","normalizeKeyboardEvent","isModifierKey","normalizeKeyName","isEventForTarget","shouldIgnoreInputEvent","matchKeyboardEvent","areHotkeysEqual","matchesKeyboardEvent"],"sources":["../src/sequence-manager.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { isRecordingEvent } from './_recording-guard'\nimport { formatHotkeySequence } from './format'\nimport { detectPlatform } from './platform'\nimport { normalizeKeyName } from './constants'\nimport { isModifierKey, parseHotkey } from './parse'\nimport { areHotkeysEqual, matchesKeyboardEvent } from './match'\nimport { matchKeyboardEvent } from './_match'\nimport { normalizeKeyboardEvent } from './_keyboard-event'\nimport {\n defaultHotkeyOptions,\n getDefaultIgnoreInputs,\n handleConflict,\n optionsEqual,\n} from './_registration'\nimport { isEventForTarget, shouldIgnoreInputEvent } from './_event-target'\nimport type { HotkeyOptions } from './hotkey-manager'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyCallbackContext,\n ParsedHotkey,\n} from './hotkey.types'\n\nexport type Target = HTMLElement | Document | Window\n\n/**\n * Options for hotkey sequence matching.\n * Extends HotkeyOptions but excludes requireReset (not applicable to sequences).\n */\nexport interface SequenceOptions extends Omit<HotkeyOptions, 'requireReset'> {\n /** Timeout between keys in milliseconds. Default: 1000 */\n timeout?: number\n}\n\n/**\n * A sequence of hotkeys for Vim-style shortcuts.\n *\n * Each element is one step (a `Hotkey` string). Steps may include modifiers;\n * the same modifier can appear on consecutive steps (e.g. `Shift+R` then\n * `Shift+T`). Modifier-only key events do not advance or reset matching—see\n * `SequenceManager`. Automatic keydown repeats and IME composition are also ignored.\n *\n * @example\n * ```ts\n * const gotoTop: HotkeySequence = ['G', 'G'] // gg\n * const deleteLine: HotkeySequence = ['D', 'D'] // dd\n * const deleteWord: HotkeySequence = ['D', 'I', 'W'] // diw\n * const chainedShift: HotkeySequence = ['Shift+R', 'Shift+T']\n * ```\n */\nexport type HotkeySequence = Array<Hotkey>\n\n/**\n * Default timeout between keys in a sequence (in milliseconds).\n * Exported for consumers that display sequence state (e.g. devtools).\n */\nexport const DEFAULT_SEQUENCE_TIMEOUT = 1000\n\nlet sequenceIdCounter = 0\n\n/**\n * Generates a unique ID for sequence registrations.\n */\nfunction generateSequenceId(): string {\n return `sequence_${++sequenceIdCounter}`\n}\n\n/**\n * View of a sequence registration for devtools display.\n * Progress fields reflect an in-progress match (between first key and completion or timeout).\n */\nexport interface SequenceRegistrationView {\n id: string\n sequence: HotkeySequence\n options: SequenceOptions\n target: Target\n triggerCount: number\n /** Whether this sequence has been triggered at least once. */\n hasFired: boolean\n /** Steps matched in the current attempt (0 when idle or just completed). */\n matchedStepCount: number\n /** `Date.now()` when the last step in the current attempt matched; 0 if none. */\n partialMatchLastKeyTime: number\n}\n\n/**\n * Internal representation of a sequence registration.\n */\nexport interface SequenceRegistration {\n id: string\n sequence: HotkeySequence\n parsedSequence: Array<ParsedHotkey>\n callback: HotkeyCallback\n options: SequenceOptions\n target: Target\n currentIndex: number\n lastKeyTime: number\n triggerCount: number\n hasFired: boolean\n}\n\n/**\n * A handle returned from SequenceManager.register() that allows updating\n * the callback and options without re-registering the sequence.\n *\n * @example\n * ```ts\n * const handle = manager.register(['G', 'G'], callback, options)\n *\n * handle.callback = newCallback\n * handle.setOptions({ timeout: 500 })\n * handle.unregister()\n * ```\n */\nexport interface SequenceRegistrationHandle {\n readonly id: string\n readonly isActive: boolean\n callback: HotkeyCallback\n setOptions: (options: Partial<SequenceOptions>) => void\n unregister: () => void\n}\n\n/**\n * Manages keyboard sequence matching for Vim-style shortcuts.\n *\n * This class allows registering multi-key sequences like 'g g' or 'd d'\n * that trigger callbacks when the full sequence is pressed within\n * a configurable timeout.\n *\n * Modifier-only `keydown` / `keyup` events (Shift, Control, Alt, or Meta\n * alone) are not processed: they neither match a step nor reset progress.\n * That supports chained modifier chords (e.g. `Shift+R` then `Shift+T`) when\n * the user presses a modifier alone between steps.\n *\n * @example\n * ```ts\n * const matcher = SequenceManager.getInstance()\n *\n * // Register 'g g' to go to top\n * const unregister = matcher.register(['G', 'G'], (event, context) => {\n * scrollToTop()\n * }, { timeout: 500 })\n *\n * // Later, to unregister:\n * unregister()\n * ```\n */\n/**\n * Builds a devtools view from an internal registration.\n */\nfunction toRegistrationView(\n reg: SequenceRegistration,\n): SequenceRegistrationView {\n const len = reg.parsedSequence.length\n const inProgress = reg.currentIndex > 0 && reg.currentIndex < len\n return {\n id: reg.id,\n sequence: reg.sequence,\n options: reg.options,\n target: reg.target,\n triggerCount: reg.triggerCount,\n hasFired: reg.hasFired,\n matchedStepCount: inProgress ? reg.currentIndex : 0,\n partialMatchLastKeyTime: inProgress ? reg.lastKeyTime : 0,\n }\n}\n\nexport class SequenceManager {\n static #instance: SequenceManager | null = null\n\n /**\n * The TanStack Store containing sequence registration views for devtools.\n * Subscribe to this to observe registration changes.\n */\n readonly registrations: Store<Map<string, SequenceRegistrationView>> =\n new Store(new Map())\n\n #registrations: Map<string, SequenceRegistration> = new Map()\n #targetListeners: Map<\n Target,\n {\n keydown: (event: KeyboardEvent) => void\n keyup: (event: KeyboardEvent) => void\n }\n > = new Map()\n #targetRegistrations: Map<Target, Set<string>> = new Map()\n #platform: 'mac' | 'windows' | 'linux'\n\n private constructor() {\n this.#platform = detectPlatform()\n }\n\n /**\n * Gets the singleton instance of SequenceManager.\n */\n static getInstance(): SequenceManager {\n if (!SequenceManager.#instance) {\n SequenceManager.#instance = new SequenceManager()\n }\n return SequenceManager.#instance\n }\n\n /**\n * Resets the singleton instance. Useful for testing.\n */\n static resetInstance(): void {\n if (SequenceManager.#instance) {\n SequenceManager.#instance.destroy()\n SequenceManager.#instance = null\n }\n }\n\n #syncRegistrationView(registration: SequenceRegistration): void {\n this.registrations.setState((prev) =>\n new Map(prev).set(registration.id, toRegistrationView(registration)),\n )\n }\n\n /**\n * Registers a hotkey sequence handler.\n *\n * @param sequence - Array of hotkey strings that form the sequence\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior\n * @returns A handle to update or unregister the sequence\n */\n register(\n sequence: HotkeySequence,\n callback: HotkeyCallback,\n options: SequenceOptions = {},\n ): SequenceRegistrationHandle {\n if (sequence.length === 0) {\n throw new Error('Sequence must contain at least one hotkey')\n }\n\n const id = generateSequenceId()\n const platform = options.platform ?? this.#platform\n const parsedSequence = sequence.map((hotkey) =>\n parseHotkey(hotkey, platform),\n )\n\n // Resolve target: default to document if not provided or null\n const target: Target =\n options.target ??\n (typeof document !== 'undefined' ? document : ({} as Document))\n\n // Resolve conflict behavior\n const conflictBehavior = options.conflictBehavior ?? 'warn'\n\n // Check for existing registrations with the same sequence and target\n const conflictingRegistration = this.#findConflictingSequence(\n parsedSequence,\n target,\n )\n\n if (conflictingRegistration) {\n handleConflict(\n conflictingRegistration.id,\n formatHotkeySequence(sequence),\n conflictBehavior,\n (regId) => this.#unregister(regId),\n )\n }\n\n const firstStep = parsedSequence[0]!\n const resolvedIgnoreInputs =\n options.ignoreInputs ?? getDefaultIgnoreInputs(firstStep)\n\n const baseOptions = {\n ...defaultHotkeyOptions,\n timeout: DEFAULT_SEQUENCE_TIMEOUT,\n ...options,\n platform,\n ignoreInputs: resolvedIgnoreInputs,\n }\n\n const registration: SequenceRegistration = {\n id,\n sequence,\n parsedSequence,\n callback,\n options: baseOptions,\n target,\n currentIndex: 0,\n lastKeyTime: 0,\n triggerCount: 0,\n hasFired: false,\n }\n\n this.#registrations.set(id, registration)\n this.registrations.setState((prev) =>\n new Map(prev).set(id, toRegistrationView(registration)),\n )\n\n // Track registration for this target\n if (!this.#targetRegistrations.has(target)) {\n this.#targetRegistrations.set(target, new Set())\n }\n this.#targetRegistrations.get(target)!.add(id)\n\n // Ensure listeners are attached for this target\n this.#ensureListenersForTarget(target)\n\n const manager = this\n const handle: SequenceRegistrationHandle = {\n get id() {\n return id\n },\n get isActive() {\n return manager.#registrations.has(id)\n },\n get callback() {\n const reg = manager.#registrations.get(id)\n return reg?.callback ?? callback\n },\n set callback(newCallback: HotkeyCallback) {\n const reg = manager.#registrations.get(id)\n if (reg) {\n reg.callback = newCallback\n }\n },\n setOptions: (newOptions: Partial<SequenceOptions>) => {\n const reg = manager.#registrations.get(id)\n if (\n reg &&\n !optionsEqual(reg.options, { ...reg.options, ...newOptions })\n ) {\n reg.options = { ...reg.options, ...newOptions }\n manager.registrations.setState((prev) =>\n new Map(prev).set(id, toRegistrationView(reg)),\n )\n }\n },\n unregister: () => {\n manager.#unregister(id)\n },\n }\n\n return handle\n }\n\n /**\n * Unregisters a sequence by its registration ID.\n */\n #unregister(id: string): void {\n const registration = this.#registrations.get(id)\n if (!registration) {\n return\n }\n\n const target = registration.target\n\n this.#registrations.delete(id)\n this.registrations.setState((prev) => {\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n\n // Remove from target registrations tracking\n const targetRegs = this.#targetRegistrations.get(target)\n if (targetRegs) {\n targetRegs.delete(id)\n if (targetRegs.size === 0) {\n this.#removeListenersForTarget(target)\n }\n }\n }\n\n /**\n * Ensures event listeners are attached for a specific target.\n */\n #ensureListenersForTarget(target: Target): void {\n if (typeof document === 'undefined') {\n return // SSR safety\n }\n\n if (this.#targetListeners.has(target)) {\n return\n }\n\n const keydownHandler = this.#createTargetKeyDownHandler(target)\n const keyupHandler = this.#createTargetKeyUpHandler(target)\n\n target.addEventListener('keydown', keydownHandler as EventListener)\n target.addEventListener('keyup', keyupHandler as EventListener)\n\n this.#targetListeners.set(target, {\n keydown: keydownHandler,\n keyup: keyupHandler,\n })\n }\n\n /**\n * Removes event listeners for a specific target.\n */\n #removeListenersForTarget(target: Target): void {\n if (typeof document === 'undefined') {\n return\n }\n\n const listeners = this.#targetListeners.get(target)\n if (!listeners) {\n return\n }\n\n target.removeEventListener('keydown', listeners.keydown as EventListener)\n target.removeEventListener('keyup', listeners.keyup as EventListener)\n\n this.#targetListeners.delete(target)\n this.#targetRegistrations.delete(target)\n }\n\n /**\n * Creates a keydown handler for a specific target.\n */\n #createTargetKeyDownHandler(target: Target): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n this.#processTargetEvent(event, target, 'keydown')\n }\n }\n\n /**\n * Creates a keyup handler for a specific target.\n */\n #createTargetKeyUpHandler(target: Target): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n this.#processTargetEvent(event, target, 'keyup')\n }\n }\n\n /**\n * Processes keyboard events for a specific target and event type.\n */\n #processTargetEvent(\n event: KeyboardEvent,\n target: Target,\n eventType: 'keydown' | 'keyup',\n ): void {\n if (isRecordingEvent(event)) {\n for (const id of this.#targetRegistrations.get(target) ?? []) {\n const registration = this.#registrations.get(id)\n if (registration && registration.currentIndex > 0) {\n registration.currentIndex = 0\n this.#syncRegistrationView(registration)\n }\n }\n return\n }\n if (\n (event.type === 'keydown' && event.repeat) ||\n normalizeKeyboardEvent(event).isComposing\n ) {\n return\n }\n // Skip modifier-only events so pressing e.g. Shift before Shift+C does not reset the sequence.\n if (isModifierKey(normalizeKeyName(event.key))) {\n return\n }\n\n const targetRegs = this.#targetRegistrations.get(target)\n if (!targetRegs) {\n return\n }\n\n const now = Date.now()\n // Score eligible next steps (or restarts) before advancing any sequence.\n const candidates = new Map<string, { score: number; nextIndex: number }>()\n let bestScore = 0\n\n registrationIds: for (const id of targetRegs) {\n const registration = this.#registrations.get(id)\n if (!registration) {\n continue\n }\n\n if (!isEventForTarget(event, target)) {\n continue\n }\n\n if (!registration.options.enabled) {\n continue\n }\n\n // Check if we should ignore input elements (defaults to true)\n if (registration.options.ignoreInputs !== false) {\n if (shouldIgnoreInputEvent(event, target, registration.target)) {\n continue registrationIds\n }\n }\n\n // Only process registrations that listen for this event type\n if (registration.options.eventType !== eventType) {\n continue\n }\n\n const timeout = registration.options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT\n\n // Check if sequence has timed out\n if (\n registration.currentIndex > 0 &&\n now - registration.lastKeyTime > timeout\n ) {\n registration.currentIndex = 0\n this.#syncRegistrationView(registration)\n }\n\n const expectedHotkey =\n registration.parsedSequence[registration.currentIndex]\n if (!expectedHotkey) {\n continue\n }\n\n let match = matchKeyboardEvent(\n event,\n expectedHotkey,\n registration.options.platform,\n )\n let nextIndex = registration.currentIndex + 1\n if (!match.matched && registration.currentIndex > 0) {\n match = matchKeyboardEvent(\n event,\n registration.parsedSequence[0]!,\n registration.options.platform,\n )\n nextIndex = 1\n }\n candidates.set(id, { score: match.score, nextIndex })\n bestScore = Math.max(bestScore, match.score)\n }\n\n for (const [id, candidate] of candidates) {\n const registration = this.#registrations.get(id)\n if (!registration || !registration.options.enabled) continue\n if (candidate.score > 0 && candidate.score === bestScore) {\n registration.lastKeyTime = now\n registration.currentIndex = candidate.nextIndex\n\n if (registration.currentIndex >= registration.parsedSequence.length) {\n if (registration.options.preventDefault) {\n event.preventDefault()\n }\n if (registration.options.stopPropagation) {\n event.stopPropagation()\n }\n\n const context: HotkeyCallbackContext = {\n hotkey: registration.sequence.join(' ') as Hotkey,\n parsedHotkey:\n registration.parsedSequence[\n registration.parsedSequence.length - 1\n ]!,\n }\n\n registration.triggerCount++\n registration.hasFired = true\n registration.currentIndex = 0\n\n this.#syncRegistrationView(registration)\n\n registration.callback(event, context)\n } else {\n this.#syncRegistrationView(registration)\n }\n } else if (registration.currentIndex > 0) {\n // A mismatched or weaker candidate cannot carry stale progress forward.\n registration.currentIndex = 0\n this.#syncRegistrationView(registration)\n }\n }\n }\n\n /**\n * Compares resolved step identities without changing the supplied sequence strings.\n */\n #findConflictingSequence(\n parsedSequence: Array<ParsedHotkey>,\n target: Target,\n ): SequenceRegistration | null {\n for (const registration of this.#registrations.values()) {\n if (\n registration.target === target &&\n registration.parsedSequence.length === parsedSequence.length &&\n parsedSequence.every((step, index) =>\n areHotkeysEqual(step, registration.parsedSequence[index]!),\n )\n ) {\n return registration\n }\n }\n return null\n }\n\n /**\n * Resets all sequence progress.\n */\n resetAll(): void {\n for (const registration of this.#registrations.values()) {\n registration.currentIndex = 0\n registration.lastKeyTime = 0\n this.#syncRegistrationView(registration)\n }\n }\n\n /**\n * Triggers a sequence's callback programmatically from devtools.\n * Creates a synthetic KeyboardEvent from the last key in the sequence.\n *\n * @param id - The registration ID to trigger\n * @returns True if the registration was found and triggered\n */\n triggerSequence(id: string): boolean {\n const registration = this.#registrations.get(id)\n if (!registration) {\n return false\n }\n\n const lastParsed =\n registration.parsedSequence[registration.parsedSequence.length - 1]\n if (!lastParsed) {\n return false\n }\n\n const syntheticEvent = new KeyboardEvent(\n registration.options.eventType ?? 'keydown',\n {\n ...(lastParsed.code !== undefined\n ? { code: lastParsed.code }\n : { key: lastParsed.key }),\n ctrlKey: lastParsed.ctrl,\n shiftKey: lastParsed.shift,\n altKey: lastParsed.alt,\n metaKey: lastParsed.meta,\n bubbles: true,\n cancelable: true,\n },\n )\n\n registration.triggerCount++\n registration.hasFired = true\n\n this.#syncRegistrationView(registration)\n\n const context: HotkeyCallbackContext = {\n hotkey: registration.sequence.join(' ') as Hotkey,\n parsedHotkey: lastParsed,\n }\n\n registration.callback(syntheticEvent, context)\n\n return true\n }\n\n /**\n * Gets the number of registered sequences.\n */\n getRegistrationCount(): number {\n return this.#registrations.size\n }\n\n /**\n * Destroys the manager and removes all listeners.\n */\n destroy(): void {\n for (const target of this.#targetListeners.keys()) {\n this.#removeListenersForTarget(target)\n }\n this.#registrations.clear()\n this.registrations.setState(() => new Map())\n }\n}\n\n/**\n * Gets the singleton SequenceManager instance.\n * Convenience function for accessing the manager.\n */\nexport function getSequenceManager(): SequenceManager {\n return SequenceManager.getInstance()\n}\n\n/**\n * Creates a simple sequence matcher for one-off use.\n *\n * This is a low-level helper that does not support ignoreInputs, target,\n * or other HotkeyOptions. Callers must handle input filtering themselves\n * if attaching to document.\n *\n * @param sequence - The sequence of hotkeys to match\n * @param options - Options including timeout\n * @returns An object with match() and reset() methods\n *\n * @example\n * ```ts\n * const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })\n *\n * document.addEventListener('keydown', (event) => {\n * if (matcher.match(event)) {\n * console.log('Sequence matched!')\n * }\n * })\n * ```\n */\nexport function createSequenceMatcher(\n sequence: HotkeySequence,\n options: { timeout?: number; platform?: 'mac' | 'windows' | 'linux' } = {},\n): {\n match: (event: KeyboardEvent) => boolean\n reset: () => void\n getProgress: () => number\n} {\n const platform = options.platform ?? detectPlatform()\n const timeout = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT\n const parsedSequence = sequence.map((hotkey) => parseHotkey(hotkey, platform))\n\n let currentIndex = 0\n let lastKeyTime = 0\n\n return {\n match(event: KeyboardEvent): boolean {\n // These events are not sequence steps and must not refresh the timeout.\n if (\n (event.type === 'keydown' && event.repeat) ||\n normalizeKeyboardEvent(event, platform).isComposing ||\n isModifierKey(normalizeKeyName(event.key))\n ) {\n return false\n }\n const now = Date.now()\n\n if (currentIndex > 0 && now - lastKeyTime > timeout) {\n currentIndex = 0\n }\n\n const expected = parsedSequence[currentIndex]\n if (!expected) {\n return false\n }\n\n if (matchesKeyboardEvent(event, expected, platform)) {\n lastKeyTime = now\n currentIndex++\n\n if (currentIndex >= parsedSequence.length) {\n currentIndex = 0\n return true\n }\n } else if (currentIndex > 0) {\n if (matchesKeyboardEvent(event, parsedSequence[0]!, platform)) {\n currentIndex = 1\n lastKeyTime = now\n } else {\n currentIndex = 0\n }\n }\n\n return false\n },\n\n reset(): void {\n currentIndex = 0\n lastKeyTime = 0\n },\n\n getProgress(): number {\n return currentIndex\n },\n }\n}\n"],"mappings":"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"}
|