@tanstack/hotkeys 0.9.0 → 0.10.1
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/_event-target.js +1 -2
- package/dist/_keyboard-event.js +1 -2
- package/dist/_match.js +1 -2
- package/dist/_named-keys.d.ts +3 -5
- package/dist/_named-keys.js +1 -2
- package/dist/_recorder-chord.js +1 -2
- package/dist/_recording-guard.js +1 -2
- package/dist/_registration.js +1 -2
- package/dist/conflicts.d.ts +4 -7
- package/dist/conflicts.js +1 -2
- package/dist/constants.d.ts +14 -17
- package/dist/constants.js +8 -9
- package/dist/display-labels.d.ts +7 -10
- package/dist/display-labels.js +1 -2
- package/dist/format.d.ts +12 -14
- package/dist/format.js +17 -7
- package/dist/hint.d.ts +3 -6
- package/dist/hint.js +1 -2
- package/dist/hotkey-manager.d.ts +8 -10
- package/dist/hotkey-manager.js +21 -3
- package/dist/hotkey-recorder.d.ts +4 -7
- package/dist/hotkey-recorder.js +9 -6
- package/dist/hotkey-sequence-recorder.d.ts +5 -8
- package/dist/hotkey-sequence-recorder.js +6 -7
- package/dist/hotkey.types.d.ts +15 -18
- package/dist/key-state-tracker.d.ts +4 -7
- package/dist/key-state-tracker.js +6 -3
- package/dist/key.types.d.ts +17 -20
- package/dist/match.d.ts +8 -11
- package/dist/match.js +1 -2
- package/dist/parse.d.ts +11 -14
- package/dist/parse.js +5 -4
- package/dist/platform.d.ts +3 -6
- package/dist/platform.js +1 -2
- package/dist/recorder-options.d.ts +6 -9
- package/dist/sequence-manager.d.ts +11 -14
- package/dist/sequence-manager.js +12 -7
- package/dist/validate.d.ts +5 -8
- package/dist/validate.js +1 -2
- package/package.json +8 -13
- package/dist/_event-target.cjs +0 -73
- package/dist/_event-target.cjs.map +0 -1
- package/dist/_event-target.js.map +0 -1
- package/dist/_keyboard-event.cjs +0 -83
- package/dist/_keyboard-event.cjs.map +0 -1
- package/dist/_keyboard-event.js.map +0 -1
- package/dist/_match.cjs +0 -49
- package/dist/_match.cjs.map +0 -1
- package/dist/_match.js.map +0 -1
- package/dist/_named-keys.cjs +0 -68
- package/dist/_named-keys.cjs.map +0 -1
- package/dist/_named-keys.d.cts +0 -8
- package/dist/_named-keys.js.map +0 -1
- package/dist/_recorder-chord.cjs +0 -44
- package/dist/_recorder-chord.cjs.map +0 -1
- package/dist/_recorder-chord.js.map +0 -1
- package/dist/_recording-guard.cjs +0 -59
- package/dist/_recording-guard.cjs.map +0 -1
- package/dist/_recording-guard.js.map +0 -1
- package/dist/_registration.cjs +0 -54
- package/dist/_registration.cjs.map +0 -1
- package/dist/_registration.js.map +0 -1
- package/dist/conflicts.cjs +0 -55
- package/dist/conflicts.cjs.map +0 -1
- package/dist/conflicts.d.cts +0 -39
- package/dist/conflicts.js.map +0 -1
- package/dist/constants.cjs +0 -410
- package/dist/constants.cjs.map +0 -1
- package/dist/constants.d.cts +0 -165
- package/dist/constants.js.map +0 -1
- package/dist/display-labels.cjs +0 -119
- package/dist/display-labels.cjs.map +0 -1
- package/dist/display-labels.d.cts +0 -93
- package/dist/display-labels.js.map +0 -1
- package/dist/format.cjs +0 -85
- package/dist/format.cjs.map +0 -1
- package/dist/format.d.cts +0 -99
- package/dist/format.js.map +0 -1
- package/dist/hint.cjs +0 -24
- package/dist/hint.cjs.map +0 -1
- package/dist/hint.d.cts +0 -23
- package/dist/hint.js.map +0 -1
- package/dist/hotkey-manager.cjs +0 -414
- package/dist/hotkey-manager.cjs.map +0 -1
- package/dist/hotkey-manager.d.cts +0 -234
- package/dist/hotkey-manager.js.map +0 -1
- package/dist/hotkey-recorder.cjs +0 -235
- package/dist/hotkey-recorder.cjs.map +0 -1
- package/dist/hotkey-recorder.d.cts +0 -119
- package/dist/hotkey-recorder.js.map +0 -1
- package/dist/hotkey-sequence-recorder.cjs +0 -255
- package/dist/hotkey-sequence-recorder.cjs.map +0 -1
- package/dist/hotkey-sequence-recorder.d.cts +0 -86
- package/dist/hotkey-sequence-recorder.js.map +0 -1
- package/dist/hotkey.types.d.cts +0 -236
- package/dist/index.cjs +0 -71
- package/dist/index.d.cts +0 -18
- package/dist/key-state-tracker.cjs +0 -197
- package/dist/key-state-tracker.cjs.map +0 -1
- package/dist/key-state-tracker.d.cts +0 -107
- package/dist/key-state-tracker.js.map +0 -1
- package/dist/key.types.d.cts +0 -82
- package/dist/match.cjs +0 -144
- package/dist/match.cjs.map +0 -1
- package/dist/match.d.cts +0 -101
- package/dist/match.js.map +0 -1
- package/dist/parse.cjs +0 -245
- package/dist/parse.cjs.map +0 -1
- package/dist/parse.d.cts +0 -134
- package/dist/parse.js.map +0 -1
- package/dist/platform.cjs +0 -55
- package/dist/platform.cjs.map +0 -1
- package/dist/platform.d.cts +0 -44
- package/dist/platform.js.map +0 -1
- package/dist/recorder-options.d.cts +0 -34
- package/dist/sequence-manager.cjs +0 -441
- package/dist/sequence-manager.cjs.map +0 -1
- package/dist/sequence-manager.d.cts +0 -172
- package/dist/sequence-manager.js.map +0 -1
- package/dist/validate.cjs +0 -112
- package/dist/validate.cjs.map +0 -1
- package/dist/validate.d.cts +0 -67
- package/dist/validate.js.map +0 -1
- package/src/_event-target.ts +0 -157
- package/src/_keyboard-event.ts +0 -101
- package/src/_match.ts +0 -96
- package/src/_named-keys.ts +0 -62
- package/src/_recorder-chord.ts +0 -76
- package/src/_recording-guard.ts +0 -63
- package/src/_registration.ts +0 -79
- package/src/conflicts.ts +0 -125
- package/src/constants.ts +0 -455
- package/src/display-labels.ts +0 -123
- package/src/format.ts +0 -201
- package/src/hint.ts +0 -42
- package/src/hotkey-manager.ts +0 -814
- package/src/hotkey-recorder.ts +0 -348
- package/src/hotkey-sequence-recorder.ts +0 -410
- package/src/hotkey.types.ts +0 -269
- package/src/index.ts +0 -17
- package/src/key-state-tracker.ts +0 -261
- package/src/key.types.ts +0 -268
- package/src/match.ts +0 -210
- package/src/parse.ts +0 -369
- package/src/platform.ts +0 -66
- package/src/recorder-options.ts +0 -34
- package/src/sequence-manager.ts +0 -769
- package/src/validate.ts +0 -179
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import { Store } from '@tanstack/store'
|
|
2
|
-
import { findHotkeyConflicts } from './conflicts'
|
|
3
|
-
import {
|
|
4
|
-
beginRecording,
|
|
5
|
-
captureRecordingEvent,
|
|
6
|
-
endRecording,
|
|
7
|
-
} from './_recording-guard'
|
|
8
|
-
import { chordRejection, hotkeyChordFromKeydown } from './_recorder-chord'
|
|
9
|
-
import { normalizeKeyboardEvent } from './_keyboard-event'
|
|
10
|
-
import { isModifierKey, parseHotkey } from './parse'
|
|
11
|
-
import { validateHotkey } from './validate'
|
|
12
|
-
import { detectPlatform } from './platform'
|
|
13
|
-
import { shouldIgnoreInputEvent } from './_event-target'
|
|
14
|
-
import type {
|
|
15
|
-
HotkeySequenceRecorderValidationContext,
|
|
16
|
-
RecorderOptions,
|
|
17
|
-
} from './recorder-options'
|
|
18
|
-
import type { HotkeySequence } from './sequence-manager'
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* How the user can commit a recorded sequence from the keyboard.
|
|
22
|
-
* - `'enter'`: plain Enter (no modifiers) commits when at least one step exists.
|
|
23
|
-
* - `'none'`: only {@link HotkeySequenceRecorder.commit} finishes recording (or idle timeout if set).
|
|
24
|
-
*/
|
|
25
|
-
export type HotkeySequenceRecorderCommitKeys = 'enter' | 'none'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* State interface for the HotkeySequenceRecorder.
|
|
29
|
-
*/
|
|
30
|
-
export interface HotkeySequenceRecorderState {
|
|
31
|
-
/** Whether recording is currently active */
|
|
32
|
-
isRecording: boolean
|
|
33
|
-
/** Chords captured so far in the current recording session */
|
|
34
|
-
steps: HotkeySequence
|
|
35
|
-
/** The last successfully committed sequence, or null if none / after starting a new session */
|
|
36
|
-
recordedSequence: HotkeySequence | null
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Options for configuring a HotkeySequenceRecorder instance.
|
|
41
|
-
*/
|
|
42
|
-
export interface HotkeySequenceRecorderOptions extends RecorderOptions {
|
|
43
|
-
/** Validate the completed sequence. Rejection preserves the steps for editing. */
|
|
44
|
-
validate?: (
|
|
45
|
-
sequence: HotkeySequence,
|
|
46
|
-
context: HotkeySequenceRecorderValidationContext,
|
|
47
|
-
) => boolean | string
|
|
48
|
-
/** Callback when a sequence is successfully recorded */
|
|
49
|
-
onRecord: (sequence: HotkeySequence) => void
|
|
50
|
-
/** Optional callback when recording is cancelled (Escape pressed) */
|
|
51
|
-
onCancel?: () => void
|
|
52
|
-
/** Optional callback when the sequence is cleared (Backspace/Delete with no steps) */
|
|
53
|
-
onClear?: () => void
|
|
54
|
-
/**
|
|
55
|
-
* Whether plain Enter commits the current steps. Ignored when {@link commitKeys} is `'none'`.
|
|
56
|
-
* @default true
|
|
57
|
-
*/
|
|
58
|
-
commitOnEnter?: boolean
|
|
59
|
-
/**
|
|
60
|
-
* Keyboard commit mode. When `'none'`, use {@link HotkeySequenceRecorder.commit} (and optional idle timeout).
|
|
61
|
-
* @default 'enter'
|
|
62
|
-
*/
|
|
63
|
-
commitKeys?: HotkeySequenceRecorderCommitKeys
|
|
64
|
-
/**
|
|
65
|
-
* Milliseconds of inactivity after the **last completed chord** before auto-committing.
|
|
66
|
-
* The timer does not run while waiting for the first chord (`steps.length === 0`).
|
|
67
|
-
*/
|
|
68
|
-
idleTimeoutMs?: number
|
|
69
|
-
/**
|
|
70
|
-
* Whether to ignore keyboard events from input-like elements (text inputs,
|
|
71
|
-
* textarea, select, contenteditable). When true, typing in inputs passes
|
|
72
|
-
* through normally instead of being captured as a sequence recording.
|
|
73
|
-
* Escape always works regardless of this setting.
|
|
74
|
-
* @default true
|
|
75
|
-
*/
|
|
76
|
-
ignoreInputs?: boolean
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const defaultHotkeySequenceRecorderOptions: Pick<
|
|
80
|
-
HotkeySequenceRecorderOptions,
|
|
81
|
-
'commitOnEnter' | 'commitKeys'
|
|
82
|
-
> = {
|
|
83
|
-
commitOnEnter: true,
|
|
84
|
-
commitKeys: 'enter',
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Resolves the Enter policy, with commitKeys: none overriding the legacy boolean. */
|
|
88
|
-
function resolvedCommitOnEnter(
|
|
89
|
-
options: HotkeySequenceRecorderOptions,
|
|
90
|
-
): boolean {
|
|
91
|
-
if (options.commitKeys === 'none') {
|
|
92
|
-
return false
|
|
93
|
-
}
|
|
94
|
-
return options.commitOnEnter !== false
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Framework-agnostic class for recording multi-chord sequences (Vim-style shortcuts).
|
|
99
|
-
*
|
|
100
|
-
* Each step is captured like a single hotkey chord. Press **Enter** (no modifiers) to commit
|
|
101
|
-
* when {@link HotkeySequenceRecorderOptions.commitKeys} is `'enter'` (default), **Escape** to cancel,
|
|
102
|
-
* **Backspace/Delete** to remove the last step or clear when empty.
|
|
103
|
-
*/
|
|
104
|
-
export class HotkeySequenceRecorder {
|
|
105
|
-
readonly store: Store<HotkeySequenceRecorderState> =
|
|
106
|
-
new Store<HotkeySequenceRecorderState>({
|
|
107
|
-
isRecording: false,
|
|
108
|
-
steps: [],
|
|
109
|
-
recordedSequence: null,
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
#keydownHandler: ((event: KeyboardEvent) => void) | null = null
|
|
113
|
-
#options: HotkeySequenceRecorderOptions
|
|
114
|
-
#platform: 'mac' | 'windows' | 'linux'
|
|
115
|
-
#events: Array<KeyboardEvent> = []
|
|
116
|
-
#idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
117
|
-
|
|
118
|
-
constructor(options: HotkeySequenceRecorderOptions) {
|
|
119
|
-
this.#options = {
|
|
120
|
-
...defaultHotkeySequenceRecorderOptions,
|
|
121
|
-
...options,
|
|
122
|
-
}
|
|
123
|
-
this.#platform = detectPlatform()
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Merges current callbacks and options without discarding recorded steps. */
|
|
127
|
-
setOptions(options: Partial<HotkeySequenceRecorderOptions>): void {
|
|
128
|
-
this.#options = {
|
|
129
|
-
...defaultHotkeySequenceRecorderOptions,
|
|
130
|
-
...this.#options,
|
|
131
|
-
...options,
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Cancels a pending idle commit so it cannot fire after editing or stopping. */
|
|
136
|
-
#clearIdleTimer(): void {
|
|
137
|
-
if (this.#idleTimer !== null) {
|
|
138
|
-
clearTimeout(this.#idleTimer)
|
|
139
|
-
this.#idleTimer = null
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Restarts optional idle commit after a step is recorded or edited. */
|
|
144
|
-
#scheduleIdleTimer(): void {
|
|
145
|
-
this.#clearIdleTimer()
|
|
146
|
-
const ms = this.#options.idleTimeoutMs
|
|
147
|
-
if (ms === undefined || ms <= 0) {
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
const steps = this.store.state.steps
|
|
151
|
-
if (steps.length === 0) {
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
this.#idleTimer = setTimeout(() => {
|
|
155
|
-
this.#idleTimer = null
|
|
156
|
-
if (!this.#keydownHandler) {
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
if (this.store.state.steps.length >= 1) {
|
|
160
|
-
this.commit()
|
|
161
|
-
}
|
|
162
|
-
}, ms)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Publishes edited steps while preserving the remaining recorder state. */
|
|
166
|
-
#patchSteps(updater: (prev: HotkeySequence) => HotkeySequence): void {
|
|
167
|
-
this.store.setState((s) => ({
|
|
168
|
-
...s,
|
|
169
|
-
steps: updater(s.steps),
|
|
170
|
-
}))
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Starts a fresh recording; repeated starts during an active session are ignored. */
|
|
174
|
-
start(): void {
|
|
175
|
-
if (this.#keydownHandler) {
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
this.#clearIdleTimer()
|
|
180
|
-
this.#events = []
|
|
181
|
-
beginRecording(this)
|
|
182
|
-
this.store.setState(() => ({
|
|
183
|
-
isRecording: true,
|
|
184
|
-
steps: [],
|
|
185
|
-
recordedSequence: null,
|
|
186
|
-
}))
|
|
187
|
-
|
|
188
|
-
const handler = (event: KeyboardEvent) => {
|
|
189
|
-
if (!this.#keydownHandler) {
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// If ignoreInputs is enabled (default) and focus is in an input element,
|
|
194
|
-
// let the event pass through so the user can type normally.
|
|
195
|
-
// Escape is the exception — it should always cancel recording.
|
|
196
|
-
if (
|
|
197
|
-
this.#options.ignoreInputs !== false &&
|
|
198
|
-
event.key !== 'Escape' &&
|
|
199
|
-
shouldIgnoreInputEvent(event, document, document)
|
|
200
|
-
)
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
const platform = this.#options.platform ?? this.#platform
|
|
204
|
-
if (normalizeKeyboardEvent(event, platform).isComposing) return
|
|
205
|
-
captureRecordingEvent(event)
|
|
206
|
-
event.preventDefault()
|
|
207
|
-
event.stopPropagation()
|
|
208
|
-
if (event.repeat) return
|
|
209
|
-
|
|
210
|
-
if (event.key === 'Escape') {
|
|
211
|
-
this.cancel()
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
216
|
-
if (
|
|
217
|
-
!event.ctrlKey &&
|
|
218
|
-
!event.shiftKey &&
|
|
219
|
-
!event.altKey &&
|
|
220
|
-
!event.metaKey
|
|
221
|
-
) {
|
|
222
|
-
const steps = this.store.state.steps
|
|
223
|
-
if (steps.length === 0) {
|
|
224
|
-
this.stop()
|
|
225
|
-
this.#options.onClear?.()
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
this.#events.pop()
|
|
229
|
-
this.#patchSteps((prev) => prev.slice(0, -1))
|
|
230
|
-
const next = this.store.state.steps
|
|
231
|
-
if (next.length === 0) {
|
|
232
|
-
this.#clearIdleTimer()
|
|
233
|
-
} else {
|
|
234
|
-
this.#scheduleIdleTimer()
|
|
235
|
-
}
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const enterCommits =
|
|
241
|
-
resolvedCommitOnEnter(this.#options) &&
|
|
242
|
-
event.key === 'Enter' &&
|
|
243
|
-
!event.ctrlKey &&
|
|
244
|
-
!event.shiftKey &&
|
|
245
|
-
!event.altKey &&
|
|
246
|
-
!event.metaKey
|
|
247
|
-
|
|
248
|
-
if (enterCommits && this.store.state.steps.length >= 1) {
|
|
249
|
-
this.commit()
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (enterCommits && this.store.state.steps.length === 0) {
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (isModifierKey(event.key) || event.key === 'AltGraph') return
|
|
258
|
-
const rejection = chordRejection(event, { ...this.#options, platform })
|
|
259
|
-
if (rejection) {
|
|
260
|
-
this.#options.onReject?.(rejection)
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
const finalHotkey = hotkeyChordFromKeydown(
|
|
264
|
-
event,
|
|
265
|
-
platform,
|
|
266
|
-
this.#options.recordBy,
|
|
267
|
-
)
|
|
268
|
-
if (finalHotkey === null) {
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const validation = validateHotkey(finalHotkey)
|
|
273
|
-
if (!validation.valid) {
|
|
274
|
-
this.#options.onReject?.({
|
|
275
|
-
reason: 'invalid',
|
|
276
|
-
message: validation.errors.join('; '),
|
|
277
|
-
hotkey: finalHotkey,
|
|
278
|
-
})
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
this.#events.push(event)
|
|
282
|
-
this.#patchSteps((prev) => [...prev, finalHotkey])
|
|
283
|
-
this.#scheduleIdleTimer()
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
this.#keydownHandler = handler
|
|
287
|
-
this.#addListener(handler)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Commit the current steps as a sequence. No-op if fewer than one step.
|
|
292
|
-
*/
|
|
293
|
-
commit(): void {
|
|
294
|
-
const steps = this.store.state.steps
|
|
295
|
-
if (steps.length < 1) {
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const sequence = [...steps]
|
|
300
|
-
const platform = this.#options.platform ?? this.#platform
|
|
301
|
-
this.#clearIdleTimer()
|
|
302
|
-
const accepted = this.#options.validate?.(sequence, {
|
|
303
|
-
events: [...this.#events],
|
|
304
|
-
parsedSequence: sequence.map((step) => parseHotkey(step, platform)),
|
|
305
|
-
})
|
|
306
|
-
if (accepted !== undefined && accepted !== true) {
|
|
307
|
-
this.#options.onReject?.({
|
|
308
|
-
reason: 'validation',
|
|
309
|
-
message:
|
|
310
|
-
typeof accepted === 'string'
|
|
311
|
-
? accepted
|
|
312
|
-
: 'This sequence is not allowed.',
|
|
313
|
-
sequence,
|
|
314
|
-
})
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
if (this.#options.detectConflicts) {
|
|
318
|
-
const conflicts = findHotkeyConflicts(sequence, {
|
|
319
|
-
...(typeof this.#options.detectConflicts === 'object'
|
|
320
|
-
? this.#options.detectConflicts
|
|
321
|
-
: {}),
|
|
322
|
-
platform,
|
|
323
|
-
events: this.#events,
|
|
324
|
-
})
|
|
325
|
-
if (conflicts.length) {
|
|
326
|
-
this.#options.onReject?.({
|
|
327
|
-
reason: 'conflict',
|
|
328
|
-
message: 'This sequence conflicts with a registered binding.',
|
|
329
|
-
sequence,
|
|
330
|
-
conflicts,
|
|
331
|
-
})
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
endRecording(this)
|
|
336
|
-
this.#events = []
|
|
337
|
-
|
|
338
|
-
if (this.#keydownHandler) {
|
|
339
|
-
this.#removeListener(this.#keydownHandler)
|
|
340
|
-
this.#keydownHandler = null
|
|
341
|
-
}
|
|
342
|
-
this.#clearIdleTimer()
|
|
343
|
-
|
|
344
|
-
this.store.setState(() => ({
|
|
345
|
-
isRecording: false,
|
|
346
|
-
steps: [],
|
|
347
|
-
recordedSequence: sequence,
|
|
348
|
-
}))
|
|
349
|
-
|
|
350
|
-
this.#options.onRecord(sequence)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Stops and discards in-progress steps without invoking onCancel. */
|
|
354
|
-
stop(): void {
|
|
355
|
-
endRecording(this)
|
|
356
|
-
this.#events = []
|
|
357
|
-
if (this.#keydownHandler) {
|
|
358
|
-
this.#removeListener(this.#keydownHandler)
|
|
359
|
-
this.#keydownHandler = null
|
|
360
|
-
}
|
|
361
|
-
this.#clearIdleTimer()
|
|
362
|
-
this.store.setState(() => ({
|
|
363
|
-
isRecording: false,
|
|
364
|
-
steps: [],
|
|
365
|
-
recordedSequence: null,
|
|
366
|
-
}))
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Stops, discards in-progress steps, and notifies onCancel. */
|
|
370
|
-
cancel(): void {
|
|
371
|
-
endRecording(this)
|
|
372
|
-
this.#events = []
|
|
373
|
-
if (this.#keydownHandler) {
|
|
374
|
-
this.#removeListener(this.#keydownHandler)
|
|
375
|
-
this.#keydownHandler = null
|
|
376
|
-
}
|
|
377
|
-
this.#clearIdleTimer()
|
|
378
|
-
this.store.setState(() => ({
|
|
379
|
-
isRecording: false,
|
|
380
|
-
steps: [],
|
|
381
|
-
recordedSequence: null,
|
|
382
|
-
}))
|
|
383
|
-
this.#options.onCancel?.()
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/** Captures keydowns before application handlers, with an SSR-safe document check. */
|
|
387
|
-
#addListener(handler: (event: KeyboardEvent) => void): void {
|
|
388
|
-
if (typeof document === 'undefined') {
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
document.addEventListener('keydown', handler, true)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/** Detaches the capture listener when recording ends. */
|
|
395
|
-
#removeListener(handler: (event: KeyboardEvent) => void): void {
|
|
396
|
-
if (typeof document === 'undefined') {
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
document.removeEventListener('keydown', handler, true)
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
destroy(): void {
|
|
403
|
-
this.stop()
|
|
404
|
-
this.store.setState(() => ({
|
|
405
|
-
isRecording: false,
|
|
406
|
-
steps: [],
|
|
407
|
-
recordedSequence: null,
|
|
408
|
-
}))
|
|
409
|
-
}
|
|
410
|
-
}
|
package/src/hotkey.types.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import type { CanonicalModifier, Key, LogicalKey } from './key.types'
|
|
2
|
-
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Hotkey Types
|
|
5
|
-
// =============================================================================
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Single modifier + key combinations.
|
|
9
|
-
* Uses canonical modifiers (4) + Mod (1) = 5 modifiers.
|
|
10
|
-
*
|
|
11
|
-
* The `Mod` modifier is platform-adaptive:
|
|
12
|
-
* - **macOS**: Resolves to `Meta` (Command key ⌘)
|
|
13
|
-
* - **Windows/Linux**: Resolves to `Control` (Ctrl key)
|
|
14
|
-
*
|
|
15
|
-
* This enables cross-platform hotkey definitions that automatically adapt to the platform.
|
|
16
|
-
* For example, `Mod+S` becomes `Command+S` on Mac and `Ctrl+S` on Windows/Linux.
|
|
17
|
-
*/
|
|
18
|
-
export type SingleModifierHotkey =
|
|
19
|
-
| `Control+${Key}`
|
|
20
|
-
| `Alt+${Key}`
|
|
21
|
-
| `Shift+${Key}`
|
|
22
|
-
| `Meta+${Key}`
|
|
23
|
-
| `Mod+${Key}`
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Two modifier + key combinations.
|
|
27
|
-
*
|
|
28
|
-
* **Platform-adaptive `Mod` combinations:**
|
|
29
|
-
* - `Mod+Alt` and `Mod+Shift` are included (safe on all platforms)
|
|
30
|
-
* - `Mod+Control` and `Mod+Meta` are excluded because they create duplicate modifiers:
|
|
31
|
-
* - `Mod+Control` duplicates `Control` on Windows/Linux (Mod = Control)
|
|
32
|
-
* - `Mod+Meta` duplicates `Meta` on macOS (Mod = Meta)
|
|
33
|
-
*/
|
|
34
|
-
export type TwoModifierHotkey =
|
|
35
|
-
| `Control+Alt+${Key}`
|
|
36
|
-
| `Control+Shift+${Key}`
|
|
37
|
-
| `Control+Meta+${Key}`
|
|
38
|
-
| `Alt+Shift+${Key}`
|
|
39
|
-
| `Alt+Meta+${Key}`
|
|
40
|
-
| `Shift+Meta+${Key}`
|
|
41
|
-
| `Mod+Alt+${Key}`
|
|
42
|
-
| `Mod+Shift+${Key}`
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Three modifier + key combinations.
|
|
46
|
-
*
|
|
47
|
-
* **Platform-adaptive `Mod` combinations:**
|
|
48
|
-
* - `Mod+Alt+Shift` is included (safe on all platforms)
|
|
49
|
-
* - `Mod+Control+Shift` and `Mod+Shift+Meta` are excluded because they create duplicate modifiers:
|
|
50
|
-
* - `Mod+Control+Shift` duplicates `Control` on Windows/Linux (Mod = Control)
|
|
51
|
-
* - `Mod+Shift+Meta` duplicates `Meta` on macOS (Mod = Meta)
|
|
52
|
-
*/
|
|
53
|
-
export type ThreeModifierHotkey =
|
|
54
|
-
| `Control+Alt+Shift+${Key}`
|
|
55
|
-
| `Control+Alt+Meta+${Key}`
|
|
56
|
-
| `Control+Shift+Meta+${Key}`
|
|
57
|
-
| `Alt+Shift+Meta+${Key}`
|
|
58
|
-
| `Mod+Alt+Shift+${Key}`
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Four modifier + key combinations.
|
|
62
|
-
*
|
|
63
|
-
* Only the canonical `Control+Alt+Shift+Meta` combination is included.
|
|
64
|
-
*
|
|
65
|
-
* **Why no `Mod` combinations?**
|
|
66
|
-
* Since `Mod` resolves to either `Control` (Windows/Linux) or `Meta` (macOS), any
|
|
67
|
-
* four-modifier combination with `Mod` would create duplicate modifiers on one platform.
|
|
68
|
-
* For example:
|
|
69
|
-
* - `Mod+Control+Alt+Shift` → duplicates `Control` on Windows/Linux
|
|
70
|
-
* - `Mod+Alt+Shift+Meta` → duplicates `Meta` on macOS
|
|
71
|
-
*/
|
|
72
|
-
export type FourModifierHotkey = `Control+Alt+Shift+Meta+${Key}`
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* A type-safe hotkey string.
|
|
76
|
-
*
|
|
77
|
-
* Provides autocomplete for:
|
|
78
|
-
* - All single keys (letters, numbers, function keys, navigation, editing, punctuation)
|
|
79
|
-
* - Single modifier + common key (Control+S, Mod+A, Mod+/, etc.)
|
|
80
|
-
* - Two modifiers + common key (Mod+Shift+S, Control+Alt+A, etc.)
|
|
81
|
-
* - Three modifiers + common key (Control+Alt+Shift+A, Mod+Alt+Shift+S, etc.)
|
|
82
|
-
* - Four modifiers + common key (Control+Alt+Shift+Meta+A, etc.)
|
|
83
|
-
*
|
|
84
|
-
* ## Modifier Names
|
|
85
|
-
*
|
|
86
|
-
* Use canonical modifier names:
|
|
87
|
-
* - `Control` (not Ctrl) - The Control key
|
|
88
|
-
* - `Alt` (not Option) - The Alt key (Option on macOS)
|
|
89
|
-
* - `Meta` (not Command/Cmd) - The Meta/Command key (macOS only)
|
|
90
|
-
* - `Shift` - The Shift key
|
|
91
|
-
*
|
|
92
|
-
* ## Platform-Adaptive `Mod` Modifier
|
|
93
|
-
*
|
|
94
|
-
* The `Mod` modifier is a special platform-adaptive modifier that automatically resolves
|
|
95
|
-
* to the "primary modifier" on each platform:
|
|
96
|
-
*
|
|
97
|
-
* - **macOS**: `Mod` → `Meta` (Command key ⌘)
|
|
98
|
-
* - **Windows/Linux**: `Mod` → `Control` (Ctrl key)
|
|
99
|
-
*
|
|
100
|
-
* This enables cross-platform hotkey definitions that work correctly on all platforms
|
|
101
|
-
* without platform-specific code. The `Mod` modifier is resolved at runtime based on
|
|
102
|
-
* the detected platform.
|
|
103
|
-
*
|
|
104
|
-
* **When to use `Mod` vs platform-specific modifiers:**
|
|
105
|
-
* - Use `Mod` for cross-platform shortcuts (e.g., `Mod+S` for save)
|
|
106
|
-
* - Use `Meta` or `Control` when you need platform-specific behavior
|
|
107
|
-
* - Use `Mod` when you want your shortcuts to follow platform conventions automatically
|
|
108
|
-
*
|
|
109
|
-
* **Limitations:**
|
|
110
|
-
* - `Mod+Control` and `Mod+Meta` combinations are not allowed (they create duplicate
|
|
111
|
-
* modifiers on one platform)
|
|
112
|
-
* - In four-modifier combinations, only canonical modifiers are allowed (no `Mod`)
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* ```ts
|
|
116
|
-
* // Cross-platform shortcuts (recommended)
|
|
117
|
-
* const save: Hotkey = 'Mod+S' // Command+S on Mac, Ctrl+S on Windows/Linux
|
|
118
|
-
* const saveAs: Hotkey = 'Mod+Shift+S' // Command+Shift+S on Mac, Ctrl+Shift+S elsewhere
|
|
119
|
-
* const comment: Hotkey = 'Mod+/' // Command+/ on Mac, Ctrl+/ elsewhere
|
|
120
|
-
*
|
|
121
|
-
* // Platform-specific shortcuts
|
|
122
|
-
* const macOnly: Hotkey = 'Meta+S' // Command+S on Mac only
|
|
123
|
-
* const windowsOnly: Hotkey = 'Control+S' // Ctrl+S on Windows/Linux only
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
export type Hotkey =
|
|
127
|
-
| Key
|
|
128
|
-
| SingleModifierHotkey
|
|
129
|
-
| TwoModifierHotkey
|
|
130
|
-
| ThreeModifierHotkey
|
|
131
|
-
| FourModifierHotkey
|
|
132
|
-
|
|
133
|
-
/** Resolved modifier flags shared by both parsed binding variants. */
|
|
134
|
-
export interface ParsedModifiers {
|
|
135
|
-
/** Whether Control is required. */
|
|
136
|
-
ctrl: boolean
|
|
137
|
-
/** Whether Shift is required. */
|
|
138
|
-
shift: boolean
|
|
139
|
-
/** Whether Alt is required. */
|
|
140
|
-
alt: boolean
|
|
141
|
-
/** Whether Meta (Command) is required. */
|
|
142
|
-
meta: boolean
|
|
143
|
-
/** Required modifiers in canonical order. */
|
|
144
|
-
modifiers: Array<CanonicalModifier>
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* A parsed binding matches either a logical key or a physical code, never both.
|
|
149
|
-
* Check `code !== undefined` to narrow to a physical binding. Its logical
|
|
150
|
-
* character is unknown without a keyboard event and is not stored in `key`.
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* parseHotkey('Alt+S') // { key: 'S', alt: true, ... }
|
|
154
|
-
* parseHotkey('Alt+[KeyS]') // { code: 'KeyS', alt: true, ... } — no key property
|
|
155
|
-
*/
|
|
156
|
-
export type ParsedHotkey = ParsedModifiers &
|
|
157
|
-
(
|
|
158
|
-
| { key: LogicalKey | (string & {}); code?: never }
|
|
159
|
-
| { code: string; key?: never }
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* A raw hotkey object for programmatic registration.
|
|
164
|
-
*
|
|
165
|
-
* Like `ParsedHotkey` but without `modifiers` (derived from booleans)
|
|
166
|
-
* and with optional modifier booleans (default to `false` when omitted).
|
|
167
|
-
* Use with `HotkeyManager.register()` and `useHotkey()` when you prefer
|
|
168
|
-
* object form over a string.
|
|
169
|
-
*
|
|
170
|
-
* The `mod` modifier is platform-adaptive: Command on macOS, Control on Windows/Linux.
|
|
171
|
-
* Pass `platform` when converting to ParsedHotkey (e.g., via `options.platform`).
|
|
172
|
-
*
|
|
173
|
-
* @example
|
|
174
|
-
* ```ts
|
|
175
|
-
* useHotkey({ key: 'S', mod: true }, handler) // Mod+S (cross-platform)
|
|
176
|
-
* useHotkey({ key: 'S', ctrl: true }, handler) // Control+S
|
|
177
|
-
* useHotkey({ key: 'Escape' }, handler) // Escape (no modifiers)
|
|
178
|
-
* useHotkey({ key: 'A', shift: true, meta: true }, handler) // Shift+Meta+A
|
|
179
|
-
* useHotkey({ key: 'S', mod: true, shift: true }, handler) // Mod+Shift+S
|
|
180
|
-
* useHotkey({ code: 'KeyW', mod: true }, handler) // Physical Mod+KeyW
|
|
181
|
-
* useHotkey({ code: 'NumpadAdd', mod: true }, handler) // Physical numpad plus
|
|
182
|
-
* ```
|
|
183
|
-
*/
|
|
184
|
-
/** Modifier flags shared by logical-key and physical-code registrations. */
|
|
185
|
-
export interface RawModifiers {
|
|
186
|
-
/** The non-modifier key (e.g., 'S', 'Escape', 'F1'). */
|
|
187
|
-
/** Platform-adaptive modifier: Command on macOS, Control on Windows/Linux. Defaults to false. */
|
|
188
|
-
mod?: boolean
|
|
189
|
-
/** Whether the Control key is required. Defaults to false. */
|
|
190
|
-
ctrl?: boolean
|
|
191
|
-
/** Whether the Shift key is required. Defaults to false. */
|
|
192
|
-
shift?: boolean
|
|
193
|
-
/** Whether the Alt key is required. Defaults to false. */
|
|
194
|
-
alt?: boolean
|
|
195
|
-
/** Whether the Meta (Command) key is required. Defaults to false. */
|
|
196
|
-
meta?: boolean
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export type RawHotkey =
|
|
200
|
-
| ({ key: LogicalKey | (string & {}); code?: never } & RawModifiers)
|
|
201
|
-
| ({ code: string; key?: never } & RawModifiers)
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* A hotkey that can be passed to `HotkeyManager.register()` and `useHotkey()`.
|
|
205
|
-
* Either a type-safe string (`Hotkey`) or a raw object (`RawHotkey`).
|
|
206
|
-
*/
|
|
207
|
-
export type RegisterableHotkey = Hotkey | RawHotkey
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Context passed to hotkey callbacks along with the keyboard event.
|
|
211
|
-
*/
|
|
212
|
-
export interface HotkeyCallbackContext {
|
|
213
|
-
/** The original hotkey string that was registered */
|
|
214
|
-
hotkey: Hotkey
|
|
215
|
-
/** The parsed representation of the hotkey */
|
|
216
|
-
parsedHotkey: ParsedHotkey
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Callback function type for hotkey handlers.
|
|
221
|
-
*
|
|
222
|
-
* @param event - The keyboard event that triggered the hotkey
|
|
223
|
-
* @param context - Additional context including the hotkey and parsed hotkey
|
|
224
|
-
*
|
|
225
|
-
* @example
|
|
226
|
-
* ```ts
|
|
227
|
-
* const handler: HotkeyCallback = (event, { hotkey, parsedHotkey }) => {
|
|
228
|
-
* console.log(`Hotkey ${hotkey} was pressed`)
|
|
229
|
-
* console.log(`Modifiers:`, parsedHotkey.modifiers)
|
|
230
|
-
* }
|
|
231
|
-
* ```
|
|
232
|
-
*/
|
|
233
|
-
export type HotkeyCallback = (
|
|
234
|
-
event: KeyboardEvent,
|
|
235
|
-
context: HotkeyCallbackContext,
|
|
236
|
-
) => void
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Metadata for hotkey and sequence registrations.
|
|
240
|
-
* Includes `name`, `description`, and `group` by default. Extend via declaration merging:
|
|
241
|
-
*
|
|
242
|
-
* @example
|
|
243
|
-
* ```ts
|
|
244
|
-
* declare module '@tanstack/hotkeys' {
|
|
245
|
-
* interface HotkeyMeta {
|
|
246
|
-
* category?: string
|
|
247
|
-
* icon?: string
|
|
248
|
-
* }
|
|
249
|
-
* }
|
|
250
|
-
* ```
|
|
251
|
-
*/
|
|
252
|
-
export interface HotkeyMeta {
|
|
253
|
-
/** Human-readable name for this hotkey (e.g., "Save Document") */
|
|
254
|
-
name?: string
|
|
255
|
-
/** Description of what this hotkey does */
|
|
256
|
-
description?: string
|
|
257
|
-
/** Optional display group. Does not affect matching or scope. */
|
|
258
|
-
group?: string
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Behavior when registering a hotkey/sequence that conflicts with an existing registration.
|
|
263
|
-
*
|
|
264
|
-
* - `'warn'` - Log a warning to the console but allow both registrations (default)
|
|
265
|
-
* - `'error'` - Throw an error and prevent the new registration
|
|
266
|
-
* - `'replace'` - Unregister the existing registration and register the new one
|
|
267
|
-
* - `'allow'` - Allow multiple registrations without warning
|
|
268
|
-
*/
|
|
269
|
-
export type ConflictBehavior = 'warn' | 'error' | 'replace' | 'allow'
|
package/src/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export * from './constants'
|
|
2
|
-
export * from './display-labels'
|
|
3
|
-
export * from './platform'
|
|
4
|
-
export * from './key.types'
|
|
5
|
-
export * from './hotkey.types'
|
|
6
|
-
export * from './format'
|
|
7
|
-
export * from './parse'
|
|
8
|
-
export * from './validate'
|
|
9
|
-
export * from './match'
|
|
10
|
-
export * from './hint'
|
|
11
|
-
export * from './conflicts'
|
|
12
|
-
export * from './hotkey-manager'
|
|
13
|
-
export * from './sequence-manager'
|
|
14
|
-
export * from './key-state-tracker'
|
|
15
|
-
export * from './recorder-options'
|
|
16
|
-
export * from './hotkey-recorder'
|
|
17
|
-
export * from './hotkey-sequence-recorder'
|