rx-hotkeys 2.2.1 → 2.4.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 +2 -2
- package/dist/hotkeys.d.ts +40 -20
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +68 -55
- package/dist/hotkeys.test.js +110 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ Registers a key sequence shortcut.
|
|
|
128
128
|
* `config`: The KeySequenceConfig object.
|
|
129
129
|
* Returns the shortcut ID if successful, undefined otherwise.
|
|
130
130
|
|
|
131
|
-
`setContext(contextName: string | null):
|
|
131
|
+
`setContext(contextName: string | null): boolean`
|
|
132
132
|
|
|
133
133
|
Sets the active context. Only shortcuts matching this context or global shortcuts (no context) will trigger.
|
|
134
134
|
|
|
@@ -165,7 +165,7 @@ Cleans up all subscriptions and resources. Essential to call to prevent memory l
|
|
|
165
165
|
`KeyCombinationConfig`
|
|
166
166
|
|
|
167
167
|
* `id: string` (required): Unique identifier for the shortcut.
|
|
168
|
-
* `keys: { key: StandardKey; ctrlKey?: boolean; altKey?: boolean; shiftKey?: boolean; metaKey?: boolean; } | StandardKey
|
|
168
|
+
* `keys: { key: StandardKey; ctrlKey?: boolean; altKey?: boolean; shiftKey?: boolean; metaKey?: boolean; } | StandardKey | Array<{ key: StandardKey; ctrlKey?: boolean; altKey?: boolean; shiftKey?: boolean; metaKey?: boolean; } | StandardKey>` (required): Defines the main key (from Keys) and optional modifier keys.
|
|
169
169
|
* `callback: (event?: KeyboardEvent) => void` (required): Function to execute when the shortcut is triggered. The triggering `KeyboardEvent` is passed as an argument.
|
|
170
170
|
* `context?: string | null`: Specifies the context in which this shortcut is active. If `null` or `undefined`, it's a global shortcut.
|
|
171
171
|
* `preventDefault?: boolean`: If true, `event.preventDefault()` will be called when the shortcut triggers. Defaults to `false`.
|
package/dist/hotkeys.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Observable } from "rxjs";
|
|
2
|
-
import { StandardKey } from "./keys.js";
|
|
1
|
+
import { Subscription, Observable } from "rxjs";
|
|
2
|
+
import { type StandardKey } from "./keys.js";
|
|
3
3
|
export declare enum ShortcutTypes {
|
|
4
4
|
Combination = "combination",
|
|
5
5
|
Sequence = "sequence"
|
|
@@ -11,6 +11,27 @@ interface ShortcutConfigBase {
|
|
|
11
11
|
preventDefault?: boolean;
|
|
12
12
|
description?: string;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Defines a single key trigger, which can be a StandardKey (for simple presses like "Escape")
|
|
16
|
+
* or an object specifying the main key and its modifiers (e.g., { key: Keys.S, ctrlKey: true }).
|
|
17
|
+
*/
|
|
18
|
+
type KeyCombinationTrigger = {
|
|
19
|
+
/**
|
|
20
|
+
* The main key for the combination.
|
|
21
|
+
* This MUST be a value from the exported `Keys` object
|
|
22
|
+
* (e.g., `Keys.A`, `Keys.Enter`, `Keys.Escape`).
|
|
23
|
+
* The library handles case-insensitivity for single character keys (like A-Z, 0-9)
|
|
24
|
+
* automatically when comparing with the actual browser event's `event.key`.
|
|
25
|
+
* For special, multi-character keys (e.g. "ArrowUp", "Escape"), the value from
|
|
26
|
+
* `Keys` ensures the correct case-sensitive string is used.
|
|
27
|
+
* Refer to: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
28
|
+
*/
|
|
29
|
+
key: StandardKey;
|
|
30
|
+
ctrlKey?: boolean;
|
|
31
|
+
altKey?: boolean;
|
|
32
|
+
shiftKey?: boolean;
|
|
33
|
+
metaKey?: boolean;
|
|
34
|
+
} | StandardKey;
|
|
14
35
|
export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
15
36
|
/**
|
|
16
37
|
* Defines the key or key combination.
|
|
@@ -23,23 +44,7 @@ export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
|
23
44
|
* Example: `Keys.Escape` for the Escape key. When using this shorthand,
|
|
24
45
|
* it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
|
|
25
46
|
*/
|
|
26
|
-
keys:
|
|
27
|
-
/**
|
|
28
|
-
* The main key for the combination.
|
|
29
|
-
* This MUST be a value from the exported `Keys` object
|
|
30
|
-
* (e.g., `Keys.A`, `Keys.Enter`, `Keys.Escape`).
|
|
31
|
-
* The library handles case-insensitivity for single character keys (like A-Z, 0-9)
|
|
32
|
-
* automatically when comparing with the actual browser event's `event.key`.
|
|
33
|
-
* For special, multi-character keys (e.g. "ArrowUp", "Escape"), the value from
|
|
34
|
-
* `Keys` ensures the correct case-sensitive string is used.
|
|
35
|
-
* Refer to: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
36
|
-
*/
|
|
37
|
-
key: StandardKey;
|
|
38
|
-
ctrlKey?: boolean;
|
|
39
|
-
altKey?: boolean;
|
|
40
|
-
shiftKey?: boolean;
|
|
41
|
-
metaKey?: boolean;
|
|
42
|
-
} | StandardKey;
|
|
47
|
+
keys: KeyCombinationTrigger | KeyCombinationTrigger[];
|
|
43
48
|
}
|
|
44
49
|
export interface KeySequenceConfig extends ShortcutConfigBase {
|
|
45
50
|
/**
|
|
@@ -59,6 +64,12 @@ export interface KeySequenceConfig extends ShortcutConfigBase {
|
|
|
59
64
|
*/
|
|
60
65
|
sequenceTimeoutMs?: number;
|
|
61
66
|
}
|
|
67
|
+
type ShortcutConfig = KeyCombinationConfig | KeySequenceConfig;
|
|
68
|
+
export interface ActiveShortcut {
|
|
69
|
+
id: string;
|
|
70
|
+
config: ShortcutConfig;
|
|
71
|
+
subscription: Subscription;
|
|
72
|
+
}
|
|
62
73
|
/**
|
|
63
74
|
* Manages keyboard shortcuts for web applications.
|
|
64
75
|
* Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
|
|
@@ -84,8 +95,9 @@ export declare class Hotkeys {
|
|
|
84
95
|
* will be active and can be triggered.
|
|
85
96
|
* @param contextName - The name of the context (e.g., "modal", "editor", "global").
|
|
86
97
|
* Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
|
|
98
|
+
* @returns boolean
|
|
87
99
|
*/
|
|
88
|
-
setContext(contextName: string | null):
|
|
100
|
+
setContext(contextName: string | null): boolean;
|
|
89
101
|
/**
|
|
90
102
|
* Gets the current active context.
|
|
91
103
|
* @returns The current context name as a string, or `null` if no context is set.
|
|
@@ -124,6 +136,14 @@ export declare class Hotkeys {
|
|
|
124
136
|
hasShortcut(id: string): boolean;
|
|
125
137
|
private filterByContext;
|
|
126
138
|
private _registerShortcut;
|
|
139
|
+
/**
|
|
140
|
+
* Parses a single key trigger definition (either shorthand StandardKey or an object with modifiers)
|
|
141
|
+
* into its constituent parts: main key and modifier states.
|
|
142
|
+
* @param keyInput - The KeyCombinationTrigger to parse.
|
|
143
|
+
* @param shortcutId - The ID of the shortcut this key trigger belongs to (for logging).
|
|
144
|
+
* @returns An object containing configuredMainKey and modifier states, or null if parsing fails.
|
|
145
|
+
*/
|
|
146
|
+
private _parseKeyTrigger;
|
|
127
147
|
/**
|
|
128
148
|
* Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
|
|
129
149
|
* The callback is triggered when the specified key and modifier keys (if any) are pressed.
|
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,
|
|
1
|
+
{"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"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"}
|
package/dist/hotkeys.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fromEvent, BehaviorSubject, EMPTY, filter, map, bufferCount, withLatestFrom, tap, catchError, scan, } from "rxjs";
|
|
1
|
+
import { fromEvent, BehaviorSubject, EMPTY, filter, map, bufferCount, withLatestFrom, tap, catchError, scan, merge, } from "rxjs";
|
|
2
2
|
// --- Enums, Interfaces and Types ---
|
|
3
3
|
export var ShortcutTypes;
|
|
4
4
|
(function (ShortcutTypes) {
|
|
@@ -63,21 +63,23 @@ 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 boolean
|
|
66
67
|
*/
|
|
67
68
|
setContext(contextName) {
|
|
68
|
-
|
|
69
|
+
const currentContext = this.activeContext$.getValue();
|
|
70
|
+
if (currentContext === contextName) {
|
|
69
71
|
if (this.debugMode) {
|
|
70
72
|
// Optional: Log that no change is happening, or simply do nothing.
|
|
71
73
|
console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
|
|
72
74
|
}
|
|
73
|
-
return; // Context is the same, so no further action is needed.
|
|
75
|
+
return false; // Context is the same, so no further action is needed.
|
|
74
76
|
}
|
|
75
77
|
// If we reach here, the context is actually changing.
|
|
76
|
-
const oldContext = this.activeContext$.getValue(); // For more informative logging
|
|
77
78
|
if (this.debugMode) {
|
|
78
|
-
console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${
|
|
79
|
+
console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${currentContext}" to "${contextName}".`);
|
|
79
80
|
}
|
|
80
81
|
this.activeContext$.next(contextName);
|
|
82
|
+
return true;
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
83
85
|
* Gets the current active context.
|
|
@@ -148,6 +150,34 @@ export class Hotkeys {
|
|
|
148
150
|
}
|
|
149
151
|
return config.id;
|
|
150
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Parses a single key trigger definition (either shorthand StandardKey or an object with modifiers)
|
|
155
|
+
* into its constituent parts: main key and modifier states.
|
|
156
|
+
* @param keyInput - The KeyCombinationTrigger to parse.
|
|
157
|
+
* @param shortcutId - The ID of the shortcut this key trigger belongs to (for logging).
|
|
158
|
+
* @returns An object containing configuredMainKey and modifier states, or null if parsing fails.
|
|
159
|
+
*/
|
|
160
|
+
_parseKeyTrigger(keyInput, shortcutId) {
|
|
161
|
+
if (typeof keyInput === "string") {
|
|
162
|
+
if (keyInput === "") {
|
|
163
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key (shorthand) in shortcut "${shortcutId}". Key string must not be empty.`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return { configuredMainKey: keyInput, ctrlKeyConfig: false, altKeyConfig: false, shiftKeyConfig: false, metaKeyConfig: false, logDetails: `key: "${keyInput}" (no mods)` };
|
|
167
|
+
}
|
|
168
|
+
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.`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const logDetails = `key: "${keyInput.key}"` +
|
|
174
|
+
(keyInput.ctrlKey !== undefined ? `, ctrl: ${keyInput.ctrlKey}` : "") +
|
|
175
|
+
(keyInput.altKey !== undefined ? `, alt: ${keyInput.altKey}` : "") +
|
|
176
|
+
(keyInput.shiftKey !== undefined ? `, shift: ${keyInput.shiftKey}` : "") +
|
|
177
|
+
(keyInput.metaKey !== undefined ? `, meta: ${keyInput.metaKey}` : "");
|
|
178
|
+
return { configuredMainKey: keyInput.key, ctrlKeyConfig: keyInput.ctrlKey, altKeyConfig: keyInput.altKey, shiftKeyConfig: keyInput.shiftKey, metaKeyConfig: keyInput.metaKey, logDetails };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
151
181
|
/**
|
|
152
182
|
* Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
|
|
153
183
|
* The callback is triggered when the specified key and modifier keys (if any) are pressed.
|
|
@@ -176,67 +206,50 @@ export class Hotkeys {
|
|
|
176
206
|
*/
|
|
177
207
|
addCombination(config) {
|
|
178
208
|
const { keys, callback, context, preventDefault = false, id } = config;
|
|
179
|
-
let
|
|
180
|
-
let
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (typeof keys === "string") {
|
|
186
|
-
// Shorthand: keys is StandardKey, implying no modifiers should be active
|
|
187
|
-
// Corrected validation: Check for actual empty string, not a string that trims to empty.
|
|
188
|
-
if (keys === "") { // StandardKey type should prevent this, but check for robustness.
|
|
189
|
-
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys" (shorthand) for combination shortcut "${id}". Key must not be an empty string. Shortcut not added.`);
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
configuredMainKey = keys;
|
|
193
|
-
ctrlKeyConfig = false; // Shorthand implies no modifiers
|
|
194
|
-
altKeyConfig = false;
|
|
195
|
-
shiftKeyConfig = false;
|
|
196
|
-
metaKeyConfig = false;
|
|
197
|
-
keyDetailsForLog = `key: "${keys}" (no modifiers implied)`;
|
|
209
|
+
let finalShortcut$;
|
|
210
|
+
let overallLogDetails = "";
|
|
211
|
+
const keyTriggers = Array.isArray(keys) ? keys : [keys];
|
|
212
|
+
if (keyTriggers.length === 0) {
|
|
213
|
+
console.warn(`${Hotkeys.LOG_PREFIX} "keys" array for combination shortcut "${id}" is empty. Shortcut not added.`);
|
|
214
|
+
return undefined;
|
|
198
215
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
216
|
+
const observables = [];
|
|
217
|
+
const logParts = [];
|
|
218
|
+
for (const keyInput of keyTriggers) {
|
|
219
|
+
const parsedTrigger = this._parseKeyTrigger(keyInput, id);
|
|
220
|
+
if (!parsedTrigger) {
|
|
221
|
+
// Error already logged by _parseKeyTrigger
|
|
204
222
|
return undefined;
|
|
205
223
|
}
|
|
206
|
-
configuredMainKey =
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig, logDetails } = parsedTrigger;
|
|
225
|
+
logParts.push(`{ ${logDetails} }`);
|
|
226
|
+
const stream = this.filterByContext(this.keydown$, context).pipe(filter(event => {
|
|
227
|
+
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
228
|
+
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
229
|
+
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
230
|
+
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
231
|
+
return ctrlMatch && altMatch && shiftMatch && metaMatch;
|
|
232
|
+
}), filter(event => compareKey(event.key, configuredMainKey)));
|
|
233
|
+
observables.push(stream);
|
|
216
234
|
}
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
226
|
-
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
227
|
-
return ctrlMatch && altMatch && shiftMatch && metaMatch;
|
|
228
|
-
}), filter(event => compareKey(event.key, configuredMainKey)), tap(event => {
|
|
235
|
+
if (observables.length === 0) {
|
|
236
|
+
// Should be caught by keyTriggers.length === 0, but as a safeguard.
|
|
237
|
+
console.warn(`${Hotkeys.LOG_PREFIX} No valid key triggers for combination shortcut "${id}". Shortcut not added.`);
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
finalShortcut$ = merge(...observables);
|
|
241
|
+
overallLogDetails = Array.isArray(keys) ? `Triggers: [ ${logParts.join(", ")} ]` : logParts[0];
|
|
242
|
+
const subscription = finalShortcut$.pipe(tap(event => {
|
|
229
243
|
if (this.debugMode) {
|
|
230
244
|
const preventAction = preventDefault ? ", preventing default" : "";
|
|
231
|
-
console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered${preventAction}.`);
|
|
245
|
+
console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered by key "${event.key}" ${preventAction}.`);
|
|
232
246
|
}
|
|
233
247
|
if (preventDefault)
|
|
234
248
|
event.preventDefault();
|
|
235
249
|
}), catchError(err => {
|
|
236
250
|
console.error(`${Hotkeys.LOG_PREFIX} Error in combination stream for shortcut "${id}":`, err);
|
|
237
251
|
return EMPTY;
|
|
238
|
-
}))
|
|
239
|
-
const subscription = shortcut$.subscribe(event => {
|
|
252
|
+
})).subscribe(event => {
|
|
240
253
|
try {
|
|
241
254
|
callback(event);
|
|
242
255
|
}
|
|
@@ -244,7 +257,7 @@ export class Hotkeys {
|
|
|
244
257
|
console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for combination shortcut "${id}":`, e);
|
|
245
258
|
}
|
|
246
259
|
});
|
|
247
|
-
return this._registerShortcut(config, subscription, ShortcutTypes.Combination,
|
|
260
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Combination, overallLogDetails);
|
|
248
261
|
}
|
|
249
262
|
/**
|
|
250
263
|
* Registers a key sequence shortcut (e.g., g -> i, or ArrowUp -> ArrowUp -> ArrowDown).
|
package/dist/hotkeys.test.js
CHANGED
|
@@ -152,16 +152,59 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
152
152
|
activeContextNextSpy.mock.restore();
|
|
153
153
|
consoleLogMock.mock.restore();
|
|
154
154
|
});
|
|
155
|
+
it(`should log "no change" if context is set to the same value (debug mode on)`, () => {
|
|
156
|
+
keyManager.setContext("sameCtx"); // Set initial context
|
|
157
|
+
const consoleLogMock = mock.method(console, "log");
|
|
158
|
+
keyManager.setDebugMode(true);
|
|
159
|
+
consoleLogMock.mock.resetCalls(); // Clear previous logs
|
|
160
|
+
keyManager.setContext("sameCtx"); // Attempt to set the same context
|
|
161
|
+
const noChangeLogCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`setContext called with the same context "sameCtx". No change made.`));
|
|
162
|
+
assert.ok(noChangeLogCall, "No-change log not found or incorrect for same context.");
|
|
163
|
+
const changedLogCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes("Context changed from"));
|
|
164
|
+
assert.strictEqual(changedLogCall, undefined, "Context changed log should not appear for same context.");
|
|
165
|
+
consoleLogMock.mock.restore();
|
|
166
|
+
});
|
|
155
167
|
it("should toggle debug mode and log its state", () => {
|
|
156
168
|
const consoleLogMock = mock.method(console, "log");
|
|
157
169
|
keyManager.setDebugMode(true);
|
|
158
170
|
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes("Debug mode enabled")));
|
|
159
171
|
consoleLogMock.mock.resetCalls();
|
|
160
172
|
keyManager.setDebugMode(false);
|
|
161
|
-
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>", consoleLogMock.mock.calls);
|
|
162
173
|
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes("Debug mode disabled")));
|
|
163
174
|
consoleLogMock.mock.restore();
|
|
164
175
|
});
|
|
176
|
+
describe("setContext return value", () => {
|
|
177
|
+
it("should return true when context is changed from null to a new value", () => {
|
|
178
|
+
assert.strictEqual(keyManager.getContext(), null, "Initial context should be null");
|
|
179
|
+
const result = keyManager.setContext("newContext");
|
|
180
|
+
assert.strictEqual(result, true, "setContext should return true for a change from null");
|
|
181
|
+
assert.strictEqual(keyManager.getContext(), "newContext");
|
|
182
|
+
});
|
|
183
|
+
it("should return true when context is changed from one value to another", () => {
|
|
184
|
+
keyManager.setContext("initialContext"); // Set a non-null initial context
|
|
185
|
+
const result = keyManager.setContext("changedContext");
|
|
186
|
+
assert.strictEqual(result, true, "setContext should return true for a value-to-value change");
|
|
187
|
+
assert.strictEqual(keyManager.getContext(), "changedContext");
|
|
188
|
+
});
|
|
189
|
+
it("should return false when context is set to the same value (non-null)", () => {
|
|
190
|
+
keyManager.setContext("sameContext");
|
|
191
|
+
const result = keyManager.setContext("sameContext");
|
|
192
|
+
assert.strictEqual(result, false, "setContext should return false for no change (non-null)");
|
|
193
|
+
assert.strictEqual(keyManager.getContext(), "sameContext");
|
|
194
|
+
});
|
|
195
|
+
it("should return true when context is changed from a value to null", () => {
|
|
196
|
+
keyManager.setContext("initialContext");
|
|
197
|
+
const result = keyManager.setContext(null);
|
|
198
|
+
assert.strictEqual(result, true, "setContext should return true for a change to null");
|
|
199
|
+
assert.strictEqual(keyManager.getContext(), null);
|
|
200
|
+
});
|
|
201
|
+
it("should return false when context is set to null and already null", () => {
|
|
202
|
+
keyManager.setContext(null); // Ensure context is null
|
|
203
|
+
const result = keyManager.setContext(null);
|
|
204
|
+
assert.strictEqual(result, false, "setContext should return false for no change (already null)");
|
|
205
|
+
assert.strictEqual(keyManager.getContext(), null);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
165
208
|
describe("onContextChange$ observable", () => {
|
|
166
209
|
it("should emit initial context to new subscriber", () => {
|
|
167
210
|
const manager = new Hotkeys("initial", false);
|
|
@@ -176,14 +219,14 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
176
219
|
const spy = createMockFn();
|
|
177
220
|
const sub = keyManager.onContextChange$.subscribe(spy); // Subscribes, gets initial null
|
|
178
221
|
spy.mockClear(); // Clear initial emission
|
|
179
|
-
keyManager.setContext("newContext");
|
|
180
|
-
assert.strictEqual(spy.calledCount, 1);
|
|
222
|
+
assert.ok(keyManager.setContext("newContext"), "setContext should have returned true for change");
|
|
223
|
+
assert.strictEqual(spy.calledCount, 1, "onContextChange$ did not emit on first change");
|
|
181
224
|
assert.strictEqual(spy.lastArgs[0], "newContext");
|
|
182
|
-
keyManager.setContext("anotherContext");
|
|
183
|
-
assert.strictEqual(spy.calledCount, 2);
|
|
225
|
+
assert.ok(keyManager.setContext("anotherContext"), "setContext should have returned true for second change");
|
|
226
|
+
assert.strictEqual(spy.calledCount, 2, "onContextChange$ did not emit on second change");
|
|
184
227
|
assert.strictEqual(spy.lastArgs[0], "anotherContext");
|
|
185
|
-
keyManager.setContext(null);
|
|
186
|
-
assert.strictEqual(spy.calledCount, 3);
|
|
228
|
+
assert.ok(keyManager.setContext(null), "setContext should have returned true for change to null");
|
|
229
|
+
assert.strictEqual(spy.calledCount, 3, "onContextChange$ did not emit on change to null");
|
|
187
230
|
assert.strictEqual(spy.lastArgs[0], null);
|
|
188
231
|
sub.unsubscribe();
|
|
189
232
|
});
|
|
@@ -192,29 +235,29 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
192
235
|
const spy = createMockFn();
|
|
193
236
|
const sub = keyManager.onContextChange$.subscribe(spy); // Subscribes, gets "testContext"
|
|
194
237
|
spy.mockClear(); // Clear initial emission
|
|
195
|
-
keyManager.setContext("testContext"); // Set same context
|
|
238
|
+
assert.strictEqual(keyManager.setContext("testContext"), false, "setContext should have returned false for no change"); // Set same context
|
|
196
239
|
assert.strictEqual(spy.calledCount, 0, "Observable should not emit if context value is the same.");
|
|
197
240
|
sub.unsubscribe();
|
|
198
241
|
});
|
|
199
242
|
});
|
|
200
243
|
});
|
|
201
244
|
describe("addCombination", () => {
|
|
202
|
-
it(
|
|
245
|
+
it(`should trigger callback for a simple key combination (e.g., "A")`, () => {
|
|
203
246
|
const config = { id: "simpleA", keys: { key: Keys.A }, callback: mockCallback };
|
|
204
247
|
const result = keyManager.addCombination(config);
|
|
205
248
|
assert.strictEqual(result, "simpleA");
|
|
206
249
|
dispatchKeyEvent("a");
|
|
207
|
-
assert.strictEqual(mockCallback.calledCount, 1,
|
|
250
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "a" not called`);
|
|
208
251
|
mockCallback.mockClear();
|
|
209
252
|
dispatchKeyEvent("A");
|
|
210
|
-
assert.strictEqual(mockCallback.calledCount, 1,
|
|
253
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "A" not called`);
|
|
211
254
|
});
|
|
212
255
|
it("should return undefined and warn if keys.key is null or undefined (runtime check in object form)", () => {
|
|
213
256
|
const config = { id: "nullKey", keys: { key: null }, callback: mockCallback };
|
|
214
257
|
const result = keyManager.addCombination(config);
|
|
215
258
|
assert.strictEqual(result, undefined, "Should return undefined for null key");
|
|
216
259
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
217
|
-
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "
|
|
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.`));
|
|
218
261
|
});
|
|
219
262
|
it("should pass the KeyboardEvent to the callback", () => {
|
|
220
263
|
const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
|
|
@@ -274,30 +317,75 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
274
317
|
dispatchKeyEvent("w");
|
|
275
318
|
assert.strictEqual(workingCallback.calledCount, 1);
|
|
276
319
|
});
|
|
320
|
+
it("should trigger callback if any of multiple key combinations are pressed", () => {
|
|
321
|
+
const config = {
|
|
322
|
+
id: "multiCombo",
|
|
323
|
+
keys: [
|
|
324
|
+
{ key: Keys.A, ctrlKey: true }, // Ctrl+A
|
|
325
|
+
Keys.Escape, // Escape
|
|
326
|
+
{ key: Keys.B, shiftKey: true, altKey: true } // Shift+Alt+B
|
|
327
|
+
],
|
|
328
|
+
callback: mockCallback
|
|
329
|
+
};
|
|
330
|
+
keyManager.addCombination(config);
|
|
331
|
+
dispatchKeyEvent("A", { ctrlKey: true });
|
|
332
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Ctrl+A did not trigger");
|
|
333
|
+
dispatchKeyEvent("Escape");
|
|
334
|
+
assert.strictEqual(mockCallback.calledCount, 2, "Escape did not trigger");
|
|
335
|
+
dispatchKeyEvent("B", { shiftKey: true, altKey: true });
|
|
336
|
+
assert.strictEqual(mockCallback.calledCount, 3, "Shift+Alt+B did not trigger");
|
|
337
|
+
dispatchKeyEvent("C"); // Should not trigger
|
|
338
|
+
assert.strictEqual(mockCallback.calledCount, 3, "Unrelated key C triggered");
|
|
339
|
+
});
|
|
340
|
+
it(`should return undefined and warn if "keys" array is empty`, () => {
|
|
341
|
+
const config = { id: "emptyKeysArray", keys: [], callback: mockCallback };
|
|
342
|
+
const result = keyManager.addCombination(config);
|
|
343
|
+
assert.strictEqual(result, undefined);
|
|
344
|
+
assert.ok(consoleWarnMock.mock.calls.some(call => call.arguments[0].includes(`"keys" array for combination shortcut "emptyKeysArray" is empty`)));
|
|
345
|
+
});
|
|
346
|
+
it(`should return undefined and warn if a key in "keys" array is invalid (shorthand)`, () => {
|
|
347
|
+
const config = { id: "invalidInArrayShorthand", keys: [Keys.A, ""], callback: mockCallback };
|
|
348
|
+
const result = keyManager.addCombination(config);
|
|
349
|
+
assert.strictEqual(result, undefined);
|
|
350
|
+
assert.ok(consoleWarnMock.mock.calls.some(call => call.arguments[0].includes(`Invalid key (shorthand) in shortcut "invalidInArrayShorthand"`)));
|
|
351
|
+
});
|
|
352
|
+
it(`should return undefined and warn if a key in "keys" array is invalid (object)`, () => {
|
|
353
|
+
const config = { id: "invalidInArrayObject", keys: [Keys.A, { key: "" }], callback: mockCallback };
|
|
354
|
+
const result = keyManager.addCombination(config);
|
|
355
|
+
assert.strictEqual(result, undefined);
|
|
356
|
+
assert.ok(consoleWarnMock.mock.calls.some(call => call.arguments[0].includes(`Invalid "key" property in shortcut "invalidInArrayObject"`)));
|
|
357
|
+
});
|
|
358
|
+
it("should correctly log multiple key triggers in debug mode", () => {
|
|
359
|
+
const consoleLogMock = mock.method(console, "log");
|
|
360
|
+
keyManager.setDebugMode(true);
|
|
361
|
+
keyManager.addCombination({ id: "multiLog", keys: [Keys.F1, { key: Keys.F2, ctrlKey: true }], callback: mockCallback });
|
|
362
|
+
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Triggers: [ { key: "F1" (no mods) }, { key: "F2", ctrl: true } ]`)), "Multi-trigger log format incorrect");
|
|
363
|
+
consoleLogMock.mock.restore();
|
|
364
|
+
});
|
|
277
365
|
describe("addCombination - Shorthand Syntax", () => {
|
|
278
366
|
it("should trigger callback for a simple key using shorthand (e.g., Keys.X)", () => {
|
|
279
367
|
const config = { id: "shorthandX", keys: Keys.X, callback: mockCallback };
|
|
280
368
|
keyManager.addCombination(config);
|
|
281
369
|
dispatchKeyEvent(Keys.X.toLowerCase());
|
|
282
|
-
assert.strictEqual(mockCallback.calledCount, 1,
|
|
370
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "x" (shorthand) not called`);
|
|
283
371
|
mockCallback.mockClear();
|
|
284
372
|
dispatchKeyEvent(Keys.X);
|
|
285
|
-
assert.strictEqual(mockCallback.calledCount, 1,
|
|
373
|
+
assert.strictEqual(mockCallback.calledCount, 1, `Callback for "X" (shorthand) not called`);
|
|
286
374
|
});
|
|
287
375
|
it("should NOT trigger callback for shorthand if modifier is pressed", () => {
|
|
288
376
|
const config = { id: "shorthandY", keys: Keys.Y, callback: mockCallback };
|
|
289
377
|
keyManager.addCombination(config);
|
|
290
378
|
dispatchKeyEvent(Keys.Y, { ctrlKey: true });
|
|
291
|
-
assert.strictEqual(mockCallback.calledCount, 0,
|
|
379
|
+
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Ctrl`);
|
|
292
380
|
mockCallback.mockClear();
|
|
293
381
|
dispatchKeyEvent(Keys.Y, { altKey: true });
|
|
294
|
-
assert.strictEqual(mockCallback.calledCount, 0,
|
|
382
|
+
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Alt`);
|
|
295
383
|
mockCallback.mockClear();
|
|
296
384
|
dispatchKeyEvent(Keys.Y, { shiftKey: true });
|
|
297
|
-
assert.strictEqual(mockCallback.calledCount, 0,
|
|
385
|
+
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Shift`);
|
|
298
386
|
mockCallback.mockClear();
|
|
299
387
|
dispatchKeyEvent(Keys.Y, { metaKey: true });
|
|
300
|
-
assert.strictEqual(mockCallback.calledCount, 0,
|
|
388
|
+
assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Meta`);
|
|
301
389
|
});
|
|
302
390
|
it("should trigger callback for shorthand if ONLY the key is pressed (no modifiers)", () => {
|
|
303
391
|
const config = { id: "shorthandZ", keys: Keys.Z, callback: mockCallback };
|
|
@@ -327,16 +415,16 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
327
415
|
const result = keyManager.addCombination(config);
|
|
328
416
|
assert.strictEqual(result, undefined);
|
|
329
417
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
330
|
-
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid
|
|
418
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid key (shorthand) in shortcut "emptyShorthand". Key string must not be empty.`));
|
|
331
419
|
});
|
|
332
420
|
it("should correctly log shorthand key details in debug mode", () => {
|
|
333
421
|
const consoleLogMock = mock.method(console, "log");
|
|
334
422
|
keyManager.setDebugMode(true);
|
|
335
423
|
const config = { id: "debugShorthand", keys: Keys.D, callback: mockCallback };
|
|
336
424
|
keyManager.addCombination(config);
|
|
337
|
-
const logMessage = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`combination shortcut "debugShorthand" added
|
|
425
|
+
const logMessage = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`combination shortcut "debugShorthand" added.`));
|
|
338
426
|
assert.ok(logMessage, "Debug log for adding shortcut not found");
|
|
339
|
-
assert.ok(logMessage.arguments[0].includes(`
|
|
427
|
+
assert.ok(logMessage.arguments[0].includes(`{ key: "D" (no mods) }`), `Log message content mismatch: ${logMessage.arguments[0]}`);
|
|
340
428
|
consoleLogMock.mock.restore();
|
|
341
429
|
keyManager.setDebugMode(false);
|
|
342
430
|
});
|
|
@@ -382,7 +470,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
382
470
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
383
471
|
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Sequence for shortcut "emptySeq" is empty`));
|
|
384
472
|
});
|
|
385
|
-
it(
|
|
473
|
+
it(`should return undefined and warn if sequence contains an invalid key (runtime check with "as any")`, () => {
|
|
386
474
|
// This test checks runtime robustness if `any` is used to bypass StandardKey[]
|
|
387
475
|
const config = { id: "invalidKeyInSeq", sequence: [Keys.A, "", Keys.C], callback: mockCallback };
|
|
388
476
|
const result = keyManager.addSequence(config);
|