@tanstack/vue-hotkeys 0.6.0 → 0.8.1

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.
@@ -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
+ }
package/src/useHotkeys.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  import { onUnmounted, unref, watch } from 'vue'
2
2
  import {
3
3
  detectPlatform,
4
- formatHotkey,
5
4
  getHotkeyManager,
6
- rawHotkeyToParsedHotkey,
5
+ normalizeRegisterableHotkey,
7
6
  } from '@tanstack/hotkeys'
8
7
  import { useDefaultHotkeysOptions } from './HotkeysProviderContext'
9
8
  import type { UseHotkeyOptions } from './useHotkey'
10
9
  import type {
11
- Hotkey,
12
10
  HotkeyCallback,
13
11
  HotkeyRegistrationHandle,
14
12
  RegisterableHotkey,
@@ -104,12 +102,10 @@ export function useHotkeys(
104
102
  } as UseHotkeyOptions
105
103
 
106
104
  const platform = mergedOptions.platform ?? detectPlatform()
107
- const hotkeyString: Hotkey =
108
- typeof resolvedHotkey === 'string'
109
- ? resolvedHotkey
110
- : (formatHotkey(
111
- rawHotkeyToParsedHotkey(resolvedHotkey as any, platform),
112
- ) as Hotkey)
105
+ const hotkeyString = normalizeRegisterableHotkey(
106
+ resolvedHotkey,
107
+ platform,
108
+ )
113
109
 
114
110
  const resolvedEnabled =
115
111
  mergedOptions.enabled === undefined