rx-hotkeys 3.0.0 → 4.0.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.
Files changed (46) hide show
  1. package/README.md +195 -42
  2. package/dist/{hotkeys.d.ts → core/hotkeys.d.ts} +35 -14
  3. package/dist/core/hotkeys.d.ts.map +1 -0
  4. package/dist/{hotkeys.js → core/hotkeys.js} +107 -42
  5. package/dist/core/hotkeys.js.map +1 -0
  6. package/dist/{hotkeys.test.d.ts.map → core/hotkeys.test.d.ts.map} +1 -1
  7. package/dist/{hotkeys.test.js → core/hotkeys.test.js} +201 -145
  8. package/dist/core/hotkeys.test.js.map +1 -0
  9. package/dist/core/index.d.ts.map +1 -0
  10. package/dist/{index.js → core/index.js} +1 -0
  11. package/dist/core/index.js.map +1 -0
  12. package/dist/core/keys.d.ts.map +1 -0
  13. package/dist/{keys.js → core/keys.js} +1 -0
  14. package/dist/core/keys.js.map +1 -0
  15. package/dist/core/testutils.d.ts.map +1 -0
  16. package/dist/{testutils.js → core/testutils.js} +1 -0
  17. package/dist/core/testutils.js.map +1 -0
  18. package/dist/integrations/react/index.d.ts +3 -0
  19. package/dist/integrations/react/index.d.ts.map +1 -0
  20. package/dist/integrations/react/index.js +3 -0
  21. package/dist/integrations/react/index.js.map +1 -0
  22. package/dist/integrations/react/provider.d.ts +36 -0
  23. package/dist/integrations/react/provider.d.ts.map +1 -0
  24. package/dist/integrations/react/provider.js +85 -0
  25. package/dist/integrations/react/provider.js.map +1 -0
  26. package/dist/integrations/react/useEventCallback.d.ts +14 -0
  27. package/dist/integrations/react/useEventCallback.d.ts.map +1 -0
  28. package/dist/integrations/react/useEventCallback.js +32 -0
  29. package/dist/integrations/react/useEventCallback.js.map +1 -0
  30. package/dist/integrations/react/useHotkeys.d.ts +43 -0
  31. package/dist/integrations/react/useHotkeys.d.ts.map +1 -0
  32. package/dist/integrations/react/useHotkeys.js +76 -0
  33. package/dist/integrations/react/useHotkeys.js.map +1 -0
  34. package/dist/integrations/react/useIsomorphicLayoutEffect.d.ts +3 -0
  35. package/dist/integrations/react/useIsomorphicLayoutEffect.d.ts.map +1 -0
  36. package/dist/integrations/react/useIsomorphicLayoutEffect.js +3 -0
  37. package/dist/integrations/react/useIsomorphicLayoutEffect.js.map +1 -0
  38. package/package.json +19 -8
  39. package/dist/hotkeys.d.ts.map +0 -1
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/keys.d.ts.map +0 -1
  42. package/dist/testutils.d.ts.map +0 -1
  43. /package/dist/{hotkeys.test.d.ts → core/hotkeys.test.d.ts} +0 -0
  44. /package/dist/{index.d.ts → core/index.d.ts} +0 -0
  45. /package/dist/{keys.d.ts → core/keys.d.ts} +0 -0
  46. /package/dist/{testutils.d.ts → core/testutils.d.ts} +0 -0
package/README.md CHANGED
@@ -4,12 +4,15 @@ rx-hotkeys is a powerful and flexible TypeScript library for managing keyboard s
4
4
 
5
5
  ## ✨ Features
6
6
 
7
+ * **Official React Hooks**: Provides an official wrapper (`HotkeysProvider`, `useHotkeys`, `useScopedHotkeysContext`) for seamless, idiomatic integration with React.
7
8
  * **Fully Observable API**: Returns an RxJS `Observable` for each shortcut, allowing for powerful stream manipulation like chaining, filtering, debouncing, and merging with other streams.
8
9
  * **Flexible Shortcut Definitions**: Define shortcuts using simple, intuitive strings (e.g., `"ctrl+s"` or `"g -> i"`) in addition to the classic object-based configuration.
9
10
  * **Element-Scoped Listeners**: Attach shortcuts to specific DOM elements, so they are only active within a certain component or area, not just on the global `document`.
10
11
  * **`keyup` Event Support**: Trigger actions on key release (`keyup`) in addition to the default key press (`keydown`).
