rx-hotkeys 2.5.0 → 2.6.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
@@ -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, strict: 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")
@@ -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
@@ -197,8 +197,20 @@ export class Hotkeys {
197
197
  }
198
198
  return false;
199
199
  }
200
- filterByContext(source$, context) {
201
- 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));
202
214
  }
203
215
  _registerShortcut(config, subscription, type, detailsForLog) {
204
216
  const existingShortcut = this.activeShortcuts.get(config.id);
@@ -281,9 +293,10 @@ export class Hotkeys {
281
293
  * ```
282
294
  */
283
295
  addCombination(config) {
284
- const { keys, callback, context, preventDefault = false, id } = config;
285
- let finalShortcut$;
286
- 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
+ }
287
300
  const keyTriggers = Array.isArray(keys) ? keys : [keys];
288
301
  if (keyTriggers.length === 0) {
289
302
  console.warn(`${Hotkeys.LOG_PREFIX} "keys" array for combination shortcut "${id}" is empty. Shortcut not added.`);
@@ -299,7 +312,7 @@ export class Hotkeys {
299
312
  }
300
313
  const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig, logDetails } = parsedTrigger;
301
314
  logParts.push(`{ ${logDetails} }`);
302
- const stream = this.filterByContext(this.keydown$, context).pipe(filter(event => {
315
+ const stream = this.filterByContext(this.keydown$, context, strict).pipe(filter(event => {
303
316
  const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
304
317
  const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
305
318
  const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
@@ -308,7 +321,7 @@ export class Hotkeys {
308
321
  }), filter(event => compareKey(event.key, configuredMainKey)),
309
322
  // New filter for priority: Specific context > Global context
310
323
  filter(event => {
311
- if (config.context != null) { // This shortcut is NOT global
324
+ if (context != null || strict) { // This shortcut is NOT global or strict
312
325
  return true;
313
326
  }
314
327
  // This shortcut IS global. Check for specific overrides.
@@ -317,12 +330,12 @@ export class Hotkeys {
317
330
  return true;
318
331
  }
319
332
  for (const [, otherAS] of this.activeShortcuts) {
320
- if (otherAS.config.id !== config.id &&
333
+ if (otherAS.config.id !== id &&
321
334
  'keys' in otherAS.config &&
322
335
  otherAS.config.context === currentSpecificContext &&
323
336
  this._shortcutMatchesEvent(otherAS.config, event)) {
324
337
  if (this.debugMode) {
325
- console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${config.id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
338
+ console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
326
339
  }
327
340
  return false; // Suppress global
328
341
  }
@@ -336,8 +349,8 @@ export class Hotkeys {
336
349
  console.warn(`${Hotkeys.LOG_PREFIX} No valid key triggers for combination shortcut "${id}". Shortcut not added.`);
337
350
  return undefined;
338
351
  }
339
- finalShortcut$ = merge(...observables);
340
- 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];
341
354
  const subscription = finalShortcut$.pipe(tap(event => {
342
355
  if (this.debugMode) {
343
356
  const preventAction = preventDefault ? ", preventing default" : "";
@@ -379,7 +392,7 @@ export class Hotkeys {
379
392
  * ```
380
393
  */
381
394
  addSequence(config) {
382
- const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs } = config;
395
+ const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs, strict = false } = config;
383
396
  if (!Array.isArray(sequence) || sequence.length === 0) {
384
397
  console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty or invalid. Shortcut not added.`);
385
398
  return undefined;
@@ -389,10 +402,13 @@ export class Hotkeys {
389
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.`);
390
403
  return undefined;
391
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
+ }
392
408
  const configuredSequence = sequence;
393
409
  const sequenceLength = configuredSequence.length;
394
410
  let shortcut$;
395
- const baseKeydownStream$ = this.filterByContext(this.keydown$, context);
411
+ const baseKeydownStream$ = this.filterByContext(this.keydown$, context, strict);
396
412
  if (sequenceTimeoutMs && sequenceTimeoutMs > 0) {
397
413
  shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
398
414
  let { matchedEvents, lastEventTime } = acc;
@@ -450,7 +466,7 @@ export class Hotkeys {
450
466
  }));
451
467
  }
