@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.
- package/README.md +122 -0
- package/dist/hotkeys-provider.cjs +26 -0
- package/dist/hotkeys-provider.cjs.map +1 -0
- package/dist/hotkeys-provider.d.cts +21 -0
- package/dist/hotkeys-provider.d.ts +21 -0
- package/dist/hotkeys-provider.js +23 -0
- package/dist/hotkeys-provider.js.map +1 -0
- package/dist/index.cjs +26 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/dist/injectHeldKeyCodes.cjs +34 -0
- package/dist/injectHeldKeyCodes.cjs.map +1 -0
- package/dist/injectHeldKeyCodes.d.cts +30 -0
- package/dist/injectHeldKeyCodes.d.ts +30 -0
- package/dist/injectHeldKeyCodes.js +34 -0
- package/dist/injectHeldKeyCodes.js.map +1 -0
- package/dist/injectHeldKeys.cjs +29 -0
- package/dist/injectHeldKeys.cjs.map +1 -0
- package/dist/injectHeldKeys.d.cts +25 -0
- package/dist/injectHeldKeys.d.ts +25 -0
- package/dist/injectHeldKeys.js +29 -0
- package/dist/injectHeldKeys.js.map +1 -0
- package/dist/injectHotkey.cjs +93 -0
- package/dist/injectHotkey.cjs.map +1 -0
- package/dist/injectHotkey.d.cts +73 -0
- package/dist/injectHotkey.d.ts +73 -0
- package/dist/injectHotkey.js +93 -0
- package/dist/injectHotkey.js.map +1 -0
- package/dist/injectHotkeyRecorder.cjs +82 -0
- package/dist/injectHotkeyRecorder.cjs.map +1 -0
- package/dist/injectHotkeyRecorder.d.cts +49 -0
- package/dist/injectHotkeyRecorder.d.ts +49 -0
- package/dist/injectHotkeyRecorder.js +82 -0
- package/dist/injectHotkeyRecorder.js.map +1 -0
- package/dist/injectHotkeySequence.cjs +55 -0
- package/dist/injectHotkeySequence.cjs.map +1 -0
- package/dist/injectHotkeySequence.d.cts +35 -0
- package/dist/injectHotkeySequence.d.ts +35 -0
- package/dist/injectHotkeySequence.js +55 -0
- package/dist/injectHotkeySequence.js.map +1 -0
- package/dist/injectKeyHold.cjs +49 -0
- package/dist/injectKeyHold.cjs.map +1 -0
- package/dist/injectKeyHold.d.cts +41 -0
- package/dist/injectKeyHold.d.ts +41 -0
- package/dist/injectKeyHold.js +49 -0
- package/dist/injectKeyHold.js.map +1 -0
- package/package.json +67 -0
- package/src/hotkeys-provider.ts +40 -0
- package/src/index.ts +13 -0
- package/src/injectHeldKeyCodes.ts +31 -0
- package/src/injectHeldKeys.ts +26 -0
- package/src/injectHotkey.ts +145 -0
- package/src/injectHotkeyRecorder.ts +128 -0
- package/src/injectHotkeySequence.ts +87 -0
- 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
|
+
}
|