11
12
  * **Key Combinations & Sequences**: Supports both simultaneous key presses (`Ctrl+S`) and ordered key sequences (`g` -> `c`).
12
13
  * **Context Management**: Activate or deactivate groups of shortcuts based on the application's current state (e.g., "editor", "modal", "global").
14
+ * **Stack-Based Context Management**: Natively handles nested contexts with an `enter`/`leave` API, perfect for hierarchical UIs like pages, modals, and dropdowns.
15
+ * **Temporary Context Override**: Safely override all contexts with a high-priority temporary context, ideal for global application states like "saving" or "loading".
13
16
  * **Strict Global Shortcuts**: Option to register global shortcuts that *only* fire when no other context is active.
14
17
  * **Type-Safe Key Definitions**: Uses an exported `Keys` object based on standard `KeyboardEvent.key` values for a superior developer experience and fewer errors.
15
18
  * **Sequence Timeouts**: Optional timeout between key presses in a sequence to prevent accidental triggers.
@@ -21,40 +24,17 @@ rx-hotkeys is a powerful and flexible TypeScript library for managing keyboard s
21
24
  npm install rxjs rx-hotkeys
22
25
  ```
23
26
 
24
- ## ⚠️ Breaking Changes (v3.0+)
27
+ ## ⚠️ Breaking Changes (v4.0+)
25
28
 
26
- Starting with v3.0, the API has been significantly updated for a more powerful and idiomatic RxJS experience. This is a major breaking change.
29
+ Starting with v4.0, the context management API has been fundamentally redesigned into a more powerful and robust dual-mode system.
27
30
 
28
- * `addCombination` and `addSequence` no longer accept a `callback` property in their configuration.
29
- * They now return an **`Observable<KeyboardEvent>`**.
30
- * You **must** now call `.subscribe()` on the returned Observable to execute your action.
31
+ * The old `setContext` method (which returned a boolean) has been replaced.
32
+ * The library now offers two distinct ways to manage contexts:
33
+ 1. **Context Stack (`enterContext`/`leaveContext`)**: For hierarchical UI states.
34
+ 2. **Context Override (`setContext` returns a `restore` function)**: For temporary, global state overrides.
35
+ * `getContext` method rename to `getActiveContext`.
31
36
 
32
- **Migration Example:**
33
-
34
- **Old (v1.x):**
35
- ```typescript
36
- // The old way
37
- keyManager.addCombination({
38
- id: "save",
39
- keys: { key: Keys.S, ctrlKey: true },
40
- callback: () => console.log("File saved!"),
41
- });
42
- ```
43
-
44
- **New (v3.0+):**
45
- ```typescript
46
- // The new, observable-based way
47
- const save$ = keyManager.addCombination({
48
- id: "save",
49
- keys: { key: Keys.S, ctrlKey: true }
50
- });
51
-
52
- const subscription = save$.subscribe(() => console.log("File saved!"));
53
-
54
- // Don't forget to unsubscribe when your component is destroyed!
55
- // The stream will also complete automatically if the shortcut is removed or keyManager.destroy() is called.
56
- // subscription.unsubscribe();
57
- ```
37
+ Please review the "Context Management" section below for details.
58
38
 
59
39
  ## Basic Usage
60
40
 
@@ -142,16 +122,55 @@ const submit$ = keyManager.addCombination({
142
122
  submit$.subscribe(() => console.log("Form submitted on Enter keyup!"));
143
123
  ```
144
124
 
145
- ### 6. Manage Contexts
125
+ ### 6. Context Management
126
+
127
+ You now have two powerful tools for managing contexts.
146
128
 
147
- Control which shortcuts are active by setting the context.
129
+ #### A) Context Stack (`enterContext` / `leaveContext`)
130
+
131
+ Use this for nested UI scopes that follow a clear hierarchy.
148
132
 
