@tanstack/angular-hotkeys 0.11.0 → 0.12.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.
Files changed (88) hide show
  1. package/dist/hotkeys-provider.d.ts +7 -10
  2. package/dist/hotkeys-provider.js +1 -2
  3. package/dist/injectHeldKeyCodes.d.ts +2 -5
  4. package/dist/injectHeldKeyCodes.js +3 -3
  5. package/dist/injectHeldKeys.d.ts +2 -5
  6. package/dist/injectHeldKeys.js +3 -3
  7. package/dist/injectHotkey.d.ts +3 -6
  8. package/dist/injectHotkey.js +3 -3
  9. package/dist/injectHotkeyHint.d.ts +2 -5
  10. package/dist/injectHotkeyHint.js +1 -2
  11. package/dist/injectHotkeyRecorder.d.ts +3 -6
  12. package/dist/injectHotkeyRecorder.js +4 -4
  13. package/dist/injectHotkeyRegistrations.d.ts +3 -6
  14. package/dist/injectHotkeyRegistrations.js +1 -2
  15. package/dist/injectHotkeySequence.d.ts +3 -6
  16. package/dist/injectHotkeySequence.js +1 -2
  17. package/dist/injectHotkeySequenceRecorder.d.ts +3 -6
  18. package/dist/injectHotkeySequenceRecorder.js +4 -4
  19. package/dist/injectHotkeySequences.d.ts +3 -6
  20. package/dist/injectHotkeySequences.js +1 -2
  21. package/dist/injectHotkeys.d.ts +3 -6
  22. package/dist/injectHotkeys.js +3 -3
  23. package/dist/injectKeyHold.d.ts +2 -5
  24. package/dist/injectKeyHold.js +3 -3
  25. package/package.json +12 -17
  26. package/dist/hotkeys-provider.cjs +0 -26
  27. package/dist/hotkeys-provider.cjs.map +0 -1
  28. package/dist/hotkeys-provider.d.cts +0 -22
  29. package/dist/hotkeys-provider.js.map +0 -1
  30. package/dist/index.cjs +0 -36
  31. package/dist/index.d.cts +0 -14
  32. package/dist/injectHeldKeyCodes.cjs +0 -34
  33. package/dist/injectHeldKeyCodes.cjs.map +0 -1
  34. package/dist/injectHeldKeyCodes.d.cts +0 -30
  35. package/dist/injectHeldKeyCodes.js.map +0 -1
  36. package/dist/injectHeldKeys.cjs +0 -29
  37. package/dist/injectHeldKeys.cjs.map +0 -1
  38. package/dist/injectHeldKeys.d.cts +0 -25
  39. package/dist/injectHeldKeys.js.map +0 -1
  40. package/dist/injectHotkey.cjs +0 -122
  41. package/dist/injectHotkey.cjs.map +0 -1
  42. package/dist/injectHotkey.d.cts +0 -75
  43. package/dist/injectHotkey.js.map +0 -1
  44. package/dist/injectHotkeyHint.cjs +0 -20
  45. package/dist/injectHotkeyHint.cjs.map +0 -1
  46. package/dist/injectHotkeyHint.d.cts +0 -15
  47. package/dist/injectHotkeyHint.js.map +0 -1
  48. package/dist/injectHotkeyRecorder.cjs +0 -82
  49. package/dist/injectHotkeyRecorder.cjs.map +0 -1
  50. package/dist/injectHotkeyRecorder.d.cts +0 -49
  51. package/dist/injectHotkeyRecorder.js.map +0 -1
  52. package/dist/injectHotkeyRegistrations.cjs +0 -28
  53. package/dist/injectHotkeyRegistrations.cjs.map +0 -1
  54. package/dist/injectHotkeyRegistrations.d.cts +0 -29
  55. package/dist/injectHotkeyRegistrations.js.map +0 -1
  56. package/dist/injectHotkeySequence.cjs +0 -93
  57. package/dist/injectHotkeySequence.cjs.map +0 -1
  58. package/dist/injectHotkeySequence.d.cts +0 -48
  59. package/dist/injectHotkeySequence.js.map +0 -1
  60. package/dist/injectHotkeySequenceRecorder.cjs +0 -58
  61. package/dist/injectHotkeySequenceRecorder.cjs.map +0 -1
  62. package/dist/injectHotkeySequenceRecorder.d.cts +0 -19
  63. package/dist/injectHotkeySequenceRecorder.js.map +0 -1
  64. package/dist/injectHotkeySequences.cjs +0 -105
  65. package/dist/injectHotkeySequences.cjs.map +0 -1
  66. package/dist/injectHotkeySequences.d.cts +0 -47
  67. package/dist/injectHotkeySequences.js.map +0 -1
  68. package/dist/injectHotkeys.cjs +0 -120
  69. package/dist/injectHotkeys.cjs.map +0 -1
  70. package/dist/injectHotkeys.d.cts +0 -66
  71. package/dist/injectHotkeys.js.map +0 -1
  72. package/dist/injectKeyHold.cjs +0 -49
  73. package/dist/injectKeyHold.cjs.map +0 -1
  74. package/dist/injectKeyHold.d.cts +0 -41
  75. package/dist/injectKeyHold.js.map +0 -1
  76. package/src/hotkeys-provider.ts +0 -44
  77. package/src/index.ts +0 -18
  78. package/src/injectHeldKeyCodes.ts +0 -31
  79. package/src/injectHeldKeys.ts +0 -26
  80. package/src/injectHotkey.ts +0 -171
  81. package/src/injectHotkeyHint.ts +0 -29
  82. package/src/injectHotkeyRecorder.ts +0 -128
  83. package/src/injectHotkeyRegistrations.ts +0 -48
  84. package/src/injectHotkeySequence.ts +0 -140
  85. package/src/injectHotkeySequenceRecorder.ts +0 -103
  86. package/src/injectHotkeySequences.ts +0 -176
  87. package/src/injectHotkeys.ts +0 -189
  88. package/src/injectKeyHold.ts +0 -57
