@tanstack/svelte-hotkeys 0.4.2 → 0.5.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.
@@ -66,9 +66,17 @@ export function createHotkey(hotkey, callback, options = {}) {
66
66
  */
67
67
  export function createHotkeyAttachment(hotkey, callback, options = {}) {
68
68
  return (element) => {
69
- const registration = registerHotkey(element, hotkey, callback, options);
69
+ let registration = null;
70
+ $effect(() => {
71
+ if (registration?.isActive) {
72
+ registration.unregister();
73
+ }
74
+ registration = registerHotkey(element, hotkey, callback, options);
75
+ });
70
76
  return () => {
71
- registration.unregister();
77
+ if (registration?.isActive) {
78
+ registration.unregister();
79
+ }
72
80
  };
73
81
  };
74
82
  }
@@ -7,6 +7,11 @@ export interface CreateHotkeySequenceOptions extends Omit<SequenceOptions, 'targ
7
7
  /**
8
8
  * Register a global keyboard shortcut sequence for the current component.
9
9
  *
10
+ * Each step may include modifiers. You can chain the same modifier across
11
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
12
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
13
+ * advance the sequence or reset progress.
14
+ *
10
15
  * @example
11
16
  * ```svelte
12
17
  * <script lang="ts">
@@ -26,6 +31,11 @@ export interface CreateHotkeySequenceOptions extends Omit<SequenceOptions, 'targ
26
31
  * createHotkeySequence(['D', 'I', 'W'], () => {
27
32
  * deleteInnerWord()
28
33
  * }, { timeout: 500 })
34
+ *
35
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
36
+ * createHotkeySequence(['Shift+R', 'Shift+T'], () => {
37
+ * nextAction()
38
+ * })
29
39
  * </script>
30
40
  *
31
41
  * <div>
@@ -16,6 +16,11 @@ function registerHotkeySequence(target, sequence, callback, options) {
16
16
  /**
17
17
  * Register a global keyboard shortcut sequence for the current component.
18
18
  *
19
+ * Each step may include modifiers. You can chain the same modifier across
20
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
21
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
22
+ * advance the sequence or reset progress.
23
+ *
19
24
  * @example
20
25
  * ```svelte
21
26
  * <script lang="ts">
@@ -35,6 +40,11 @@ function registerHotkeySequence(target, sequence, callback, options) {
35
40
  * createHotkeySequence(['D', 'I', 'W'], () => {
36
41
  * deleteInnerWord()
37
42
  * }, { timeout: 500 })
43
+ *
44
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
45
+ * createHotkeySequence(['Shift+R', 'Shift+T'], () => {
46
+ * nextAction()
47
+ * })
38
48
  * </script>
39
49
  *
40
50
  * <div>
@@ -77,13 +87,22 @@ export function createHotkeySequence(sequence, callback, options = {}) {
77
87
  */
78
88
  export function createHotkeySequenceAttachment(sequence, callback, options = {}) {
79
89
  return (element) => {
80
- const resolvedSequence = resolveMaybeGetter(sequence);
81
- if (resolvedSequence.length === 0) {
82
- return;
83
- }
84
- const registration = registerHotkeySequence(element, resolvedSequence, callback, options);
90
+ let registration = null;
91
+ $effect(() => {
92
+ const resolvedSequence = resolveMaybeGetter(sequence);
93
+ if (registration?.isActive) {
94
+ registration.unregister();
95
+ registration = null;
96
+ }
97
+ if (resolvedSequence.length === 0) {
98
+ return;
99
+ }
100
+ registration = registerHotkeySequence(element, resolvedSequence, callback, options);
101
+ });
85
102
  return () => {
86
- registration.unregister();
103
+ if (registration?.isActive) {
104
+ registration.unregister();
105
+ }
87
106
  };
88
107
  };
89
108
  }
@@ -0,0 +1,52 @@
1
+ import type { HotkeyCallback, RegisterableHotkey } from '@tanstack/hotkeys';
2
+ import type { CreateHotkeyOptions } from './createHotkey.svelte';
3
+ import type { MaybeGetter } from './internal.svelte';
4
+ import type { Attachment } from 'svelte/attachments';
5
+ /**
6
+ * A single hotkey definition for use with `createHotkeys`.
7
+ */
8
+ export interface CreateHotkeyDefinition {
9
+ /** The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object */
10
+ hotkey: MaybeGetter<RegisterableHotkey>;
11
+ /** The function to call when the hotkey is pressed */
12
+ callback: HotkeyCallback;
13
+ /** Per-hotkey options (merged on top of commonOptions) */
14
+ options?: MaybeGetter<CreateHotkeyOptions>;
15
+ }
16
+ /**
17
+ * Register multiple global hotkeys for the current component.
18
+ *
19
+ * @example
20
+ * ```svelte
21
+ * <script lang="ts">
22
+ * import { createHotkeys } from '@tanstack/svelte-hotkeys'
23
+ *
24
+ * createHotkeys([
25
+ * { hotkey: 'Mod+S', callback: () => save() },
26
+ * { hotkey: 'Mod+Z', callback: () => undo() },
27
+ * { hotkey: 'Escape', callback: () => close() },
28
+ * ])
29
+ * </script>
30
+ * ```
31
+ */
32
+ export declare function createHotkeys(hotkeys: MaybeGetter<Array<CreateHotkeyDefinition>>, commonOptions?: MaybeGetter<CreateHotkeyOptions>): void;
33
+ /**
34
+ * Create an attachment for element-scoped multi-hotkey registration.
35
+ *
36
+ * @example
37
+ * ```svelte
38
+ * <script lang="ts">
39
+ * import { createHotkeysAttachment } from '@tanstack/svelte-hotkeys'
40
+ *
41
+ * const editorKeys = createHotkeysAttachment([
42
+ * { hotkey: 'Mod+S', callback: () => save() },
43
+ * { hotkey: 'Mod+Z', callback: () => undo() },
44
+ * ])
45
+ * </script>
46
+ *
47
+ * <div tabindex="0" {@attach editorKeys}>
48
+ * Editor content
49
+ * </div>
50
+ * ```
51
+ */
52
+ export declare function createHotkeysAttachment(hotkeys: MaybeGetter<Array<CreateHotkeyDefinition>>, commonOptions?: MaybeGetter<CreateHotkeyOptions>): Attachment<HTMLElement>;
@@ -0,0 +1,190 @@
1
+ import { detectPlatform, formatHotkey, getHotkeyManager, rawHotkeyToParsedHotkey, } 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
+ function normalizeHotkey(hotkey, options) {
7
+ const platform = options.platform ?? detectPlatform();
8
+ return typeof hotkey === 'string'
9
+ ? hotkey
10
+ : formatHotkey(rawHotkeyToParsedHotkey(hotkey, platform));
11
+ }
12
+ function cleanupRegistrations(registrations) {
13
+ for (const { handle } of registrations.values()) {
14
+ if (handle.isActive) {
15
+ handle.unregister();
16
+ }
17
+ }
18
+ registrations.clear();
19
+ }
20
+ /**
21
+ * Register multiple global hotkeys for the current component.
22
+ *
23
+ * @example
24
+ * ```svelte
25
+ * <script lang="ts">
26
+ * import { createHotkeys } from '@tanstack/svelte-hotkeys'
27
+ *
28
+ * createHotkeys([
29
+ * { hotkey: 'Mod+S', callback: () => save() },
30
+ * { hotkey: 'Mod+Z', callback: () => undo() },
31
+ * { hotkey: 'Escape', callback: () => close() },
32
+ * ])
33
+ * </script>
34
+ * ```
35
+ */
36
+ export function createHotkeys(hotkeys, commonOptions = {}) {
37
+ const registrations = new SvelteMap();
38
+ onDestroy(() => {
39
+ cleanupRegistrations(registrations);
40
+ });
41
+ $effect(() => {
42
+ if (typeof document === 'undefined') {
43
+ return;
44
+ }
45
+ const resolvedHotkeys = resolveMaybeGetter(hotkeys);
46
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions);
47
+ const nextKeys = new SvelteSet();
48
+ const prepared = [];
49
+ for (let i = 0; i < resolvedHotkeys.length; i++) {
50
+ const def = resolvedHotkeys[i];
51
+ const resolvedHotkey = resolveMaybeGetter(def.hotkey);
52
+ const resolvedDefOptions = def.options
53
+ ? resolveMaybeGetter(def.options)
54
+ : {};
55
+ const mergedOptions = {
56
+ ...getDefaultHotkeysOptions().hotkey,
57
+ ...resolvedCommonOptions,
58
+ ...resolvedDefOptions,
59
+ };
60
+ const resolvedTarget = mergedOptions.target ??
61
+ (typeof document !== 'undefined' ? document : null);
62
+ if (!resolvedTarget) {
63
+ continue;
64
+ }
65
+ const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions);
66
+ const registrationKey = `${i}:${hotkeyString}`;
67
+ nextKeys.add(registrationKey);
68
+ prepared.push({
69
+ registrationKey,
70
+ def,
71
+ mergedOptions,
72
+ hotkeyString,
73
+ resolvedTarget,
74
+ });
75
+ }
76
+ for (const [key, record] of [...registrations.entries()]) {
77
+ if (!nextKeys.has(key)) {
78
+ if (record.handle.isActive) {
79
+ record.handle.unregister();
80
+ }
81
+ registrations.delete(key);
82
+ }
83
+ }
84
+ for (const p of prepared) {
85
+ const existing = registrations.get(p.registrationKey);
86
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
87
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
88
+ existing.handle.callback = p.def.callback;
89
+ existing.handle.setOptions(optionsWithoutTarget);
90
+ continue;
91
+ }
92
+ if (existing?.handle.isActive) {
93
+ existing.handle.unregister();
94
+ }
95
+ if (existing) {
96
+ registrations.delete(p.registrationKey);
97
+ }
98
+ const handle = getHotkeyManager().register(p.hotkeyString, p.def.callback, {
99
+ ...optionsWithoutTarget,
100
+ target: p.resolvedTarget,
101
+ });
102
+ registrations.set(p.registrationKey, {
103
+ handle,
104
+ target: p.resolvedTarget,
105
+ });
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * Create an attachment for element-scoped multi-hotkey registration.
111
+ *
112
+ * @example
113
+ * ```svelte
114
+ * <script lang="ts">
115
+ * import { createHotkeysAttachment } from '@tanstack/svelte-hotkeys'
116
+ *
117
+ * const editorKeys = createHotkeysAttachment([
118
+ * { hotkey: 'Mod+S', callback: () => save() },
119
+ * { hotkey: 'Mod+Z', callback: () => undo() },
120
+ * ])
121
+ * </script>
122
+ *
123
+ * <div tabindex="0" {@attach editorKeys}>
124
+ * Editor content
125
+ * </div>
126
+ * ```
127
+ */
128
+ export function createHotkeysAttachment(hotkeys, commonOptions = {}) {
129
+ return (element) => {
130
+ const registrations = new SvelteMap();
131
+ $effect(() => {
132
+ const resolvedHotkeys = resolveMaybeGetter(hotkeys);
133
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions);
134
+ const nextKeys = new SvelteSet();
135
+ const prepared = [];
136
+ for (let i = 0; i < resolvedHotkeys.length; i++) {
137
+ const def = resolvedHotkeys[i];
138
+ const resolvedHotkey = resolveMaybeGetter(def.hotkey);
139
+ const resolvedDefOptions = def.options
140
+ ? resolveMaybeGetter(def.options)
141
+ : {};
142
+ const mergedOptions = {
143
+ ...getDefaultHotkeysOptions().hotkey,
144
+ ...resolvedCommonOptions,
145
+ ...resolvedDefOptions,
146
+ };
147
+ const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions);
148
+ const registrationKey = `${i}:${hotkeyString}`;
149
+ nextKeys.add(registrationKey);
150
+ prepared.push({
151
+ registrationKey,
152
+ def,
153
+ mergedOptions,
154
+ hotkeyString,
155
+ });
156
+ }
157
+ for (const [key, record] of [...registrations.entries()]) {
158
+ if (!nextKeys.has(key)) {
159
+ if (record.handle.isActive) {
160
+ record.handle.unregister();
161
+ }
162
+ registrations.delete(key);
163
+ }
164
+ }
165
+ for (const p of prepared) {
166
+ const existing = registrations.get(p.registrationKey);
167
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
168
+ if (existing?.handle.isActive) {
169
+ existing.handle.callback = p.def.callback;
170
+ existing.handle.setOptions(optionsWithoutTarget);
171
+ continue;
172
+ }
173
+ if (existing?.handle.isActive) {
174
+ existing.handle.unregister();
175
+ }
176
+ if (existing) {
177
+ registrations.delete(p.registrationKey);
178
+ }
179
+ const handle = getHotkeyManager().register(p.hotkeyString, p.def.callback, {
180
+ ...optionsWithoutTarget,
181
+ target: element,
182
+ });
183
+ registrations.set(p.registrationKey, { handle, target: element });
184
+ }
185
+ });
186
+ return () => {
187
+ cleanupRegistrations(registrations);
188
+ };
189
+ };
190
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from '@tanstack/hotkeys';
2
2
  export * from './createHotkey.svelte';
3
+ export * from './createHotkeys.svelte';
3
4
  export * from './createHotkeySequence.svelte';
4
5
  export * from './createHotkeyRecorder.svelte';
5
6
  export * from './getHeldKeys.svelte';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Re-export everything from the core package
2
2
  export * from '@tanstack/hotkeys';
3
3
  export * from './createHotkey.svelte';
4
+ export * from './createHotkeys.svelte';
4
5
  export * from './createHotkeySequence.svelte';
5
6
  export * from './createHotkeyRecorder.svelte';
6
7
  export * from './getHeldKeys.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/svelte-hotkeys",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Svelte adapter for TanStack Hotkeys",
5
5
  "author": "Kunal Rao",
6
6
  "license": "MIT",
@@ -40,14 +40,14 @@
40
40
  "src"
41
41
  ],