149
133
  ```typescript
150
- // Assuming some shortcuts are configured with context: "editor"
151
- keyManager.setContext("editor"); // Activates "editor" shortcuts and global shortcuts
134
+ // A shortcut with context: "editor" will NOT be active here.
135
+ console.log(keyManager.getActiveContext()); // null
136
+
137
+ // Activate the "editor" context
138
+ keyManager.enterContext("editor");
139
+ // Now, pressing Ctrl+S will trigger the "saveFile" shortcut.
140
+ console.log(keyManager.getActiveContext()); // 'editor'
152
141
 
153
- // To activate only global shortcuts (those with no context or context: null)
154
- keyManager.setContext(null);
142
+ // Imagine opening a dropdown menu inside the editor
143
+ keyManager.enterContext("dropdown-menu");
144
+ console.log(keyManager.getActiveContext()); // 'dropdown-menu'
145
+
146
+ // When the dropdown closes, leave its context
147
+ keyManager.leaveContext();
148
+ console.log(keyManager.getActiveContext()); // 'editor' (restored automatically)
149
+ ```
150
+
151
+ #### B) Context Override (`setContext` and `restore`)
152
+
153
+ Use this for temporary, high-priority states that should override everything else.
154
+
155
+ ```typescript
156
+ async function performSave() {
157
+ // Set a temporary "saving" context that overrides the stack.
158
+ const restore = keyManager.setContext('saving');
159
+
160
+ // Any shortcuts with context: 'saving' are now active.
161
+ // All other shortcuts (editor, etc.) are inactive.
162
+ console.log(keyManager.getActiveContext()); // 'saving'
163
+
164
+ try {
165
+ await someAsyncSaveOperation();
166
+ } finally {
167
+ // No matter what happens, call restore() to clear the override
168
+ // and return control to the context stack.
169
+ restore();
170
+ }
171
+
172
+ console.log(keyManager.getActiveContext()); // e.g., 'editor' (restored from the stack)
173
+ }
155
174
  ```
156
175
 
157
176
  ### 7. Clean Up
@@ -163,6 +182,99 @@ When the Hotkeys instance is no longer needed (e.g., component unmount), call `d
163
182
  keyManager.destroy();
164
183
  ```
165
184
 
185
+ ---
186
+
187
+ ## Usage with React
188
+
189
+ The library provides a dedicated React wrapper for the best developer experience.
190
+
191
+ ### Step 1: Wrap Your App with `HotkeysProvider`
192
+
193
+ First, import `HotkeysProvider` and wrap your root application component with it. This creates a single, shared instance of the hotkeys manager for your entire app.
194
+
195
+ ```jsx
196
+ // In your main App.js or index.js
197
+ import React from 'react';
198
+ import ReactDOM from 'react-dom/client';
199
+ import { HotkeysProvider } from 'rx-hotkeys/react';
200
+ import App from './App';
201
+
202
+ const root = ReactDOM.createRoot(document.getElementById('root'));
203
+ root.render(
204
+ <React.StrictMode>
205
+ <HotkeysProvider debugMode={true}>
206
+ <App />
207
+ </HotkeysProvider>
208
+ </React.StrictMode>
209
+ );
210
+ ```
211
+
212
+ ### Step 2: Use the `useHotkeys` Hook in Your Components
213
+
214
+ Now, you can use the `useHotkeys` and `useSequence` hooks anywhere in your component tree. The hook automatically handles registration, cleanup, and stale closures. You no longer need to provide a dependency array for your callback.
215
+
216
+ ```jsx
217
+ // src/components/Counter.jsx
218
+ import React, { useState } from 'react';
219
+ import { useHotkeys, useSequence } from 'rx-hotkeys/react';
220
+
221
+ export function Counter() {
222
+ const [count, setCount] = useState(0);
223
+
224
+ // The callback can safely use the latest component state (like `count`)
225
+ // without you needing to worry about stale closures or dependency arrays.
226
+ const handleIncrement = () => {
227
+ console.log(`Incrementing from ${count}...`);
228
+ setCount(count + 1);
229
+ };
230
+
231
+ // Register '+' key to increment.
232
+ useHotkeys('+', handleIncrement);
233
+
234
+ // Register 'c' key to increment, with options.
235
+ useHotkeys('c', handleIncrement, { preventDefault: true });
236
+
237
+ // Register a sequence to reset the counter.
238
+ useSequence('r -> e -> s -> e -> t', () => {
239
+ console.log('Resetting counter!');
240
+ setCount(0);
241
+ });
242
+
243
+ return (
244
+ <div>
245
+ <h2>Count: {count}</h2>
246
+ <p>Press '+' or 'c' to increment. Type 'reset' to reset.</p>
247
+ </div>
248
+ );
249
+ }
250
+ ```
251
+
252
+ ### Step 3: Manage Context with `useScopedHotkeysContext`
253
+
254
+ This hook allows a component (like a modal) to activate a specific context only while it is mounted.
255
+
256
+ ```jsx
257
+ // src/components/MyModal.jsx
258
+ import { useHotkeys, useScopedHotkeysContext } from 'rx-hotkeys/react';
259
+
260
+ export function MyModal({ onClose }) {
261
+ // This activates the 'modal' context for all children of this component.
262
+ // When MyModal unmounts, this context is automatically removed from the stack.
263
+ useScopedHotkeysContext('modal');
264
+
265
+ // This hotkey will only be active when the 'modal' context is active.
266
+ useHotkeys('escape', onClose, { context: 'modal' });
267
+
268
+ return (
269
+ <div className="modal">
270
+ <p>This is a modal. Press ESC to close.</p>
271
+ {/* ... other modal content ... */}
272
+ </div>
273
+ );
274
+ }
275
+ ```
276
+
277
+ ---
166
278
 
