@tanstack/angular-hotkeys 0.19.1 → 0.20.0
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/dist/_zone.js +14 -0
- package/dist/hotkeys-provider.d.ts +22 -1
- package/dist/hotkeys-provider.js +22 -1
- package/dist/injectHeldKeyCodes.js +2 -1
- package/dist/injectHeldKeys.js +2 -1
- package/dist/injectHotkey.d.ts +5 -4
- package/dist/injectHotkeyHint.js +2 -1
- package/dist/injectHotkeyRecorder.js +18 -8
- package/dist/injectHotkeys.d.ts +3 -1
- package/dist/injectHotkeys.js +14 -4
- package/dist/injectKeyHold.js +2 -1
- package/package.json +4 -3
- package/skills/angular-hotkeys/SKILL.md +6 -9
package/dist/_zone.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NgZone, inject } from "@angular/core";
|
|
2
|
+
|
|
3
|
+
//#region src/_zone.ts
|
|
4
|
+
/**
|
|
5
|
+
* Runs `fn` outside the Angular zone, so core listeners it attaches don't
|
|
6
|
+
* run change detection on every event. Zoneless apps call `fn` directly.
|
|
7
|
+
* Must be called in an injection context.
|
|
8
|
+
*/
|
|
9
|
+
function runOutsideZone(fn) {
|
|
10
|
+
return inject(NgZone).runOutsideAngular(fn);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { runOutsideZone };
|
|
@@ -62,7 +62,9 @@ export type MaybeGetter<T> = T | (() => T);
|
|
|
62
62
|
* inherits app hotkeys and merges default options. With `scope`, hotkeys
|
|
63
63
|
* injected in the component and its children fire only while it exists and
|
|
64
64
|
* is enabled; with `exclusive`, only they fire. The scope activates when the
|
|
65
|
-
* first hotkey in it is injected.
|
|
65
|
+
* first hotkey in it is injected. Angular creates the provider lazily, so a
|
|
66
|
+
* component with no hotkeys of its own calls `injectHotkeysContext()` to
|
|
67
|
+
* activate its scope.
|
|
66
68
|
*
|
|
67
69
|
* @example
|
|
68
70
|
* ```ts
|
|
@@ -94,6 +96,21 @@ export type MaybeGetter<T> = T | (() => T);
|
|
|
94
96
|
*
|
|
95
97
|
* @example
|
|
96
98
|
* ```ts
|
|
99
|
+
* // An overlay with no hotkeys that suspends the page's hotkeys.
|
|
100
|
+
* @Component({
|
|
101
|
+
* selector: 'app-loading-overlay',
|
|
102
|
+
* providers: [provideHotkeys({ scope: 'loading', exclusive: true })],
|
|
103
|
+
* template: `...`,
|
|
104
|
+
* })
|
|
105
|
+
* export class LoadingOverlay {
|
|
106
|
+
* constructor() {
|
|
107
|
+
* injectHotkeysContext()
|
|
108
|
+
* }
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
97
114
|
* // A scope that follows the providing component's input.
|
|
98
115
|
* @Component({
|
|
99
116
|
* selector: 'app-folder',
|
|
@@ -112,6 +129,10 @@ export type MaybeGetter<T> = T | (() => T);
|
|
|
112
129
|
* ```
|
|
113
130
|
*/
|
|
114
131
|
export declare function provideHotkeys(options?: ProvideHotkeysOptions): Provider;
|
|
132
|
+
/**
|
|
133
|
+
* Returns the nearest provider's context. Injecting it activates that
|
|
134
|
+
* provider's scope.
|
|
135
|
+
*/
|
|
115
136
|
export declare function injectHotkeysContext(): HotkeysContextValue;
|
|
116
137
|
export declare function injectDefaultHotkeysOptions(): HotkeysProviderOptions;
|
|
117
138
|
//#endregion
|
package/dist/hotkeys-provider.js
CHANGED
|
@@ -71,7 +71,9 @@ function createHotkeysContext(options) {
|
|
|
71
71
|
* inherits app hotkeys and merges default options. With `scope`, hotkeys
|
|
72
72
|
* injected in the component and its children fire only while it exists and
|
|
73
73
|
* is enabled; with `exclusive`, only they fire. The scope activates when the
|
|
74
|
-
* first hotkey in it is injected.
|
|
74
|
+
* first hotkey in it is injected. Angular creates the provider lazily, so a
|
|
75
|
+
* component with no hotkeys of its own calls `injectHotkeysContext()` to
|
|
76
|
+
* activate its scope.
|
|
75
77
|
*
|
|
76
78
|
* @example
|
|
77
79
|
* ```ts
|
|
@@ -103,6 +105,21 @@ function createHotkeysContext(options) {
|
|
|
103
105
|
*
|
|
104
106
|
* @example
|
|
105
107
|
* ```ts
|
|
108
|
+
* // An overlay with no hotkeys that suspends the page's hotkeys.
|
|
109
|
+
* @Component({
|
|
110
|
+
* selector: 'app-loading-overlay',
|
|
111
|
+
* providers: [provideHotkeys({ scope: 'loading', exclusive: true })],
|
|
112
|
+
* template: `...`,
|
|
113
|
+
* })
|
|
114
|
+
* export class LoadingOverlay {
|
|
115
|
+
* constructor() {
|
|
116
|
+
* injectHotkeysContext()
|
|
117
|
+
* }
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
106
123
|
* // A scope that follows the providing component's input.
|
|
107
124
|
* @Component({
|
|
108
125
|
* selector: 'app-folder',
|
|
@@ -126,6 +143,10 @@ function provideHotkeys(options = {}) {
|
|
|
126
143
|
useFactory: () => createHotkeysContext(options)
|
|
127
144
|
};
|
|
128
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns the nearest provider's context. Injecting it activates that
|
|
148
|
+
* provider's scope.
|
|
149
|
+
*/
|
|
129
150
|
function injectHotkeysContext() {
|
|
130
151
|
return inject(HOTKEYS_INJECTION_TOKEN);
|
|
131
152
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { injectSelector } from "./_injectSelector.js";
|
|
2
|
+
import { runOutsideZone } from "./_zone.js";
|
|
2
3
|
import { getKeyStateTracker } from "@tanstack/hotkeys";
|
|
3
4
|
|
|
4
5
|
//#region src/injectHeldKeyCodes.ts
|
|
@@ -26,7 +27,7 @@ import { getKeyStateTracker } from "@tanstack/hotkeys";
|
|
|
26
27
|
* ```
|
|
27
28
|
*/
|
|
28
29
|
function injectHeldKeyCodes() {
|
|
29
|
-
const tracker = getKeyStateTracker
|
|
30
|
+
const tracker = runOutsideZone(getKeyStateTracker);
|
|
30
31
|
return injectSelector(tracker.store, (state) => state.heldCodes);
|
|
31
32
|
}
|
|
32
33
|
|
package/dist/injectHeldKeys.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { injectSelector } from "./_injectSelector.js";
|
|
2
|
+
import { runOutsideZone } from "./_zone.js";
|
|
2
3
|
import { getKeyStateTracker } from "@tanstack/hotkeys";
|
|
3
4
|
|
|
4
5
|
//#region src/injectHeldKeys.ts
|
|
@@ -21,7 +22,7 @@ import { getKeyStateTracker } from "@tanstack/hotkeys";
|
|
|
21
22
|
* ```
|
|
22
23
|
*/
|
|
23
24
|
function injectHeldKeys() {
|
|
24
|
-
const tracker = getKeyStateTracker
|
|
25
|
+
const tracker = runOutsideZone(getKeyStateTracker);
|
|
25
26
|
return injectSelector(tracker.store, (state) => state.heldKeys);
|
|
26
27
|
}
|
|
27
28
|
|
package/dist/injectHotkey.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { AppHotkey, HotkeyCallback, HotkeyCallbackContextFor, RegisterableHotkey, Target } from "@tanstack/hotkeys";
|
|
2
|
+
import { ElementRef, Signal } from "@angular/core";
|
|
2
3
|
import { AdapterAppHotkeyOptions, AdapterChordOptions, AdapterCommonOptions, AdapterHotkeyOptions, AdapterOptionsFor, AdapterSequenceOptions } from "@tanstack/hotkeys/adapter";
|
|
3
4
|
//#region src/injectHotkey.d.ts
|
|
4
5
|
/**
|
|
5
|
-
* The element to listen on
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* The element to listen on: an element, an `ElementRef`, or a signal of
|
|
7
|
+
* either, such as `viewChild<ElementRef>('panel')`. A `null` target, or a
|
|
8
|
+
* signal of `undefined`, skips registration until the element exists.
|
|
8
9
|
*/
|
|
9
|
-
export type InjectHotkeyTarget = Target | null;
|
|
10
|
+
export type InjectHotkeyTarget = Target | ElementRef<Target> | Signal<Target | ElementRef<Target> | null | undefined> | null;
|
|
10
11
|
/** Options shared by chords and sequences. */
|
|
11
12
|
export type InjectHotkeyOptions = AdapterHotkeyOptions<InjectHotkeyTarget>;
|
|
12
13
|
/** Options for a chord such as `'Mod+S'`. */
|
package/dist/injectHotkeyHint.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { injectHotkeysContext } from "./hotkeys-provider.js";
|
|
2
2
|
import { injectSelector } from "./_injectSelector.js";
|
|
3
|
+
import { runOutsideZone } from "./_zone.js";
|
|
3
4
|
import { getHotkeyManager, getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
|
|
4
5
|
import { computed } from "@angular/core";
|
|
5
6
|
|
|
@@ -17,7 +18,7 @@ function injectHotkeyHint(hotkey, options = {}) {
|
|
|
17
18
|
const context = injectHotkeysContext();
|
|
18
19
|
const manager = getHotkeyManager();
|
|
19
20
|
const registrations = injectSelector(manager.registrations, (state) => state);
|
|
20
|
-
const held = injectSelector(getKeyStateTracker
|
|
21
|
+
const held = injectSelector(runOutsideZone(getKeyStateTracker).store, (state) => state.heldKeys);
|
|
21
22
|
return computed(() => {
|
|
22
23
|
context.appHotkeysState();
|
|
23
24
|
const value = typeof hotkey === "function" ? hotkey() : hotkey;
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { injectDefaultHotkeysOptions } from "./hotkeys-provider.js";
|
|
2
2
|
import { HotkeyRecorder } from "@tanstack/hotkeys";
|
|
3
|
-
import { DestroyRef, computed, effect, inject, linkedSignal, untracked } from "@angular/core";
|
|
3
|
+
import { DestroyRef, NgZone, computed, effect, inject, linkedSignal, untracked } from "@angular/core";
|
|
4
4
|
import { withDefaults } from "@tanstack/hotkeys/adapter";
|
|
5
5
|
|
|
6
6
|
//#region src/injectHotkeyRecorder.ts
|
|
7
7
|
function injectHotkeyRecorder(options) {
|
|
8
8
|
const defaultOptions = injectDefaultHotkeysOptions();
|
|
9
9
|
const destroyRef = inject(DestroyRef);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const zone = inject(NgZone);
|
|
11
|
+
const resolve = () => {
|
|
12
|
+
const resolved = withDefaults(defaultOptions.hotkeyRecorder, typeof options === "function" ? options() : options);
|
|
13
|
+
for (const key of [
|
|
14
|
+
"onRecord",
|
|
15
|
+
"onCancel",
|
|
16
|
+
"onClear",
|
|
17
|
+
"onReject"
|
|
18
|
+
]) {
|
|
19
|
+
const callback = resolved[key];
|
|
20
|
+
if (callback) resolved[key] = (...args) => zone.run(callback, null, args);
|
|
21
|
+
}
|
|
22
|
+
return resolved;
|
|
23
|
+
};
|
|
24
|
+
const recorderSignal = computed(() => untracked(() => new HotkeyRecorder(resolve())));
|
|
14
25
|
const recorderStore = computed(() => untracked(() => recorderSignal().store));
|
|
15
26
|
const isRecording = injectLazyStore(recorderStore, (state) => state.isRecording);
|
|
16
27
|
const steps = injectLazyStore(recorderStore, (state) => state.steps);
|
|
17
28
|
const recordedHotkey = injectLazyStore(recorderStore, (state) => state.recordedHotkey);
|
|
18
29
|
effect(() => {
|
|
19
|
-
|
|
20
|
-
recorderSignal().setOptions(withDefaults(defaultOptions.hotkeyRecorder, resolved));
|
|
30
|
+
recorderSignal().setOptions(resolve());
|
|
21
31
|
});
|
|
22
32
|
destroyRef.onDestroy(() => {
|
|
23
33
|
recorderSignal().destroy();
|
|
@@ -26,7 +36,7 @@ function injectHotkeyRecorder(options) {
|
|
|
26
36
|
isRecording,
|
|
27
37
|
steps,
|
|
28
38
|
recordedHotkey,
|
|
29
|
-
startRecording: () => recorderSignal().start(),
|
|
39
|
+
startRecording: () => zone.runOutsideAngular(() => recorderSignal().start()),
|
|
30
40
|
stopRecording: () => recorderSignal().stop(),
|
|
31
41
|
cancelRecording: () => recorderSignal().cancel(),
|
|
32
42
|
commitRecording: () => recorderSignal().commit()
|
package/dist/injectHotkeys.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ export type InjectHotkeyDefinition = AdapterHotkeyDefinition<InjectHotkeyTarget>
|
|
|
13
13
|
* Options merge in this order: provider defaults, app hotkey declaration,
|
|
14
14
|
* commonOptions, per-hotkey options. Each option applies only to the kinds
|
|
15
15
|
* that support it. Unbound app hotkeys and empty sequences register nothing.
|
|
16
|
-
* A `
|
|
16
|
+
* A target may be an element, an `ElementRef`, or a signal of either, such
|
|
17
|
+
* as `viewChild('panel')`. A `null` target, or a signal of `undefined`, skips
|
|
18
|
+
* registration until the element exists.
|
|
17
19
|
*
|
|
18
20
|
* @example
|
|
19
21
|
* ```ts
|
package/dist/injectHotkeys.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { injectHotkeysContext } from "./hotkeys-provider.js";
|
|
2
|
-
import { DestroyRef, effect, inject, untracked } from "@angular/core";
|
|
2
|
+
import { DestroyRef, ElementRef, NgZone, effect, inject, untracked } from "@angular/core";
|
|
3
3
|
import { createHotkeyBindings, prepareHotkeyDefinitions } from "@tanstack/hotkeys/adapter";
|
|
4
4
|
|
|
5
5
|
//#region src/injectHotkeys.ts
|
|
@@ -10,7 +10,9 @@ import { createHotkeyBindings, prepareHotkeyDefinitions } from "@tanstack/hotkey
|
|
|
10
10
|
* Options merge in this order: provider defaults, app hotkey declaration,
|
|
11
11
|
* commonOptions, per-hotkey options. Each option applies only to the kinds
|
|
12
12
|
* that support it. Unbound app hotkeys and empty sequences register nothing.
|
|
13
|
-
* A `
|
|
13
|
+
* A target may be an element, an `ElementRef`, or a signal of either, such
|
|
14
|
+
* as `viewChild('panel')`. A `null` target, or a signal of `undefined`, skips
|
|
15
|
+
* registration until the element exists.
|
|
14
16
|
*
|
|
15
17
|
* @example
|
|
16
18
|
* ```ts
|
|
@@ -22,6 +24,7 @@ import { createHotkeyBindings, prepareHotkeyDefinitions } from "@tanstack/hotkey
|
|
|
22
24
|
*/
|
|
23
25
|
function injectHotkeys(hotkeys, commonOptions = {}) {
|
|
24
26
|
const context = injectHotkeysContext();
|
|
27
|
+
const zone = inject(NgZone);
|
|
25
28
|
const bindings = createHotkeyBindings();
|
|
26
29
|
inject(DestroyRef).onDestroy(() => bindings.destroy());
|
|
27
30
|
effect(() => {
|
|
@@ -30,9 +33,16 @@ function injectHotkeys(hotkeys, commonOptions = {}) {
|
|
|
30
33
|
appHotkeysStore: context.appHotkeysStore,
|
|
31
34
|
defaults: context.defaultOptions.hotkey,
|
|
32
35
|
commonOptions: typeof commonOptions === "function" ? commonOptions() : commonOptions,
|
|
33
|
-
resolveTarget: (target) =>
|
|
36
|
+
resolveTarget: (target) => {
|
|
37
|
+
const value = typeof target === "function" ? target() : target;
|
|
38
|
+
return (value instanceof ElementRef ? value.nativeElement : value) ?? null;
|
|
39
|
+
}
|
|
34
40
|
});
|
|
35
|
-
|
|
41
|
+
for (const definition of definitions) {
|
|
42
|
+
const callback = definition.callback;
|
|
43
|
+
definition.callback = (...args) => zone.run(callback, null, args);
|
|
44
|
+
}
|
|
45
|
+
untracked(() => zone.runOutsideAngular(() => bindings.update(definitions)));
|
|
36
46
|
});
|
|
37
47
|
}
|
|
38
48
|
|
package/dist/injectKeyHold.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { injectSelector } from "./_injectSelector.js";
|
|
2
|
+
import { runOutsideZone } from "./_zone.js";
|
|
2
3
|
import { getKeyStateTracker } from "@tanstack/hotkeys";
|
|
3
4
|
import { computed } from "@angular/core";
|
|
4
5
|
|
|
@@ -37,7 +38,7 @@ import { computed } from "@angular/core";
|
|
|
37
38
|
* ```
|
|
38
39
|
*/
|
|
39
40
|
function injectKeyHold(key) {
|
|
40
|
-
const tracker = getKeyStateTracker
|
|
41
|
+
const tracker = runOutsideZone(getKeyStateTracker);
|
|
41
42
|
const heldKeysSelector = injectSelector(tracker.store, (state) => state.heldKeys);
|
|
42
43
|
return computed(() => {
|
|
43
44
|
const normalizedKey = (typeof key === "function" ? key() : key).toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/angular-hotkeys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Angular adapter for TanStack Hotkeys",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"skills"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@tanstack/hotkeys": "0.22.
|
|
40
|
+
"@tanstack/hotkeys": "0.22.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@angular/common": ">=19.0.0",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"@angular/common": "^22.2.1",
|
|
48
48
|
"@angular/core": "^22.2.1",
|
|
49
49
|
"@angular/platform-browser-dynamic": "^22.2.1",
|
|
50
|
-
"typescript": "6.0.3"
|
|
50
|
+
"typescript": "6.0.3",
|
|
51
|
+
"zone.js": "~0.16.3"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
54
|
"clean": "premove ./build ./dist",
|
|
@@ -48,13 +48,8 @@ export class PanelComponent {
|
|
|
48
48
|
() => this.save(),
|
|
49
49
|
() => ({ enabled: this.isDirty() }),
|
|
50
50
|
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
() => this.close(),
|
|
54
|
-
() => ({
|
|
55
|
-
target: this.panel()?.nativeElement ?? null,
|
|
56
|
-
}),
|
|
57
|
-
)
|
|
51
|
+
// A viewChild signal target registers once the element renders.
|
|
52
|
+
injectHotkey('Escape', () => this.close(), { target: this.panel })
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
save() {}
|
|
@@ -69,7 +64,7 @@ export class PanelComponent {
|
|
|
69
64
|
| `injectHotkey(hotkey, callback, options?)` | Register one chord, sequence, or app hotkey for the component's or service's lifetime; options can be a function that reads signals. |
|
|
70
65
|
| `injectHotkeys(definitions, commonOptions?)` | Register several hotkeys, or a function returning a list built from data, in one call. |
|
|
71
66
|
| `provideHotkeys(options)` | Set default options, a scope (`exclusive` for dialogs), `enabled`, and app hotkeys in the app config or a component's `providers`. |
|
|
72
|
-
| `injectHotkeysContext()` | Read the nearest provided defaults and enabled state
|
|
67
|
+
| `injectHotkeysContext()` | Read the nearest provided defaults and enabled state. Activates that provider's scope. |
|
|
73
68
|
| `injectDefaultHotkeysOptions()` | Read the default options merged from every provider above. |
|
|
74
69
|
| `injectAppHotkeys()` | Get a signal of the declared app hotkeys with the user's custom hotkeys applied. |
|
|
75
70
|
| `injectAppHotkeyList()` | Get a signal listing every app hotkey with its status, for a cheat sheet or settings screen. |
|
|
@@ -116,7 +111,9 @@ A component's `providers` apply to the component itself, so it can register its
|
|
|
116
111
|
|
|
117
112
|
## Pitfalls
|
|
118
113
|
|
|
119
|
-
- **Pass options as a function.** `{ enabled: this.isDirty() }` is read once, in the constructor.
|
|
114
|
+
- **Pass options as a function.** `{ enabled: this.isDirty() }` is read once, in the constructor. Use `() => ({ ... })`.
|
|
115
|
+
- **Pass the `viewChild` signal as `target`, not its value.** `this.panel()` in the constructor is `undefined`, and an undefined target means `document`. `{ target: this.panel }` waits for the element.
|
|
116
|
+
- **An exclusive provider with no hotkeys never activates.** Angular creates providers lazily. Call `injectHotkeysContext()` in the component's constructor to activate the scope.
|
|
120
117
|
- **Replace `@HostListener('document:keydown')` and `(keydown.control.s)` bindings with `injectHotkey`.**
|
|
121
118
|
- **Register runtime lists in the constructor.** `injectHotkey` throws outside an injection context, such as in `ngOnInit`. For hotkeys from data, use `injectHotkeys(() => this.items().map(...))`.
|
|
122
119
|
- **Call the inject functions directly.** A shortcut service or `injectShortcut(keys: string)` wrapper has to cast strings to hotkeys, which turns off type checking and drops sequences, signal options, and app hotkeys.
|