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.
@@ -257,7 +257,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
257
257
  const result = keyManager.addCombination(config);
258
258
  assert.strictEqual(result, undefined, "Should return undefined for null key");
259
259
  assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
260
- assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "key" property in shortcut "nullKey". Key must be a non-empty value from Keys.`));
260
+ assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "key" property in shortcut "nullKey". Key must be a non-empty string value from Keys.`));
261
261
  });
262
262
  it("should pass the KeyboardEvent to the callback", () => {
263
263
  const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
@@ -442,6 +442,178 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
442
442
  });
443
443
  });
444
444
  });
445
+ describe("Context Priority (Specific > Global)", () => {
446
+ let globalCallback;
447
+ let specificCallback;
448
+ beforeEach(() => {
449
+ globalCallback = createMockFn();
450
+ specificCallback = createMockFn();
451
+ // Global shortcut: Ctrl+G
452
+ keyManager.addCombination({
453
+ id: "globalCtrlG",
454
+ keys: { key: Keys.G, ctrlKey: true },
455
+ callback: globalCallback,
456
+ context: null // Explicitly global
457
+ });
458
+ // Specific context shortcut: Ctrl+G in "editor" context
459
+ keyManager.addCombination({
460
+ id: "editorCtrlG",
461
+ keys: { key: Keys.G, ctrlKey: true },
462
+ callback: specificCallback,
463
+ context: "editor"
464
+ });
465
+ });
466
+ it("should only trigger specific context callback when specific context is active", () => {
467
+ keyManager.setContext("editor");
468
+ dispatchKeyEvent(Keys.G, { ctrlKey: true });
469
+ assert.strictEqual(specificCallback.calledCount, 1, "Specific callback should have been called");
470
+ assert.strictEqual(globalCallback.calledCount, 0, "Global callback should NOT have been called");
471
+ });
472
+ it("should only trigger global callback when no specific context is active (or context doesn't match)", () => {
473
+ keyManager.setContext(null); // No specific context
474
+ dispatchKeyEvent(Keys.G, { ctrlKey: true });
475
+ assert.strictEqual(specificCallback.calledCount, 0, "Specific callback should NOT have been called");
476
+ assert.strictEqual(globalCallback.calledCount, 1, "Global callback should have been called");
477
+ globalCallback.mockClear();
478
+ keyManager.setContext("anotherContext"); // Different specific context
479
+ dispatchKeyEvent(Keys.G, { ctrlKey: true });
480
+ assert.strictEqual(specificCallback.calledCount, 0, `Specific callback should NOT have been called for "anotherContext"`);
481
+ assert.strictEqual(globalCallback.calledCount, 1, `Global callback should have been called when in "anotherContext"`);
482
+ });
483
+ });
484
+ describe("Sequence Context Priority (Specific > Global)", () => {
485
+ let globalSeqCallback;
486
+ let specificSeqCallback;
487
+ const testSequence = [Keys.G, Keys.I];
488
+ beforeEach(() => {
489
+ globalSeqCallback = createMockFn();
490
+ specificSeqCallback = createMockFn();
491
+ keyManager.addSequence({
492
+ id: "globalGI",
493
+ sequence: testSequence,
494
+ callback: globalSeqCallback,
495
+ context: null // Global
496
+ });
497
+ keyManager.addSequence({
498
+ id: "editorGI",
499
+ sequence: testSequence,
500
+ callback: specificSeqCallback,
501
+ context: "editor" // Specific
502
+ });
503
+ });
504
+ it("should only trigger specific context sequence callback when specific context is active", () => {
505
+ keyManager.setContext("editor");
506
+ testSequence.forEach(key => dispatchKeyEvent(key));
507
+ assert.strictEqual(specificSeqCallback.calledCount, 1, "Specific sequence callback should have been called");
508
+ assert.strictEqual(globalSeqCallback.calledCount, 0, "Global sequence callback should NOT have been called");
509
+ });
510
+ it("should only trigger global sequence callback when no specific context is active (or context doesn't match)", () => {
511
+ keyManager.setContext(null); // No specific context
512
+ testSequence.forEach(key => dispatchKeyEvent(key));
513
+ assert.strictEqual(specificSeqCallback.calledCount, 0, "Specific sequence callback should NOT have been called");
514
+ assert.strictEqual(globalSeqCallback.calledCount, 1, "Global sequence callback should have been called");
515
+ globalSeqCallback.mockClear();
516
+ specificSeqCallback.mockClear(); // Clear for next part of test
517
+ keyManager.setContext("anotherContext"); // Different specific context
518
+ testSequence.forEach(key => dispatchKeyEvent(key));
519
+ assert.strictEqual(specificSeqCallback.calledCount, 0, `Specific sequence callback should NOT have been called for "anotherContext"`);
520
+ assert.strictEqual(globalSeqCallback.calledCount, 1, `Global sequence callback should have been called when in "anotherContext"`);
521
+ });
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
+ });
445
617
  describe("addSequence", () => {
446
618
  it("should trigger callback for a simple key sequence", () => {
447
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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"}