167
279
  ## API Reference
168
280
 
@@ -171,7 +283,7 @@ keyManager.destroy();
171
283
  * `Keys`: An exported constant object containing standard `KeyboardEvent.key` string values (e.g., `Keys.Enter`, `Keys.ArrowUp`, `Keys.A`). It's highly recommended to use these for type safety and to avoid typos.
172
284
  * `StandardKey`: A TypeScript type representing any valid key string from the `Keys` object.
173
285
 
174
- ### `Hotkeys` Class
286
+ ### `Hotkeys` Class (Core)
175
287
 
176
288
  `constructor(initialContext?: string | null, debugMode?: boolean)`
177
289
 
@@ -189,13 +301,25 @@ Registers a key sequence shortcut.
189
301
  * `config`: The `KeySequenceConfig` object.
190
302
  * Returns an `Observable<KeyboardEvent>` that emits the final `KeyboardEvent` when the sequence is completed.
191
303
 
192
- `setContext(contextName: string | null): boolean`
304
+ `enterContext(contextName: string | null): void`
305
+
306
+ Pushes a context onto the **context stack**. It becomes active if no override is set.
307
+
308
+ `leaveContext(): string | null | undefined`
193
309
 
194
- Sets the active context. Only shortcuts matching this context or global shortcuts will trigger.
310
+ Pops a context from the **context stack**, returning the context that was left.
195
311
 
196
- `getContext(): string | null`
312
+ `setContext(contextName: string | null): () => void`
197
313
 
198
- Returns the current active context name, or `null`.
314
+ Sets a temporary **override context**. Returns a `restore` function to clear the override.
315
+
316
+ `getActiveContext(): string | null`
317
+
318
+ Returns the current active context (checks for an override first, then the stack top).
319
+
320
+ `onContextChange$: Observable<string | null>`
321
+
322
+ A public `Observable` property that emits the active context whenever it changes.
199
323
 
200
324
  `remove(id: string): boolean`
201
325
 
@@ -219,6 +343,33 @@ Enables or disables console logging for debug purposes.
219
343
 
220
344
  Cleans up all subscriptions and resources. Essential to call to prevent memory leaks.
221
345
 
346
+ ### React Hooks (`rx-hotkeys/react`)
347
+
348
+ `HotkeysProvider({ children, initialContext?, debugMode? })`
349
+
350
+ A React component that provides the Hotkeys instance to its children.
351
+
352
+ `useHotkeys(keys, callback, options?)`
353
+
354
+ A React hook to register a key combination.
355
+ * `keys: string | string[]`: The shortcut definition (e.g., `'ctrl+s'`).
356
+ * `callback: (event: KeyboardEvent) => void`: The function to execute.
357
+ * `options?: HotkeyHookOptions`: Optional config for `preventDefault`, `context`, `target`, etc.
358
+
359
+ `useSequence(sequence, callback, options?)`
360
+
361
+ A React hook to register a key sequence.
362
+ * `sequence: string | string[]`: The sequence definition (e.g., `'g -> i'`).
363
+ * `callback: (event: KeyboardEvent) => void`: The function to execute.
364
+ * `options?: SequenceHookOptions`: Optional config for `preventDefault`, `context`, etc.
365
+
366
+ `useScopedHotkeysContext(context, enabled: boolean = true)`
367
+
368
+ A React hook to apply a specific context for the lifetime of the component.
369
+
370
+ `useHotkeysManager(): Hotkeys`
371
+ A hook to get direct access to the `Hotkeys` manager instance.
372
+
222
373
  ### Configuration Interfaces
223
374
 
224
375
  #### `KeyCombinationConfig`
@@ -252,6 +403,8 @@ Cleans up all subscriptions and resources. Essential to call to prevent memory l
252
403
  * **Aliases**: Common aliases are supported in string definitions, such as `cmd` for `Meta`, `option` for `Alt`, and `esc` for `Escape`.
