rx-hotkeys 2.3.0 → 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 CHANGED
@@ -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` (required): Defines the main key (from Keys) and optional modifier keys.
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).
@@ -125,6 +136,14 @@ export declare class Hotkeys {
125
136
  hasShortcut(id: string): boolean;
126
137
  private filterByContext;
127
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;
128
147
  /**
129
148
  * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
130
149
  * The callback is triggered when the specified key and modifier keys (if any) are pressed.
@@ -1 +1 @@
1
- {"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"AAAA,OAAO,EACuC,UAAU,EAEvD,MAAM,MAAM,CAAC;AACd,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIxC,oBAAY,aAAa;IACrB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;CACxB;AAED,UAAU,kBAAkB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC5D;;;;;;;;;;OAUG;IACH,IAAI,EAAE;QACF;;;;;;;;;WASG;QACH,GAAG,EAAE,WAAW,CAAC;QACjB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD;;;;;;;;OAQG;IACH,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAyCD;;;;GAIG;AACH,qBAAa,OAAO;IAChB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAa;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAc;IAEhD,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,cAAc,CAAiC;IACvD,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,SAAS,CAAU;IAE3B;;;;;OAKG;gBACS,cAAc,GAAE,MAAM,GAAG,IAAW,EAAE,SAAS,GAAE,OAAe;IAe5E;;;;;;;OAOG;IACI,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAkBtD;;;OAGG;IACI,UAAU,IAAI,MAAM,GAAG,IAAI;IAIlC;;;;OAIG;IACI,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAa1C;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAW,gBAAgB,IAAI,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,CAEvD;IAED;;;;OAIG;IACI,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIvC,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,iBAAiB;IAkBzB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACI,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS;IAgFvE;;;;;;;;;;;;;;;;;;;OAmBG;IACI,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS;IAkHjE;;;;;;OAMG;IACI,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYlC;;;;;;OAMG;IACI,kBAAkB,IAAI;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAC,EAAE;IAa/G;;;;;OAKG;IACI,OAAO,IAAI,IAAI;CAOzB"}
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) {
@@ -150,6 +150,34 @@ export class Hotkeys {
150
150
  }
151
151
  return config.id;
152
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
+ }
153
181
  /**
154
182
  * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
155
183
  * The callback is triggered when the specified key and modifier keys (if any) are pressed.
@@ -178,67 +206,50 @@ export class Hotkeys {
178
206
  */
179
207
  addCombination(config) {
180
208
  const { keys, callback, context, preventDefault = false, id } = config;
181
- let configuredMainKey;
182
- let ctrlKeyConfig;
183
- let altKeyConfig;
184
- let shiftKeyConfig;
185
- let metaKeyConfig;
186
- let keyDetailsForLog;
187
- if (typeof keys === "string") {
188
- // Shorthand: keys is StandardKey, implying no modifiers should be active
189
- // Corrected validation: Check for actual empty string, not a string that trims to empty.
190
- if (keys === "") { // StandardKey type should prevent this, but check for robustness.
191
- console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys" (shorthand) for combination shortcut "${id}". Key must not be an empty string. Shortcut not added.`);
192
- return undefined;
193
- }
194
- configuredMainKey = keys;
195
- ctrlKeyConfig = false; // Shorthand implies no modifiers
196
- altKeyConfig = false;
197
- shiftKeyConfig = false;
198
- metaKeyConfig = false;
199
- 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;
200
215
  }
