rx-hotkeys 2.4.1 → 2.6.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/README.md +32 -8
- package/dist/hotkeys.d.ts +39 -11
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +180 -44
- package/dist/hotkeys.test.js +173 -1
- package/dist/tt.d.ts +125 -0
- package/dist/tt.d.ts.map +1 -0
- package/dist/tt.js +384 -0
- package/dist/ttt.d.ts +164 -0
- package/dist/ttt.d.ts.map +1 -0
- package/dist/ttt.js +439 -0
- package/dist/tttt.d.ts +67 -0
- package/dist/tttt.d.ts.map +1 -0
- package/dist/tttt.js +301 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ rx-hotkeys is a powerful and flexible TypeScript library for managing keyboard s
|
|
|
8
8
|
* **Key Sequences:** Define shortcuts that trigger when a series of keys are pressed in a specific order.
|
|
9
9
|
* **Sequence Timeouts:** Optional timeout between key presses in a sequence to prevent accidental triggers or indefinite waiting.
|
|
10
10
|
* **Context Management:** Activate or deactivate groups of shortcuts based on the application's current state (e.g., "editor", "modal", "global").
|
|
11
|
+
* **Strict Global Shortcuts:** Option to register global shortcuts that *only* fire when no other context is active, preventing them from triggering unintentionally.
|
|
11
12
|
* **Type-Safe Key Definitions:** Uses an exported `Keys` object based on standard `KeyboardEvent.key` values for improved developer experience and fewer errors.
|
|
12
13
|
* **RxJS Powered:** Built on RxJS for robust and efficient event handling.
|
|
13
14
|
* **Prevent Default:** Option to prevent the default browser action for a triggered shortcut.
|
|
@@ -29,7 +30,7 @@ First, ensure you have the `rx-hotkeys` library and its helper Keys imported:
|
|
|
29
30
|
import { Hotkeys, Keys, KeyCombinationConfig, KeySequenceConfig } from 'rx-hotkeys';
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
1. Initialize Hotkeys
|
|
33
|
+
### 1. Initialize Hotkeys
|
|
33
34
|
|
|
34
35
|
Create an instance of the `Hotkeys` class. You can optionally provide an initial context and enable debug mode.
|
|
35
36
|
|
|
@@ -40,7 +41,7 @@ const keyManager = new Hotkeys(); // No initial context, debug mode off
|
|
|
40
41
|
// const keyManager = new Hotkeys('editor', true);
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
2. Add a Key Combination
|
|
44
|
+
### 2. Add a Key Combination
|
|
44
45
|
|
|
45
46
|
Register a shortcut for a key combination, like Ctrl+S.
|
|
46
47
|
|
|
@@ -58,7 +59,9 @@ const saveConfig: KeyCombinationConfig = {
|
|
|
58
59
|
keyManager.addCombination(saveConfig);
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
3. Add a Key
|
|
62
|
+
### 3. Add a Key Sequence
|
|
63
|
+
|
|
64
|
+
Register a shortcut for a sequence of keys, like the Konami code.
|
|
62
65
|
|
|
63
66
|
```typescript
|
|
64
67
|
const konamiConfig: KeySequenceConfig = {
|
|
@@ -81,7 +84,9 @@ const konamiConfig: KeySequenceConfig = {
|
|
|
81
84
|
keyManager.addSequence(konamiConfig);
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
4. Manage
|
|
87
|
+
### 4. Manage Contexts
|
|
88
|
+
|
|
89
|
+
Control which shortcuts are active by setting the context.
|
|
85
90
|
|
|
86
91
|
```typescript
|
|
87
92
|
// Assuming some shortcuts are configured with context: "editor"
|
|
@@ -91,7 +96,25 @@ keyManager.setContext("editor"); // Activates "editor" shortcuts and global shor
|
|
|
91
96
|
keyManager.setContext(null);
|
|
92
97
|
```
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
#### Global vs. Strict Global Shortcuts
|
|
100
|
+
|
|
101
|
+
Global shortcuts (those without a `context` property) have two behaviors:
|
|
102
|
+
|
|
103
|
+
* **Default Global**: By default, a global shortcut will fire in *any* context, unless a more specific shortcut for the same key combination exists for that context.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// This shortcut for Ctrl+P will fire in the "editor" context, "modal" context, or any other,
|
|
107
|
+
// unless a specific "editor" shortcut for Ctrl+P exists.
|
|
108
|
+
keyManager.addCombination({ id: 'globalPrint', keys: { key: Keys.P, ctrlKey: true }, callback: myCallback });
|
|
109
|
+
```
|
|
110
|
+
* **Strict Global**: By passing `true` as the second argument to `addCombination` or `addSequence`, you can register a "strict" global shortcut. This shortcut will **only** fire when no context is active (`keyManager.getContext()` returns `null`).
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// This help shortcut for "?" will ONLY fire when no other context is active.
|
|
114
|
+
keyManager.addCombination({ id: 'strictHelp', keys: Keys.QuestionMark, callback: openHelpModal }, true);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 5. Clean Up
|
|
95
118
|
|
|
96
119
|
When the Hotkeys instance is no longer needed (e.g., component unmount), call `destroy()` to clean up subscriptions and prevent memory leaks.
|
|
97
120
|
|
|
@@ -170,6 +193,7 @@ Cleans up all subscriptions and resources. Essential to call to prevent memory l
|
|
|
170
193
|
* `context?: string | null`: Specifies the context in which this shortcut is active. If `null` or `undefined`, it's a global shortcut.
|
|
171
194
|
* `preventDefault?: boolean`: If true, `event.preventDefault()` will be called when the shortcut triggers. Defaults to `false`.
|
|
172
195
|
* `description?: string`: An optional description for the shortcut (e.g., for help menus).
|
|
196
|
+
* `strict?: boolean` (optional): If `true` and the shortcut has no `context`, it will only fire when no other context is active. Defaults to `false`.
|
|
173
197
|
|
|
174
198
|
`KeySequenceConfig`
|
|
175
199
|
|
|
@@ -180,13 +204,13 @@ Cleans up all subscriptions and resources. Essential to call to prevent memory l
|
|
|
180
204
|
* `preventDefault?: boolean`: If true, `event.preventDefault()` is called for the last event in the sequence. Defaults to `false`.
|
|
181
205
|
* `description?: string`: Optional description.
|
|
182
206
|
* `sequenceTimeoutMs?: number`: Optional. Maximum time (in milliseconds) allowed between consecutive key presses in the sequence. If exceeded, the sequence resets. If `0` or `undefined`, no inter-key timeout is applied (uses simpler buffer-based matching).
|
|
207
|
+
* `strict?: boolean` (optional): If `true` and the shortcut has no `context`, it will only fire when no other context is active. Defaults to `false`.
|
|
183
208
|
|
|
184
209
|
|
|
185
210
|
## Key Matching Logic
|
|
186
211
|
|
|
187
|
-
* Single Character Keys (e.g., `Keys.A`, `Keys.Digit7`): When you configure a shortcut with a single character key from `Keys`, the library matches it case-insensitively against the `event.key` from the browser. For example, if you configure `Keys.A`, it will trigger for both "a" and "A" key presses (assuming Shift isn't a required modifier).
|
|
188
|
-
* Special Keys (e.g., `Keys.Enter`, `Keys.ArrowUp`, `Keys.Escape`): These are multi-character `event.key` values. The library matches these case-sensitively against the `event.key`. Using the `Keys` object ensures you provide the correct, standard case-sensitive string.
|
|
189
|
-
|
|
212
|
+
* **Single Character Keys** (e.g., `Keys.A`, `Keys.Digit7`): When you configure a shortcut with a single character key from `Keys`, the library matches it case-insensitively against the `event.key` from the browser. For example, if you configure `Keys.A`, it will trigger for both "a" and "A" key presses (assuming Shift isn't a required modifier).
|
|
213
|
+
* **Special Keys** (e.g., `Keys.Enter`, `Keys.ArrowUp`, `Keys.Escape`): These are multi-character `event.key` values. The library matches these case-sensitively against the `event.key`. Using the `Keys` object ensures you provide the correct, standard case-sensitive string.
|
|
190
214
|
|
|
191
215
|
## Contributing
|
|
192
216
|
|
package/dist/hotkeys.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ interface ShortcutConfigBase {
|
|
|
10
10
|
context?: string | null;
|
|
11
11
|
preventDefault?: boolean;
|
|
12
12
|
description?: string;
|
|
13
|
+
/**
|
|
14
|
+
* **Only applicable if the shortcut has no top-level `context` defined.**
|
|
15
|
+
* If `true`, this shortcut is **strictly global** and will only fire when the active
|
|
16
|
+
* hotkey context is `null`.
|
|
17
|
+
* If `false` or `undefined` (the default), the shortcut can fire in *any* context,
|
|
18
|
+
* but will be suppressed by an identical shortcut that belongs to the active context.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
strict?: boolean;
|
|
13
22
|
}
|
|
14
23
|
/**
|
|
15
24
|
* Defines a single key trigger, which can be a StandardKey (for simple presses like "Escape")
|
|
@@ -34,15 +43,19 @@ type KeyCombinationTrigger = {
|
|
|
34
43
|
} | StandardKey;
|
|
35
44
|
export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
36
45
|
/**
|
|
37
|
-
* Defines the key or key combination.
|
|
38
|
-
* Can be
|
|
46
|
+
* Defines the key or key combination(s) that trigger the shortcut.
|
|
47
|
+
* Can be a single trigger or an array of triggers.
|
|
48
|
+
* Each trigger can be an object specifying the main `key` (from `StandardKey`) and optional
|
|
39
49
|
* modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
|
|
40
50
|
* Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
|
|
41
51
|
*
|
|
42
|
-
* Alternatively, for a simple key press without any modifiers,
|
|
52
|
+
* Alternatively, for a simple key press without any modifiers, a trigger can be
|
|
43
53
|
* a `StandardKey` directly.
|
|
44
54
|
* Example: `Keys.Escape` for the Escape key. When using this shorthand,
|
|
45
55
|
* it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
|
|
56
|
+
*
|
|
57
|
+
* To define multiple triggers for the same action:
|
|
58
|
+
* Example: `keys: [Keys.Enter, { key: Keys.Space, ctrlKey: true }]`
|
|
46
59
|
*/
|
|
47
60
|
keys: KeyCombinationTrigger | KeyCombinationTrigger[];
|
|
48
61
|
}
|
|
@@ -95,7 +108,7 @@ export declare class Hotkeys {
|
|
|
95
108
|
* will be active and can be triggered.
|
|
96
109
|
* @param contextName - The name of the context (e.g., "modal", "editor", "global").
|
|
97
110
|
* Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
|
|
98
|
-
* @returns
|
|
111
|
+
* @returns `true` if the context was changed, `false` if the new context was the same as the current one.
|
|
99
112
|
*/
|
|
100
113
|
setContext(contextName: string | null): boolean;
|
|
101
114
|
/**
|
|
@@ -109,6 +122,12 @@ export declare class Hotkeys {
|
|
|
109
122
|
* @param enable - True to enable debug logs, false to disable.
|
|
110
123
|
*/
|
|
111
124
|
setDebugMode(enable: boolean): void;
|
|
125
|
+
/**
|
|
126
|
+
* Checks if a shortcut with the given ID is currently registered and active.
|
|
127
|
+
* @param id - The unique ID of the shortcut to check.
|
|
128
|
+
* @returns True if a shortcut with the specified ID exists, false otherwise.
|
|
129
|
+
*/
|
|
130
|
+
hasShortcut(id: string): boolean;
|
|
112
131
|
/**
|
|
113
132
|
* An Observable that emits the new context name (or null) whenever the active context changes.
|
|
114
133
|
* This allows external parts of the application to react to context transitions.
|
|
@@ -129,11 +148,20 @@ export declare class Hotkeys {
|
|
|
129
148
|
*/
|
|
130
149
|
get onContextChange$(): Observable<string | null>;
|
|
131
150
|
/**
|
|
132
|
-
*
|
|
133
|
-
* @param
|
|
134
|
-
* @
|
|
151
|
+
* Compares two sequences of StandardKey arrays to see if they are identical.
|
|
152
|
+
* @param seq1 - The first sequence array.
|
|
153
|
+
* @param seq2 - The second sequence array.
|
|
154
|
+
* @returns True if the sequences are identical, false otherwise.
|
|
135
155
|
*/
|
|
136
|
-
|
|
156
|
+
private _areSequencesIdentical;
|
|
157
|
+
/**
|
|
158
|
+
* Checks if a given KeyCombinationConfig matches a given KeyboardEvent.
|
|
159
|
+
* This is used internally for priority checking.
|
|
160
|
+
* @param shortcutConfig The KeyCombinationConfig to check.
|
|
161
|
+
* @param event The KeyboardEvent to match against.
|
|
162
|
+
* @returns True if the shortcutConfig matches the event, false otherwise.
|
|
163
|
+
*/
|
|
164
|
+
private _shortcutMatchesEvent;
|
|
137
165
|
private filterByContext;
|
|
138
166
|
private _registerShortcut;
|
|
139
167
|
/**
|
|
@@ -162,10 +190,10 @@ export declare class Hotkeys {
|
|
|
162
190
|
* callback: () => console.log("File saved!"),
|
|
163
191
|
* context: "editor"
|
|
164
192
|
* });
|
|
165
|
-
* // For just the Escape key
|
|
193
|
+
* // For just the Escape key, or Ctrl+Space
|
|
166
194
|
* keyManager.addCombination({
|
|
167
195
|
* id: "closeModal",
|
|
168
|
-
* keys: Keys.Escape,
|
|
196
|
+
* keys: [Keys.Escape, {key: Keys.Space, ctrlKey: true}],
|
|
169
197
|
* callback: () => console.log("Modal closed!")
|
|
170
198
|
* });
|
|
171
199
|
* ```
|
|
@@ -205,7 +233,7 @@ export declare class Hotkeys {
|
|
|
205
233
|
* This can be useful for displaying available shortcuts to the user or for debugging.
|
|
206
234
|
* @returns An array of objects, where each object represents an active shortcut
|
|
207
235
|
* and includes its `id`, `description` (if provided), `context` (if any),
|
|
208
|
-
* and `type` (
|
|
236
|
+
* and `type` (from `ShortcutTypes` enum).
|
|
209
237
|
*/
|
|
210
238
|
getActiveShortcuts(): {
|
|
211
239
|
id: string;
|
package/dist/hotkeys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"AAAA,OAAO,EACyB,YAAY,EAAE,UAAU,EAEvD,MAAM,MAAM,CAAC;AACd,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAI7C,oBAAY,aAAa;IACrB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;CACxB;
|
|
1
|
+
{"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"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"}
|
package/dist/hotkeys.js
CHANGED
|
@@ -5,6 +5,12 @@ export var ShortcutTypes;
|
|
|
5
5
|
ShortcutTypes["Combination"] = "combination";
|
|
6
6
|
ShortcutTypes["Sequence"] = "sequence";
|
|
7
7
|
})(ShortcutTypes || (ShortcutTypes = {}));
|
|
8
|
+
var EmitStates;
|
|
9
|
+
(function (EmitStates) {
|
|
10
|
+
EmitStates[EmitStates["Emit"] = 0] = "Emit";
|
|
11
|
+
EmitStates[EmitStates["Ignore"] = 1] = "Ignore";
|
|
12
|
+
EmitStates[EmitStates["InProgress"] = 2] = "InProgress";
|
|
13
|
+
})(EmitStates || (EmitStates = {}));
|
|
8
14
|
// --- Helper function to compare keys ---
|
|
9
15
|
/**
|
|
10
16
|
* Compares a browser event's key with a configured key.
|
|
@@ -21,12 +27,6 @@ function compareKey(eventKey, configuredKey) {
|
|
|
21
27
|
return eventKey === configuredKey;
|
|
22
28
|
}
|
|
23
29
|
// --- Hotkeys Library ---
|
|
24
|
-
var EmitStates;
|
|
25
|
-
(function (EmitStates) {
|
|
26
|
-
EmitStates[EmitStates["Emit"] = 0] = "Emit";
|
|
27
|
-
EmitStates[EmitStates["Ignore"] = 1] = "Ignore";
|
|
28
|
-
EmitStates[EmitStates["InProgress"] = 2] = "InProgress";
|
|
29
|
-
})(EmitStates || (EmitStates = {}));
|
|
30
30
|
/**
|
|
31
31
|
* Manages keyboard shortcuts for web applications.
|
|
32
32
|
* Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
|
|
@@ -63,7 +63,7 @@ export class Hotkeys {
|
|
|
63
63
|
* will be active and can be triggered.
|
|
64
64
|
* @param contextName - The name of the context (e.g., "modal", "editor", "global").
|
|
65
65
|
* Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
|
|
66
|
-
* @returns
|
|
66
|
+
* @returns `true` if the context was changed, `false` if the new context was the same as the current one.
|
|
67
67
|
*/
|
|
68
68
|
setContext(contextName) {
|
|
69
69
|
const currentContext = this.activeContext$.getValue();
|
|
@@ -72,14 +72,14 @@ export class Hotkeys {
|
|
|
72
72
|
// Optional: Log that no change is happening, or simply do nothing.
|
|
73
73
|
console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
|
|
74
74
|
}
|
|
75
|
-
return false; // Context
|
|
75
|
+
return false; // Context was NOT updated
|
|
76
76
|
}
|
|
77
77
|
// If we reach here, the context is actually changing.
|
|
78
78
|
if (this.debugMode) {
|
|
79
79
|
console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${currentContext}" to "${contextName}".`);
|
|
80
80
|
}
|
|
81
81
|
this.activeContext$.next(contextName);
|
|
82
|
-
return true;
|
|
82
|
+
return true; // Context WAS updated
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Gets the current active context.
|
|
@@ -94,17 +94,25 @@ export class Hotkeys {
|
|
|
94
94
|
* @param enable - True to enable debug logs, false to disable.
|
|
95
95
|
*/
|
|
96
96
|
setDebugMode(enable) {
|
|
97
|
-
if (this.debugMode === enable) {
|
|
98
|
-
return;
|
|
97
|
+
if (this.debugMode === enable) {
|
|
98
|
+
return;
|
|
99
99
|
}
|
|
100
|
-
this.debugMode = enable;
|
|
101
|
-
if (enable) {
|
|
100
|
+
this.debugMode = enable;
|
|
101
|
+
if (enable) {
|
|
102
102
|
console.log(`${Hotkeys.LOG_PREFIX} Debug mode enabled.`);
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
105
105
|
console.log(`${Hotkeys.LOG_PREFIX} Debug mode disabled.`);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Checks if a shortcut with the given ID is currently registered and active.
|
|
110
|
+
* @param id - The unique ID of the shortcut to check.
|
|
111
|
+
* @returns True if a shortcut with the specified ID exists, false otherwise.
|
|
112
|
+
*/
|
|
113
|
+
hasShortcut(id) {
|
|
114
|
+
return this.activeShortcuts.has(id);
|
|
115
|
+
}
|
|
108
116
|
/**
|
|
109
117
|
* An Observable that emits the new context name (or null) whenever the active context changes.
|
|
110
118
|
* This allows external parts of the application to react to context transitions.
|
|
@@ -127,18 +135,84 @@ export class Hotkeys {
|
|
|
127
135
|
return this.activeContext$.asObservable();
|
|
128
136
|
}
|
|
129
137
|
/**
|
|
130
|
-
*
|
|
131
|
-
* @param
|
|
132
|
-
* @
|
|
138
|
+
* Compares two sequences of StandardKey arrays to see if they are identical.
|
|
139
|
+
* @param seq1 - The first sequence array.
|
|
140
|
+
* @param seq2 - The second sequence array.
|
|
141
|
+
* @returns True if the sequences are identical, false otherwise.
|
|
133
142
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
_areSequencesIdentical(seq1, seq2) {
|
|
144
|
+
if (seq1.length !== seq2.length) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
for (let i = 0; i < seq1.length; i++) {
|
|
148
|
+
if (seq1[i] !== seq2[i]) { // Direct comparison for canonical StandardKey values
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Checks if a given KeyCombinationConfig matches a given KeyboardEvent.
|
|
156
|
+
* This is used internally for priority checking.
|
|
157
|
+
* @param shortcutConfig The KeyCombinationConfig to check.
|
|
158
|
+
* @param event The KeyboardEvent to match against.
|
|
159
|
+
* @returns True if the shortcutConfig matches the event, false otherwise.
|
|
160
|
+
*/
|
|
161
|
+
_shortcutMatchesEvent(shortcutConfig, event) {
|
|
162
|
+
const keyTriggers = Array.isArray(shortcutConfig.keys) ? shortcutConfig.keys : [shortcutConfig.keys];
|
|
163
|
+
for (const keyInput of keyTriggers) {
|
|
164
|
+
let configuredMainKey;
|
|
165
|
+
let ctrlKeyConfig;
|
|
166
|
+
let altKeyConfig;
|
|
167
|
+
let shiftKeyConfig;
|
|
168
|
+
let metaKeyConfig;
|
|
169
|
+
if (typeof keyInput === "string") {
|
|
170
|
+
if (keyInput === "")
|
|
171
|
+
continue; // Invalid trigger, skip
|
|
172
|
+
configuredMainKey = keyInput;
|
|
173
|
+
ctrlKeyConfig = false;
|
|
174
|
+
altKeyConfig = false;
|
|
175
|
+
shiftKeyConfig = false;
|
|
176
|
+
metaKeyConfig = false;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
if (!keyInput.key || keyInput.key === "")
|
|
180
|
+
continue; // Invalid trigger, skip
|
|
181
|
+
configuredMainKey = keyInput.key;
|
|
182
|
+
ctrlKeyConfig = keyInput.ctrlKey;
|
|
183
|
+
altKeyConfig = keyInput.altKey;
|
|
184
|
+
shiftKeyConfig = keyInput.shiftKey;
|
|
185
|
+
metaKeyConfig = keyInput.metaKey;
|
|
186
|
+
}
|
|
187
|
+
const keyMatch = compareKey(event.key, configuredMainKey);
|
|
188
|
+
if (!keyMatch)
|
|
189
|
+
continue;
|
|
190
|
+
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
191
|
+
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
192
|
+
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
193
|
+
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
194
|
+
if (ctrlMatch && altMatch && shiftMatch && metaMatch) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
136
199
|
}
|
|
137
|
-
filterByContext(source$, context) {
|
|
138
|
-
return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) =>
|
|
200
|
+
filterByContext(source$, context, strict) {
|
|
201
|
+
return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => {
|
|
202
|
+
if (context == null) {
|
|
203
|
+
if (strict) {
|
|
204
|
+
return activeCtx == null;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
return context === activeCtx;
|
|
212
|
+
}
|
|
213
|
+
}), map(([event, /* _activeCtx */]) => event));
|
|
139
214
|
}
|
|
140
|
-
_registerShortcut(config, subscription, type,
|
|
141
|
-
detailsForLog) {
|
|
215
|
+
_registerShortcut(config, subscription, type, detailsForLog) {
|
|
142
216
|
const existingShortcut = this.activeShortcuts.get(config.id);
|
|
143
217
|
if (existingShortcut) {
|
|
144
218
|
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
|
|
@@ -163,11 +237,18 @@ export class Hotkeys {
|
|
|
163
237
|
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key (shorthand) in shortcut "${shortcutId}". Key string must not be empty.`);
|
|
164
238
|
return null;
|
|
165
239
|
}
|
|
166
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
configuredMainKey: keyInput,
|
|
242
|
+
ctrlKeyConfig: false,
|
|
243
|
+
altKeyConfig: false,
|
|
244
|
+
shiftKeyConfig: false,
|
|
245
|
+
metaKeyConfig: false,
|
|
246
|
+
logDetails: `key: "${keyInput}" (no mods)`,
|
|
247
|
+
};
|
|
167
248
|
}
|
|
168
249
|
else {
|
|
169
|
-
if (!keyInput.key || keyInput.key === "") {
|
|
170
|
-
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "key" property in shortcut "${shortcutId}". Key must be a non-empty value from Keys.`);
|
|
250
|
+
if (!keyInput.key || typeof keyInput.key !== "string" || keyInput.key === "") {
|
|
251
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "key" property in shortcut "${shortcutId}". Key must be a non-empty string value from Keys.`);
|
|
171
252
|
return null;
|
|
172
253
|
}
|
|
173
254
|
const logDetails = `key: "${keyInput.key}"` +
|
|
@@ -175,7 +256,14 @@ export class Hotkeys {
|
|
|
175
256
|
(keyInput.altKey !== undefined ? `, alt: ${keyInput.altKey}` : "") +
|
|
176
257
|
(keyInput.shiftKey !== undefined ? `, shift: ${keyInput.shiftKey}` : "") +
|
|
177
258
|
(keyInput.metaKey !== undefined ? `, meta: ${keyInput.metaKey}` : "");
|
|
178
|
-
return {
|
|
259
|
+
return {
|
|
260
|
+
configuredMainKey: keyInput.key,
|
|
261
|
+
ctrlKeyConfig: keyInput.ctrlKey,
|
|
262
|
+
altKeyConfig: keyInput.altKey,
|
|
263
|
+
shiftKeyConfig: keyInput.shiftKey,
|
|
264
|
+
metaKeyConfig: keyInput.metaKey,
|
|
265
|
+
logDetails,
|
|
266
|
+
};
|
|
179
267
|
}
|
|
180
268
|
}
|
|
181
269
|
/**
|
|
@@ -196,18 +284,19 @@ export class Hotkeys {
|
|
|
196
284
|
* callback: () => console.log("File saved!"),
|
|
197
285
|
* context: "editor"
|
|
198
286
|
* });
|
|
199
|
-
* // For just the Escape key
|
|
287
|
+
* // For just the Escape key, or Ctrl+Space
|
|
200
288
|
* keyManager.addCombination({
|
|
201
289
|
* id: "closeModal",
|
|
202
|
-
* keys: Keys.Escape,
|
|
290
|
+
* keys: [Keys.Escape, {key: Keys.Space, ctrlKey: true}],
|
|
203
291
|
* callback: () => console.log("Modal closed!")
|
|
204
292
|
* });
|
|
205
293
|
* ```
|
|
206
294
|
*/
|
|
207
295
|
addCombination(config) {
|
|
208
|
-
const { keys, callback, context, preventDefault = false, id } = config;
|
|
209
|
-
|
|
210
|
-
|
|
296
|
+
const { keys, callback, context, preventDefault = false, id, strict = false } = config;
|
|
297
|
+
if (context != null && strict) {
|
|
298
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context(${context}) and the 'strict' flag. The 'strict' flag will be ignored.`);
|
|
299
|
+
}
|
|
211
300
|
const keyTriggers = Array.isArray(keys) ? keys : [keys];
|
|
212
301
|
if (keyTriggers.length === 0) {
|
|
213
302
|
console.warn(`${Hotkeys.LOG_PREFIX} "keys" array for combination shortcut "${id}" is empty. Shortcut not added.`);
|
|
@@ -223,13 +312,36 @@ export class Hotkeys {
|
|
|
223
312
|
}
|
|
224
313
|
const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig, logDetails } = parsedTrigger;
|
|
225
314
|
logParts.push(`{ ${logDetails} }`);
|
|
226
|
-
const stream = this.filterByContext(this.keydown$, context).pipe(filter(event => {
|
|
315
|
+
const stream = this.filterByContext(this.keydown$, context, strict).pipe(filter(event => {
|
|
227
316
|
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
228
317
|
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
229
318
|
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
230
319
|
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
231
320
|
return ctrlMatch && altMatch && shiftMatch && metaMatch;
|
|
232
|
-
}), filter(event => compareKey(event.key, configuredMainKey))
|
|
321
|
+
}), filter(event => compareKey(event.key, configuredMainKey)),
|
|
322
|
+
// New filter for priority: Specific context > Global context
|
|
323
|
+
filter(event => {
|
|
324
|
+
if (context != null || strict) { // This shortcut is NOT global or strict
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
// This shortcut IS global. Check for specific overrides.
|
|
328
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
329
|
+
if (currentSpecificContext == null) { // No specific context active
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
333
|
+
if (otherAS.config.id !== id &&
|
|
334
|
+
'keys' in otherAS.config &&
|
|
335
|
+
otherAS.config.context === currentSpecificContext &&
|
|
336
|
+
this._shortcutMatchesEvent(otherAS.config, event)) {
|
|
337
|
+
if (this.debugMode) {
|
|
338
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
|
|
339
|
+
}
|
|
340
|
+
return false; // Suppress global
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return true; // Global can proceed
|
|
344
|
+
}));
|
|
233
345
|
observables.push(stream);
|
|
234
346
|
}
|
|
235
347
|
if (observables.length === 0) {
|
|
@@ -237,8 +349,8 @@ export class Hotkeys {
|
|
|
237
349
|
console.warn(`${Hotkeys.LOG_PREFIX} No valid key triggers for combination shortcut "${id}". Shortcut not added.`);
|
|
238
350
|
return undefined;
|
|
239
351
|
}
|
|
240
|
-
finalShortcut$ = merge(...observables);
|
|
241
|
-
overallLogDetails = Array.isArray(keys) ? `Triggers: [ ${logParts.join(", ")} ]` : logParts[0];
|
|
352
|
+
const finalShortcut$ = merge(...observables);
|
|
353
|
+
const overallLogDetails = Array.isArray(keys) ? `Triggers: [ ${logParts.join(", ")} ]` : logParts[0];
|
|
242
354
|
const subscription = finalShortcut$.pipe(tap(event => {
|
|
243
355
|
if (this.debugMode) {
|
|
244
356
|
const preventAction = preventDefault ? ", preventing default" : "";
|
|
@@ -280,7 +392,7 @@ export class Hotkeys {
|
|
|
280
392
|
* ```
|
|
281
393
|
*/
|
|
282
394
|
addSequence(config) {
|
|
283
|
-
const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs } = config;
|
|
395
|
+
const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs, strict = false } = config;
|
|
284
396
|
if (!Array.isArray(sequence) || sequence.length === 0) {
|
|
285
397
|
console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty or invalid. Shortcut not added.`);
|
|
286
398
|
return undefined;
|
|
@@ -290,10 +402,13 @@ export class Hotkeys {
|
|
|
290
402
|
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key in sequence for shortcut "${id}". All keys must be non-empty string values from Keys. Shortcut not added.`);
|
|
291
403
|
return undefined;
|
|
292
404
|
}
|
|
405
|
+
if (context && strict) {
|
|
406
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context and the 'strict' flag. The 'strict' flag will be ignored.`);
|
|
407
|
+
}
|
|
293
408
|
const configuredSequence = sequence;
|
|
294
409
|
const sequenceLength = configuredSequence.length;
|
|
295
410
|
let shortcut$;
|
|
296
|
-
const baseKeydownStream$ = this.filterByContext(this.keydown$, context);
|
|
411
|
+
const baseKeydownStream$ = this.filterByContext(this.keydown$, context, strict);
|
|
297
412
|
if (sequenceTimeoutMs && sequenceTimeoutMs > 0) {
|
|
298
413
|
shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
|
|
299
414
|
let { matchedEvents, lastEventTime } = acc;
|
|
@@ -320,7 +435,7 @@ export class Hotkeys {
|
|
|
320
435
|
if (compareKey(event.key, configuredSequence[nextExpectedKeyIndex])) {
|
|
321
436
|
const newMatchedEvents = [...matchedEvents, event];
|
|
322
437
|
if (newMatchedEvents.length === sequenceLength) {
|
|
323
|
-
if (this.debugMode &&
|
|
438
|
+
if (this.debugMode && acc.emitState !== EmitStates.Emit)
|
|
324
439
|
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) matched.`);
|
|
325
440
|
return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.Emit };
|
|
326
441
|
}
|
|
@@ -350,7 +465,28 @@ export class Hotkeys {
|
|
|
350
465
|
return events.every((event, index) => compareKey(event.key, configuredSequence[index]));
|
|
351
466
|
}));
|
|
352
467
|
}
|
|
353
|
-
const
|
|
468
|
+
const finalShortcutWithPriority$ = shortcut$.pipe(filter((completedEvents) => {
|
|
469
|
+
if (context != null || strict) { // This sequence is NOT global or strict
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
// This sequence IS global. Check for specific overrides.
|
|
473
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
474
|
+
if (currentSpecificContext == null) { // No specific context active
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
478
|
+
if (otherAS.config.id !== id &&
|
|
479
|
+
"sequence" in otherAS.config &&
|
|
480
|
+
otherAS.config.context === currentSpecificContext &&
|
|
481
|
+
this._areSequencesIdentical(sequence, otherAS.config.sequence)) {
|
|
482
|
+
if (this.debugMode) {
|
|
483
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${id}" suppressed by identical specific-context shortcut "${otherAS.config.id}".`);
|
|
484
|
+
}
|
|
485
|
+
return false; // Suppress global
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return true; // Global sequence can proceed
|
|
489
|
+
}), tap((events) => {
|
|
354
490
|
if (this.debugMode) {
|
|
355
491
|
const timeoutInfo = (sequenceTimeoutMs && sequenceTimeoutMs > 0) ? ` (with timeout logic)` : ` (no timeout logic)`;
|
|
356
492
|
const preventAction = preventDefault ? ", preventing default for last event" : "";
|
|
@@ -363,7 +499,7 @@ export class Hotkeys {
|
|
|
363
499
|
console.error(`${Hotkeys.LOG_PREFIX} Error in sequence stream for shortcut "${id}":`, err);
|
|
364
500
|
return EMPTY;
|
|
365
501
|
}));
|
|
366
|
-
const subscription =
|
|
502
|
+
const subscription = finalShortcutWithPriority$.subscribe((events) => {
|
|
367
503
|
try {
|
|
368
504
|
// Ensure callback receives the last event of the sequence, similar to combination.
|
|
369
505
|
if (events.length > 0)
|
|
@@ -374,7 +510,7 @@ export class Hotkeys {
|
|
|
374
510
|
}
|
|
375
511
|
});
|
|
376
512
|
const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ""}`;
|
|
377
|
-
return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails);
|
|
513
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails);
|
|
378
514
|
}
|
|
379
515
|
/**
|
|
380
516
|
* Removes a registered shortcut by its ID.
|
|
@@ -400,7 +536,7 @@ export class Hotkeys {
|
|
|
400
536
|
* This can be useful for displaying available shortcuts to the user or for debugging.
|
|
401
537
|
* @returns An array of objects, where each object represents an active shortcut
|
|
402
538
|
* and includes its `id`, `description` (if provided), `context` (if any),
|
|
403
|
-
* and `type` (
|
|
539
|
+
* and `type` (from `ShortcutTypes` enum).
|
|
404
540
|
*/
|
|
405
541
|
getActiveShortcuts() {
|
|
406
542
|
const shortcuts = [];
|
|
@@ -409,7 +545,7 @@ export class Hotkeys {
|
|
|
409
545
|
id,
|
|
410
546
|
description: activeShortcut.config.description,
|
|
411
547
|
context: activeShortcut.config.context,
|
|
412
|
-
type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination
|
|
548
|
+
type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination
|
|
413
549
|
});
|
|
414
550
|
}
|
|
415
551
|
return shortcuts;
|
|
@@ -425,7 +561,7 @@ export class Hotkeys {
|
|
|
425
561
|
console.log(`${Hotkeys.LOG_PREFIX} Destroying library instance and unsubscribing all shortcuts.`);
|
|
426
562
|
this.activeShortcuts.forEach(shortcut => shortcut.subscription.unsubscribe());
|
|
427
563
|
this.activeShortcuts.clear();
|
|
428
|
-
this.activeContext$.complete();
|
|
564
|
+
this.activeContext$.complete();
|
|
429
565
|
if (this.debugMode)
|
|
430
566
|
console.log(`${Hotkeys.LOG_PREFIX} Library destroyed.`);
|
|
431
567
|
}
|