253
404
  * **Special Keys**: For full type-safety, it is recommended to use the exported `Keys` object (e.g., `Keys.Enter`, `Keys.ArrowUp`).
254
405
 
406
+ ---
407
+
255
408
  ## Contributing
256
409
 
257
410
  Contributions are welcome! Please feel free to submit issues, fork the repository, and create pull requests.
@@ -111,18 +111,30 @@ export declare class Hotkeys {
111
111
  private static readonly KEYDOWN_EVENT;
112
112
  private static readonly KEYUP_EVENT;
113
113
  private static readonly LOG_PREFIX;
114
+ private static readonly NO_OVERRIDE;
114
115
  private keydownStreams;
115
116
  private keyupStreams;
116
- private activeContext$;
117
117
  private activeShortcuts;
118
118
  private debugMode;
119
+ private contextStack$;
120
+ private overrideContext$;
121
+ /**
122
+ * An Observable that emits the new active context name (or null) whenever it changes.
123
+ * The active context is the override context if one is set, otherwise it's the context
124
+ * from the top of the stack.
125
+ */
126
+ private readonly activeContext$;
119
127
  /**
120
128
  * Creates an instance of Hotkeys.
121
- * @param initialContext - Optional initial context name. Shortcuts will only trigger if their context matches this, or if they have no context defined.
129
+ * @param initialContext - Optional initial context name. This forms the base of the context stack.
122
130
  * @param debugMode - Optional. If true, debug messages will be logged to the console. Defaults to false.
123
131
  * @throws Error if not in a browser environment (i.e., `document` or `performance` is undefined).
124
132
  */
125
133
  constructor(initialContext?: string | null, debugMode?: boolean);
134
+ /**
135
+ * Helper method to determine the active context based on override and stack.
136
+ */
137
+ private _resolveActiveContext;
126
138
  /**
127
139
  * Gets or creates a shared event stream for a given event type and target.
128
140
  * @param eventType The type of event ("keydown" or "keyup").
@@ -131,19 +143,32 @@ export declare class Hotkeys {
131
143
  */
132
144
  private _getEventStream;
133
145
  /**
134
- * Sets the active context for shortcuts.
135
- * Only shortcuts matching this context (or shortcuts with no specific context defined)
136
- * will be active and can be triggered.
137
- * @param contextName - The name of the context (e.g., "modal", "editor", "global").
138
- * Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
139
- * @returns `true` if the context was changed, `false` if the new context was the same as the current one.
146
+ * Sets a temporary, high-priority override context that takes precedence over the context stack.
147
+ * @param contextName The override context to activate (can be a string or `null`).
148
+ * @returns A `restore` function that, when called, clears the override context, reverting to the stack.
140
149
  */
141
- setContext(contextName: string | null): boolean;
150
+ setContext(contextName: string | null): () => void;
142
151
  /**
143
- * Gets the current active context.
152
+ * @deprecated Rename to `getActiveContext`
153
+ * Gets the current active context, considering any override.
144
154
  * @returns The current context name as a string, or `null` if no context is set.
145
155
  */
146
156
  getContext(): string | null;
157
+ /**
158
+ * Gets the current active context, considering any override.
159
+ * @returns The current context name as a string, or `null` if no context is set.
160
+ */
161
+ getActiveContext(): string | null;
162
+ /**
163
+ * Pushes a new context onto the context stack. It will become active if no override context is set.
164
+ * @param contextName The name of the context to enter (e.g., "modal", "editor").
165
+ */
166
+ enterContext(contextName: string | null): void;
167
+ /**
168
+ * Pops the current context from the stack.
169
+ * @returns The context that was just left from the stack, or `undefined` if at the base.
170
+ */
171
+ leaveContext(): string | null | undefined;
147
172
  /**
148
173
  * Enables or disables debug logging for the Hotkeys instance.
149
174
  * When enabled, various internal actions and shortcut triggers will be logged to the console.
@@ -158,10 +183,6 @@ export declare class Hotkeys {
158
183
  hasShortcut(id: string): boolean;
159
184
  /**
160
185
  * An Observable that emits the new context name (or null) whenever the active context changes.
161
- * This allows external parts of the application to react to context transitions.
162
- *
163
- * Note: This observable benefits from the distinct check within the `setContext` method,
164
- * meaning it will only emit when the context value actually changes.
165
186
  *
166
187
  * @example
167
188
  * ```typescript
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../../src/core/hotkeys.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { fromEvent, BehaviorSubject, EMPTY, filter, map, bufferCount, withLatestFrom, tap, catchError, scan, merge, Subject, takeUntil, share, } from "rxjs";
1
+ import { fromEvent, BehaviorSubject, EMPTY, filter, map, bufferCount, withLatestFrom, tap, catchError, scan, merge, Subject, takeUntil, share, distinctUntilChanged, combineLatest, } from "rxjs";
2
2
  import { Keys, KeyAliases } from "./keys.js";
3
3
  // --- Enums, Interfaces and Types ---
4
4
  export var ShortcutTypes;
@@ -59,30 +59,57 @@ export class Hotkeys {
59
59
  static KEYDOWN_EVENT = "keydown";
60
60
  static KEYUP_EVENT = "keyup";
61
61
  static LOG_PREFIX = "Hotkeys:";
62
+ // --- Sentinel value for no override ---
63
+ static NO_OVERRIDE = Symbol("No Hotkey Override");
62
64
  keydownStreams;
63
65
  keyupStreams;
64
- activeContext$;
65
66
  activeShortcuts;
66
67
  debugMode;
68
+ // --- Separate states for stack and override ---
69
+ contextStack$;
70
+ overrideContext$;
71
+ /**
72
+ * An Observable that emits the new active context name (or null) whenever it changes.
73
+ * The active context is the override context if one is set, otherwise it's the context
74
+ * from the top of the stack.
75
+ */
76
+ activeContext$;
67
77
  /**
68
78
  * Creates an instance of Hotkeys.
69
- * @param initialContext - Optional initial context name. Shortcuts will only trigger if their context matches this, or if they have no context defined.
79
+ * @param initialContext - Optional initial context name. This forms the base of the context stack.
70
80
  * @param debugMode - Optional. If true, debug messages will be logged to the console. Defaults to false.
71
81
  * @throws Error if not in a browser environment (i.e., `document` or `performance` is undefined).
72
82
  */
73
83
  constructor(initialContext = null, debugMode = false) {
74
84
  this.debugMode = debugMode;
75
85
  if (typeof document === "undefined" || typeof performance === "undefined") {
76
- throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment with global "document" and "performance" objects.`);
86
+ throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment.`);
77
87
  }
78
88
  this.keydownStreams = new WeakMap();
79
89
  this.keyupStreams = new WeakMap();
80
- this.activeContext$ = new BehaviorSubject(initialContext);
81
90
  this.activeShortcuts = new Map();
91
+ // The context stack is the source of truth for the active context.
92
+ this.contextStack$ = new BehaviorSubject([initialContext]);
93
+ this.overrideContext$ = new BehaviorSubject(Hotkeys.NO_OVERRIDE);
94
+ // The public activeContext$ now correctly handles the sentinel value.
95
+ this.activeContext$ = combineLatest([
96
+ this.overrideContext$,
97
+ this.contextStack$.pipe(map(stack => stack.length > 0 ? stack[stack.length - 1] : null))
98
+ ]).pipe(map(([overrideCtx, stackCtx]) => this._resolveActiveContext(overrideCtx, stackCtx)), distinctUntilChanged());
82
99
  if (this.debugMode) {
83
100
  console.log(`${Hotkeys.LOG_PREFIX} Library initialized. Initial context: "${initialContext}". Debug mode: ${debugMode}.`);
101
+ // Optional: Log context changes for debugging
102
+ this.activeContext$.subscribe(newContext => {
103
+ console.log(`${Hotkeys.LOG_PREFIX} Active context changed to: ${newContext}`);
104
+ });
84
105
  }
85
106
  }
