@tanstack/svelte-hotkeys 0.10.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,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
- }
@@ -1,276 +0,0 @@
1
- import {
2
- detectPlatform,
3
- getHotkeyManager,
4
- normalizeRegisterableHotkey,
5
- } from '@tanstack/hotkeys'
6
- import { onDestroy } from 'svelte'
7
- import { SvelteMap, SvelteSet } from 'svelte/reactivity'
8
- import { getDefaultHotkeysOptions } from './HotkeysCtx'
9
- import { resolveMaybeGetter } from './internal.svelte'
10
- import type {
11
- Hotkey,
12
- HotkeyCallback,
13
- HotkeyRegistrationHandle,
14
- RegisterableHotkey,
15
- } from '@tanstack/hotkeys'
16
- import type { CreateHotkeyOptions } from './createHotkey.svelte'
17
- import type { MaybeGetter } from './internal.svelte'
18
- import type { Attachment } from 'svelte/attachments'
19
-
20
- /**
21
- * A single hotkey definition for use with `createHotkeys`.
22
- */
23
- export interface CreateHotkeyDefinition {
24
- /** The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object */
25
- hotkey: MaybeGetter<RegisterableHotkey>
26
- /** The function to call when the hotkey is pressed */
27
- callback: HotkeyCallback
28
- /** Per-hotkey options (merged on top of commonOptions) */
29
- options?: MaybeGetter<CreateHotkeyOptions>
30
- }
31
-
32
- type RegistrationRecord = {
33
- handle: HotkeyRegistrationHandle
34
- target: Document | HTMLElement | Window
35
- }
36
-
37
- function cleanupRegistrations(
38
- registrations:
39
- | Map<string, RegistrationRecord>
40
- | SvelteMap<string, RegistrationRecord>,
41
- ) {
42
- for (const { handle } of registrations.values()) {
43
- if (handle.isActive) {
44
- handle.unregister()
45
- }
46
- }
47
- registrations.clear()
48
- }
49
-
50
- /**
51
- * Register multiple global hotkeys for the current component.
52
- *
53
- * @example
54
- * ```svelte
55
- * <script lang="ts">
56
- * import { createHotkeys } from '@tanstack/svelte-hotkeys'
57
- *
58
- * createHotkeys([
59
- * { hotkey: 'Mod+S', callback: () => save() },
60
- * { hotkey: 'Mod+Z', callback: () => undo() },
61
- * { hotkey: 'Escape', callback: () => close() },
62
- * ])
63
- * </script>
64
- * ```
65
- */
66
- export function createHotkeys(
67
- hotkeys: MaybeGetter<Array<CreateHotkeyDefinition>>,
68
- commonOptions: MaybeGetter<CreateHotkeyOptions> = {},
69
- ): void {
70
- const registrations = new SvelteMap<string, RegistrationRecord>()
71
-
72
- onDestroy(() => {
73
- cleanupRegistrations(registrations)
74
- })
75
-
76
- $effect(() => {
77
- if (typeof document === 'undefined') {
78
- return
79
- }
80
-
81
- const resolvedHotkeys = resolveMaybeGetter(hotkeys)
82
- const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
83
- const nextKeys = new SvelteSet<string>()
84
- const prepared: Array<{
85
- registrationKey: string
86
- def: CreateHotkeyDefinition
87
- mergedOptions: CreateHotkeyOptions
88
- hotkeyString: Hotkey
89
- resolvedTarget: Document | HTMLElement | Window
90
- }> = []
91
-
92
- for (let i = 0; i < resolvedHotkeys.length; i++) {
93
- const def = resolvedHotkeys[i]!
94
- const resolvedHotkey = resolveMaybeGetter(def.hotkey)
95
- const resolvedDefOptions = def.options
96
- ? resolveMaybeGetter(def.options)
97
- : {}
98
-
99
- const mergedOptions = {
100
- ...getDefaultHotkeysOptions().hotkey,
101
- ...resolvedCommonOptions,
102
- ...resolvedDefOptions,
103
- } as CreateHotkeyOptions
104
-
105
- const resolvedTarget =
106
- mergedOptions.target ??
107
- (typeof document !== 'undefined' ? document : null)
108
-
109
- if (!resolvedTarget) {
110
- continue
111
- }
112
-
113
- const hotkeyString = normalizeRegisterableHotkey(
114
- resolvedHotkey,
115
- mergedOptions.platform ?? detectPlatform(),
116
- )
117
- const registrationKey = `${i}:${hotkeyString}`
118
- nextKeys.add(registrationKey)
119
- prepared.push({
120
- registrationKey,
121
- def,
122
- mergedOptions,
123
- hotkeyString,
124
- resolvedTarget,
125
- })
126
- }
127
-
128
- for (const [key, record] of [...registrations.entries()]) {
129
- if (!nextKeys.has(key)) {
130
- if (record.handle.isActive) {
131
- record.handle.unregister()
132
- }
133
- registrations.delete(key)
134
- }
135
- }
136
-
137
- for (const p of prepared) {
138
- const existing = registrations.get(p.registrationKey)
139
- const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
140
-
141
- if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
142
- existing.handle.callback = p.def.callback
143
- existing.handle.setOptions(optionsWithoutTarget)
144
- continue
145
- }
146
-
147
- if (existing?.handle.isActive) {
148
- existing.handle.unregister()
149
- }
150
- if (existing) {
151
- registrations.delete(p.registrationKey)
152
- }
153
-
154
- const handle = getHotkeyManager().register(
155
- p.hotkeyString,
156
- p.def.callback,
157
- {
158
- ...optionsWithoutTarget,
159
- target: p.resolvedTarget,
160
- },
161
- )
162
- registrations.set(p.registrationKey, {
163
- handle,
164
- target: p.resolvedTarget,
165
- })
166
- }
167
- })
168
- }
169
-
170
- /**
171
- * Create an attachment for element-scoped multi-hotkey registration.
172
- *
173
- * @example
174
- * ```svelte
175
- * <script lang="ts">
176
- * import { createHotkeysAttachment } from '@tanstack/svelte-hotkeys'
177
- *
178
- * const editorKeys = createHotkeysAttachment([
179
- * { hotkey: 'Mod+S', callback: () => save() },
180
- * { hotkey: 'Mod+Z', callback: () => undo() },
181
- * ])
182
- * </script>
183
- *
184
- * <div tabindex="0" {@attach editorKeys}>
185
- * Editor content
186
- * </div>
187
- * ```
188
- */
189
- export function createHotkeysAttachment(
190
- hotkeys: MaybeGetter<Array<CreateHotkeyDefinition>>,
191
- commonOptions: MaybeGetter<CreateHotkeyOptions> = {},
192
- ): Attachment<HTMLElement> {
193
- return (element) => {
194
- const registrations = new SvelteMap<string, RegistrationRecord>()
195
-
196
- $effect(() => {
197
- const resolvedHotkeys = resolveMaybeGetter(hotkeys)
198
- const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
199
- const nextKeys = new SvelteSet<string>()
200
- const prepared: Array<{
201
- registrationKey: string
202
- def: CreateHotkeyDefinition
203
- mergedOptions: CreateHotkeyOptions
204
- hotkeyString: Hotkey
205
- }> = []
206
-
207
- for (let i = 0; i < resolvedHotkeys.length; i++) {
208
- const def = resolvedHotkeys[i]!
209
- const resolvedHotkey = resolveMaybeGetter(def.hotkey)
210
- const resolvedDefOptions = def.options
211
- ? resolveMaybeGetter(def.options)
212
- : {}
213
-
214
- const mergedOptions = {
215
- ...getDefaultHotkeysOptions().hotkey,
216
- ...resolvedCommonOptions,
217
- ...resolvedDefOptions,
218
- } as CreateHotkeyOptions
219
-
220
- const hotkeyString = normalizeRegisterableHotkey(
221
- resolvedHotkey,
222
- mergedOptions.platform ?? detectPlatform(),
223
- )
224
- const registrationKey = `${i}:${hotkeyString}`
225
- nextKeys.add(registrationKey)
226
- prepared.push({
227
- registrationKey,
228
- def,
229
- mergedOptions,
230
- hotkeyString,
231
- })
232
- }
233
-
234
- for (const [key, record] of [...registrations.entries()]) {
235
- if (!nextKeys.has(key)) {
236
- if (record.handle.isActive) {
237
- record.handle.unregister()
238
- }
239
- registrations.delete(key)
240
- }
241
- }
242
-
243
- for (const p of prepared) {
244
- const existing = registrations.get(p.registrationKey)
245
- const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
246
-
247
- if (existing?.handle.isActive) {
248
- existing.handle.callback = p.def.callback
249
- existing.handle.setOptions(optionsWithoutTarget)
250
- continue
251
- }
252
-
253
- if (existing?.handle.isActive) {
254
- existing.handle.unregister()
255
- }
256
- if (existing) {
257
- registrations.delete(p.registrationKey)
258
- }
259
-
260
- const handle = getHotkeyManager().register(
261
- p.hotkeyString,
262
- p.def.callback,
263
- {
264
- ...optionsWithoutTarget,
265
- target: element,
266
- },
267
- )
268
- registrations.set(p.registrationKey, { handle, target: element })
269
- }
270
- })
271
-
272
- return () => {
273
- cleanupRegistrations(registrations)
274
- }
275
- }
276
- }
@@ -1,44 +0,0 @@
1
- import { getKeyStateTracker } from '@tanstack/hotkeys'
2
- import { createStoreSubscriber } from './internal.svelte'
3
-
4
- export interface SvelteHeldKeyCodesMap {
5
- readonly codes: Record<string, string>
6
- }
7
-
8
- class HeldKeyCodesMapState implements SvelteHeldKeyCodesMap {
9
- #tracker = getKeyStateTracker()
10
- #subscribe = createStoreSubscriber(this.#tracker.store)
11
-
12
- get codes(): Record<string, string> {
13
- this.#subscribe()
14
- return this.#tracker.store.state.heldCodes
15
- }
16
- }
17
-
18
- /**
19
- * Svelte function that returns reactive access to the map of currently held key names
20
- * to their physical `event.code` values.
21
- *
22
- * This is useful for debugging which physical key was pressed (e.g. distinguishing
23
- * left vs right Shift via "ShiftLeft" / "ShiftRight").
24
- *
25
- * @returns Object with a reactive `codes` property
26
- *
27
- * ```svelte
28
- * <script>
29
- * import { getHeldKeyCodesMap } from '@tanstack/svelte-hotkeys'
30
- * const heldKeyCodesMap = getHeldKeyCodesMap()
31
- * </script>
32
- *
33
- * <div>
34
- * {#each Object.entries(heldKeyCodesMap.codes) as [key, code]}
35
- * <kbd>
36
- * {key} <small>{code}</small>
37
- * </kbd>
38
- * {/each}
39
- * </div>
40
- * ```
41
- */
42
- export function getHeldKeyCodesMap(): SvelteHeldKeyCodesMap {
43
- return new HeldKeyCodesMapState()
44
- }
@@ -1,40 +0,0 @@
1
- import { getKeyStateTracker } from '@tanstack/hotkeys'
2
- import { createStoreSubscriber } from './internal.svelte'
3
-
4
- export interface SvelteHeldKeys {
5
- readonly keys: Array<string>
6
- }
7
-
8
- class HeldKeysState implements SvelteHeldKeys {
9
- #tracker = getKeyStateTracker()
10
- #subscribe = createStoreSubscriber(this.#tracker.store)
11
-
12
- get keys(): Array<string> {
13
- this.#subscribe()
14
- return this.#tracker.store.state.heldKeys
15
- }
16
- }
17
-
18
- /**
19
- * Svelte function that returns reactive access to currently held keyboard keys.
20
- *
21
- * This function uses the global KeyStateTracker and updates whenever keys are pressed
22
- * or released.
23
- *
24
- * @returns Object with a reactive `keys` property
25
- *
26
- * @example
27
- * ```svelte
28
- * <script>
29
- * import { getHeldKeys } from '@tanstack/svelte-hotkeys'
30
- *
31
- * const heldKeys = getHeldKeys()
32
- * </script>
33
- * <div>
34
- * Currently pressed: {heldKeys.keys.join(' + ') || 'None'}
35
- * </div>
36
- * ```
37
- */
38
- export function getHeldKeys(): SvelteHeldKeys {
39
- return new HeldKeysState()
40
- }
@@ -1,60 +0,0 @@
1
- import {
2
- getHotkeyManager,
3
- getSequenceManager,
4
- toHotkeyRegistrationView,
5
- } from '@tanstack/hotkeys'
6
- import { createStoreSubscriber } from './internal.svelte'
7
- import type {
8
- HotkeyRegistrationView,
9
- SequenceRegistrationView,
10
- } from '@tanstack/hotkeys'
11
-
12
- export interface SvelteHotkeyRegistrations {
13
- readonly hotkeys: Array<HotkeyRegistrationView>
14
- readonly sequences: Array<SequenceRegistrationView>
15
- }
16
-
17
- class HotkeyRegistrationsState implements SvelteHotkeyRegistrations {
18
- #hotkeyManager = getHotkeyManager()
19
- #sequenceManager = getSequenceManager()
20
- #subscribeHotkeys = createStoreSubscriber(this.#hotkeyManager.registrations)
21
- #subscribeSequences = createStoreSubscriber(
22
- this.#sequenceManager.registrations,
23
- )
24
-
25
- get hotkeys(): Array<HotkeyRegistrationView> {
26
- this.#subscribeHotkeys()
27
- return Array.from(this.#hotkeyManager.registrations.state.values()).map(
28
- toHotkeyRegistrationView,
29
- )
30
- }
31
-
32
- get sequences(): Array<SequenceRegistrationView> {
33
- this.#subscribeSequences()
34
- return Array.from(this.#sequenceManager.registrations.state.values())
35
- }
36
- }
37
-
38
- /**
39
- * Svelte function that returns reactive access to all hotkey and sequence
40
- * registrations from the singleton managers.
41
- *
42
- * This is a standalone function that does NOT require the HotkeysProvider.
43
- *
44
- * @returns Object with reactive `hotkeys` and `sequences` properties
45
- *
46
- * @example
47
- * ```svelte
48
- * <script>
49
- * import { getHotkeyRegistrations } from '@tanstack/svelte-hotkeys'
50
- *
51
- * const registrations = getHotkeyRegistrations()
52
- * </script>
53
- * {#each registrations.hotkeys as reg}
54
- * <div>{reg.options.meta?.name} — {reg.triggerCount} triggers</div>
55
- * {/each}
56
- * ```
57
- */
58
- export function getHotkeyRegistrations(): SvelteHotkeyRegistrations {
59
- return new HotkeyRegistrationsState()
60
- }