42
42
  "dependencies": {
43
- "@tanstack/hotkeys": "0.4.2"
43
+ "@tanstack/hotkeys": "0.4.3"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@sveltejs/package": "^2.5.7",
47
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
48
- "@typescript-eslint/parser": "^8.56.1",
49
- "eslint-plugin-svelte": "^3.15.0",
50
- "svelte": "^5.53.7"
47
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
48
+ "@typescript-eslint/parser": "^8.57.1",
49
+ "eslint-plugin-svelte": "^3.16.0",
50
+ "svelte": "^5.54.1"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "svelte": "^5.25.0"
@@ -111,10 +111,20 @@ export function createHotkeyAttachment(
111
111
  options: MaybeGetter<CreateHotkeyOptions> = {},
112
112
  ): Attachment<HTMLElement> {
113
113
  return (element) => {
114
- const registration = registerHotkey(element, hotkey, callback, options)
114
+ let registration: ReturnType<typeof registerHotkey> | null = null
115
+
116
+ $effect(() => {
117
+ if (registration?.isActive) {
118
+ registration.unregister()
119
+ }
120
+
121
+ registration = registerHotkey(element, hotkey, callback, options)
122
+ })
115
123
 
116
124
  return () => {
117
- registration.unregister()
125
+ if (registration?.isActive) {
126
+ registration.unregister()
127
+ }
118
128
  }
119
129
  }
120
130
  }
@@ -38,6 +38,11 @@ function registerHotkeySequence(
38
38
  /**
39
39
  * Register a global keyboard shortcut sequence for the current component.
40
40
  *
41
+ * Each step may include modifiers. You can chain the same modifier across
42
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
43
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
44
+ * advance the sequence or reset progress.
45
+ *
41
46
  * @example
42
47
  * ```svelte
43
48
  * <script lang="ts">
@@ -57,6 +62,11 @@ function registerHotkeySequence(
57
62
  * createHotkeySequence(['D', 'I', 'W'], () => {
58
63
  * deleteInnerWord()
59
64
  * }, { timeout: 500 })
65
+ *
66
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
67
+ * createHotkeySequence(['Shift+R', 'Shift+T'], () => {
68
+ * nextAction()
69
+ * })
60
70
  * </script>
61
71
  *
62
72
  * <div>
@@ -116,21 +126,32 @@ export function createHotkeySequenceAttachment(
116
126
  options: MaybeGetter<CreateHotkeySequenceOptions> = {},
117
127
  ): Attachment<HTMLElement> {
118
128
  return (element) => {
119
- const resolvedSequence = resolveMaybeGetter(sequence)
129
+ let registration: ReturnType<typeof registerHotkeySequence> | null = null
120
130
 
121
- if (resolvedSequence.length === 0) {
122
- return
123
- }
131
+ $effect(() => {
132
+ const resolvedSequence = resolveMaybeGetter(sequence)
124
133
 
125
- const registration = registerHotkeySequence(
126
- element,
127
- resolvedSequence,
128
- callback,
129
- options,
130
- )
134
+ if (registration?.isActive) {
135
+ registration.unregister()
136
+ registration = null
137
+ }
138
+
139
+ if (resolvedSequence.length === 0) {
140
+ return
141
+ }
142
+
143
+ registration = registerHotkeySequence(
144
+ element,
145
+ resolvedSequence,
146
+ callback,
147
+ options,
148
+ )
149
+ })
131
150
 
132
151
  return () => {
133
- registration.unregister()
152
+ if (registration?.isActive) {
153
+ registration.unregister()
154
+ }
134
155
  }
135
156
  }
136
157
  }
@@ -0,0 +1,281 @@
1
+ import {
2
+ detectPlatform,
3
+ formatHotkey,
4
+ getHotkeyManager,
5
+ rawHotkeyToParsedHotkey,
6
+ } from '@tanstack/hotkeys'
7
+ import { onDestroy } from 'svelte'
8
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity'
9
+ import { getDefaultHotkeysOptions } from './HotkeysCtx'
10
+ import { resolveMaybeGetter } from './internal.svelte'
11
+ import type {
12
+ Hotkey,
13
+ HotkeyCallback,
14
+ HotkeyRegistrationHandle,
15
+ RegisterableHotkey,
16
+ } from '@tanstack/hotkeys'
17
+ import type { CreateHotkeyOptions } from './createHotkey.svelte'
18
+ import type { MaybeGetter } from './internal.svelte'
19
+ import type { Attachment } from 'svelte/attachments'
20
+
21
+ /**
22
+ * A single hotkey definition for use with `createHotkeys`.
23
+ */
24
+ export interface CreateHotkeyDefinition {
25
+ /** The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object */
26
+ hotkey: MaybeGetter<RegisterableHotkey>
27
+ /** The function to call when the hotkey is pressed */
28
+ callback: HotkeyCallback
29
+ /** Per-hotkey options (merged on top of commonOptions) */
30
+ options?: MaybeGetter<CreateHotkeyOptions>
31
+ }
32
+
33
+ function normalizeHotkey(
34
+ hotkey: RegisterableHotkey,
35
+ options: CreateHotkeyOptions,
36
+ ): Hotkey {
37
+ const platform = options.platform ?? detectPlatform()
38
+ return typeof hotkey === 'string'
39
+ ? hotkey
40
+ : (formatHotkey(rawHotkeyToParsedHotkey(hotkey, platform)) as Hotkey)
41
+ }
42
+
43
+ type RegistrationRecord = {
44
+ handle: HotkeyRegistrationHandle
45
+ target: Document | HTMLElement | Window
46
+ }
47
+
48
+ function cleanupRegistrations(
49
+ registrations:
50
+ | Map<string, RegistrationRecord>
51
+ | SvelteMap<string, RegistrationRecord>,
52
+ ) {
53
+ for (const { handle } of registrations.values()) {
54
+ if (handle.isActive) {
55
+ handle.unregister()
56
+ }
57
+ }
58
+ registrations.clear()
59
+ }
60
+
61
+ /**
62
+ * Register multiple global hotkeys for the current component.
63
+ *
64
+ * @example
65
+ * ```svelte
66
+ * <script lang="ts">
67
+ * import { createHotkeys } from '@tanstack/svelte-hotkeys'
68
+ *
69
+ * createHotkeys([
70
+ * { hotkey: 'Mod+S', callback: () => save() },
71
+ * { hotkey: 'Mod+Z', callback: () => undo() },
72
+ * { hotkey: 'Escape', callback: () => close() },
73
+ * ])
74
+ * </script>
75
+ * ```
76
+ */
77
+ export function createHotkeys(
78
+ hotkeys: MaybeGetter<Array<CreateHotkeyDefinition>>,
79
+ commonOptions: MaybeGetter<CreateHotkeyOptions> = {},
80
+ ): void {
81
+ const registrations = new SvelteMap<string, RegistrationRecord>()
82
+
83
+ onDestroy(() => {
84
+ cleanupRegistrations(registrations)
85
+ })
86
+
87
+ $effect(() => {
88
+ if (typeof document === 'undefined') {
89
+ return
90
+ }
91
+
92
+ const resolvedHotkeys = resolveMaybeGetter(hotkeys)
93
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
94
+ const nextKeys = new SvelteSet<string>()
95
+ const prepared: Array<{
96
+ registrationKey: string
97
+ def: CreateHotkeyDefinition
98
+ mergedOptions: CreateHotkeyOptions
99
+ hotkeyString: Hotkey
100
+ resolvedTarget: Document | HTMLElement | Window
101
+ }> = []
102
+
103
+ for (let i = 0; i < resolvedHotkeys.length; i++) {
104
+ const def = resolvedHotkeys[i]!
105
+ const resolvedHotkey = resolveMaybeGetter(def.hotkey)
106
+ const resolvedDefOptions = def.options
107
+ ? resolveMaybeGetter(def.options)
108
+ : {}
109
+
110
+ const mergedOptions = {
111
+ ...getDefaultHotkeysOptions().hotkey,
112
+ ...resolvedCommonOptions,
113
+ ...resolvedDefOptions,
114
+ } as CreateHotkeyOptions
115
+
116
+ const resolvedTarget =
117
+ mergedOptions.target ??
118
+ (typeof document !== 'undefined' ? document : null)
119
+
120
+ if (!resolvedTarget) {
121
+ continue
122
+ }
123
+
124
+ const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions)
125
+ const registrationKey = `${i}:${hotkeyString}`
126
+ nextKeys.add(registrationKey)
127
+ prepared.push({
128
+ registrationKey,
129
+ def,
130
+ mergedOptions,
131
+ hotkeyString,
132
+ resolvedTarget,
133
+ })
134
+ }
135
+
136
+ for (const [key, record] of [...registrations.entries()]) {
137
+ if (!nextKeys.has(key)) {
138
+ if (record.handle.isActive) {
139
+ record.handle.unregister()
140
+ }
141
+ registrations.delete(key)
142
+ }
143
+ }
144
+
145
+ for (const p of prepared) {
146
+ const existing = registrations.get(p.registrationKey)
147
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
148
+
149
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
150
+ existing.handle.callback = p.def.callback
151
+ existing.handle.setOptions(optionsWithoutTarget)
152
+ continue
153
+ }
154
+
155
+ if (existing?.handle.isActive) {
156
+ existing.handle.unregister()
157
+ }
158
+ if (existing) {
159
+ registrations.delete(p.registrationKey)
160
+ }
161
+
162
+ const handle = getHotkeyManager().register(
163
+ p.hotkeyString,
164
+ p.def.callback,
165
+ {
166
+ ...optionsWithoutTarget,
167
+ target: p.resolvedTarget,
168
+ },
169
+ )
170
+ registrations.set(p.registrationKey, {
171
+ handle,
172
+ target: p.resolvedTarget,
173
+ })
174
+ }
175
+ })
176
+ }
177
+
178
+ /**
179
+ * Create an attachment for element-scoped multi-hotkey registration.
180
+ *
181
+ * @example
182
+ * ```svelte
183
+ * <script lang="ts">
184
+ * import { createHotkeysAttachment } from '@tanstack/svelte-hotkeys'
185
+ *
186
+ * const editorKeys = createHotkeysAttachment([
187
+ * { hotkey: 'Mod+S', callback: () => save() },
188
+ * { hotkey: 'Mod+Z', callback: () => undo() },
189
+ * ])
190
+ * </script>
191
+ *
192
+ * <div tabindex="0" {@attach editorKeys}>
193
+ * Editor content
194
+ * </div>
195
+ * ```
196
+ */
197
+ export function createHotkeysAttachment(
198
+ hotkeys: MaybeGetter<Array<CreateHotkeyDefinition>>,
199
+ commonOptions: MaybeGetter<CreateHotkeyOptions> = {},
200
+ ): Attachment<HTMLElement> {
201
+ return (element) => {
202
+ const registrations = new SvelteMap<string, RegistrationRecord>()
203
+
204
+ $effect(() => {
205
+ const resolvedHotkeys = resolveMaybeGetter(hotkeys)
206
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions)
207
+ const nextKeys = new SvelteSet<string>()
208
+ const prepared: Array<{
209
+ registrationKey: string
210
+ def: CreateHotkeyDefinition
211
+ mergedOptions: CreateHotkeyOptions
212
+ hotkeyString: Hotkey
213
+ }> = []
214
+
215
+ for (let i = 0; i < resolvedHotkeys.length; i++) {
216
+ const def = resolvedHotkeys[i]!
217
+ const resolvedHotkey = resolveMaybeGetter(def.hotkey)
218
+ const resolvedDefOptions = def.options
219
+ ? resolveMaybeGetter(def.options)
220
+ : {}
221
+
222
+ const mergedOptions = {
223
+ ...getDefaultHotkeysOptions().hotkey,
224
+ ...resolvedCommonOptions,
225
+ ...resolvedDefOptions,
226
+ } as CreateHotkeyOptions
227
+
228
+ const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions)
229
+ const registrationKey = `${i}:${hotkeyString}`
230
+ nextKeys.add(registrationKey)
231
+ prepared.push({
232
+ registrationKey,
233
+ def,
234
+ mergedOptions,
235
+ hotkeyString,
236
+ })
237
+ }
238
+
239
+ for (const [key, record] of [...registrations.entries()]) {
240
+ if (!nextKeys.has(key)) {
241
+ if (record.handle.isActive) {
242
+ record.handle.unregister()
243
+ }
244
+ registrations.delete(key)
245
+ }
246
+ }
247
+
248
+ for (const p of prepared) {
249
+ const existing = registrations.get(p.registrationKey)
250
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
251
+
252
+ if (existing?.handle.isActive) {
253
+ existing.handle.callback = p.def.callback
254
+ existing.handle.setOptions(optionsWithoutTarget)
255
+ continue
256
+ }
257
+
258
+ if (existing?.handle.isActive) {
259
+ existing.handle.unregister()
260
+ }
261
+ if (existing) {
262
+ registrations.delete(p.registrationKey)
263
+ }
264
+
265
+ const handle = getHotkeyManager().register(
266
+ p.hotkeyString,
267
+ p.def.callback,
268
+ {
269
+ ...optionsWithoutTarget,
270
+ target: element,
271
+ },
272
+ )
273
+ registrations.set(p.registrationKey, { handle, target: element })
274
+ }
275
+ })
276
+
277
+ return () => {
278
+ cleanupRegistrations(registrations)
279
+ }
280
+ }
281
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  export * from '@tanstack/hotkeys'
3
3
 
4
4
  export * from './createHotkey.svelte'
5
+ export * from './createHotkeys.svelte'
5
6
  export * from './createHotkeySequence.svelte'
6
7
  export * from './createHotkeyRecorder.svelte'
7
8
  export * from './getHeldKeys.svelte'