@tanstack/svelte-hotkeys 0.4.1 → 0.5.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/dist/createHotkey.svelte.js +10 -2
- package/dist/createHotkeySequence.svelte.js +15 -6
- package/dist/createHotkeys.svelte.d.ts +52 -0
- package/dist/createHotkeys.svelte.js +190 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +6 -6
- package/src/createHotkey.svelte.ts +12 -2
- package/src/createHotkeySequence.svelte.ts +22 -11
- package/src/createHotkeys.svelte.ts +281 -0
- package/src/index.ts +1 -0
|
@@ -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
|
-
|
|
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
|
|
77
|
+
if (registration?.isActive) {
|
|
78
|
+
registration.unregister();
|
|
79
|
+
}
|
|
72
80
|
};
|
|
73
81
|
};
|
|
74
82
|
}
|
|
@@ -77,13 +77,22 @@ export function createHotkeySequence(sequence, callback, options = {}) {
|
|
|
77
77
|
*/
|
|
78
78
|
export function createHotkeySequenceAttachment(sequence, callback, options = {}) {
|
|
79
79
|
return (element) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
let registration = null;
|
|
81
|
+
$effect(() => {
|
|
82
|
+
const resolvedSequence = resolveMaybeGetter(sequence);
|
|
83
|
+
if (registration?.isActive) {
|
|
84
|
+
registration.unregister();
|
|
85
|
+
registration = null;
|
|
86
|
+
}
|
|
87
|
+
if (resolvedSequence.length === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
registration = registerHotkeySequence(element, resolvedSequence, callback, options);
|
|
91
|
+
});
|
|
85
92
|
return () => {
|
|
86
|
-
registration
|
|
93
|
+
if (registration?.isActive) {
|
|
94
|
+
registration.unregister();
|
|
95
|
+
}
|
|
87
96
|
};
|
|
88
97
|
};
|
|
89
98
|
}
|
|
@@ -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
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.
|
|
3
|
+
"version": "0.5.0",
|
|
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.
|
|
43
|
+
"@tanstack/hotkeys": "0.4.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@sveltejs/package": "^2.5.7",
|
|
47
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
48
|
-
"@typescript-eslint/parser": "^8.
|
|
49
|
-
"eslint-plugin-svelte": "^3.
|
|
50
|
-
"svelte": "^5.
|
|
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
|
-
|
|
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
|
|
125
|
+
if (registration?.isActive) {
|
|
126
|
+
registration.unregister()
|
|
127
|
+
}
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
}
|
|
@@ -116,21 +116,32 @@ export function createHotkeySequenceAttachment(
|
|
|
116
116
|
options: MaybeGetter<CreateHotkeySequenceOptions> = {},
|
|
117
117
|
): Attachment<HTMLElement> {
|
|
118
118
|
return (element) => {
|
|
119
|
-
|
|
119
|
+
let registration: ReturnType<typeof registerHotkeySequence> | null = null
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
121
|
+
$effect(() => {
|
|
122
|
+
const resolvedSequence = resolveMaybeGetter(sequence)
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
if (registration?.isActive) {
|
|
125
|
+
registration.unregister()
|
|
126
|
+
registration = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (resolvedSequence.length === 0) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
registration = registerHotkeySequence(
|
|
134
|
+
element,
|
|
135
|
+
resolvedSequence,
|
|
136
|
+
callback,
|
|
137
|
+
options,
|
|
138
|
+
)
|
|
139
|
+
})
|
|
131
140
|
|
|
132
141
|
return () => {
|
|
133
|
-
registration
|
|
142
|
+
if (registration?.isActive) {
|
|
143
|
+
registration.unregister()
|
|
144
|
+
}
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
}
|
|
@@ -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