@tanstack/svelte-hotkeys 0.11.0 → 0.12.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.
@@ -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,51 +0,0 @@
1
- import { createSubscriber } from 'svelte/reactivity'
2
- import { getKeyStateTracker, matchesHeldModifiers } from '@tanstack/hotkeys'
3
- import { resolveMaybeGetter } from './internal.svelte'
4
- import type { HeldModifierOptions, RegisterableHotkey } from '@tanstack/hotkeys'
5
- import type { MaybeGetter } from './internal.svelte'
6
-
7
- export interface SvelteHotkeyHint {
8
- readonly visible: boolean
9
- }
10
-
11
- /**
12
- * Returns an object with a reactive `visible` getter indicating whether held modifiers reveal this shortcut.
13
- * Pass getters to track changing bindings and options.
14
- * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires
15
- * every modifier. Combine with the action's enabled state before showing a badge.
16
- * This helper does not register a shortcut or check whether its target is focused.
17
- */
18
- export function getHotkeyHint(
19
- hotkey: MaybeGetter<RegisterableHotkey>,
20
- options: MaybeGetter<HeldModifierOptions> = {},
21
- ): SvelteHotkeyHint {
22
- const tracker = getKeyStateTracker()
23
- // Resolve current reactive arguments without caching a stale binding.
24
- const read = () =>
25
- matchesHeldModifiers(
26
- resolveMaybeGetter(hotkey),
27
- tracker.store.state.heldKeys,
28
- resolveMaybeGetter(options),
29
- )
30
- let previous = false
31
- // Notify Svelte only when visibility changes, not for every unrelated held key.
32
- const subscribe = createSubscriber((update) => {
33
- previous = read()
34
- const subscription = tracker.store.subscribe(() => {
35
- const next = read()
36
- if (next !== previous) {
37
- previous = next
38
- update()
39
- }
40
- })
41
- return () => subscription.unsubscribe()
42
- })
43
- return {
44
- /** Reads visibility and subscribes the current reactive consumer when needed. */
45
- get visible() {
46
- subscribe()
47
- previous = read()
48
- return previous
49
- },
50
- }
51
- }
@@ -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
- }
@@ -1,68 +0,0 @@
1
- import { getKeyStateTracker } from '@tanstack/hotkeys'
2
- import { createStoreSubscriber } from './internal.svelte'
3
- import type { IndividualKey } from '@tanstack/hotkeys'
4
-
5
- export interface SvelteHeldKeyState {
6
- readonly held: boolean
7
- }
8
-
9
- class HeldKeyState implements SvelteHeldKeyState {
10
- #tracker = getKeyStateTracker()
11
- #normalizedKey: string
12
- #subscribe = createStoreSubscriber(this.#tracker.store)
13
-
14
- constructor(key: IndividualKey) {
15
- this.#normalizedKey = key.toLowerCase()
16
- }
17
-
18
- get held(): boolean {
19
- this.#subscribe()
20
- return this.#tracker.store.state.heldKeys.some(
21
- (heldKey) => heldKey.toLowerCase() === this.#normalizedKey,
22
- )
23
- }
24
- }
25
-
26
- /**
27
- * Svelte function that returns reactive access to whether a specific key is currently being held.
28
- *
29
- * This function uses the global KeyStateTracker and updates whenever keys are pressed
30
- * or released.
31
- *
32
- * @param key - The key to check (e.g., 'Shift', 'Control', 'A')
33
- * @returns Object with a reactive `held` property
34
- *
35
- * @example
36
- * ```svelte
37
- * <script>
38
- * import { getIsKeyHeld } from '@tanstack/svelte-hotkeys'
39
- *
40
- * const isShiftHeld = getIsKeyHeld('Shift')
41
- * </script>
42
- *
43
- * <div>
44
- * {isShiftHeld.held ? 'Shift is pressed!' : 'Press Shift'}
45
- * </div>
46
- * ```
47
- *
48
- * @example
49
- * ```svelte
50
- * <script>
51
- * import { getIsKeyHeld } from '@tanstack/svelte-hotkeys'
52
- *
53
- * const isCtrlHeld = getIsKeyHeld('Control')
54
- * const isShiftHeld = getIsKeyHeld('Shift')
55
- * const isAltHeld = getIsKeyHeld('Alt')
56
- * </script>
57
- *
58
- * <div>
59
- * <span style:opacity={isCtrlHeld.held ? 1 : 0.3}>Ctrl</span>
60
- * <span style:opacity={isShiftHeld.held ? 1 : 0.3}>Shift</span>
61
- * <span style:opacity={isAltHeld.held ? 1 : 0.3}>Alt</span>
62
- * </div>
63
- * ```
64
- */
65
-
66
- export function getIsKeyHeld(key: IndividualKey): SvelteHeldKeyState {
67
- return new HeldKeyState(key)
68
- }
package/src/index.ts DELETED
@@ -1,22 +0,0 @@
1
- // Re-export everything from the core package
2
- export * from '@tanstack/hotkeys'
3
-
4
- export * from './createHotkey.svelte'
5
- export * from './createHotkeys.svelte'
6
- export * from './createHotkeySequence.svelte'
7
- export * from './createHotkeySequences.svelte'
8
- export * from './createHotkeyRecorder.svelte'
9
- export * from './createHotkeySequenceRecorder.svelte'
10
- export * from './getHeldKeys.svelte'
11
- export * from './getHotkeyRegistrations.svelte'
12
- export * from './getHeldKeyCodesMap.svelte'
13
- export * from './getIsKeyHeld.svelte'
14
- export { default as HotkeysProvider } from './HotkeysProvider.svelte'
15
- export {
16
- DEFAULT_OPTIONS,
17
- getDefaultHotkeysOptions,
18
- getHotkeysContext,
19
- setHotkeysContext,
20
- } from './HotkeysCtx'
21
- export type { HotkeysProviderOptions, HotkeysProviderProps } from './HotkeysCtx'
22
- export * from './getHotkeyHint.svelte'
@@ -1,26 +0,0 @@
1
- import { createSubscriber } from 'svelte/reactivity'
2
-
3
- export type MaybeGetter<T> = T | (() => T)
4
-
5
- interface SubscribableStore<TState> {
6
- state: TState
7
- subscribe: (
8
- listener: () => void,
9
- ) => (() => void) | { unsubscribe: () => void }
10
- }
11
-
12
- export function resolveMaybeGetter<T>(value: MaybeGetter<T>): T {
13
- return typeof value === 'function' ? (value as () => T)() : value
14
- }
15
-
16
- export function createStoreSubscriber<TState>(
17
- store: SubscribableStore<TState>,
18
- ): () => void {
19
- return createSubscriber((update) => {
20
- const subscription = store.subscribe(update)
21
-
22
- return typeof subscription === 'function'
23
- ? subscription
24
- : () => subscription.unsubscribe()
25
- })
26
- }