201
- else {
202
- // Object form
203
- // Corrected validation for keys.key
204
- if (!keys || !keys.key || typeof keys.key !== "string" || keys.key === "") { // StandardKey type should prevent empty string for key.key
205
- console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys.key" for combination shortcut "${id}". Key must be a non-empty string value from Keys. Shortcut not added.`);
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
206
222
  return undefined;
207
223
  }
208
- configuredMainKey = keys.key;
209
- ctrlKeyConfig = keys.ctrlKey;
210
- altKeyConfig = keys.altKey;
211
- shiftKeyConfig = keys.shiftKey;
212
- metaKeyConfig = keys.metaKey;
213
- keyDetailsForLog = `key: "${keys.key}"` +
214
- (keys.ctrlKey !== undefined ? `, ctrlKey: ${keys.ctrlKey}` : "") +
215
- (keys.altKey !== undefined ? `, altKey: ${keys.altKey}` : "") +
216
- (keys.shiftKey !== undefined ? `, shiftKey: ${keys.shiftKey}` : "") +
217
- (keys.metaKey !== undefined ? `, metaKey: ${keys.metaKey}` : "");
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);
218
234
  }
219
- const shortcut$ = this.filterByContext(this.keydown$, context).pipe(filter(event => {
220
- // For shorthand (where modifiers are explicitly false), we want an exact match.
221
- // For object form:
222
- // - if modifier is true, event.modifier must be true.
223
- // - if modifier is false, event.modifier must be false.
224
- // - if modifier is undefined, we don't care about event.modifier.
225
- const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
226
- const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
227
- const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
228
- const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
229
- return ctrlMatch && altMatch && shiftMatch && metaMatch;
230
- }), 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 => {
231
243
  if (this.debugMode) {
232
244
  const preventAction = preventDefault ? ", preventing default" : "";
233
- console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered${preventAction}.`);
245
+ console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered by key "${event.key}" ${preventAction}.`);
234
246
  }
235
247
  if (preventDefault)
236
248
  event.preventDefault();
237
249
  }), catchError(err => {
238
250
  console.error(`${Hotkeys.LOG_PREFIX} Error in combination stream for shortcut "${id}":`, err);
239
251
  return EMPTY;
240
- }));
241
- const subscription = shortcut$.subscribe(event => {
252
+ })).subscribe(event => {
242
253
  try {
243
254
  callback(event);
244
255
  }
@@ -246,7 +257,7 @@ export class Hotkeys {
246
257
  console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for combination shortcut "${id}":`, e);
247
258
  }
248
259
  });
249
- return this._registerShortcut(config, subscription, ShortcutTypes.Combination, `Keys: { ${keyDetailsForLog} }`); // Use Enum
260
+ return this._registerShortcut(config, subscription, ShortcutTypes.Combination, overallLogDetails);
250
261
  }
251
262
  /**
252
263
  * Registers a key sequence shortcut (e.g., g -> i, or ArrowUp -> ArrowUp -> ArrowDown).
@@ -242,22 +242,22 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
242
242
  });
243
243
  });
244
244
  describe("addCombination", () => {
245
- it("should trigger callback for a simple key combination (e.g., 'A')", () => {
245
+ it(`should trigger callback for a simple key combination (e.g., "A")`, () => {
246
246
  const config = { id: "simpleA", keys: { key: Keys.A }, callback: mockCallback };
247
247
  const result = keyManager.addCombination(config);
248
248
  assert.strictEqual(result, "simpleA");
249
249
  dispatchKeyEvent("a");
250
- assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'a' not called");
250
+ assert.strictEqual(mockCallback.calledCount, 1, `Callback for "a" not called`);
251
251
  mockCallback.mockClear();
252
252
  dispatchKeyEvent("A");
253
- assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'A' not called");
253
+ assert.strictEqual(mockCallback.calledCount, 1, `Callback for "A" not called`);
254
254
  });
255
255
  it("should return undefined and warn if keys.key is null or undefined (runtime check in object form)", () => {
256
256
  const config = { id: "nullKey", keys: { key: null }, callback: mockCallback };
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 "keys.key" for combination shortcut "nullKey"`));
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.`));
261
261
  });
262
262
  it("should pass the KeyboardEvent to the callback", () => {
263
263
  const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
@@ -317,30 +317,75 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
317
317
  dispatchKeyEvent("w");
318
318
  assert.strictEqual(workingCallback.calledCount, 1);
319
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
+ });
320
365
  describe("addCombination - Shorthand Syntax", () => {
321
366
  it("should trigger callback for a simple key using shorthand (e.g., Keys.X)", () => {
322
367
  const config = { id: "shorthandX", keys: Keys.X, callback: mockCallback };
323
368
  keyManager.addCombination(config);
324
369
  dispatchKeyEvent(Keys.X.toLowerCase());
325
- assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'x' (shorthand) not called");
370
+ assert.strictEqual(mockCallback.calledCount, 1, `Callback for "x" (shorthand) not called`);
326
371
  mockCallback.mockClear();
327
372
  dispatchKeyEvent(Keys.X);
328
- assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'X' (shorthand) not called");
373
+ assert.strictEqual(mockCallback.calledCount, 1, `Callback for "X" (shorthand) not called`);
329
374
  });
330
375
  it("should NOT trigger callback for shorthand if modifier is pressed", () => {
331
376
  const config = { id: "shorthandY", keys: Keys.Y, callback: mockCallback };
332
377
  keyManager.addCombination(config);
333
378
  dispatchKeyEvent(Keys.Y, { ctrlKey: true });
334
- assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Ctrl");
379
+ assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Ctrl`);
335
380
  mockCallback.mockClear();
336
381
  dispatchKeyEvent(Keys.Y, { altKey: true });
337
- assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Alt");
382
+ assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Alt`);
338
383
  mockCallback.mockClear();
339
384
  dispatchKeyEvent(Keys.Y, { shiftKey: true });
340
- assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Shift");
385
+ assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Shift`);
341
386
  mockCallback.mockClear();
342
387
  dispatchKeyEvent(Keys.Y, { metaKey: true });
343
- assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Meta");
388
+ assert.strictEqual(mockCallback.calledCount, 0, `Callback for "y" (shorthand) should not be called with Meta`);
344
389
  });
345
390
  it("should trigger callback for shorthand if ONLY the key is pressed (no modifiers)", () => {
346
391
  const config = { id: "shorthandZ", keys: Keys.Z, callback: mockCallback };
@@ -370,16 +415,16 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
370
415
  const result = keyManager.addCombination(config);
371
416
  assert.strictEqual(result, undefined);
372
417
  assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
373
- assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "keys" (shorthand) for combination shortcut "emptyShorthand"`));
418
+ assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid key (shorthand) in shortcut "emptyShorthand". Key string must not be empty.`));
374
419
  });
375
420
  it("should correctly log shorthand key details in debug mode", () => {
376
421
  const consoleLogMock = mock.method(console, "log");
377
422
  keyManager.setDebugMode(true);
378
423
  const config = { id: "debugShorthand", keys: Keys.D, callback: mockCallback };
379
424
  keyManager.addCombination(config);
380
- 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.`));
381
426
  assert.ok(logMessage, "Debug log for adding shortcut not found");
382
- assert.ok(logMessage.arguments[0].includes(`Keys: { key: "D" (no modifiers implied) }`), `Log message content mismatch: ${logMessage.arguments[0]}`);
427
+ assert.ok(logMessage.arguments[0].includes(`{ key: "D" (no mods) }`), `Log message content mismatch: ${logMessage.arguments[0]}`);
383
428
  consoleLogMock.mock.restore();
384
429
  keyManager.setDebugMode(false);
385
430
  });
@@ -425,7 +470,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
425
470
  assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
426
471
  assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Sequence for shortcut "emptySeq" is empty`));
427
472
  });
428
- it("should return undefined and warn if sequence contains an invalid key (runtime check with 'as any')", () => {
473
+ it(`should return undefined and warn if sequence contains an invalid key (runtime check with "as any")`, () => {
429
474
  // This test checks runtime robustness if `any` is used to bypass StandardKey[]
430
475
  const config = { id: "invalidKeyInSeq", sequence: [Keys.A, "", Keys.C], callback: mockCallback };
431
476
  const result = keyManager.addSequence(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rx-hotkeys",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsc",