@tanstack/angular-hotkeys 0.3.3

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 (56) hide show
  1. package/README.md +122 -0
  2. package/dist/hotkeys-provider.cjs +26 -0
  3. package/dist/hotkeys-provider.cjs.map +1 -0
  4. package/dist/hotkeys-provider.d.cts +21 -0
  5. package/dist/hotkeys-provider.d.ts +21 -0
  6. package/dist/hotkeys-provider.js +23 -0
  7. package/dist/hotkeys-provider.js.map +1 -0
  8. package/dist/index.cjs +26 -0
  9. package/dist/index.d.cts +9 -0
  10. package/dist/index.d.ts +9 -0
  11. package/dist/index.js +11 -0
  12. package/dist/injectHeldKeyCodes.cjs +34 -0
  13. package/dist/injectHeldKeyCodes.cjs.map +1 -0
  14. package/dist/injectHeldKeyCodes.d.cts +30 -0
  15. package/dist/injectHeldKeyCodes.d.ts +30 -0
  16. package/dist/injectHeldKeyCodes.js +34 -0
  17. package/dist/injectHeldKeyCodes.js.map +1 -0
  18. package/dist/injectHeldKeys.cjs +29 -0
  19. package/dist/injectHeldKeys.cjs.map +1 -0
  20. package/dist/injectHeldKeys.d.cts +25 -0
  21. package/dist/injectHeldKeys.d.ts +25 -0
  22. package/dist/injectHeldKeys.js +29 -0
  23. package/dist/injectHeldKeys.js.map +1 -0
  24. package/dist/injectHotkey.cjs +93 -0
  25. package/dist/injectHotkey.cjs.map +1 -0
  26. package/dist/injectHotkey.d.cts +73 -0
  27. package/dist/injectHotkey.d.ts +73 -0
  28. package/dist/injectHotkey.js +93 -0
  29. package/dist/injectHotkey.js.map +1 -0
  30. package/dist/injectHotkeyRecorder.cjs +82 -0
  31. package/dist/injectHotkeyRecorder.cjs.map +1 -0
  32. package/dist/injectHotkeyRecorder.d.cts +49 -0
  33. package/dist/injectHotkeyRecorder.d.ts +49 -0
  34. package/dist/injectHotkeyRecorder.js +82 -0
  35. package/dist/injectHotkeyRecorder.js.map +1 -0
  36. package/dist/injectHotkeySequence.cjs +55 -0
  37. package/dist/injectHotkeySequence.cjs.map +1 -0
  38. package/dist/injectHotkeySequence.d.cts +35 -0
  39. package/dist/injectHotkeySequence.d.ts +35 -0
  40. package/dist/injectHotkeySequence.js +55 -0
  41. package/dist/injectHotkeySequence.js.map +1 -0
  42. package/dist/injectKeyHold.cjs +49 -0
  43. package/dist/injectKeyHold.cjs.map +1 -0
  44. package/dist/injectKeyHold.d.cts +41 -0
  45. package/dist/injectKeyHold.d.ts +41 -0
  46. package/dist/injectKeyHold.js +49 -0
  47. package/dist/injectKeyHold.js.map +1 -0
  48. package/package.json +67 -0
  49. package/src/hotkeys-provider.ts +40 -0
  50. package/src/index.ts +13 -0
  51. package/src/injectHeldKeyCodes.ts +31 -0
  52. package/src/injectHeldKeys.ts +26 -0
  53. package/src/injectHotkey.ts +145 -0
  54. package/src/injectHotkeyRecorder.ts +128 -0
  55. package/src/injectHotkeySequence.ts +87 -0
  56. package/src/injectKeyHold.ts +52 -0
@@ -0,0 +1,128 @@
1
+ import {
2
+ DestroyRef,
3
+ computed,
4
+ effect,
5
+ inject,
6
+ linkedSignal,
7
+ untracked,
8
+ } from '@angular/core'
9
+ import { HotkeyRecorder } 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 { Hotkey, HotkeyRecorderOptions } from '@tanstack/hotkeys'
14
+
15
+ export interface AngularHotkeyRecorder {
16
+ /** Whether recording is currently active */
17
+ readonly isRecording: () => boolean
18
+ /** The currently recorded hotkey (for live preview) */
19
+ readonly recordedHotkey: () => Hotkey | null
20
+ /** Start recording a new hotkey */
21
+ readonly startRecording: () => void
22
+ /** Stop recording (same as cancel) */
23
+ readonly stopRecording: () => void
24
+ /** Cancel recording without saving */
25
+ readonly cancelRecording: () => void
26
+ }
27
+
28
+ /**
29
+ * Angular inject-based API for recording keyboard shortcuts.
30
+ *
31
+ * Thin wrapper around the framework-agnostic HotkeyRecorder class: captures
32
+ * keyboard events, converts them to hotkey strings, and handles Escape to
33
+ * cancel or Backspace/Delete to clear.
34
+ *
35
+ * @param options - Configuration options for the recorder (or getter)
36
+ * @returns Object with recording state signals and control functions
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * @Component({ ... })
41
+ * export class ShortcutSettingsComponent {
42
+ * shortcut = signal<Hotkey>('Mod+S')
43
+ * recorder = injectHotkeyRecorder({
44
+ * onRecord: (hotkey) => this.shortcut.set(hotkey),
45
+ * onCancel: () => console.log('Recording cancelled'),
46
+ * })
47
+ *
48
+ * constructor() {
49
+ * injectHotkey(
50
+ * () => this.shortcut(),
51
+ * () => this.handleSave(),
52
+ * () => ({ enabled: !this.recorder.isRecording() }),
53
+ * )
54
+ * }
55
+ * }
56
+ * ```
57
+ */
58
+ export function injectHotkeyRecorder(
59
+ options: HotkeyRecorderOptions | (() => HotkeyRecorderOptions),
60
+ ): AngularHotkeyRecorder {
61
+ const defaultOptions = injectDefaultHotkeysOptions()
62
+ const destroyRef = inject(DestroyRef)
63
+
64
+ // Stable signal to lazy initialize the recorder
65
+ const recorderSignal = computed(() =>
66
+ untracked(() => {
67
+ const resolvedOptions =
68
+ typeof options === 'function' ? options() : options
69
+
70
+ const mergedOptions = {
71
+ ...defaultOptions.hotkeyRecorder,
72
+ ...resolvedOptions,
73
+ } as HotkeyRecorderOptions
74
+
75
+ return new HotkeyRecorder(mergedOptions)
76
+ }),
77
+ )
78
+
79
+ // Subscribe to recorder state
80
+ const recorderStore = computed(() => untracked(() => recorderSignal().store))
81
+ const isRecording = injectLazyStore(
82
+ recorderStore,
83
+ (state) => state.isRecording,
84
+ )
85
+ const recordedHotkey = injectLazyStore(
86
+ recorderStore,
87
+ (state) => state.recordedHotkey,
88
+ )
89
+
90
+ // Sync options on every effect run (matches React's sync on render)
91
+ effect(() => {
92
+ const resolved = typeof options === 'function' ? options() : options
93
+ recorderSignal().setOptions({
94
+ ...defaultOptions.hotkeyRecorder,
95
+ ...resolved,
96
+ } as HotkeyRecorderOptions)
97
+ })
98
+
99
+ destroyRef.onDestroy(() => {
100
+ recorderSignal().destroy()
101
+ })
102
+
103
+ return {
104
+ isRecording,
105
+ recordedHotkey,
106
+ startRecording: () => recorderSignal().start(),
107
+ stopRecording: () => recorderSignal().stop(),
108
+ cancelRecording: () => recorderSignal().cancel(),
109
+ }
110
+ }
111
+
112
+ function injectLazyStore<TState, TSelected = NoInfer<TState>>(
113
+ storeSignal: Signal<Atom<TState> | ReadonlyAtom<TState>>,
114
+ selector: (state: NoInfer<TState>) => TSelected,
115
+ ): Signal<TSelected> {
116
+ const slice = linkedSignal(() => selector(storeSignal().get()))
117
+
118
+ effect((onCleanup) => {
119
+ const currentStore = storeSignal()
120
+ slice.set(selector(currentStore.get()))
121
+ const { unsubscribe } = currentStore.subscribe((s) => {
122
+ slice.set(selector(s))
123
+ })
124
+ onCleanup(() => unsubscribe())
125
+ })
126
+
127
+ return slice.asReadonly()
128
+ }
@@ -0,0 +1,87 @@
1
+ import { effect } from '@angular/core'
2
+ import { getSequenceManager } from '@tanstack/hotkeys'
3
+ import { injectDefaultHotkeysOptions } from './hotkeys-provider'
4
+ import type {
5
+ HotkeyCallback,
6
+ HotkeySequence,
7
+ SequenceOptions,
8
+ } from '@tanstack/hotkeys'
9
+
10
+ export interface InjectHotkeySequenceOptions extends Omit<
11
+ SequenceOptions,
12
+ 'enabled'
13
+ > {
14
+ /** Whether the sequence is enabled. Defaults to true. */
15
+ enabled?: boolean
16
+ }
17
+
18
+ /**
19
+ * Angular inject-based API for registering a keyboard shortcut sequence (Vim-style).
20
+ *
21
+ * Allows you to register multi-key sequences like 'g g' or 'd d' that trigger
22
+ * when the full sequence is pressed within a timeout.
23
+ *
24
+ * @param sequence - Array of hotkey strings that form the sequence (or getter function)
25
+ * @param callback - Function to call when the sequence is completed
26
+ * @param options - Options for the sequence behavior (or getter function)
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * @Component({ ... })
31
+ * export class VimEditorComponent {
32
+ * lastSequence = signal<string | null>(null)
33
+ *
34
+ * constructor() {
35
+ * injectHotkeySequence(['G', 'G'], () => this.lastSequence.set('gg → Go to top'))
36
+ * injectHotkeySequence(['D', 'D'], () => this.lastSequence.set('dd → Delete line'))
37
+ * injectHotkeySequence(['C', 'I', 'W'], () => this.lastSequence.set('ciw'), { timeout: 500 })
38
+ * }
39
+ * }
40
+ * ```
41
+ */
42
+ export function injectHotkeySequence(
43
+ sequence: HotkeySequence | (() => HotkeySequence),
44
+ callback: HotkeyCallback,
45
+ options:
46
+ | InjectHotkeySequenceOptions
47
+ | (() => InjectHotkeySequenceOptions) = {},
48
+ ): void {
49
+ const defaultOptions = injectDefaultHotkeysOptions()
50
+
51
+ effect((onCleanup) => {
52
+ // Resolve reactive values
53
+ const resolvedSequence =
54
+ typeof sequence === 'function' ? sequence() : sequence
55
+ const resolvedOptions = typeof options === 'function' ? options() : options
56
+
57
+ const mergedOptions = {
58
+ ...defaultOptions.hotkeySequence,
59
+ ...resolvedOptions,
60
+ } as InjectHotkeySequenceOptions
61
+
62
+ const { enabled = true, ...sequenceOptions } = mergedOptions
63
+
64
+ if (!enabled || resolvedSequence.length === 0) {
65
+ return
66
+ }
67
+
68
+ const manager = getSequenceManager()
69
+
70
+ // Pass through options; default target to document when not provided
71
+ const registerOptions: SequenceOptions = {
72
+ ...sequenceOptions,
73
+ enabled: true,
74
+ target:
75
+ sequenceOptions.target ??
76
+ (typeof document !== 'undefined' ? document : undefined),
77
+ }
78
+
79
+ const handle = manager.register(resolvedSequence, callback, registerOptions)
80
+
81
+ onCleanup(() => {
82
+ if (handle.isActive) {
83
+ handle.unregister()
84
+ }
85
+ })
86
+ })
87
+ }
@@ -0,0 +1,52 @@
1
+ import { computed } from '@angular/core'
2
+ import { injectStore } from '@tanstack/angular-store'
3
+ import { getKeyStateTracker } from '@tanstack/hotkeys'
4
+ import type { HeldKey } from '@tanstack/hotkeys'
5
+ import type { Signal } from '@angular/core'
6
+
7
+ /**
8
+ * Angular inject-based API that returns a signal that is true when the given key is held.
9
+ *
10
+ * Subscribes to the global KeyStateTracker and uses a computed to determine
11
+ * if the specified key is held.
12
+ *
13
+ * @param key - The key to check (e.g. 'Shift', 'Control', 'Space') - can be a getter function
14
+ * @returns Signal that returns true if the key is currently held down
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * @Component({
19
+ * template: `{{ isShiftHeld() ? 'Shift is pressed!' : 'Press Shift' }}`,
20
+ * })
21
+ * export class ShiftIndicatorComponent {
22
+ * isShiftHeld = injectKeyHold('Shift')
23
+ * }
24
+ * ```
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * @Component({
29
+ * template: `
30
+ * <span [style.opacity]="ctrl() ? 1 : 0.3">Ctrl</span>
31
+ * <span [style.opacity]="shift() ? 1 : 0.3">Shift</span>
32
+ * `,
33
+ * })
34
+ * export class ModifierIndicatorsComponent {
35
+ * ctrl = injectKeyHold('Control')
36
+ * shift = injectKeyHold('Shift')
37
+ * }
38
+ * ```
39
+ */
40
+ export function injectKeyHold(key: HeldKey | (() => HeldKey)): Signal<boolean> {
41
+ const tracker = getKeyStateTracker()
42
+ const heldKeysSelector = injectStore(tracker.store, (state) => state.heldKeys)
43
+
44
+ return computed(() => {
45
+ const resolvedKey = typeof key === 'function' ? key() : key
46
+ const normalizedKey = resolvedKey.toLowerCase()
47
+
48
+ return heldKeysSelector().some(
49
+ (heldKey) => heldKey.toLowerCase() === normalizedKey,
50
+ )
51
+ })
52
+ }