107
+ /**
108
+ * Helper method to determine the active context based on override and stack.
109
+ */
110
+ _resolveActiveContext(overrideCtx, stackCtx) {
111
+ return overrideCtx !== Hotkeys.NO_OVERRIDE ? overrideCtx : stackCtx;
112
+ }
86
113
  /**
87
114
  * Gets or creates a shared event stream for a given event type and target.
88
115
  * @param eventType The type of event ("keydown" or "keyup").
@@ -102,35 +129,78 @@ export class Hotkeys {
102
129
  return streamCache.get(target);
103
130
  }
104
131
  /**
105
- * Sets the active context for shortcuts.
106
- * Only shortcuts matching this context (or shortcuts with no specific context defined)
107
- * will be active and can be triggered.
108
- * @param contextName - The name of the context (e.g., "modal", "editor", "global").
109
- * Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
110
- * @returns `true` if the context was changed, `false` if the new context was the same as the current one.
132
+ * Sets a temporary, high-priority override context that takes precedence over the context stack.
133
+ * @param contextName The override context to activate (can be a string or `null`).
134
+ * @returns A `restore` function that, when called, clears the override context, reverting to the stack.
111
135
  */
112
136
  setContext(contextName) {
113
- const currentContext = this.activeContext$.getValue();
114
- if (currentContext === contextName) {
115
- if (this.debugMode) {
116
- // Optional: Log that no change is happening, or simply do nothing.
117
- console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
118
- }
119
- return false; // Context was NOT updated
120
- }
121
- // If we reach here, the context is actually changing.
122
137
  if (this.debugMode) {
123
- console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${currentContext}" to "${contextName}".`);
124
- }
125
- this.activeContext$.next(contextName);
126
- return true; // Context WAS updated
138
+ console.log(`${Hotkeys.LOG_PREFIX} Setting override context to: "${contextName}".`);
139
+ }
140
+ this.overrideContext$.next(contextName);
141
+ const restore = () => {
142
+ // Only clear the override if it's still the one we set.
143
+ if (this.overrideContext$.getValue() === contextName) {
144
+ if (this.debugMode) {
145
+ console.log(`${Hotkeys.LOG_PREFIX} Restoring/clearing override context from: "${contextName}".`);
146
+ }
147
+ // Restore now sets the special "NO_OVERRIDE" value.
148
+ this.overrideContext$.next(Hotkeys.NO_OVERRIDE);
149
+ }
150
+ };
151
+ return restore;
127
152
  }
