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 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): void`
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` (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).
@@ -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): void;
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.
@@ -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;;;;;;OAMG;IACI,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAiBnD;;;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":"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;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;;;GAGG;AACH,KAAK,qBAAqB,GAAG;IACzB;;;;;;;;;OASG;IACH,GAAG,EAAE,WAAW,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,WAAW,CAAC;AAEhB,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC5D;;;;;;;;;;OAUG;IACH,IAAI,EAAE,qBAAqB,GAAG,qBAAqB,EAAE,CAAC;CACzD;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD;;;;;;;;OAQG;IACH,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,KAAK,cAAc,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,YAAY,EAAE,YAAY,CAAC;CAC9B;AAiCD;;;;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;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IA4BxB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACI,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS;IAuEvE;;;;;;;;;;;;;;;;;;;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"}
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
- if (this.activeContext$.getValue() === contextName) {
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 "${oldContext}" to "${contextName}".`);
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 configuredMainKey;
180
- let ctrlKeyConfig;
181
- let altKeyConfig;
182
- let shiftKeyConfig;
183
- let metaKeyConfig;
184
- let keyDetailsForLog;
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
- else {
200
- // Object form
201
- // Corrected validation for keys.key
202
- if (!keys || !keys.key || typeof keys.key !== "string" || keys.key === "") { // StandardKey type should prevent empty string for key.key
203
- 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
204
222
  return undefined;
205
223
  }
206
- configuredMainKey = keys.key;
207
- ctrlKeyConfig = keys.ctrlKey;
208
- altKeyConfig = keys.altKey;
209
- shiftKeyConfig = keys.shiftKey;
210
- metaKeyConfig = keys.metaKey;
211
- keyDetailsForLog = `key: "${keys.key}"` +
212
- (keys.ctrlKey !== undefined ? `, ctrlKey: ${keys.ctrlKey}` : "") +
213
- (keys.altKey !== undefined ? `, altKey: ${keys.altKey}` : "") +
214
- (keys.shiftKey !== undefined ? `, shiftKey: ${keys.shiftKey}` : "") +
215
- (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);
216
234
  }
217
- const shortcut$ = this.filterByContext(this.keydown$, context).pipe(filter(event => {
218
- // For shorthand (where modifiers are explicitly false), we want an exact match.
219
- // For object form:
220
- // - if modifier is true, event.modifier must be true.
221
- // - if modifier is false, event.modifier must be false.
222
- // - if modifier is undefined, we don't care about event.modifier.
223
- const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
224
- const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
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, `Keys: { ${keyDetailsForLog} }`); // Use Enum
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).
@@ -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("should trigger callback for a simple key combination (e.g., 'A')", () => {
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, "Callback for 'a' not called");
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, "Callback for 'A' not called");
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 "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.`));
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, "Callback for 'x' (shorthand) not called");
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, "Callback for 'X' (shorthand) not called");
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, "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`);
292
380
  mockCallback.mockClear();
293
381
  dispatchKeyEvent(Keys.Y, { altKey: true });
294
- 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`);
295
383
  mockCallback.mockClear();
296
384
  dispatchKeyEvent(Keys.Y, { shiftKey: true });
297
- 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`);
298
386
  mockCallback.mockClear();
299
387
  dispatchKeyEvent(Keys.Y, { metaKey: true });
300
- 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`);
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 "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.`));
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(`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]}`);
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("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")`, () => {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rx-hotkeys",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsc",