@tanstack/hotkeys 0.10.1 → 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/README.md +3 -0
- package/dist/adapter.d.ts +116 -0
- package/dist/adapter.js +134 -0
- 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/constants.d.ts +1 -1
- 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 -41
- package/dist/hotkey-recorder.js +158 -109
- 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 +3 -2
- package/dist/hotkey-sequence-recorder.d.ts +0 -83
- package/dist/hotkey-sequence-recorder.js +0 -254
- 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,77 +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
|
-
constructor(options:
|
|
100
|
+
constructor(options: HotkeyChordRecorderOptions | (() => HotkeyChordRecorderOptions));
|
|
101
|
+
constructor(options: HotkeySequenceRecorderOptions | (() => HotkeySequenceRecorderOptions));
|
|
82
102
|
/**
|
|
83
103
|
* Updates the recorder options, including callbacks.
|
|
84
104
|
* This allows framework adapters to sync callback changes without recreating the recorder.
|
|
@@ -88,10 +108,15 @@ export declare class HotkeyRecorder {
|
|
|
88
108
|
* Start recording a new hotkey.
|
|
89
109
|
*
|
|
90
110
|
* Sets up a keydown event listener that captures keyboard events
|
|
91
|
-
* and
|
|
92
|
-
* a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
|
|
111
|
+
* and records chords according to `maxSteps`.
|
|
93
112
|
*/
|
|
94
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;
|
|
95
120
|
/**
|
|
96
121
|
* Stop recording (same as cancel, but doesn't call onCancel).
|
|
97
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,59 +9,64 @@ 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;
|
|
55
|
+
#optionOverrides = {};
|
|
62
56
|
#platform;
|
|
57
|
+
#events = [];
|
|
58
|
+
#idleTimer = null;
|
|
59
|
+
get #options() {
|
|
60
|
+
return {
|
|
61
|
+
...typeof this.#optionsSource === "function" ? this.#optionsSource() : this.#optionsSource,
|
|
62
|
+
...this.#optionOverrides
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
get #maxSteps() {
|
|
66
|
+
return Math.max(1, this.#options.maxSteps ?? 1);
|
|
67
|
+
}
|
|
63
68
|
constructor(options) {
|
|
64
|
-
this.#
|
|
69
|
+
this.#optionsSource = options;
|
|
65
70
|
this.#platform = detectPlatform();
|
|
66
71
|
}
|
|
67
72
|
/**
|
|
@@ -69,24 +74,40 @@ var HotkeyRecorder = class {
|
|
|
69
74
|
* This allows framework adapters to sync callback changes without recreating the recorder.
|
|
70
75
|
*/
|
|
71
76
|
setOptions(options) {
|
|
72
|
-
this.#
|
|
73
|
-
...this.#
|
|
77
|
+
this.#optionOverrides = {
|
|
78
|
+
...this.#optionOverrides,
|
|
74
79
|
...options
|
|
75
80
|
};
|
|
76
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
|
+
}
|
|
77
97
|
/**
|
|
78
98
|
* Start recording a new hotkey.
|
|
79
99
|
*
|
|
80
100
|
* Sets up a keydown event listener that captures keyboard events
|
|
81
|
-
* and
|
|
82
|
-
* a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
|
|
101
|
+
* and records chords according to `maxSteps`.
|
|
83
102
|
*/
|
|
84
103
|
start() {
|
|
85
104
|
if (this.#keydownHandler) return;
|
|
105
|
+
this.#clearIdleTimer();
|
|
106
|
+
this.#events = [];
|
|
86
107
|
beginRecording(this);
|
|
87
108
|
this.store.setState(() => ({
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
...initialState,
|
|
110
|
+
isRecording: true
|
|
90
111
|
}));
|
|
91
112
|
const handler = (event) => {
|
|
92
113
|
if (!this.#keydownHandler) return;
|
|
@@ -101,120 +122,152 @@ var HotkeyRecorder = class {
|
|
|
101
122
|
this.cancel();
|
|
102
123
|
return;
|
|
103
124
|
}
|
|
104
|
-
|
|
105
|
-
|
|
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) {
|
|
106
129
|
this.stop();
|
|
107
130
|
this.#options.onClear?.();
|
|
108
131
|
return;
|
|
109
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;
|
|
110
146
|
}
|
|
111
147
|
if (isModifierKey(event.key) || event.key === "AltGraph") return;
|
|
112
148
|
const rejection = chordRejection(event, {
|
|
113
|
-
...
|
|
149
|
+
...options,
|
|
114
150
|
platform
|
|
115
151
|
});
|
|
116
152
|
if (rejection) {
|
|
117
|
-
|
|
153
|
+
options.onReject?.(rejection);
|
|
118
154
|
return;
|
|
119
155
|
}
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
const validation = validateHotkey(
|
|
156
|
+
const chord = hotkeyChordFromKeydown(event, platform, options.recordBy);
|
|
157
|
+
if (chord === null) return;
|
|
158
|
+
const validation = validateHotkey(chord);
|
|
123
159
|
if (!validation.valid) {
|
|
124
|
-
|
|
160
|
+
options.onReject?.({
|
|
125
161
|
reason: "invalid",
|
|
126
162
|
message: validation.errors.join("; "),
|
|
127
|
-
hotkey:
|
|
128
|
-
});
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const accepted = this.#options.validate?.(finalHotkey, {
|
|
132
|
-
event,
|
|
133
|
-
parsedHotkey: parseHotkey(finalHotkey, platform)
|
|
134
|
-
});
|
|
135
|
-
if (accepted !== void 0 && accepted !== true) {
|
|
136
|
-
this.#options.onReject?.({
|
|
137
|
-
reason: "validation",
|
|
138
|
-
message: typeof accepted === "string" ? accepted : "This shortcut is not allowed.",
|
|
139
|
-
hotkey: finalHotkey
|
|
163
|
+
hotkey: chord
|
|
140
164
|
});
|
|
141
165
|
return;
|
|
142
166
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
events: [event]
|
|
148
|
-
});
|
|
149
|
-
if (conflicts.length) {
|
|
150
|
-
this.#options.onReject?.({
|
|
151
|
-
reason: "conflict",
|
|
152
|
-
message: "This shortcut conflicts with a registered binding.",
|
|
153
|
-
hotkey: finalHotkey,
|
|
154
|
-
conflicts
|
|
155
|
-
});
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
endRecording(this);
|
|
160
|
-
const handlerToRemove = this.#keydownHandler;
|
|
161
|
-
if (handlerToRemove) {
|
|
162
|
-
this.#removeListener(handlerToRemove);
|
|
163
|
-
this.#keydownHandler = null;
|
|
164
|
-
}
|
|
165
|
-
this.store.setState(() => ({
|
|
166
|
-
isRecording: false,
|
|
167
|
-
recordedHotkey: finalHotkey
|
|
167
|
+
this.#events.push(event);
|
|
168
|
+
this.store.setState((s) => ({
|
|
169
|
+
...s,
|
|
170
|
+
steps: [...s.steps, chord]
|
|
168
171
|
}));
|
|
169
|
-
this.#
|
|
172
|
+
if (this.store.state.steps.length >= this.#maxSteps) this.commit();
|
|
173
|
+
else this.#scheduleIdleTimer();
|
|
170
174
|
};
|
|
171
175
|
this.#keydownHandler = handler;
|
|
172
176
|
this.#addListener(handler);
|
|
173
177
|
}
|
|
174
178
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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.
|
|
178
182
|
*/
|
|
179
|
-
|
|
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) {
|
|
180
237
|
endRecording(this);
|
|
238
|
+
this.#events = [];
|
|
239
|
+
this.#clearIdleTimer();
|
|
181
240
|
if (this.#keydownHandler) {
|
|
182
241
|
this.#removeListener(this.#keydownHandler);
|
|
183
242
|
this.#keydownHandler = null;
|
|
184
243
|
}
|
|
185
244
|
this.store.setState(() => ({
|
|
186
|
-
|
|
187
|
-
recordedHotkey
|
|
245
|
+
...initialState,
|
|
246
|
+
recordedHotkey
|
|
188
247
|
}));
|
|
189
248
|
}
|
|
190
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
|
+
/**
|
|
191
258
|
* Cancel recording without saving.
|
|
192
259
|
*
|
|
193
260
|
* Removes the event listener, resets the recording state, and calls
|
|
194
261
|
* the onCancel callback if provided.
|
|
195
262
|
*/
|
|
196
263
|
cancel() {
|
|
197
|
-
|
|
198
|
-
if (this.#keydownHandler) {
|
|
199
|
-
this.#removeListener(this.#keydownHandler);
|
|
200
|
-
this.#keydownHandler = null;
|
|
201
|
-
}
|
|
202
|
-
this.store.setState(() => ({
|
|
203
|
-
isRecording: false,
|
|
204
|
-
recordedHotkey: null
|
|
205
|
-
}));
|
|
264
|
+
this.#finish(null);
|
|
206
265
|
this.#options.onCancel?.();
|
|
207
266
|
}
|
|
208
|
-
/**
|
|
209
|
-
* Adds the keydown event listener to the document.
|
|
210
|
-
*/
|
|
211
267
|
#addListener(handler) {
|
|
212
268
|
if (typeof document === "undefined") return;
|
|
213
269
|
document.addEventListener("keydown", handler, true);
|
|
214
270
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Removes the keydown event listener from the document.
|
|
217
|
-
*/
|
|
218
271
|
#removeListener(handler) {
|
|
219
272
|
if (typeof document === "undefined") return;
|
|
220
273
|
document.removeEventListener("keydown", handler, true);
|
|
@@ -226,11 +279,7 @@ var HotkeyRecorder = class {
|
|
|
226
279
|
* all event listeners are properly removed.
|
|
227
280
|
*/
|
|
228
281
|
destroy() {
|
|
229
|
-
this
|
|
230
|
-
this.store.setState(() => ({
|
|
231
|
-
isRecording: false,
|
|
232
|
-
recordedHotkey: null
|
|
233
|
-
}));
|
|
282
|
+
this.#finish(null);
|
|
234
283
|
}
|
|
235
284
|
};
|
|
236
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, 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";
|
|
2
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";
|
|
3
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";
|
|
4
7
|
import { detectPlatform, resolveModifier } from "./platform.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
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";
|
|
10
12
|
import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
|
|
11
|
-
import { HotkeyManager, HotkeyOptions, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
12
|
-
import { DEFAULT_SEQUENCE_TIMEOUT, HotkeySequence, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, Target, createSequenceMatcher, getSequenceManager } from "./sequence-manager.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 };
|