@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,35 @@
|
|
|
1
|
+
import { HotkeyCallback, HotkeySequence, SequenceOptions } from "@tanstack/hotkeys";
|
|
2
|
+
|
|
3
|
+
//#region src/injectHotkeySequence.d.ts
|
|
4
|
+
interface InjectHotkeySequenceOptions extends Omit<SequenceOptions, 'enabled'> {
|
|
5
|
+
/** Whether the sequence is enabled. Defaults to true. */
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Angular inject-based API for registering a keyboard shortcut sequence (Vim-style).
|
|
10
|
+
*
|
|
11
|
+
* Allows you to register multi-key sequences like 'g g' or 'd d' that trigger
|
|
12
|
+
* when the full sequence is pressed within a timeout.
|
|
13
|
+
*
|
|
14
|
+
* @param sequence - Array of hotkey strings that form the sequence (or getter function)
|
|
15
|
+
* @param callback - Function to call when the sequence is completed
|
|
16
|
+
* @param options - Options for the sequence behavior (or getter function)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* @Component({ ... })
|
|
21
|
+
* export class VimEditorComponent {
|
|
22
|
+
* lastSequence = signal<string | null>(null)
|
|
23
|
+
*
|
|
24
|
+
* constructor() {
|
|
25
|
+
* injectHotkeySequence(['G', 'G'], () => this.lastSequence.set('gg → Go to top'))
|
|
26
|
+
* injectHotkeySequence(['D', 'D'], () => this.lastSequence.set('dd → Delete line'))
|
|
27
|
+
* injectHotkeySequence(['C', 'I', 'W'], () => this.lastSequence.set('ciw'), { timeout: 500 })
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare function injectHotkeySequence(sequence: HotkeySequence | (() => HotkeySequence), callback: HotkeyCallback, options?: InjectHotkeySequenceOptions | (() => InjectHotkeySequenceOptions)): void;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { InjectHotkeySequenceOptions, injectHotkeySequence };
|
|
35
|
+
//# sourceMappingURL=injectHotkeySequence.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { injectDefaultHotkeysOptions } from "./hotkeys-provider.js";
|
|
2
|
+
import { getSequenceManager } from "@tanstack/hotkeys";
|
|
3
|
+
import { effect } from "@angular/core";
|
|
4
|
+
|
|
5
|
+
//#region src/injectHotkeySequence.ts
|
|
6
|
+
/**
|
|
7
|
+
* Angular inject-based API for registering a keyboard shortcut sequence (Vim-style).
|
|
8
|
+
*
|
|
9
|
+
* Allows you to register multi-key sequences like 'g g' or 'd d' that trigger
|
|
10
|
+
* when the full sequence is pressed within a timeout.
|
|
11
|
+
*
|
|
12
|
+
* @param sequence - Array of hotkey strings that form the sequence (or getter function)
|
|
13
|
+
* @param callback - Function to call when the sequence is completed
|
|
14
|
+
* @param options - Options for the sequence behavior (or getter function)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* @Component({ ... })
|
|
19
|
+
* export class VimEditorComponent {
|
|
20
|
+
* lastSequence = signal<string | null>(null)
|
|
21
|
+
*
|
|
22
|
+
* constructor() {
|
|
23
|
+
* injectHotkeySequence(['G', 'G'], () => this.lastSequence.set('gg → Go to top'))
|
|
24
|
+
* injectHotkeySequence(['D', 'D'], () => this.lastSequence.set('dd → Delete line'))
|
|
25
|
+
* injectHotkeySequence(['C', 'I', 'W'], () => this.lastSequence.set('ciw'), { timeout: 500 })
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function injectHotkeySequence(sequence, callback, options = {}) {
|
|
31
|
+
const defaultOptions = injectDefaultHotkeysOptions();
|
|
32
|
+
effect((onCleanup) => {
|
|
33
|
+
const resolvedSequence = typeof sequence === "function" ? sequence() : sequence;
|
|
34
|
+
const resolvedOptions = typeof options === "function" ? options() : options;
|
|
35
|
+
const { enabled = true, ...sequenceOptions } = {
|
|
36
|
+
...defaultOptions.hotkeySequence,
|
|
37
|
+
...resolvedOptions
|
|
38
|
+
};
|
|
39
|
+
if (!enabled || resolvedSequence.length === 0) return;
|
|
40
|
+
const manager = getSequenceManager();
|
|
41
|
+
const registerOptions = {
|
|
42
|
+
...sequenceOptions,
|
|
43
|
+
enabled: true,
|
|
44
|
+
target: sequenceOptions.target ?? (typeof document !== "undefined" ? document : void 0)
|
|
45
|
+
};
|
|
46
|
+
const handle = manager.register(resolvedSequence, callback, registerOptions);
|
|
47
|
+
onCleanup(() => {
|
|
48
|
+
if (handle.isActive) handle.unregister();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { injectHotkeySequence };
|
|
55
|
+
//# sourceMappingURL=injectHotkeySequence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injectHotkeySequence.js","names":[],"sources":["../src/injectHotkeySequence.ts"],"sourcesContent":["import { effect } from '@angular/core'\nimport { getSequenceManager } from '@tanstack/hotkeys'\nimport { injectDefaultHotkeysOptions } from './hotkeys-provider'\nimport type {\n HotkeyCallback,\n HotkeySequence,\n SequenceOptions,\n} from '@tanstack/hotkeys'\n\nexport interface InjectHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'enabled'\n> {\n /** Whether the sequence is enabled. Defaults to true. */\n enabled?: boolean\n}\n\n/**\n * Angular inject-based API for registering a keyboard shortcut sequence (Vim-style).\n *\n * Allows you to register multi-key sequences like 'g g' or 'd d' that trigger\n * when the full sequence is pressed within a timeout.\n *\n * @param sequence - Array of hotkey strings that form the sequence (or getter function)\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior (or getter function)\n *\n * @example\n * ```ts\n * @Component({ ... })\n * export class VimEditorComponent {\n * lastSequence = signal<string | null>(null)\n *\n * constructor() {\n * injectHotkeySequence(['G', 'G'], () => this.lastSequence.set('gg → Go to top'))\n * injectHotkeySequence(['D', 'D'], () => this.lastSequence.set('dd → Delete line'))\n * injectHotkeySequence(['C', 'I', 'W'], () => this.lastSequence.set('ciw'), { timeout: 500 })\n * }\n * }\n * ```\n */\nexport function injectHotkeySequence(\n sequence: HotkeySequence | (() => HotkeySequence),\n callback: HotkeyCallback,\n options:\n | InjectHotkeySequenceOptions\n | (() => InjectHotkeySequenceOptions) = {},\n): void {\n const defaultOptions = injectDefaultHotkeysOptions()\n\n effect((onCleanup) => {\n // Resolve reactive values\n const resolvedSequence =\n typeof sequence === 'function' ? sequence() : sequence\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkeySequence,\n ...resolvedOptions,\n } as InjectHotkeySequenceOptions\n\n const { enabled = true, ...sequenceOptions } = mergedOptions\n\n if (!enabled || resolvedSequence.length === 0) {\n return\n }\n\n const manager = getSequenceManager()\n\n // Pass through options; default target to document when not provided\n const registerOptions: SequenceOptions = {\n ...sequenceOptions,\n enabled: true,\n target:\n sequenceOptions.target ??\n (typeof document !== 'undefined' ? document : undefined),\n }\n\n const handle = manager.register(resolvedSequence, callback, registerOptions)\n\n onCleanup(() => {\n if (handle.isActive) {\n handle.unregister()\n }\n })\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,qBACd,UACA,UACA,UAE0C,EAAE,EACtC;CACN,MAAM,iBAAiB,6BAA6B;AAEpD,SAAQ,cAAc;EAEpB,MAAM,mBACJ,OAAO,aAAa,aAAa,UAAU,GAAG;EAChD,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAOpE,MAAM,EAAE,UAAU,MAAM,GAAG,oBALL;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;AAID,MAAI,CAAC,WAAW,iBAAiB,WAAW,EAC1C;EAGF,MAAM,UAAU,oBAAoB;EAGpC,MAAM,kBAAmC;GACvC,GAAG;GACH,SAAS;GACT,QACE,gBAAgB,WACf,OAAO,aAAa,cAAc,WAAW;GACjD;EAED,MAAM,SAAS,QAAQ,SAAS,kBAAkB,UAAU,gBAAgB;AAE5E,kBAAgB;AACd,OAAI,OAAO,SACT,QAAO,YAAY;IAErB;GACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
let _tanstack_hotkeys = require("@tanstack/hotkeys");
|
|
2
|
+
let _angular_core = require("@angular/core");
|
|
3
|
+
let _tanstack_angular_store = require("@tanstack/angular-store");
|
|
4
|
+
|
|
5
|
+
//#region src/injectKeyHold.ts
|
|
6
|
+
/**
|
|
7
|
+
* Angular inject-based API that returns a signal that is true when the given key is held.
|
|
8
|
+
*
|
|
9
|
+
* Subscribes to the global KeyStateTracker and uses a computed to determine
|
|
10
|
+
* if the specified key is held.
|
|
11
|
+
*
|
|
12
|
+
* @param key - The key to check (e.g. 'Shift', 'Control', 'Space') - can be a getter function
|
|
13
|
+
* @returns Signal that returns true if the key is currently held down
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* @Component({
|
|
18
|
+
* template: `{{ isShiftHeld() ? 'Shift is pressed!' : 'Press Shift' }}`,
|
|
19
|
+
* })
|
|
20
|
+
* export class ShiftIndicatorComponent {
|
|
21
|
+
* isShiftHeld = injectKeyHold('Shift')
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* @Component({
|
|
28
|
+
* template: `
|
|
29
|
+
* <span [style.opacity]="ctrl() ? 1 : 0.3">Ctrl</span>
|
|
30
|
+
* <span [style.opacity]="shift() ? 1 : 0.3">Shift</span>
|
|
31
|
+
* `,
|
|
32
|
+
* })
|
|
33
|
+
* export class ModifierIndicatorsComponent {
|
|
34
|
+
* ctrl = injectKeyHold('Control')
|
|
35
|
+
* shift = injectKeyHold('Shift')
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function injectKeyHold(key) {
|
|
40
|
+
const heldKeysSelector = (0, _tanstack_angular_store.injectStore)((0, _tanstack_hotkeys.getKeyStateTracker)().store, (state) => state.heldKeys);
|
|
41
|
+
return (0, _angular_core.computed)(() => {
|
|
42
|
+
const normalizedKey = (typeof key === "function" ? key() : key).toLowerCase();
|
|
43
|
+
return heldKeysSelector().some((heldKey) => heldKey.toLowerCase() === normalizedKey);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
exports.injectKeyHold = injectKeyHold;
|
|
49
|
+
//# sourceMappingURL=injectKeyHold.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injectKeyHold.cjs","names":[],"sources":["../src/injectKeyHold.ts"],"sourcesContent":["import { computed } from '@angular/core'\nimport { injectStore } from '@tanstack/angular-store'\nimport { getKeyStateTracker } from '@tanstack/hotkeys'\nimport type { HeldKey } from '@tanstack/hotkeys'\nimport type { Signal } from '@angular/core'\n\n/**\n * Angular inject-based API that returns a signal that is true when the given key is held.\n *\n * Subscribes to the global KeyStateTracker and uses a computed to determine\n * if the specified key is held.\n *\n * @param key - The key to check (e.g. 'Shift', 'Control', 'Space') - can be a getter function\n * @returns Signal that returns true if the key is currently held down\n *\n * @example\n * ```ts\n * @Component({\n * template: `{{ isShiftHeld() ? 'Shift is pressed!' : 'Press Shift' }}`,\n * })\n * export class ShiftIndicatorComponent {\n * isShiftHeld = injectKeyHold('Shift')\n * }\n * ```\n *\n * @example\n * ```ts\n * @Component({\n * template: `\n * <span [style.opacity]=\"ctrl() ? 1 : 0.3\">Ctrl</span>\n * <span [style.opacity]=\"shift() ? 1 : 0.3\">Shift</span>\n * `,\n * })\n * export class ModifierIndicatorsComponent {\n * ctrl = injectKeyHold('Control')\n * shift = injectKeyHold('Shift')\n * }\n * ```\n */\nexport function injectKeyHold(key: HeldKey | (() => HeldKey)): Signal<boolean> {\n const tracker = getKeyStateTracker()\n const heldKeysSelector = injectStore(tracker.store, (state) => state.heldKeys)\n\n return computed(() => {\n const resolvedKey = typeof key === 'function' ? key() : key\n const normalizedKey = resolvedKey.toLowerCase()\n\n return heldKeysSelector().some(\n (heldKey) => heldKey.toLowerCase() === normalizedKey,\n )\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,cAAc,KAAiD;CAE7E,MAAM,uGAD8B,CACS,QAAQ,UAAU,MAAM,SAAS;AAE9E,0CAAsB;EAEpB,MAAM,iBADc,OAAO,QAAQ,aAAa,KAAK,GAAG,KACtB,aAAa;AAE/C,SAAO,kBAAkB,CAAC,MACvB,YAAY,QAAQ,aAAa,KAAK,cACxC;GACD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HeldKey } from "@tanstack/hotkeys";
|
|
2
|
+
import { Signal } from "@angular/core";
|
|
3
|
+
|
|
4
|
+
//#region src/injectKeyHold.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Angular inject-based API that returns a signal that is true when the given key is held.
|
|
7
|
+
*
|
|
8
|
+
* Subscribes to the global KeyStateTracker and uses a computed to determine
|
|
9
|
+
* if the specified key is held.
|
|
10
|
+
*
|
|
11
|
+
* @param key - The key to check (e.g. 'Shift', 'Control', 'Space') - can be a getter function
|
|
12
|
+
* @returns Signal that returns true if the key is currently held down
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* @Component({
|
|
17
|
+
* template: `{{ isShiftHeld() ? 'Shift is pressed!' : 'Press Shift' }}`,
|
|
18
|
+
* })
|
|
19
|
+
* export class ShiftIndicatorComponent {
|
|
20
|
+
* isShiftHeld = injectKeyHold('Shift')
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* @Component({
|
|
27
|
+
* template: `
|
|
28
|
+
* <span [style.opacity]="ctrl() ? 1 : 0.3">Ctrl</span>
|
|
29
|
+
* <span [style.opacity]="shift() ? 1 : 0.3">Shift</span>
|
|
30
|
+
* `,
|
|
31
|
+
* })
|
|
32
|
+
* export class ModifierIndicatorsComponent {
|
|
33
|
+
* ctrl = injectKeyHold('Control')
|
|
34
|
+
* shift = injectKeyHold('Shift')
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function injectKeyHold(key: HeldKey | (() => HeldKey)): Signal<boolean>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { injectKeyHold };
|
|
41
|
+
//# sourceMappingURL=injectKeyHold.d.cts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HeldKey } from "@tanstack/hotkeys";
|
|
2
|
+
import { Signal } from "@angular/core";
|
|
3
|
+
|
|
4
|
+
//#region src/injectKeyHold.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Angular inject-based API that returns a signal that is true when the given key is held.
|
|
7
|
+
*
|
|
8
|
+
* Subscribes to the global KeyStateTracker and uses a computed to determine
|
|
9
|
+
* if the specified key is held.
|
|
10
|
+
*
|
|
11
|
+
* @param key - The key to check (e.g. 'Shift', 'Control', 'Space') - can be a getter function
|
|
12
|
+
* @returns Signal that returns true if the key is currently held down
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* @Component({
|
|
17
|
+
* template: `{{ isShiftHeld() ? 'Shift is pressed!' : 'Press Shift' }}`,
|
|
18
|
+
* })
|
|
19
|
+
* export class ShiftIndicatorComponent {
|
|
20
|
+
* isShiftHeld = injectKeyHold('Shift')
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* @Component({
|
|
27
|
+
* template: `
|
|
28
|
+
* <span [style.opacity]="ctrl() ? 1 : 0.3">Ctrl</span>
|
|
29
|
+
* <span [style.opacity]="shift() ? 1 : 0.3">Shift</span>
|
|
30
|
+
* `,
|
|
31
|
+
* })
|
|
32
|
+
* export class ModifierIndicatorsComponent {
|
|
33
|
+
* ctrl = injectKeyHold('Control')
|
|
34
|
+
* shift = injectKeyHold('Shift')
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function injectKeyHold(key: HeldKey | (() => HeldKey)): Signal<boolean>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { injectKeyHold };
|
|
41
|
+
//# sourceMappingURL=injectKeyHold.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getKeyStateTracker } from "@tanstack/hotkeys";
|
|
2
|
+
import { computed } from "@angular/core";
|
|
3
|
+
import { injectStore } from "@tanstack/angular-store";
|
|
4
|
+
|
|
5
|
+
//#region src/injectKeyHold.ts
|
|
6
|
+
/**
|
|
7
|
+
* Angular inject-based API that returns a signal that is true when the given key is held.
|
|
8
|
+
*
|
|
9
|
+
* Subscribes to the global KeyStateTracker and uses a computed to determine
|
|
10
|
+
* if the specified key is held.
|
|
11
|
+
*
|
|
12
|
+
* @param key - The key to check (e.g. 'Shift', 'Control', 'Space') - can be a getter function
|
|
13
|
+
* @returns Signal that returns true if the key is currently held down
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* @Component({
|
|
18
|
+
* template: `{{ isShiftHeld() ? 'Shift is pressed!' : 'Press Shift' }}`,
|
|
19
|
+
* })
|
|
20
|
+
* export class ShiftIndicatorComponent {
|
|
21
|
+
* isShiftHeld = injectKeyHold('Shift')
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* @Component({
|
|
28
|
+
* template: `
|
|
29
|
+
* <span [style.opacity]="ctrl() ? 1 : 0.3">Ctrl</span>
|
|
30
|
+
* <span [style.opacity]="shift() ? 1 : 0.3">Shift</span>
|
|
31
|
+
* `,
|
|
32
|
+
* })
|
|
33
|
+
* export class ModifierIndicatorsComponent {
|
|
34
|
+
* ctrl = injectKeyHold('Control')
|
|
35
|
+
* shift = injectKeyHold('Shift')
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function injectKeyHold(key) {
|
|
40
|
+
const heldKeysSelector = injectStore(getKeyStateTracker().store, (state) => state.heldKeys);
|
|
41
|
+
return computed(() => {
|
|
42
|
+
const normalizedKey = (typeof key === "function" ? key() : key).toLowerCase();
|
|
43
|
+
return heldKeysSelector().some((heldKey) => heldKey.toLowerCase() === normalizedKey);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { injectKeyHold };
|
|
49
|
+
//# sourceMappingURL=injectKeyHold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injectKeyHold.js","names":[],"sources":["../src/injectKeyHold.ts"],"sourcesContent":["import { computed } from '@angular/core'\nimport { injectStore } from '@tanstack/angular-store'\nimport { getKeyStateTracker } from '@tanstack/hotkeys'\nimport type { HeldKey } from '@tanstack/hotkeys'\nimport type { Signal } from '@angular/core'\n\n/**\n * Angular inject-based API that returns a signal that is true when the given key is held.\n *\n * Subscribes to the global KeyStateTracker and uses a computed to determine\n * if the specified key is held.\n *\n * @param key - The key to check (e.g. 'Shift', 'Control', 'Space') - can be a getter function\n * @returns Signal that returns true if the key is currently held down\n *\n * @example\n * ```ts\n * @Component({\n * template: `{{ isShiftHeld() ? 'Shift is pressed!' : 'Press Shift' }}`,\n * })\n * export class ShiftIndicatorComponent {\n * isShiftHeld = injectKeyHold('Shift')\n * }\n * ```\n *\n * @example\n * ```ts\n * @Component({\n * template: `\n * <span [style.opacity]=\"ctrl() ? 1 : 0.3\">Ctrl</span>\n * <span [style.opacity]=\"shift() ? 1 : 0.3\">Shift</span>\n * `,\n * })\n * export class ModifierIndicatorsComponent {\n * ctrl = injectKeyHold('Control')\n * shift = injectKeyHold('Shift')\n * }\n * ```\n */\nexport function injectKeyHold(key: HeldKey | (() => HeldKey)): Signal<boolean> {\n const tracker = getKeyStateTracker()\n const heldKeysSelector = injectStore(tracker.store, (state) => state.heldKeys)\n\n return computed(() => {\n const resolvedKey = typeof key === 'function' ? key() : key\n const normalizedKey = resolvedKey.toLowerCase()\n\n return heldKeysSelector().some(\n (heldKey) => heldKey.toLowerCase() === normalizedKey,\n )\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,cAAc,KAAiD;CAE7E,MAAM,mBAAmB,YADT,oBAAoB,CACS,QAAQ,UAAU,MAAM,SAAS;AAE9E,QAAO,eAAe;EAEpB,MAAM,iBADc,OAAO,QAAQ,aAAa,KAAK,GAAG,KACtB,aAAa;AAE/C,SAAO,kBAAkB,CAAC,MACvB,YAAY,QAAQ,aAAa,KAAK,cACxC;GACD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/angular-hotkeys",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Angular adapter for TanStack Hotkeys",
|
|
5
|
+
"author": "Tanner Linsley",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/hotkeys.git",
|
|
10
|
+
"directory": "packages/angular-hotkeys"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://tanstack.com/hotkeys",
|
|
13
|
+
"funding": {
|
|
14
|
+
"type": "github",
|
|
15
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"angular",
|
|
19
|
+
"tanstack",
|
|
20
|
+
"hotkeys",
|
|
21
|
+
"keyboard",
|
|
22
|
+
"shortcuts"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "premove ./build ./dist",
|
|
26
|
+
"lint": "eslint ./src",
|
|
27
|
+
"lint:fix": "eslint ./src --fix",
|
|
28
|
+
"test:eslint": "eslint ./src",
|
|
29
|
+
"test:lib": "vitest --passWithNoTests",
|
|
30
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
31
|
+
"test:types": "tsc",
|
|
32
|
+
"build": "tsdown"
|
|
33
|
+
},
|
|
34
|
+
"type": "module",
|
|
35
|
+
"main": "./dist/index.cjs",
|
|
36
|
+
"module": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.cts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"require": "./dist/index.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"src"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@tanstack/angular-store": "^0.9.2",
|
|
55
|
+
"@tanstack/hotkeys": "workspace:*"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@angular/common": ">=19.0.0",
|
|
59
|
+
"@angular/core": ">=19.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@angular/common": "^21.2.1",
|
|
63
|
+
"@angular/core": "^21.2.1",
|
|
64
|
+
"@angular/platform-browser-dynamic": "^21.2.1",
|
|
65
|
+
"typescript": "5.9.3"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { InjectionToken, inject } from '@angular/core'
|
|
2
|
+
import type { HotkeyRecorderOptions } from '@tanstack/hotkeys'
|
|
3
|
+
import type { InjectHotkeyOptions } from './injectHotkey'
|
|
4
|
+
import type { InjectHotkeySequenceOptions } from './injectHotkeySequence'
|
|
5
|
+
import type { StaticProvider } from '@angular/core'
|
|
6
|
+
|
|
7
|
+
export interface HotkeysProviderOptions {
|
|
8
|
+
hotkey?: Partial<InjectHotkeyOptions>
|
|
9
|
+
hotkeyRecorder?: Partial<HotkeyRecorderOptions>
|
|
10
|
+
hotkeySequence?: Partial<InjectHotkeySequenceOptions>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HotkeysContextValue {
|
|
14
|
+
readonly defaultOptions: HotkeysProviderOptions
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const HOTKEYS_INJECTION_TOKEN = new InjectionToken<HotkeysContextValue>(
|
|
18
|
+
'HOTKEYS_INJECTION_TOKEN',
|
|
19
|
+
{
|
|
20
|
+
providedIn: 'root',
|
|
21
|
+
factory: () => ({ defaultOptions: {} }),
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export function provideHotkeys(
|
|
26
|
+
defaultOptions: HotkeysProviderOptions,
|
|
27
|
+
): StaticProvider {
|
|
28
|
+
return {
|
|
29
|
+
provide: HOTKEYS_INJECTION_TOKEN,
|
|
30
|
+
useValue: { defaultOptions },
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function injectHotkeysContext(): HotkeysContextValue {
|
|
35
|
+
return inject(HOTKEYS_INJECTION_TOKEN)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function injectDefaultHotkeysOptions(): HotkeysProviderOptions {
|
|
39
|
+
return injectHotkeysContext().defaultOptions
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Re-export everything from the core package
|
|
2
|
+
export * from '@tanstack/hotkeys'
|
|
3
|
+
|
|
4
|
+
// Provider
|
|
5
|
+
export * from './hotkeys-provider'
|
|
6
|
+
|
|
7
|
+
// Angular-specific primitives
|
|
8
|
+
export * from './injectHotkey'
|
|
9
|
+
export * from './injectHotkeySequence'
|
|
10
|
+
export * from './injectHeldKeys'
|
|
11
|
+
export * from './injectHeldKeyCodes'
|
|
12
|
+
export * from './injectKeyHold'
|
|
13
|
+
export * from './injectHotkeyRecorder'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getKeyStateTracker } from '@tanstack/hotkeys'
|
|
2
|
+
import { injectStore } from '@tanstack/angular-store'
|
|
3
|
+
import type { Signal } from '@angular/core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Angular inject-based API that returns a signal of a map from held key names to their physical `event.code` values.
|
|
7
|
+
*
|
|
8
|
+
* Useful for debugging which physical key was pressed (e.g. distinguishing
|
|
9
|
+
* left vs right Shift via "ShiftLeft" / "ShiftRight").
|
|
10
|
+
*
|
|
11
|
+
* @returns Signal of record mapping normalized key names to their `event.code` values
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* @Component({
|
|
16
|
+
* template: `
|
|
17
|
+
* @for (key of heldKeys(); track key) {
|
|
18
|
+
* <kbd>{{ key }} <small>{{ heldCodes()[key] }}</small></kbd>
|
|
19
|
+
* }
|
|
20
|
+
* `,
|
|
21
|
+
* })
|
|
22
|
+
* export class KeyDebugComponent {
|
|
23
|
+
* heldKeys = injectHeldKeys()
|
|
24
|
+
* heldCodes = injectHeldKeyCodes()
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function injectHeldKeyCodes(): Signal<Record<string, string>> {
|
|
29
|
+
const tracker = getKeyStateTracker()
|
|
30
|
+
return injectStore(tracker.store, (state) => state.heldCodes)
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getKeyStateTracker } from '@tanstack/hotkeys'
|
|
2
|
+
import { injectStore } from '@tanstack/angular-store'
|
|
3
|
+
import type { Signal } from '@angular/core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Angular inject-based API that returns a signal of currently held keyboard keys.
|
|
7
|
+
*
|
|
8
|
+
* Subscribes to the global KeyStateTracker and updates whenever keys are
|
|
9
|
+
* pressed or released.
|
|
10
|
+
*
|
|
11
|
+
* @returns Signal of array of currently held key names
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* @Component({
|
|
16
|
+
* template: `<div>Currently pressed: {{ heldKeys().join(' + ') || 'None' }}</div>`,
|
|
17
|
+
* })
|
|
18
|
+
* export class KeyDisplayComponent {
|
|
19
|
+
* heldKeys = injectHeldKeys()
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function injectHeldKeys(): Signal<Array<string>> {
|
|
24
|
+
const tracker = getKeyStateTracker()
|
|
25
|
+
return injectStore(tracker.store, (state) => state.heldKeys)
|
|
26
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { effect } from '@angular/core'
|
|
2
|
+
import {
|
|
3
|
+
detectPlatform,
|
|
4
|
+
formatHotkey,
|
|
5
|
+
getHotkeyManager,
|
|
6
|
+
rawHotkeyToParsedHotkey,
|
|
7
|
+
} from '@tanstack/hotkeys'
|
|
8
|
+
import { injectDefaultHotkeysOptions } from './hotkeys-provider'
|
|
9
|
+
import type {
|
|
10
|
+
Hotkey,
|
|
11
|
+
HotkeyCallback,
|
|
12
|
+
HotkeyOptions,
|
|
13
|
+
RegisterableHotkey,
|
|
14
|
+
} from '@tanstack/hotkeys'
|
|
15
|
+
|
|
16
|
+
export interface InjectHotkeyOptions extends Omit<HotkeyOptions, 'target'> {
|
|
17
|
+
/**
|
|
18
|
+
* The DOM element to attach the event listener to.
|
|
19
|
+
* Can be a direct DOM element, an accessor (for reactive targets that become
|
|
20
|
+
* available after mount), or null. Defaults to document.
|
|
21
|
+
* When using scoped targets, pass an accessor: () => ({ target: elementSignal() })
|
|
22
|
+
* so the hotkey waits for the element to be attached before registering.
|
|
23
|
+
*/
|
|
24
|
+
target?: HTMLElement | Document | Window | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Angular inject-based API for registering a keyboard hotkey.
|
|
29
|
+
*
|
|
30
|
+
* Uses the singleton HotkeyManager for efficient event handling.
|
|
31
|
+
* The callback receives both the keyboard event and a context object
|
|
32
|
+
* containing the hotkey string and parsed hotkey.
|
|
33
|
+
*
|
|
34
|
+
* Call in an injection context (e.g. constructor or field initializer).
|
|
35
|
+
* Uses effect() to track reactive dependencies and update registration
|
|
36
|
+
* when options or the callback change.
|
|
37
|
+
*
|
|
38
|
+
* @param hotkey - The hotkey string (e.g. 'Mod+S', 'Escape') or getter function
|
|
39
|
+
* @param callback - The function to call when the hotkey is pressed
|
|
40
|
+
* @param options - Options for the hotkey behavior, or getter for reactive options
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* @Component({ ... })
|
|
45
|
+
* export class SaveButtonComponent {
|
|
46
|
+
* private readonly saveCount = signal(0)
|
|
47
|
+
*
|
|
48
|
+
* constructor() {
|
|
49
|
+
* injectHotkey('Mod+S', (event, { hotkey }) => {
|
|
50
|
+
* event.preventDefault()
|
|
51
|
+
* this.saveCount.update(c => c + 1)
|
|
52
|
+
* })
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* @Component({ ... })
|
|
60
|
+
* export class ModalComponent {
|
|
61
|
+
* isOpen = signal(true)
|
|
62
|
+
*
|
|
63
|
+
* constructor() {
|
|
64
|
+
* injectHotkey('Escape', () => this.close(), () => ({ enabled: this.isOpen() }))
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* @Component({ ... })
|
|
72
|
+
* export class EditorComponent {
|
|
73
|
+
* private readonly editorRef = viewChild<ElementRef<HTMLDivElement>>('editorRef')
|
|
74
|
+
*
|
|
75
|
+
* constructor() {
|
|
76
|
+
* injectHotkey('Mod+B', () => this.toggleBold(), () => ({
|
|
77
|
+
* target: this.editorRef()?.nativeElement ?? null,
|
|
78
|
+
* }))
|
|
79
|
+
* }
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function injectHotkey(
|
|
84
|
+
hotkey: RegisterableHotkey | (() => RegisterableHotkey),
|
|
85
|
+
callback: HotkeyCallback,
|
|
86
|
+
options: InjectHotkeyOptions | (() => InjectHotkeyOptions) = {},
|
|
87
|
+
): void {
|
|
88
|
+
const defaultOptions = injectDefaultHotkeysOptions()
|
|
89
|
+
const manager = getHotkeyManager()
|
|
90
|
+
|
|
91
|
+
effect((onCleanup) => {
|
|
92
|
+
// Resolve reactive values
|
|
93
|
+
const resolvedHotkey = typeof hotkey === 'function' ? hotkey() : hotkey
|
|
94
|
+
const resolvedOptions = typeof options === 'function' ? options() : options
|
|
95
|
+
|
|
96
|
+
const mergedOptions = {
|
|
97
|
+
...defaultOptions.hotkey,
|
|
98
|
+
...resolvedOptions,
|
|
99
|
+
} as InjectHotkeyOptions
|
|
100
|
+
|
|
101
|
+
// Normalize to hotkey string
|
|
102
|
+
const platform = mergedOptions.platform ?? detectPlatform()
|
|
103
|
+
const hotkeyString: Hotkey =
|
|
104
|
+
typeof resolvedHotkey === 'string'
|
|
105
|
+
? resolvedHotkey
|
|
106
|
+
: (formatHotkey(
|
|
107
|
+
rawHotkeyToParsedHotkey(resolvedHotkey, platform),
|
|
108
|
+
) as Hotkey)
|
|
109
|
+
|
|
110
|
+
// Resolve target: when explicitly provided (even as null), use it and skip if null.
|
|
111
|
+
// When not provided, default to document. Matches React's ref handling.
|
|
112
|
+
const resolvedTarget =
|
|
113
|
+
'target' in mergedOptions
|
|
114
|
+
? (mergedOptions.target ?? null)
|
|
115
|
+
: typeof document !== 'undefined'
|
|
116
|
+
? document
|
|
117
|
+
: null
|
|
118
|
+
|
|
119
|
+
if (!resolvedTarget) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract options without target (target is handled separately)
|
|
124
|
+
const { target: _target, ...optionsWithoutTarget } = mergedOptions
|
|
125
|
+
|
|
126
|
+
// Register the hotkey
|
|
127
|
+
const registration = manager.register(hotkeyString, callback, {
|
|
128
|
+
...optionsWithoutTarget,
|
|
129
|
+
target: resolvedTarget,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Update callback and options on every effect run
|
|
133
|
+
if (registration.isActive) {
|
|
134
|
+
registration.callback = callback
|
|
135
|
+
registration.setOptions(optionsWithoutTarget)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Cleanup on disposal
|
|
139
|
+
onCleanup(() => {
|
|
140
|
+
if (registration.isActive) {
|
|
141
|
+
registration.unregister()
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
}
|