@@ -1,48 +0,0 @@
1
- import { injectSelector } from '@tanstack/angular-store'
2
- import {
3
- getHotkeyManager,
4
- getSequenceManager,
5
- toHotkeyRegistrationView,
6
- } from '@tanstack/hotkeys'
7
- import type {
8
- HotkeyRegistrationView,
9
- SequenceRegistrationView,
10
- } from '@tanstack/hotkeys'
11
- import type { Signal } from '@angular/core'
12
-
13
- /**
14
- * Return type for injectHotkeyRegistrations.
15
- */
16
- export interface HotkeyRegistrationsResult {
17
- /** Signal for all registered hotkeys (public view, no callbacks) */
18
- hotkeys: Signal<Array<HotkeyRegistrationView>>
19
- /** Signal for all registered sequences */
20
- sequences: Signal<Array<SequenceRegistrationView>>
21
- }
22
-
23
- /**
24
- * Angular injectable that reactively reads all hotkey and sequence registrations
25
- * from the singleton managers.
26
- *
27
- * This is a standalone injectable that does NOT require the HotkeysProvider.
28
- * It subscribes to both HotkeyManager and SequenceManager stores and
29
- * triggers change detection when registrations change.
30
- *
31
- * Must be called in an injection context (constructor, factory, or `inject()`).
32
- *
33
- * @returns Object with `hotkeys` and `sequences` signals
34
- */
35
- export function injectHotkeyRegistrations(): HotkeyRegistrationsResult {
36
- const hotkeyManager = getHotkeyManager()
37
- const sequenceManager = getSequenceManager()
38
-
39
- const hotkeys = injectSelector(hotkeyManager.registrations, (state) =>
40
- Array.from(state.values()).map(toHotkeyRegistrationView),
41
- )
42
-
43
- const sequences = injectSelector(sequenceManager.registrations, (state) =>
44
- Array.from(state.values()),
45
- )
46
-
47
- return { hotkeys, sequences }
48
- }
@@ -1,140 +0,0 @@
1
- import { DestroyRef, effect, inject } from '@angular/core'
2
- import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
3
- import { injectDefaultHotkeysOptions } from './hotkeys-provider'
4
- import type {
5
- HotkeyCallback,
6
- HotkeySequence,
7
- SequenceOptions,
8
- SequenceRegistrationHandle,
9
- } from '@tanstack/hotkeys'
10
-
11
- type SequenceTarget = Document | HTMLElement | Window
12
-
13
- export interface InjectHotkeySequenceOptions extends Omit<
14
- SequenceOptions,
15
- 'enabled' | 'target'
16
- > {
17
- /** Whether the sequence is enabled. Defaults to true. */
18
- enabled?: boolean
19
- /**
20
- * The DOM element to attach the event listener to.
21
- * Can be a direct DOM element, an accessor target, or null.
22
- * Defaults to document when omitted.
23
- */
24
- target?: HTMLElement | Document | Window | null
25
- }
26
-
27
- /**
28
- * Angular inject-based API for registering a keyboard shortcut sequence (Vim-style).
29
- *
30
- * Allows you to register multi-key sequences like 'g g' or 'd d' that trigger
31
- * when the full sequence is pressed within a timeout.
32
- *
33
- * Each step may include modifiers. You can chain the same modifier across
34
- * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
35
- * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
36
- * advance the sequence or reset progress.
37
- *
38
- * @param sequence - Array of hotkey strings that form the sequence (or getter function)
39
- * @param callback - Function to call when the sequence is completed
40
- * @param options - Options for the sequence behavior (or getter function). `enabled: false` still registers
41
- * the sequence (visible in devtools); only execution is suppressed.
42
- *
43
- * @example
44
- * ```ts
45
- * @Component({ ... })
46
- * export class VimEditorComponent {
47
- * lastSequence = signal<string | null>(null)
48
- *
49
- * constructor() {
50
- * injectHotkeySequence(['G', 'G'], () => this.lastSequence.set('gg → Go to top'))
51
- * injectHotkeySequence(['D', 'D'], () => this.lastSequence.set('dd → Delete line'))
52
- * injectHotkeySequence(['C', 'I', 'W'], () => this.lastSequence.set('ciw'), { timeout: 500 })
53
- * injectHotkeySequence(['Shift+R', 'Shift+T'], () => this.lastSequence.set('⇧R⇧T'))
54
- * }
55
- * }
56
- * ```
57
- */
58
- export function injectHotkeySequence(
59
- sequence: HotkeySequence | (() => HotkeySequence),
60
- callback: HotkeyCallback,
61
- options:
62
- | InjectHotkeySequenceOptions
63
- | (() => InjectHotkeySequenceOptions) = {},
64
- ): void {
65
- const defaultOptions = injectDefaultHotkeysOptions()
66
- const manager = getSequenceManager()
67
- const destroyRef = inject(DestroyRef)
68
-
69
- let handle: SequenceRegistrationHandle | null = null
70
- let lastSequenceKey: string | null = null
71
- let lastTarget: SequenceTarget | null = null
72
-
73
- destroyRef.onDestroy(() => {
74
- if (handle?.isActive) {
75
- handle.unregister()
76
- handle = null
77
- }
78
- lastSequenceKey = null
79
- lastTarget = null
80
- })
81
-
82
- effect(() => {
83
- // Resolve reactive values
84
- const resolvedSequence =
85
- typeof sequence === 'function' ? sequence() : sequence
86
- const resolvedOptions = typeof options === 'function' ? options() : options
87
-
88
- const mergedOptions = {
89
- ...defaultOptions.hotkeySequence,
90
- ...resolvedOptions,
91
- } as InjectHotkeySequenceOptions
92
-
93
- const { enabled = true, ...sequenceOptions } = mergedOptions
94
-
95
- const resolvedTarget =
96
- 'target' in sequenceOptions
97
- ? (sequenceOptions.target ?? null)
98
- : typeof document !== 'undefined'
99
- ? document
100
- : null
101
-
102
- if (resolvedSequence.length === 0 || !resolvedTarget) {
103
- if (handle?.isActive) {
104
- handle.unregister()
105
- handle = null
106
- }
107
- lastSequenceKey = null
108
- lastTarget = null
109
- return
110
- }
111
-
112
- const sequenceKey = formatHotkeySequence(resolvedSequence)
113
-
114
- const registerPayload: SequenceOptions = {
115
- ...sequenceOptions,
116
- enabled,
117
- target: resolvedTarget,
118
- }
119
- const { target: _t, ...optionsWithoutTarget } = registerPayload
120
-
121
- if (
122
- handle?.isActive &&
123
- lastSequenceKey === sequenceKey &&
124
- lastTarget === resolvedTarget
125
- ) {
126
- handle.callback = callback
127
- handle.setOptions(optionsWithoutTarget)
128
- return
129
- }
130
-
131
- if (handle?.isActive) {
132
- handle.unregister()
133
- handle = null
134
- }
135
-
136
- handle = manager.register(resolvedSequence, callback, registerPayload)
137
- lastSequenceKey = sequenceKey
138
- lastTarget = resolvedTarget
139
- })
140
- }
@@ -1,103 +0,0 @@
1
- import {
2
- DestroyRef,
3
- computed,
4
- effect,
5
- inject,
6
- linkedSignal,
7
- untracked,
8
- } from '@angular/core'
9
- import { HotkeySequenceRecorder } from '@tanstack/hotkeys'
10
- import { injectDefaultHotkeysOptions } from './hotkeys-provider'
11
- import type { Atom, ReadonlyAtom } from '@tanstack/angular-store'
12
- import type { Signal } from '@angular/core'
13
- import type {
14
- HotkeySequence,
15
- HotkeySequenceRecorderOptions,
16
- } from '@tanstack/hotkeys'
17
-
18
- export interface AngularHotkeySequenceRecorder {
19
- readonly isRecording: () => boolean
20
- readonly steps: () => HotkeySequence
21
- readonly recordedSequence: () => HotkeySequence | null
22
- readonly startRecording: () => void
23
- readonly stopRecording: () => void
24
- readonly cancelRecording: () => void
25
- readonly commitRecording: () => void
26
- }
27
-
28
- /**
29
- * Angular inject-based API for recording multi-chord sequences.
30
- */
31
- export function injectHotkeySequenceRecorder(
32
- options:
33
- | HotkeySequenceRecorderOptions
34
- | (() => HotkeySequenceRecorderOptions),
35
- ): AngularHotkeySequenceRecorder {
36
- const defaultOptions = injectDefaultHotkeysOptions()
37
- const destroyRef = inject(DestroyRef)
38
-
39
- const recorderSignal = computed(() =>
40
- untracked(() => {
41
- const resolvedOptions =
42
- typeof options === 'function' ? options() : options
43
-
44
- const mergedOptions = {
45
- ...defaultOptions.hotkeySequenceRecorder,
46
- ...resolvedOptions,
47
- } as HotkeySequenceRecorderOptions
48
-
49
- return new HotkeySequenceRecorder(mergedOptions)
50
- }),
51
- )
52
-
53
- const recorderStore = computed(() => untracked(() => recorderSignal().store))
54
- const isRecording = injectLazyStore(
55
- recorderStore,
56
- (state) => state.isRecording,
57
- )
58
- const steps = injectLazyStore(recorderStore, (state) => state.steps)
59
- const recordedSequence = injectLazyStore(
60
- recorderStore,
61
- (state) => state.recordedSequence,
62
- )
63
-
64
- effect(() => {
65
- const resolved = typeof options === 'function' ? options() : options
66
- recorderSignal().setOptions({
67
- ...defaultOptions.hotkeySequenceRecorder,
68
- ...resolved,
69
- } as HotkeySequenceRecorderOptions)
70
- })
71
-
72
- destroyRef.onDestroy(() => {
73
- recorderSignal().destroy()
74
- })
75
-
76
- return {
77
- isRecording,
78
- steps,
79
- recordedSequence,
80
- startRecording: () => recorderSignal().start(),
81
- stopRecording: () => recorderSignal().stop(),
82
- cancelRecording: () => recorderSignal().cancel(),
83
- commitRecording: () => recorderSignal().commit(),
84
- }
85
- }
86
-
87
- function injectLazyStore<TState, TSelected = NoInfer<TState>>(
88
- storeSignal: Signal<Atom<TState> | ReadonlyAtom<TState>>,
89
- selector: (state: NoInfer<TState>) => TSelected,
90
- ): Signal<TSelected> {
91
- const slice = linkedSignal(() => selector(storeSignal().get()))
92
-
93
- effect((onCleanup) => {
94
- const currentStore = storeSignal()
95
- slice.set(selector(currentStore.get()))
96
- const { unsubscribe } = currentStore.subscribe((s) => {
97
- slice.set(selector(s))
98
- })
99
- onCleanup(() => unsubscribe())
100
- })
101
-
102
- return slice.asReadonly()
103
- }
@@ -1,176 +0,0 @@
1
- import { DestroyRef, effect, inject } from '@angular/core'
2
- import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
3
- import { injectDefaultHotkeysOptions } from './hotkeys-provider'
4
- import type { InjectHotkeySequenceOptions } from './injectHotkeySequence'
5
- import type {
6
- HotkeyCallback,
7
- HotkeySequence,
8
- SequenceRegistrationHandle,
9
- } from '@tanstack/hotkeys'
10
-
11
- /**
12
- * A single sequence definition for use with `injectHotkeySequences`.
13
- */
14
- export interface InjectHotkeySequenceDefinition {
15
- /** Array of hotkey strings that form the sequence */
16
- sequence: HotkeySequence | (() => HotkeySequence)
17
- /** The function to call when the sequence is completed */
18
- callback: HotkeyCallback
19
- /** Per-sequence options (merged on top of commonOptions) */
20
- options?: InjectHotkeySequenceOptions | (() => InjectHotkeySequenceOptions)
21
- }
22
-
23
- /**
24
- * Angular inject-based API for registering multiple keyboard shortcut sequences at once (Vim-style).
25
- *
26
- * Uses the singleton SequenceManager. Call in an injection context (e.g. constructor).
27
- * Uses `effect()` to track reactive dependencies when definitions or options are getters.
28
- *
29
- * Options are merged in this order:
30
- * provideHotkeys defaults < commonOptions < per-definition options
31
- *
32
- * Definitions with an empty `sequence` are skipped. Disabled sequences (`enabled: false`)
33
- * remain registered so they stay visible in devtools; the core manager suppresses execution.
34
- *
35
- * @param sequences - Array of sequence definitions, or getter returning them
36
- * @param commonOptions - Shared options for all sequences, or getter
37
- *
38
- * @example
39
- * ```ts
40
- * @Component({ ... })
41
- * export class VimComponent {
42
- * constructor() {
43
- * injectHotkeySequences([
44
- * { sequence: ['G', 'G'], callback: () => this.goTop() },
45
- * { sequence: ['D', 'D'], callback: () => this.deleteLine() },
46
- * ])
47
- * }
48
- * }
49
- * ```
50
- */
51
- export function injectHotkeySequences(
52
- sequences:
53
- | Array<InjectHotkeySequenceDefinition>
54
- | (() => Array<InjectHotkeySequenceDefinition>),
55
- commonOptions:
56
- | InjectHotkeySequenceOptions
57
- | (() => InjectHotkeySequenceOptions) = {},
58
- ): void {
59
- type RegistrationRecord = {
60
- handle: SequenceRegistrationHandle
61
- target: Document | HTMLElement | Window
62
- }
63
-
64
- const defaultOptions = injectDefaultHotkeysOptions()
65
- const manager = getSequenceManager()
66
- const destroyRef = inject(DestroyRef)
67
-
68
- const registrations = new Map<string, RegistrationRecord>()
69
-
70
- destroyRef.onDestroy(() => {
71
- for (const { handle } of registrations.values()) {
72
- if (handle.isActive) {
73
- handle.unregister()
74
- }
75
- }
76
- registrations.clear()
77
- })
78
-
79
- effect(() => {
80
- const resolvedSequences =
81
- typeof sequences === 'function' ? sequences() : sequences
82
- const resolvedCommonOptions =
83
- typeof commonOptions === 'function' ? commonOptions() : commonOptions
84
-
85
- const nextKeys = new Set<string>()
86
- const prepared: Array<{
87
- registrationKey: string
88
- def: InjectHotkeySequenceDefinition
89
- resolvedSequence: HotkeySequence
90
- enabled: boolean
91
- sequenceOpts: Omit<InjectHotkeySequenceOptions, 'enabled'>
92
- resolvedTarget: Document | HTMLElement | Window
93
- }> = []
94
-
95
- for (let i = 0; i < resolvedSequences.length; i++) {
96
- const def = resolvedSequences[i]!
97
- const resolvedSequence =
98
- typeof def.sequence === 'function' ? def.sequence() : def.sequence
99
- const resolvedDefOptions =
100
- typeof def.options === 'function' ? def.options() : (def.options ?? {})
101
-
102
- const mergedOptions = {
103
- ...defaultOptions.hotkeySequence,
104
- ...resolvedCommonOptions,
105
- ...resolvedDefOptions,
106
- } as InjectHotkeySequenceOptions
107
-
108
- const { enabled = true, ...sequenceOpts } = mergedOptions
109
-
110
- if (resolvedSequence.length === 0) {
111
- continue
112
- }
113
-
114
- const resolvedTarget =
115
- 'target' in sequenceOpts
116
- ? (sequenceOpts.target ?? null)
117
- : typeof document !== 'undefined'
118
- ? document
119
- : null
120
-
121
- if (!resolvedTarget) {
122
- continue
123
- }
124
-
125
- const registrationKey = `${i}:${formatHotkeySequence(resolvedSequence)}`
126
- nextKeys.add(registrationKey)
127
- prepared.push({
128
- registrationKey,
129
- def,
130
- resolvedSequence,
131
- enabled,
132
- sequenceOpts,
133
- resolvedTarget,
134
- })
135
- }
136
-
137
- for (const [key, record] of [...registrations.entries()]) {
138
- if (!nextKeys.has(key)) {
139
- if (record.handle.isActive) {
140
- record.handle.unregister()
141
- }
142
- registrations.delete(key)
143
- }
144
- }
145
-
146
- for (const p of prepared) {
147
- const existing = registrations.get(p.registrationKey)
148
- if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
149
- existing.handle.callback = p.def.callback
150
- const { target: _target, ...optionsWithoutTarget } = p.sequenceOpts
151
- existing.handle.setOptions({
152
- ...optionsWithoutTarget,
153
- enabled: p.enabled,
154
- })
155
- continue
156
- }
157
-
158
- if (existing) {
159
- if (existing.handle.isActive) {
160
- existing.handle.unregister()
161
- }
162
- registrations.delete(p.registrationKey)
163
- }
164
-
165
- const handle = manager.register(p.resolvedSequence, p.def.callback, {
166
- ...p.sequenceOpts,
167
- enabled: p.enabled,
168
- target: p.resolvedTarget,
169
- })
170
- registrations.set(p.registrationKey, {
171
- handle,
172
- target: p.resolvedTarget,
173
- })
174
- }
175
- })
176
- }
@@ -1,189 +0,0 @@
1
- import { DestroyRef, effect, inject } from '@angular/core'
2
- import {
3
- detectPlatform,
4
- getHotkeyManager,
5
- normalizeRegisterableHotkey,
6
- } from '@tanstack/hotkeys'
7
- import { injectDefaultHotkeysOptions } from './hotkeys-provider'
8
- import type { InjectHotkeyOptions } from './injectHotkey'
9
- import type {
10
- Hotkey,
11
- HotkeyCallback,
12
- HotkeyRegistrationHandle,
13
- RegisterableHotkey,
14
- } from '@tanstack/hotkeys'
15
-
16
- /**
17
- * A single hotkey definition for use with `injectHotkeys`.
18
- */
19
- export interface InjectHotkeyDefinition {
20
- /** The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object */
21
- hotkey: RegisterableHotkey | (() => RegisterableHotkey)
22
- /** The function to call when the hotkey is pressed */
23
- callback: HotkeyCallback
24
- /** Per-hotkey options (merged on top of commonOptions) */
25
- options?: InjectHotkeyOptions | (() => InjectHotkeyOptions)
26
- }
27
-
28
- /**
29
- * Angular inject-based API for registering multiple keyboard hotkeys at once.
30
- *
31
- * Uses the singleton HotkeyManager for efficient event handling.
32
- * Accepts a dynamic array of hotkey definitions.
33
- *
34
- * Call in an injection context (e.g. constructor or field initializer).
35
- * Uses effect() to track reactive dependencies and update registrations
36
- * when options or the callback change.
37
- *
38
- * Options are merged in this order:
39
- * provideHotkeys defaults < commonOptions < per-definition options
40
- *
41
- * @param hotkeys - Array of hotkey definitions, or getter returning them
42
- * @param commonOptions - Shared options for all hotkeys, or getter
43
- *
44
- * @example
45
- * ```ts
46
- * @Component({ ... })
47
- * export class EditorComponent {
48
- * constructor() {
49
- * injectHotkeys([
50
- * { hotkey: 'Mod+S', callback: () => this.save() },
51
- * { hotkey: 'Mod+Z', callback: () => this.undo() },
52
- * { hotkey: 'Escape', callback: () => this.close() },
53
- * ])
54
- * }
55
- * }
56
- * ```
57
- *
58
- * @example
59
- * ```ts
60
- * @Component({ ... })
61
- * export class DynamicShortcuts {
62
- * shortcuts = signal([...])
63
- *
64
- * constructor() {
65
- * injectHotkeys(
66
- * () => this.shortcuts().map((s) => ({
67
- * hotkey: s.key,
68
- * callback: s.action,
69
- * })),
70
- * )
71
- * }
72
- * }
73
- * ```
74
- */
75
- export function injectHotkeys(
76
- hotkeys:
77
- | Array<InjectHotkeyDefinition>
78
- | (() => Array<InjectHotkeyDefinition>),
79
- commonOptions: InjectHotkeyOptions | (() => InjectHotkeyOptions) = {},
80
- ): void {
81
- type RegistrationRecord = {
82
- handle: HotkeyRegistrationHandle
83
- target: Document | HTMLElement | Window
84
- }
85
-
86
- const defaultOptions = injectDefaultHotkeysOptions()
87
- const manager = getHotkeyManager()
88
- const destroyRef = inject(DestroyRef)
89
-
90
- const registrations = new Map<string, RegistrationRecord>()
91
-
92
- destroyRef.onDestroy(() => {
93
- for (const { handle } of registrations.values()) {
94
- if (handle.isActive) {
95
- handle.unregister()
96
- }
97
- }
98
- registrations.clear()
99
- })
100
-
101
- effect(() => {
102
- const resolvedHotkeys = typeof hotkeys === 'function' ? hotkeys() : hotkeys
103
- const resolvedCommonOptions =
104
- typeof commonOptions === 'function' ? commonOptions() : commonOptions
105
-
106
- const nextKeys = new Set<string>()
107
- const prepared: Array<{
108
- registrationKey: string
109
- def: InjectHotkeyDefinition
110
- mergedOptions: InjectHotkeyOptions
111
- hotkeyString: Hotkey
112
- resolvedTarget: Document | HTMLElement | Window
113
- }> = []
114
-
115
- for (let i = 0; i < resolvedHotkeys.length; i++) {
116
- const def = resolvedHotkeys[i]!
117
- const resolvedHotkey =
118
- typeof def.hotkey === 'function' ? def.hotkey() : def.hotkey
119
- const resolvedDefOptions =
120
- typeof def.options === 'function' ? def.options() : (def.options ?? {})
121
-
122
- const mergedOptions = {
123
- ...defaultOptions.hotkey,
124
- ...resolvedCommonOptions,
125
- ...resolvedDefOptions,
126
- } as InjectHotkeyOptions
127
-
128
- const platform = mergedOptions.platform ?? detectPlatform()
129
- const hotkeyString = normalizeRegisterableHotkey(resolvedHotkey, platform)
130
-
131
- const resolvedTarget =
132
- 'target' in mergedOptions
133
- ? (mergedOptions.target ?? null)
134
- : typeof document !== 'undefined'
135
- ? document
136
- : null
137
-
138
- if (!resolvedTarget) {
139
- continue
140
- }
141
-
142
- const registrationKey = `${i}:${hotkeyString}`
143
- nextKeys.add(registrationKey)
144
- prepared.push({
145
- registrationKey,
146
- def,
147
- mergedOptions,
148
- hotkeyString,
149
- resolvedTarget,
150
- })
151
- }
152
-
153
- for (const [key, record] of [...registrations.entries()]) {
154
- if (!nextKeys.has(key)) {
155
- if (record.handle.isActive) {
156
- record.handle.unregister()
157
- }
158
- registrations.delete(key)
159
- }
160
- }
161
-
162
- for (const p of prepared) {
163
- const existing = registrations.get(p.registrationKey)
164
- if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
165
- existing.handle.callback = p.def.callback
166
- const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
167
- existing.handle.setOptions(optionsWithoutTarget)
168
- continue
169
- }
170
-
171
- if (existing) {
172
- if (existing.handle.isActive) {
173
- existing.handle.unregister()
174
- }
175
- registrations.delete(p.registrationKey)
176
- }
177
-
178
- const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
179
- const handle = manager.register(p.hotkeyString, p.def.callback, {
180
- ...optionsWithoutTarget,
181
- target: p.resolvedTarget,
182
- })
183
- registrations.set(p.registrationKey, {
184
- handle,
185
- target: p.resolvedTarget,
186
- })
187
- }
188
- })
189
- }