452
468
  const finalShortcutWithPriority$ = shortcut$.pipe(filter((completedEvents) => {
453
- if (config.context != null) { // This sequence is NOT global
469
+ if (context != null || strict) { // This sequence is NOT global or strict
454
470
  return true;
455
471
  }
456
472
  // This sequence IS global. Check for specific overrides.
@@ -459,12 +475,12 @@ export class Hotkeys {
459
475
  return true;
460
476
  }
461
477
  for (const [, otherAS] of this.activeShortcuts) {
462
- if (otherAS.config.id !== config.id &&
478
+ if (otherAS.config.id !== id &&
463
479
  "sequence" in otherAS.config &&
464
480
  otherAS.config.context === currentSpecificContext &&
465
- this._areSequencesIdentical(config.sequence, otherAS.config.sequence)) {
481
+ this._areSequencesIdentical(sequence, otherAS.config.sequence)) {
466
482
  if (this.debugMode) {
467
- console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${config.id}" suppressed by identical specific-context shortcut "${otherAS.config.id}".`);
483
+ console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${id}" suppressed by identical specific-context shortcut "${otherAS.config.id}".`);
468
484
  }
469
485
  return false; // Suppress global
470
486
  }
@@ -520,6 +520,100 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
520
520
  assert.strictEqual(globalSeqCallback.calledCount, 1, `Global sequence callback should have been called when in "anotherContext"`);
521
521
  });
522
522
  });
523
+ describe("Global Shortcut Context Behavior (`strict` flag)", () => {
524
+ let strictGlobalCallback;
525
+ let defaultGlobalCallback;
526
+ let specificContextCallback;
527
+ beforeEach(() => {
528
+ strictGlobalCallback = createMockFn();
529
+ defaultGlobalCallback = createMockFn();
530
+ specificContextCallback = createMockFn();
531
+ // 1. A specific shortcut for the "editor" context
532
+ keyManager.addCombination({
533
+ id: "editorSave",
534
+ keys: { key: Keys.S, ctrlKey: true },
535
+ callback: specificContextCallback,
536
+ context: "editor"
537
+ });
538
+ // 2. A "strictly global" shortcut, which only runs when context is null
539
+ keyManager.addCombination({
540
+ id: "strictGlobalOpen",
541
+ keys: { key: Keys.O, ctrlKey: true }, // Using shorthand is now possible!
542
+ callback: strictGlobalCallback,
543
+ strict: true, // `strict` is at the top level
544
+ });
545
+ // 3. A default global shortcut, which runs in any context unless overridden
546
+ keyManager.addCombination({
547
+ id: "defaultGlobalSave",
548
+ keys: { key: Keys.S, ctrlKey: true },
549
+ callback: defaultGlobalCallback,
550
+ // No `strict` flag here
551
+ });
552
+ });
553
+ it("should trigger both strict and default global shortcuts when context is null", () => {
554
+ keyManager.setContext(null);
555
+ dispatchKeyEvent(Keys.O, { ctrlKey: true });
556
+ assert.strictEqual(strictGlobalCallback.calledCount, 1, "Strictly global (Ctrl+O) should fire");
557
+ dispatchKeyEvent(Keys.S, { ctrlKey: true });
558
+ assert.strictEqual(defaultGlobalCallback.calledCount, 1, "Default global (Ctrl+S) should fire");
559
+ assert.strictEqual(specificContextCallback.calledCount, 0, "Specific context callback should not fire");
560
+ });
561
+ it("should suppress strict global but allow default global (which is then suppressed by priority)", () => {
562
+ keyManager.setContext("editor");
563
+ dispatchKeyEvent(Keys.O, { ctrlKey: true });
564
+ assert.strictEqual(strictGlobalCallback.calledCount, 0, "Strictly global (Ctrl+O) should NOT fire in 'editor' context");
565
+ dispatchKeyEvent(Keys.S, { ctrlKey: true });
566
+ assert.strictEqual(defaultGlobalCallback.calledCount, 0, "Default global (Ctrl+S) should be suppressed by the specific one");
567
+ assert.strictEqual(specificContextCallback.calledCount, 1, "Specific 'editor' callback (Ctrl+S) should fire and take priority");
568
+ });
569
+ it("should suppress strict global but trigger default global in a non-conflicting context", () => {
570
+ keyManager.setContext("someOtherContext");
571
+ dispatchKeyEvent(Keys.O, { ctrlKey: true });
572
+ assert.strictEqual(strictGlobalCallback.calledCount, 0, "Strictly global (Ctrl+O) should NOT fire in 'someOtherContext'");
573
+ dispatchKeyEvent(Keys.S, { ctrlKey: true });
574
+ assert.strictEqual(defaultGlobalCallback.calledCount, 1, "Default global (Ctrl+S) should fire since no override exists for this context");
575
+ assert.strictEqual(specificContextCallback.calledCount, 0, "Specific 'editor' callback should not fire");
576
+ });
577
+ });
578
+ describe("Sequence Context Behavior (`strict` flag)", () => {
579
+ // This test suite remains valid as `addSequence` already had the correct structure.
580
+ let strictSeqCallback;
581
+ let defaultSeqCallback;
582
+ let specificSeqCallback;
583
+ const testSequence = [Keys.M, Keys.A, Keys.P];
584
+ beforeEach(() => {
585
+ strictSeqCallback = createMockFn();
586
+ defaultSeqCallback = createMockFn();
587
+ specificSeqCallback = createMockFn();
588
+ keyManager.addSequence({ id: "strictSeq", sequence: [Keys.G, Keys.O], callback: strictSeqCallback, strict: true });
589
+ keyManager.addSequence({ id: "defaultSeq", sequence: testSequence, callback: defaultSeqCallback });
590
+ keyManager.addSequence({ id: "specificSeq", sequence: testSequence, callback: specificSeqCallback, context: "editor" });
591
+ });
592
+ it("should trigger both strict and default global sequences when context is null", () => {
593
+ keyManager.setContext(null);
594
+ [Keys.G, Keys.O].forEach(key => dispatchKeyEvent(key));
595
+ assert.strictEqual(strictSeqCallback.calledCount, 1, "Strict sequence should fire");
596
+ testSequence.forEach(key => dispatchKeyEvent(key));
597
+ assert.strictEqual(defaultSeqCallback.calledCount, 1, "Default sequence should fire");
598
+ assert.strictEqual(specificSeqCallback.calledCount, 0, "Specific sequence should not fire");
599
+ });
600
+ it("should suppress strict sequence and prioritize specific sequence in a matching context", () => {
601
+ keyManager.setContext("editor");
602
+ [Keys.G, Keys.O].forEach(key => dispatchKeyEvent(key));
603
+ assert.strictEqual(strictSeqCallback.calledCount, 0, "Strict sequence should NOT fire in 'editor' context");
604
+ testSequence.forEach(key => dispatchKeyEvent(key));
605
+ assert.strictEqual(defaultSeqCallback.calledCount, 0, "Default global sequence should be suppressed");
606
+ assert.strictEqual(specificSeqCallback.calledCount, 1, "Specific 'editor' sequence should fire");
607
+ });
608
+ it("should suppress strict sequence but trigger default global sequence in a non-conflicting context", () => {
609
+ keyManager.setContext("someOtherContext");
610
+ [Keys.G, Keys.O].forEach(key => dispatchKeyEvent(key));
611
+ assert.strictEqual(strictSeqCallback.calledCount, 0, "Strict sequence should NOT fire");
612
+ testSequence.forEach(key => dispatchKeyEvent(key));
613
+ assert.strictEqual(defaultSeqCallback.calledCount, 1, "Default global sequence should fire");
614
+ assert.strictEqual(specificSeqCallback.calledCount, 0, "Specific 'editor' sequence should not fire");
615
+ });
616
+ });
523
617
  describe("addSequence", () => {
524
618
  it("should trigger callback for a simple key sequence", () => {
525
619
  const config = { id: "seqGI", sequence: [Keys.G, Keys.I], callback: mockCallback };
package/dist/tt.d.ts ADDED
@@ -0,0 +1,125 @@
1
+ import { Subscription, Observable } from "rxjs";
2
+ import { type StandardKey } from "./keys.js";
3
+ export declare enum ShortcutTypes {
4
+ Combination = "combination",
5
+ Sequence = "sequence"
6
+ }
7
+ interface ShortcutConfigBase {
8
+ id: string;
9
+ callback: (event: KeyboardEvent) => void;
10
+ context?: string | null;
11
+ preventDefault?: boolean;
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;
22
+ }
23
+ /**
24
+ * Defines a single key trigger, which can be a StandardKey (for simple presses like "Escape")
25
+ * or an object specifying the main key and its modifiers (e.g., { key: Keys.S, ctrlKey: true }).
26
+ */
27
+ type KeyCombinationTrigger = {
28
+ /**
29
+ * The main key for the combination.
30
+ * This MUST be a value from the exported `Keys` object
31
+ * (e.g., `Keys.A`, `Keys.Enter`, `Keys.Escape`).
32
+ */
33
+ key: StandardKey;
34
+ ctrlKey?: boolean;
35
+ altKey?: boolean;
36
+ shiftKey?: boolean;
37
+ metaKey?: boolean;
38
+ } | StandardKey;
39
+ export interface KeyCombinationConfig extends ShortcutConfigBase {
40
+ /**
41
+ * Defines the key or key combination(s) that trigger the shortcut.
42
+ */
43
+ keys: KeyCombinationTrigger | KeyCombinationTrigger[];
44
+ }
45
+ export interface KeySequenceConfig extends ShortcutConfigBase {
46
+ /**
47
+ * An array of keys that form the sequence.
48
+ */
49
+ sequence: StandardKey[];
50
+ /**
51
+ * Optional: Timeout in milliseconds between consecutive key presses in the sequence.
52
+ * @default undefined
53
+ */
54
+ sequenceTimeoutMs?: number;
55
+ }
56
+ type ShortcutConfig = KeyCombinationConfig | KeySequenceConfig;
57
+ export interface ActiveShortcut {
58
+ id: string;
59
+ config: ShortcutConfig;
60
+ subscription: Subscription;
61
+ }
62
+ /**
63
+ * Manages keyboard shortcuts for web applications.
64
+ */
65
+ export declare class Hotkeys {
66
+ private static readonly KEYDOWN_EVENT;
67
+ private static readonly LOG_PREFIX;
68
+ private keydown$;
69
+ private activeContext$;
70
+ private activeShortcuts;
71
+ private debugMode;
72
+ /**
73
+ * Creates an instance of Hotkeys.
74
+ * @param initialContext - Optional initial context name.
75
+ * @param debugMode - Optional. If true, debug messages will be logged to the console.
76
+ */
77
+ constructor(initialContext?: string | null, debugMode?: boolean);
78
+ /**
79
+ * Sets the active context for shortcuts.
80
+ * @param contextName - The name of the context (e.g., "editor"). Pass `null` to clear the context.
81
+ * @returns `true` if the context was changed, `false` otherwise.
82
+ */
83
+ setContext(contextName: string | null): boolean;
84
+ /**
85
+ * Gets the current active context.
86
+ */
87
+ getContext(): string | null;
88
+ /**
89
+ * Enables or disables debug logging.
90
+ */
91
+ setDebugMode(enable: boolean): void;
92
+ /**
93
+ * Checks if a shortcut with the given ID is registered.
94
+ */
95
+ hasShortcut(id: string): boolean;
96
+ /**
97
+ * An Observable that emits the new context name whenever it changes.
98
+ */
99
+ get onContextChange$(): Observable<string | null>;
100
+ private _areSequencesIdentical;
101
+ private _shortcutMatchesEvent;
102
+ private _registerShortcut;
103
+ private _parseKeyTrigger;
104
+ addCombination(config: KeyCombinationConfig): string | undefined;
105
+ addSequence(config: KeySequenceConfig): string | undefined;
106
+ /**
107
+ * Removes a registered shortcut by its ID.
108
+ */
109
+ remove(id: string): boolean;
110
+ /**
111
+ * Retrieves a list of all currently active shortcuts.
112
+ */
113
+ getActiveShortcuts(): {
114
+ id: string;
115
+ description?: string;
116
+ context?: string | null;
117
+ type: ShortcutTypes;
118
+ }[];
119
+ /**
120
+ * Cleans up all active subscriptions.
121
+ */
122
+ destroy(): void;
123
+ }
124
+ export {};
125
+ //# sourceMappingURL=tt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tt.d.ts","sourceRoot":"","sources":["../src/tt.ts"],"names":[],"mappings":"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"}