@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.
- package/dist/HotkeysCtx.d.ts +2 -1
- package/dist/createHotkey.svelte.js +46 -12
- package/dist/createHotkeySequence.svelte.js +57 -16
- package/dist/createHotkeySequenceRecorder.svelte.d.ts +15 -0
- package/dist/createHotkeySequenceRecorder.svelte.js +74 -0
- package/dist/createHotkeySequences.svelte.d.ts +49 -0
- package/dist/createHotkeySequences.svelte.js +189 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +2 -2
- package/src/HotkeysCtx.ts +5 -1
- package/src/createHotkey.svelte.ts +66 -14
- package/src/createHotkeySequence.svelte.ts +70 -20
- package/src/createHotkeySequenceRecorder.svelte.ts +93 -0
- package/src/createHotkeySequences.svelte.ts +274 -0
- package/src/index.ts +2 -0
package/dist/HotkeysCtx.d.ts
CHANGED
|
@@ -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,
|
|
11
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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,
|
|
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,
|
|
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,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.
|
|
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.
|
|
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 {
|
|
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:
|
|
37
|
+
hotkey: RegisterableHotkey,
|
|
36
38
|
callback: HotkeyCallback,
|
|
37
|
-
|
|
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(
|
|
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
|
|
78
|
+
const resolvedHotkey = resolveMaybeGetter(hotkey)
|
|
79
|
+
const resolvedOptions = resolveMaybeGetter(options)
|
|
80
|
+
const mergedOptions = {
|
|
81
|
+
...getDefaultHotkeysOptions().hotkey,
|
|
82
|
+
...resolvedOptions,
|
|
83
|
+
} as CreateHotkeyOptions
|
|
81
84
|
|
|
82
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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,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'
|