@tanstack/hotkeys 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +108 -19
- package/dist/adapter.js +90 -65
- package/dist/app-hotkeys.d.ts +239 -0
- package/dist/app-hotkeys.js +295 -0
- package/dist/conflicts.d.ts +5 -9
- package/dist/conflicts.js +6 -11
- package/dist/format.d.ts +19 -24
- package/dist/format.js +19 -30
- package/dist/hint.d.ts +1 -0
- package/dist/hint.js +5 -2
- package/dist/hotkey-manager.d.ts +110 -59
- package/dist/hotkey-manager.js +254 -151
- package/dist/hotkey-recorder.d.ts +66 -42
- package/dist/hotkey-recorder.js +148 -107
- package/dist/hotkey.types.d.ts +26 -12
- package/dist/index.d.ts +10 -11
- package/dist/index.js +5 -6
- package/dist/match.d.ts +38 -5
- package/dist/match.js +72 -4
- package/dist/parse.d.ts +20 -8
- package/dist/parse.js +15 -5
- package/dist/recorder-options.d.ts +6 -3
- package/dist/validate.d.ts +10 -4
- package/dist/validate.js +26 -4
- package/package.json +1 -1
- package/dist/hotkey-sequence-recorder.d.ts +0 -84
- package/dist/hotkey-sequence-recorder.js +0 -259
- package/dist/sequence-manager.d.ts +0 -169
- package/dist/sequence-manager.js +0 -443
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Hotkey } from "./hotkey.types.js";
|
|
1
|
+
import { Hotkey, HotkeyChord, HotkeySequence } from "./hotkey.types.js";
|
|
2
2
|
import { HotkeyRecorderValidationContext, RecorderOptions } from "./recorder-options.js";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/hotkey-recorder.d.ts
|
|
@@ -8,78 +8,97 @@ import { Store } from "@tanstack/store";
|
|
|
8
8
|
export interface HotkeyRecorderState {
|
|
9
9
|
/** Whether recording is currently active */
|
|
10
10
|
isRecording: boolean;
|
|
11
|
-
/**
|
|
11
|
+
/** Chords recorded so far in a multi-step recording (empty for `maxSteps: 1`) */
|
|
12
|
+
steps: HotkeySequence;
|
|
13
|
+
/** The last committed hotkey: a chord, or a sequence of two or more steps */
|
|
12
14
|
recordedHotkey: Hotkey | null;
|
|
13
15
|
}
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export interface HotkeyRecorderOptions extends RecorderOptions {
|
|
18
|
-
/** Return true to accept, or false/a message to reject while staying in recording mode. */
|
|
19
|
-
validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string;
|
|
20
|
-
/** Callback when a hotkey is successfully recorded */
|
|
21
|
-
onRecord: (hotkey: Hotkey) => void;
|
|
16
|
+
/** Which keys commit a multi-step recording before it reaches `maxSteps`. */
|
|
17
|
+
export type HotkeyRecorderCommitKeys = 'enter' | 'none';
|
|
18
|
+
interface HotkeyRecorderBaseOptions extends RecorderOptions {
|
|
22
19
|
/** Optional callback when recording is cancelled (Escape pressed) */
|
|
23
20
|
onCancel?: () => void;
|
|
24
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Optional callback when the recording is cleared: Backspace/Delete with no
|
|
23
|
+
* modifiers and no recorded steps
|
|
24
|
+
*/
|
|
25
25
|
onClear?: () => void;
|
|
26
26
|
/**
|
|
27
27
|
* Whether to ignore keyboard events from input-like elements (text inputs,
|
|
28
|
-
* textarea, select, contenteditable). When true, typing in
|
|
29
|
-
* through normally
|
|
30
|
-
* Escape always works regardless of this setting.
|
|
31
|
-
* @default true
|
|
28
|
+
* textarea, select, contenteditable) while recording. When true, typing in
|
|
29
|
+
* inputs passes through normally (Escape still cancels). Defaults to true.
|
|
32
30
|
*/
|
|
33
31
|
ignoreInputs?: boolean;
|
|
34
32
|
}
|
|
33
|
+
/** Options for recording a single chord. This is the default (`maxSteps: 1`). */
|
|
34
|
+
export interface HotkeyChordRecorderOptions extends HotkeyRecorderBaseOptions {
|
|
35
|
+
/** Maximum number of chords to record. Default: 1 */
|
|
36
|
+
maxSteps?: 1;
|
|
37
|
+
/** Return true to accept, or false/a message to reject while staying in recording mode. */
|
|
38
|
+
validate?: (hotkey: HotkeyChord, context: HotkeyRecorderValidationContext) => boolean | string;
|
|
39
|
+
/** Callback when a chord is successfully recorded */
|
|
40
|
+
onRecord: (hotkey: HotkeyChord) => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Options for recording up to `maxSteps` chords. A one-step recording commits
|
|
44
|
+
* as a chord string; two or more steps commit as a sequence.
|
|
45
|
+
*/
|
|
46
|
+
export interface HotkeySequenceRecorderOptions extends HotkeyRecorderBaseOptions {
|
|
47
|
+
/** Maximum number of chords to record. Reaching it commits automatically. Use `Infinity` for no limit. */
|
|
48
|
+
maxSteps: number;
|
|
49
|
+
/** Return true to accept, or false/a message to reject while staying in recording mode. */
|
|
50
|
+
validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string;
|
|
51
|
+
/** Callback when a hotkey is successfully recorded */
|
|
52
|
+
onRecord: (hotkey: Hotkey) => void;
|
|
53
|
+
/** Whether plain Enter commits the recorded steps. Default: 'enter' */
|
|
54
|
+
commitKeys?: HotkeyRecorderCommitKeys;
|
|
55
|
+
/** Commit automatically after this many milliseconds without a new step. Disabled when unset or 0. */
|
|
56
|
+
idleTimeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Options for HotkeyRecorder. `maxSteps` (default 1) selects between recording
|
|
60
|
+
* a single chord and recording a sequence.
|
|
61
|
+
*/
|
|
62
|
+
export type HotkeyRecorderOptions = HotkeyChordRecorderOptions | HotkeySequenceRecorderOptions;
|
|
35
63
|
/**
|
|
36
64
|
* Framework-agnostic class for recording keyboard shortcuts.
|
|
37
65
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
66
|
+
* With the default `maxSteps: 1`, the first valid chord commits. With a larger
|
|
67
|
+
* `maxSteps`, each chord adds a step; the recording commits on Enter (unless
|
|
68
|
+
* `commitKeys: 'none'`), after `idleTimeoutMs`, or when it reaches `maxSteps`.
|
|
69
|
+
* Backspace/Delete removes the last step, and on an empty recording calls
|
|
70
|
+
* `onClear`. Escape cancels. A one-step result is a chord string.
|
|
41
71
|
*
|
|
42
72
|
* State Management:
|
|
43
73
|
* - Uses TanStack Store for reactive state management
|
|
44
74
|
* - State can be accessed via `recorder.store.state` when using the class directly
|
|
45
|
-
* - When using framework adapters
|
|
75
|
+
* - When using framework adapters, use the `useHotkeyRecorder` hook for reactive state
|
|
46
76
|
*
|
|
47
77
|
* @example
|
|
48
78
|
* ```ts
|
|
49
79
|
* const recorder = new HotkeyRecorder({
|
|
50
80
|
* onRecord: (hotkey) => {
|
|
51
|
-
* console.log('Recorded:', hotkey)
|
|
52
|
-
* },
|
|
53
|
-
* onCancel: () => {
|
|
54
|
-
* console.log('Recording cancelled')
|
|
81
|
+
* console.log('Recorded:', hotkey) // 'Mod+S'
|
|
55
82
|
* },
|
|
56
83
|
* })
|
|
57
84
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* //
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* // Subscribe to changes with TanStack Store
|
|
65
|
-
* const unsubscribe = recorder.store.subscribe(() => {
|
|
66
|
-
* console.log('Recording:', recorder.store.state.isRecording)
|
|
85
|
+
* const sequenceRecorder = new HotkeyRecorder({
|
|
86
|
+
* maxSteps: 3,
|
|
87
|
+
* onRecord: (hotkey) => {
|
|
88
|
+
* console.log('Recorded:', hotkey) // 'Mod+S' or ['G', 'H']
|
|
89
|
+
* },
|
|
67
90
|
* })
|
|
68
91
|
*
|
|
69
|
-
*
|
|
92
|
+
* recorder.start()
|
|
93
|
+
* // ... later
|
|
70
94
|
* recorder.destroy()
|
|
71
|
-
* unsubscribe()
|
|
72
95
|
* ```
|
|
73
96
|
*/
|
|
74
97
|
export declare class HotkeyRecorder {
|
|
75
98
|
#private;
|
|
76
|
-
/**
|
|
77
|
-
* The TanStack Store instance containing the recorder state.
|
|
78
|
-
* Use this to subscribe to state changes or access current state.
|
|
79
|
-
*/
|
|
80
99
|
readonly store: Store<HotkeyRecorderState>;
|
|
81
|
-
|
|
82
|
-
constructor(options:
|
|
100
|
+
constructor(options: HotkeyChordRecorderOptions | (() => HotkeyChordRecorderOptions));
|
|
101
|
+
constructor(options: HotkeySequenceRecorderOptions | (() => HotkeySequenceRecorderOptions));
|
|
83
102
|
/**
|
|
84
103
|
* Updates the recorder options, including callbacks.
|
|
85
104
|
* This allows framework adapters to sync callback changes without recreating the recorder.
|
|
@@ -89,10 +108,15 @@ export declare class HotkeyRecorder {
|
|
|
89
108
|
* Start recording a new hotkey.
|
|
90
109
|
*
|
|
91
110
|
* Sets up a keydown event listener that captures keyboard events
|
|
92
|
-
* and
|
|
93
|
-
* a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
|
|
111
|
+
* and records chords according to `maxSteps`.
|
|
94
112
|
*/
|
|
95
113
|
start(): void;
|
|
114
|
+
/**
|
|
115
|
+
* Commits the recorded steps after validation and conflict checks.
|
|
116
|
+
* Called automatically at `maxSteps`, on Enter, or after `idleTimeoutMs`.
|
|
117
|
+
* A rejected commit leaves recording active with its steps intact.
|
|
118
|
+
*/
|
|
119
|
+
commit(): void;
|
|
96
120
|
/**
|
|
97
121
|
* Stop recording (same as cancel, but doesn't call onCancel).
|
|
98
122
|
*
|
package/dist/hotkey-recorder.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
2
|
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
3
|
-
import { isModifierKey, parseHotkey } from "./parse.js";
|
|
3
|
+
import { isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
|
|
4
4
|
import { validateHotkey } from "./validate.js";
|
|
5
5
|
import { beginRecording, captureRecordingEvent, endRecording } from "./_recording-guard.js";
|
|
6
6
|
import { shouldIgnoreInputEvent } from "./_event-target.js";
|
|
@@ -9,65 +9,62 @@ import { chordRejection, hotkeyChordFromKeydown } from "./_recorder-chord.js";
|
|
|
9
9
|
import { Store } from "@tanstack/store";
|
|
10
10
|
|
|
11
11
|
//#region src/hotkey-recorder.ts
|
|
12
|
+
const initialState = {
|
|
13
|
+
isRecording: false,
|
|
14
|
+
steps: [],
|
|
15
|
+
recordedHotkey: null
|
|
16
|
+
};
|
|
12
17
|
/**
|
|
13
18
|
* Framework-agnostic class for recording keyboard shortcuts.
|
|
14
19
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
20
|
+
* With the default `maxSteps: 1`, the first valid chord commits. With a larger
|
|
21
|
+
* `maxSteps`, each chord adds a step; the recording commits on Enter (unless
|
|
22
|
+
* `commitKeys: 'none'`), after `idleTimeoutMs`, or when it reaches `maxSteps`.
|
|
23
|
+
* Backspace/Delete removes the last step, and on an empty recording calls
|
|
24
|
+
* `onClear`. Escape cancels. A one-step result is a chord string.
|
|
18
25
|
*
|
|
19
26
|
* State Management:
|
|
20
27
|
* - Uses TanStack Store for reactive state management
|
|
21
28
|
* - State can be accessed via `recorder.store.state` when using the class directly
|
|
22
|
-
* - When using framework adapters
|
|
29
|
+
* - When using framework adapters, use the `useHotkeyRecorder` hook for reactive state
|
|
23
30
|
*
|
|
24
31
|
* @example
|
|
25
32
|
* ```ts
|
|
26
33
|
* const recorder = new HotkeyRecorder({
|
|
27
34
|
* onRecord: (hotkey) => {
|
|
28
|
-
* console.log('Recorded:', hotkey)
|
|
29
|
-
* },
|
|
30
|
-
* onCancel: () => {
|
|
31
|
-
* console.log('Recording cancelled')
|
|
35
|
+
* console.log('Recorded:', hotkey) // 'Mod+S'
|
|
32
36
|
* },
|
|
33
37
|
* })
|
|
34
38
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* //
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* // Subscribe to changes with TanStack Store
|
|
42
|
-
* const unsubscribe = recorder.store.subscribe(() => {
|
|
43
|
-
* console.log('Recording:', recorder.store.state.isRecording)
|
|
39
|
+
* const sequenceRecorder = new HotkeyRecorder({
|
|
40
|
+
* maxSteps: 3,
|
|
41
|
+
* onRecord: (hotkey) => {
|
|
42
|
+
* console.log('Recorded:', hotkey) // 'Mod+S' or ['G', 'H']
|
|
43
|
+
* },
|
|
44
44
|
* })
|
|
45
45
|
*
|
|
46
|
-
*
|
|
46
|
+
* recorder.start()
|
|
47
|
+
* // ... later
|
|
47
48
|
* recorder.destroy()
|
|
48
|
-
* unsubscribe()
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
51
|
var HotkeyRecorder = class {
|
|
52
|
-
|
|
53
|
-
* The TanStack Store instance containing the recorder state.
|
|
54
|
-
* Use this to subscribe to state changes or access current state.
|
|
55
|
-
*/
|
|
56
|
-
store = new Store({
|
|
57
|
-
isRecording: false,
|
|
58
|
-
recordedHotkey: null
|
|
59
|
-
});
|
|
52
|
+
store = new Store(initialState);
|
|
60
53
|
#keydownHandler = null;
|
|
61
54
|
#optionsSource;
|
|
62
55
|
#optionOverrides = {};
|
|
56
|
+
#platform;
|
|
57
|
+
#events = [];
|
|
58
|
+
#idleTimer = null;
|
|
63
59
|
get #options() {
|
|
64
60
|
return {
|
|
65
61
|
...typeof this.#optionsSource === "function" ? this.#optionsSource() : this.#optionsSource,
|
|
66
62
|
...this.#optionOverrides
|
|
67
63
|
};
|
|
68
64
|
}
|
|
69
|
-
#
|
|
70
|
-
|
|
65
|
+
get #maxSteps() {
|
|
66
|
+
return Math.max(1, this.#options.maxSteps ?? 1);
|
|
67
|
+
}
|
|
71
68
|
constructor(options) {
|
|
72
69
|
this.#optionsSource = options;
|
|
73
70
|
this.#platform = detectPlatform();
|
|
@@ -82,19 +79,35 @@ var HotkeyRecorder = class {
|
|
|
82
79
|
...options
|
|
83
80
|
};
|
|
84
81
|
}
|
|
82
|
+
#clearIdleTimer() {
|
|
83
|
+
if (this.#idleTimer !== null) {
|
|
84
|
+
clearTimeout(this.#idleTimer);
|
|
85
|
+
this.#idleTimer = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
#scheduleIdleTimer() {
|
|
89
|
+
this.#clearIdleTimer();
|
|
90
|
+
const ms = this.#maxSteps > 1 ? this.#options.idleTimeoutMs : void 0;
|
|
91
|
+
if (ms === void 0 || ms <= 0 || this.store.state.steps.length === 0) return;
|
|
92
|
+
this.#idleTimer = setTimeout(() => {
|
|
93
|
+
this.#idleTimer = null;
|
|
94
|
+
if (this.#keydownHandler && this.store.state.steps.length >= 1) this.commit();
|
|
95
|
+
}, ms);
|
|
96
|
+
}
|
|
85
97
|
/**
|
|
86
98
|
* Start recording a new hotkey.
|
|
87
99
|
*
|
|
88
100
|
* Sets up a keydown event listener that captures keyboard events
|
|
89
|
-
* and
|
|
90
|
-
* a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
|
|
101
|
+
* and records chords according to `maxSteps`.
|
|
91
102
|
*/
|
|
92
103
|
start() {
|
|
93
104
|
if (this.#keydownHandler) return;
|
|
105
|
+
this.#clearIdleTimer();
|
|
106
|
+
this.#events = [];
|
|
94
107
|
beginRecording(this);
|
|
95
108
|
this.store.setState(() => ({
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
...initialState,
|
|
110
|
+
isRecording: true
|
|
98
111
|
}));
|
|
99
112
|
const handler = (event) => {
|
|
100
113
|
if (!this.#keydownHandler) return;
|
|
@@ -109,120 +122,152 @@ var HotkeyRecorder = class {
|
|
|
109
122
|
this.cancel();
|
|
110
123
|
return;
|
|
111
124
|
}
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
const unmodified = !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
|
|
126
|
+
const multiStep = this.#maxSteps > 1;
|
|
127
|
+
if ((event.key === "Backspace" || event.key === "Delete") && unmodified) {
|
|
128
|
+
if (this.store.state.steps.length === 0) {
|
|
114
129
|
this.stop();
|
|
115
130
|
this.#options.onClear?.();
|
|
116
131
|
return;
|
|
117
132
|
}
|
|
133
|
+
this.#events.pop();
|
|
134
|
+
this.store.setState((s) => ({
|
|
135
|
+
...s,
|
|
136
|
+
steps: s.steps.slice(0, -1)
|
|
137
|
+
}));
|
|
138
|
+
if (this.store.state.steps.length === 0) this.#clearIdleTimer();
|
|
139
|
+
else this.#scheduleIdleTimer();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const options = this.#options;
|
|
143
|
+
if (multiStep && options.commitKeys !== "none" && event.key === "Enter" && unmodified) {
|
|
144
|
+
if (this.store.state.steps.length >= 1) this.commit();
|
|
145
|
+
return;
|
|
118
146
|
}
|
|
119
147
|
if (isModifierKey(event.key) || event.key === "AltGraph") return;
|
|
120
148
|
const rejection = chordRejection(event, {
|
|
121
|
-
...
|
|
149
|
+
...options,
|
|
122
150
|
platform
|
|
123
151
|
});
|
|
124
152
|
if (rejection) {
|
|
125
|
-
|
|
153
|
+
options.onReject?.(rejection);
|
|
126
154
|
return;
|
|
127
155
|
}
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
const validation = validateHotkey(
|
|
156
|
+
const chord = hotkeyChordFromKeydown(event, platform, options.recordBy);
|
|
157
|
+
if (chord === null) return;
|
|
158
|
+
const validation = validateHotkey(chord);
|
|
131
159
|
if (!validation.valid) {
|
|
132
|
-
|
|
160
|
+
options.onReject?.({
|
|
133
161
|
reason: "invalid",
|
|
134
162
|
message: validation.errors.join("; "),
|
|
135
|
-
hotkey:
|
|
136
|
-
});
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const accepted = this.#options.validate?.(finalHotkey, {
|
|
140
|
-
event,
|
|
141
|
-
parsedHotkey: parseHotkey(finalHotkey, platform)
|
|
142
|
-
});
|
|
143
|
-
if (accepted !== void 0 && accepted !== true) {
|
|
144
|
-
this.#options.onReject?.({
|
|
145
|
-
reason: "validation",
|
|
146
|
-
message: typeof accepted === "string" ? accepted : "This shortcut is not allowed.",
|
|
147
|
-
hotkey: finalHotkey
|
|
163
|
+
hotkey: chord
|
|
148
164
|
});
|
|
149
165
|
return;
|
|
150
166
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
events: [event]
|
|
156
|
-
});
|
|
157
|
-
if (conflicts.length) {
|
|
158
|
-
this.#options.onReject?.({
|
|
159
|
-
reason: "conflict",
|
|
160
|
-
message: "This shortcut conflicts with a registered binding.",
|
|
161
|
-
hotkey: finalHotkey,
|
|
162
|
-
conflicts
|
|
163
|
-
});
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
endRecording(this);
|
|
168
|
-
const handlerToRemove = this.#keydownHandler;
|
|
169
|
-
if (handlerToRemove) {
|
|
170
|
-
this.#removeListener(handlerToRemove);
|
|
171
|
-
this.#keydownHandler = null;
|
|
172
|
-
}
|
|
173
|
-
this.store.setState(() => ({
|
|
174
|
-
isRecording: false,
|
|
175
|
-
recordedHotkey: finalHotkey
|
|
167
|
+
this.#events.push(event);
|
|
168
|
+
this.store.setState((s) => ({
|
|
169
|
+
...s,
|
|
170
|
+
steps: [...s.steps, chord]
|
|
176
171
|
}));
|
|
177
|
-
this.#
|
|
172
|
+
if (this.store.state.steps.length >= this.#maxSteps) this.commit();
|
|
173
|
+
else this.#scheduleIdleTimer();
|
|
178
174
|
};
|
|
179
175
|
this.#keydownHandler = handler;
|
|
180
176
|
this.#addListener(handler);
|
|
181
177
|
}
|
|
182
178
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
179
|
+
* Commits the recorded steps after validation and conflict checks.
|
|
180
|
+
* Called automatically at `maxSteps`, on Enter, or after `idleTimeoutMs`.
|
|
181
|
+
* A rejected commit leaves recording active with its steps intact.
|
|
186
182
|
*/
|
|
187
|
-
|
|
183
|
+
commit() {
|
|
184
|
+
const steps = this.store.state.steps;
|
|
185
|
+
if (steps.length < 1) return;
|
|
186
|
+
const options = this.#options;
|
|
187
|
+
const platform = options.platform ?? this.#platform;
|
|
188
|
+
const hotkey = simplifyHotkey(steps);
|
|
189
|
+
const single = this.#maxSteps === 1;
|
|
190
|
+
this.#clearIdleTimer();
|
|
191
|
+
const rejectAndKeepRecording = (rejection) => {
|
|
192
|
+
if (single) {
|
|
193
|
+
this.#events = [];
|
|
194
|
+
this.store.setState((s) => ({
|
|
195
|
+
...s,
|
|
196
|
+
steps: []
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
options.onReject?.(rejection);
|
|
200
|
+
};
|
|
201
|
+
const parsedSequence = steps.map((step) => parseHotkey(step, platform));
|
|
202
|
+
const accepted = options.validate?.(hotkey, {
|
|
203
|
+
event: this.#events[this.#events.length - 1],
|
|
204
|
+
parsedHotkey: parsedSequence[parsedSequence.length - 1],
|
|
205
|
+
events: [...this.#events],
|
|
206
|
+
parsedSequence
|
|
207
|
+
});
|
|
208
|
+
if (accepted !== void 0 && accepted !== true) {
|
|
209
|
+
rejectAndKeepRecording({
|
|
210
|
+
reason: "validation",
|
|
211
|
+
message: typeof accepted === "string" ? accepted : `This ${single ? "shortcut" : "sequence"} is not allowed.`,
|
|
212
|
+
hotkey
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (options.detectConflicts) {
|
|
217
|
+
const conflicts = findHotkeyConflicts(hotkey, {
|
|
218
|
+
...typeof options.detectConflicts === "object" ? options.detectConflicts : {},
|
|
219
|
+
platform,
|
|
220
|
+
events: this.#events
|
|
221
|
+
});
|
|
222
|
+
if (conflicts.length) {
|
|
223
|
+
rejectAndKeepRecording({
|
|
224
|
+
reason: "conflict",
|
|
225
|
+
message: `This ${single ? "shortcut" : "sequence"} conflicts with a registered hotkey.`,
|
|
226
|
+
hotkey,
|
|
227
|
+
conflicts
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.#finish(hotkey);
|
|
233
|
+
options.onRecord(hotkey);
|
|
234
|
+
}
|
|
235
|
+
/** Stops listening and stores the final state. */
|
|
236
|
+
#finish(recordedHotkey) {
|
|
188
237
|
endRecording(this);
|
|
238
|
+
this.#events = [];
|
|
239
|
+
this.#clearIdleTimer();
|
|
189
240
|
if (this.#keydownHandler) {
|
|
190
241
|
this.#removeListener(this.#keydownHandler);
|
|
191
242
|
this.#keydownHandler = null;
|
|
192
243
|
}
|
|
193
244
|
this.store.setState(() => ({
|
|
194
|
-
|
|
195
|
-
recordedHotkey
|
|
245
|
+
...initialState,
|
|
246
|
+
recordedHotkey
|
|
196
247
|
}));
|
|
197
248
|
}
|
|
198
249
|
/**
|
|
250
|
+
* Stop recording (same as cancel, but doesn't call onCancel).
|
|
251
|
+
*
|
|
252
|
+
* Removes the event listener and resets the recording state.
|
|
253
|
+
*/
|
|
254
|
+
stop() {
|
|
255
|
+
this.#finish(null);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
199
258
|
* Cancel recording without saving.
|
|
200
259
|
*
|
|
201
260
|
* Removes the event listener, resets the recording state, and calls
|
|
202
261
|
* the onCancel callback if provided.
|
|
203
262
|
*/
|
|
204
263
|
cancel() {
|
|
205
|
-
|
|
206
|
-
if (this.#keydownHandler) {
|
|
207
|
-
this.#removeListener(this.#keydownHandler);
|
|
208
|
-
this.#keydownHandler = null;
|
|
209
|
-
}
|
|
210
|
-
this.store.setState(() => ({
|
|
211
|
-
isRecording: false,
|
|
212
|
-
recordedHotkey: null
|
|
213
|
-
}));
|
|
264
|
+
this.#finish(null);
|
|
214
265
|
this.#options.onCancel?.();
|
|
215
266
|
}
|
|
216
|
-
/**
|
|
217
|
-
* Adds the keydown event listener to the document.
|
|
218
|
-
*/
|
|
219
267
|
#addListener(handler) {
|
|
220
268
|
if (typeof document === "undefined") return;
|
|
221
269
|
document.addEventListener("keydown", handler, true);
|
|
222
270
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Removes the keydown event listener from the document.
|
|
225
|
-
*/
|
|
226
271
|
#removeListener(handler) {
|
|
227
272
|
if (typeof document === "undefined") return;
|
|
228
273
|
document.removeEventListener("keydown", handler, true);
|
|
@@ -234,11 +279,7 @@ var HotkeyRecorder = class {
|
|
|
234
279
|
* all event listeners are properly removed.
|
|
235
280
|
*/
|
|
236
281
|
destroy() {
|
|
237
|
-
this
|
|
238
|
-
this.store.setState(() => ({
|
|
239
|
-
isRecording: false,
|
|
240
|
-
recordedHotkey: null
|
|
241
|
-
}));
|
|
282
|
+
this.#finish(null);
|
|
242
283
|
}
|
|
243
284
|
};
|
|
244
285
|
|
package/dist/hotkey.types.d.ts
CHANGED
|
@@ -88,16 +88,16 @@ export type FourModifierHotkey = `Control+Alt+Shift+Meta+${Key}`;
|
|
|
88
88
|
* @example
|
|
89
89
|
* ```ts
|
|
90
90
|
* // Cross-platform shortcuts (recommended)
|
|
91
|
-
* const save:
|
|
92
|
-
* const saveAs:
|
|
93
|
-
* const comment:
|
|
91
|
+
* const save: HotkeyChord = 'Mod+S' // Command+S on Mac, Ctrl+S on Windows/Linux
|
|
92
|
+
* const saveAs: HotkeyChord = 'Mod+Shift+S' // Command+Shift+S on Mac, Ctrl+Shift+S elsewhere
|
|
93
|
+
* const comment: HotkeyChord = 'Mod+/' // Command+/ on Mac, Ctrl+/ elsewhere
|
|
94
94
|
*
|
|
95
95
|
* // Platform-specific shortcuts
|
|
96
|
-
* const macOnly:
|
|
97
|
-
* const windowsOnly:
|
|
96
|
+
* const macOnly: HotkeyChord = 'Meta+S' // Command+S on Mac only
|
|
97
|
+
* const windowsOnly: HotkeyChord = 'Control+S' // Ctrl+S on Windows/Linux only
|
|
98
98
|
* ```
|
|
99
99
|
*/
|
|
100
|
-
export type
|
|
100
|
+
export type HotkeyChord = Key | SingleModifierHotkey | TwoModifierHotkey | ThreeModifierHotkey | FourModifierHotkey;
|
|
101
101
|
/** Resolved modifier flags shared by both parsed binding variants. */
|
|
102
102
|
export interface ParsedModifiers {
|
|
103
103
|
/** Whether Control is required. */
|
|
@@ -171,17 +171,31 @@ export type RawHotkey = ({
|
|
|
171
171
|
key?: never;
|
|
172
172
|
} & RawModifiers);
|
|
173
173
|
/**
|
|
174
|
-
* A
|
|
175
|
-
*
|
|
174
|
+
* A sequence of chords pressed one after another (Vim-style), such as `['G', 'G']`.
|
|
175
|
+
*
|
|
176
|
+
* Steps may include modifiers; the same modifier can appear on consecutive
|
|
177
|
+
* steps (e.g. `['Shift+R', 'Shift+T']`). Modifier-only key events do not
|
|
178
|
+
* advance or reset matching. Automatic repeats and IME composition are ignored.
|
|
179
|
+
* A one-step sequence is treated as its single chord.
|
|
180
|
+
*/
|
|
181
|
+
export type HotkeySequence = ReadonlyArray<HotkeyChord>;
|
|
182
|
+
/**
|
|
183
|
+
* A hotkey: one chord (`'Mod+S'`) or a sequence of chords (`['G', 'G']`).
|
|
184
|
+
*/
|
|
185
|
+
export type Hotkey = HotkeyChord | HotkeySequence;
|
|
186
|
+
/** A single chord as a type-safe string (`HotkeyChord`) or a raw object (`RawHotkey`). */
|
|
187
|
+
export type RegisterableHotkeyChord = HotkeyChord | RawHotkey;
|
|
188
|
+
/**
|
|
189
|
+
* Anything that can be registered: a chord string, a raw chord object, or a sequence.
|
|
176
190
|
*/
|
|
177
|
-
export type RegisterableHotkey =
|
|
191
|
+
export type RegisterableHotkey = RegisterableHotkeyChord | HotkeySequence;
|
|
178
192
|
/**
|
|
179
193
|
* Context passed to hotkey callbacks along with the keyboard event.
|
|
180
194
|
*/
|
|
181
195
|
export interface HotkeyCallbackContext {
|
|
182
|
-
/** The
|
|
196
|
+
/** The registered hotkey: a chord string or a sequence */
|
|
183
197
|
hotkey: Hotkey;
|
|
184
|
-
/** The parsed
|
|
198
|
+
/** The parsed chord that completed the match (the last step of a sequence) */
|
|
185
199
|
parsedHotkey: ParsedHotkey;
|
|
186
200
|
}
|
|
187
201
|
/**
|
|
@@ -200,7 +214,7 @@ export interface HotkeyCallbackContext {
|
|
|
200
214
|
*/
|
|
201
215
|
export type HotkeyCallback = (event: KeyboardEvent, context: HotkeyCallbackContext) => void;
|
|
202
216
|
/**
|
|
203
|
-
* Metadata for hotkey and
|
|
217
|
+
* Metadata for hotkey registrations, chords and sequences alike.
|
|
204
218
|
* Includes `name`, `description`, and `group` by default. Extend via declaration merging:
|
|
205
219
|
*
|
|
206
220
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { CanonicalModifier, EditingKey, FunctionKey, IndividualKey, Key, LetterKey, LogicalKey, Modifier, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PhysicalKey, PhysicalKeyCode, PunctuationKey } from "./key.types.js";
|
|
2
|
-
import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyMeta, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
|
|
3
|
-
import { HotkeyManager, HotkeyOptions, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
4
|
-
import {
|
|
2
|
+
import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, RegisterableHotkeyChord, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
|
|
3
|
+
import { HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
4
|
+
import { AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CustomHotkeys, DefinedAppHotkeys, Register, RegisteredAppHotkeys, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
|
|
5
5
|
import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
|
|
6
6
|
import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
|
|
7
7
|
import { detectPlatform, resolveModifier } from "./platform.js";
|
|
8
|
-
import { DisplayHotkey, FormatDisplayOptions, formatForDisplay, formatHotkey
|
|
9
|
-
import { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
|
|
10
|
-
import { ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
|
|
11
|
-
import { CreateHotkeyHandlerOptions, KeyboardEventMatch, MultiHotkeyHandler, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent } from "./match.js";
|
|
8
|
+
import { DisplayHotkey, DisplayHotkeyChord, FormatDisplayOptions, formatForDisplay, formatHotkey } from "./format.js";
|
|
9
|
+
import { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
|
|
10
|
+
import { ValidatableHotkey, ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
|
|
11
|
+
import { CreateHotkeyHandlerOptions, DEFAULT_SEQUENCE_TIMEOUT, KeyboardEventMatch, MultiHotkeyHandler, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, matchesKeyboardEvent } from "./match.js";
|
|
12
12
|
import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
|
|
13
13
|
import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./conflicts.js";
|
|
14
14
|
import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
|
|
15
|
-
import { HotkeyRecorderValidationContext,
|
|
16
|
-
import { HotkeyRecorder, HotkeyRecorderOptions, HotkeyRecorderState } from "./hotkey-recorder.js";
|
|
17
|
-
|
|
18
|
-
export { ALL_KEYS, CanonicalModifier, ConflictBehavior, CreateHotkeyHandlerOptions, DEFAULT_SEQUENCE_TIMEOUT, DisplayHotkey, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkey, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyRecorder, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequence, HotkeySequenceRecorder, HotkeySequenceRecorderCommitKeys, HotkeySequenceRecorderOptions, HotkeySequenceRecorderState, HotkeySequenceRecorderValidationContext, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkey, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkey, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, RegisterableHotkey, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, SingleModifierHotkey, Target, ThreeModifierHotkey, TwoModifierHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels, getHotkeyManager, getKeyStateTracker, getSequenceManager, hasNonModifierKey, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, resolveModifier, toHotkeyRegistrationView, validateHotkey };
|
|
15
|
+
import { HotkeyRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./recorder-options.js";
|
|
16
|
+
import { HotkeyChordRecorderOptions, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeySequenceRecorderOptions } from "./hotkey-recorder.js";
|
|
17
|
+
export { ALL_KEYS, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkey, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyChord, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeySequence, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkey, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkey, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisteredAppHotkeys, SingleModifierHotkey, Target, ThreeModifierHotkey, TwoModifierHotkey, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyHandler, createMultiHotkeyHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeKeyName, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, provideAppHotkeys, publishAppHotkeysStore, rawHotkeyToParsedHotkey, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
|