@tanstack/svelte-hotkeys 0.6.0 → 0.8.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/README.md CHANGED
@@ -37,7 +37,7 @@
37
37
  # TanStack Hotkeys
38
38
 
39
39
  > [!NOTE]
40
- > TanStack Hotkeys is pre-alpha (prototyping phase). We are actively developing the library and are open to feedback and contributions.
40
+ > TanStack Hotkeys is alpha. We are actively developing the library and are open to feedback and contributions.
41
41
 
42
42
  Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objects, a cross-platform `Mod` key, a singleton Hotkey Manager, and utilities for cheatsheet UIs—built to stay SSR-friendly.
43
43
 
@@ -1,20 +1,9 @@
1
- import { detectPlatform, formatHotkey, getHotkeyManager, rawHotkeyToParsedHotkey, } from '@tanstack/hotkeys';
1
+ import { detectPlatform, getHotkeyManager, normalizeRegisterableHotkey, } from '@tanstack/hotkeys';
2
+ import { onDestroy } from 'svelte';
2
3
  import { getDefaultHotkeysOptions } from './HotkeysCtx';
3
4
  import { resolveMaybeGetter } from './internal.svelte';
4
- function normalizeHotkey(hotkey, options) {
5
- const platform = options.platform ?? detectPlatform();
6
- return typeof hotkey === 'string'
7
- ? hotkey
8
- : formatHotkey(rawHotkeyToParsedHotkey(hotkey, platform));
9
- }
10
- function registerHotkey(target, hotkey, callback, options) {
11
- const resolvedHotkey = resolveMaybeGetter(hotkey);
12
- const resolvedOptions = resolveMaybeGetter(options);
13
- const mergedOptions = {
14
- ...getDefaultHotkeysOptions().hotkey,
15
- ...resolvedOptions,
16
- };
17
- return getHotkeyManager().register(normalizeHotkey(resolvedHotkey, mergedOptions), callback, {
5
+ function registerHotkey(target, hotkey, callback, mergedOptions) {
6
+ return getHotkeyManager().register(normalizeRegisterableHotkey(hotkey, mergedOptions.platform ?? detectPlatform()), callback, {
18
7
  ...mergedOptions,
19
8
  target,
20
9
  });
@@ -34,14 +23,37 @@ function registerHotkey(target, hotkey, callback, options) {
34
23
  * ```
35
24
  */
36
25
  export function createHotkey(hotkey, callback, options = {}) {
26
+ let registration = null;
27
+ let lastHotkeyStr = null;
37
28
  $effect(() => {
38
29
  if (typeof document === 'undefined') {
39
30
  return;
40
31
  }
41
- const registration = registerHotkey(document, hotkey, callback, options);
42
- return () => {
43
- registration.unregister();
32
+ const resolvedHotkey = resolveMaybeGetter(hotkey);
33
+ const resolvedOptions = resolveMaybeGetter(options);
34
+ const mergedOptions = {
35
+ ...getDefaultHotkeysOptions().hotkey,
36
+ ...resolvedOptions,
44
37
  };
38
+ const hotkeyStr = normalizeRegisterableHotkey(resolvedHotkey, mergedOptions.platform ?? detectPlatform());
39
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions;
40
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
41
+ registration.callback = callback;
42
+ registration.setOptions(optionsWithoutTarget);
43
+ return;
44
+ }
45
+ if (registration?.isActive) {
46
+ registration.unregister();
47
+ registration = null;
48
+ }
49
+ registration = registerHotkey(document, resolvedHotkey, callback, mergedOptions);
50
+ lastHotkeyStr = hotkeyStr;
51
+ });
52
+ onDestroy(() => {
53
+ if (registration?.isActive) {
54
+ registration.unregister();
55
+ registration = null;
56
+ }
45
57
  });
46
58
  }
47
59
  /**
@@ -67,11 +79,27 @@ export function createHotkey(hotkey, callback, options = {}) {
67
79
  export function createHotkeyAttachment(hotkey, callback, options = {}) {
68
80
  return (element) => {
69
81
  let registration = null;
82
+ let lastHotkeyStr = null;
70
83
  $effect(() => {
84
+ const resolvedHotkey = resolveMaybeGetter(hotkey);
85
+ const resolvedOptions = resolveMaybeGetter(options);
86
+ const mergedOptions = {
87
+ ...getDefaultHotkeysOptions().hotkey,
88
+ ...resolvedOptions,
89
+ };
90
+ const hotkeyStr = normalizeRegisterableHotkey(resolvedHotkey, mergedOptions.platform ?? detectPlatform());
91
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions;
92
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
93
+ registration.callback = callback;
94
+ registration.setOptions(optionsWithoutTarget);
95
+ return;
96
+ }
71
97
  if (registration?.isActive) {
72
98
  registration.unregister();
99
+ registration = null;
73
100
  }
74
- registration = registerHotkey(element, hotkey, callback, options);
101
+ registration = registerHotkey(element, resolvedHotkey, callback, mergedOptions);
102
+ lastHotkeyStr = hotkeyStr;
75
103
  });
76
104
  return () => {
77
105
  if (registration?.isActive) {
@@ -1,14 +1,9 @@
1
- import { getSequenceManager } from '@tanstack/hotkeys';
1
+ import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys';
2
+ import { onDestroy } from 'svelte';
2
3
  import { getDefaultHotkeysOptions } from './HotkeysCtx';
3
4
  import { resolveMaybeGetter } from './internal.svelte';
4
- function registerHotkeySequence(target, sequence, callback, options) {
5
- const resolvedSequence = resolveMaybeGetter(sequence);
6
- const resolvedOptions = resolveMaybeGetter(options);
7
- const mergedOptions = {
8
- ...getDefaultHotkeysOptions().hotkeySequence,
9
- ...resolvedOptions,
10
- };
11
- return getSequenceManager().register(resolvedSequence, callback, {
5
+ function registerHotkeySequence(target, sequence, callback, mergedOptions) {
6
+ return getSequenceManager().register(sequence, callback, {
12
7
  ...mergedOptions,
13
8
  target,
14
9
  });
@@ -53,18 +48,45 @@ function registerHotkeySequence(target, sequence, callback, options) {
53
48
  * ```
54
49
  */
55
50
  export function createHotkeySequence(sequence, callback, options = {}) {
51
+ let registration = null;
52
+ let lastSequenceKey = null;
56
53
  $effect(() => {
57
54
  if (typeof document === 'undefined') {
58
55
  return;
59
56
  }
60
57
  const resolvedSequence = resolveMaybeGetter(sequence);
58
+ const resolvedOptions = resolveMaybeGetter(options);
59
+ const mergedOptions = {
60
+ ...getDefaultHotkeysOptions().hotkeySequence,
61
+ ...resolvedOptions,
62
+ };
63
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions;
61
64
  if (resolvedSequence.length === 0) {
65
+ if (registration?.isActive) {
66
+ registration.unregister();
67
+ registration = null;
68
+ }
69
+ lastSequenceKey = null;
62
70
  return;
63
71
  }
64
- const registration = registerHotkeySequence(document, resolvedSequence, callback, options);
65
- return () => {
72
+ const sequenceKey = formatHotkeySequence(resolvedSequence);
73
+ if (registration?.isActive && lastSequenceKey === sequenceKey) {
74
+ registration.callback = callback;
75
+ registration.setOptions(optionsWithoutTarget);
76
+ return;
77
+ }
78
+ if (registration?.isActive) {
66
79
  registration.unregister();
67
- };
80
+ registration = null;
81
+ }
82
+ registration = registerHotkeySequence(document, resolvedSequence, callback, mergedOptions);
83
+ lastSequenceKey = sequenceKey;
84
+ });
85
+ onDestroy(() => {
86
+ if (registration?.isActive) {
87
+ registration.unregister();
88
+ registration = null;
89
+ }
68
90
  });
69
91
  }
70
92
  /**
@@ -88,16 +110,35 @@ export function createHotkeySequence(sequence, callback, options = {}) {
88
110
  export function createHotkeySequenceAttachment(sequence, callback, options = {}) {
89
111
  return (element) => {
90
112
  let registration = null;
113
+ let lastSequenceKey = null;
91
114
  $effect(() => {
92
115
  const resolvedSequence = resolveMaybeGetter(sequence);
116
+ const resolvedOptions = resolveMaybeGetter(options);
117
+ const mergedOptions = {
118
+ ...getDefaultHotkeysOptions().hotkeySequence,
119
+ ...resolvedOptions,
120
+ };
121
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions;
122
+ if (resolvedSequence.length === 0) {
123
+ if (registration?.isActive) {
124
+ registration.unregister();
125
+ registration = null;
126
+ }
127
+ lastSequenceKey = null;
128
+ return;
129
+ }
130
+ const sequenceKey = formatHotkeySequence(resolvedSequence);
131
+ if (registration?.isActive && lastSequenceKey === sequenceKey) {
132
+ registration.callback = callback;
133
+ registration.setOptions(optionsWithoutTarget);
134
+ return;
135
+ }
93
136
  if (registration?.isActive) {
94
137
  registration.unregister();
95
138
  registration = null;
96
139
  }
97
- if (resolvedSequence.length === 0) {
98
- return;
99
- }
100
- registration = registerHotkeySequence(element, resolvedSequence, callback, options);
140
+ registration = registerHotkeySequence(element, resolvedSequence, callback, mergedOptions);
141
+ lastSequenceKey = sequenceKey;
101
142
  });
102
143
  return () => {
103
144
  if (registration?.isActive) {
@@ -0,0 +1,49 @@
1
+ import type { HotkeyCallback, HotkeySequence } from '@tanstack/hotkeys';
2
+ import type { CreateHotkeySequenceOptions } from './createHotkeySequence.svelte';
3
+ import type { MaybeGetter } from './internal.svelte';
4
+ import type { Attachment } from 'svelte/attachments';
5
+ /**
6
+ * A single sequence definition for use with `createHotkeySequences`.
7
+ */
8
+ export interface CreateHotkeySequenceDefinition {
9
+ /** Array of hotkey strings that form the sequence */
10
+ sequence: MaybeGetter<HotkeySequence>;
11
+ /** The function to call when the sequence is completed */
12
+ callback: HotkeyCallback;
13
+ /** Per-sequence options (merged on top of commonOptions) */
14
+ options?: MaybeGetter<CreateHotkeySequenceOptions>;
15
+ }
16
+ /**
17
+ * Register multiple global keyboard shortcut sequences for the current component.
18
+ *
19
+ * @example
20
+ * ```svelte
21
+ * <script lang="ts">
22
+ * import { createHotkeySequences } from '@tanstack/svelte-hotkeys'
23
+ *
24
+ * createHotkeySequences([
25
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
26
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
27
+ * ])
28
+ * </script>
29
+ * ```
30
+ */
31
+ export declare function createHotkeySequences(definitions: MaybeGetter<Array<CreateHotkeySequenceDefinition>>, commonOptions?: MaybeGetter<CreateHotkeySequenceOptions>): void;
32
+ /**
33
+ * Create an attachment for element-scoped multi-sequence registration.
34
+ *
35
+ * @example
36
+ * ```svelte
37
+ * <script lang="ts">
38
+ * import { createHotkeySequencesAttachment } from '@tanstack/svelte-hotkeys'
39
+ *
40
+ * const vim = createHotkeySequencesAttachment([
41
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
42
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
43
+ * ])
44
+ * </script>
45
+ *
46
+ * <div tabindex="0" {@attach vim}>Editor</div>
47
+ * ```
48
+ */
49
+ export declare function createHotkeySequencesAttachment(definitions: MaybeGetter<Array<CreateHotkeySequenceDefinition>>, commonOptions?: MaybeGetter<CreateHotkeySequenceOptions>): Attachment<HTMLElement>;
@@ -0,0 +1,189 @@
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
+ function cleanupRegistrations(registrations) {
7
+ for (const { handle } of registrations.values()) {
8
+ if (handle.isActive) {
9
+ handle.unregister();
10
+ }
11
+ }
12
+ registrations.clear();
13
+ }
14
+ /**
15
+ * Register multiple global keyboard shortcut sequences for the current component.
16
+ *
17
+ * @example
18
+ * ```svelte
19
+ * <script lang="ts">
20
+ * import { createHotkeySequences } from '@tanstack/svelte-hotkeys'
21
+ *
22
+ * createHotkeySequences([
23
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
24
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
25
+ * ])
26
+ * </script>
27
+ * ```
28
+ */
29
+ export function createHotkeySequences(definitions, commonOptions = {}) {
30
+ const registrations = new SvelteMap();
31
+ onDestroy(() => {
32
+ cleanupRegistrations(registrations);
33
+ });
34
+ $effect(() => {
35
+ if (typeof document === 'undefined') {
36
+ return;
37
+ }
38
+ const resolvedDefinitions = resolveMaybeGetter(definitions);
39
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions);
40
+ const nextKeys = new SvelteSet();
41
+ const prepared = [];
42
+ for (let i = 0; i < resolvedDefinitions.length; i++) {
43
+ const def = resolvedDefinitions[i];
44
+ const resolvedSequence = resolveMaybeGetter(def.sequence);
45
+ if (resolvedSequence.length === 0) {
46
+ continue;
47
+ }
48
+ const resolvedDefOptions = def.options
49
+ ? resolveMaybeGetter(def.options)
50
+ : {};
51
+ const mergedOptions = {
52
+ ...getDefaultHotkeysOptions().hotkeySequence,
53
+ ...resolvedCommonOptions,
54
+ ...resolvedDefOptions,
55
+ };
56
+ const resolvedTarget = mergedOptions.target ??
57
+ (typeof document !== 'undefined' ? document : null);
58
+ if (!resolvedTarget) {
59
+ continue;
60
+ }
61
+ const sequenceString = formatHotkeySequence(resolvedSequence);
62
+ const registrationKey = `${i}:${sequenceString}`;
63
+ nextKeys.add(registrationKey);
64
+ prepared.push({
65
+ registrationKey,
66
+ def,
67
+ mergedOptions,
68
+ sequenceString,
69
+ resolvedSequence,
70
+ resolvedTarget,
71
+ });
72
+ }
73
+ for (const [key, record] of [...registrations.entries()]) {
74
+ if (!nextKeys.has(key)) {
75
+ if (record.handle.isActive) {
76
+ record.handle.unregister();
77
+ }
78
+ registrations.delete(key);
79
+ }
80
+ }
81
+ for (const p of prepared) {
82
+ const existing = registrations.get(p.registrationKey);
83
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
84
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
85
+ existing.handle.callback = p.def.callback;
86
+ existing.handle.setOptions(optionsWithoutTarget);
87
+ continue;
88
+ }
89
+ if (existing?.handle.isActive) {
90
+ existing.handle.unregister();
91
+ }
92
+ if (existing) {
93
+ registrations.delete(p.registrationKey);
94
+ }
95
+ const handle = getSequenceManager().register(p.resolvedSequence, p.def.callback, {
96
+ ...optionsWithoutTarget,
97
+ target: p.resolvedTarget,
98
+ });
99
+ registrations.set(p.registrationKey, {
100
+ handle,
101
+ target: p.resolvedTarget,
102
+ });
103
+ }
104
+ });
105
+ }
106
+ /**
107
+ * Create an attachment for element-scoped multi-sequence registration.
108
+ *
109
+ * @example
110
+ * ```svelte
111
+ * <script lang="ts">
112
+ * import { createHotkeySequencesAttachment } from '@tanstack/svelte-hotkeys'
113
+ *
114
+ * const vim = createHotkeySequencesAttachment([
115
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
116
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
117
+ * ])
118
+ * </script>
119
+ *
120
+ * <div tabindex="0" {@attach vim}>Editor</div>
121
+ * ```
122
+ */
123
+ export function createHotkeySequencesAttachment(definitions, commonOptions = {}) {
124
+ return (element) => {
125
+ const registrations = new SvelteMap();
126
+ $effect(() => {
127
+ const resolvedDefinitions = resolveMaybeGetter(definitions);
128
+ const resolvedCommonOptions = resolveMaybeGetter(commonOptions);
129
+ const nextKeys = new SvelteSet();
130
+ const prepared = [];
131
+ for (let i = 0; i < resolvedDefinitions.length; i++) {
132
+ const def = resolvedDefinitions[i];
133
+ const resolvedSequence = resolveMaybeGetter(def.sequence);
134
+ if (resolvedSequence.length === 0) {
135
+ continue;
136
+ }
137
+ const resolvedDefOptions = def.options
138
+ ? resolveMaybeGetter(def.options)
139
+ : {};
140
+ const mergedOptions = {
141
+ ...getDefaultHotkeysOptions().hotkeySequence,
142
+ ...resolvedCommonOptions,
143
+ ...resolvedDefOptions,
144
+ };
145
+ const sequenceString = formatHotkeySequence(resolvedSequence);
146
+ const registrationKey = `${i}:${sequenceString}`;
147
+ nextKeys.add(registrationKey);
148
+ prepared.push({
149
+ registrationKey,
150
+ def,
151
+ mergedOptions,
152
+ sequenceString,
153
+ resolvedSequence,
154
+ });
155
+ }
156
+ for (const [key, record] of [...registrations.entries()]) {
157
+ if (!nextKeys.has(key)) {
158
+ if (record.handle.isActive) {
159
+ record.handle.unregister();
160
+ }
161
+ registrations.delete(key);
162
+ }
163
+ }
164
+ for (const p of prepared) {
165
+ const existing = registrations.get(p.registrationKey);
166
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
167
+ if (existing?.handle.isActive && existing.target === element) {
168
+ existing.handle.callback = p.def.callback;
169
+ existing.handle.setOptions(optionsWithoutTarget);
170
+ continue;
171
+ }
172
+ if (existing?.handle.isActive) {
173
+ existing.handle.unregister();
174
+ }
175
+ if (existing) {
176
+ registrations.delete(p.registrationKey);
177
+ }
178
+ const handle = getSequenceManager().register(p.resolvedSequence, p.def.callback, {
179
+ ...optionsWithoutTarget,
180
+ target: element,
181
+ });
182
+ registrations.set(p.registrationKey, { handle, target: element });
183
+ }
184
+ });
185
+ return () => {
186
+ cleanupRegistrations(registrations);
187
+ };
188
+ };
189
+ }
@@ -1,14 +1,8 @@
1
- import { detectPlatform, formatHotkey, getHotkeyManager, rawHotkeyToParsedHotkey, } from '@tanstack/hotkeys';
1
+ import { detectPlatform, getHotkeyManager, normalizeRegisterableHotkey, } from '@tanstack/hotkeys';
2
2
  import { onDestroy } from 'svelte';
3
3
  import { SvelteMap, SvelteSet } from 'svelte/reactivity';
4
4
  import { getDefaultHotkeysOptions } from './HotkeysCtx';
5
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
6
  function cleanupRegistrations(registrations) {
13
7
  for (const { handle } of registrations.values()) {
14
8
  if (handle.isActive) {
@@ -62,7 +56,7 @@ export function createHotkeys(hotkeys, commonOptions = {}) {
62
56
  if (!resolvedTarget) {
63
57
  continue;
64
58
  }
65
- const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions);
59
+ const hotkeyString = normalizeRegisterableHotkey(resolvedHotkey, mergedOptions.platform ?? detectPlatform());
66
60
  const registrationKey = `${i}:${hotkeyString}`;
67
61
  nextKeys.add(registrationKey);
68
62
  prepared.push({
@@ -144,7 +138,7 @@ export function createHotkeysAttachment(hotkeys, commonOptions = {}) {
144
138
  ...resolvedCommonOptions,
145
139
  ...resolvedDefOptions,
146
140
  };
147
- const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions);
141
+ const hotkeyString = normalizeRegisterableHotkey(resolvedHotkey, mergedOptions.platform ?? detectPlatform());
148
142
  const registrationKey = `${i}:${hotkeyString}`;
149
143
  nextKeys.add(registrationKey);
150
144
  prepared.push({
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from '@tanstack/hotkeys';
2
2
  export * from './createHotkey.svelte';
3
3
  export * from './createHotkeys.svelte';
4
4
  export * from './createHotkeySequence.svelte';
5
+ export * from './createHotkeySequences.svelte';
5
6
  export * from './createHotkeyRecorder.svelte';
6
7
  export * from './createHotkeySequenceRecorder.svelte';
7
8
  export * from './getHeldKeys.svelte';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from '@tanstack/hotkeys';
3
3
  export * from './createHotkey.svelte';
4
4
  export * from './createHotkeys.svelte';
5
5
  export * from './createHotkeySequence.svelte';
6
+ export * from './createHotkeySequences.svelte';
6
7
  export * from './createHotkeyRecorder.svelte';
7
8
  export * from './createHotkeySequenceRecorder.svelte';
8
9
  export * from './getHeldKeys.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/svelte-hotkeys",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Svelte adapter for TanStack Hotkeys",
5
5
  "author": "Kunal Rao",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "src"
41
41
  ],
42
42
  "dependencies": {
43
- "@tanstack/hotkeys": "0.5.0"
43
+ "@tanstack/hotkeys": "0.6.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@sveltejs/package": "^2.5.7",
@@ -1,15 +1,16 @@
1
1
  import {
2
2
  detectPlatform,
3
- formatHotkey,
4
3
  getHotkeyManager,
5
- rawHotkeyToParsedHotkey,
4
+ normalizeRegisterableHotkey,
6
5
  } from '@tanstack/hotkeys'
6
+ import { onDestroy } from 'svelte'
7
7
  import { getDefaultHotkeysOptions } from './HotkeysCtx'
8
8
  import { resolveMaybeGetter } from './internal.svelte'
9
9
  import type {
10
10
  Hotkey,
11
11
  HotkeyCallback,
12
12
  HotkeyOptions,
13
+ HotkeyRegistrationHandle,
13
14
  RegisterableHotkey,
14
15
  } from '@tanstack/hotkeys'
15
16
  import type { MaybeGetter } from './internal.svelte'
@@ -19,32 +20,17 @@ export interface CreateHotkeyOptions extends Omit<HotkeyOptions, 'target'> {
19
20
  target?: Document | Window // not html elements, use attachment instead
20
21
  }
21
22
 
22
- function normalizeHotkey(
23
- hotkey: RegisterableHotkey,
24
- options: CreateHotkeyOptions,
25
- ): Hotkey {
26
- const platform = options.platform ?? detectPlatform()
27
-
28
- return typeof hotkey === 'string'
29
- ? hotkey
30
- : (formatHotkey(rawHotkeyToParsedHotkey(hotkey, platform)) as Hotkey)
31
- }
32
-
33
23
  function registerHotkey(
34
24
  target: HTMLElement | Document | Window,
35
- hotkey: MaybeGetter<RegisterableHotkey>,
25
+ hotkey: RegisterableHotkey,
36
26
  callback: HotkeyCallback,
37
- options: MaybeGetter<CreateHotkeyOptions>,
27
+ mergedOptions: CreateHotkeyOptions,
38
28
  ) {
39
- const resolvedHotkey = resolveMaybeGetter(hotkey)
40
- const resolvedOptions = resolveMaybeGetter(options)
41
- const mergedOptions = {
42
- ...getDefaultHotkeysOptions().hotkey,
43
- ...resolvedOptions,
44
- } as CreateHotkeyOptions
45
-
46
29
  return getHotkeyManager().register(
47
- normalizeHotkey(resolvedHotkey, mergedOptions),
30
+ normalizeRegisterableHotkey(
31
+ hotkey,
32
+ mergedOptions.platform ?? detectPlatform(),
33
+ ),
48
34
  callback,
49
35
  {
50
36
  ...mergedOptions,
@@ -72,15 +58,51 @@ export function createHotkey(
72
58
  callback: HotkeyCallback,
73
59
  options: MaybeGetter<CreateHotkeyOptions> = {},
74
60
  ): void {
61
+ let registration: HotkeyRegistrationHandle | null = null
62
+ let lastHotkeyStr: Hotkey | null = null
63
+
75
64
  $effect(() => {
76
65
  if (typeof document === 'undefined') {
77
66
  return
78
67
  }
79
68
 
80
- const registration = registerHotkey(document, hotkey, callback, options)
69
+ const resolvedHotkey = resolveMaybeGetter(hotkey)
70
+ const resolvedOptions = resolveMaybeGetter(options)
71
+ const mergedOptions = {
72
+ ...getDefaultHotkeysOptions().hotkey,
73
+ ...resolvedOptions,
74
+ } as CreateHotkeyOptions
81
75
 
82
- return () => {
76
+ const hotkeyStr = normalizeRegisterableHotkey(
77
+ resolvedHotkey,
78
+ mergedOptions.platform ?? detectPlatform(),
79
+ )
80
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
81
+
82
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
83
+ registration.callback = callback
84
+ registration.setOptions(optionsWithoutTarget)
85
+ return
86
+ }
87
+
88
+ if (registration?.isActive) {
83
89
  registration.unregister()
90
+ registration = null
91
+ }
92
+
93
+ registration = registerHotkey(
94
+ document,
95
+ resolvedHotkey,
96
+ callback,
97
+ mergedOptions,
98
+ )
99
+ lastHotkeyStr = hotkeyStr
100
+ })
101
+
102
+ onDestroy(() => {
103
+ if (registration?.isActive) {
104
+ registration.unregister()
105
+ registration = null
84
106
  }
85
107
  })
86
108
  }
@@ -111,14 +133,41 @@ export function createHotkeyAttachment(
111
133
  options: MaybeGetter<CreateHotkeyOptions> = {},
112
134
  ): Attachment<HTMLElement> {
113
135
  return (element) => {
114
- let registration: ReturnType<typeof registerHotkey> | null = null
136
+ let registration: HotkeyRegistrationHandle | null = null
137
+ let lastHotkeyStr: Hotkey | null = null
115
138
 
116
139
  $effect(() => {
140
+ const resolvedHotkey = resolveMaybeGetter(hotkey)
141
+ const resolvedOptions = resolveMaybeGetter(options)
142
+ const mergedOptions = {
143
+ ...getDefaultHotkeysOptions().hotkey,
144
+ ...resolvedOptions,
145
+ } as CreateHotkeyOptions
146
+
147
+ const hotkeyStr = normalizeRegisterableHotkey(
148
+ resolvedHotkey,
149
+ mergedOptions.platform ?? detectPlatform(),
150
+ )
151
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
152
+
153
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
154
+ registration.callback = callback
155
+ registration.setOptions(optionsWithoutTarget)
156
+ return
157
+ }
158
+
117
159
  if (registration?.isActive) {
118
160
  registration.unregister()
161
+ registration = null
119
162
  }
120
163
 
121
- registration = registerHotkey(element, hotkey, callback, options)
164
+ registration = registerHotkey(
165
+ element,
166
+ resolvedHotkey,
167
+ callback,
168
+ mergedOptions,
169
+ )
170
+ lastHotkeyStr = hotkeyStr
122
171
  })
123
172
 
124
173
  return () => {
@@ -1,10 +1,12 @@
1
- import { getSequenceManager } from '@tanstack/hotkeys'
1
+ import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
2
+ import { onDestroy } from 'svelte'
2
3
  import { getDefaultHotkeysOptions } from './HotkeysCtx'
3
4
  import { resolveMaybeGetter } from './internal.svelte'
4
5
  import type {
5
6
  HotkeyCallback,
6
7
  HotkeySequence,
7
8
  SequenceOptions,
9
+ SequenceRegistrationHandle,
8
10
  } from '@tanstack/hotkeys'
9
11
  import type { MaybeGetter } from './internal.svelte'
10
12
  import type { Attachment } from 'svelte/attachments'
@@ -18,18 +20,11 @@ export interface CreateHotkeySequenceOptions extends Omit<
18
20
 
19
21
  function registerHotkeySequence(
20
22
  target: HTMLElement | Document | Window,
21
- sequence: MaybeGetter<HotkeySequence>,
23
+ sequence: HotkeySequence,
22
24
  callback: HotkeyCallback,
23
- options: MaybeGetter<CreateHotkeySequenceOptions>,
25
+ mergedOptions: CreateHotkeySequenceOptions,
24
26
  ) {
25
- const resolvedSequence = resolveMaybeGetter(sequence)
26
- const resolvedOptions = resolveMaybeGetter(options)
27
- const mergedOptions = {
28
- ...getDefaultHotkeysOptions().hotkeySequence,
29
- ...resolvedOptions,
30
- } as CreateHotkeySequenceOptions
31
-
32
- return getSequenceManager().register(resolvedSequence, callback, {
27
+ return getSequenceManager().register(sequence, callback, {
33
28
  ...mergedOptions,
34
29
  target,
35
30
  })
@@ -79,25 +74,58 @@ export function createHotkeySequence(
79
74
  callback: HotkeyCallback,
80
75
  options: MaybeGetter<CreateHotkeySequenceOptions> = {},
81
76
  ): void {
77
+ let registration: SequenceRegistrationHandle | null = null
78
+ let lastSequenceKey: string | null = null
79
+
82
80
  $effect(() => {
83
81
  if (typeof document === 'undefined') {
84
82
  return
85
83
  }
86
84
 
87
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
+
88
94
  if (resolvedSequence.length === 0) {
95
+ if (registration?.isActive) {
96
+ registration.unregister()
97
+ registration = null
98
+ }
99
+ lastSequenceKey = null
89
100
  return
90
101
  }
91
102
 
92
- const registration = registerHotkeySequence(
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(
93
117
  document,
94
118
  resolvedSequence,
95
119
  callback,
96
- options,
120
+ mergedOptions,
97
121
  )
122
+ lastSequenceKey = sequenceKey
123
+ })
98
124
 
99
- return () => {
125
+ onDestroy(() => {
126
+ if (registration?.isActive) {
100
127
  registration.unregister()
128
+ registration = null
101
129
  }
102
130
  })
103
131
  }
@@ -126,26 +154,48 @@ export function createHotkeySequenceAttachment(
126
154
  options: MaybeGetter<CreateHotkeySequenceOptions> = {},
127
155
  ): Attachment<HTMLElement> {
128
156
  return (element) => {
129
- let registration: ReturnType<typeof registerHotkeySequence> | null = null
157
+ let registration: SequenceRegistrationHandle | null = null
158
+ let lastSequenceKey: string | null = null
130
159
 
131
160
  $effect(() => {
132
161
  const resolvedSequence = resolveMaybeGetter(sequence)
162
+ const resolvedOptions = resolveMaybeGetter(options)
163
+ const mergedOptions = {
164
+ ...getDefaultHotkeysOptions().hotkeySequence,
165
+ ...resolvedOptions,
166
+ } as CreateHotkeySequenceOptions
133
167
 
134
- if (registration?.isActive) {
135
- registration.unregister()
136
- registration = null
137
- }
168
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
138
169
 
139
170
  if (resolvedSequence.length === 0) {
171
+ if (registration?.isActive) {
172
+ registration.unregister()
173
+ registration = null
174
+ }
175
+ lastSequenceKey = null
140
176
  return
141
177
  }
142
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
+
143
192
  registration = registerHotkeySequence(
144
193
  element,
145
194
  resolvedSequence,
146
195
  callback,
147
- options,
196
+ mergedOptions,
148
197
  )
198
+ lastSequenceKey = sequenceKey
149
199
  })
150
200
 
151
201
  return () => {
@@ -0,0 +1,274 @@
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,8 +1,7 @@
1
1
  import {
2
2
  detectPlatform,
3
- formatHotkey,
4
3
  getHotkeyManager,
5
- rawHotkeyToParsedHotkey,
4
+ normalizeRegisterableHotkey,
6
5
  } from '@tanstack/hotkeys'
7
6
  import { onDestroy } from 'svelte'
8
7
  import { SvelteMap, SvelteSet } from 'svelte/reactivity'
@@ -30,16 +29,6 @@ export interface CreateHotkeyDefinition {
30
29
  options?: MaybeGetter<CreateHotkeyOptions>
31
30
  }
32
31
 
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
32
  type RegistrationRecord = {
44
33
  handle: HotkeyRegistrationHandle
45
34
  target: Document | HTMLElement | Window
@@ -121,7 +110,10 @@ export function createHotkeys(
121
110
  continue
122
111
  }
123
112
 
124
- const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions)
113
+ const hotkeyString = normalizeRegisterableHotkey(
114
+ resolvedHotkey,
115
+ mergedOptions.platform ?? detectPlatform(),
116
+ )
125
117
  const registrationKey = `${i}:${hotkeyString}`
126
118
  nextKeys.add(registrationKey)
127
119
  prepared.push({
@@ -225,7 +217,10 @@ export function createHotkeysAttachment(
225
217
  ...resolvedDefOptions,
226
218
  } as CreateHotkeyOptions
227
219
 
228
- const hotkeyString = normalizeHotkey(resolvedHotkey, mergedOptions)
220
+ const hotkeyString = normalizeRegisterableHotkey(
221
+ resolvedHotkey,
222
+ mergedOptions.platform ?? detectPlatform(),
223
+ )
229
224
  const registrationKey = `${i}:${hotkeyString}`
230
225
  nextKeys.add(registrationKey)
231
226
  prepared.push({
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from '@tanstack/hotkeys'
4
4
  export * from './createHotkey.svelte'
5
5
  export * from './createHotkeys.svelte'
6
6
  export * from './createHotkeySequence.svelte'
7
+ export * from './createHotkeySequences.svelte'
7
8
  export * from './createHotkeyRecorder.svelte'
8
9
  export * from './createHotkeySequenceRecorder.svelte'
9
10
  export * from './getHeldKeys.svelte'