@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,254 +0,0 @@
|
|
|
1
|
-
import { detectPlatform } from "./platform.js";
|
|
2
|
-
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
3
|
-
import { isModifierKey, parseHotkey } from "./parse.js";
|
|
4
|
-
import { validateHotkey } from "./validate.js";
|
|
5
|
-
import { beginRecording, captureRecordingEvent, endRecording } from "./_recording-guard.js";
|
|
6
|
-
import { shouldIgnoreInputEvent } from "./_event-target.js";
|
|
7
|
-
import { findHotkeyConflicts } from "./conflicts.js";
|
|
8
|
-
import { chordRejection, hotkeyChordFromKeydown } from "./_recorder-chord.js";
|
|
9
|
-
import { Store } from "@tanstack/store";
|
|
10
|
-
|
|
11
|
-
//#region src/hotkey-sequence-recorder.ts
|
|
12
|
-
const defaultHotkeySequenceRecorderOptions = {
|
|
13
|
-
commitOnEnter: true,
|
|
14
|
-
commitKeys: "enter"
|
|
15
|
-
};
|
|
16
|
-
/** Resolves the Enter policy, with commitKeys: none overriding the legacy boolean. */
|
|
17
|
-
function resolvedCommitOnEnter(options) {
|
|
18
|
-
if (options.commitKeys === "none") return false;
|
|
19
|
-
return options.commitOnEnter !== false;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Framework-agnostic class for recording multi-chord sequences (Vim-style shortcuts).
|
|
23
|
-
*
|
|
24
|
-
* Each step is captured like a single hotkey chord. Press **Enter** (no modifiers) to commit
|
|
25
|
-
* when {@link HotkeySequenceRecorderOptions.commitKeys} is `'enter'` (default), **Escape** to cancel,
|
|
26
|
-
* **Backspace/Delete** to remove the last step or clear when empty.
|
|
27
|
-
*/
|
|
28
|
-
var HotkeySequenceRecorder = class {
|
|
29
|
-
store = new Store({
|
|
30
|
-
isRecording: false,
|
|
31
|
-
steps: [],
|
|
32
|
-
recordedSequence: null
|
|
33
|
-
});
|
|
34
|
-
#keydownHandler = null;
|
|
35
|
-
#options;
|
|
36
|
-
#platform;
|
|
37
|
-
#events = [];
|
|
38
|
-
#idleTimer = null;
|
|
39
|
-
constructor(options) {
|
|
40
|
-
this.#options = {
|
|
41
|
-
...defaultHotkeySequenceRecorderOptions,
|
|
42
|
-
...options
|
|
43
|
-
};
|
|
44
|
-
this.#platform = detectPlatform();
|
|
45
|
-
}
|
|
46
|
-
/** Merges current callbacks and options without discarding recorded steps. */
|
|
47
|
-
setOptions(options) {
|
|
48
|
-
this.#options = {
|
|
49
|
-
...defaultHotkeySequenceRecorderOptions,
|
|
50
|
-
...this.#options,
|
|
51
|
-
...options
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** Cancels a pending idle commit so it cannot fire after editing or stopping. */
|
|
55
|
-
#clearIdleTimer() {
|
|
56
|
-
if (this.#idleTimer !== null) {
|
|
57
|
-
clearTimeout(this.#idleTimer);
|
|
58
|
-
this.#idleTimer = null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/** Restarts optional idle commit after a step is recorded or edited. */
|
|
62
|
-
#scheduleIdleTimer() {
|
|
63
|
-
this.#clearIdleTimer();
|
|
64
|
-
const ms = this.#options.idleTimeoutMs;
|
|
65
|
-
if (ms === void 0 || ms <= 0) return;
|
|
66
|
-
if (this.store.state.steps.length === 0) return;
|
|
67
|
-
this.#idleTimer = setTimeout(() => {
|
|
68
|
-
this.#idleTimer = null;
|
|
69
|
-
if (!this.#keydownHandler) return;
|
|
70
|
-
if (this.store.state.steps.length >= 1) this.commit();
|
|
71
|
-
}, ms);
|
|
72
|
-
}
|
|
73
|
-
/** Publishes edited steps while preserving the remaining recorder state. */
|
|
74
|
-
#patchSteps(updater) {
|
|
75
|
-
this.store.setState((s) => ({
|
|
76
|
-
...s,
|
|
77
|
-
steps: updater(s.steps)
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
/** Starts a fresh recording; repeated starts during an active session are ignored. */
|
|
81
|
-
start() {
|
|
82
|
-
if (this.#keydownHandler) return;
|
|
83
|
-
this.#clearIdleTimer();
|
|
84
|
-
this.#events = [];
|
|
85
|
-
beginRecording(this);
|
|
86
|
-
this.store.setState(() => ({
|
|
87
|
-
isRecording: true,
|
|
88
|
-
steps: [],
|
|
89
|
-
recordedSequence: null
|
|
90
|
-
}));
|
|
91
|
-
const handler = (event) => {
|
|
92
|
-
if (!this.#keydownHandler) return;
|
|
93
|
-
if (this.#options.ignoreInputs !== false && event.key !== "Escape" && shouldIgnoreInputEvent(event, document, document)) return;
|
|
94
|
-
const platform = this.#options.platform ?? this.#platform;
|
|
95
|
-
if (normalizeKeyboardEvent(event, platform).isComposing) return;
|
|
96
|
-
captureRecordingEvent(event);
|
|
97
|
-
event.preventDefault();
|
|
98
|
-
event.stopPropagation();
|
|
99
|
-
if (event.repeat) return;
|
|
100
|
-
if (event.key === "Escape") {
|
|
101
|
-
this.cancel();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (event.key === "Backspace" || event.key === "Delete") {
|
|
105
|
-
if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
|
|
106
|
-
if (this.store.state.steps.length === 0) {
|
|
107
|
-
this.stop();
|
|
108
|
-
this.#options.onClear?.();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
this.#events.pop();
|
|
112
|
-
this.#patchSteps((prev) => prev.slice(0, -1));
|
|
113
|
-
if (this.store.state.steps.length === 0) this.#clearIdleTimer();
|
|
114
|
-
else this.#scheduleIdleTimer();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const enterCommits = resolvedCommitOnEnter(this.#options) && event.key === "Enter" && !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
|
|
119
|
-
if (enterCommits && this.store.state.steps.length >= 1) {
|
|
120
|
-
this.commit();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (enterCommits && this.store.state.steps.length === 0) return;
|
|
124
|
-
if (isModifierKey(event.key) || event.key === "AltGraph") return;
|
|
125
|
-
const rejection = chordRejection(event, {
|
|
126
|
-
...this.#options,
|
|
127
|
-
platform
|
|
128
|
-
});
|
|
129
|
-
if (rejection) {
|
|
130
|
-
this.#options.onReject?.(rejection);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const finalHotkey = hotkeyChordFromKeydown(event, platform, this.#options.recordBy);
|
|
134
|
-
if (finalHotkey === null) return;
|
|
135
|
-
const validation = validateHotkey(finalHotkey);
|
|
136
|
-
if (!validation.valid) {
|
|
137
|
-
this.#options.onReject?.({
|
|
138
|
-
reason: "invalid",
|
|
139
|
-
message: validation.errors.join("; "),
|
|
140
|
-
hotkey: finalHotkey
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
this.#events.push(event);
|
|
145
|
-
this.#patchSteps((prev) => [...prev, finalHotkey]);
|
|
146
|
-
this.#scheduleIdleTimer();
|
|
147
|
-
};
|
|
148
|
-
this.#keydownHandler = handler;
|
|
149
|
-
this.#addListener(handler);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Commit the current steps as a sequence. No-op if fewer than one step.
|
|
153
|
-
*/
|
|
154
|
-
commit() {
|
|
155
|
-
const steps = this.store.state.steps;
|
|
156
|
-
if (steps.length < 1) return;
|
|
157
|
-
const sequence = [...steps];
|
|
158
|
-
const platform = this.#options.platform ?? this.#platform;
|
|
159
|
-
this.#clearIdleTimer();
|
|
160
|
-
const accepted = this.#options.validate?.(sequence, {
|
|
161
|
-
events: [...this.#events],
|
|
162
|
-
parsedSequence: sequence.map((step) => parseHotkey(step, platform))
|
|
163
|
-
});
|
|
164
|
-
if (accepted !== void 0 && accepted !== true) {
|
|
165
|
-
this.#options.onReject?.({
|
|
166
|
-
reason: "validation",
|
|
167
|
-
message: typeof accepted === "string" ? accepted : "This sequence is not allowed.",
|
|
168
|
-
sequence
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (this.#options.detectConflicts) {
|
|
173
|
-
const conflicts = findHotkeyConflicts(sequence, {
|
|
174
|
-
...typeof this.#options.detectConflicts === "object" ? this.#options.detectConflicts : {},
|
|
175
|
-
platform,
|
|
176
|
-
events: this.#events
|
|
177
|
-
});
|
|
178
|
-
if (conflicts.length) {
|
|
179
|
-
this.#options.onReject?.({
|
|
180
|
-
reason: "conflict",
|
|
181
|
-
message: "This sequence conflicts with a registered binding.",
|
|
182
|
-
sequence,
|
|
183
|
-
conflicts
|
|
184
|
-
});
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
endRecording(this);
|
|
189
|
-
this.#events = [];
|
|
190
|
-
if (this.#keydownHandler) {
|
|
191
|
-
this.#removeListener(this.#keydownHandler);
|
|
192
|
-
this.#keydownHandler = null;
|
|
193
|
-
}
|
|
194
|
-
this.#clearIdleTimer();
|
|
195
|
-
this.store.setState(() => ({
|
|
196
|
-
isRecording: false,
|
|
197
|
-
steps: [],
|
|
198
|
-
recordedSequence: sequence
|
|
199
|
-
}));
|
|
200
|
-
this.#options.onRecord(sequence);
|
|
201
|
-
}
|
|
202
|
-
/** Stops and discards in-progress steps without invoking onCancel. */
|
|
203
|
-
stop() {
|
|
204
|
-
endRecording(this);
|
|
205
|
-
this.#events = [];
|
|
206
|
-
if (this.#keydownHandler) {
|
|
207
|
-
this.#removeListener(this.#keydownHandler);
|
|
208
|
-
this.#keydownHandler = null;
|
|
209
|
-
}
|
|
210
|
-
this.#clearIdleTimer();
|
|
211
|
-
this.store.setState(() => ({
|
|
212
|
-
isRecording: false,
|
|
213
|
-
steps: [],
|
|
214
|
-
recordedSequence: null
|
|
215
|
-
}));
|
|
216
|
-
}
|
|
217
|
-
/** Stops, discards in-progress steps, and notifies onCancel. */
|
|
218
|
-
cancel() {
|
|
219
|
-
endRecording(this);
|
|
220
|
-
this.#events = [];
|
|
221
|
-
if (this.#keydownHandler) {
|
|
222
|
-
this.#removeListener(this.#keydownHandler);
|
|
223
|
-
this.#keydownHandler = null;
|
|
224
|
-
}
|
|
225
|
-
this.#clearIdleTimer();
|
|
226
|
-
this.store.setState(() => ({
|
|
227
|
-
isRecording: false,
|
|
228
|
-
steps: [],
|
|
229
|
-
recordedSequence: null
|
|
230
|
-
}));
|
|
231
|
-
this.#options.onCancel?.();
|
|
232
|
-
}
|
|
233
|
-
/** Captures keydowns before application handlers, with an SSR-safe document check. */
|
|
234
|
-
#addListener(handler) {
|
|
235
|
-
if (typeof document === "undefined") return;
|
|
236
|
-
document.addEventListener("keydown", handler, true);
|
|
237
|
-
}
|
|
238
|
-
/** Detaches the capture listener when recording ends. */
|
|
239
|
-
#removeListener(handler) {
|
|
240
|
-
if (typeof document === "undefined") return;
|
|
241
|
-
document.removeEventListener("keydown", handler, true);
|
|
242
|
-
}
|
|
243
|
-
destroy() {
|
|
244
|
-
this.stop();
|
|
245
|
-
this.store.setState(() => ({
|
|
246
|
-
isRecording: false,
|
|
247
|
-
steps: [],
|
|
248
|
-
recordedSequence: null
|
|
249
|
-
}));
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
//#endregion
|
|
254
|
-
export { HotkeySequenceRecorder };
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { Hotkey, HotkeyCallback, ParsedHotkey } from "./hotkey.types.js";
|
|
2
|
-
import { HotkeyOptions } from "./hotkey-manager.js";
|
|
3
|
-
import { Store } from "@tanstack/store";
|
|
4
|
-
//#region src/sequence-manager.d.ts
|
|
5
|
-
export type Target = HTMLElement | Document | Window;
|
|
6
|
-
/**
|
|
7
|
-
* Options for hotkey sequence matching.
|
|
8
|
-
* Extends HotkeyOptions but excludes requireReset (not applicable to sequences).
|
|
9
|
-
*/
|
|
10
|
-
export interface SequenceOptions extends Omit<HotkeyOptions, 'requireReset'> {
|
|
11
|
-
/** Timeout between keys in milliseconds. Default: 1000 */
|
|
12
|
-
timeout?: number;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* A sequence of hotkeys for Vim-style shortcuts.
|
|
16
|
-
*
|
|
17
|
-
* Each element is one step (a `Hotkey` string). Steps may include modifiers;
|
|
18
|
-
* the same modifier can appear on consecutive steps (e.g. `Shift+R` then
|
|
19
|
-
* `Shift+T`). Modifier-only key events do not advance or reset matching—see
|
|
20
|
-
* `SequenceManager`. Automatic keydown repeats and IME composition are also ignored.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* const gotoTop: HotkeySequence = ['G', 'G'] // gg
|
|
25
|
-
* const deleteLine: HotkeySequence = ['D', 'D'] // dd
|
|
26
|
-
* const deleteWord: HotkeySequence = ['D', 'I', 'W'] // diw
|
|
27
|
-
* const chainedShift: HotkeySequence = ['Shift+R', 'Shift+T']
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export type HotkeySequence = Array<Hotkey>;
|
|
31
|
-
/**
|
|
32
|
-
* Default timeout between keys in a sequence (in milliseconds).
|
|
33
|
-
* Exported for consumers that display sequence state (e.g. devtools).
|
|
34
|
-
*/
|
|
35
|
-
export declare const DEFAULT_SEQUENCE_TIMEOUT = 1000;
|
|
36
|
-
/**
|
|
37
|
-
* View of a sequence registration for devtools display.
|
|
38
|
-
* Progress fields reflect an in-progress match (between first key and completion or timeout).
|
|
39
|
-
*/
|
|
40
|
-
export interface SequenceRegistrationView {
|
|
41
|
-
id: string;
|
|
42
|
-
sequence: HotkeySequence;
|
|
43
|
-
options: SequenceOptions;
|
|
44
|
-
target: Target;
|
|
45
|
-
triggerCount: number;
|
|
46
|
-
/** Whether this sequence has been triggered at least once. */
|
|
47
|
-
hasFired: boolean;
|
|
48
|
-
/** Steps matched in the current attempt (0 when idle or just completed). */
|
|
49
|
-
matchedStepCount: number;
|
|
50
|
-
/** `Date.now()` when the last step in the current attempt matched; 0 if none. */
|
|
51
|
-
partialMatchLastKeyTime: number;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Internal representation of a sequence registration.
|
|
55
|
-
*/
|
|
56
|
-
export interface SequenceRegistration {
|
|
57
|
-
id: string;
|
|
58
|
-
sequence: HotkeySequence;
|
|
59
|
-
parsedSequence: Array<ParsedHotkey>;
|
|
60
|
-
callback: HotkeyCallback;
|
|
61
|
-
options: SequenceOptions;
|
|
62
|
-
target: Target;
|
|
63
|
-
currentIndex: number;
|
|
64
|
-
lastKeyTime: number;
|
|
65
|
-
triggerCount: number;
|
|
66
|
-
hasFired: boolean;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* A handle returned from SequenceManager.register() that allows updating
|
|
70
|
-
* the callback and options without re-registering the sequence.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* const handle = manager.register(['G', 'G'], callback, options)
|
|
75
|
-
*
|
|
76
|
-
* handle.callback = newCallback
|
|
77
|
-
* handle.setOptions({ timeout: 500 })
|
|
78
|
-
* handle.unregister()
|
|
79
|
-
* ```
|
|
80
|
-
*/
|
|
81
|
-
export interface SequenceRegistrationHandle {
|
|
82
|
-
readonly id: string;
|
|
83
|
-
readonly isActive: boolean;
|
|
84
|
-
callback: HotkeyCallback;
|
|
85
|
-
setOptions: (options: Partial<SequenceOptions>) => void;
|
|
86
|
-
unregister: () => void;
|
|
87
|
-
}
|
|
88
|
-
export declare class SequenceManager {
|
|
89
|
-
#private;
|
|
90
|
-
/**
|
|
91
|
-
* The TanStack Store containing sequence registration views for devtools.
|
|
92
|
-
* Subscribe to this to observe registration changes.
|
|
93
|
-
*/
|
|
94
|
-
readonly registrations: Store<Map<string, SequenceRegistrationView>>;
|
|
95
|
-
private constructor();
|
|
96
|
-
/**
|
|
97
|
-
* Gets the singleton instance of SequenceManager.
|
|
98
|
-
*/
|
|
99
|
-
static getInstance(): SequenceManager;
|
|
100
|
-
/**
|
|
101
|
-
* Resets the singleton instance. Useful for testing.
|
|
102
|
-
*/
|
|
103
|
-
static resetInstance(): void;
|
|
104
|
-
/**
|
|
105
|
-
* Registers a hotkey sequence handler.
|
|
106
|
-
*
|
|
107
|
-
* @param sequence - Array of hotkey strings that form the sequence
|
|
108
|
-
* @param callback - Function to call when the sequence is completed
|
|
109
|
-
* @param options - Options for the sequence behavior
|
|
110
|
-
* @returns A handle to update or unregister the sequence
|
|
111
|
-
*/
|
|
112
|
-
register(sequence: HotkeySequence, callback: HotkeyCallback, options?: SequenceOptions): SequenceRegistrationHandle;
|
|
113
|
-
/**
|
|
114
|
-
* Resets all sequence progress.
|
|
115
|
-
*/
|
|
116
|
-
resetAll(): void;
|
|
117
|
-
/**
|
|
118
|
-
* Triggers a sequence's callback programmatically from devtools.
|
|
119
|
-
* Creates a synthetic KeyboardEvent from the last key in the sequence.
|
|
120
|
-
*
|
|
121
|
-
* @param id - The registration ID to trigger
|
|
122
|
-
* @returns True if the registration was found and triggered
|
|
123
|
-
*/
|
|
124
|
-
triggerSequence(id: string): boolean;
|
|
125
|
-
/**
|
|
126
|
-
* Gets the number of registered sequences.
|
|
127
|
-
*/
|
|
128
|
-
getRegistrationCount(): number;
|
|
129
|
-
/**
|
|
130
|
-
* Destroys the manager and removes all listeners.
|
|
131
|
-
*/
|
|
132
|
-
destroy(): void;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Gets the singleton SequenceManager instance.
|
|
136
|
-
* Convenience function for accessing the manager.
|
|
137
|
-
*/
|
|
138
|
-
export declare function getSequenceManager(): SequenceManager;
|
|
139
|
-
/**
|
|
140
|
-
* Creates a simple sequence matcher for one-off use.
|
|
141
|
-
*
|
|
142
|
-
* This is a low-level helper that does not support ignoreInputs, target,
|
|
143
|
-
* or other HotkeyOptions. Callers must handle input filtering themselves
|
|
144
|
-
* if attaching to document.
|
|
145
|
-
*
|
|
146
|
-
* @param sequence - The sequence of hotkeys to match
|
|
147
|
-
* @param options - Options including timeout
|
|
148
|
-
* @returns An object with match() and reset() methods
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```ts
|
|
152
|
-
* const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })
|
|
153
|
-
*
|
|
154
|
-
* document.addEventListener('keydown', (event) => {
|
|
155
|
-
* if (matcher.match(event)) {
|
|
156
|
-
* console.log('Sequence matched!')
|
|
157
|
-
* }
|
|
158
|
-
* })
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
export declare function createSequenceMatcher(sequence: HotkeySequence, options?: {
|
|
162
|
-
timeout?: number;
|
|
163
|
-
platform?: 'mac' | 'windows' | 'linux';
|
|
164
|
-
}): {
|
|
165
|
-
match: (event: KeyboardEvent) => boolean;
|
|
166
|
-
reset: () => void;
|
|
167
|
-
getProgress: () => number;
|
|
168
|
-
};
|
|
169
|
-
//#endregion
|