@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.
- package/dist/createHotkeyRecorder.svelte.js +22 -35
- package/dist/createHotkeySequenceRecorder.svelte.js +28 -41
- package/dist/getHeldKeyCodesMap.svelte.js +4 -13
- package/dist/getHeldKeys.svelte.js +4 -13
- package/dist/getHotkeyRegistrations.svelte.js +8 -17
- package/dist/getIsKeyHeld.svelte.js +6 -19
- package/package.json +12 -10
- package/src/HotkeysCtx.ts +0 -61
- package/src/HotkeysProvider.svelte +0 -12
- package/src/createHotkey.svelte.ts +0 -179
- package/src/createHotkeyRecorder.svelte.ts +0 -121
- package/src/createHotkeySequence.svelte.ts +0 -207
- package/src/createHotkeySequenceRecorder.svelte.ts +0 -97
- package/src/createHotkeySequences.svelte.ts +0 -274
- package/src/createHotkeys.svelte.ts +0 -276
- package/src/getHeldKeyCodesMap.svelte.ts +0 -44
- package/src/getHeldKeys.svelte.ts +0 -40
- package/src/getHotkeyHint.svelte.ts +0 -51
- package/src/getHotkeyRegistrations.svelte.ts +0 -60
- package/src/getIsKeyHeld.svelte.ts +0 -68
- package/src/index.ts +0 -22
- package/src/internal.svelte.ts +0 -26
|
@@ -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
|
-
}
|