@tanstack/svelte-hotkeys 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,121 +0,0 @@
1
- import { HotkeyRecorder } from '@tanstack/hotkeys'
2
- import { onDestroy } from 'svelte'
3
- import { getDefaultHotkeysOptions } from './HotkeysCtx'
4
- import { createStoreSubscriber, resolveMaybeGetter } from './internal.svelte'
5
- import type { Hotkey, HotkeyRecorderOptions } from '@tanstack/hotkeys'
6
- import type { MaybeGetter } from './internal.svelte'
7
-
8
- export interface SvelteHotkeyRecorder {
9
- /** Whether recording is currently active */
10
- readonly isRecording: boolean
11
- /** The currently recorded hotkey (for live preview) */
12
- readonly recordedHotkey: Hotkey | null
13
- /** Start recording a new hotkey */
14
- startRecording: () => void
15
- /** Stop recording (same as cancel) */
16
- stopRecording: () => void
17
- /** Cancel recording without saving */
18
- cancelRecording: () => void
19
- }
20
-
21
- /**
22
- * Svelte function for recording keyboard shortcuts.
23
- *
24
- * This function provides a thin wrapper around the framework-agnostic `HotkeyRecorder`
25
- * class, managing all the complexity of capturing keyboard events, converting them
26
- * to hotkey strings, and handling edge cases like Escape to cancel or Backspace/Delete
27
- * to clear.
28
- *
29
- * @param options - Configuration options for the recorder
30
- * @returns An object with recording state and control functions
31
- *
32
- * @example
33
- * ```svelte
34
- * <script>
35
- * import { createHotkeyRecorder } from '@tanstack/svelte-hotkeys'
36
- *
37
- * const recorder = createHotkeyRecorder({
38
- * onRecord: (hotkey) => {
39
- * console.log('Recorded:', hotkey) // e.g., "Mod+Shift S"
40
- * },
41
- * onCancel: () => {
42
- * console.log('Recording cancelled')
43
- * },
44
- * })
45
- * </script>
46
- *
47
- * <div>
48
- * <button onclick={recorder.startRecording}>
49
- * {recorder.isRecording ? 'Recording...' : 'Edit Shortcut'}
50
- * </button>
51
- * {#if recorder.recordedHotkey}
52
- * <div>Recording: {recorder.recordedHotkey}</div>
53
- * {/if}
54
- * </div>
55
- * ```
56
- */
57
-
58
- class SvelteHotkeyRecorderState implements SvelteHotkeyRecorder {
59
- #recorder: HotkeyRecorder
60
- #subscribe: () => void
61
-
62
- constructor(options: HotkeyRecorderOptions) {
63
- this.#recorder = new HotkeyRecorder(options)
64
- this.#subscribe = createStoreSubscriber(this.#recorder.store)
65
- }
66
-
67
- get isRecording(): boolean {
68
- this.#subscribe()
69
- return this.#recorder.store.state.isRecording
70
- }
71
-
72
- get recordedHotkey(): Hotkey | null {
73
- this.#subscribe()
74
- return this.#recorder.store.state.recordedHotkey
75
- }
76
-
77
- setOptions(options: HotkeyRecorderOptions): void {
78
- this.#recorder.setOptions(options)
79
- }
80
-
81
- // Keep the recorder receiver when used directly as an event handler.
82
- startRecording = (): void => {
83
- this.#recorder.start()
84
- }
85
-
86
- // Keep the recorder receiver when used directly as an event handler.
87
- stopRecording = (): void => {
88
- this.#recorder.stop()
89
- }
90
-
91
- // Keep the recorder receiver when used directly as an event handler.
92
- cancelRecording = (): void => {
93
- this.#recorder.cancel()
94
- }
95
-
96
- destroy(): void {
97
- this.#recorder.destroy()
98
- }
99
- }
100
-
101
- export function createHotkeyRecorder(
102
- options: MaybeGetter<HotkeyRecorderOptions>,
103
- ): SvelteHotkeyRecorder {
104
- const recorder = new SvelteHotkeyRecorderState({
105
- ...getDefaultHotkeysOptions().hotkeyRecorder,
106
- ...resolveMaybeGetter(options),
107
- } as HotkeyRecorderOptions)
108
-
109
- $effect(() => {
110
- recorder.setOptions({
111
- ...getDefaultHotkeysOptions().hotkeyRecorder,
112
- ...resolveMaybeGetter(options),
113
- } as HotkeyRecorderOptions)
114
- })
115
-
116
- onDestroy(() => {
117
- recorder.destroy()
118
- })
119
-
120
- return recorder
121
- }
@@ -1,207 +0,0 @@
1
- import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
2
- import { onDestroy } from 'svelte'
3
- import { getDefaultHotkeysOptions } from './HotkeysCtx'
4
- import { resolveMaybeGetter } from './internal.svelte'
5
- import type {
6
- HotkeyCallback,
7
- HotkeySequence,
8
- SequenceOptions,
9
- SequenceRegistrationHandle,
10
- } from '@tanstack/hotkeys'
11
- import type { MaybeGetter } from './internal.svelte'
12
- import type { Attachment } from 'svelte/attachments'
13
-
14
- export interface CreateHotkeySequenceOptions extends Omit<
15
- SequenceOptions,
16
- 'target'
17
- > {
18
- target?: Document | Window // not html elements, use attachment instead
19
- }
20
-
21
- function registerHotkeySequence(
22
- target: HTMLElement | Document | Window,
23
- sequence: HotkeySequence,
24
- callback: HotkeyCallback,
25
- mergedOptions: CreateHotkeySequenceOptions,
26
- ) {
27
- return getSequenceManager().register(sequence, callback, {
28
- ...mergedOptions,
29
- target,
30
- })
31
- }
32
-
33
- /**
34
- * Register a global keyboard shortcut sequence for the current component.
35
- *
36
- * Each step may include modifiers. You can chain the same modifier across
37
- * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
38
- * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
39
- * advance the sequence or reset progress.
40
- *
41
- * @example
42
- * ```svelte
43
- * <script lang="ts">
44
- * import { createHotkeySequence } from '@tanstack/svelte-hotkeys'
45
- *
46
- * // Scroll to top when 'G G' is pressed
47
- * createHotkeySequence(['G', 'G'], () => {
48
- * scrollToTop()
49
- * })
50
- *
51
- * // Delete line when 'D D' is pressed
52
- * createHotkeySequence(['D', 'D'], () => {
53
- * deleteLine()
54
- * })
55
- *
56
- * // Delete inner word when 'D I W' is pressed
57
- * createHotkeySequence(['D', 'I', 'W'], () => {
58
- * deleteInnerWord()
59
- * }, { timeout: 500 })
60
- *
61
- * // Same modifier on consecutive steps (bare Shift between chords is ignored)
62
- * createHotkeySequence(['Shift+R', 'Shift+T'], () => {
63
- * nextAction()
64
- * })
65
- * </script>
66
- *
67
- * <div>
68
- * ....
69
- * </div>
70
- * ```
71
- */
72
- export function createHotkeySequence(
73
- sequence: MaybeGetter<HotkeySequence>,
74
- callback: HotkeyCallback,
75
- options: MaybeGetter<CreateHotkeySequenceOptions> = {},
76
- ): void {
77
- let registration: SequenceRegistrationHandle | null = null
78
- let lastSequenceKey: string | null = null
79
-
80
- $effect(() => {
81
- if (typeof document === 'undefined') {
82
- return
83
- }
84
-
85
- const resolvedSequence = resolveMaybeGetter(sequence)
86
- const resolvedOptions = resolveMaybeGetter(options)
87
- const mergedOptions = {
88
- ...getDefaultHotkeysOptions().hotkeySequence,
89
- ...resolvedOptions,
90
- } as CreateHotkeySequenceOptions
91
-
92
- const { target: _t, ...optionsWithoutTarget } = mergedOptions
93
-
94
- if (resolvedSequence.length === 0) {
95
- if (registration?.isActive) {
96
- registration.unregister()
97
- registration = null
98
- }
99
- lastSequenceKey = null
100
- return
101
- }
102
-
103
- const sequenceKey = formatHotkeySequence(resolvedSequence)
104
-
105
- if (registration?.isActive && lastSequenceKey === sequenceKey) {
106
- registration.callback = callback
107
- registration.setOptions(optionsWithoutTarget)
108
- return
109
- }
110
-
111
- if (registration?.isActive) {
112
- registration.unregister()
113
- registration = null
114
- }
115
-
116
- registration = registerHotkeySequence(
117
- document,
118
- resolvedSequence,
119
- callback,
120
- mergedOptions,
121
- )
122
- lastSequenceKey = sequenceKey
123
- })
124
-
125
- onDestroy(() => {
126
- if (registration?.isActive) {
127
- registration.unregister()
128
- registration = null
129
- }
130
- })
131
- }
132
-
133
- /**
134
- * Create an attachment for element-scoped keyboard sequences.
135
- *
136
- * @example
137
- * ```svelte
138
- * <script lang="ts">
139
- * import { createHotkeySequenceAttachment } from '@tanstack/svelte-hotkeys'
140
- *
141
- * const vimKeys = createHotkeySequenceAttachment(['G', 'G'], () => {
142
- * scrollToTop()
143
- * })
144
- * </script>
145
- *
146
- * <div tabindex="0" {@attach vimKeys}>
147
- * Focus here and press g then g
148
- * </div>
149
- * ```
150
- */
151
- export function createHotkeySequenceAttachment(
152
- sequence: MaybeGetter<HotkeySequence>,
153
- callback: HotkeyCallback,
154
- options: MaybeGetter<CreateHotkeySequenceOptions> = {},
155
- ): Attachment<HTMLElement> {
156
- return (element) => {
157
- let registration: SequenceRegistrationHandle | null = null
158
- let lastSequenceKey: string | null = null
159
-
160
- $effect(() => {
161
- const resolvedSequence = resolveMaybeGetter(sequence)
162
- const resolvedOptions = resolveMaybeGetter(options)
163
- const mergedOptions = {
164
- ...getDefaultHotkeysOptions().hotkeySequence,
165
- ...resolvedOptions,
166
- } as CreateHotkeySequenceOptions
167
-
168
- const { target: _t, ...optionsWithoutTarget } = mergedOptions
169
-
170
- if (resolvedSequence.length === 0) {
171
- if (registration?.isActive) {
172
- registration.unregister()
173
- registration = null
174
- }
175
- lastSequenceKey = null
176
- return
177
- }
178
-
179
- const sequenceKey = formatHotkeySequence(resolvedSequence)
180
-
181
- if (registration?.isActive && lastSequenceKey === sequenceKey) {
182
- registration.callback = callback
183
- registration.setOptions(optionsWithoutTarget)
184
- return
185
- }
186
-
187
- if (registration?.isActive) {
188
- registration.unregister()
189
- registration = null
190
- }
191
-
192
- registration = registerHotkeySequence(
193
- element,
194
- resolvedSequence,
195
- callback,
196
- mergedOptions,
197
- )
198
- lastSequenceKey = sequenceKey
199
- })
200
-
201
- return () => {
202
- if (registration?.isActive) {
203
- registration.unregister()
204
- }
205
- }
206
- }
207
- }
@@ -1,97 +0,0 @@
1
- import { HotkeySequenceRecorder } from '@tanstack/hotkeys'
2
- import { onDestroy } from 'svelte'
3
- import { getDefaultHotkeysOptions } from './HotkeysCtx'
4
- import { createStoreSubscriber, resolveMaybeGetter } from './internal.svelte'
5
- import type {
6
- HotkeySequence,
7
- HotkeySequenceRecorderOptions,
8
- } from '@tanstack/hotkeys'
9
- import type { MaybeGetter } from './internal.svelte'
10
-
11
- export interface SvelteHotkeySequenceRecorder {
12
- readonly isRecording: boolean
13
- readonly steps: HotkeySequence
14
- readonly recordedSequence: HotkeySequence | null
15
- startRecording: () => void
16
- stopRecording: () => void
17
- cancelRecording: () => void
18
- commitRecording: () => void
19
- }
20
-
21
- class SvelteHotkeySequenceRecorderState implements SvelteHotkeySequenceRecorder {
22
- #recorder: HotkeySequenceRecorder
23
- #subscribe: () => void
24
-
25
- constructor(options: HotkeySequenceRecorderOptions) {
26
- this.#recorder = new HotkeySequenceRecorder(options)
27
- this.#subscribe = createStoreSubscriber(this.#recorder.store)
28
- }
29
-
30
- get isRecording(): boolean {
31
- this.#subscribe()
32
- return this.#recorder.store.state.isRecording
33
- }
34
-
35
- get steps(): HotkeySequence {
36
- this.#subscribe()
37
- return this.#recorder.store.state.steps
38
- }
39
-
40
- get recordedSequence(): HotkeySequence | null {
41
- this.#subscribe()
42
- return this.#recorder.store.state.recordedSequence
43
- }
44
-
45
- setOptions(options: HotkeySequenceRecorderOptions): void {
46
- this.#recorder.setOptions(options)
47
- }
48
-
49
- // Keep the recorder receiver when used directly as an event handler.
50
- startRecording = (): void => {
51
- this.#recorder.start()
52
- }
53
-
54
- // Keep the recorder receiver when used directly as an event handler.
55
- stopRecording = (): void => {
56
- this.#recorder.stop()
57
- }
58
-
59
- // Keep the recorder receiver when used directly as an event handler.
60
- cancelRecording = (): void => {
61
- this.#recorder.cancel()
62
- }
63
-
64
- // Keep the recorder receiver when used directly as an event handler.
65
- commitRecording = (): void => {
66
- this.#recorder.commit()
67
- }
68
-
69
- destroy(): void {
70
- this.#recorder.destroy()
71
- }
72
- }
73
-
74
- /**
75
- * Svelte helper for recording multi-chord sequences (Vim-style shortcuts).
76
- */
77
- export function createHotkeySequenceRecorder(
78
- options: MaybeGetter<HotkeySequenceRecorderOptions>,
79
- ): SvelteHotkeySequenceRecorder {
80
- const recorder = new SvelteHotkeySequenceRecorderState({
81
- ...getDefaultHotkeysOptions().hotkeySequenceRecorder,
82
- ...resolveMaybeGetter(options),
83
- } as HotkeySequenceRecorderOptions)
84
-
85
- $effect(() => {
86
- recorder.setOptions({
87
- ...getDefaultHotkeysOptions().hotkeySequenceRecorder,
88
- ...resolveMaybeGetter(options),
89
- } as HotkeySequenceRecorderOptions)
90
- })
91
-
92
- onDestroy(() => {
93
- recorder.destroy()
94
- })
95
-
96
- return recorder
97
- }
@@ -1,274 +0,0 @@
1
- import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
2
- import { onDestroy } from 'svelte'
3
- import { SvelteMap, SvelteSet } from 'svelte/reactivity'
4
- import { getDefaultHotkeysOptions } from './HotkeysCtx'
5
- import { resolveMaybeGetter } from './internal.svelte'
6
- import type {
7
- HotkeyCallback,
8
- HotkeySequence,
9
- SequenceRegistrationHandle,
10
- } from '@tanstack/hotkeys'
11
- import type { CreateHotkeySequenceOptions } from './createHotkeySequence.svelte'
12
- import type { MaybeGetter } from './internal.svelte'
13
- import type { Attachment } from 'svelte/attachments'
14
-
15
- /**
16
- * A single sequence definition for use with `createHotkeySequences`.
17
- */
18
- export interface CreateHotkeySequenceDefinition {
19
- /** Array of hotkey strings that form the sequence */
20
- sequence: MaybeGetter<HotkeySequence>
21
- /** The function to call when the sequence is completed */
22
- callback: HotkeyCallback
23
- /** Per-sequence options (merged on top of commonOptions) */
24
- options?: MaybeGetter<CreateHotkeySequenceOptions>
25
- }
26
-
27
- type RegistrationRecord = {
28
- handle: SequenceRegistrationHandle
29
- target: Document | HTMLElement | Window
30
- }
31
-
32
- function cleanupRegistrations(
33
- registrations:
34
- | Map<string, RegistrationRecord>
35
- | SvelteMap<string, RegistrationRecord>,
36
- ) {
37
- for (const { handle } of registrations.values()) {
38
- if (handle.isActive) {
39
- handle.unregister()
40
- }
41
- }
42
- registrations.clear()
43
- }
44
-
45
- /**
46
- * Register multiple global keyboard shortcut sequences for the current component.
47
- *
48
- * @example
49
- * ```svelte
50
- * <script lang="ts">
51
- * import { createHotkeySequences } from '@tanstack/svelte-hotkeys'
52
- *
53
- * createHotkeySequences([
54
- * { sequence: ['G', 'G'], callback: () => scrollToTop() },
55
- * { sequence: ['D', 'D'], callback: () => deleteLine() },
56
- * ])
57
- * </script>
58
- * ```
59
- */
60
- export function createHotkeySequences(
61
- definitions: MaybeGetter<Array<CreateHotkeySequenceDefinition>>,
62
- commonOptions: MaybeGetter<CreateHotkeySequenceOptions> = {},
63
- ): void {
64
- const registrations = new SvelteMap<string, RegistrationRecord>()
65
-
66
- onDestroy(() => {
67
- cleanupRegistrations(registrations)
68
- })
69
-
70
- $effect(() => {
71
- if (typeof document === 'undefined') {
72
- return
73
- }
74
-
75
- const resolvedDefinitions = resolveMaybeGetter(definitions)
76
- const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
77
- const nextKeys = new SvelteSet<string>()
78
- const prepared: Array<{
79
- registrationKey: string
80
- def: CreateHotkeySequenceDefinition
81
- mergedOptions: CreateHotkeySequenceOptions
82
- sequenceString: string
83
- resolvedSequence: HotkeySequence
84
- resolvedTarget: Document | HTMLElement | Window
85
- }> = []
86
-
87
- for (let i = 0; i < resolvedDefinitions.length; i++) {
88
- const def = resolvedDefinitions[i]!
89
- const resolvedSequence = resolveMaybeGetter(def.sequence)
90
- if (resolvedSequence.length === 0) {
91
- continue
92
- }
93
-
94
- const resolvedDefOptions = def.options
95
- ? resolveMaybeGetter(def.options)
96
- : {}
97
-
98
- const mergedOptions = {
99
- ...getDefaultHotkeysOptions().hotkeySequence,
100
- ...resolvedCommonOptions,
101
- ...resolvedDefOptions,
102
- } as CreateHotkeySequenceOptions
103
-
104
- const resolvedTarget =
105
- mergedOptions.target ??
106
- (typeof document !== 'undefined' ? document : null)
107
-
108
- if (!resolvedTarget) {
109
- continue
110
- }
111
-
112
- const sequenceString = formatHotkeySequence(resolvedSequence)
113
- const registrationKey = `${i}:${sequenceString}`
114
- nextKeys.add(registrationKey)
115
- prepared.push({
116
- registrationKey,
117
- def,
118
- mergedOptions,
119
- sequenceString,
120
- resolvedSequence,
121
- resolvedTarget,
122
- })
123
- }
124
-
125
- for (const [key, record] of [...registrations.entries()]) {
126
- if (!nextKeys.has(key)) {
127
- if (record.handle.isActive) {
128
- record.handle.unregister()
129
- }
130
- registrations.delete(key)
131
- }
132
- }
133
-
134
- for (const p of prepared) {
135
- const existing = registrations.get(p.registrationKey)
136
- const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
137
-
138
- if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
139
- existing.handle.callback = p.def.callback
140
- existing.handle.setOptions(optionsWithoutTarget)
141
- continue
142
- }
143
-
144
- if (existing?.handle.isActive) {
145
- existing.handle.unregister()
146
- }
147
- if (existing) {
148
- registrations.delete(p.registrationKey)
149
- }
150
-
151
- const handle = getSequenceManager().register(
152
- p.resolvedSequence,
153
- p.def.callback,
154
- {
155
- ...optionsWithoutTarget,
156
- target: p.resolvedTarget,
157
- },
158
- )
159
- registrations.set(p.registrationKey, {
160
- handle,
161
- target: p.resolvedTarget,
162
- })
163
- }
164
- })
165
- }
166
-
167
- /**
168
- * Create an attachment for element-scoped multi-sequence registration.
169
- *
170
- * @example
171
- * ```svelte
172
- * <script lang="ts">
173
- * import { createHotkeySequencesAttachment } from '@tanstack/svelte-hotkeys'
174
- *
175
- * const vim = createHotkeySequencesAttachment([
176
- * { sequence: ['G', 'G'], callback: () => scrollToTop() },
177
- * { sequence: ['D', 'D'], callback: () => deleteLine() },
178
- * ])
179
- * </script>
180
- *
181
- * <div tabindex="0" {@attach vim}>Editor</div>
182
- * ```
183
- */
184
- export function createHotkeySequencesAttachment(
185
- definitions: MaybeGetter<Array<CreateHotkeySequenceDefinition>>,
186
- commonOptions: MaybeGetter<CreateHotkeySequenceOptions> = {},
187
- ): Attachment<HTMLElement> {
188
- return (element) => {
189
- const registrations = new SvelteMap<string, RegistrationRecord>()
190
-
191
- $effect(() => {
192
- const resolvedDefinitions = resolveMaybeGetter(definitions)
193
- const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
194
- const nextKeys = new SvelteSet<string>()
195
- const prepared: Array<{
196
- registrationKey: string
197
- def: CreateHotkeySequenceDefinition
198
- mergedOptions: CreateHotkeySequenceOptions
199
- sequenceString: string
200
- resolvedSequence: HotkeySequence
201
- }> = []
202
-
203
- for (let i = 0; i < resolvedDefinitions.length; i++) {
204
- const def = resolvedDefinitions[i]!
205
- const resolvedSequence = resolveMaybeGetter(def.sequence)
206
- if (resolvedSequence.length === 0) {
207
- continue
208
- }
209
-
210
- const resolvedDefOptions = def.options
211
- ? resolveMaybeGetter(def.options)
212
- : {}
213
-
214
- const mergedOptions = {
215
- ...getDefaultHotkeysOptions().hotkeySequence,
216
- ...resolvedCommonOptions,
217
- ...resolvedDefOptions,
218
- } as CreateHotkeySequenceOptions
219
-
220
- const sequenceString = formatHotkeySequence(resolvedSequence)
221
- const registrationKey = `${i}:${sequenceString}`
222
- nextKeys.add(registrationKey)
223
- prepared.push({
224
- registrationKey,
225
- def,
226
- mergedOptions,
227
- sequenceString,
228
- resolvedSequence,
229
- })
230
- }
231
-
232
- for (const [key, record] of [...registrations.entries()]) {
233
- if (!nextKeys.has(key)) {
234
- if (record.handle.isActive) {
235
- record.handle.unregister()
236
- }
237
- registrations.delete(key)
238
- }
239
- }
240
-
241
- for (const p of prepared) {
242
- const existing = registrations.get(p.registrationKey)
243
- const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
244
-
245
- if (existing?.handle.isActive && existing.target === element) {
246
- existing.handle.callback = p.def.callback
247
- existing.handle.setOptions(optionsWithoutTarget)
248
- continue
249
- }
250
-
251
- if (existing?.handle.isActive) {
252
- existing.handle.unregister()
253
- }
254
- if (existing) {
255
- registrations.delete(p.registrationKey)
256
- }
257
-
258
- const handle = getSequenceManager().register(
259
- p.resolvedSequence,
260
- p.def.callback,
261
- {
262
- ...optionsWithoutTarget,
263
- target: element,
264
- },
265
- )
266
- registrations.set(p.registrationKey, { handle, target: element })
267
- }
268
- })
269
-
270
- return () => {
271
- cleanupRegistrations(registrations)
272
- }
273
- }
274
- }