@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.
Files changed (148) hide show
  1. package/dist/_event-target.js +1 -2
  2. package/dist/_keyboard-event.js +1 -2
  3. package/dist/_match.js +1 -2
  4. package/dist/_named-keys.d.ts +3 -5
  5. package/dist/_named-keys.js +1 -2
  6. package/dist/_recorder-chord.js +1 -2
  7. package/dist/_recording-guard.js +1 -2
  8. package/dist/_registration.js +1 -2
  9. package/dist/conflicts.d.ts +4 -7
  10. package/dist/conflicts.js +1 -2
  11. package/dist/constants.d.ts +14 -17
  12. package/dist/constants.js +8 -9
  13. package/dist/display-labels.d.ts +7 -10
  14. package/dist/display-labels.js +1 -2
  15. package/dist/format.d.ts +12 -14
  16. package/dist/format.js +17 -7
  17. package/dist/hint.d.ts +3 -6
  18. package/dist/hint.js +1 -2
  19. package/dist/hotkey-manager.d.ts +8 -10
  20. package/dist/hotkey-manager.js +21 -3
  21. package/dist/hotkey-recorder.d.ts +4 -7
  22. package/dist/hotkey-recorder.js +9 -6
  23. package/dist/hotkey-sequence-recorder.d.ts +5 -8
  24. package/dist/hotkey-sequence-recorder.js +6 -7
  25. package/dist/hotkey.types.d.ts +15 -18
  26. package/dist/key-state-tracker.d.ts +4 -7
  27. package/dist/key-state-tracker.js +6 -3
  28. package/dist/key.types.d.ts +17 -20
  29. package/dist/match.d.ts +8 -11
  30. package/dist/match.js +1 -2
  31. package/dist/parse.d.ts +11 -14
  32. package/dist/parse.js +5 -4
  33. package/dist/platform.d.ts +3 -6
  34. package/dist/platform.js +1 -2
  35. package/dist/recorder-options.d.ts +6 -9
  36. package/dist/sequence-manager.d.ts +11 -14
  37. package/dist/sequence-manager.js +12 -7
  38. package/dist/validate.d.ts +5 -8
  39. package/dist/validate.js +1 -2
  40. package/package.json +8 -13
  41. package/dist/_event-target.cjs +0 -73
  42. package/dist/_event-target.cjs.map +0 -1
  43. package/dist/_event-target.js.map +0 -1
  44. package/dist/_keyboard-event.cjs +0 -83
  45. package/dist/_keyboard-event.cjs.map +0 -1
  46. package/dist/_keyboard-event.js.map +0 -1
  47. package/dist/_match.cjs +0 -49
  48. package/dist/_match.cjs.map +0 -1
  49. package/dist/_match.js.map +0 -1
  50. package/dist/_named-keys.cjs +0 -68
  51. package/dist/_named-keys.cjs.map +0 -1
  52. package/dist/_named-keys.d.cts +0 -8
  53. package/dist/_named-keys.js.map +0 -1
  54. package/dist/_recorder-chord.cjs +0 -44
  55. package/dist/_recorder-chord.cjs.map +0 -1
  56. package/dist/_recorder-chord.js.map +0 -1
  57. package/dist/_recording-guard.cjs +0 -59
  58. package/dist/_recording-guard.cjs.map +0 -1
  59. package/dist/_recording-guard.js.map +0 -1
  60. package/dist/_registration.cjs +0 -54
  61. package/dist/_registration.cjs.map +0 -1
  62. package/dist/_registration.js.map +0 -1
  63. package/dist/conflicts.cjs +0 -55
  64. package/dist/conflicts.cjs.map +0 -1
  65. package/dist/conflicts.d.cts +0 -39
  66. package/dist/conflicts.js.map +0 -1
  67. package/dist/constants.cjs +0 -410
  68. package/dist/constants.cjs.map +0 -1
  69. package/dist/constants.d.cts +0 -165
  70. package/dist/constants.js.map +0 -1
  71. package/dist/display-labels.cjs +0 -119
  72. package/dist/display-labels.cjs.map +0 -1
  73. package/dist/display-labels.d.cts +0 -93
  74. package/dist/display-labels.js.map +0 -1
  75. package/dist/format.cjs +0 -85
  76. package/dist/format.cjs.map +0 -1
  77. package/dist/format.d.cts +0 -99
  78. package/dist/format.js.map +0 -1
  79. package/dist/hint.cjs +0 -24
  80. package/dist/hint.cjs.map +0 -1
  81. package/dist/hint.d.cts +0 -23
  82. package/dist/hint.js.map +0 -1
  83. package/dist/hotkey-manager.cjs +0 -414
  84. package/dist/hotkey-manager.cjs.map +0 -1
  85. package/dist/hotkey-manager.d.cts +0 -234
  86. package/dist/hotkey-manager.js.map +0 -1
  87. package/dist/hotkey-recorder.cjs +0 -235
  88. package/dist/hotkey-recorder.cjs.map +0 -1
  89. package/dist/hotkey-recorder.d.cts +0 -119
  90. package/dist/hotkey-recorder.js.map +0 -1
  91. package/dist/hotkey-sequence-recorder.cjs +0 -255
  92. package/dist/hotkey-sequence-recorder.cjs.map +0 -1
  93. package/dist/hotkey-sequence-recorder.d.cts +0 -86
  94. package/dist/hotkey-sequence-recorder.js.map +0 -1
  95. package/dist/hotkey.types.d.cts +0 -236
  96. package/dist/index.cjs +0 -71
  97. package/dist/index.d.cts +0 -18
  98. package/dist/key-state-tracker.cjs +0 -197
  99. package/dist/key-state-tracker.cjs.map +0 -1
  100. package/dist/key-state-tracker.d.cts +0 -107
  101. package/dist/key-state-tracker.js.map +0 -1
  102. package/dist/key.types.d.cts +0 -82
  103. package/dist/match.cjs +0 -144
  104. package/dist/match.cjs.map +0 -1
  105. package/dist/match.d.cts +0 -101
  106. package/dist/match.js.map +0 -1
  107. package/dist/parse.cjs +0 -245
  108. package/dist/parse.cjs.map +0 -1
  109. package/dist/parse.d.cts +0 -134
  110. package/dist/parse.js.map +0 -1
  111. package/dist/platform.cjs +0 -55
  112. package/dist/platform.cjs.map +0 -1
  113. package/dist/platform.d.cts +0 -44
  114. package/dist/platform.js.map +0 -1
  115. package/dist/recorder-options.d.cts +0 -34
  116. package/dist/sequence-manager.cjs +0 -441
  117. package/dist/sequence-manager.cjs.map +0 -1
  118. package/dist/sequence-manager.d.cts +0 -172
  119. package/dist/sequence-manager.js.map +0 -1
  120. package/dist/validate.cjs +0 -112
  121. package/dist/validate.cjs.map +0 -1
  122. package/dist/validate.d.cts +0 -67
  123. package/dist/validate.js.map +0 -1
  124. package/src/_event-target.ts +0 -157
  125. package/src/_keyboard-event.ts +0 -101
  126. package/src/_match.ts +0 -96
  127. package/src/_named-keys.ts +0 -62
  128. package/src/_recorder-chord.ts +0 -76
  129. package/src/_recording-guard.ts +0 -63
  130. package/src/_registration.ts +0 -79
  131. package/src/conflicts.ts +0 -125
  132. package/src/constants.ts +0 -455
  133. package/src/display-labels.ts +0 -123
  134. package/src/format.ts +0 -201
  135. package/src/hint.ts +0 -42
  136. package/src/hotkey-manager.ts +0 -814
  137. package/src/hotkey-recorder.ts +0 -348
  138. package/src/hotkey-sequence-recorder.ts +0 -410
  139. package/src/hotkey.types.ts +0 -269
  140. package/src/index.ts +0 -17
  141. package/src/key-state-tracker.ts +0 -261
  142. package/src/key.types.ts +0 -268
  143. package/src/match.ts +0 -210
  144. package/src/parse.ts +0 -369
  145. package/src/platform.ts +0 -66
  146. package/src/recorder-options.ts +0 -34
  147. package/src/sequence-manager.ts +0 -769
  148. package/src/validate.ts +0 -179
@@ -1,348 +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
- HotkeyRecorderValidationContext,
16
- RecorderOptions,
17
- } from './recorder-options'
18
- import type { Hotkey } from './hotkey.types'
19
-
20
- /**
21
- * State interface for the HotkeyRecorder.
22
- */
23
- export interface HotkeyRecorderState {
24
- /** Whether recording is currently active */
25
- isRecording: boolean
26
- /** The currently recorded hotkey (for live preview) */
27
- recordedHotkey: Hotkey | null
28
- }
29
-
30
- /**
31
- * Options for configuring a HotkeyRecorder instance.
32
- */
33
- export interface HotkeyRecorderOptions extends RecorderOptions {
34
- /** Return true to accept, or false/a message to reject while staying in recording mode. */
35
- validate?: (
36
- hotkey: Hotkey,
37
- context: HotkeyRecorderValidationContext,
38
- ) => boolean | string
39
- /** Callback when a hotkey is successfully recorded */
40
- onRecord: (hotkey: Hotkey) => void
41
- /** Optional callback when recording is cancelled (Escape pressed) */
42
- onCancel?: () => void
43
- /** Optional callback when shortcut is cleared (Backspace/Delete pressed) */
44
- onClear?: () => void
45
- /**
46
- * Whether to ignore keyboard events from input-like elements (text inputs,
47
- * textarea, select, contenteditable). When true, typing in inputs passes
48
- * through normally instead of being captured as a hotkey recording.
49
- * Escape always works regardless of this setting.
50
- * @default true
51
- */
52
- ignoreInputs?: boolean
53
- }
54
-
55
- /**
56
- * Framework-agnostic class for recording keyboard shortcuts.
57
- *
58
- * This class handles all the complexity of capturing keyboard events,
59
- * converting them to hotkey strings, and handling edge cases like
60
- * Escape to cancel or Backspace/Delete to clear.
61
- *
62
- * State Management:
63
- * - Uses TanStack Store for reactive state management
64
- * - State can be accessed via `recorder.store.state` when using the class directly
65
- * - When using framework adapters (React), use `useSelector` hooks for reactive state
66
- *
67
- * @example
68
- * ```ts
69
- * const recorder = new HotkeyRecorder({
70
- * onRecord: (hotkey) => {
71
- * console.log('Recorded:', hotkey)
72
- * },
73
- * onCancel: () => {
74
- * console.log('Recording cancelled')
75
- * },
76
- * })
77
- *
78
- * // Start recording
79
- * recorder.start()
80
- *
81
- * // Access state directly
82
- * console.log(recorder.store.state.isRecording) // true
83
- *
84
- * // Subscribe to changes with TanStack Store
85
- * const unsubscribe = recorder.store.subscribe(() => {
86
- * console.log('Recording:', recorder.store.state.isRecording)
87
- * })
88
- *
89
- * // Cleanup
90
- * recorder.destroy()
91
- * unsubscribe()
92
- * ```
93
- */
94
- export class HotkeyRecorder {
95
- /**
96
- * The TanStack Store instance containing the recorder state.
97
- * Use this to subscribe to state changes or access current state.
98
- */
99
- readonly store: Store<HotkeyRecorderState> = new Store<HotkeyRecorderState>({
100
- isRecording: false,
101
- recordedHotkey: null,
102
- })
103
-
104
- #keydownHandler: ((event: KeyboardEvent) => void) | null = null
105
- #options: HotkeyRecorderOptions
106
- #platform: 'mac' | 'windows' | 'linux'
107
-
108
- constructor(options: HotkeyRecorderOptions) {
109
- this.#options = options
110
- this.#platform = detectPlatform()
111
- }
112
-
113
- /**
114
- * Updates the recorder options, including callbacks.
115
- * This allows framework adapters to sync callback changes without recreating the recorder.
116
- */
117
- setOptions(options: Partial<HotkeyRecorderOptions>): void {
118
- this.#options = {
119
- ...this.#options,
120
- ...options,
121
- }
122
- }
123
-
124
- /**
125
- * Start recording a new hotkey.
126
- *
127
- * Sets up a keydown event listener that captures keyboard events
128
- * and converts them to hotkey strings. Recording continues until
129
- * a valid hotkey is recorded, Escape is pressed, or stop/cancel is called.
130
- */
131
- start(): void {
132
- // Prevent starting recording if already recording
133
- if (this.#keydownHandler) {
134
- return
135
- }
136
-
137
- beginRecording(this)
138
- // Update store state
139
- this.store.setState(() => ({
140
- isRecording: true,
141
- recordedHotkey: null,
142
- }))
143
-
144
- // Create keydown handler
145
- const handler = (event: KeyboardEvent) => {
146
- // Check if we're still recording (handler might be called after stop/cancel)
147
- if (!this.#keydownHandler) {
148
- return
149
- }
150
-
151
- // If ignoreInputs is enabled (default) and focus is in an input element,
152
- // let the event pass through so the user can type normally.
153
- // Escape is the exception — it should always cancel recording.
154
- if (
155
- this.#options.ignoreInputs !== false &&
156
- event.key !== 'Escape' &&
157
- shouldIgnoreInputEvent(event, document, document)
158
- )
159
- return
160
-
161
- const platform = this.#options.platform ?? this.#platform
162
- if (normalizeKeyboardEvent(event, platform).isComposing) return
163
- captureRecordingEvent(event)
164
- event.preventDefault()
165
- event.stopPropagation()
166
- if (event.repeat) return
167
-
168
- // Handle Escape to cancel
169
- if (event.key === 'Escape') {
170
- this.cancel()
171
- return
172
- }
173
-
174
- // Handle Backspace/Delete to clear shortcut
175
- if (event.key === 'Backspace' || event.key === 'Delete') {
176
- if (
177
- !event.ctrlKey &&
178
- !event.shiftKey &&
179
- !event.altKey &&
180
- !event.metaKey
181
- ) {
182
- this.stop()
183
- this.#options.onClear?.()
184
- return
185
- }
186
- }
187
-
188
- if (isModifierKey(event.key) || event.key === 'AltGraph') return
189
- const rejection = chordRejection(event, { ...this.#options, platform })
190
- if (rejection) {
191
- this.#options.onReject?.(rejection)
192
- return
193
- }
194
- const finalHotkey = hotkeyChordFromKeydown(
195
- event,
196
- platform,
197
- this.#options.recordBy,
198
- )
199
- if (finalHotkey === null) {
200
- return
201
- }
202
-
203
- const validation = validateHotkey(finalHotkey)
204
- if (!validation.valid) {
205
- this.#options.onReject?.({
206
- reason: 'invalid',
207
- message: validation.errors.join('; '),
208
- hotkey: finalHotkey,
209
- })
210
- return
211
- }
212
- const accepted = this.#options.validate?.(finalHotkey, {
213
- event,
214
- parsedHotkey: parseHotkey(finalHotkey, platform),
215
- })
216
- if (accepted !== undefined && accepted !== true) {
217
- this.#options.onReject?.({
218
- reason: 'validation',
219
- message:
220
- typeof accepted === 'string'
221
- ? accepted
222
- : 'This shortcut is not allowed.',
223
- hotkey: finalHotkey,
224
- })
225
- return
226
- }
227
- if (this.#options.detectConflicts) {
228
- const conflicts = findHotkeyConflicts(finalHotkey, {
229
- ...(typeof this.#options.detectConflicts === 'object'
230
- ? this.#options.detectConflicts
231
- : {}),
232
- platform,
233
- events: [event],
234
- })
235
- if (conflicts.length) {
236
- this.#options.onReject?.({
237
- reason: 'conflict',
238
- message: 'This shortcut conflicts with a registered binding.',
239
- hotkey: finalHotkey,
240
- conflicts,
241
- })
242
- return
243
- }
244
- }
245
- endRecording(this)
246
- // Remove listener FIRST to prevent any additional events
247
- const handlerToRemove = this.#keydownHandler as
248
- | ((event: KeyboardEvent) => void)
249
- | null
250
- if (handlerToRemove) {
251
- this.#removeListener(handlerToRemove)
252
- this.#keydownHandler = null
253
- }
254
-
255
- // Update store state immediately
256
- this.store.setState(() => ({
257
- isRecording: false,
258
- recordedHotkey: finalHotkey,
259
- }))
260
-
261
- // Call callback AFTER listener is removed and state is set
262
- this.#options.onRecord(finalHotkey)
263
- }
264
-
265
- this.#keydownHandler = handler
266
- this.#addListener(handler)
267
- }
268
-
269
- /**
270
- * Stop recording (same as cancel, but doesn't call onCancel).
271
- *
272
- * Removes the event listener and resets the recording state.
273
- */
274
- stop(): void {
275
- endRecording(this)
276
- // Remove event listener immediately
277
- if (this.#keydownHandler) {
278
- this.#removeListener(this.#keydownHandler)
279
- this.#keydownHandler = null
280
- }
281
-
282
- // Update store state
283
- this.store.setState(() => ({
284
- isRecording: false,
285
- recordedHotkey: null,
286
- }))
287
- }
288
-
289
- /**
290
- * Cancel recording without saving.
291
- *
292
- * Removes the event listener, resets the recording state, and calls
293
- * the onCancel callback if provided.
294
- */
295
- cancel(): void {
296
- endRecording(this)
297
- // Remove event listener immediately
298
- if (this.#keydownHandler) {
299
- this.#removeListener(this.#keydownHandler)
300
- this.#keydownHandler = null
301
- }
302
-
303
- // Update store state
304
- this.store.setState(() => ({
305
- isRecording: false,
306
- recordedHotkey: null,
307
- }))
308
-
309
- // Call cancel callback
310
- this.#options.onCancel?.()
311
- }
312
-
313
- /**
314
- * Adds the keydown event listener to the document.
315
- */
316
- #addListener(handler: (event: KeyboardEvent) => void): void {
317
- if (typeof document === 'undefined') {
318
- return // SSR safety
319
- }
320
-
321
- document.addEventListener('keydown', handler, true)
322
- }
323
-
324
- /**
325
- * Removes the keydown event listener from the document.
326
- */
327
- #removeListener(handler: (event: KeyboardEvent) => void): void {
328
- if (typeof document === 'undefined') {
329
- return
330
- }
331
-
332
- document.removeEventListener('keydown', handler, true)
333
- }
334
-
335
- /**
336
- * Clean up event listeners and reset state.
337
- *
338
- * Call this when you're done with the recorder to ensure
339
- * all event listeners are properly removed.
340
- */
341
- destroy(): void {
342
- this.stop()
343
- this.store.setState(() => ({
344
- isRecording: false,
345
- recordedHotkey: null,
346
- }))
347
- }
348
- }