128
153
  /**
129
- * Gets the current active context.
154
+ * @deprecated Rename to `getActiveContext`
155
+ * Gets the current active context, considering any override.
130
156
  * @returns The current context name as a string, or `null` if no context is set.
131
157
  */
132
158
  getContext() {
133
- return this.activeContext$.getValue();
159
+ console.warn(`${Hotkeys.LOG_PREFIX} "getContext" is deprecated. Use "getActiveContext()" or subscribe to "onContextChange$" instead.`);
160
+ return this.getActiveContext();
161
+ }
162
+ /**
163
+ * Gets the current active context, considering any override.
164
+ * @returns The current context name as a string, or `null` if no context is set.
165
+ */
166
+ getActiveContext() {
167
+ const overrideCtx = this.overrideContext$.getValue();
168
+ const stack = this.contextStack$.getValue();
169
+ const stackCtx = stack.length > 0 ? stack[stack.length - 1] : null;
170
+ // Also uses the abstracted helper method.
171
+ return this._resolveActiveContext(overrideCtx, stackCtx);
172
+ }
173
+ /**
174
+ * Pushes a new context onto the context stack. It will become active if no override context is set.
175
+ * @param contextName The name of the context to enter (e.g., "modal", "editor").
176
+ */
177
+ enterContext(contextName) {
178
+ const currentStack = this.contextStack$.getValue();
179
+ const newStack = [...currentStack, contextName];
180
+ if (this.debugMode) {
181
+ console.log(`${Hotkeys.LOG_PREFIX} Entering context: "${contextName}". New stack: [${newStack.join(", ")}]`);
182
+ }
183
+ this.contextStack$.next(newStack);
184
+ }
185
+ /**
186
+ * Pops the current context from the stack.
187
+ * @returns The context that was just left from the stack, or `undefined` if at the base.
188
+ */
189
+ leaveContext() {
190
+ const currentStack = this.contextStack$.getValue();
191
+ if (currentStack.length <= 1) {
192
+ if (this.debugMode) {
193
+ console.log(`${Hotkeys.LOG_PREFIX} Attempted to leave the base stack context. No change made.`);
194
+ }
195
+ return undefined; // Nothing was left
196
+ }
197
+ const leavingContext = currentStack[currentStack.length - 1];
198
+ const newStack = currentStack.slice(0, -1);
199
+ if (this.debugMode) {
200
+ console.log(`${Hotkeys.LOG_PREFIX} Leaving context: "${leavingContext}". New stack: [${newStack.join(", ")}]`);
201
+ }
202
+ this.contextStack$.next(newStack);
203
+ return leavingContext;
134
204
  }
135
205
  /**
136
206
  * Enables or disables debug logging for the Hotkeys instance.
@@ -159,10 +229,6 @@ export class Hotkeys {
159
229
  }
160
230
  /**
161
231
  * An Observable that emits the new context name (or null) whenever the active context changes.
162
- * This allows external parts of the application to react to context transitions.
163
- *
164
- * Note: This observable benefits from the distinct check within the `setContext` method,
165
- * meaning it will only emit when the context value actually changes.
166
232
  *
167
233
  * @example
168
234
  * ```typescript
@@ -176,7 +242,7 @@ export class Hotkeys {
176
242
  * ```
177
243
  */
