@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 +1 -1
- package/dist/createHotkey.svelte.js +47 -19
- package/dist/createHotkeySequence.svelte.js +57 -16
- package/dist/createHotkeySequences.svelte.d.ts +49 -0
- package/dist/createHotkeySequences.svelte.js +189 -0
- package/dist/createHotkeys.svelte.js +3 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
- package/src/createHotkey.svelte.ts +76 -27
- package/src/createHotkeySequence.svelte.ts +70 -20
- package/src/createHotkeySequences.svelte.ts +274 -0
- package/src/createHotkeys.svelte.ts +9 -14
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
# TanStack Hotkeys
|
|
38
38
|
|
|
39
39
|
> [!NOTE]
|
|
40
|
-
> TanStack Hotkeys is
|
|
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,
|
|
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
|
|
5
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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,
|
|
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,
|
|
5
|
-
|
|
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
|
|
65
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
25
|
+
hotkey: RegisterableHotkey,
|
|
36
26
|
callback: HotkeyCallback,
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
69
|
+
const resolvedHotkey = resolveMaybeGetter(hotkey)
|
|
70
|
+
const resolvedOptions = resolveMaybeGetter(options)
|
|
71
|
+
const mergedOptions = {
|
|
72
|
+
...getDefaultHotkeysOptions().hotkey,
|
|
73
|
+
...resolvedOptions,
|
|
74
|
+
} as CreateHotkeyOptions
|
|
81
75
|
|
|
82
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
23
|
+
sequence: HotkeySequence,
|
|
22
24
|
callback: HotkeyCallback,
|
|
23
|
-
|
|
25
|
+
mergedOptions: CreateHotkeySequenceOptions,
|
|
24
26
|
) {
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
120
|
+
mergedOptions,
|
|
97
121
|
)
|
|
122
|
+
lastSequenceKey = sequenceKey
|
|
123
|
+
})
|
|
98
124
|
|
|
99
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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'
|