rx-hotkeys 2.4.1 → 2.5.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/hotkeys.d.ts +30 -11
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +154 -34
- package/dist/hotkeys.test.js +79 -1
- package/package.json +1 -1
package/dist/hotkeys.d.ts
CHANGED
|
@@ -34,15 +34,19 @@ type KeyCombinationTrigger = {
|
|
|
34
34
|
} | StandardKey;
|
|
35
35
|
export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
36
36
|
/**
|
|
37
|
-
* Defines the key or key combination.
|
|
38
|
-
* Can be
|
|
37
|
+
* Defines the key or key combination(s) that trigger the shortcut.
|
|
38
|
+
* Can be a single trigger or an array of triggers.
|
|
39
|
+
* Each trigger can be an object specifying the main `key` (from `StandardKey`) and optional
|
|
39
40
|
* modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
|
|
40
41
|
* Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
|
|
41
42
|
*
|
|
42
|
-
* Alternatively, for a simple key press without any modifiers,
|
|
43
|
+
* Alternatively, for a simple key press without any modifiers, a trigger can be
|
|
43
44
|
* a `StandardKey` directly.
|
|
44
45
|
* Example: `Keys.Escape` for the Escape key. When using this shorthand,
|
|
45
46
|
* it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
|
|
47
|
+
*
|
|
48
|
+
* To define multiple triggers for the same action:
|
|
49
|
+
* Example: `keys: [Keys.Enter, { key: Keys.Space, ctrlKey: true }]`
|
|
46
50
|
*/
|
|
47
51
|
keys: KeyCombinationTrigger | KeyCombinationTrigger[];
|
|
48
52
|
}
|
|
@@ -95,7 +99,7 @@ export declare class Hotkeys {
|
|
|
95
99
|
* will be active and can be triggered.
|
|
96
100
|
* @param contextName - The name of the context (e.g., "modal", "editor", "global").
|
|
97
101
|
* Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
|
|
98
|
-
* @returns
|
|
102
|
+
* @returns `true` if the context was changed, `false` if the new context was the same as the current one.
|
|
99
103
|
*/
|
|
100
104
|
setContext(contextName: string | null): boolean;
|
|
101
105
|
/**
|
|
@@ -109,6 +113,12 @@ export declare class Hotkeys {
|
|
|
109
113
|
* @param enable - True to enable debug logs, false to disable.
|
|
110
114
|
*/
|
|
111
115
|
setDebugMode(enable: boolean): void;
|
|
116
|
+
/**
|
|
117
|
+
* Checks if a shortcut with the given ID is currently registered and active.
|
|
118
|
+
* @param id - The unique ID of the shortcut to check.
|
|
119
|
+
* @returns True if a shortcut with the specified ID exists, false otherwise.
|
|
120
|
+
*/
|
|
121
|
+
hasShortcut(id: string): boolean;
|
|
112
122
|
/**
|
|
113
123
|
* An Observable that emits the new context name (or null) whenever the active context changes.
|
|
114
124
|
* This allows external parts of the application to react to context transitions.
|
|
@@ -129,11 +139,20 @@ export declare class Hotkeys {
|
|
|
129
139
|
*/
|
|
130
140
|
get onContextChange$(): Observable<string | null>;
|
|
131
141
|
/**
|
|
132
|
-
*
|
|
133
|
-
* @param
|
|
134
|
-
* @
|
|
142
|
+
* Compares two sequences of StandardKey arrays to see if they are identical.
|
|
143
|
+
* @param seq1 - The first sequence array.
|
|
144
|
+
* @param seq2 - The second sequence array.
|
|
145
|
+
* @returns True if the sequences are identical, false otherwise.
|
|
135
146
|
*/
|
|
136
|
-
|
|
147
|
+
private _areSequencesIdentical;
|
|
148
|
+
/**
|
|
149
|
+
* Checks if a given KeyCombinationConfig matches a given KeyboardEvent.
|
|
150
|
+
* This is used internally for priority checking.
|
|
151
|
+
* @param shortcutConfig The KeyCombinationConfig to check.
|
|
152
|
+
* @param event The KeyboardEvent to match against.
|
|
153
|
+
* @returns True if the shortcutConfig matches the event, false otherwise.
|
|
154
|
+
*/
|
|
155
|
+
private _shortcutMatchesEvent;
|
|
137
156
|
private filterByContext;
|
|
138
157
|
private _registerShortcut;
|
|
139
158
|
/**
|
|
@@ -162,10 +181,10 @@ export declare class Hotkeys {
|
|
|
162
181
|
* callback: () => console.log("File saved!"),
|
|
163
182
|
* context: "editor"
|
|
164
183
|
* });
|
|
165
|
-
* // For just the Escape key
|
|
184
|
+
* // For just the Escape key, or Ctrl+Space
|
|
166
185
|
* keyManager.addCombination({
|
|
167
186
|
* id: "closeModal",
|
|
168
|
-
* keys: Keys.Escape,
|
|
187
|
+
* keys: [Keys.Escape, {key: Keys.Space, ctrlKey: true}],
|
|
169
188
|
* callback: () => console.log("Modal closed!")
|
|
170
189
|
* });
|
|
171
190
|
* ```
|
|
@@ -205,7 +224,7 @@ export declare class Hotkeys {
|
|
|
205
224
|
* This can be useful for displaying available shortcuts to the user or for debugging.
|
|
206
225
|
* @returns An array of objects, where each object represents an active shortcut
|
|
207
226
|
* and includes its `id`, `description` (if provided), `context` (if any),
|
|
208
|
-
* and `type` (
|
|
227
|
+
* and `type` (from `ShortcutTypes` enum).
|
|
209
228
|
*/
|
|
210
229
|
getActiveShortcuts(): {
|
|
211
230
|
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,72 @@ 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
200
|
filterByContext(source$, context) {
|
|
138
201
|
return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => context == null || context === activeCtx), map(([event, /* _activeCtx */]) => event));
|
|
139
202
|
}
|
|
140
|
-
_registerShortcut(config, subscription, type,
|
|
141
|
-
detailsForLog) {
|
|
203
|
+
_registerShortcut(config, subscription, type, detailsForLog) {
|
|
142
204
|
const existingShortcut = this.activeShortcuts.get(config.id);
|
|
143
205
|
if (existingShortcut) {
|
|
144
206
|
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
|
|
@@ -163,11 +225,18 @@ export class Hotkeys {
|
|
|
163
225
|
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key (shorthand) in shortcut "${shortcutId}". Key string must not be empty.`);
|
|
164
226
|
return null;
|
|
165
227
|
}
|
|
166
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
configuredMainKey: keyInput,
|
|
230
|
+
ctrlKeyConfig: false,
|
|
231
|
+
altKeyConfig: false,
|
|
232
|
+
shiftKeyConfig: false,
|
|
233
|
+
metaKeyConfig: false,
|
|
234
|
+
logDetails: `key: "${keyInput}" (no mods)`,
|
|
235
|
+
};
|
|
167
236
|
}
|
|
168
237
|
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.`);
|
|
238
|
+
if (!keyInput.key || typeof keyInput.key !== "string" || keyInput.key === "") {
|
|
239
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "key" property in shortcut "${shortcutId}". Key must be a non-empty string value from Keys.`);
|
|
171
240
|
return null;
|
|
172
241
|
}
|
|
173
242
|
const logDetails = `key: "${keyInput.key}"` +
|
|
@@ -175,7 +244,14 @@ export class Hotkeys {
|
|
|
175
244
|
(keyInput.altKey !== undefined ? `, alt: ${keyInput.altKey}` : "") +
|
|
176
245
|
(keyInput.shiftKey !== undefined ? `, shift: ${keyInput.shiftKey}` : "") +
|
|
177
246
|
(keyInput.metaKey !== undefined ? `, meta: ${keyInput.metaKey}` : "");
|
|
178
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
configuredMainKey: keyInput.key,
|
|
249
|
+
ctrlKeyConfig: keyInput.ctrlKey,
|
|
250
|
+
altKeyConfig: keyInput.altKey,
|
|
251
|
+
shiftKeyConfig: keyInput.shiftKey,
|
|
252
|
+
metaKeyConfig: keyInput.metaKey,
|
|
253
|
+
logDetails,
|
|
254
|
+
};
|
|
179
255
|
}
|
|
180
256
|
}
|
|
181
257
|
/**
|
|
@@ -196,10 +272,10 @@ export class Hotkeys {
|
|
|
196
272
|
* callback: () => console.log("File saved!"),
|
|
197
273
|
* context: "editor"
|
|
198
274
|
* });
|
|
199
|
-
* // For just the Escape key
|
|
275
|
+
* // For just the Escape key, or Ctrl+Space
|
|
200
276
|
* keyManager.addCombination({
|
|
201
277
|
* id: "closeModal",
|
|
202
|
-
* keys: Keys.Escape,
|
|
278
|
+
* keys: [Keys.Escape, {key: Keys.Space, ctrlKey: true}],
|
|
203
279
|
* callback: () => console.log("Modal closed!")
|
|
204
280
|
* });
|
|
205
281
|
* ```
|
|
@@ -229,7 +305,30 @@ export class Hotkeys {
|
|
|
229
305
|
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
230
306
|
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
231
307
|
return ctrlMatch && altMatch && shiftMatch && metaMatch;
|
|
232
|
-
}), filter(event => compareKey(event.key, configuredMainKey))
|
|
308
|
+
}), filter(event => compareKey(event.key, configuredMainKey)),
|
|
309
|
+
// New filter for priority: Specific context > Global context
|
|
310
|
+
filter(event => {
|
|
311
|
+
if (config.context != null) { // This shortcut is NOT global
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
// This shortcut IS global. Check for specific overrides.
|
|
315
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
316
|
+
if (currentSpecificContext == null) { // No specific context active
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
320
|
+
if (otherAS.config.id !== config.id &&
|
|
321
|
+
'keys' in otherAS.config &&
|
|
322
|
+
otherAS.config.context === currentSpecificContext &&
|
|
323
|
+
this._shortcutMatchesEvent(otherAS.config, event)) {
|
|
324
|
+
if (this.debugMode) {
|
|
325
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${config.id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
|
|
326
|
+
}
|
|
327
|
+
return false; // Suppress global
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return true; // Global can proceed
|
|
331
|
+
}));
|
|
233
332
|
observables.push(stream);
|
|
234
333
|
}
|
|
235
334
|
if (observables.length === 0) {
|
|
@@ -320,7 +419,7 @@ export class Hotkeys {
|
|
|
320
419
|
if (compareKey(event.key, configuredSequence[nextExpectedKeyIndex])) {
|
|
321
420
|
const newMatchedEvents = [...matchedEvents, event];
|
|
322
421
|
if (newMatchedEvents.length === sequenceLength) {
|
|
323
|
-
if (this.debugMode &&
|
|
422
|
+
if (this.debugMode && acc.emitState !== EmitStates.Emit)
|
|
324
423
|
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) matched.`);
|
|
325
424
|
return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.Emit };
|
|
326
425
|
}
|
|
@@ -350,7 +449,28 @@ export class Hotkeys {
|
|
|
350
449
|
return events.every((event, index) => compareKey(event.key, configuredSequence[index]));
|
|
351
450
|
}));
|
|
352
451
|
}
|
|
353
|
-
const
|
|
452
|
+
const finalShortcutWithPriority$ = shortcut$.pipe(filter((completedEvents) => {
|
|
453
|
+
if (config.context != null) { // This sequence is NOT global
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
// This sequence IS global. Check for specific overrides.
|
|
457
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
458
|
+
if (currentSpecificContext == null) { // No specific context active
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
462
|
+
if (otherAS.config.id !== config.id &&
|
|
463
|
+
"sequence" in otherAS.config &&
|
|
464
|
+
otherAS.config.context === currentSpecificContext &&
|
|
465
|
+
this._areSequencesIdentical(config.sequence, otherAS.config.sequence)) {
|
|
466
|
+
if (this.debugMode) {
|
|
467
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${config.id}" suppressed by identical specific-context shortcut "${otherAS.config.id}".`);
|
|
468
|
+
}
|
|
469
|
+
return false; // Suppress global
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return true; // Global sequence can proceed
|
|
473
|
+
}), tap((events) => {
|
|
354
474
|
if (this.debugMode) {
|
|
355
475
|
const timeoutInfo = (sequenceTimeoutMs && sequenceTimeoutMs > 0) ? ` (with timeout logic)` : ` (no timeout logic)`;
|
|
356
476
|
const preventAction = preventDefault ? ", preventing default for last event" : "";
|
|
@@ -363,7 +483,7 @@ export class Hotkeys {
|
|
|
363
483
|
console.error(`${Hotkeys.LOG_PREFIX} Error in sequence stream for shortcut "${id}":`, err);
|
|
364
484
|
return EMPTY;
|
|
365
485
|
}));
|
|
366
|
-
const subscription =
|
|
486
|
+
const subscription = finalShortcutWithPriority$.subscribe((events) => {
|
|
367
487
|
try {
|
|
368
488
|
// Ensure callback receives the last event of the sequence, similar to combination.
|
|
369
489
|
if (events.length > 0)
|
|
@@ -374,7 +494,7 @@ export class Hotkeys {
|
|
|
374
494
|
}
|
|
375
495
|
});
|
|
376
496
|
const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ""}`;
|
|
377
|
-
return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails);
|
|
497
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails);
|
|
378
498
|
}
|
|
379
499
|
/**
|
|
380
500
|
* Removes a registered shortcut by its ID.
|
|
@@ -400,7 +520,7 @@ export class Hotkeys {
|
|
|
400
520
|
* This can be useful for displaying available shortcuts to the user or for debugging.
|
|
401
521
|
* @returns An array of objects, where each object represents an active shortcut
|
|
402
522
|
* and includes its `id`, `description` (if provided), `context` (if any),
|
|
403
|
-
* and `type` (
|
|
523
|
+
* and `type` (from `ShortcutTypes` enum).
|
|
404
524
|
*/
|
|
405
525
|
getActiveShortcuts() {
|
|
406
526
|
const shortcuts = [];
|
|
@@ -409,7 +529,7 @@ export class Hotkeys {
|
|
|
409
529
|
id,
|
|
410
530
|
description: activeShortcut.config.description,
|
|
411
531
|
context: activeShortcut.config.context,
|
|
412
|
-
type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination
|
|
532
|
+
type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination
|
|
413
533
|
});
|
|
414
534
|
}
|
|
415
535
|
return shortcuts;
|
|
@@ -425,7 +545,7 @@ export class Hotkeys {
|
|
|
425
545
|
console.log(`${Hotkeys.LOG_PREFIX} Destroying library instance and unsubscribing all shortcuts.`);
|
|
426
546
|
this.activeShortcuts.forEach(shortcut => shortcut.subscription.unsubscribe());
|
|
427
547
|
this.activeShortcuts.clear();
|
|
428
|
-
this.activeContext$.complete();
|
|
548
|
+
this.activeContext$.complete();
|
|
429
549
|
if (this.debugMode)
|
|
430
550
|
console.log(`${Hotkeys.LOG_PREFIX} Library destroyed.`);
|
|
431
551
|
}
|
package/dist/hotkeys.test.js
CHANGED
|
@@ -257,7 +257,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
257
257
|
const result = keyManager.addCombination(config);
|
|
258
258
|
assert.strictEqual(result, undefined, "Should return undefined for null key");
|
|
259
259
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
260
|
-
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "key" property in shortcut "nullKey". Key must be a non-empty value from Keys.`));
|
|
260
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "key" property in shortcut "nullKey". Key must be a non-empty string value from Keys.`));
|
|
261
261
|
});
|
|
262
262
|
it("should pass the KeyboardEvent to the callback", () => {
|
|
263
263
|
const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
|
|
@@ -442,6 +442,84 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
442
442
|
});
|
|
443
443
|
});
|
|
444
444
|
});
|
|
445
|
+
describe("Context Priority (Specific > Global)", () => {
|
|
446
|
+
let globalCallback;
|
|
447
|
+
let specificCallback;
|
|
448
|
+
beforeEach(() => {
|
|
449
|
+
globalCallback = createMockFn();
|
|
450
|
+
specificCallback = createMockFn();
|
|
451
|
+
// Global shortcut: Ctrl+G
|
|
452
|
+
keyManager.addCombination({
|
|
453
|
+
id: "globalCtrlG",
|
|
454
|
+
keys: { key: Keys.G, ctrlKey: true },
|
|
455
|
+
callback: globalCallback,
|
|
456
|
+
context: null // Explicitly global
|
|
457
|
+
});
|
|
458
|
+
// Specific context shortcut: Ctrl+G in "editor" context
|
|
459
|
+
keyManager.addCombination({
|
|
460
|
+
id: "editorCtrlG",
|
|
461
|
+
keys: { key: Keys.G, ctrlKey: true },
|
|
462
|
+
callback: specificCallback,
|
|
463
|
+
context: "editor"
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
it("should only trigger specific context callback when specific context is active", () => {
|
|
467
|
+
keyManager.setContext("editor");
|
|
468
|
+
dispatchKeyEvent(Keys.G, { ctrlKey: true });
|
|
469
|
+
assert.strictEqual(specificCallback.calledCount, 1, "Specific callback should have been called");
|
|
470
|
+
assert.strictEqual(globalCallback.calledCount, 0, "Global callback should NOT have been called");
|
|
471
|
+
});
|
|
472
|
+
it("should only trigger global callback when no specific context is active (or context doesn't match)", () => {
|
|
473
|
+
keyManager.setContext(null); // No specific context
|
|
474
|
+
dispatchKeyEvent(Keys.G, { ctrlKey: true });
|
|
475
|
+
assert.strictEqual(specificCallback.calledCount, 0, "Specific callback should NOT have been called");
|
|
476
|
+
assert.strictEqual(globalCallback.calledCount, 1, "Global callback should have been called");
|
|
477
|
+
globalCallback.mockClear();
|
|
478
|
+
keyManager.setContext("anotherContext"); // Different specific context
|
|
479
|
+
dispatchKeyEvent(Keys.G, { ctrlKey: true });
|
|
480
|
+
assert.strictEqual(specificCallback.calledCount, 0, `Specific callback should NOT have been called for "anotherContext"`);
|
|
481
|
+
assert.strictEqual(globalCallback.calledCount, 1, `Global callback should have been called when in "anotherContext"`);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
describe("Sequence Context Priority (Specific > Global)", () => {
|
|
485
|
+
let globalSeqCallback;
|
|
486
|
+
let specificSeqCallback;
|
|
487
|
+
const testSequence = [Keys.G, Keys.I];
|
|
488
|
+
beforeEach(() => {
|
|
489
|
+
globalSeqCallback = createMockFn();
|
|
490
|
+
specificSeqCallback = createMockFn();
|
|
491
|
+
keyManager.addSequence({
|
|
492
|
+
id: "globalGI",
|
|
493
|
+
sequence: testSequence,
|
|
494
|
+
callback: globalSeqCallback,
|
|
495
|
+
context: null // Global
|
|
496
|
+
});
|
|
497
|
+
keyManager.addSequence({
|
|
498
|
+
id: "editorGI",
|
|
499
|
+
sequence: testSequence,
|
|
500
|
+
callback: specificSeqCallback,
|
|
501
|
+
context: "editor" // Specific
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
it("should only trigger specific context sequence callback when specific context is active", () => {
|
|
505
|
+
keyManager.setContext("editor");
|
|
506
|
+
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
507
|
+
assert.strictEqual(specificSeqCallback.calledCount, 1, "Specific sequence callback should have been called");
|
|
508
|
+
assert.strictEqual(globalSeqCallback.calledCount, 0, "Global sequence callback should NOT have been called");
|
|
509
|
+
});
|
|
510
|
+
it("should only trigger global sequence callback when no specific context is active (or context doesn't match)", () => {
|
|
511
|
+
keyManager.setContext(null); // No specific context
|
|
512
|
+
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
513
|
+
assert.strictEqual(specificSeqCallback.calledCount, 0, "Specific sequence callback should NOT have been called");
|
|
514
|
+
assert.strictEqual(globalSeqCallback.calledCount, 1, "Global sequence callback should have been called");
|
|
515
|
+
globalSeqCallback.mockClear();
|
|
516
|
+
specificSeqCallback.mockClear(); // Clear for next part of test
|
|
517
|
+
keyManager.setContext("anotherContext"); // Different specific context
|
|
518
|
+
testSequence.forEach(key => dispatchKeyEvent(key));
|
|
519
|
+
assert.strictEqual(specificSeqCallback.calledCount, 0, `Specific sequence callback should NOT have been called for "anotherContext"`);
|
|
520
|
+
assert.strictEqual(globalSeqCallback.calledCount, 1, `Global sequence callback should have been called when in "anotherContext"`);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
445
523
|
describe("addSequence", () => {
|
|
446
524
|
it("should trigger callback for a simple key sequence", () => {
|
|
447
525
|
const config = { id: "seqGI", sequence: [Keys.G, Keys.I], callback: mockCallback };
|