@tanstack/svelte-hotkeys 0.5.1 → 0.7.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,10 +1,11 @@
1
- import type { HotkeyRecorderOptions } from '@tanstack/hotkeys';
1
+ import type { HotkeyRecorderOptions, HotkeySequenceRecorderOptions } from '@tanstack/hotkeys';
2
2
  import type { CreateHotkeyOptions } from './createHotkey.svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import type { CreateHotkeySequenceOptions } from './createHotkeySequence.svelte';
5
5
  export interface HotkeysProviderOptions {
6
6
  hotkey?: Partial<CreateHotkeyOptions>;
7
7
  hotkeyRecorder?: Partial<HotkeyRecorderOptions>;
8
+ hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>;
8
9
  hotkeySequence?: Partial<CreateHotkeySequenceOptions>;
9
10
  }
10
11
  export interface HotkeysProviderProps {
@@ -1,4 +1,5 @@
1
1
  import { detectPlatform, formatHotkey, getHotkeyManager, rawHotkeyToParsedHotkey, } from '@tanstack/hotkeys';
2
+ import { onDestroy } from 'svelte';
2
3
  import { getDefaultHotkeysOptions } from './HotkeysCtx';
3
4
  import { resolveMaybeGetter } from './internal.svelte';
4
5
  function normalizeHotkey(hotkey, options) {
@@ -7,14 +8,8 @@ function normalizeHotkey(hotkey, options) {
7
8
  ? hotkey
8
9
  : formatHotkey(rawHotkeyToParsedHotkey(hotkey, platform));
9
10
  }
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, {
11
+ function registerHotkey(target, hotkey, callback, mergedOptions) {
12
+ return getHotkeyManager().register(normalizeHotkey(hotkey, mergedOptions), callback, {
18
13
  ...mergedOptions,
19
14
  target,
20
15
  });
@@ -34,14 +29,37 @@ function registerHotkey(target, hotkey, callback, options) {
34
29
  * ```
35
30
  */
36
31
  export function createHotkey(hotkey, callback, options = {}) {
32
+ let registration = null;
33
+ let lastHotkeyStr = null;
37
34
  $effect(() => {
38
35
  if (typeof document === 'undefined') {
39
36
  return;
40
37
  }
41
- const registration = registerHotkey(document, hotkey, callback, options);
42
- return () => {
43
- registration.unregister();
38
+ const resolvedHotkey = resolveMaybeGetter(hotkey);
39
+ const resolvedOptions = resolveMaybeGetter(options);
40
+ const mergedOptions = {
41
+ ...getDefaultHotkeysOptions().hotkey,
42
+ ...resolvedOptions,
44
43
  };
44
+ const hotkeyStr = normalizeHotkey(resolvedHotkey, mergedOptions);
45
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions;
46
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
47
+ registration.callback = callback;
48
+ registration.setOptions(optionsWithoutTarget);
49
+ return;
50
+ }
51
+ if (registration?.isActive) {
52
+ registration.unregister();
53
+ registration = null;
54
+ }
55
+ registration = registerHotkey(document, resolvedHotkey, callback, mergedOptions);
56
+ lastHotkeyStr = hotkeyStr;
57
+ });
58
+ onDestroy(() => {
59
+ if (registration?.isActive) {
60
+ registration.unregister();
61
+ registration = null;
62
+ }
45
63
  });
46
64
  }
47
65
  /**
@@ -67,11 +85,27 @@ export function createHotkey(hotkey, callback, options = {}) {
67
85
  export function createHotkeyAttachment(hotkey, callback, options = {}) {
68
86
  return (element) => {
69
87
  let registration = null;
88
+ let lastHotkeyStr = null;
70
89
  $effect(() => {
90
+ const resolvedHotkey = resolveMaybeGetter(hotkey);
91
+ const resolvedOptions = resolveMaybeGetter(options);
92
+ const mergedOptions = {
93
+ ...getDefaultHotkeysOptions().hotkey,
94
+ ...resolvedOptions,
95
+ };
96
+ const hotkeyStr = normalizeHotkey(resolvedHotkey, mergedOptions);
97
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions;
98
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
99
+ registration.callback = callback;
100
+ registration.setOptions(optionsWithoutTarget);
101
+ return;
102
+ }
71
103
  if (registration?.isActive) {
72
104
  registration.unregister();
105
+ registration = null;
73
106
  }
74
- registration = registerHotkey(element, hotkey, callback, options);
107
+ registration = registerHotkey(element, resolvedHotkey, callback, mergedOptions);
108
+ lastHotkeyStr = hotkeyStr;
75
109
  });
76
110
  return () => {
77
111
  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,15 @@
1
+ import type { HotkeySequence, HotkeySequenceRecorderOptions } from '@tanstack/hotkeys';
2
+ import type { MaybeGetter } from './internal.svelte';
3
+ export interface SvelteHotkeySequenceRecorder {
4
+ readonly isRecording: boolean;
5
+ readonly steps: HotkeySequence;
6
+ readonly recordedSequence: HotkeySequence | null;
7
+ startRecording: () => void;
8
+ stopRecording: () => void;
9
+ cancelRecording: () => void;
10
+ commitRecording: () => void;
11
+ }
12
+ /**
13
+ * Svelte helper for recording multi-chord sequences (Vim-style shortcuts).
14
+ */
15
+ export declare function createHotkeySequenceRecorder(options: MaybeGetter<HotkeySequenceRecorderOptions>): SvelteHotkeySequenceRecorder;
@@ -0,0 +1,74 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _SvelteHotkeySequenceRecorderState_recorder, _SvelteHotkeySequenceRecorderState_subscribe;
13
+ import { HotkeySequenceRecorder } from '@tanstack/hotkeys';
14
+ import { onDestroy } from 'svelte';
15
+ import { getDefaultHotkeysOptions } from './HotkeysCtx';
16
+ import { createStoreSubscriber, resolveMaybeGetter } from './internal.svelte';
17
+ class SvelteHotkeySequenceRecorderState {
18
+ constructor(options) {
19
+ _SvelteHotkeySequenceRecorderState_recorder.set(this, void 0);
20
+ _SvelteHotkeySequenceRecorderState_subscribe.set(this, void 0);
21
+ __classPrivateFieldSet(this, _SvelteHotkeySequenceRecorderState_recorder, new HotkeySequenceRecorder(options), "f");
22
+ __classPrivateFieldSet(this, _SvelteHotkeySequenceRecorderState_subscribe, createStoreSubscriber(__classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").store), "f");
23
+ }
24
+ get isRecording() {
25
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_subscribe, "f").call(this);
26
+ return __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").store.state.isRecording;
27
+ }
28
+ get steps() {
29
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_subscribe, "f").call(this);
30
+ return __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").store.state.steps;
31
+ }
32
+ get recordedSequence() {
33
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_subscribe, "f").call(this);
34
+ return __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").store.state.recordedSequence;
35
+ }
36
+ setOptions(options) {
37
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").setOptions(options);
38
+ }
39
+ startRecording() {
40
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").start();
41
+ }
42
+ stopRecording() {
43
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").stop();
44
+ }
45
+ cancelRecording() {
46
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").cancel();
47
+ }
48
+ commitRecording() {
49
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").commit();
50
+ }
51
+ destroy() {
52
+ __classPrivateFieldGet(this, _SvelteHotkeySequenceRecorderState_recorder, "f").destroy();
53
+ }
54
+ }
55
+ _SvelteHotkeySequenceRecorderState_recorder = new WeakMap(), _SvelteHotkeySequenceRecorderState_subscribe = new WeakMap();
56
+ /**
57
+ * Svelte helper for recording multi-chord sequences (Vim-style shortcuts).
58
+ */
59
+ export function createHotkeySequenceRecorder(options) {
60
+ const recorder = new SvelteHotkeySequenceRecorderState({
61
+ ...getDefaultHotkeysOptions().hotkeySequenceRecorder,
62
+ ...resolveMaybeGetter(options),
63
+ });
64
+ $effect(() => {
65
+ recorder.setOptions({
66
+ ...getDefaultHotkeysOptions().hotkeySequenceRecorder,
67
+ ...resolveMaybeGetter(options),
68
+ });
69
+ });
70
+ onDestroy(() => {
71
+ recorder.destroy();
72
+ });
73
+ return recorder;
74
+ }
@@ -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
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,9 @@ 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';
7
+ export * from './createHotkeySequenceRecorder.svelte';
6
8
  export * from './getHeldKeys.svelte';
7
9
  export * from './getHeldKeyCodesMap.svelte';
8
10
  export * from './getIsKeyHeld.svelte';
package/dist/index.js CHANGED
@@ -3,7 +3,9 @@ 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';
8
+ export * from './createHotkeySequenceRecorder.svelte';
7
9
  export * from './getHeldKeys.svelte';
8
10
  export * from './getHeldKeyCodesMap.svelte';
9
11
  export * from './getIsKeyHeld.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/svelte-hotkeys",
3
- "version": "0.5.1",
3
+ "version": "0.7.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.4.3"
43
+ "@tanstack/hotkeys": "0.5.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@sveltejs/package": "^2.5.7",
package/src/HotkeysCtx.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { createContext } from 'svelte'
2
- import type { HotkeyRecorderOptions } from '@tanstack/hotkeys'
2
+ import type {
3
+ HotkeyRecorderOptions,
4
+ HotkeySequenceRecorderOptions,
5
+ } from '@tanstack/hotkeys'
3
6
  import type { CreateHotkeyOptions } from './createHotkey.svelte'
4
7
  import type { Snippet } from 'svelte'
5
8
  import type { CreateHotkeySequenceOptions } from './createHotkeySequence.svelte'
@@ -7,6 +10,7 @@ import type { CreateHotkeySequenceOptions } from './createHotkeySequence.svelte'
7
10
  export interface HotkeysProviderOptions {
8
11
  hotkey?: Partial<CreateHotkeyOptions>
9
12
  hotkeyRecorder?: Partial<HotkeyRecorderOptions>
13
+ hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>
10
14
  hotkeySequence?: Partial<CreateHotkeySequenceOptions>
11
15
  }
12
16
 
@@ -4,12 +4,14 @@ import {
4
4
  getHotkeyManager,
5
5
  rawHotkeyToParsedHotkey,
6
6
  } from '@tanstack/hotkeys'
7
+ import { onDestroy } from 'svelte'
7
8
  import { getDefaultHotkeysOptions } from './HotkeysCtx'
8
9
  import { resolveMaybeGetter } from './internal.svelte'
9
10
  import type {
10
11
  Hotkey,
11
12
  HotkeyCallback,
12
13
  HotkeyOptions,
14
+ HotkeyRegistrationHandle,
13
15
  RegisterableHotkey,
14
16
  } from '@tanstack/hotkeys'
15
17
  import type { MaybeGetter } from './internal.svelte'
@@ -32,19 +34,12 @@ function normalizeHotkey(
32
34
 
33
35
  function registerHotkey(
34
36
  target: HTMLElement | Document | Window,
35
- hotkey: MaybeGetter<RegisterableHotkey>,
37
+ hotkey: RegisterableHotkey,
36
38
  callback: HotkeyCallback,
37
- options: MaybeGetter<CreateHotkeyOptions>,
39
+ mergedOptions: CreateHotkeyOptions,
38
40
  ) {
39
- const resolvedHotkey = resolveMaybeGetter(hotkey)
40
- const resolvedOptions = resolveMaybeGetter(options)
41
- const mergedOptions = {
42
- ...getDefaultHotkeysOptions().hotkey,
43
- ...resolvedOptions,
44
- } as CreateHotkeyOptions
45
-
46
41
  return getHotkeyManager().register(
47
- normalizeHotkey(resolvedHotkey, mergedOptions),
42
+ normalizeHotkey(hotkey, mergedOptions),
48
43
  callback,
49
44
  {
50
45
  ...mergedOptions,
@@ -72,15 +67,48 @@ export function createHotkey(
72
67
  callback: HotkeyCallback,
73
68
  options: MaybeGetter<CreateHotkeyOptions> = {},
74
69
  ): void {
70
+ let registration: HotkeyRegistrationHandle | null = null
71
+ let lastHotkeyStr: Hotkey | null = null
72
+
75
73
  $effect(() => {
76
74
  if (typeof document === 'undefined') {
77
75
  return
78
76
  }
79
77
 
80
- const registration = registerHotkey(document, hotkey, callback, options)
78
+ const resolvedHotkey = resolveMaybeGetter(hotkey)
79
+ const resolvedOptions = resolveMaybeGetter(options)
80
+ const mergedOptions = {
81
+ ...getDefaultHotkeysOptions().hotkey,
82
+ ...resolvedOptions,
83
+ } as CreateHotkeyOptions
81
84
 
82
- return () => {
85
+ const hotkeyStr = normalizeHotkey(resolvedHotkey, mergedOptions)
86
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
87
+
88
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
89
+ registration.callback = callback
90
+ registration.setOptions(optionsWithoutTarget)
91
+ return
92
+ }
93
+
94
+ if (registration?.isActive) {
83
95
  registration.unregister()
96
+ registration = null
97
+ }
98
+
99
+ registration = registerHotkey(
100
+ document,
101
+ resolvedHotkey,
102
+ callback,
103
+ mergedOptions,
104
+ )
105
+ lastHotkeyStr = hotkeyStr
106
+ })
107
+
108
+ onDestroy(() => {
109
+ if (registration?.isActive) {
110
+ registration.unregister()
111
+ registration = null
84
112
  }
85
113
  })
86
114
  }
@@ -111,14 +139,38 @@ export function createHotkeyAttachment(
111
139
  options: MaybeGetter<CreateHotkeyOptions> = {},
112
140
  ): Attachment<HTMLElement> {
113
141
  return (element) => {
114
- let registration: ReturnType<typeof registerHotkey> | null = null
142
+ let registration: HotkeyRegistrationHandle | null = null
143
+ let lastHotkeyStr: Hotkey | null = null
115
144
 
116
145
  $effect(() => {
146
+ const resolvedHotkey = resolveMaybeGetter(hotkey)
147
+ const resolvedOptions = resolveMaybeGetter(options)
148
+ const mergedOptions = {
149
+ ...getDefaultHotkeysOptions().hotkey,
150
+ ...resolvedOptions,
151
+ } as CreateHotkeyOptions
152
+
153
+ const hotkeyStr = normalizeHotkey(resolvedHotkey, mergedOptions)
154
+ const { target: _t, ...optionsWithoutTarget } = mergedOptions
155
+
156
+ if (registration?.isActive && lastHotkeyStr === hotkeyStr) {
157
+ registration.callback = callback
158
+ registration.setOptions(optionsWithoutTarget)
159
+ return
160
+ }
161
+
117
162
  if (registration?.isActive) {
118
163
  registration.unregister()
164
+ registration = null
119
165
  }
120
166
 
121
- registration = registerHotkey(element, hotkey, callback, options)
167
+ registration = registerHotkey(
168
+ element,
169
+ resolvedHotkey,
170
+ callback,
171
+ mergedOptions,
172
+ )
173
+ lastHotkeyStr = hotkeyStr
122
174
  })
123
175
 
124
176
  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,93 @@
1
+ import { HotkeySequenceRecorder } from '@tanstack/hotkeys'
2
+ import { onDestroy } from 'svelte'
3
+ import { getDefaultHotkeysOptions } from './HotkeysCtx'
4
+ import { createStoreSubscriber, resolveMaybeGetter } from './internal.svelte'
5
+ import type {
6
+ HotkeySequence,
7
+ HotkeySequenceRecorderOptions,
8
+ } from '@tanstack/hotkeys'
9
+ import type { MaybeGetter } from './internal.svelte'
10
+
11
+ export interface SvelteHotkeySequenceRecorder {
12
+ readonly isRecording: boolean
13
+ readonly steps: HotkeySequence
14
+ readonly recordedSequence: HotkeySequence | null
15
+ startRecording: () => void
16
+ stopRecording: () => void
17
+ cancelRecording: () => void
18
+ commitRecording: () => void
19
+ }
20
+
21
+ class SvelteHotkeySequenceRecorderState implements SvelteHotkeySequenceRecorder {
22
+ #recorder: HotkeySequenceRecorder
23
+ #subscribe: () => void
24
+
25
+ constructor(options: HotkeySequenceRecorderOptions) {
26
+ this.#recorder = new HotkeySequenceRecorder(options)
27
+ this.#subscribe = createStoreSubscriber(this.#recorder.store)
28
+ }
29
+
30
+ get isRecording(): boolean {
31
+ this.#subscribe()
32
+ return this.#recorder.store.state.isRecording
33
+ }
34
+
35
+ get steps(): HotkeySequence {
36
+ this.#subscribe()
37
+ return this.#recorder.store.state.steps
38
+ }
39
+
40
+ get recordedSequence(): HotkeySequence | null {
41
+ this.#subscribe()
42
+ return this.#recorder.store.state.recordedSequence
43
+ }
44
+
45
+ setOptions(options: HotkeySequenceRecorderOptions): void {
46
+ this.#recorder.setOptions(options)
47
+ }
48
+
49
+ startRecording(): void {
50
+ this.#recorder.start()
51
+ }
52
+
53
+ stopRecording(): void {
54
+ this.#recorder.stop()
55
+ }
56
+
57
+ cancelRecording(): void {
58
+ this.#recorder.cancel()
59
+ }
60
+
61
+ commitRecording(): void {
62
+ this.#recorder.commit()
63
+ }
64
+
65
+ destroy(): void {
66
+ this.#recorder.destroy()
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Svelte helper for recording multi-chord sequences (Vim-style shortcuts).
72
+ */
73
+ export function createHotkeySequenceRecorder(
74
+ options: MaybeGetter<HotkeySequenceRecorderOptions>,
75
+ ): SvelteHotkeySequenceRecorder {
76
+ const recorder = new SvelteHotkeySequenceRecorderState({
77
+ ...getDefaultHotkeysOptions().hotkeySequenceRecorder,
78
+ ...resolveMaybeGetter(options),
79
+ } as HotkeySequenceRecorderOptions)
80
+
81
+ $effect(() => {
82
+ recorder.setOptions({
83
+ ...getDefaultHotkeysOptions().hotkeySequenceRecorder,
84
+ ...resolveMaybeGetter(options),
85
+ } as HotkeySequenceRecorderOptions)
86
+ })
87
+
88
+ onDestroy(() => {
89
+ recorder.destroy()
90
+ })
91
+
92
+ return recorder
93
+ }
@@ -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
+ }
package/src/index.ts CHANGED
@@ -4,7 +4,9 @@ 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'
9
+ export * from './createHotkeySequenceRecorder.svelte'
8
10
  export * from './getHeldKeys.svelte'
9
11
  export * from './getHeldKeyCodesMap.svelte'
10
12
  export * from './getIsKeyHeld.svelte'