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 +32 -8
- package/dist/hotkeys.d.ts +9 -0
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +33 -17
- package/dist/hotkeys.test.js +94 -0
- package/dist/tt.d.ts +125 -0
- package/dist/tt.d.ts.map +1 -0
- package/dist/tt.js +384 -0
- package/dist/ttt.d.ts +164 -0
- package/dist/ttt.d.ts.map +1 -0
- package/dist/ttt.js +439 -0
- package/dist/tttt.d.ts +67 -0
- package/dist/tttt.d.ts.map +1 -0
- package/dist/tttt.js +301 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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")
|
package/dist/hotkeys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"AAAA,OAAO,EACyB,YAAY,EAAE,UAAU,EAEvD,MAAM,MAAM,CAAC;AACd,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAI7C,oBAAY,aAAa;IACrB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;CACxB;AAcD,UAAU,kBAAkB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
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]) =>
|
|
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
|
-
|
|
286
|
-
|
|
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 (
|
|
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 !==
|
|
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 "${
|
|
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 (
|
|
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 !==
|
|
478
|
+
if (otherAS.config.id !== id &&
|
|
463
479
|
"sequence" in otherAS.config &&
|
|
464
480
|
otherAS.config.context === currentSpecificContext &&
|
|
465
|
-
this._areSequencesIdentical(
|
|
481
|
+
this._areSequencesIdentical(sequence, otherAS.config.sequence)) {
|
|
466
482
|
if (this.debugMode) {
|
|
467
|
-
console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${
|
|
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
|
}
|
package/dist/hotkeys.test.js
CHANGED
|
@@ -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
|
package/dist/tt.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tt.d.ts","sourceRoot":"","sources":["../src/tt.ts"],"names":[],"mappings":"AAAA,OAAO,EACyB,YAAY,EAAE,UAAU,EAEvD,MAAM,MAAM,CAAC;AACd,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAI7C,oBAAY,aAAa;IACrB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;CACxB;AAcD,UAAU,kBAAkB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,KAAK,qBAAqB,GAAG;IACzB;;;;OAIG;IACH,GAAG,EAAE,WAAW,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,WAAW,CAAC;AAGhB,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC5D;;OAEG;IACH,IAAI,EAAE,qBAAqB,GAAG,qBAAqB,EAAE,CAAC;CAEzD;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD;;OAEG;IACH,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAE9B;AAED,KAAK,cAAc,GAAG,oBAAoB,GAAG,iBAAiB,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,YAAY,EAAE,YAAY,CAAC;CAC9B;AAkBD;;GAEG;AACH,qBAAa,OAAO;IAChB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAa;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAc;IAEhD,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,cAAc,CAAiC;IACvD,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,SAAS,CAAU;IAE3B;;;;OAIG;gBACS,cAAc,GAAE,MAAM,GAAG,IAAW,EAAE,SAAS,GAAE,OAAe;IAe5E;;;;OAIG;IACI,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAetD;;OAEG;IACI,UAAU,IAAI,MAAM,GAAG,IAAI;IAIlC;;OAEG;IACI,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAM1C;;OAEG;IACI,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIvC;;OAEG;IACH,IAAW,gBAAgB,IAAI,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,CAEvD;IAED,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,qBAAqB;IAiC7B,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,gBAAgB;IAkCjB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS;IA0FhE,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS;IAkGjE;;OAEG;IACI,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYlC;;OAEG;IACI,kBAAkB,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAE,EAAE;IAajH;;OAEG;IACI,OAAO,IAAI,IAAI;CAMzB"}
|