rx-hotkeys 2.0.0 → 2.1.1

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,8 @@
1
1
  import { StandardKey } from "./keys.js";
2
+ export declare enum ShortcutTypes {
3
+ Combination = "combination",
4
+ Sequence = "sequence"
5
+ }
2
6
  interface ShortcutConfigBase {
3
7
  id: string;
4
8
  callback: (event?: KeyboardEvent) => void;
@@ -7,6 +11,17 @@ interface ShortcutConfigBase {
7
11
  description?: string;
8
12
  }
9
13
  export interface KeyCombinationConfig extends ShortcutConfigBase {
14
+ /**
15
+ * Defines the key or key combination.
16
+ * Can be an object specifying the main `key` (from `StandardKey`) and optional
17
+ * modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
18
+ * Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
19
+ *
20
+ * Alternatively, for a simple key press without any modifiers, this can be
21
+ * a `StandardKey` directly.
22
+ * Example: `Keys.Escape` for the Escape key. When using this shorthand,
23
+ * it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
24
+ */
10
25
  keys: {
11
26
  /**
12
27
  * The main key for the combination.
@@ -23,7 +38,7 @@ export interface KeyCombinationConfig extends ShortcutConfigBase {
23
38
  altKey?: boolean;
24
39
  shiftKey?: boolean;
25
40
  metaKey?: boolean;
26
- };
41
+ } | StandardKey;
27
42
  }
28
43
  export interface KeySequenceConfig extends ShortcutConfigBase {
29
44
  /**
@@ -90,22 +105,29 @@ export declare class Hotkeys {
90
105
  private filterByContext;
91
106
  private _registerShortcut;
92
107
  /**
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.
108
+ * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
109
+ * The callback is triggered when the specified key and modifier keys (if any) are pressed.
95
110
  * @param config - Configuration object for the key combination.
96
111
  * 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).
112
+ * The `key` property (or the direct `StandardKey` if using shorthand) must be a value from the `Keys` object.
113
+ * @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid.
99
114
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
100
115
  * @example
101
116
  * ```typescript
102
- * import { Keys } from './keys';
117
+ * import { Keys } from "./keys";
118
+ * // For Ctrl+S
103
119
  * keyManager.addCombination({
104
120
  * id: "saveFile",
105
121
  * keys: { key: Keys.S, ctrlKey: true },
106
122
  * callback: () => console.log("File saved!"),
107
123
  * context: "editor"
108
124
  * });
125
+ * // For just the Escape key
126
+ * keyManager.addCombination({
127
+ * id: "closeModal",
128
+ * keys: Keys.Escape, // Shorthand syntax
129
+ * callback: () => console.log("Modal closed!")
130
+ * });
109
131
  * ```
110
132
  */
111
133
  addCombination(config: KeyCombinationConfig): string | undefined;
@@ -120,7 +142,7 @@ export declare class Hotkeys {
120
142
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
121
143
  * @example
122
144
  * ```typescript
123
- * import { Keys } from './keys';
145
+ * import { Keys } from "./keys";
124
146
  * keyManager.addSequence({
125
147
  * id: "konamiCode",
126
148
  * sequence: [Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown, Keys.A, Keys.B],
@@ -149,7 +171,7 @@ export declare class Hotkeys {
149
171
  id: string;
150
172
  description?: string;
151
173
  context?: string | null;
152
- type: "combination" | "sequence";
174
+ type: ShortcutTypes;
153
175
  }[];
154
176
  /**
155
177
  * 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":"AAIA,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;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;;;;;;;;;;;;;;;;;;;;;;;;;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);
@@ -73,7 +85,7 @@ export class Hotkeys {
73
85
  setDebugMode(enable) {
74
86
  this.debugMode = enable;
75
87
  if (this.debugMode) {
76
- console.log(`${Hotkeys.LOG_PREFIX} Debug mode ${enable ? 'enabled' : 'disabled'}.`);
88
+ console.log(`${Hotkeys.LOG_PREFIX} Debug mode ${enable ? "enabled" : "disabled"}.`);
77
89
  }
78
90
  }
79
91
  /**
@@ -87,7 +99,8 @@ export class Hotkeys {
87
99
  filterByContext(source$, context) {
88
100
  return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => context == null || context === activeCtx), map(([event, /* _activeCtx */]) => event));
89
101
  }
90
- _registerShortcut(config, subscription, type, detailsForLog) {
102
+ _registerShortcut(config, subscription, type, // Changed to use Enum
103
+ detailsForLog) {
91
104
  const existingShortcut = this.activeShortcuts.get(config.id);
92
105
  if (existingShortcut) {
93
106
  console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
@@ -100,35 +113,81 @@ export class Hotkeys {
100
113
  return config.id;
101
114
  }
102
115
  /**
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.
116
+ * Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
117
+ * The callback is triggered when the specified key and modifier keys (if any) are pressed.
105
118
  * @param config - Configuration object for the key combination.
106
119
  * 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).
120
+ * The `key` property (or the direct `StandardKey` if using shorthand) must be a value from the `Keys` object.
121
+ * @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid.
109
122
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
110
123
  * @example
111
124
  * ```typescript
112
- * import { Keys } from './keys';
125
+ * import { Keys } from "./keys";
126
+ * // For Ctrl+S
113
127
  * keyManager.addCombination({
114
128
  * id: "saveFile",
115
129
  * keys: { key: Keys.S, ctrlKey: true },
116
130
  * callback: () => console.log("File saved!"),
117
131
  * context: "editor"
118
132
  * });
133
+ * // For just the Escape key
134
+ * keyManager.addCombination({
135
+ * id: "closeModal",
136
+ * keys: Keys.Escape, // Shorthand syntax
137
+ * callback: () => console.log("Modal closed!")
138
+ * });
119
139
  * ```
120
140
  */
121
141
  addCombination(config) {
122
142
  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;
143
+ let configuredMainKey;
144
+ let ctrlKeyConfig;
145
+ let altKeyConfig;
146
+ let shiftKeyConfig;
147
+ let metaKeyConfig;
148
+ let keyDetailsForLog;
149
+ if (typeof keys === "string") {
150
+ // Shorthand: keys is StandardKey, implying no modifiers should be active
151
+ if (keys.trim() === "") {
152
+ console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys" (shorthand) for combination shortcut "${id}". Key string must not be empty. Shortcut not added.`);
153
+ return undefined;
154
+ }
155
+ configuredMainKey = keys;
156
+ ctrlKeyConfig = false; // Shorthand implies no modifiers
157
+ altKeyConfig = false;
158
+ shiftKeyConfig = false;
159
+ metaKeyConfig = false;
160
+ keyDetailsForLog = `key: "${keys}" (no modifiers implied)`;
161
+ }
162
+ else {
163
+ // Object form
164
+ if (!keys || !keys.key || typeof keys.key !== "string" || keys.key.trim() === "") {
165
+ console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys.key" for combination shortcut "${id}". Key must be a non-empty value from Keys. Shortcut not added.`);
166
+ return undefined;
167
+ }
168
+ configuredMainKey = keys.key;
169
+ ctrlKeyConfig = keys.ctrlKey;
170
+ altKeyConfig = keys.altKey;
171
+ shiftKeyConfig = keys.shiftKey;
172
+ metaKeyConfig = keys.metaKey;
173
+ keyDetailsForLog = `key: "${keys.key}"` +
174
+ (keys.ctrlKey !== undefined ? `, ctrlKey: ${keys.ctrlKey}` : "") +
175
+ (keys.altKey !== undefined ? `, altKey: ${keys.altKey}` : "") +
176
+ (keys.shiftKey !== undefined ? `, shiftKey: ${keys.shiftKey}` : "") +
177
+ (keys.metaKey !== undefined ? `, metaKey: ${keys.metaKey}` : "");
126
178
  }
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 => {
179
+ const shortcut$ = this.filterByContext(this.keydown$, context).pipe(filter(event => {
180
+ // For shorthand (where modifiers are explicitly false), we want an exact match.
181
+ // For object form:
182
+ // - if modifier is true, event.modifier must be true.
183
+ // - if modifier is false, event.modifier must be false.
184
+ // - if modifier is undefined, we don't care about event.modifier.
185
+ const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
186
+ const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
187
+ const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
188
+ const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
189
+ return ctrlMatch && altMatch && shiftMatch && metaMatch;
190
+ }), filter(event => compareKey(event.key, configuredMainKey)), tap(event => {
132
191
  if (this.debugMode) {
133
192
  const preventAction = preventDefault ? ", preventing default" : "";
134
193
  console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered${preventAction}.`);
@@ -147,12 +206,7 @@ export class Hotkeys {
147
206
  console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for combination shortcut "${id}":`, e);
148
207
  }
149
208
  });
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} }`);
209
+ return this._registerShortcut(config, subscription, ShortcutTypes.Combination, `Keys: { ${keyDetailsForLog} }`); // Use Enum
156
210
  }
157
211
  /**
158
212
  * Registers a key sequence shortcut (e.g., g -> i, or ArrowUp -> ArrowUp -> ArrowDown).
@@ -165,7 +219,7 @@ export class Hotkeys {
165
219
  * A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
166
220
  * @example
167
221
  * ```typescript