178
244
  get onContextChange$() {
179
- return this.activeContext$.asObservable();
245
+ return this.activeContext$;
180
246
  }
181
247
  /**
182
248
  * Compares two sequences of StandardKey arrays to see if they are identical.
@@ -440,19 +506,18 @@ export class Hotkeys {
440
506
  return ctrlMatch && altMatch && shiftMatch && metaMatch;
441
507
  }), filter(event => compareKey(event.key, configuredMainKey)),
442
508
  // New filter for priority: Specific context > Global context
443
- filter(event => {
509
+ withLatestFrom(this.activeContext$), filter(([event, activeCtx]) => {
444
510
  if (context != null || strict) { // This shortcut is NOT global or strict
445
511
  return true;
446
512
  }
447
513
  // This shortcut IS global. Check for specific overrides.
448
- const currentSpecificContext = this.activeContext$.getValue();
449
- if (currentSpecificContext == null) { // No specific context active
514
+ if (activeCtx == null) { // No specific context active
450
515
  return true;
451
516
  }
452
517
  for (const [, otherAS] of this.activeShortcuts) {
453
518
  if (otherAS.config.id !== id &&
454
519
  "keys" in otherAS.config &&
455
- otherAS.config.context === currentSpecificContext &&
520
+ otherAS.config.context === activeCtx &&
456
521
  this._shortcutMatchesEvent(otherAS.config, event)) {
457
522
  if (this.debugMode) {
458
523
  console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
@@ -461,7 +526,7 @@ export class Hotkeys {
461
526
  }
462
527
  }
463
528
  return true; // Global can proceed
464
- }));
529
+ }), map(([event]) => event));
465
530
  observables.push(stream);
466
531
  }
467
532
  if (observables.length === 0) {
@@ -601,19 +666,18 @@ export class Hotkeys {
601
666
  }));
602
667
  }
603
668
  const terminator$ = new Subject();
604
- const finalShortcutWithPriority$ = shortcut$.pipe(filter((completedEvents) => {
669
+ const finalShortcutWithPriority$ = shortcut$.pipe(withLatestFrom(this.activeContext$), filter(([_completedEvents, activeCtx]) => {
605
670
  if (context != null || strict) { // This sequence is NOT global or strict
606
671
  return true;
607
672
  }
608
673
  // This sequence IS global. Check for specific overrides.
609
- const currentSpecificContext = this.activeContext$.getValue();
610
- if (currentSpecificContext == null) { // No specific context active
674
+ if (activeCtx == null) { // No specific context active
611
675
  return true;
612
676
  }
613
677
  for (const [, otherAS] of this.activeShortcuts) {
614
678
  if (otherAS.config.id !== id &&
615
679
  "sequence" in otherAS.config &&
616
- otherAS.config.context === currentSpecificContext &&
680
+ otherAS.config.context === activeCtx &&
617
681
  this._areSequencesIdentical(configuredSequence, typeof otherAS.config.sequence === "string" ? this._parseSequenceString(otherAS.config.sequence) : otherAS.config.sequence)) {
618
682
  if (this.debugMode) {
619
683
  console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${id}" suppressed by identical specific-context shortcut "${otherAS.config.id}".`);
@@ -622,7 +686,7 @@ export class Hotkeys {
622
686
  }
623
687
  }
624
688
  return true; // Global sequence can proceed
625
- }), tap((events) => {
689
+ }), map(([events]) => events), tap((events) => {
626
690
  if (this.debugMode) {
627
691
  const timeoutInfo = (sequenceTimeoutMs && sequenceTimeoutMs > 0) ? ` (with timeout logic)` : ` (no timeout logic)`;
628
692
  const preventAction = preventDefault ? ", preventing default for last event" : "";
@@ -692,8 +756,9 @@ export class Hotkeys {
692
756
  shortcut.terminator$.complete();
693
757
  });
694
758
  this.activeShortcuts.clear();
695
- this.activeContext$.complete();
759
+ this.contextStack$.complete();
696
760
  if (this.debugMode)
697
761
  console.log(`${Hotkeys.LOG_PREFIX} Library destroyed.`);
698
762
  }
699
763
  }
764
+ //# sourceMappingURL=hotkeys.js.map