rx-hotkeys 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/hotkeys.d.ts +50 -8
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +136 -41
- package/dist/hotkeys.test.js +194 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,7 +165,7 @@ Cleans up all subscriptions and resources. Essential to call to prevent memory l
|
|
|
165
165
|
`KeyCombinationConfig`
|
|
166
166
|
|
|
167
167
|
* `id: string` (required): Unique identifier for the shortcut.
|
|
168
|
-
* `keys: { key: StandardKey; ctrlKey?: boolean; altKey?: boolean; shiftKey?: boolean; metaKey?: boolean; }` (required): Defines the main key (from Keys) and optional modifier keys.
|
|
168
|
+
* `keys: { key: StandardKey; ctrlKey?: boolean; altKey?: boolean; shiftKey?: boolean; metaKey?: boolean; } | StandardKey` (required): Defines the main key (from Keys) and optional modifier keys.
|
|
169
169
|
* `callback: (event?: KeyboardEvent) => void` (required): Function to execute when the shortcut is triggered. The triggering `KeyboardEvent` is passed as an argument.
|
|
170
170
|
* `context?: string | null`: Specifies the context in which this shortcut is active. If `null` or `undefined`, it's a global shortcut.
|
|
171
171
|
* `preventDefault?: boolean`: If true, `event.preventDefault()` will be called when the shortcut triggers. Defaults to `false`.
|
package/dist/hotkeys.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { Observable } from "rxjs";
|
|
1
2
|
import { StandardKey } from "./keys.js";
|
|
3
|
+
export declare enum ShortcutTypes {
|
|
4
|
+
Combination = "combination",
|
|
5
|
+
Sequence = "sequence"
|
|
6
|
+
}
|
|
2
7
|
interface ShortcutConfigBase {
|
|
3
8
|
id: string;
|
|
4
9
|
callback: (event?: KeyboardEvent) => void;
|
|
@@ -7,6 +12,17 @@ interface ShortcutConfigBase {
|
|
|
7
12
|
description?: string;
|
|
8
13
|
}
|
|
9
14
|
export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
15
|
+
/**
|
|
16
|
+
* Defines the key or key combination.
|
|
17
|
+
* Can be an object specifying the main `key` (from `StandardKey`) and optional
|
|
18
|
+
* modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
|
|
19
|
+
* Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
|
|
20
|
+
*
|
|
21
|
+
* Alternatively, for a simple key press without any modifiers, this can be
|
|
22
|
+
* a `StandardKey` directly.
|
|
23
|
+
* Example: `Keys.Escape` for the Escape key. When using this shorthand,
|
|
24
|
+
* it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
|
|
25
|
+
*/
|
|
10
26
|
keys: {
|
|
11
27
|
/**
|
|
12
28
|
* The main key for the combination.
|
|
@@ -23,7 +39,7 @@ export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
|
23
39
|
altKey?: boolean;
|
|
24
40
|
shiftKey?: boolean;
|
|
25
41
|
metaKey?: boolean;
|
|
26
|
-
};
|
|
42
|
+
} | StandardKey;
|
|
27
43
|
}
|
|
28
44
|
export interface KeySequenceConfig extends ShortcutConfigBase {
|
|
29
45
|
/**
|
|
@@ -81,6 +97,25 @@ export declare class Hotkeys {
|
|
|
81
97
|
* @param enable - True to enable debug logs, false to disable.
|
|
82
98
|
*/
|
|
83
99
|
setDebugMode(enable: boolean): void;
|
|
100
|
+
/**
|
|
101
|
+
* An Observable that emits the new context name (or null) whenever the active context changes.
|
|
102
|
+
* This allows external parts of the application to react to context transitions.
|
|
103
|
+
*
|
|
104
|
+
* Note: This observable benefits from the distinct check within the `setContext` method,
|
|
105
|
+
* meaning it will only emit when the context value actually changes.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const hotkeys = new Hotkeys();
|
|
110
|
+
* const subscription = hotkeys.onContextChange$.subscribe(newContext => {
|
|
111
|
+
* console.log("Hotkey context changed to:", newContext);
|
|
112
|
+
* // Update UI or perform other actions
|
|
113
|
+
* });
|
|
114
|
+
* // To unsubscribe when no longer needed:
|
|
115
|
+
* // subscription.unsubscribe();
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
get onContextChange$(): Observable<string | null>;
|
|
84
119
|
/**
|
|
85
120
|
* Checks if a shortcut with the given ID is currently registered and active.
|
|
86
121
|
* @param id - The unique ID of the shortcut to check.
|
|
@@ -90,22 +125,29 @@ export declare class Hotkeys {
|
|
|
90
125
|
private filterByContext;
|
|
91
126
|
private _registerShortcut;
|
|
92
127
|
/**
|
|
93
|
-
* Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter).
|
|
94
|
-
* The callback is triggered when the specified key and modifier keys are pressed
|
|
128
|
+
* Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
|
|
129
|
+
* The callback is triggered when the specified key and modifier keys (if any) are pressed.
|
|
95
130
|
* @param config - Configuration object for the key combination.
|
|
96
131
|
* See {@link KeyCombinationConfig} for details.
|
|
97
|
-
* The `key` property
|
|
98
|
-
* @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid
|
|
132
|
+
* The `key` property (or the direct `StandardKey` if using shorthand) must be a value from the `Keys` object.
|
|
133
|
+
* @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid.
|
|
99
134
|
* A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
|
|
100
135
|
* @example
|
|
101
136
|
* ```typescript
|
|
102
|
-
* import { Keys } from
|
|
137
|
+
* import { Keys } from "./keys";
|
|
138
|
+
* // For Ctrl+S
|
|
103
139
|
* keyManager.addCombination({
|
|
104
140
|
* id: "saveFile",
|
|
105
141
|
* keys: { key: Keys.S, ctrlKey: true },
|
|
106
142
|
* callback: () => console.log("File saved!"),
|
|
107
143
|
* context: "editor"
|
|
108
144
|
* });
|
|
145
|
+
* // For just the Escape key
|
|
146
|
+
* keyManager.addCombination({
|
|
147
|
+
* id: "closeModal",
|
|
148
|
+
* keys: Keys.Escape, // Shorthand syntax
|
|
149
|
+
* callback: () => console.log("Modal closed!")
|
|
150
|
+
* });
|
|
109
151
|
* ```
|
|
110
152
|
*/
|
|
111
153
|
addCombination(config: KeyCombinationConfig): string | undefined;
|
|
@@ -120,7 +162,7 @@ export declare class Hotkeys {
|
|
|
120
162
|
* A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
|
|
121
163
|
* @example
|
|
122
164
|
* ```typescript
|
|
123
|
-
* import { Keys } from
|
|
165
|
+
* import { Keys } from "./keys";
|
|
124
166
|
* keyManager.addSequence({
|
|
125
167
|
* id: "konamiCode",
|
|
126
168
|
* sequence: [Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown, Keys.A, Keys.B],
|
|
@@ -149,7 +191,7 @@ export declare class Hotkeys {
|
|
|
149
191
|
id: string;
|
|
150
192
|
description?: string;
|
|
151
193
|
context?: string | null;
|
|
152
|
-
type:
|
|
194
|
+
type: ShortcutTypes;
|
|
153
195
|
}[];
|
|
154
196
|
/**
|
|
155
197
|
* Cleans up all active subscriptions and resources used by the Hotkeys instance.
|
package/dist/hotkeys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hotkeys.d.ts","sourceRoot":"","sources":["../src/hotkeys.ts"],"names":[],"mappings":"AAAA,OAAO,EACuC,UAAU,EAEvD,MAAM,MAAM,CAAC;AACd,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIxC,oBAAY,aAAa;IACrB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;CACxB;AAED,UAAU,kBAAkB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC5D;;;;;;;;;;OAUG;IACH,IAAI,EAAE;QACF;;;;;;;;;WASG;QACH,GAAG,EAAE,WAAW,CAAC;QACjB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD;;;;;;;;OAQG;IACH,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAyCD;;;;GAIG;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;;;;;OAKG;gBACS,cAAc,GAAE,MAAM,GAAG,IAAW,EAAE,SAAS,GAAE,OAAe;IAe5E;;;;;;OAMG;IACI,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAiBnD;;;OAGG;IACI,UAAU,IAAI,MAAM,GAAG,IAAI;IAIlC;;;;OAIG;IACI,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAa1C;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAW,gBAAgB,IAAI,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,CAEvD;IAED;;;;OAIG;IACI,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIvC,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,iBAAiB;IAkBzB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACI,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS;IA6EvE;;;;;;;;;;;;;;;;;;;OAmBG;IACI,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS;IAiHjE;;;;;;OAMG;IACI,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYlC;;;;;;OAMG;IACI,kBAAkB,IAAI;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAC,EAAE;IAa/G;;;;;OAKG;IACI,OAAO,IAAI,IAAI;CAOzB"}
|
package/dist/hotkeys.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { fromEvent, BehaviorSubject, EMPTY, filter, map, bufferCount, withLatestFrom, tap, catchError, scan, } from "rxjs";
|
|
2
|
+
// --- Enums, Interfaces and Types ---
|
|
3
|
+
export var ShortcutTypes;
|
|
4
|
+
(function (ShortcutTypes) {
|
|
5
|
+
ShortcutTypes["Combination"] = "combination";
|
|
6
|
+
ShortcutTypes["Sequence"] = "sequence";
|
|
7
|
+
})(ShortcutTypes || (ShortcutTypes = {}));
|
|
2
8
|
// --- Helper function to compare keys ---
|
|
3
9
|
/**
|
|
4
10
|
* Compares a browser event's key with a configured key.
|
|
@@ -15,6 +21,12 @@ function compareKey(eventKey, configuredKey) {
|
|
|
15
21
|
return eventKey === configuredKey;
|
|
16
22
|
}
|
|
17
23
|
// --- Hotkeys Library ---
|
|
24
|
+
var EmitStates;
|
|
25
|
+
(function (EmitStates) {
|
|
26
|
+
EmitStates[EmitStates["Emit"] = 0] = "Emit";
|
|
27
|
+
EmitStates[EmitStates["Ignore"] = 1] = "Ignore";
|
|
28
|
+
EmitStates[EmitStates["InProgress"] = 2] = "InProgress";
|
|
29
|
+
})(EmitStates || (EmitStates = {}));
|
|
18
30
|
/**
|
|
19
31
|
* Manages keyboard shortcuts for web applications.
|
|
20
32
|
* Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
|
|
@@ -36,7 +48,7 @@ export class Hotkeys {
|
|
|
36
48
|
constructor(initialContext = null, debugMode = false) {
|
|
37
49
|
this.debugMode = debugMode;
|
|
38
50
|
if (typeof document === "undefined" || typeof performance === "undefined") {
|
|
39
|
-
throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment with global
|
|
51
|
+
throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment with global "document" and "performance" objects.`);
|
|
40
52
|
}
|
|
41
53
|
this.keydown$ = fromEvent(document, Hotkeys.KEYDOWN_EVENT);
|
|
42
54
|
this.activeContext$ = new BehaviorSubject(initialContext);
|
|
@@ -53,8 +65,17 @@ export class Hotkeys {
|
|
|
53
65
|
* Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
|
|
54
66
|
*/
|
|
55
67
|
setContext(contextName) {
|
|
68
|
+
if (this.activeContext$.getValue() === contextName) {
|
|
69
|
+
if (this.debugMode) {
|
|
70
|
+
// Optional: Log that no change is happening, or simply do nothing.
|
|
71
|
+
console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
|
|
72
|
+
}
|
|
73
|
+
return; // Context is the same, so no further action is needed.
|
|
74
|
+
}
|
|
75
|
+
// If we reach here, the context is actually changing.
|
|
76
|
+
const oldContext = this.activeContext$.getValue(); // For more informative logging
|
|
56
77
|
if (this.debugMode) {
|
|
57
|
-
console.log(`${Hotkeys.LOG_PREFIX} Context changed to "${contextName}"
|
|
78
|
+
console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${oldContext}" to "${contextName}".`);
|
|
58
79
|
}
|
|
59
80
|
this.activeContext$.next(contextName);
|
|
60
81
|
}
|
|
@@ -71,11 +92,38 @@ export class Hotkeys {
|
|
|
71
92
|
* @param enable - True to enable debug logs, false to disable.
|
|
72
93
|
*/
|
|
73
94
|
setDebugMode(enable) {
|
|
74
|
-
this.debugMode
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
if (this.debugMode === enable) { // Check if state is actually changing
|
|
96
|
+
return; // If no change, do nothing (no log)
|
|
97
|
+
}
|
|
98
|
+
this.debugMode = enable; // Set the new state
|
|
99
|
+
if (enable) { // Log based on the NEW state after a change
|
|
100
|
+
console.log(`${Hotkeys.LOG_PREFIX} Debug mode enabled.`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(`${Hotkeys.LOG_PREFIX} Debug mode disabled.`);
|
|
77
104
|
}
|
|
78
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* An Observable that emits the new context name (or null) whenever the active context changes.
|
|
108
|
+
* This allows external parts of the application to react to context transitions.
|
|
109
|
+
*
|
|
110
|
+
* Note: This observable benefits from the distinct check within the `setContext` method,
|
|
111
|
+
* meaning it will only emit when the context value actually changes.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const hotkeys = new Hotkeys();
|
|
116
|
+
* const subscription = hotkeys.onContextChange$.subscribe(newContext => {
|
|
117
|
+
* console.log("Hotkey context changed to:", newContext);
|
|
118
|
+
* // Update UI or perform other actions
|
|
119
|
+
* });
|
|
120
|
+
* // To unsubscribe when no longer needed:
|
|
121
|
+
* // subscription.unsubscribe();
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
get onContextChange$() {
|
|
125
|
+
return this.activeContext$.asObservable();
|
|
126
|
+
}
|
|
79
127
|
/**
|
|
80
128
|
* Checks if a shortcut with the given ID is currently registered and active.
|
|
81
129
|
* @param id - The unique ID of the shortcut to check.
|
|
@@ -87,7 +135,8 @@ export class Hotkeys {
|
|
|
87
135
|
filterByContext(source$, context) {
|
|
88
136
|
return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => context == null || context === activeCtx), map(([event, /* _activeCtx */]) => event));
|
|
89
137
|
}
|
|
90
|
-
_registerShortcut(config, subscription, type,
|
|
138
|
+
_registerShortcut(config, subscription, type, // Changed to use Enum
|
|
139
|
+
detailsForLog) {
|
|
91
140
|
const existingShortcut = this.activeShortcuts.get(config.id);
|
|
92
141
|
if (existingShortcut) {
|
|
93
142
|
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
|
|
@@ -100,35 +149,81 @@ export class Hotkeys {
|
|
|
100
149
|
return config.id;
|
|
101
150
|
}
|
|
102
151
|
/**
|
|
103
|
-
* Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter).
|
|
104
|
-
* The callback is triggered when the specified key and modifier keys are pressed
|
|
152
|
+
* Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape).
|
|
153
|
+
* The callback is triggered when the specified key and modifier keys (if any) are pressed.
|
|
105
154
|
* @param config - Configuration object for the key combination.
|
|
106
155
|
* See {@link KeyCombinationConfig} for details.
|
|
107
|
-
* The `key` property
|
|
108
|
-
* @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid
|
|
156
|
+
* The `key` property (or the direct `StandardKey` if using shorthand) must be a value from the `Keys` object.
|
|
157
|
+
* @returns The ID of the registered shortcut if successful, or `undefined` if the configuration is invalid.
|
|
109
158
|
* A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
|
|
110
159
|
* @example
|
|
111
160
|
* ```typescript
|
|
112
|
-
* import { Keys } from
|
|
161
|
+
* import { Keys } from "./keys";
|
|
162
|
+
* // For Ctrl+S
|
|
113
163
|
* keyManager.addCombination({
|
|
114
164
|
* id: "saveFile",
|
|
115
165
|
* keys: { key: Keys.S, ctrlKey: true },
|
|
116
166
|
* callback: () => console.log("File saved!"),
|
|
117
167
|
* context: "editor"
|
|
118
168
|
* });
|
|
169
|
+
* // For just the Escape key
|
|
170
|
+
* keyManager.addCombination({
|
|
171
|
+
* id: "closeModal",
|
|
172
|
+
* keys: Keys.Escape, // Shorthand syntax
|
|
173
|
+
* callback: () => console.log("Modal closed!")
|
|
174
|
+
* });
|
|
119
175
|
* ```
|
|
120
176
|
*/
|
|
121
177
|
addCombination(config) {
|
|
122
178
|
const { keys, callback, context, preventDefault = false, id } = config;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
179
|
+
let configuredMainKey;
|
|
180
|
+
let ctrlKeyConfig;
|
|
181
|
+
let altKeyConfig;
|
|
182
|
+
let shiftKeyConfig;
|
|
183
|
+
let metaKeyConfig;
|
|
184
|
+
let keyDetailsForLog;
|
|
185
|
+
if (typeof keys === "string") {
|
|
186
|
+
// Shorthand: keys is StandardKey, implying no modifiers should be active
|
|
187
|
+
if (keys.trim() === "") {
|
|
188
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys" (shorthand) for combination shortcut "${id}". Key string must not be empty. Shortcut not added.`);
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
configuredMainKey = keys;
|
|
192
|
+
ctrlKeyConfig = false; // Shorthand implies no modifiers
|
|
193
|
+
altKeyConfig = false;
|
|
194
|
+
shiftKeyConfig = false;
|
|
195
|
+
metaKeyConfig = false;
|
|
196
|
+
keyDetailsForLog = `key: "${keys}" (no modifiers implied)`;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Object form
|
|
200
|
+
if (!keys || !keys.key || typeof keys.key !== "string" || keys.key.trim() === "") {
|
|
201
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "keys.key" for combination shortcut "${id}". Key must be a non-empty value from Keys. Shortcut not added.`);
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
configuredMainKey = keys.key;
|
|
205
|
+
ctrlKeyConfig = keys.ctrlKey;
|
|
206
|
+
altKeyConfig = keys.altKey;
|
|
207
|
+
shiftKeyConfig = keys.shiftKey;
|
|
208
|
+
metaKeyConfig = keys.metaKey;
|
|
209
|
+
keyDetailsForLog = `key: "${keys.key}"` +
|
|
210
|
+
(keys.ctrlKey !== undefined ? `, ctrlKey: ${keys.ctrlKey}` : "") +
|
|
211
|
+
(keys.altKey !== undefined ? `, altKey: ${keys.altKey}` : "") +
|
|
212
|
+
(keys.shiftKey !== undefined ? `, shiftKey: ${keys.shiftKey}` : "") +
|
|
213
|
+
(keys.metaKey !== undefined ? `, metaKey: ${keys.metaKey}` : "");
|
|
126
214
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
215
|
+
const shortcut$ = this.filterByContext(this.keydown$, context).pipe(filter(event => {
|
|
216
|
+
// For shorthand (where modifiers are explicitly false), we want an exact match.
|
|
217
|
+
// For object form:
|
|
218
|
+
// - if modifier is true, event.modifier must be true.
|
|
219
|
+
// - if modifier is false, event.modifier must be false.
|
|
220
|
+
// - if modifier is undefined, we don't care about event.modifier.
|
|
221
|
+
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
222
|
+
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
223
|
+
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
224
|
+
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
225
|
+
return ctrlMatch && altMatch && shiftMatch && metaMatch;
|
|
226
|
+
}), filter(event => compareKey(event.key, configuredMainKey)), tap(event => {
|
|
132
227
|
if (this.debugMode) {
|
|
133
228
|
const preventAction = preventDefault ? ", preventing default" : "";
|
|
134
229
|
console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered${preventAction}.`);
|
|
@@ -147,12 +242,7 @@ export class Hotkeys {
|
|
|
147
242
|
console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for combination shortcut "${id}":`, e);
|
|
148
243
|
}
|
|
149
244
|
});
|
|
150
|
-
|
|
151
|
-
(keys.ctrlKey !== undefined ? `, ctrlKey: ${keys.ctrlKey}` : "") +
|
|
152
|
-
(keys.altKey !== undefined ? `, altKey: ${keys.altKey}` : "") +
|
|
153
|
-
(keys.shiftKey !== undefined ? `, shiftKey: ${keys.shiftKey}` : "") +
|
|
154
|
-
(keys.metaKey !== undefined ? `, metaKey: ${keys.metaKey}` : "");
|
|
155
|
-
return this._registerShortcut(config, subscription, "Combination", `Keys: { ${keyDetails} }`);
|
|
245
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Combination, `Keys: { ${keyDetailsForLog} }`); // Use Enum
|
|
156
246
|
}
|
|
157
247
|
/**
|
|
158
248
|
* Registers a key sequence shortcut (e.g., g -> i, or ArrowUp -> ArrowUp -> ArrowDown).
|
|
@@ -165,7 +255,7 @@ export class Hotkeys {
|
|
|
165
255
|
* A warning is logged to the console if the configuration is invalid or if a shortcut with the same ID is overwritten.
|
|
166
256
|
* @example
|
|
167
257
|
* ```typescript
|
|
168
|
-
* import { Keys } from
|
|
258
|
+
* import { Keys } from "./keys";
|
|
169
259
|
* keyManager.addSequence({
|
|
170
260
|
* id: "konamiCode",
|
|
171
261
|
* sequence: [Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown, Keys.A, Keys.B],
|
|
@@ -180,7 +270,7 @@ export class Hotkeys {
|
|
|
180
270
|
console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty or invalid. Shortcut not added.`);
|
|
181
271
|
return undefined;
|
|
182
272
|
}
|
|
183
|
-
if (sequence.some(key => typeof key !==
|
|
273
|
+
if (sequence.some(key => typeof key !== "string" || key.trim() === "")) {
|
|
184
274
|
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key in sequence for shortcut "${id}". All keys must be non-empty strings from Keys. Shortcut not added.`);
|
|
185
275
|
return undefined;
|
|
186
276
|
}
|
|
@@ -192,48 +282,52 @@ export class Hotkeys {
|
|
|
192
282
|
shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
|
|
193
283
|
let { matchedEvents, lastEventTime } = acc;
|
|
194
284
|
const currentTime = performance.now();
|
|
195
|
-
if (acc.emitState ===
|
|
285
|
+
if (acc.emitState === EmitStates.Emit) {
|
|
196
286
|
matchedEvents = [];
|
|
197
287
|
lastEventTime = 0;
|
|
198
288
|
}
|
|
199
289
|
if (matchedEvents.length > 0 && (currentTime - lastEventTime > sequenceTimeoutMs)) {
|
|
200
290
|
if (this.debugMode) {
|
|
201
|
-
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) attempt timed out. Matched: ${matchedEvents.map(e => e.key).join(
|
|
291
|
+
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) attempt timed out. Matched: ${matchedEvents.map(e => e.key).join(",")}. Resetting.`);
|
|
202
292
|
}
|
|
203
293
|
matchedEvents = [];
|
|
204
294
|
}
|
|
205
295
|
const nextExpectedKeyIndex = matchedEvents.length;
|
|
206
296
|
if (nextExpectedKeyIndex >= sequenceLength) {
|
|
297
|
+
// Sequence was already emitted or buffer is too long (should not happen if reset correctly)
|
|
298
|
+
// Start new sequence if current key matches the first key of the sequence
|
|
207
299
|
if (sequenceLength > 0 && compareKey(event.key, configuredSequence[0])) {
|
|
208
|
-
return { matchedEvents: [event], lastEventTime: currentTime, emitState:
|
|
300
|
+
return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
|
|
209
301
|
}
|
|
210
|
-
return { matchedEvents: [], lastEventTime: 0, emitState:
|
|
302
|
+
return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
|
|
211
303
|
}
|
|
212
304
|
if (compareKey(event.key, configuredSequence[nextExpectedKeyIndex])) {
|
|
213
305
|
const newMatchedEvents = [...matchedEvents, event];
|
|
214
306
|
if (newMatchedEvents.length === sequenceLength) {
|
|
215
|
-
if (this.debugMode)
|
|
307
|
+
if (this.debugMode && !acc.emitState)
|
|
216
308
|
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) matched.`);
|
|
217
|
-
return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState:
|
|
309
|
+
return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.Emit };
|
|
218
310
|
}
|
|
219
311
|
else {
|
|
220
|
-
return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState:
|
|
312
|
+
return { matchedEvents: newMatchedEvents, lastEventTime: currentTime, emitState: EmitStates.InProgress };
|
|
221
313
|
}
|
|
222
314
|
}
|
|
223
315
|
else {
|
|
316
|
+
// If current key breaks sequence, check if it starts a new sequence
|
|
224
317
|
if (matchedEvents.length > 0 && this.debugMode) {
|
|
225
|
-
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) broken by key "${event.key}". Matched: ${matchedEvents.map(e => e.key).join(
|
|
318
|
+
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" (timeout: ${sequenceTimeoutMs}ms) broken by key "${event.key}". Matched: ${matchedEvents.map(e => e.key).join(",")}. Resetting.`);
|
|
226
319
|
}
|
|
227
320
|
if (sequenceLength > 0 && compareKey(event.key, configuredSequence[0])) {
|
|
228
|
-
return { matchedEvents: [event], lastEventTime: currentTime, emitState:
|
|
321
|
+
return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
|
|
229
322
|
}
|
|
230
323
|
else {
|
|
231
|
-
return { matchedEvents: [], lastEventTime: 0, emitState:
|
|
324
|
+
return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
|
|
232
325
|
}
|
|
233
326
|
}
|
|
234
|
-
}, { matchedEvents: [], lastEventTime: 0, emitState:
|
|
327
|
+
}, { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore }), filter(state => state.emitState === EmitStates.Emit), map(state => state.matchedEvents));
|
|
235
328
|
}
|
|
236
329
|
else {
|
|
330
|
+
// No timeout logic: simple buffer-based matching
|
|
237
331
|
shortcut$ = baseKeydownStream$.pipe(bufferCount(sequenceLength, 1), filter((events) => {
|
|
238
332
|
if (events.length < sequenceLength)
|
|
239
333
|
return false;
|
|
@@ -255,6 +349,7 @@ export class Hotkeys {
|
|
|
255
349
|
}));
|
|
256
350
|
const subscription = finalShortcut$.subscribe((events) => {
|
|
257
351
|
try {
|
|
352
|
+
// Ensure callback receives the last event of the sequence, similar to combination.
|
|
258
353
|
if (events.length > 0)
|
|
259
354
|
callback(events[events.length - 1]);
|
|
260
355
|
}
|
|
@@ -262,8 +357,8 @@ export class Hotkeys {
|
|
|
262
357
|
console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for sequence shortcut "${id}":`, e);
|
|
263
358
|
}
|
|
264
359
|
});
|
|
265
|
-
const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` :
|
|
266
|
-
return this._registerShortcut(config, subscription,
|
|
360
|
+
const logDetails = `Sequence: ${sequence.join(" -> ")}${sequenceTimeoutMs && sequenceTimeoutMs > 0 ? ` (timeout: ${sequenceTimeoutMs}ms)` : ""}`;
|
|
361
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails); // Use Enum
|
|
267
362
|
}
|
|
268
363
|
/**
|
|
269
364
|
* Removes a registered shortcut by its ID.
|
|
@@ -298,7 +393,7 @@ export class Hotkeys {
|
|
|
298
393
|
id,
|
|
299
394
|
description: activeShortcut.config.description,
|
|
300
395
|
context: activeShortcut.config.context,
|
|
301
|
-
type:
|
|
396
|
+
type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination // Use Enum values
|
|
302
397
|
});
|
|
303
398
|
}
|
|
304
399
|
return shortcuts;
|
package/dist/hotkeys.test.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { describe, it, before, beforeEach, afterEach, mock } from "node:test";
|
|
2
2
|
import assert from "node:assert";
|
|
3
|
-
|
|
4
|
-
import { Hotkeys } from "./hotkeys.js";
|
|
5
|
-
// Importing Keys and StandardKey from the separate keys.js file
|
|
3
|
+
import { Hotkeys, ShortcutTypes } from "./hotkeys.js";
|
|
6
4
|
import { Keys } from "./keys.js";
|
|
7
5
|
import { fromEvent, BehaviorSubject } from "rxjs";
|
|
8
6
|
import { createMockFn, dispatchKeyEvent } from "./testutils.js";
|
|
@@ -27,14 +25,14 @@ before(() => {
|
|
|
27
25
|
// @ts-ignore
|
|
28
26
|
global.fromEvent = fromEvent;
|
|
29
27
|
// @ts-ignore
|
|
30
|
-
if (typeof global.performance ===
|
|
28
|
+
if (typeof global.performance === "undefined") {
|
|
31
29
|
// @ts-ignore
|
|
32
30
|
global.performance = {};
|
|
33
31
|
}
|
|
34
32
|
// @ts-ignore
|
|
35
33
|
originalPerformanceNow = global.performance.now;
|
|
36
34
|
// @ts-ignore
|
|
37
|
-
if (typeof global.performance.now !==
|
|
35
|
+
if (typeof global.performance.now !== "function") {
|
|
38
36
|
// @ts-ignore
|
|
39
37
|
global.performance.now = (() => {
|
|
40
38
|
const start = Date.now();
|
|
@@ -70,35 +68,135 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
70
68
|
global.performance.now = originalPerformanceNow;
|
|
71
69
|
}
|
|
72
70
|
});
|
|
73
|
-
|
|
71
|
+
// ... (Initialization and Basic Context tests remain the same) ...
|
|
72
|
+
describe("Initialization and Context Management", () => {
|
|
74
73
|
it("should initialize without errors", () => {
|
|
75
74
|
assert(keyManager instanceof Hotkeys);
|
|
75
|
+
assert.strictEqual(keyManager.getContext(), null); // Default initial context
|
|
76
76
|
});
|
|
77
77
|
it("should initialize with a null context by default", () => {
|
|
78
78
|
assert.strictEqual(keyManager.getContext(), null);
|
|
79
79
|
});
|
|
80
|
-
it("should initialize with a given initial context", () => {
|
|
81
|
-
const manager = new Hotkeys("editor");
|
|
80
|
+
it("should initialize with a given initial context (debug off)", () => {
|
|
81
|
+
const manager = new Hotkeys("editor", false);
|
|
82
82
|
assert.strictEqual(manager.getContext(), "editor");
|
|
83
83
|
manager.destroy();
|
|
84
84
|
});
|
|
85
|
-
it("should
|
|
85
|
+
it("should log library initialization with context in debug mode", () => {
|
|
86
|
+
const consoleLogMock = mock.method(console, "log");
|
|
87
|
+
const manager = new Hotkeys("debugInitCtx", true);
|
|
88
|
+
const initLog = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`Library initialized. Initial context: "debugInitCtx"`));
|
|
89
|
+
assert.ok(initLog, "Library initialization log not found or incorrect.");
|
|
90
|
+
manager.destroy();
|
|
91
|
+
consoleLogMock.mock.restore();
|
|
92
|
+
});
|
|
93
|
+
it("should set and get context correctly", () => {
|
|
86
94
|
keyManager.setContext("modal");
|
|
87
95
|
assert.strictEqual(keyManager.getContext(), "modal");
|
|
88
96
|
keyManager.setContext(null);
|
|
89
97
|
assert.strictEqual(keyManager.getContext(), null);
|
|
90
98
|
});
|
|
91
|
-
it("should
|
|
99
|
+
it("should log context change correctly when context is different (debug mode on)", () => {
|
|
92
100
|
const consoleLogMock = mock.method(console, "log");
|
|
93
101
|
keyManager.setDebugMode(true);
|
|
102
|
+
// Initial context is null for keyManager
|
|
103
|
+
consoleLogMock.mock.resetCalls(); // Clear "Debug mode enabled" log
|
|
94
104
|
keyManager.setContext("debug_test");
|
|
95
|
-
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(
|
|
105
|
+
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Context changed from "null" to "debug_test"`)), "Log for context change from null incorrect.");
|
|
106
|
+
assert.strictEqual(keyManager.getContext(), "debug_test");
|
|
107
|
+
consoleLogMock.mock.resetCalls();
|
|
108
|
+
keyManager.setContext("another_test");
|
|
109
|
+
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes(`Context changed from "debug_test" to "another_test"`)), "Log for context change between non-null incorrect.");
|
|
110
|
+
assert.strictEqual(keyManager.getContext(), "another_test");
|
|
111
|
+
consoleLogMock.mock.restore();
|
|
112
|
+
});
|
|
113
|
+
it("should log context change from non-null to null (debug mode on)", () => {
|
|
114
|
+
const consoleLogMock = mock.method(console, "log");
|
|
115
|
+
keyManager.setContext("fromCtx"); // Initial context
|
|
116
|
+
keyManager.setDebugMode(true);
|
|
117
|
+
consoleLogMock.mock.resetCalls();
|
|
118
|
+
keyManager.setContext(null);
|
|
119
|
+
const logCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`Context changed from "fromCtx" to "null"`));
|
|
120
|
+
assert.ok(logCall, "Context change to null log not found or incorrect.");
|
|
121
|
+
assert.strictEqual(keyManager.getContext(), null);
|
|
122
|
+
consoleLogMock.mock.restore();
|
|
123
|
+
});
|
|
124
|
+
it(`should not call activeContext$.next and log "no change" if context is set to the same value (debug mode on)`, () => {
|
|
125
|
+
keyManager.setContext("sameCtx"); // Set initial context
|
|
126
|
+
const consoleLogMock = mock.method(console, "log");
|
|
127
|
+
keyManager.setDebugMode(true); // Enable debug for this test
|
|
128
|
+
consoleLogMock.mock.resetCalls(); // Clear "Debug mode enabled" log
|
|
129
|
+
// @ts-ignore: Accessing private member for test
|
|
130
|
+
const activeContextNextSpy = mock.method(keyManager["activeContext$"], "next");
|
|
131
|
+
keyManager.setContext("sameCtx"); // Attempt to set the same context
|
|
132
|
+
const noChangeLogCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`setContext called with the same context "sameCtx". No change made.`));
|
|
133
|
+
assert.ok(noChangeLogCall, "No-change log not found or incorrect for same context.");
|
|
134
|
+
const changedLogCall = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`Context changed from`));
|
|
135
|
+
assert.strictEqual(changedLogCall, undefined, "Context changed log should not appear for same context.");
|
|
136
|
+
assert.strictEqual(keyManager.getContext(), "sameCtx");
|
|
137
|
+
assert.strictEqual(activeContextNextSpy.mock.callCount(), 0, "activeContext$.next should not have been called.");
|
|
138
|
+
activeContextNextSpy.mock.restore();
|
|
139
|
+
consoleLogMock.mock.restore();
|
|
140
|
+
});
|
|
141
|
+
it("should not log or call next if context is set to the same value (debug mode off)", () => {
|
|
142
|
+
keyManager.setContext("sameCtxNoDebug"); // Set initial context
|
|
143
|
+
keyManager.setDebugMode(false);
|
|
144
|
+
// Ensure debug is off
|
|
145
|
+
const consoleLogMock = mock.method(console, "log");
|
|
146
|
+
// @ts-ignore: Accessing private member for test
|
|
147
|
+
const activeContextNextSpy = mock.method(keyManager["activeContext$"], "next");
|
|
148
|
+
keyManager.setContext("sameCtxNoDebug"); // Attempt to set the same context
|
|
149
|
+
assert.strictEqual(consoleLogMock.mock.callCount(), 0, "Console.log should not have been called with debug mode off.");
|
|
150
|
+
assert.strictEqual(activeContextNextSpy.mock.callCount(), 0, "activeContext$.next should not have been called.");
|
|
151
|
+
assert.strictEqual(keyManager.getContext(), "sameCtxNoDebug");
|
|
152
|
+
activeContextNextSpy.mock.restore();
|
|
153
|
+
consoleLogMock.mock.restore();
|
|
154
|
+
});
|
|
155
|
+
it("should toggle debug mode and log its state", () => {
|
|
156
|
+
const consoleLogMock = mock.method(console, "log");
|
|
157
|
+
keyManager.setDebugMode(true);
|
|
158
|
+
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes("Debug mode enabled")));
|
|
96
159
|
consoleLogMock.mock.resetCalls();
|
|
97
160
|
keyManager.setDebugMode(false);
|
|
98
|
-
|
|
99
|
-
assert.ok(
|
|
161
|
+
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>", consoleLogMock.mock.calls);
|
|
162
|
+
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes("Debug mode disabled")));
|
|
100
163
|
consoleLogMock.mock.restore();
|
|
101
164
|
});
|
|
165
|
+
describe("onContextChange$ observable", () => {
|
|
166
|
+
it("should emit initial context to new subscriber", () => {
|
|
167
|
+
const manager = new Hotkeys("initial", false);
|
|
168
|
+
const spy = createMockFn();
|
|
169
|
+
const sub = manager.onContextChange$.subscribe(spy);
|
|
170
|
+
assert.strictEqual(spy.calledCount, 1);
|
|
171
|
+
assert.strictEqual(spy.lastArgs[0], "initial");
|
|
172
|
+
sub.unsubscribe();
|
|
173
|
+
manager.destroy();
|
|
174
|
+
});
|
|
175
|
+
it("should emit when context changes", () => {
|
|
176
|
+
const spy = createMockFn();
|
|
177
|
+
const sub = keyManager.onContextChange$.subscribe(spy); // Subscribes, gets initial null
|
|
178
|
+
spy.mockClear(); // Clear initial emission
|
|
179
|
+
keyManager.setContext("newContext");
|
|
180
|
+
assert.strictEqual(spy.calledCount, 1);
|
|
181
|
+
assert.strictEqual(spy.lastArgs[0], "newContext");
|
|
182
|
+
keyManager.setContext("anotherContext");
|
|
183
|
+
assert.strictEqual(spy.calledCount, 2);
|
|
184
|
+
assert.strictEqual(spy.lastArgs[0], "anotherContext");
|
|
185
|
+
keyManager.setContext(null);
|
|
186
|
+
assert.strictEqual(spy.calledCount, 3);
|
|
187
|
+
assert.strictEqual(spy.lastArgs[0], null);
|
|
188
|
+
sub.unsubscribe();
|
|
189
|
+
});
|
|
190
|
+
it("should not emit if context is set to the same value", () => {
|
|
191
|
+
keyManager.setContext("testContext");
|
|
192
|
+
const spy = createMockFn();
|
|
193
|
+
const sub = keyManager.onContextChange$.subscribe(spy); // Subscribes, gets "testContext"
|
|
194
|
+
spy.mockClear(); // Clear initial emission
|
|
195
|
+
keyManager.setContext("testContext"); // Set same context
|
|
196
|
+
assert.strictEqual(spy.calledCount, 0, "Observable should not emit if context value is the same.");
|
|
197
|
+
sub.unsubscribe();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
102
200
|
});
|
|
103
201
|
describe("addCombination", () => {
|
|
104
202
|
it("should trigger callback for a simple key combination (e.g., 'A')", () => {
|
|
@@ -111,13 +209,12 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
111
209
|
dispatchKeyEvent("A");
|
|
112
210
|
assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'A' not called");
|
|
113
211
|
});
|
|
114
|
-
it("should return undefined and warn if keys.key is null or undefined (runtime check)", () => {
|
|
115
|
-
// This test checks runtime robustness if `any` is used to bypass StandardKey
|
|
212
|
+
it("should return undefined and warn if keys.key is null or undefined (runtime check in object form)", () => {
|
|
116
213
|
const config = { id: "nullKey", keys: { key: null }, callback: mockCallback };
|
|
117
214
|
const result = keyManager.addCombination(config);
|
|
118
215
|
assert.strictEqual(result, undefined, "Should return undefined for null key");
|
|
119
216
|
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
120
|
-
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(
|
|
217
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "keys.key" for combination shortcut "nullKey"`));
|
|
121
218
|
});
|
|
122
219
|
it("should pass the KeyboardEvent to the callback", () => {
|
|
123
220
|
const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
|
|
@@ -140,14 +237,14 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
140
237
|
dispatchKeyEvent("a", { ctrlKey: false });
|
|
141
238
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
142
239
|
});
|
|
143
|
-
it("should trigger for special keys like Escape", () => {
|
|
144
|
-
const config = { id: "
|
|
240
|
+
it("should trigger for special keys like Escape (object form)", () => {
|
|
241
|
+
const config = { id: "escapeKeyObj", keys: { key: Keys.Escape }, callback: mockCallback };
|
|
145
242
|
keyManager.addCombination(config);
|
|
146
243
|
dispatchKeyEvent("Escape"); // Event key matches Keys.Escape
|
|
147
244
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
148
245
|
});
|
|
149
|
-
it("should handle preventDefault correctly", () => {
|
|
150
|
-
const config = { id: "
|
|
246
|
+
it("should handle preventDefault correctly (object form)", () => {
|
|
247
|
+
const config = { id: "preventAObj", keys: { key: Keys.A }, callback: mockCallback, preventDefault: true };
|
|
151
248
|
keyManager.addCombination(config);
|
|
152
249
|
const event = dispatchKeyEvent("a");
|
|
153
250
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
@@ -173,25 +270,92 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
173
270
|
keyManager.addCombination({ id: "workingCombo", keys: { key: Keys.W }, callback: workingCallback });
|
|
174
271
|
dispatchKeyEvent("e");
|
|
175
272
|
assert.strictEqual(consoleErrorMock.mock.calls.length, 1);
|
|
176
|
-
assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(
|
|
273
|
+
assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(`Error in user callback for combination shortcut "errorCombo"`));
|
|
177
274
|
dispatchKeyEvent("w");
|
|
178
275
|
assert.strictEqual(workingCallback.calledCount, 1);
|
|
179
276
|
});
|
|
277
|
+
describe("addCombination - Shorthand Syntax", () => {
|
|
278
|
+
it("should trigger callback for a simple key using shorthand (e.g., Keys.X)", () => {
|
|
279
|
+
const config = { id: "shorthandX", keys: Keys.X, callback: mockCallback };
|
|
280
|
+
keyManager.addCombination(config);
|
|
281
|
+
dispatchKeyEvent(Keys.X.toLowerCase());
|
|
282
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'x' (shorthand) not called");
|
|
283
|
+
mockCallback.mockClear();
|
|
284
|
+
dispatchKeyEvent(Keys.X);
|
|
285
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'X' (shorthand) not called");
|
|
286
|
+
});
|
|
287
|
+
it("should NOT trigger callback for shorthand if modifier is pressed", () => {
|
|
288
|
+
const config = { id: "shorthandY", keys: Keys.Y, callback: mockCallback };
|
|
289
|
+
keyManager.addCombination(config);
|
|
290
|
+
dispatchKeyEvent(Keys.Y, { ctrlKey: true });
|
|
291
|
+
assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Ctrl");
|
|
292
|
+
mockCallback.mockClear();
|
|
293
|
+
dispatchKeyEvent(Keys.Y, { altKey: true });
|
|
294
|
+
assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Alt");
|
|
295
|
+
mockCallback.mockClear();
|
|
296
|
+
dispatchKeyEvent(Keys.Y, { shiftKey: true });
|
|
297
|
+
assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Shift");
|
|
298
|
+
mockCallback.mockClear();
|
|
299
|
+
dispatchKeyEvent(Keys.Y, { metaKey: true });
|
|
300
|
+
assert.strictEqual(mockCallback.calledCount, 0, "Callback for 'y' (shorthand) should not be called with Meta");
|
|
301
|
+
});
|
|
302
|
+
it("should trigger callback for shorthand if ONLY the key is pressed (no modifiers)", () => {
|
|
303
|
+
const config = { id: "shorthandZ", keys: Keys.Z, callback: mockCallback };
|
|
304
|
+
keyManager.addCombination(config);
|
|
305
|
+
dispatchKeyEvent(Keys.Z, { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false });
|
|
306
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
307
|
+
});
|
|
308
|
+
it("should handle preventDefault correctly for shorthand", () => {
|
|
309
|
+
const config = { id: "shorthandPrevent", keys: Keys.P, callback: mockCallback, preventDefault: true };
|
|
310
|
+
keyManager.addCombination(config);
|
|
311
|
+
const event = dispatchKeyEvent(Keys.P.toLowerCase());
|
|
312
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
313
|
+
assert.strictEqual(event.defaultPrevented, true);
|
|
314
|
+
});
|
|
315
|
+
it("should respect context for shorthand", () => {
|
|
316
|
+
const config = { id: "shorthandContext", keys: Keys.C, callback: mockCallback, context: "editor" };
|
|
317
|
+
keyManager.addCombination(config);
|
|
318
|
+
keyManager.setContext("other");
|
|
319
|
+
dispatchKeyEvent(Keys.C.toLowerCase());
|
|
320
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
321
|
+
keyManager.setContext("editor");
|
|
322
|
+
dispatchKeyEvent(Keys.C.toLowerCase());
|
|
323
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
324
|
+
});
|
|
325
|
+
it("should return undefined and warn if shorthand key is an empty string", () => {
|
|
326
|
+
const config = { id: "emptyShorthand", keys: "", callback: mockCallback };
|
|
327
|
+
const result = keyManager.addCombination(config);
|
|
328
|
+
assert.strictEqual(result, undefined);
|
|
329
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
330
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid "keys" (shorthand) for combination shortcut "emptyShorthand"`));
|
|
331
|
+
});
|
|
332
|
+
it("should correctly log shorthand key details in debug mode", () => {
|
|
333
|
+
const consoleLogMock = mock.method(console, "log");
|
|
334
|
+
keyManager.setDebugMode(true);
|
|
335
|
+
const config = { id: "debugShorthand", keys: Keys.D, callback: mockCallback };
|
|
336
|
+
keyManager.addCombination(config);
|
|
337
|
+
const logMessage = consoleLogMock.mock.calls.find(call => call.arguments[0].includes(`combination shortcut "debugShorthand" added`));
|
|
338
|
+
assert.ok(logMessage, "Debug log for adding shortcut not found");
|
|
339
|
+
assert.ok(logMessage.arguments[0].includes(`Keys: { key: "D" (no modifiers implied) }`), `Log message content mismatch: ${logMessage.arguments[0]}`);
|
|
340
|
+
consoleLogMock.mock.restore();
|
|
341
|
+
keyManager.setDebugMode(false);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
180
344
|
});
|
|
181
345
|
describe("addSequence", () => {
|
|
182
346
|
it("should trigger callback for a simple key sequence", () => {
|
|
183
347
|
const config = { id: "seqGI", sequence: [Keys.G, Keys.I], callback: mockCallback };
|
|
184
348
|
const result = keyManager.addSequence(config);
|
|
185
349
|
assert.strictEqual(result, "seqGI");
|
|
186
|
-
dispatchKeyEvent("g"); // Dispatch
|
|
187
|
-
dispatchKeyEvent("i"); // Dispatch
|
|
350
|
+
dispatchKeyEvent("g"); // Dispatch "g" (lowercase)
|
|
351
|
+
dispatchKeyEvent("i"); // Dispatch "i" (lowercase)
|
|
188
352
|
assert.strictEqual(mockCallback.calledCount, 1);
|
|
189
353
|
});
|
|
190
354
|
it("should trigger callback for Konami code using Keys", () => {
|
|
191
355
|
const konamiSequence = [
|
|
192
356
|
Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown,
|
|
193
357
|
Keys.ArrowLeft, Keys.ArrowRight, Keys.ArrowLeft, Keys.ArrowRight,
|
|
194
|
-
Keys.B, Keys.A // Using
|
|
358
|
+
Keys.B, Keys.A // Using "B" and "A" from Keys
|
|
195
359
|
];
|
|
196
360
|
const config = { id: "konami", sequence: konamiSequence, callback: mockCallback };
|
|
197
361
|
keyManager.addSequence(config);
|
|
@@ -236,7 +400,7 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
236
400
|
dispatchKeyEvent("e");
|
|
237
401
|
dispatchKeyEvent("s");
|
|
238
402
|
assert.strictEqual(consoleErrorMock.mock.calls.length, 1);
|
|
239
|
-
assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(
|
|
403
|
+
assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes(`Error in user callback for sequence shortcut "errorSeq"`));
|
|
240
404
|
});
|
|
241
405
|
describe("Sequence Contextual Triggering", () => {
|
|
242
406
|
let editorSequenceConfig;
|
|
@@ -317,17 +481,17 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
317
481
|
});
|
|
318
482
|
});
|
|
319
483
|
describe("getActiveShortcuts", () => {
|
|
320
|
-
it("should return active combination and sequence shortcuts", () => {
|
|
484
|
+
it("should return active combination and sequence shortcuts with enum types", () => {
|
|
321
485
|
keyManager.addCombination({ id: "combo1", keys: { key: Keys.A }, callback: createMockFn(), description: "Test A" });
|
|
322
486
|
keyManager.addSequence({ id: "seq1", sequence: [Keys.B, Keys.C], callback: createMockFn(), context: "modal", description: "Test BC" });
|
|
323
487
|
const active = keyManager.getActiveShortcuts();
|
|
324
488
|
assert.strictEqual(active.length, 2);
|
|
325
489
|
const combo = active.find(s => s.id === "combo1");
|
|
326
490
|
assert.ok(combo);
|
|
327
|
-
assert.strictEqual(combo.type,
|
|
491
|
+
assert.strictEqual(combo.type, ShortcutTypes.Combination); // Use Enum for comparison
|
|
328
492
|
const seq = active.find(s => s.id === "seq1");
|
|
329
493
|
assert.ok(seq);
|
|
330
|
-
assert.strictEqual(seq.type,
|
|
494
|
+
assert.strictEqual(seq.type, ShortcutTypes.Sequence); // Use Enum for comparison
|
|
331
495
|
});
|
|
332
496
|
});
|
|
333
497
|
describe("hasShortcut", () => {
|
|
@@ -347,10 +511,10 @@ describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
|
347
511
|
keyManager.addCombination({ id: "destroyTestCombo", keys: { key: Keys.D }, callback: mockCallback });
|
|
348
512
|
keyManager.addSequence({ id: "destroyTestSeq", sequence: [Keys.X, Keys.Y], callback: mockCallback });
|
|
349
513
|
// @ts-ignore
|
|
350
|
-
assert.strictEqual(keyManager[
|
|
514
|
+
assert.strictEqual(keyManager["activeShortcuts"].size, 2);
|
|
351
515
|
keyManager.destroy();
|
|
352
516
|
// @ts-ignore
|
|
353
|
-
assert.strictEqual(keyManager[
|
|
517
|
+
assert.strictEqual(keyManager["activeShortcuts"].size, 0);
|
|
354
518
|
dispatchKeyEvent(Keys.D);
|
|
355
519
|
dispatchKeyEvent(Keys.X);
|
|
356
520
|
dispatchKeyEvent(Keys.Y);
|