168
- * import { Keys } from './keys';
222
+ * import { Keys } from "./keys";
169
223
  * keyManager.addSequence({
170
224
  * id: "konamiCode",
171
225
  * sequence: [Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown, Keys.A, Keys.B],
@@ -180,7 +234,7 @@ export class Hotkeys {
180
234
  console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty or invalid. Shortcut not added.`);
181
235
  return undefined;
182
236
  }
183
- if (sequence.some(key => typeof key !== 'string' || key.trim() === '')) {
237
+ if (sequence.some(key => typeof key !== "string" || key.trim() === "")) {
184
238
  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
239
  return undefined;
186
240
  }
@@ -192,48 +246,52 @@ export class Hotkeys {
192
246
  shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
193
247
  let { matchedEvents, lastEventTime } = acc;
194
248
  const currentTime = performance.now();
195
- if (acc.emitState === 'emit') {
249
+ if (acc.emitState === EmitStates.Emit) {
196
250
  matchedEvents = [];
197
251
  lastEventTime = 0;
198
252
  }
199
253
  if (matchedEvents.length > 0 && (currentTime - lastEventTime > sequenceTimeoutMs)) {
200
254
  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.`);
255
+ console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) attempt timed out. Matched: ${matchedEvents.map(e => e.key).join(",")}. Resetting.`);
202
256
  }
203
257
  matchedEvents = [];
204
258
  }
205
259
  const nextExpectedKeyIndex = matchedEvents.length;
206
260
  if (nextExpectedKeyIndex >= sequenceLength) {
261
+ // Sequence was already emitted or buffer is too long (should not happen if reset correctly)
262
+ // Start new sequence if current key matches the first key of the sequence
207
263
  if (sequenceLength > 0 && compareKey(event.key, configuredSequence[0])) {
208
- return { matchedEvents: [event], lastEventTime: currentTime, emitState: 'in-progress' };
264
+ return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
209
265
  }
210
- return { matchedEvents: [], lastEventTime: 0, emitState: 'ignore' };
266
+ return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
211
267
  }
212
268
  if (compareKey(event.key, configuredSequence[nextExpectedKeyIndex])) {
213
269
  const newMatchedEvents = [...matchedEvents, event];
214
270
  if (newMatchedEvents.length === sequenceLength) {
215
- if (this.debugMode)
271
+ if (this.debugMode && !acc.emitState)
216
272
  console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) matched.`);
217
- return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: 'emit' };
273
+ return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.Emit };
218
274
  }
219
275
  else {
220
- return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: 'in-progress' };
276
+ return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.InProgress };
221
277
  }
222
278
  }
223
279
  else {
280
+ // If current key breaks sequence, check if it starts a new sequence
224
281
  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.`);
282
+ console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) broken by key "${event.key}". Matched: ${matchedEvents.map(e => e.key).join(",")}. Resetting.`);
226
283
  }
227
284
  if (sequenceLength > 0 && compareKey(event.key, configuredSequence[0])) {
228
- return { matchedEvents: [event], lastEventTime: currentTime, emitState: 'in-progress' };
285
+ return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
229
286
  }
230
287
  else {
231
- return { matchedEvents: [], lastEventTime: 0, emitState: 'ignore' };
288
+ return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
232
289
  }
233
290
  }
234
- }, { matchedEvents: [], lastEventTime: 0, emitState: 'ignore' }), filter(state => state.emitState === 'emit'), map(state => state.matchedEvents));
291
+ }, { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore }), filter(state => state.emitState === EmitStates.Emit), map(state => state.matchedEvents));
235
292
  }
236
293
  else {
294
+ // No timeout logic: simple buffer-based matching
237
295
  shortcut$ = baseKeydownStream$.pipe(bufferCount(sequenceLength, 1), filter((events) => {
238
296
  if (events.length < sequenceLength)
239
297
  return false;
@@ -255,6 +313,7 @@ export class Hotkeys {
255
313
  }));
256
314
  const subscription = finalShortcut$.subscribe((events) => {
257
315
  try {
316
+ // Ensure callback receives the last event of the sequence, similar to combination.
258
317
  if (events.length > 0)
259
318
  callback(events[events.length - 1]);
260
319
  }
@@ -262,8 +321,8 @@ export class Hotkeys {
262
321
  console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for sequence shortcut "${id}":`, e);
263
322
  }
264
323
  });
265
- const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ''}`;
266
- return this._registerShortcut(config, subscription, "Sequence", logDetails);
324
+ const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ""}`;
325
+ return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails); // Use Enum
267
326
  }
268
327
  /**
269
328
  * Removes a registered shortcut by its ID.
@@ -298,7 +357,7 @@ export class Hotkeys {
298
357
  id,
299
358
  description: activeShortcut.config.description,
300
359
  context: activeShortcut.config.context,
301
- type: 'keys' in activeShortcut.config ? "combination" : "sequence"
360
+ type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination // Use Enum values
302
361
  });
303
362
  }
304
363
  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,6 +68,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
70
68
  global.performance.now = originalPerformanceNow;
71
69
  }
72
70
  });
71
+ // ... (Initialization and Basic Context tests remain the same) ...
73
72
  describe("Initialization and Basic Context", () => {
74
73
  it("should initialize without errors", () => {
75
74
  assert(keyManager instanceof Hotkeys);
@@ -92,11 +91,11 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
92
91
  const consoleLogMock = mock.method(console, "log");
93
92
  keyManager.setDebugMode(true);
94
93
  keyManager.setContext("debug_test");
95
- assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes('Context changed to "debug_test"')));
94
+ assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Context changed to "debug_test"`)));
96
95
  consoleLogMock.mock.resetCalls();
97
96
  keyManager.setDebugMode(false);
98
97
  keyManager.setContext("no_debug_test");
99
- assert.ok(!consoleLogMock.mock.calls.some(call => call.arguments[0].includes('Context changed to "no_debug_test"')));
98
+ assert.ok(!consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Context changed to "no_debug_test"`)));
100
99
  consoleLogMock.mock.restore();
101
100
  });
102
101
  });
@@ -111,13 +110,12 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
111
110
  dispatchKeyEvent("A");
112
111
  assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'A' not called");
113
112
  });
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
113
+ it("should return undefined and warn if keys.key is null or undefined (runtime check in object form)", () => {
116
114
  const config = { id: "nullKey", keys: { key: null }, callback: mockCallback };
117
115
  const result = keyManager.addCombination(config);
118
116
  assert.strictEqual(result, undefined, "Should return undefined for null key");
119
117
  assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
120
- assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes('Invalid \'keys.key\' for combination shortcut "nullKey"'));
118
+ assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "keys.key" for combination shortcut "nullKey"`));
121
119
  });
