rx-hotkeys 2.4.1 → 2.6.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
@@ -8,6 +8,7 @@ rx-hotkeys is a powerful and flexible TypeScript library for managing keyboard s
8
8
  * **Key Sequences:** Define shortcuts that trigger when a series of keys are pressed in a specific order.
9
9
  * **Sequence Timeouts:** Optional timeout between key presses in a sequence to prevent accidental triggers or indefinite waiting.
10
10
  * **Context Management:** Activate or deactivate groups of shortcuts based on the application's current state (e.g., "editor", "modal", "global").
11
+ * **Strict Global Shortcuts:** Option to register global shortcuts that *only* fire when no other context is active, preventing them from triggering unintentionally.
11
12
  * **Type-Safe Key Definitions:** Uses an exported `Keys` object based on standard `KeyboardEvent.key` values for improved developer experience and fewer errors.
12
13
  * **RxJS Powered:** Built on RxJS for robust and efficient event handling.
13
14
  * **Prevent Default:** Option to prevent the default browser action for a triggered shortcut.
@@ -29,7 +30,7 @@ First, ensure you have the `rx-hotkeys` library and its helper Keys imported:
29
30
  import { Hotkeys, Keys, KeyCombinationConfig, KeySequenceConfig } from 'rx-hotkeys';
30
31
  ```
31
32
 
32
- 1. Initialize Hotkeys
33
+ ### 1. Initialize Hotkeys
33
34
 
34
35
  Create an instance of the `Hotkeys` class. You can optionally provide an initial context and enable debug mode.
35
36
 
@@ -40,7 +41,7 @@ const keyManager = new Hotkeys(); // No initial context, debug mode off
40
41
  // const keyManager = new Hotkeys('editor', true);
41
42
  ```
42
43
 
43
- 2. Add a Key Combination
44
+ ### 2. Add a Key Combination
44
45
 
45
46
  Register a shortcut for a key combination, like Ctrl+S.
46
47
 
@@ -58,7 +59,9 @@ const saveConfig: KeyCombinationConfig = {
58
59
  keyManager.addCombination(saveConfig);
59
60
  ```
60
61
 
61
- 3. Add a Key SequenceRegister a shortcut for a sequence of keys, like the Konami code.
62
+ ### 3. Add a Key Sequence
63
+
64
+ Register a shortcut for a sequence of keys, like the Konami code.
62
65
 
63
66
  ```typescript
64
67
  const konamiConfig: KeySequenceConfig = {
@@ -81,7 +84,9 @@ const konamiConfig: KeySequenceConfig = {
81
84
  keyManager.addSequence(konamiConfig);
82
85
  ```
83
86
 
84
- 4. Manage ContextsControl which shortcuts are active by setting the context.
87
+ ### 4. Manage Contexts
88
+
89
+ Control which shortcuts are active by setting the context.
85
90
 
86
91
  ```typescript
87
92
  // Assuming some shortcuts are configured with context: "editor"
@@ -91,7 +96,25 @@ keyManager.setContext("editor"); // Activates "editor" shortcuts and global shor
91
96
  keyManager.setContext(null);
92
97
  ```
93
98
 
94
- 5. Clean Up
99
+ #### Global vs. Strict Global Shortcuts
100
+
101
+ Global shortcuts (those without a `context` property) have two behaviors:
102
+
103
+ * **Default Global**: By default, a global shortcut will fire in *any* context, unless a more specific shortcut for the same key combination exists for that context.
104
+
105
+ ```typescript
106
+ // This shortcut for Ctrl+P will fire in the "editor" context, "modal" context, or any other,
107
+ // unless a specific "editor" shortcut for Ctrl+P exists.
108
+ keyManager.addCombination({ id: 'globalPrint', keys: { key: Keys.P, ctrlKey: true }, callback: myCallback });
109
+ ```
110
+ * **Strict Global**: By passing `true` as the second argument to `addCombination` or `addSequence`, you can register a "strict" global shortcut. This shortcut will **only** fire when no context is active (`keyManager.getContext()` returns `null`).
111
+
112
+ ```typescript
113
+ // This help shortcut for "?" will ONLY fire when no other context is active.
114
+ keyManager.addCombination({ id: 'strictHelp', keys: Keys.QuestionMark, callback: openHelpModal }, true);
115
+ ```
116
+
117
+ ### 5. Clean Up
95
118
 
96
119
  When the Hotkeys instance is no longer needed (e.g., component unmount), call `destroy()` to clean up subscriptions and prevent memory leaks.
97
120
 
@@ -170,6 +193,7 @@ Cleans up all subscriptions and resources. Essential to call to prevent memory l
170
193
  * `context?: string | null`: Specifies the context in which this shortcut is active. If `null` or `undefined`, it's a global shortcut.
171
194
  * `preventDefault?: boolean`: If true, `event.preventDefault()` will be called when the shortcut triggers. Defaults to `false`.
172
195
  * `description?: string`: An optional description for the shortcut (e.g., for help menus).
196
+ * `strict?: boolean` (optional): If `true` and the shortcut has no `context`, it will only fire when no other context is active. Defaults to `false`.
173
197
 
174
198
  `KeySequenceConfig`
175
199
 
@@ -180,13 +204,13 @@ Cleans up all subscriptions and resources. Essential to call to prevent memory l
180
204
  * `preventDefault?: boolean`: If true, `event.preventDefault()` is called for the last event in the sequence. Defaults to `false`.
181
205
  * `description?: string`: Optional description.
182
206
  * `sequenceTimeoutMs?: number`: Optional. Maximum time (in milliseconds) allowed between consecutive key presses in the sequence. If exceeded, the sequence resets. If `0` or `undefined`, no inter-key timeout is applied (uses simpler buffer-based matching).
207
+ * `strict?: boolean` (optional): If `true` and the shortcut has no `context`, it will only fire when no other context is active. Defaults to `false`.
183
208
 
184
209
 
185
210
  ## Key Matching Logic
186
211
 
187
- * Single Character Keys (e.g., `Keys.A`, `Keys.Digit7`): When you configure a shortcut with a single character key from `Keys`, the library matches it case-insensitively against the `event.key` from the browser. For example, if you configure `Keys.A`, it will trigger for both "a" and "A" key presses (assuming Shift isn't a required modifier).
188
- * Special Keys (e.g., `Keys.Enter`, `Keys.ArrowUp`, `Keys.Escape`): These are multi-character `event.key` values. The library matches these case-sensitively against the `event.key`. Using the `Keys` object ensures you provide the correct, standard case-sensitive string.
189
-
212
+ * **Single Character Keys** (e.g., `Keys.A`, `Keys.Digit7`): When you configure a shortcut with a single character key from `Keys`, the library matches it case-insensitively against the `event.key` from the browser. For example, if you configure `Keys.A`, it will trigger for both "a" and "A" key presses (assuming Shift isn't a required modifier).
213
+ * **Special Keys** (e.g., `Keys.Enter`, `Keys.ArrowUp`, `Keys.Escape`): These are multi-character `event.key` values. The library matches these case-sensitively against the `event.key`. Using the `Keys` object ensures you provide the correct, standard case-sensitive string.
190
214
 
191
215
  ## Contributing
192
216
 
package/dist/hotkeys.d.ts CHANGED
@@ -10,6 +10,15 @@ interface ShortcutConfigBase {
10
10
  context?: string | null;
11
11
  preventDefault?: boolean;
12
12
  description?: string;
13
+ /**
14
+ * **Only applicable if the shortcut has no top-level `context` defined.**
15
+ * If `true`, this shortcut is **strictly global** and will only fire when the active
16
+ * hotkey context is `null`.
17
+ * If `false` or `undefined` (the default), the shortcut can fire in *any* context,
18
+ * but will be suppressed by an identical shortcut that belongs to the active context.
19
+ * @default false
20
+ */
21
+ strict?: boolean;
13
22
  }
14
23
  /**
15
24
  * Defines a single key trigger, which can be a StandardKey (for simple presses like "Escape")
@@ -34,15 +43,19 @@ type KeyCombinationTrigger = {
34
43
  } | StandardKey;
35
44
  export interface KeyCombinationConfig extends ShortcutConfigBase {
36
45
  /**
37
- * Defines the key or key combination.
38
- * Can be an object specifying the main `key` (from `StandardKey`) and optional
46
+ * Defines the key or key combination(s) that trigger the shortcut.
47
+ * Can be a single trigger or an array of triggers.
48
+ * Each trigger can be an object specifying the main `key` (from `StandardKey`) and optional
39
49
  * modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
40
50
  * Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
41
51
  *
42
- * Alternatively, for a simple key press without any modifiers, this can be
52
+ * Alternatively, for a simple key press without any modifiers, a trigger can be
43
53
  * a `StandardKey` directly.
44
54
  * Example: `Keys.Escape` for the Escape key. When using this shorthand,
45
55
  * it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
56
+ *
57
+ * To define multiple triggers for the same action:
58
+ * Example: `keys: [Keys.Enter, { key: Keys.Space, ctrlKey: true }]`
46
59
  */
47
60
  keys: KeyCombinationTrigger | KeyCombinationTrigger[];
48
61
  }
@@ -95,7 +108,7 @@ export declare class Hotkeys {
95
108
  * will be active and can be triggered.
96
109
  * @param contextName - The name of the context (e.g., "modal", "editor", "global").
97
110
  * Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
98
- * @returns boolean
111
+ * @returns `true` if the context was changed, `false` if the new context was the same as the current one.
99
112
  */
100
113
  setContext(contextName: string | null): boolean;
101
114
  /**
@@ -109,6 +122,12 @@ export declare class Hotkeys {
109
122
  * @param enable - True to enable debug logs, false to disable.
110
123
  */
111
124
  setDebugMode(enable: boolean): void;
125
+ /**
126
+ * Checks if a shortcut with the given ID is currently registered and active.
127
+ * @param id - The unique ID of the shortcut to check.
128
+ * @returns True if a shortcut with the specified ID exists, false otherwise.
129
+ */
130
+ hasShortcut(id: string): boolean;
112
131
  /**
113
132
  * An Observable that emits the new context name (or null) whenever the active context changes.
114
133
  * This allows external parts of the application to react to context transitions.
@@ -129,11 +148,20 @@ export declare class Hotkeys {
129
148
  */
130
149
  get onContextChange$(): Observable<string | null>;
131
150
  /**
132
- * Checks if a shortcut with the given ID is currently registered and active.
133
- * @param id - The unique ID of the shortcut to check.
134
- * @returns True if a shortcut with the specified ID exists, false otherwise.
151
+ * Compares two sequences of StandardKey arrays to see if they are identical.
152
+ * @param seq1 - The first sequence array.
153
+ * @param seq2 - The second sequence array.
154
+ * @returns True if the sequences are identical, false otherwise.
135
155
  */
136
- hasShortcut(id: string): boolean;
156
+ private _areSequencesIdentical;
157
+ /**
158
+ * Checks if a given KeyCombinationConfig matches a given KeyboardEvent.
159
+ * This is used internally for priority checking.
160
+ * @param shortcutConfig The KeyCombinationConfig to check.
161
+ * @param event The KeyboardEvent to match against.
162
+ * @returns True if the shortcutConfig matches the event, false otherwise.
163
+ */
164
+ private _shortcutMatchesEvent;
137
165
  private filterByContext;
138
166
  private _registerShortcut;
139
167
  /**
@@ -162,10 +190,10 @@ export declare class Hotkeys {
162
190
  * callback: () => console.log("File saved!"),
163
191
  * context: "editor"
164
192
  * });
165
- * // For just the Escape key
193
+ * // For just the Escape key, or Ctrl+Space
166
194
  * keyManager.addCombination({
167
195
  * id: "closeModal",
168
- * keys: Keys.Escape, // Shorthand syntax
196
+ * keys: [Keys.Escape, {key: Keys.Space, ctrlKey: true}],
169
197
  * callback: () => console.log("Modal closed!")
170
198
  * });
171
199
  * ```
@@ -205,7 +233,7 @@ export declare class Hotkeys {
205
233
  * This can be useful for displaying available shortcuts to the user or for debugging.
206
234
  * @returns An array of objects, where each object represents an active shortcut
207
235
  * and includes its `id`, `description` (if provided), `context` (if any),
208
- * and `type` ("combination" or "sequence").
236
+ * and `type` (from `ShortcutTypes` enum).
209
237
  */
210
238
  getActiveShortcuts(): {
211
239
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"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"}
package/dist/hotkeys.js CHANGED
@@ -5,6 +5,12 @@ export var ShortcutTypes;
5
5
  ShortcutTypes["Combination"] = "combination";
6
6
  ShortcutTypes["Sequence"] = "sequence";
7
7
  })(ShortcutTypes || (ShortcutTypes = {}));
8
+ var EmitStates;
9
+ (function (EmitStates) {
10
+ EmitStates[EmitStates["Emit"] = 0] = "Emit";
11
+ EmitStates[EmitStates["Ignore"] = 1] = "Ignore";
12
+ EmitStates[EmitStates["InProgress"] = 2] = "InProgress";
13
+ })(EmitStates || (EmitStates = {}));
8
14
  // --- Helper function to compare keys ---
9
15
  /**
10
16
  * Compares a browser event's key with a configured key.
@@ -21,12 +27,6 @@ function compareKey(eventKey, configuredKey) {
21
27
  return eventKey === configuredKey;
22
28
  }
23
29
  // --- 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 = {}));
30
30
  /**
31
31
  * Manages keyboard shortcuts for web applications.
32
32
  * Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
@@ -63,7 +63,7 @@ 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
+ * @returns `true` if the context was changed, `false` if the new context was the same as the current one.
67
67
  */
68
68
  setContext(contextName) {
69
69
  const currentContext = this.activeContext$.getValue();
@@ -72,14 +72,14 @@ export class Hotkeys {
72
72
  // Optional: Log that no change is happening, or simply do nothing.
73
73
  console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
74
74
  }
75
- return false; // Context is the same, so no further action is needed.
75
+ return false; // Context was NOT updated
76
76
  }
77
77
  // If we reach here, the context is actually changing.
78
78
  if (this.debugMode) {
79
79
  console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${currentContext}" to "${contextName}".`);
80
80
  }
81
81
  this.activeContext$.next(contextName);
82
- return true;
82
+ return true; // Context WAS updated
83
83
  }
84
84
  /**
85
85
  * Gets the current active context.
@@ -94,17 +94,25 @@ export class Hotkeys {
94
94
  * @param enable - True to enable debug logs, false to disable.
95
95
  */
96
96
  setDebugMode(enable) {
97
- if (this.debugMode === enable) { // Check if state is actually changing
98
- return; // If no change, do nothing (no log)
97
+ if (this.debugMode === enable) {
98
+ return;
99
99
  }
100
- this.debugMode = enable; // Set the new state
101
- if (enable) { // Log based on the NEW state after a change
100
+ this.debugMode = enable;
101
+ if (enable) {
102
102
  console.log(`${Hotkeys.LOG_PREFIX} Debug mode enabled.`);
103
103
  }
104
104
  else {
105
105
  console.log(`${Hotkeys.LOG_PREFIX} Debug mode disabled.`);
106
106
  }
107
107
  }
108
+ /**
109
+ * Checks if a shortcut with the given ID is currently registered and active.
110
+ * @param id - The unique ID of the shortcut to check.
111
+ * @returns True if a shortcut with the specified ID exists, false otherwise.
112
+ */
113
+ hasShortcut(id) {
114
+ return this.activeShortcuts.has(id);
115
+ }
108
116
  /**
109
117
  * An Observable that emits the new context name (or null) whenever the active context changes.
110
118
  * This allows external parts of the application to react to context transitions.
@@ -127,18 +135,84 @@ export class Hotkeys {
127
135
  return this.activeContext$.asObservable();
128
136
  }
129
137
  /**
130
- * Checks if a shortcut with the given ID is currently registered and active.
131
- * @param id - The unique ID of the shortcut to check.
132
- * @returns True if a shortcut with the specified ID exists, false otherwise.
138
+ * Compares two sequences of StandardKey arrays to see if they are identical.
139
+ * @param seq1 - The first sequence array.
140
+ * @param seq2 - The second sequence array.
141
+ * @returns True if the sequences are identical, false otherwise.
133
142
  */
134
- hasShortcut(id) {
135
- return this.activeShortcuts.has(id);
143
+ _areSequencesIdentical(seq1, seq2) {
144
+ if (seq1.length !== seq2.length) {
145
+ return false;
146
+ }
147
+ for (let i = 0; i < seq1.length; i++) {
148
+ if (seq1[i] !== seq2[i]) { // Direct comparison for canonical StandardKey values
149
+ return false;
150
+ }
151
+ }
152
+ return true;
153
+ }
154
+ /**
155
+ * Checks if a given KeyCombinationConfig matches a given KeyboardEvent.
156
+ * This is used internally for priority checking.
157
+ * @param shortcutConfig The KeyCombinationConfig to check.
158
+ * @param event The KeyboardEvent to match against.
159
+ * @returns True if the shortcutConfig matches the event, false otherwise.
160
+ */
161
+ _shortcutMatchesEvent(shortcutConfig, event) {
162
+ const keyTriggers = Array.isArray(shortcutConfig.keys) ? shortcutConfig.keys : [shortcutConfig.keys];
163
+ for (const keyInput of keyTriggers) {
164
+ let configuredMainKey;
165
+ let ctrlKeyConfig;
166
+ let altKeyConfig;
167
+ let shiftKeyConfig;
168
+ let metaKeyConfig;
169
+ if (typeof keyInput === "string") {
170
+ if (keyInput === "")
171
+ continue; // Invalid trigger, skip
172
+ configuredMainKey = keyInput;
173
+ ctrlKeyConfig = false;
174
+ altKeyConfig = false;
175
+ shiftKeyConfig = false;
176
+ metaKeyConfig = false;
177
+ }
178
+ else {
179
+ if (!keyInput.key || keyInput.key === "")
180
+ continue; // Invalid trigger, skip
181
+ configuredMainKey = keyInput.key;
182
+ ctrlKeyConfig = keyInput.ctrlKey;
183
+ altKeyConfig = keyInput.altKey;
184
+ shiftKeyConfig = keyInput.shiftKey;
185
+ metaKeyConfig = keyInput.metaKey;
186
+ }
187
+ const keyMatch = compareKey(event.key, configuredMainKey);
188
+ if (!keyMatch)
189
+ continue;
190
+ const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
191
+ const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
192
+ const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
193
+ const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
194
+ if (ctrlMatch && altMatch && shiftMatch && metaMatch) {
195
+ return true;
196
+ }
197
+ }
198
+ return false;
136
199
  }
137
- filterByContext(source$, context) {
138
- return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => context == null || context === activeCtx), map(([event, /* _activeCtx */]) => event));
200
+ filterByContext(source$, context, strict) {
201
+ return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => {
202
+ if (context == null) {
203
+ if (strict) {
204
+ return activeCtx == null;
205
+ }
206
+ else {
207
+ return true;
208
+ }
209
+ }
210
+ else {
211
+ return context === activeCtx;
212
+ }
213
+ }), map(([event, /* _activeCtx */]) => event));
139
214
  }
140
- _registerShortcut(config, subscription, type, // Changed to use Enum
141
- detailsForLog) {
215
+ _registerShortcut(config, subscription, type, detailsForLog) {
142
216
  const existingShortcut = this.activeShortcuts.get(config.id);
143
217
  if (existingShortcut) {
144
218
  console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
@@ -163,11 +237,18 @@ export class Hotkeys {
163
237
  console.warn(`${Hotkeys.LOG_PREFIX} Invalid key (shorthand) in shortcut "${shortcutId}". Key string must not be empty.`);
164
238
  return null;
165
239
  }
166
- return { configuredMainKey: keyInput, ctrlKeyConfig: false, altKeyConfig: false, shiftKeyConfig: false, metaKeyConfig: false, logDetails: `key: "${keyInput}" (no mods)` };
240
+ return {
241
+ configuredMainKey: keyInput,
242
+ ctrlKeyConfig: false,
243
+ altKeyConfig: false,
244
+ shiftKeyConfig: false,
245
+ metaKeyConfig: false,
246
+ logDetails: `key: "${keyInput}" (no mods)`,
247
+ };
167
248
  }
168
249
  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.`);
250
+ if (!keyInput.key || typeof keyInput.key !== "string" || keyInput.key === "") {
251
+ console.warn(`${Hotkeys.LOG_PREFIX} Invalid "key" property in shortcut "${shortcutId}". Key must be a non-empty string value from Keys.`);
171
252
  return null;
172
253
  }
173
254
  const logDetails = `key: "${keyInput.key}"` +
@@ -175,7 +256,14 @@ export class Hotkeys {
175
256
  (keyInput.altKey !== undefined ? `, alt: ${keyInput.altKey}` : "") +
176
257
  (keyInput.shiftKey !== undefined ? `, shift: ${keyInput.shiftKey}` : "") +
177
258
  (keyInput.metaKey !== undefined ? `, meta: ${keyInput.metaKey}` : "");
178
- return { configuredMainKey: keyInput.key, ctrlKeyConfig: keyInput.ctrlKey, altKeyConfig: keyInput.altKey, shiftKeyConfig: keyInput.shiftKey, metaKeyConfig: keyInput.metaKey, logDetails };
259
+ return {
260
+ configuredMainKey: keyInput.key,
261
+ ctrlKeyConfig: keyInput.ctrlKey,
262
+ altKeyConfig: keyInput.altKey,
263
+ shiftKeyConfig: keyInput.shiftKey,
264
+ metaKeyConfig: keyInput.metaKey,
265
+ logDetails,
266
+ };
179
267
  }
180
268
  }
181
269
  /**
@@ -196,18 +284,19 @@ export class Hotkeys {
196
284
  * callback: () => console.log("File saved!"),
197
285
  * context: "editor"
198
286
  * });
199
- * // For just the Escape key
287
+ * // For just the Escape key, or Ctrl+Space
200
288
  * keyManager.addCombination({
201
289
  * id: "closeModal",
202
- * keys: Keys.Escape, // Shorthand syntax
290
+ * keys: [Keys.Escape, {key: Keys.Space, ctrlKey: true}],
203
291
  * callback: () => console.log("Modal closed!")
204
292
  * });
205
293
  * ```
206
294
  */
207
295
  addCombination(config) {
208
- const { keys, callback, context, preventDefault = false, id } = config;
209
- let finalShortcut$;
210
- let overallLogDetails = "";
296
+ const { keys, callback, context, preventDefault = false, id, strict = false } = config;
297
+ if (context != null && strict) {
298
+ console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context(${context}) and the 'strict' flag. The 'strict' flag will be ignored.`);
299
+ }
211
300
  const keyTriggers = Array.isArray(keys) ? keys : [keys];
212
301
  if (keyTriggers.length === 0) {
213
302
  console.warn(`${Hotkeys.LOG_PREFIX} "keys" array for combination shortcut "${id}" is empty. Shortcut not added.`);
@@ -223,13 +312,36 @@ export class Hotkeys {
223
312
  }
224
313
  const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig, logDetails } = parsedTrigger;
225
314
  logParts.push(`{ ${logDetails} }`);
226
- const stream = this.filterByContext(this.keydown$, context).pipe(filter(event => {
315
+ const stream = this.filterByContext(this.keydown$, context, strict).pipe(filter(event => {
227
316
  const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
228
317
  const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
229
318
  const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
230
319
  const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
231
320
  return ctrlMatch && altMatch && shiftMatch && metaMatch;
232
- }), filter(event => compareKey(event.key, configuredMainKey)));
321
+ }), filter(event => compareKey(event.key, configuredMainKey)),
322
+ // New filter for priority: Specific context > Global context
323
+ filter(event => {
324
+ if (context != null || strict) { // This shortcut is NOT global or strict
325
+ return true;
326
+ }
327
+ // This shortcut IS global. Check for specific overrides.
328
+ const currentSpecificContext = this.activeContext$.getValue();
329
+ if (currentSpecificContext == null) { // No specific context active
330
+ return true;
331
+ }
332
+ for (const [, otherAS] of this.activeShortcuts) {
333
+ if (otherAS.config.id !== id &&
334
+ 'keys' in otherAS.config &&
335
+ otherAS.config.context === currentSpecificContext &&
336
+ this._shortcutMatchesEvent(otherAS.config, event)) {
337
+ if (this.debugMode) {
338
+ console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
339
+ }
340
+ return false; // Suppress global
341
+ }
342
+ }
343
+ return true; // Global can proceed
344
+ }));
233
345
  observables.push(stream);
234
346
  }
235
347
  if (observables.length === 0) {
@@ -237,8 +349,8 @@ export class Hotkeys {
237
349
  console.warn(`${Hotkeys.LOG_PREFIX} No valid key triggers for combination shortcut "${id}". Shortcut not added.`);
238
350
  return undefined;
239
351
  }
240
- finalShortcut$ = merge(...observables);
241
- overallLogDetails = Array.isArray(keys) ? `Triggers: [ ${logParts.join(", ")} ]` : logParts[0];
352
+ const finalShortcut$ = merge(...observables);
353
+ const overallLogDetails = Array.isArray(keys) ? `Triggers: [ ${logParts.join(", ")} ]` : logParts[0];
242
354
  const subscription = finalShortcut$.pipe(tap(event => {
243
355
  if (this.debugMode) {
244
356
  const preventAction = preventDefault ? ", preventing default" : "";
@@ -280,7 +392,7 @@ export class Hotkeys {
280
392
  * ```
281
393
  */
282
394
  addSequence(config) {
283
- const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs } = config;
395
+ const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs, strict = false } = config;
284
396
  if (!Array.isArray(sequence) || sequence.length === 0) {
285
397
  console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty or invalid. Shortcut not added.`);
286
398
  return undefined;
@@ -290,10 +402,13 @@ export class Hotkeys {
290
402
  console.warn(`${Hotkeys.LOG_PREFIX} Invalid key in sequence for shortcut "${id}". All keys must be non-empty string values from Keys. Shortcut not added.`);
291
403
  return undefined;
292
404
  }
405
+ if (context && strict) {
406
+ console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context and the 'strict' flag. The 'strict' flag will be ignored.`);
407
+ }
293
408
  const configuredSequence = sequence;
294
409
  const sequenceLength = configuredSequence.length;
295
410
  let shortcut$;
296
- const baseKeydownStream$ = this.filterByContext(this.keydown$, context);
411
+ const baseKeydownStream$ = this.filterByContext(this.keydown$, context, strict);
297
412
  if (sequenceTimeoutMs && sequenceTimeoutMs > 0) {
298
413
  shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
299
414
  let { matchedEvents, lastEventTime } = acc;
@@ -320,7 +435,7 @@ export class Hotkeys {
320
435
  if (compareKey(event.key, configuredSequence[nextExpectedKeyIndex])) {
321
436
  const newMatchedEvents = [...matchedEvents, event];
322
437
  if (newMatchedEvents.length === sequenceLength) {
323
- if (this.debugMode && !acc.emitState)
438
+ if (this.debugMode && acc.emitState !== EmitStates.Emit)
324
439
  console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) matched.`);
325
440
  return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.Emit };
326
441
  }
@@ -350,7 +465,28 @@ export class Hotkeys {
350
465
  return events.every((event, index) => compareKey(event.key, configuredSequence[index]));
351
466
  }));
352
467
  }
353
- const finalShortcut$ = shortcut$.pipe(tap((events) => {
468
+ const finalShortcutWithPriority$ = shortcut$.pipe(filter((completedEvents) => {
469
+ if (context != null || strict) { // This sequence is NOT global or strict
470
+ return true;
471
+ }
472
+ // This sequence IS global. Check for specific overrides.
473
+ const currentSpecificContext = this.activeContext$.getValue();
474
+ if (currentSpecificContext == null) { // No specific context active
475
+ return true;
476
+ }
477
+ for (const [, otherAS] of this.activeShortcuts) {
478
+ if (otherAS.config.id !== id &&
479
+ "sequence" in otherAS.config &&
480
+ otherAS.config.context === currentSpecificContext &&
481
+ this._areSequencesIdentical(sequence, otherAS.config.sequence)) {
482
+ if (this.debugMode) {
483
+ console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${id}" suppressed by identical specific-context shortcut "${otherAS.config.id}".`);
484
+ }
485
+ return false; // Suppress global
486
+ }
487
+ }
488
+ return true; // Global sequence can proceed
489
+ }), tap((events) => {
354
490
  if (this.debugMode) {
355
491
  const timeoutInfo = (sequenceTimeoutMs && sequenceTimeoutMs > 0) ? ` (with timeout logic)` : ` (no timeout logic)`;
356
492
  const preventAction = preventDefault ? ", preventing default for last event" : "";
@@ -363,7 +499,7 @@ export class Hotkeys {
363
499
  console.error(`${Hotkeys.LOG_PREFIX} Error in sequence stream for shortcut "${id}":`, err);
364
500
  return EMPTY;
365
501
  }));
366
- const subscription = finalShortcut$.subscribe((events) => {
502
+ const subscription = finalShortcutWithPriority$.subscribe((events) => {
367
503
  try {
368
504
  // Ensure callback receives the last event of the sequence, similar to combination.
369
505
  if (events.length > 0)
@@ -374,7 +510,7 @@ export class Hotkeys {
374
510
  }
375
511
  });
376
512
  const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ""}`;
377
- return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails); // Use Enum
513
+ return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails);
378
514
  }
379
515
  /**
380
516
  * Removes a registered shortcut by its ID.
@@ -400,7 +536,7 @@ export class Hotkeys {
400
536
  * This can be useful for displaying available shortcuts to the user or for debugging.
401
537
  * @returns An array of objects, where each object represents an active shortcut
402
538
  * and includes its `id`, `description` (if provided), `context` (if any),
403
- * and `type` ("combination" or "sequence").
539
+ * and `type` (from `ShortcutTypes` enum).
404
540
  */
405
541
  getActiveShortcuts() {
406
542
  const shortcuts = [];
@@ -409,7 +545,7 @@ export class Hotkeys {
409
545
  id,
410
546
  description: activeShortcut.config.description,
411
547
  context: activeShortcut.config.context,
412
- type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination // Use Enum values
548
+ type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination
413
549
  });
414
550
  }
415
551
  return shortcuts;
@@ -425,7 +561,7 @@ export class Hotkeys {
425
561
  console.log(`${Hotkeys.LOG_PREFIX} Destroying library instance and unsubscribing all shortcuts.`);
426
562
  this.activeShortcuts.forEach(shortcut => shortcut.subscription.unsubscribe());
427
563
  this.activeShortcuts.clear();
428
- this.activeContext$.complete(); // Complete the BehaviorSubject to release its resources
564
+ this.activeContext$.complete();
429
565
  if (this.debugMode)
430
566
  console.log(`${Hotkeys.LOG_PREFIX} Library destroyed.`);
431
567
  }