@tanstack/vue-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/HotkeysProviderContext.cjs.map +1 -1
- package/dist/HotkeysProviderContext.d.cts +2 -1
- package/dist/HotkeysProviderContext.d.ts +2 -1
- package/dist/HotkeysProviderContext.js.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/useHotkey.cjs +22 -5
- package/dist/useHotkey.cjs.map +1 -1
- package/dist/useHotkey.js +22 -5
- package/dist/useHotkey.js.map +1 -1
- package/dist/useHotkeySequence.cjs +23 -6
- package/dist/useHotkeySequence.cjs.map +1 -1
- package/dist/useHotkeySequence.js +24 -7
- package/dist/useHotkeySequence.js.map +1 -1
- package/dist/useHotkeySequenceRecorder.cjs +45 -0
- package/dist/useHotkeySequenceRecorder.cjs.map +1 -0
- package/dist/useHotkeySequenceRecorder.d.cts +20 -0
- package/dist/useHotkeySequenceRecorder.d.ts +20 -0
- package/dist/useHotkeySequenceRecorder.js +45 -0
- package/dist/useHotkeySequenceRecorder.js.map +1 -0
- package/dist/useHotkeySequences.cjs +140 -0
- package/dist/useHotkeySequences.cjs.map +1 -0
- package/dist/useHotkeySequences.d.cts +64 -0
- package/dist/useHotkeySequences.d.ts +64 -0
- package/dist/useHotkeySequences.js +140 -0
- package/dist/useHotkeySequences.js.map +1 -0
- package/package.json +2 -2
- package/src/HotkeysProviderContext.ts +5 -1
- package/src/index.ts +2 -0
- package/src/useHotkey.ts +31 -9
- package/src/useHotkeySequence.ts +34 -15
- package/src/useHotkeySequenceRecorder.ts +75 -0
- package/src/useHotkeySequences.ts +221 -0
package/src/useHotkeySequence.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { onUnmounted, unref, watch } from 'vue'
|
|
2
|
-
import { getSequenceManager } from '@tanstack/hotkeys'
|
|
2
|
+
import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
|
|
3
3
|
import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
|
|
4
4
|
import type { MaybeRefOrGetter } from 'vue'
|
|
5
5
|
import type {
|
|
@@ -82,6 +82,8 @@ export function useHotkeySequence(
|
|
|
82
82
|
const manager = getSequenceManager()
|
|
83
83
|
|
|
84
84
|
let registration: SequenceRegistrationHandle | null = null
|
|
85
|
+
let lastSequenceKey: string | null = null
|
|
86
|
+
let lastTarget: HTMLElement | Document | Window | null = null
|
|
85
87
|
|
|
86
88
|
// Watch for changes to reactive dependencies
|
|
87
89
|
const stopWatcher = watch(
|
|
@@ -109,23 +111,24 @@ export function useHotkeySequence(
|
|
|
109
111
|
}
|
|
110
112
|
},
|
|
111
113
|
({ resolvedSequence, mergedOptions, resolvedEnabled, resolvedTarget }) => {
|
|
112
|
-
if (resolvedEnabled === false || resolvedSequence.length === 0) {
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Resolve target
|
|
117
114
|
const finalTarget =
|
|
118
|
-
resolvedTarget
|
|
115
|
+
resolvedTarget === undefined
|
|
116
|
+
? typeof document !== 'undefined'
|
|
117
|
+
? document
|
|
118
|
+
: null
|
|
119
|
+
: resolvedTarget
|
|
119
120
|
|
|
120
|
-
if (!finalTarget) {
|
|
121
|
+
if (resolvedSequence.length === 0 || !finalTarget) {
|
|
122
|
+
if (registration?.isActive) {
|
|
123
|
+
registration.unregister()
|
|
124
|
+
registration = null
|
|
125
|
+
}
|
|
126
|
+
lastSequenceKey = null
|
|
127
|
+
lastTarget = null
|
|
121
128
|
return
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
|
|
125
|
-
if (registration?.isActive) {
|
|
126
|
-
registration.unregister()
|
|
127
|
-
registration = null
|
|
128
|
-
}
|
|
131
|
+
const sequenceKey = formatHotkeySequence(resolvedSequence)
|
|
129
132
|
|
|
130
133
|
// Extract options without target (target is handled separately)
|
|
131
134
|
const {
|
|
@@ -138,17 +141,33 @@ export function useHotkeySequence(
|
|
|
138
141
|
...(resolvedEnabled === undefined ? {} : { enabled: resolvedEnabled }),
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
|
|
144
|
+
if (
|
|
145
|
+
registration?.isActive &&
|
|
146
|
+
lastSequenceKey === sequenceKey &&
|
|
147
|
+
lastTarget === finalTarget
|
|
148
|
+
) {
|
|
149
|
+
registration.callback = callback
|
|
150
|
+
registration.setOptions(optionsWithoutTarget)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (registration?.isActive) {
|
|
155
|
+
registration.unregister()
|
|
156
|
+
registration = null
|
|
157
|
+
}
|
|
158
|
+
|
|
142
159
|
registration = manager.register(resolvedSequence, callback, {
|
|
143
160
|
...optionsWithoutTarget,
|
|
144
161
|
target: finalTarget,
|
|
145
162
|
})
|
|
146
163
|
|
|
147
|
-
// Update callback and options
|
|
148
164
|
if (registration.isActive) {
|
|
149
165
|
registration.callback = callback
|
|
150
166
|
registration.setOptions(optionsWithoutTarget)
|
|
151
167
|
}
|
|
168
|
+
|
|
169
|
+
lastSequenceKey = sequenceKey
|
|
170
|
+
lastTarget = finalTarget
|
|
152
171
|
},
|
|
153
172
|
{ immediate: true },
|
|
154
173
|
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { onUnmounted, unref, watch } from 'vue'
|
|
2
|
+
import { useStore } from '@tanstack/vue-store'
|
|
3
|
+
import { HotkeySequenceRecorder } from '@tanstack/hotkeys'
|
|
4
|
+
import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
|
|
5
|
+
import type { MaybeRefOrGetter, Ref } from 'vue'
|
|
6
|
+
import type {
|
|
7
|
+
HotkeySequence,
|
|
8
|
+
HotkeySequenceRecorderOptions,
|
|
9
|
+
} from '@tanstack/hotkeys'
|
|
10
|
+
|
|
11
|
+
export interface VueHotkeySequenceRecorder {
|
|
12
|
+
isRecording: Ref<boolean>
|
|
13
|
+
steps: Ref<HotkeySequence>
|
|
14
|
+
recordedSequence: Ref<HotkeySequence | null>
|
|
15
|
+
startRecording: () => void
|
|
16
|
+
stopRecording: () => void
|
|
17
|
+
cancelRecording: () => void
|
|
18
|
+
commitRecording: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Vue composable for recording multi-chord sequences (Vim-style shortcuts).
|
|
23
|
+
*/
|
|
24
|
+
export function useHotkeySequenceRecorder(
|
|
25
|
+
options: MaybeRefOrGetter<HotkeySequenceRecorderOptions>,
|
|
26
|
+
): VueHotkeySequenceRecorder {
|
|
27
|
+
const defaultOptions = useDefaultHotkeysOptions()
|
|
28
|
+
const recorder = new HotkeySequenceRecorder(
|
|
29
|
+
resolveHotkeySequenceRecorderOptions(options, defaultOptions),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const isRecording = useStore(recorder.store, (state) => state.isRecording)
|
|
33
|
+
const steps = useStore(recorder.store, (state) => state.steps)
|
|
34
|
+
const recordedSequence = useStore(
|
|
35
|
+
recorder.store,
|
|
36
|
+
(state) => state.recordedSequence,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const stopWatcher = watch(
|
|
40
|
+
() => resolveHotkeySequenceRecorderOptions(options, defaultOptions),
|
|
41
|
+
(resolvedOptions) => {
|
|
42
|
+
recorder.setOptions(resolvedOptions)
|
|
43
|
+
},
|
|
44
|
+
{ immediate: true },
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
onUnmounted(() => {
|
|
48
|
+
stopWatcher()
|
|
49
|
+
recorder.destroy()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
isRecording: isRecording as Ref<boolean>,
|
|
54
|
+
steps: steps as Ref<HotkeySequence>,
|
|
55
|
+
recordedSequence: recordedSequence as Ref<HotkeySequence | null>,
|
|
56
|
+
startRecording: () => recorder.start(),
|
|
57
|
+
stopRecording: () => recorder.stop(),
|
|
58
|
+
cancelRecording: () => recorder.cancel(),
|
|
59
|
+
commitRecording: () => recorder.commit(),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveHotkeySequenceRecorderOptions(
|
|
64
|
+
options: MaybeRefOrGetter<HotkeySequenceRecorderOptions>,
|
|
65
|
+
defaultOptions: ReturnType<typeof useDefaultHotkeysOptions>,
|
|
66
|
+
): HotkeySequenceRecorderOptions {
|
|
67
|
+
return {
|
|
68
|
+
...defaultOptions.hotkeySequenceRecorder,
|
|
69
|
+
...resolveMaybeRefOrGetter(options),
|
|
70
|
+
} as HotkeySequenceRecorderOptions
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveMaybeRefOrGetter<T>(value: MaybeRefOrGetter<T>): T {
|
|
74
|
+
return typeof value === 'function' ? (value as () => T)() : unref(value)
|
|
75
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { onUnmounted, unref, watch } from 'vue'
|
|
2
|
+
import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
|
|
3
|
+
import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
|
|
4
|
+
import type { UseHotkeySequenceOptions } from './useHotkeySequence'
|
|
5
|
+
import type {
|
|
6
|
+
HotkeyCallback,
|
|
7
|
+
HotkeySequence,
|
|
8
|
+
SequenceRegistrationHandle,
|
|
9
|
+
} from '@tanstack/hotkeys'
|
|
10
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A single sequence definition for use with `useHotkeySequences`.
|
|
14
|
+
*/
|
|
15
|
+
export interface UseHotkeySequenceDefinition {
|
|
16
|
+
/** Array of hotkey strings that form the sequence */
|
|
17
|
+
sequence: MaybeRefOrGetter<HotkeySequence>
|
|
18
|
+
/** The function to call when the sequence is completed */
|
|
19
|
+
callback: HotkeyCallback
|
|
20
|
+
/** Per-sequence options (merged on top of commonOptions) */
|
|
21
|
+
options?: MaybeRefOrGetter<UseHotkeySequenceOptions>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Vue composable for registering multiple keyboard shortcut sequences at once (Vim-style).
|
|
26
|
+
*
|
|
27
|
+
* Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or a getter/ref
|
|
28
|
+
* that returns one, so you can register variable-length lists safely.
|
|
29
|
+
*
|
|
30
|
+
* Options are merged in this order:
|
|
31
|
+
* HotkeysProvider defaults < commonOptions < per-definition options
|
|
32
|
+
*
|
|
33
|
+
* Definitions with an empty `sequence` are skipped (no registration).
|
|
34
|
+
*
|
|
35
|
+
* @param definitions - Array of sequence definitions, or a getter/ref
|
|
36
|
+
* @param commonOptions - Shared options applied to all sequences (overridden by per-definition options)
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```vue
|
|
40
|
+
* <script setup>
|
|
41
|
+
* import { useHotkeySequences } from '@tanstack/vue-hotkeys'
|
|
42
|
+
*
|
|
43
|
+
* useHotkeySequences([
|
|
44
|
+
* { sequence: ['G', 'G'], callback: () => scrollToTop() },
|
|
45
|
+
* { sequence: ['D', 'D'], callback: () => deleteLine() },
|
|
46
|
+
* ])
|
|
47
|
+
* </script>
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```vue
|
|
52
|
+
* <script setup>
|
|
53
|
+
* import { computed } from 'vue'
|
|
54
|
+
* import { useHotkeySequences } from '@tanstack/vue-hotkeys'
|
|
55
|
+
*
|
|
56
|
+
* const items = computed(() => [...])
|
|
57
|
+
* useHotkeySequences(
|
|
58
|
+
* () => items.value.map((item) => ({
|
|
59
|
+
* sequence: item.chords,
|
|
60
|
+
* callback: item.action,
|
|
61
|
+
* options: { enabled: item.enabled },
|
|
62
|
+
* })),
|
|
63
|
+
* { preventDefault: true },
|
|
64
|
+
* )
|
|
65
|
+
* </script>
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function useHotkeySequences(
|
|
69
|
+
definitions: MaybeRefOrGetter<Array<UseHotkeySequenceDefinition>>,
|
|
70
|
+
commonOptions: MaybeRefOrGetter<UseHotkeySequenceOptions> = {},
|
|
71
|
+
): void {
|
|
72
|
+
type RegistrationRecord = {
|
|
73
|
+
handle: SequenceRegistrationHandle
|
|
74
|
+
target: Document | HTMLElement | Window
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const defaultOptions = useDefaultHotkeysOptions()
|
|
78
|
+
const manager = getSequenceManager()
|
|
79
|
+
|
|
80
|
+
const registrations = new Map<string, RegistrationRecord>()
|
|
81
|
+
|
|
82
|
+
const stopWatcher = watch(
|
|
83
|
+
() => {
|
|
84
|
+
const resolvedDefinitions = resolveMaybeRefOrGetter(definitions)
|
|
85
|
+
const resolvedCommonOptions = resolveMaybeRefOrGetter(commonOptions)
|
|
86
|
+
|
|
87
|
+
return resolvedDefinitions.map((def, i) => {
|
|
88
|
+
const resolvedSequence = resolveMaybeRefOrGetter(def.sequence)
|
|
89
|
+
const resolvedDefOptions = def.options
|
|
90
|
+
? resolveMaybeRefOrGetter(def.options)
|
|
91
|
+
: {}
|
|
92
|
+
|
|
93
|
+
const mergedOptions = {
|
|
94
|
+
...defaultOptions.hotkeySequence,
|
|
95
|
+
...resolvedCommonOptions,
|
|
96
|
+
...resolvedDefOptions,
|
|
97
|
+
} as UseHotkeySequenceOptions
|
|
98
|
+
|
|
99
|
+
const sequenceString = formatHotkeySequence(resolvedSequence)
|
|
100
|
+
|
|
101
|
+
const resolvedEnabled =
|
|
102
|
+
mergedOptions.enabled === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: resolveMaybeRefOrGetter(mergedOptions.enabled)
|
|
105
|
+
const resolvedTarget =
|
|
106
|
+
mergedOptions.target === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: resolveMaybeRefOrGetter(mergedOptions.target as any)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
index: i,
|
|
112
|
+
resolvedSequence,
|
|
113
|
+
sequenceString,
|
|
114
|
+
callback: def.callback,
|
|
115
|
+
mergedOptions,
|
|
116
|
+
resolvedEnabled,
|
|
117
|
+
resolvedTarget,
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
(resolved) => {
|
|
122
|
+
const nextKeys = new Set<string>()
|
|
123
|
+
const prepared: Array<{
|
|
124
|
+
registrationKey: string
|
|
125
|
+
entry: (typeof resolved)[number]
|
|
126
|
+
finalTarget: Document | HTMLElement | Window
|
|
127
|
+
}> = []
|
|
128
|
+
|
|
129
|
+
for (const entry of resolved) {
|
|
130
|
+
if (entry.resolvedSequence.length === 0) {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const finalTarget =
|
|
135
|
+
entry.resolvedTarget ??
|
|
136
|
+
(typeof document !== 'undefined' ? document : null)
|
|
137
|
+
|
|
138
|
+
if (!finalTarget) {
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const registrationKey = `${entry.index}:${entry.sequenceString}`
|
|
143
|
+
nextKeys.add(registrationKey)
|
|
144
|
+
prepared.push({ registrationKey, entry, finalTarget })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const [key, record] of [...registrations.entries()]) {
|
|
148
|
+
if (!nextKeys.has(key)) {
|
|
149
|
+
if (record.handle.isActive) {
|
|
150
|
+
record.handle.unregister()
|
|
151
|
+
}
|
|
152
|
+
registrations.delete(key)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const { registrationKey, entry, finalTarget } of prepared) {
|
|
157
|
+
const existing = registrations.get(registrationKey)
|
|
158
|
+
if (existing?.handle.isActive && existing.target === finalTarget) {
|
|
159
|
+
existing.handle.callback = entry.callback
|
|
160
|
+
const {
|
|
161
|
+
target: _target,
|
|
162
|
+
enabled: _enabled,
|
|
163
|
+
...restOptions
|
|
164
|
+
} = entry.mergedOptions
|
|
165
|
+
const optionsWithoutTarget = {
|
|
166
|
+
...restOptions,
|
|
167
|
+
...(entry.resolvedEnabled === undefined
|
|
168
|
+
? {}
|
|
169
|
+
: { enabled: entry.resolvedEnabled }),
|
|
170
|
+
}
|
|
171
|
+
existing.handle.setOptions(optionsWithoutTarget)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (existing) {
|
|
176
|
+
if (existing.handle.isActive) {
|
|
177
|
+
existing.handle.unregister()
|
|
178
|
+
}
|
|
179
|
+
registrations.delete(registrationKey)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const {
|
|
183
|
+
target: _target,
|
|
184
|
+
enabled: _enabled,
|
|
185
|
+
...restOptions
|
|
186
|
+
} = entry.mergedOptions
|
|
187
|
+
const optionsWithoutTarget = {
|
|
188
|
+
...restOptions,
|
|
189
|
+
...(entry.resolvedEnabled === undefined
|
|
190
|
+
? {}
|
|
191
|
+
: { enabled: entry.resolvedEnabled }),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handle = manager.register(
|
|
195
|
+
entry.resolvedSequence,
|
|
196
|
+
entry.callback,
|
|
197
|
+
{
|
|
198
|
+
...optionsWithoutTarget,
|
|
199
|
+
target: finalTarget,
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
registrations.set(registrationKey, { handle, target: finalTarget })
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{ immediate: true },
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
onUnmounted(() => {
|
|
209
|
+
stopWatcher()
|
|
210
|
+
for (const { handle } of registrations.values()) {
|
|
211
|
+
if (handle.isActive) {
|
|
212
|
+
handle.unregister()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
registrations.clear()
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveMaybeRefOrGetter<T>(value: MaybeRefOrGetter<T>): T {
|
|
220
|
+
return typeof value === 'function' ? (value as () => T)() : unref(value)
|
|
221
|
+
}
|