122
120
  it("should pass the KeyboardEvent to the callback", () => {
123
121
  const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
@@ -140,14 +138,14 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
140
138
  dispatchKeyEvent("a", { ctrlKey: false });
141
139
  assert.strictEqual(mockCallback.calledCount, 1);
142
140
  });
143
- it("should trigger for special keys like Escape", () => {
144
- const config = { id: "escapeKey", keys: { key: Keys.Escape }, callback: mockCallback };
141
+ it("should trigger for special keys like Escape (object form)", () => {
142
+ const config = { id: "escapeKeyObj", keys: { key: Keys.Escape }, callback: mockCallback };
145
143
  keyManager.addCombination(config);
146
144
  dispatchKeyEvent("Escape"); // Event key matches Keys.Escape
147
145
  assert.strictEqual(mockCallback.calledCount, 1);
148
146
  });
149
- it("should handle preventDefault correctly", () => {
150
- const config = { id: "preventA", keys: { key: Keys.A }, callback: mockCallback, preventDefault: true };
147
+ it("should handle preventDefault correctly (object form)", () => {
148
+ const config = { id: "preventAObj", keys: { key: Keys.A }, callback: mockCallback, preventDefault: true };
151
149
  keyManager.addCombination(config);
152
150
  const event = dispatchKeyEvent("a");
153
151
  assert.strictEqual(mockCallback.calledCount, 1);
@@ -173,25 +171,92 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
173
171
  keyManager.addCombination({ id: "workingCombo", keys: { key: Keys.W }, callback: workingCallback });
174
172
  dispatchKeyEvent("e");
175
173
  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"'));
174
+ assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(`Error in user callback for combination shortcut "errorCombo"`));
177
175
  dispatchKeyEvent("w");
178
176
  assert.strictEqual(workingCallback.calledCount, 1);
179
177
  });
178
+ describe("addCombination - Shorthand Syntax", () => {
179
+ it("should trigger callback for a simple key using shorthand (e.g., Keys.X)", () => {
180
+ const config = { id: "shorthandX", keys: Keys.X, callback: mockCallback };
181
+ keyManager.addCombination(config);
182
+ dispatchKeyEvent(Keys.X.toLowerCase());
183
+ assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'x' (shorthand) not called");
184
+ mockCallback.mockClear();
185
+ dispatchKeyEvent(Keys.X);
186
+ assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'X' (shorthand) not called");
187
+ });
188
+ it("should NOT trigger callback for shorthand if modifier is pressed", () => {
189
+ const config = { id: "shorthandY", keys: Keys.Y, callback: mockCallback };
190
+ keyManager.addCombination(config);
191
+ dispatchKeyEvent(Keys.Y, { ctrlKey: true });
192
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Ctrl");
193
+ mockCallback.mockClear();
194
+ dispatchKeyEvent(Keys.Y, { altKey: true });
195
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Alt");
196
+ mockCallback.mockClear();
197
+ dispatchKeyEvent(Keys.Y, { shiftKey: true });
198
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Shift");
199
+ mockCallback.mockClear();
200
+ dispatchKeyEvent(Keys.Y, { metaKey: true });
201
+ assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Meta");
202
+ });
203
+ it("should trigger callback for shorthand if ONLY the key is pressed (no modifiers)", () => {
204
+ const config = { id: "shorthandZ", keys: Keys.Z, callback: mockCallback };
205
+ keyManager.addCombination(config);
206
+ dispatchKeyEvent(Keys.Z, { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false });
207
+ assert.strictEqual(mockCallback.calledCount, 1);
208
+ });
209
+ it("should handle preventDefault correctly for shorthand", () => {
210
+ const config = { id: "shorthandPrevent", keys: Keys.P, callback: mockCallback, preventDefault: true };
211
+ keyManager.addCombination(config);
212
+ const event = dispatchKeyEvent(Keys.P.toLowerCase());
213
+ assert.strictEqual(mockCallback.calledCount, 1);
214
+ assert.strictEqual(event.defaultPrevented, true);
215
+ });
216
+ it("should respect context for shorthand", () => {
217
+ const config = { id: "shorthandContext", keys: Keys.C, callback: mockCallback, context: "editor" };
218
+ keyManager.addCombination(config);
219
+ keyManager.setContext("other");
220
+ dispatchKeyEvent(Keys.C.toLowerCase());
221
+ assert.strictEqual(mockCallback.calledCount, 0);
222
+ keyManager.setContext("editor");
223
+ dispatchKeyEvent(Keys.C.toLowerCase());
224
+ assert.strictEqual(mockCallback.calledCount, 1);
225
+ });
226
+ it("should return undefined and warn if shorthand key is an empty string", () => {
227
+ const config = { id: "emptyShorthand", keys: "", callback: mockCallback };
228
+ const result = keyManager.addCombination(config);
229
+ assert.strictEqual(result, undefined);
230
+ assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
231
+ assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "keys" (shorthand) for combination shortcut "emptyShorthand"`));
232
+ });
233
+ it("should correctly log shorthand key details in debug mode", () => {
234
+ const consoleLogMock = mock.method(console, "log");
235
+ keyManager.setDebugMode(true);
236
+ const config = { id: "debugShorthand", keys: Keys.D, callback: mockCallback };
237
+ keyManager.addCombination(config);
238
+ const logMessage = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`combination shortcut "debugShorthand" added`));
239
+ assert.ok(logMessage, "Debug log for adding shortcut not found");
240
+ assert.ok(logMessage.arguments[0].includes(`Keys: { key: "D" (no modifiers implied) }`), `Log message content mismatch: ${logMessage.arguments[0]}`);
241
+ consoleLogMock.mock.restore();
242
+ keyManager.setDebugMode(false);
243
+ });
244
+ });
180
245
  });
181
246
  describe("addSequence", () => {
182
247
  it("should trigger callback for a simple key sequence", () => {
183
248
  const config = { id: "seqGI", sequence: [Keys.G, Keys.I], callback: mockCallback };
184
249
  const result = keyManager.addSequence(config);
185
250
  assert.strictEqual(result, "seqGI");
186
- dispatchKeyEvent("g"); // Dispatch 'g' (lowercase)
187
- dispatchKeyEvent("i"); // Dispatch 'i' (lowercase)
251
+ dispatchKeyEvent("g"); // Dispatch "g" (lowercase)
252
+ dispatchKeyEvent("i"); // Dispatch "i" (lowercase)
188
253
  assert.strictEqual(mockCallback.calledCount, 1);
189
254
  });
190
255
  it("should trigger callback for Konami code using Keys", () => {
191
256
  const konamiSequence = [
192
257
  Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown,
193
258
  Keys.ArrowLeft, Keys.ArrowRight, Keys.ArrowLeft, Keys.ArrowRight,
194
- Keys.B, Keys.A // Using 'B' and 'A' from Keys
259
+ Keys.B, Keys.A // Using "B" and "A" from Keys
195
260
  ];
196
261
  const config = { id: "konami", sequence: konamiSequence, callback: mockCallback };
197
262
  keyManager.addSequence(config);
@@ -236,7 +301,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
236
301
  dispatchKeyEvent("e");
237
302
  dispatchKeyEvent("s");
238
303
  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"'));
304
+ assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(`Error in user callback for sequence shortcut "errorSeq"`));
240
305
  });
241
306
  describe("Sequence Contextual Triggering", () => {
242
307
  let editorSequenceConfig;
@@ -317,17 +382,17 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
317
382
  });
318
383
  });
319
384
  describe("getActiveShortcuts", () => {
320
- it("should return active combination and sequence shortcuts", () => {
385
+ it("should return active combination and sequence shortcuts with enum types", () => {
321
386
  keyManager.addCombination({ id: "combo1", keys: { key: Keys.A }, callback: createMockFn(), description: "Test A" });
322
387
  keyManager.addSequence({ id: "seq1", sequence: [Keys.B, Keys.C], callback: createMockFn(), context: "modal", description: "Test BC" });
323
388
  const active = keyManager.getActiveShortcuts();
324
389
  assert.strictEqual(active.length, 2);
325
390
  const combo = active.find(s => s.id === "combo1");
326
391
  assert.ok(combo);
327
- assert.strictEqual(combo.type, "combination");
392
+ assert.strictEqual(combo.type, ShortcutTypes.Combination); // Use Enum for comparison
328
393
  const seq = active.find(s => s.id === "seq1");
329
394
  assert.ok(seq);
330
- assert.strictEqual(seq.type, "sequence");
395
+ assert.strictEqual(seq.type, ShortcutTypes.Sequence); // Use Enum for comparison
331
396
  });
332
397
  });
333
398
  describe("hasShortcut", () => {
@@ -347,10 +412,10 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
347
412
  keyManager.addCombination({ id: "destroyTestCombo", keys: { key: Keys.D }, callback: mockCallback });
348
413
  keyManager.addSequence({ id: "destroyTestSeq", sequence: [Keys.X, Keys.Y], callback: mockCallback });
349
414
  // @ts-ignore
350
- assert.strictEqual(keyManager['activeShortcuts'].size, 2);
415
+ assert.strictEqual(keyManager["activeShortcuts"].size, 2);
351
416
  keyManager.destroy();
352
417
  // @ts-ignore
353
- assert.strictEqual(keyManager['activeShortcuts'].size, 0);
418
+ assert.strictEqual(keyManager["activeShortcuts"].size, 0);
354
419
  dispatchKeyEvent(Keys.D);
355
420
  dispatchKeyEvent(Keys.X);
356
421
  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.1.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsc",