rx-hotkeys 2.0.0 → 2.2.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; }` (required): Defines the main key (from Keys) and optional modifier keys.
168
+ * `keys: { 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,4 +1,9 @@
1
+ import { Observable } from "rxjs";
1
2
  import { StandardKey } from "./keys.js";
3
+ export declare enum ShortcutTypes {
4
+ Combination = "combination",
5
+ Sequence = "sequence"
6
+ }
2
7
  interface ShortcutConfigBase {
3
8
  id: string;
4
9
  callback: (event?: KeyboardEvent) => void;
@@ -7,6 +12,17 @@ interface ShortcutConfigBase {
7
12
  description?: string;
8
13
  }
9
14
  export interface KeyCombinationConfig extends ShortcutConfigBase {
15
+ /**
16
+ * Defines the key or key combination.
17
+ * Can be an object specifying the main `key` (from `StandardKey`) and optional
18
+ * modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
19
+ * Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
20
+ *
21
+ * Alternatively, for a simple key press without any modifiers, this can be
22
+ * a `StandardKey` directly.
23
+ * Example: `Keys.Escape` for the Escape key. When using this shorthand,
24
+ * it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
25
+ */
10
26
  keys: {
11
27
  /**
12
28
  * The main key for the combination.
@@ -23,7 +39,7 @@ export interface KeyCombinationConfig extends ShortcutConfigBase {
23
39
  altKey?: boolean;
24
40
  shiftKey?: boolean;
25
41
  metaKey?: boolean;
26
- };
42
+ } | StandardKey;
27
43
  }
28
44
  export interface KeySequenceConfig extends ShortcutConfigBase {
29
45
  /**
@@ -81,6 +97,25 @@ export declare class Hotkeys {
81
97
  * @param enable - True to enable debug logs, false to disable.
82
98
  */
83
99
  setDebugMode(enable: boolean): void;
100
+ /**
101
+ * An Observable that emits the new context name (or null) whenever the active context changes.
102
+ * This allows external parts of the application to react to context transitions.
103
+ *
104
+ * Note: This observable benefits from the distinct check within the `setContext` method,
105
+ * meaning it will only emit when the context value actually changes.
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const hotkeys = new Hotkeys();
110
+ * const subscription = hotkeys.onContextChange$.subscribe(newContext => {
111
+ * console.log("Hotkey context changed to:", newContext);
112
+ * // Update UI or perform other actions
113
+ * });
114
+ * // To unsubscribe when no longer needed:
115
+ * // subscription.unsubscribe();
116
+ * ```
117
+ */
118
+ get onContextChange$(): Observable<string | null>;
84
119
  /**
85
120
  * Checks if a shortcut with the given ID is currently registered and active.
86
121
  * @param id - The unique ID of the shortcut to check.
@@ -90,22 +125,29 @@ export declare class Hotkeys {
90
125
  private filterByContext;
91
126
  private _registerShortcut;
92
127
  /**
93
- * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter).
94
- * The callback is triggered when the specified key and modifier keys are pressed simultaneously.
128
+ * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
129
+ * The callback is triggered when the specified key and modifier keys (if any) are pressed.
95
130
  * @param config - Configuration object for the key combination.
96
131
  * See {@link KeyCombinationConfig} for details.
97
- * The `key` property within `config.keys` must be a value from the `Keys` object.
98
- * @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid (e.g., empty key).
132
+ * The `key` property (or the direct `StandardKey` if using shorthand) must be a value from the `Keys` object.
133
+ * @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid.
99
134
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
100
135
  * @example
101
136
  * ```typescript
102
- * import { Keys } from './keys';
137
+ * import { Keys } from "./keys";
138
+ * // For Ctrl+S
103
139
  * keyManager.addCombination({
104
140
  * id: "saveFile",
105
141
  * keys: { key: Keys.S, ctrlKey: true },
106
142
  * callback: () => console.log("File saved!"),
107
143
  * context: "editor"
108
144
  * });
145
+ * // For just the Escape key
146
+ * keyManager.addCombination({
147
+ * id: "closeModal",
148
+ * keys: Keys.Escape, // Shorthand syntax
149
+ * callback: () => console.log("Modal closed!")
150
+ * });
109
151
  * ```
110
152
  */
111
153
  addCombination(config: KeyCombinationConfig): string | undefined;
@@ -120,7 +162,7 @@ export declare class Hotkeys {
120
162
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
121
163
  * @example
122
164
  * ```typescript
123
- * import { Keys } from './keys';
165
+ * import { Keys } from "./keys";
124
166
  * keyManager.addSequence({
125
167
  * id: "konamiCode",
126
168
  * sequence: [Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown, Keys.A, Keys.B],
@@ -149,7 +191,7 @@ export declare class Hotkeys {
149
191
  id: string;
150
192
  description?: string;
151
193
  context?: string | null;
152
- type: "combination" | "sequence";
194
+ type: ShortcutTypes;
153
195
  }[];
154
196
  /**
155
197
  * Cleans up all active subscriptions and resources used by the Hotkeys instance.
@@ -1 +1 @@
1
- {"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIxC,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,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,CAAC;CACL;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD;;;;;;;;OAQG;IACH,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AA6BD;;;;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;IAOnD;;;OAGG;IACI,UAAU,IAAI,MAAM,GAAG,IAAI;IAIlC;;;;OAIG;IACI,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAO1C;;;;OAIG;IACI,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIvC,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,iBAAiB;IAkBzB;;;;;;;;;;;;;;;;;;OAkBG;IACI,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS;IA+CvE;;;;;;;;;;;;;;;;;;;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,GAAG,UAAU,CAAA;KAAC,EAAE;IAa5H;;;;;OAKG;IACI,OAAO,IAAI,IAAI;CAOzB"}
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;IA6EvE;;;;;;;;;;;;;;;;;;;OAmBG;IACI,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS;IAiHjE;;;;;;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,10 @@
1
1
  import { fromEvent, BehaviorSubject, EMPTY, filter, map, bufferCount, withLatestFrom, tap, catchError, scan, } from "rxjs";
2
+ // --- Enums, Interfaces and Types ---
3
+ export var ShortcutTypes;
4
+ (function (ShortcutTypes) {
5
+ ShortcutTypes["Combination"] = "combination";
6
+ ShortcutTypes["Sequence"] = "sequence";
7
+ })(ShortcutTypes || (ShortcutTypes = {}));
2
8
  // --- Helper function to compare keys ---
3
9
  /**
4
10
  * Compares a browser event's key with a configured key.
@@ -15,6 +21,12 @@ function compareKey(eventKey, configuredKey) {
15
21
  return eventKey === configuredKey;
16
22
  }
17
23
  // --- Hotkeys Library ---
24
+ var EmitStates;
25
+ (function (EmitStates) {
26
+ EmitStates[EmitStates["Emit"] = 0] = "Emit";
27
+ EmitStates[EmitStates["Ignore"] = 1] = "Ignore";
28
+ EmitStates[EmitStates["InProgress"] = 2] = "InProgress";
29
+ })(EmitStates || (EmitStates = {}));
18
30
  /**
19
31
  * Manages keyboard shortcuts for web applications.
20
32
  * Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
@@ -36,7 +48,7 @@ export class Hotkeys {
36
48
  constructor(initialContext = null, debugMode = false) {
37
49
  this.debugMode = debugMode;
38
50
  if (typeof document === "undefined" || typeof performance === "undefined") {
39
- throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment with global 'document' and 'performance' objects.`);
51
+ throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment with global "document" and "performance" objects.`);
40
52
  }
41
53
  this.keydown$ = fromEvent(document, Hotkeys.KEYDOWN_EVENT);
42
54
  this.activeContext$ = new BehaviorSubject(initialContext);
@@ -53,8 +65,17 @@ export class Hotkeys {
53
65
  * Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
54
66
  */
55
67
  setContext(contextName) {
68
+ if (this.activeContext$.getValue() === contextName) {
69
+ if (this.debugMode) {
70
+ // Optional: Log that no change is happening, or simply do nothing.
71
+ console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
72
+ }
73
+ return; // Context is the same, so no further action is needed.
74
+ }
75
+ // If we reach here, the context is actually changing.
76
+ const oldContext = this.activeContext$.getValue(); // For more informative logging
56
77
  if (this.debugMode) {
57
- console.log(`${Hotkeys.LOG_PREFIX} Context changed to "${contextName}"`);
78
+ console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${oldContext}" to "${contextName}".`);
58
79
  }
59
80
  this.activeContext$.next(contextName);
60
81
  }
@@ -71,11 +92,38 @@ export class Hotkeys {
71
92
  * @param enable - True to enable debug logs, false to disable.
72
93
  */
73
94
  setDebugMode(enable) {
74
- this.debugMode = enable;
75
- if (this.debugMode) {
76
- console.log(`${Hotkeys.LOG_PREFIX} Debug mode ${enable ? 'enabled' : 'disabled'}.`);
95
+ if (this.debugMode === enable) { // Check if state is actually changing
96
+ return; // If no change, do nothing (no log)
97
+ }
98
+ this.debugMode = enable; // Set the new state
99
+ if (enable) { // Log based on the NEW state after a change
100
+ console.log(`${Hotkeys.LOG_PREFIX} Debug mode enabled.`);
101
+ }
102
+ else {
103
+ console.log(`${Hotkeys.LOG_PREFIX} Debug mode disabled.`);
77
104
  }
78
105
  }
106
+ /**
107
+ * An Observable that emits the new context name (or null) whenever the active context changes.
108
+ * This allows external parts of the application to react to context transitions.
109
+ *
110
+ * Note: This observable benefits from the distinct check within the `setContext` method,
111
+ * meaning it will only emit when the context value actually changes.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const hotkeys = new Hotkeys();
116
+ * const subscription = hotkeys.onContextChange$.subscribe(newContext => {
117
+ * console.log("Hotkey context changed to:", newContext);
118
+ * // Update UI or perform other actions
119
+ * });
120
+ * // To unsubscribe when no longer needed:
121
+ * // subscription.unsubscribe();
122
+ * ```
123
+ */
124
+ get onContextChange$() {
125
+ return this.activeContext$.asObservable();
126
+ }
79
127
  /**
80
128
  * Checks if a shortcut with the given ID is currently registered and active.
81
129
  * @param id - The unique ID of the shortcut to check.
@@ -87,7 +135,8 @@ export class Hotkeys {
87
135
  filterByContext(source$, context) {
88
136
  return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => context == null || context === activeCtx), map(([event, /* _activeCtx */]) => event));
89
137
  }
90
- _registerShortcut(config, subscription, type, detailsForLog) {
138
+ _registerShortcut(config, subscription, type, // Changed to use Enum
139
+ detailsForLog) {
91
140
  const existingShortcut = this.activeShortcuts.get(config.id);
92
141
  if (existingShortcut) {
93
142
  console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
@@ -100,35 +149,81 @@ export class Hotkeys {
100
149
  return config.id;
101
150
  }
102
151
  /**
103
- * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter).
104
- * The callback is triggered when the specified key and modifier keys are pressed simultaneously.
152
+ * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
153
+ * The callback is triggered when the specified key and modifier keys (if any) are pressed.
105
154
  * @param config - Configuration object for the key combination.
106
155
  * See {@link KeyCombinationConfig} for details.
107
- * The `key` property within `config.keys` must be a value from the `Keys` object.
108
- * @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid (e.g., empty key).
156
+ * The `key` property (or the direct `StandardKey` if using shorthand) must be a value from the `Keys` object.
157
+ * @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid.
109
158
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
110
159
  * @example
111
160
  * ```typescript
112
- * import { Keys } from './keys';
161
+ * import { Keys } from "./keys";
162
+ * // For Ctrl+S
113
163
  * keyManager.addCombination({
114
164
  * id: "saveFile",
115
165
  * keys: { key: Keys.S, ctrlKey: true },
116
166
  * callback: () => console.log("File saved!"),
117
167
  * context: "editor"
118
168
  * });
169
+ * // For just the Escape key
170
+ * keyManager.addCombination({
171
+ * id: "closeModal",
172
+ * keys: Keys.Escape, // Shorthand syntax
173
+ * callback: () => console.log("Modal closed!")
174
+ * });
119
175
  * ```
120
176
  */
121
177
  addCombination(config) {
122
178
  const { keys, callback, context, preventDefault = false, id } = config;
123
- if (!keys || !keys.key || typeof keys.key !== 'string' || keys.key.trim() === '') {
124
- console.warn(`${Hotkeys.LOG_PREFIX} Invalid 'keys.key' for combination shortcut "${id}". Key must be a non-empty value from Keys. Shortcut not added.`);
125
- return undefined;
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
+ if (keys.trim() === "") {
188
+ console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys" (shorthand) for combination shortcut "${id}". Key string must not be empty. Shortcut not added.`);
189
+ return undefined;
190
+ }
191
+ configuredMainKey = keys;
192
+ ctrlKeyConfig = false; // Shorthand implies no modifiers
193
+ altKeyConfig = false;
194
+ shiftKeyConfig = false;
195
+ metaKeyConfig = false;
196
+ keyDetailsForLog = `key: "${keys}" (no modifiers implied)`;
197
+ }
198
+ else {
199
+ // Object form
200
+ if (!keys || !keys.key || typeof keys.key !== "string" || keys.key.trim() === "") {
201
+ console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys.key" for combination shortcut "${id}". Key must be a non-empty value from Keys. Shortcut not added.`);
202
+ return undefined;
203
+ }
204
+ configuredMainKey = keys.key;
205
+ ctrlKeyConfig = keys.ctrlKey;
206
+ altKeyConfig = keys.altKey;
207
+ shiftKeyConfig = keys.shiftKey;
208
+ metaKeyConfig = keys.metaKey;
209
+ keyDetailsForLog = `key: "${keys.key}"` +
210
+ (keys.ctrlKey !== undefined ? `, ctrlKey: ${keys.ctrlKey}` : "") +
211
+ (keys.altKey !== undefined ? `, altKey: ${keys.altKey}` : "") +
212
+ (keys.shiftKey !== undefined ? `, shiftKey: ${keys.shiftKey}` : "") +
213
+ (keys.metaKey !== undefined ? `, metaKey: ${keys.metaKey}` : "");
126
214
  }
127
- const configuredMainKey = keys.key;
128
- const shortcut$ = this.filterByContext(this.keydown$, context).pipe(filter(event => (keys.ctrlKey === undefined || event.ctrlKey === keys.ctrlKey) &&
129
- (keys.altKey === undefined || event.altKey === keys.altKey) &&
130
- (keys.shiftKey === undefined || event.shiftKey === keys.shiftKey) &&
131
- (keys.metaKey === undefined || event.metaKey === keys.metaKey)), filter(event => compareKey(event.key, configuredMainKey)), tap(event => {
215
+ const shortcut$ = this.filterByContext(this.keydown$, context).pipe(filter(event => {
216
+ // For shorthand (where modifiers are explicitly false), we want an exact match.
217
+ // For object form:
218
+ // - if modifier is true, event.modifier must be true.
219
+ // - if modifier is false, event.modifier must be false.
220
+ // - if modifier is undefined, we don't care about event.modifier.
221
+ const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
222
+ const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
223
+ const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
224
+ const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
225
+ return ctrlMatch && altMatch && shiftMatch && metaMatch;
226
+ }), filter(event => compareKey(event.key, configuredMainKey)), tap(event => {
132
227
  if (this.debugMode) {
133
228
  const preventAction = preventDefault ? ", preventing default" : "";
134
229
  console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered${preventAction}.`);
@@ -147,12 +242,7 @@ export class Hotkeys {
147
242
  console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for combination shortcut "${id}":`, e);
148
243
  }
149
244
  });
150
- const keyDetails = `key: "${keys.key}"` +
151
- (keys.ctrlKey !== undefined ? `, ctrlKey: ${keys.ctrlKey}` : "") +
152
- (keys.altKey !== undefined ? `, altKey: ${keys.altKey}` : "") +
153
- (keys.shiftKey !== undefined ? `, shiftKey: ${keys.shiftKey}` : "") +
154
- (keys.metaKey !== undefined ? `, metaKey: ${keys.metaKey}` : "");
155
- return this._registerShortcut(config, subscription, "Combination", `Keys: { ${keyDetails} }`);
245
+ return this._registerShortcut(config, subscription, ShortcutTypes.Combination, `Keys: { ${keyDetailsForLog} }`); // Use Enum
156
246
  }
157
247
  /**
158
248
  * Registers a key sequence shortcut (e.g., g -> i, or ArrowUp -> ArrowUp -> ArrowDown).
@@ -165,7 +255,7 @@ export class Hotkeys {
165
255
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
166
256
  * @example
167
257
  * ```typescript
168
- * import { Keys } from './keys';
258
+ * import { Keys } from "./keys";
169
259
  * keyManager.addSequence({
170
260
  * id: "konamiCode",
171
261
  * sequence: [Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown, Keys.A, Keys.B],
@@ -180,7 +270,7 @@ export class Hotkeys {
180
270
  console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty or invalid. Shortcut not added.`);
181
271
  return undefined;
182
272
  }
183
- if (sequence.some(key => typeof key !== 'string' || key.trim() === '')) {
273
+ if (sequence.some(key => typeof key !== "string" || key.trim() === "")) {
184
274
  console.warn(`${Hotkeys.LOG_PREFIX} Invalid key in sequence for shortcut "${id}". All keys must be non-empty strings from Keys. Shortcut not added.`);
185
275
  return undefined;
186
276
  }
@@ -192,48 +282,52 @@ export class Hotkeys {
192
282
  shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
193
283
  let { matchedEvents, lastEventTime } = acc;
194
284
  const currentTime = performance.now();
195
- if (acc.emitState === 'emit') {
285
+ if (acc.emitState === EmitStates.Emit) {
196
286
  matchedEvents = [];
197
287
  lastEventTime = 0;
198
288
  }
199
289
  if (matchedEvents.length > 0 && (currentTime - lastEventTime > sequenceTimeoutMs)) {
200
290
  if (this.debugMode) {
201
- console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) attempt timed out. Matched: ${matchedEvents.map(e => e.key).join(',')}. Resetting.`);
291
+ console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) attempt timed out. Matched: ${matchedEvents.map(e => e.key).join(",")}. Resetting.`);
202
292
  }
203
293
  matchedEvents = [];
204
294
  }
205
295
  const nextExpectedKeyIndex = matchedEvents.length;
206
296
  if (nextExpectedKeyIndex >= sequenceLength) {
297
+ // Sequence was already emitted or buffer is too long (should not happen if reset correctly)
298
+ // Start new sequence if current key matches the first key of the sequence
207
299
  if (sequenceLength > 0 && compareKey(event.key, configuredSequence[0])) {
208
- return { matchedEvents: [event], lastEventTime: currentTime, emitState: 'in-progress' };
300
+ return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
209
301
  }
210
- return { matchedEvents: [], lastEventTime: 0, emitState: 'ignore' };
302
+ return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
211
303
  }
212
304
  if (compareKey(event.key, configuredSequence[nextExpectedKeyIndex])) {
213
305
  const newMatchedEvents = [...matchedEvents, event];
214
306
  if (newMatchedEvents.length === sequenceLength) {
215
- if (this.debugMode)
307
+ if (this.debugMode && !acc.emitState)
216
308
  console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) matched.`);
217
- return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: 'emit' };
309
+ return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.Emit };
218
310
  }
219
311
  else {
220
- return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: 'in-progress' };
312
+ return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.InProgress };
221
313
  }
222
314
  }
223
315
  else {
316
+ // If current key breaks sequence, check if it starts a new sequence
224
317
  if (matchedEvents.length > 0 && this.debugMode) {
225
- console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) broken by key "${event.key}". Matched: ${matchedEvents.map(e => e.key).join(',')}. Resetting.`);
318
+ console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) broken by key "${event.key}". Matched: ${matchedEvents.map(e => e.key).join(",")}. Resetting.`);
226
319
  }
227
320
  if (sequenceLength > 0 && compareKey(event.key, configuredSequence[0])) {
228
- return { matchedEvents: [event], lastEventTime: currentTime, emitState: 'in-progress' };
321
+ return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
229
322
  }
230
323
  else {
231
- return { matchedEvents: [], lastEventTime: 0, emitState: 'ignore' };
324
+ return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
232
325
  }
233
326
  }
234
- }, { matchedEvents: [], lastEventTime: 0, emitState: 'ignore' }), filter(state => state.emitState === 'emit'), map(state => state.matchedEvents));
327
+ }, { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore }), filter(state => state.emitState === EmitStates.Emit), map(state => state.matchedEvents));
235
328
  }
236
329
  else {
330
+ // No timeout logic: simple buffer-based matching
237
331
  shortcut$ = baseKeydownStream$.pipe(bufferCount(sequenceLength, 1), filter((events) => {
238
332
  if (events.length < sequenceLength)
239
333
  return false;
@@ -255,6 +349,7 @@ export class Hotkeys {
255
349
  }));
256
350
  const subscription = finalShortcut$.subscribe((events) => {
257
351
  try {
352
+ // Ensure callback receives the last event of the sequence, similar to combination.
258
353
  if (events.length > 0)
259
354
  callback(events[events.length - 1]);
260
355
  }
@@ -262,8 +357,8 @@ export class Hotkeys {
262
357
  console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for sequence shortcut "${id}":`, e);
263
358
  }
264
359
  });
265
- const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ''}`;
266
- return this._registerShortcut(config, subscription, "Sequence", logDetails);
360
+ const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ""}`;
361
+ return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails); // Use Enum
267
362
  }
268
363
  /**
269
364
  * Removes a registered shortcut by its ID.
@@ -298,7 +393,7 @@ export class Hotkeys {
298
393
  id,
299
394
  description: activeShortcut.config.description,
300
395
  context: activeShortcut.config.context,
301
- type: 'keys' in activeShortcut.config ? "combination" : "sequence"
396
+ type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination // Use Enum values
302
397
  });
303
398
  }
304
399
  return shortcuts;
@@ -1,8 +1,6 @@
1
1
  import { describe, it, before, beforeEach, afterEach, mock } from "node:test";
2
2
  import assert from "node:assert";
3
- // Importing main library components
4
- import { Hotkeys } from "./hotkeys.js";
5
- // Importing Keys and StandardKey from the separate keys.js file
3
+ import { Hotkeys, ShortcutTypes } from "./hotkeys.js";
6
4
  import { Keys } from "./keys.js";
7
5
  import { fromEvent, BehaviorSubject } from "rxjs";
8
6
  import { createMockFn, dispatchKeyEvent } from "./testutils.js";
@@ -27,14 +25,14 @@ before(() => {
27
25
  // @ts-ignore
28
26
  global.fromEvent = fromEvent;
29
27
  // @ts-ignore
30
- if (typeof global.performance === 'undefined') {
28
+ if (typeof global.performance === "undefined") {
31
29
  // @ts-ignore
32
30
  global.performance = {};
33
31
  }
34
32
  // @ts-ignore
35
33
  originalPerformanceNow = global.performance.now;
36
34
  // @ts-ignore
37
- if (typeof global.performance.now !== 'function') {
35
+ if (typeof global.performance.now !== "function") {
38
36
  // @ts-ignore
39
37
  global.performance.now = (() => {
40
38
  const start = Date.now();
@@ -70,35 +68,135 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
70
68
  global.performance.now = originalPerformanceNow;
71
69
  }
72
70
  });
73
- describe("Initialization and Basic Context", () => {
71
+ // ... (Initialization and Basic Context tests remain the same) ...
72
+ describe("Initialization and Context Management", () => {
74
73
  it("should initialize without errors", () => {
75
74
  assert(keyManager instanceof Hotkeys);
75
+ assert.strictEqual(keyManager.getContext(), null); // Default initial context
76
76
  });
77
77
  it("should initialize with a null context by default", () => {
78
78
  assert.strictEqual(keyManager.getContext(), null);
79
79
  });
80
- it("should initialize with a given initial context", () => {
81
- const manager = new Hotkeys("editor");
80
+ it("should initialize with a given initial context (debug off)", () => {
81
+ const manager = new Hotkeys("editor", false);
82
82
  assert.strictEqual(manager.getContext(), "editor");
83
83
  manager.destroy();
84
84
  });
85
- it("should set and get context", () => {
85
+ it("should log library initialization with context in debug mode", () => {
86
+ const consoleLogMock = mock.method(console, "log");
87
+ const manager = new Hotkeys("debugInitCtx", true);
88
+ const initLog = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`Library initialized. Initial context: "debugInitCtx"`));
89
+ assert.ok(initLog, "Library initialization log not found or incorrect.");
90
+ manager.destroy();
91
+ consoleLogMock.mock.restore();
92
+ });
93
+ it("should set and get context correctly", () => {
86
94
  keyManager.setContext("modal");
87
95
  assert.strictEqual(keyManager.getContext(), "modal");
88
96
  keyManager.setContext(null);
89
97
  assert.strictEqual(keyManager.getContext(), null);
90
98
  });
91
- it("should toggle debug mode and log appropriately", () => {
99
+ it("should log context change correctly when context is different (debug mode on)", () => {
92
100
  const consoleLogMock = mock.method(console, "log");
93
101
  keyManager.setDebugMode(true);
102
+ // Initial context is null for keyManager
103
+ consoleLogMock.mock.resetCalls(); // Clear "Debug mode enabled" log
94
104
  keyManager.setContext("debug_test");
95
- assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes('Context changed to "debug_test"')));
105
+ assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Context changed from "null" to "debug_test"`)), "Log for context change from null incorrect.");
106
+ assert.strictEqual(keyManager.getContext(), "debug_test");
107
+ consoleLogMock.mock.resetCalls();
108
+ keyManager.setContext("another_test");
109
+ assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Context changed from "debug_test" to "another_test"`)), "Log for context change between non-null incorrect.");
110
+ assert.strictEqual(keyManager.getContext(), "another_test");
111
+ consoleLogMock.mock.restore();
112
+ });
113
+ it("should log context change from non-null to null (debug mode on)", () => {
114
+ const consoleLogMock = mock.method(console, "log");
115
+ keyManager.setContext("fromCtx"); // Initial context
116
+ keyManager.setDebugMode(true);
117
+ consoleLogMock.mock.resetCalls();
118
+ keyManager.setContext(null);
119
+ const logCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`Context changed from "fromCtx" to "null"`));
120
+ assert.ok(logCall, "Context change to null log not found or incorrect.");
121
+ assert.strictEqual(keyManager.getContext(), null);
122
+ consoleLogMock.mock.restore();
123
+ });
124
+ it(`should not call activeContext$.next and log "no change" if context is set to the same value (debug mode on)`, () => {
125
+ keyManager.setContext("sameCtx"); // Set initial context
126
+ const consoleLogMock = mock.method(console, "log");
127
+ keyManager.setDebugMode(true); // Enable debug for this test
128
+ consoleLogMock.mock.resetCalls(); // Clear "Debug mode enabled" log
129
+ // @ts-ignore: Accessing private member for test
130
+ const activeContextNextSpy = mock.method(keyManager["activeContext$"], "next");
131
+ keyManager.setContext("sameCtx"); // Attempt to set the same context
132
+ const noChangeLogCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`setContext called with the same context "sameCtx". No change made.`));
133
+ assert.ok(noChangeLogCall, "No-change log not found or incorrect for same context.");
134
+ const changedLogCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`Context changed from`));
135
+ assert.strictEqual(changedLogCall, undefined, "Context changed log should not appear for same context.");
136
+ assert.strictEqual(keyManager.getContext(), "sameCtx");
137
+ assert.strictEqual(activeContextNextSpy.mock.callCount(), 0, "activeContext$.next should not have been called.");
138
+ activeContextNextSpy.mock.restore();
139
+ consoleLogMock.mock.restore();
140
+ });
141
+ it("should not log or call next if context is set to the same value (debug mode off)", () => {
142
+ keyManager.setContext("sameCtxNoDebug"); // Set initial context
143
+ keyManager.setDebugMode(false);
144
+ // Ensure debug is off
145
+ const consoleLogMock = mock.method(console, "log");
146
+ // @ts-ignore: Accessing private member for test
147
+ const activeContextNextSpy = mock.method(keyManager["activeContext$"], "next");
148
+ keyManager.setContext("sameCtxNoDebug"); // Attempt to set the same context
149
+ assert.strictEqual(consoleLogMock.mock.callCount(), 0, "Console.log should not have been called with debug mode off.");
150
+ assert.strictEqual(activeContextNextSpy.mock.callCount(), 0, "activeContext$.next should not have been called.");
151
+ assert.strictEqual(keyManager.getContext(), "sameCtxNoDebug");
152
+ activeContextNextSpy.mock.restore();
153
+ consoleLogMock.mock.restore();
154
+ });
155
+ it("should toggle debug mode and log its state", () => {
156
+ const consoleLogMock = mock.method(console, "log");
157
+ keyManager.setDebugMode(true);
158
+ assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes("Debug mode enabled")));
96
159
  consoleLogMock.mock.resetCalls();
97
160
  keyManager.setDebugMode(false);
98
- keyManager.setContext("no_debug_test");
99
- assert.ok(!consoleLogMock.mock.calls.some(call => call.arguments[0].includes('Context changed to "no_debug_test"')));
161
+ console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>", consoleLogMock.mock.calls);
162
+ assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes("Debug mode disabled")));
100
163
  consoleLogMock.mock.restore();
101
164
  });
165
+ describe("onContextChange$ observable", () => {
166
+ it("should emit initial context to new subscriber", () => {
167
+ const manager = new Hotkeys("initial", false);
168
+ const spy = createMockFn();
169
+ const sub = manager.onContextChange$.subscribe(spy);
170
+ assert.strictEqual(spy.calledCount, 1);
171
+ assert.strictEqual(spy.lastArgs[0], "initial");
172
+ sub.unsubscribe();
173
+ manager.destroy();
174
+ });
175
+ it("should emit when context changes", () => {
176
+ const spy = createMockFn();
177
+ const sub = keyManager.onContextChange$.subscribe(spy); // Subscribes, gets initial null
178
+ spy.mockClear(); // Clear initial emission
179
+ keyManager.setContext("newContext");
180
+ assert.strictEqual(spy.calledCount, 1);
181
+ assert.strictEqual(spy.lastArgs[0], "newContext");
182
+ keyManager.setContext("anotherContext");
183
+ assert.strictEqual(spy.calledCount, 2);
184
+ assert.strictEqual(spy.lastArgs[0], "anotherContext");
185
+ keyManager.setContext(null);
186
+ assert.strictEqual(spy.calledCount, 3);
187
+ assert.strictEqual(spy.lastArgs[0], null);
188
+ sub.unsubscribe();
189
+ });
190
+ it("should not emit if context is set to the same value", () => {
191
+ keyManager.setContext("testContext");
192
+ const spy = createMockFn();
193
+ const sub = keyManager.onContextChange$.subscribe(spy); // Subscribes, gets "testContext"
194
+ spy.mockClear(); // Clear initial emission
195
+ keyManager.setContext("testContext"); // Set same context
196
+ assert.strictEqual(spy.calledCount, 0, "Observable should not emit if context value is the same.");
197
+ sub.unsubscribe();
198
+ });
199
+ });
102
200
  });
103
201
  describe("addCombination", () => {
104
202
  it("should trigger callback for a simple key combination (e.g., 'A')", () => {
@@ -111,13 +209,12 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
111
209
  dispatchKeyEvent("A");
112
210
  assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'A' not called");
113
211
  });
114
- it("should return undefined and warn if keys.key is null or undefined (runtime check)", () => {
115
- // This test checks runtime robustness if `any` is used to bypass StandardKey
212
+ it("should return undefined and warn if keys.key is null or undefined (runtime check in object form)", () => {
116
213
  const config = { id: "nullKey", keys: { key: null }, callback: mockCallback };
117
214
  const result = keyManager.addCombination(config);
118
215
  assert.strictEqual(result, undefined, "Should return undefined for null key");
119
216
  assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
120
- assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes('Invalid \'keys.key\' for combination shortcut "nullKey"'));
217
+ assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "keys.key" for combination shortcut "nullKey"`));
121
218
  });
122
219
  it("should pass the KeyboardEvent to the callback", () => {
123
220
  const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
@@ -140,14 +237,14 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
140
237
  dispatchKeyEvent("a", { ctrlKey: false });
141
238
  assert.strictEqual(mockCallback.calledCount, 1);
142
239
  });
143
- it("should trigger for special keys like Escape", () => {
144
- const config = { id: "escapeKey", keys: { key: Keys.Escape }, callback: mockCallback };
240
+ it("should trigger for special keys like Escape (object form)", () => {
241
+ const config = { id: "escapeKeyObj", keys: { key: Keys.Escape }, callback: mockCallback };
145
242
  keyManager.addCombination(config);
146
243
  dispatchKeyEvent("Escape"); // Event key matches Keys.Escape
147
244
  assert.strictEqual(mockCallback.calledCount, 1);
148
245
  });
149
- it("should handle preventDefault correctly", () => {
150
- const config = { id: "preventA", keys: { key: Keys.A }, callback: mockCallback, preventDefault: true };
246
+ it("should handle preventDefault correctly (object form)", () => {
247
+ const config = { id: "preventAObj", keys: { key: Keys.A }, callback: mockCallback, preventDefault: true };
151
248
  keyManager.addCombination(config);
152
249
  const event = dispatchKeyEvent("a");
153
250
  assert.strictEqual(mockCallback.calledCount, 1);
@@ -173,25 +270,92 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
173
270
  keyManager.addCombination({ id: "workingCombo", keys: { key: Keys.W }, callback: workingCallback });
174
271
  dispatchKeyEvent("e");
175
272
  assert.strictEqual(consoleErrorMock.mock.calls.length, 1);
176
- assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes('Error in user callback for combination shortcut "errorCombo"'));
273
+ assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(`Error in user callback for combination shortcut "errorCombo"`));
177
274
  dispatchKeyEvent("w");
178
275
  assert.strictEqual(workingCallback.calledCount, 1);
179
276
  });
277
+ describe("addCombination - Shorthand Syntax", () => {
278
+ it("should trigger callback for a simple key using shorthand (e.g., Keys.X)", () => {
279
+ const config = { id: "shorthandX", keys: Keys.X, callback: mockCallback };
280
+ keyManager.addCombination(config);
281
+ dispatchKeyEvent(Keys.X.toLowerCase());
282
+ assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'x' (shorthand) not called");
283
+ mockCallback.mockClear();
284
+ dispatchKeyEvent(Keys.X);
285
+ assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'X' (shorthand) not called");
286
+ });
287
+ it("should NOT trigger callback for shorthand if modifier is pressed", () => {
288
+ const config = { id: "shorthandY", keys: Keys.Y, callback: mockCallback };
289
+ keyManager.addCombination(config);
290
+ dispatchKeyEvent(Keys.Y, { ctrlKey: true });
291
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Ctrl");
292
+ mockCallback.mockClear();
293
+ dispatchKeyEvent(Keys.Y, { altKey: true });
294
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Alt");
295
+ mockCallback.mockClear();
296
+ dispatchKeyEvent(Keys.Y, { shiftKey: true });
297
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Shift");
298
+ mockCallback.mockClear();
299
+ dispatchKeyEvent(Keys.Y, { metaKey: true });
300
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Meta");
301
+ });
302
+ it("should trigger callback for shorthand if ONLY the key is pressed (no modifiers)", () => {
303
+ const config = { id: "shorthandZ", keys: Keys.Z, callback: mockCallback };
304
+ keyManager.addCombination(config);
305
+ dispatchKeyEvent(Keys.Z, { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false });
306
+ assert.strictEqual(mockCallback.calledCount, 1);
307
+ });
308
+ it("should handle preventDefault correctly for shorthand", () => {
309
+ const config = { id: "shorthandPrevent", keys: Keys.P, callback: mockCallback, preventDefault: true };
310
+ keyManager.addCombination(config);
311
+ const event = dispatchKeyEvent(Keys.P.toLowerCase());
312
+ assert.strictEqual(mockCallback.calledCount, 1);
313
+ assert.strictEqual(event.defaultPrevented, true);
314
+ });
315
+ it("should respect context for shorthand", () => {
316
+ const config = { id: "shorthandContext", keys: Keys.C, callback: mockCallback, context: "editor" };
317
+ keyManager.addCombination(config);
318
+ keyManager.setContext("other");
319
+ dispatchKeyEvent(Keys.C.toLowerCase());
320
+ assert.strictEqual(mockCallback.calledCount, 0);
321
+ keyManager.setContext("editor");
322
+ dispatchKeyEvent(Keys.C.toLowerCase());
323
+ assert.strictEqual(mockCallback.calledCount, 1);
324
+ });
325
+ it("should return undefined and warn if shorthand key is an empty string", () => {
326
+ const config = { id: "emptyShorthand", keys: "", callback: mockCallback };
327
+ const result = keyManager.addCombination(config);
328
+ assert.strictEqual(result, undefined);
329
+ assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
330
+ assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "keys" (shorthand) for combination shortcut "emptyShorthand"`));
331
+ });
332
+ it("should correctly log shorthand key details in debug mode", () => {
333
+ const consoleLogMock = mock.method(console, "log");
334
+ keyManager.setDebugMode(true);
335
+ const config = { id: "debugShorthand", keys: Keys.D, callback: mockCallback };
336
+ keyManager.addCombination(config);
337
+ const logMessage = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`combination shortcut "debugShorthand" added`));
338
+ 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]}`);
340
+ consoleLogMock.mock.restore();
341
+ keyManager.setDebugMode(false);
342
+ });
343
+ });
180
344
  });
181
345
  describe("addSequence", () => {
182
346
  it("should trigger callback for a simple key sequence", () => {
183
347
  const config = { id: "seqGI", sequence: [Keys.G, Keys.I], callback: mockCallback };
184
348
  const result = keyManager.addSequence(config);
185
349
  assert.strictEqual(result, "seqGI");
186
- dispatchKeyEvent("g"); // Dispatch 'g' (lowercase)
187
- dispatchKeyEvent("i"); // Dispatch 'i' (lowercase)
350
+ dispatchKeyEvent("g"); // Dispatch "g" (lowercase)
351
+ dispatchKeyEvent("i"); // Dispatch "i" (lowercase)
188
352
  assert.strictEqual(mockCallback.calledCount, 1);
189
353
  });
190
354
  it("should trigger callback for Konami code using Keys", () => {
191
355
  const konamiSequence = [
192
356
  Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown,
193
357
  Keys.ArrowLeft, Keys.ArrowRight, Keys.ArrowLeft, Keys.ArrowRight,
194
- Keys.B, Keys.A // Using 'B' and 'A' from Keys
358
+ Keys.B, Keys.A // Using "B" and "A" from Keys
195
359
  ];
196
360
  const config = { id: "konami", sequence: konamiSequence, callback: mockCallback };
197
361
  keyManager.addSequence(config);
@@ -236,7 +400,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
236
400
  dispatchKeyEvent("e");
237
401
  dispatchKeyEvent("s");
238
402
  assert.strictEqual(consoleErrorMock.mock.calls.length, 1);
239
- assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes('Error in user callback for sequence shortcut "errorSeq"'));
403
+ assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(`Error in user callback for sequence shortcut "errorSeq"`));
240
404
  });
241
405
  describe("Sequence Contextual Triggering", () => {
242
406
  let editorSequenceConfig;
@@ -317,17 +481,17 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
317
481
  });
318
482
  });
319
483
  describe("getActiveShortcuts", () => {
320
- it("should return active combination and sequence shortcuts", () => {
484
+ it("should return active combination and sequence shortcuts with enum types", () => {
321
485
  keyManager.addCombination({ id: "combo1", keys: { key: Keys.A }, callback: createMockFn(), description: "Test A" });
322
486
  keyManager.addSequence({ id: "seq1", sequence: [Keys.B, Keys.C], callback: createMockFn(), context: "modal", description: "Test BC" });
323
487
  const active = keyManager.getActiveShortcuts();
324
488
  assert.strictEqual(active.length, 2);
325
489
  const combo = active.find(s => s.id === "combo1");
326
490
  assert.ok(combo);
327
- assert.strictEqual(combo.type, "combination");
491
+ assert.strictEqual(combo.type, ShortcutTypes.Combination); // Use Enum for comparison
328
492
  const seq = active.find(s => s.id === "seq1");
329
493
  assert.ok(seq);
330
- assert.strictEqual(seq.type, "sequence");
494
+ assert.strictEqual(seq.type, ShortcutTypes.Sequence); // Use Enum for comparison
331
495
  });
332
496
  });
333
497
  describe("hasShortcut", () => {
@@ -347,10 +511,10 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
347
511
  keyManager.addCombination({ id: "destroyTestCombo", keys: { key: Keys.D }, callback: mockCallback });
348
512
  keyManager.addSequence({ id: "destroyTestSeq", sequence: [Keys.X, Keys.Y], callback: mockCallback });
349
513
  // @ts-ignore
350
- assert.strictEqual(keyManager['activeShortcuts'].size, 2);
514
+ assert.strictEqual(keyManager["activeShortcuts"].size, 2);
351
515
  keyManager.destroy();
352
516
  // @ts-ignore
353
- assert.strictEqual(keyManager['activeShortcuts'].size, 0);
517
+ assert.strictEqual(keyManager["activeShortcuts"].size, 0);
354
518
  dispatchKeyEvent(Keys.D);
355
519
  dispatchKeyEvent(Keys.X);
356
520
  dispatchKeyEvent(Keys.Y);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rx-hotkeys",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsc",