rx-hotkeys 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -0
- package/dist/hotkeys.d.ts +163 -0
- package/dist/hotkeys.d.ts.map +1 -0
- package/dist/hotkeys.js +321 -0
- package/dist/hotkeys.test.d.ts +2 -0
- package/dist/hotkeys.test.d.ts.map +1 -0
- package/dist/hotkeys.test.js +360 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/keys.d.ts +155 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +117 -0
- package/dist/testutils.d.ts +14 -0
- package/dist/testutils.d.ts.map +1 -0
- package/dist/testutils.js +31 -0
- package/package.json +39 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { describe, it, before, beforeEach, afterEach, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
// Importing main library components
|
|
4
|
+
import { HotKeys } from "./hotkeys.js";
|
|
5
|
+
// Importing Keys and StandardKey from the separate keys.js file
|
|
6
|
+
import { Keys } from "./keys.js";
|
|
7
|
+
import { fromEvent, BehaviorSubject } from "rxjs";
|
|
8
|
+
import { createMockFn, dispatchKeyEvent } from "./testutils.js";
|
|
9
|
+
import { JSDOM } from "jsdom";
|
|
10
|
+
// --- JSDOM and RxJS setup for Node.js tests ---
|
|
11
|
+
let dom;
|
|
12
|
+
let window;
|
|
13
|
+
let document;
|
|
14
|
+
let originalPerformanceNow;
|
|
15
|
+
before(() => {
|
|
16
|
+
dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
|
17
|
+
url: "http://localhost",
|
|
18
|
+
});
|
|
19
|
+
window = dom.window;
|
|
20
|
+
document = window.document;
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
global.document = document;
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
global.KeyboardEvent = window.KeyboardEvent;
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
global.BehaviorSubject = BehaviorSubject;
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
global.fromEvent = fromEvent;
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
if (typeof global.performance === 'undefined') {
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
global.performance = {};
|
|
33
|
+
}
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
originalPerformanceNow = global.performance.now;
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
if (typeof global.performance.now !== 'function') {
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
global.performance.now = (() => {
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
return () => Date.now() - start;
|
|
42
|
+
})();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
describe("Hotkeys Library (Node.js Test Runner)", () => {
|
|
46
|
+
let keyManager;
|
|
47
|
+
let mockCallback;
|
|
48
|
+
let consoleWarnMock;
|
|
49
|
+
let consoleErrorMock;
|
|
50
|
+
let performanceNowMock; // To mock global.performance.now specifically for sequence tests
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
keyManager = new HotKeys(null, false);
|
|
53
|
+
mockCallback = createMockFn();
|
|
54
|
+
consoleWarnMock = mock.method(console, "warn");
|
|
55
|
+
consoleErrorMock = mock.method(console, "error");
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
if (keyManager) {
|
|
59
|
+
keyManager.destroy();
|
|
60
|
+
}
|
|
61
|
+
mockCallback.mockClear();
|
|
62
|
+
mock.reset();
|
|
63
|
+
if (consoleWarnMock && consoleWarnMock.mock)
|
|
64
|
+
consoleWarnMock.mock.restore();
|
|
65
|
+
if (consoleErrorMock && consoleErrorMock.mock)
|
|
66
|
+
consoleErrorMock.mock.restore();
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
if (global.performance && global.performance.now !== originalPerformanceNow) {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
global.performance.now = originalPerformanceNow;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
describe("Initialization and Basic Context", () => {
|
|
74
|
+
it("should initialize without errors", () => {
|
|
75
|
+
assert(keyManager instanceof HotKeys);
|
|
76
|
+
});
|
|
77
|
+
it("should initialize with a null context by default", () => {
|
|
78
|
+
assert.strictEqual(keyManager.getContext(), null);
|
|
79
|
+
});
|
|
80
|
+
it("should initialize with a given initial context", () => {
|
|
81
|
+
const manager = new HotKeys("editor");
|
|
82
|
+
assert.strictEqual(manager.getContext(), "editor");
|
|
83
|
+
manager.destroy();
|
|
84
|
+
});
|
|
85
|
+
it("should set and get context", () => {
|
|
86
|
+
keyManager.setContext("modal");
|
|
87
|
+
assert.strictEqual(keyManager.getContext(), "modal");
|
|
88
|
+
keyManager.setContext(null);
|
|
89
|
+
assert.strictEqual(keyManager.getContext(), null);
|
|
90
|
+
});
|
|
91
|
+
it("should toggle debug mode and log appropriately", () => {
|
|
92
|
+
const consoleLogMock = mock.method(console, "log");
|
|
93
|
+
keyManager.setDebugMode(true);
|
|
94
|
+
keyManager.setContext("debug_test");
|
|
95
|
+
assert.ok(consoleLogMock.mock.calls.some(call => call.arguments[0].includes('Context changed to "debug_test"')));
|
|
96
|
+
consoleLogMock.mock.resetCalls();
|
|
97
|
+
keyManager.setDebugMode(false);
|
|
98
|
+
keyManager.setContext("no_debug_test");
|
|
99
|
+
assert.ok(!consoleLogMock.mock.calls.some(call => call.arguments[0].includes('Context changed to "no_debug_test"')));
|
|
100
|
+
consoleLogMock.mock.restore();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("addCombination", () => {
|
|
104
|
+
it("should trigger callback for a simple key combination (e.g., 'A')", () => {
|
|
105
|
+
const config = { id: "simpleA", keys: { key: Keys.A }, callback: mockCallback };
|
|
106
|
+
const result = keyManager.addCombination(config);
|
|
107
|
+
assert.strictEqual(result, "simpleA");
|
|
108
|
+
dispatchKeyEvent("a");
|
|
109
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'a' not called");
|
|
110
|
+
mockCallback.mockClear();
|
|
111
|
+
dispatchKeyEvent("A");
|
|
112
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Callback for 'A' not called");
|
|
113
|
+
});
|
|
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
|
|
116
|
+
const config = { id: "nullKey", keys: { key: null }, callback: mockCallback };
|
|
117
|
+
const result = keyManager.addCombination(config);
|
|
118
|
+
assert.strictEqual(result, undefined, "Should return undefined for null key");
|
|
119
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
120
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes('Invalid \'keys.key\' for combination shortcut "nullKey"'));
|
|
121
|
+
});
|
|
122
|
+
it("should pass the KeyboardEvent to the callback", () => {
|
|
123
|
+
const config = { id: "eventPass", keys: { key: Keys.E }, callback: mockCallback };
|
|
124
|
+
keyManager.addCombination(config);
|
|
125
|
+
const event = dispatchKeyEvent("e");
|
|
126
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
127
|
+
assert.deepStrictEqual(mockCallback.lastArgs, [event]);
|
|
128
|
+
});
|
|
129
|
+
it("should trigger callback for a combination with Ctrl key", () => {
|
|
130
|
+
const config = { id: "ctrlS", keys: { key: Keys.S, ctrlKey: true }, callback: mockCallback };
|
|
131
|
+
keyManager.addCombination(config);
|
|
132
|
+
dispatchKeyEvent("s", { ctrlKey: true });
|
|
133
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
134
|
+
});
|
|
135
|
+
it("should NOT trigger callback if specified modifier key (ctrlKey: false) is false and event has it true", () => {
|
|
136
|
+
const config = { id: "noCtrlA", keys: { key: Keys.A, ctrlKey: false }, callback: mockCallback };
|
|
137
|
+
keyManager.addCombination(config);
|
|
138
|
+
dispatchKeyEvent("a", { ctrlKey: true });
|
|
139
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
140
|
+
dispatchKeyEvent("a", { ctrlKey: false });
|
|
141
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
142
|
+
});
|
|
143
|
+
it("should trigger for special keys like Escape", () => {
|
|
144
|
+
const config = { id: "escapeKey", keys: { key: Keys.Escape }, callback: mockCallback };
|
|
145
|
+
keyManager.addCombination(config);
|
|
146
|
+
dispatchKeyEvent("Escape"); // Event key matches Keys.Escape
|
|
147
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
148
|
+
});
|
|
149
|
+
it("should handle preventDefault correctly", () => {
|
|
150
|
+
const config = { id: "preventA", keys: { key: Keys.A }, callback: mockCallback, preventDefault: true };
|
|
151
|
+
keyManager.addCombination(config);
|
|
152
|
+
const event = dispatchKeyEvent("a");
|
|
153
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
154
|
+
assert.strictEqual(event.defaultPrevented, true);
|
|
155
|
+
});
|
|
156
|
+
it("should overwrite an existing combination with the same ID and warn", () => {
|
|
157
|
+
const firstCallback = createMockFn();
|
|
158
|
+
const secondCallback = createMockFn();
|
|
159
|
+
keyManager.addCombination({ id: "combo1", keys: { key: Keys.K }, callback: firstCallback });
|
|
160
|
+
consoleWarnMock.mock.resetCalls();
|
|
161
|
+
keyManager.addCombination({ id: "combo1", keys: { key: Keys.K, ctrlKey: true }, callback: secondCallback });
|
|
162
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
163
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Shortcut with ID "combo1" already exists`));
|
|
164
|
+
dispatchKeyEvent("k");
|
|
165
|
+
assert.strictEqual(firstCallback.calledCount, 0);
|
|
166
|
+
dispatchKeyEvent("k", { ctrlKey: true });
|
|
167
|
+
assert.strictEqual(secondCallback.calledCount, 1);
|
|
168
|
+
});
|
|
169
|
+
it("should log an error via console.error if callback throws, and not affect other shortcuts", () => {
|
|
170
|
+
const errorCallback = () => { throw new Error("Test callback error"); };
|
|
171
|
+
const workingCallback = createMockFn();
|
|
172
|
+
keyManager.addCombination({ id: "errorCombo", keys: { key: Keys.E }, callback: errorCallback });
|
|
173
|
+
keyManager.addCombination({ id: "workingCombo", keys: { key: Keys.W }, callback: workingCallback });
|
|
174
|
+
dispatchKeyEvent("e");
|
|
175
|
+
assert.strictEqual(consoleErrorMock.mock.calls.length, 1);
|
|
176
|
+
assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes('Error in user callback for combination shortcut "errorCombo"'));
|
|
177
|
+
dispatchKeyEvent("w");
|
|
178
|
+
assert.strictEqual(workingCallback.calledCount, 1);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe("addSequence", () => {
|
|
182
|
+
it("should trigger callback for a simple key sequence", () => {
|
|
183
|
+
const config = { id: "seqGI", sequence: [Keys.G, Keys.I], callback: mockCallback };
|
|
184
|
+
const result = keyManager.addSequence(config);
|
|
185
|
+
assert.strictEqual(result, "seqGI");
|
|
186
|
+
dispatchKeyEvent("g"); // Dispatch 'g' (lowercase)
|
|
187
|
+
dispatchKeyEvent("i"); // Dispatch 'i' (lowercase)
|
|
188
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
189
|
+
});
|
|
190
|
+
it("should trigger callback for Konami code using Keys", () => {
|
|
191
|
+
const konamiSequence = [
|
|
192
|
+
Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown,
|
|
193
|
+
Keys.ArrowLeft, Keys.ArrowRight, Keys.ArrowLeft, Keys.ArrowRight,
|
|
194
|
+
Keys.B, Keys.A // Using 'B' and 'A' from Keys
|
|
195
|
+
];
|
|
196
|
+
const config = { id: "konami", sequence: konamiSequence, callback: mockCallback };
|
|
197
|
+
keyManager.addSequence(config);
|
|
198
|
+
// Dispatch events using the string values that browser events would produce
|
|
199
|
+
["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"].forEach(key => dispatchKeyEvent(key));
|
|
200
|
+
assert.strictEqual(mockCallback.calledCount, 1, "Konami sequence callback not triggered");
|
|
201
|
+
});
|
|
202
|
+
it("should return undefined and warn if sequence is empty", () => {
|
|
203
|
+
const config = { id: "emptySeq", sequence: [], callback: mockCallback };
|
|
204
|
+
const result = keyManager.addSequence(config);
|
|
205
|
+
assert.strictEqual(result, undefined);
|
|
206
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1);
|
|
207
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Sequence for shortcut "emptySeq" is empty`));
|
|
208
|
+
});
|
|
209
|
+
it("should return undefined and warn if sequence contains an invalid key (runtime check with 'as any')", () => {
|
|
210
|
+
// This test checks runtime robustness if `any` is used to bypass StandardKey[]
|
|
211
|
+
const config = { id: "invalidKeyInSeq", sequence: [Keys.A, "", Keys.C], callback: mockCallback };
|
|
212
|
+
const result = keyManager.addSequence(config);
|
|
213
|
+
assert.strictEqual(result, undefined, "addSequence should return undefined for sequence with empty string");
|
|
214
|
+
assert.strictEqual(consoleWarnMock.mock.calls.length, 1, "console.warn was not called for invalid key in sequence");
|
|
215
|
+
assert.ok(consoleWarnMock.mock.calls[0].arguments[0].includes(`Invalid key in sequence for shortcut "invalidKeyInSeq"`));
|
|
216
|
+
});
|
|
217
|
+
it("should pass the last KeyboardEvent of the sequence to the callback", () => {
|
|
218
|
+
const config = { id: "seqEventPass", sequence: [Keys.X, Keys.Y], callback: mockCallback };
|
|
219
|
+
keyManager.addSequence(config);
|
|
220
|
+
dispatchKeyEvent("x");
|
|
221
|
+
const lastEvent = dispatchKeyEvent("y");
|
|
222
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
223
|
+
assert.deepStrictEqual(mockCallback.lastArgs, [lastEvent]);
|
|
224
|
+
});
|
|
225
|
+
it("should prevent default for the last key event in the sequence when preventDefault is true", () => {
|
|
226
|
+
const config = { id: "seqPrevent", sequence: [Keys.M, Keys.N], callback: mockCallback, preventDefault: true };
|
|
227
|
+
keyManager.addSequence(config);
|
|
228
|
+
dispatchKeyEvent("m");
|
|
229
|
+
const eventN = dispatchKeyEvent("n");
|
|
230
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
231
|
+
assert.strictEqual(eventN.defaultPrevented, true);
|
|
232
|
+
});
|
|
233
|
+
it("should log an error via console.error if sequence callback throws", () => {
|
|
234
|
+
const errorCallback = () => { throw new Error("Test sequence callback error"); };
|
|
235
|
+
keyManager.addSequence({ id: "errorSeq", sequence: [Keys.E, Keys.S], callback: errorCallback });
|
|
236
|
+
dispatchKeyEvent("e");
|
|
237
|
+
dispatchKeyEvent("s");
|
|
238
|
+
assert.strictEqual(consoleErrorMock.mock.calls.length, 1);
|
|
239
|
+
assert.ok(consoleErrorMock.mock.calls[0].arguments[0].includes('Error in user callback for sequence shortcut "errorSeq"'));
|
|
240
|
+
});
|
|
241
|
+
describe("Sequence Contextual Triggering", () => {
|
|
242
|
+
let editorSequenceConfig;
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
editorSequenceConfig = {
|
|
245
|
+
id: "sequenceInEditor",
|
|
246
|
+
sequence: [Keys.C, Keys.O, Keys.D, Keys.E],
|
|
247
|
+
callback: mockCallback,
|
|
248
|
+
context: "editor",
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
it("should trigger sequence in matching context", () => {
|
|
252
|
+
keyManager.addSequence(editorSequenceConfig);
|
|
253
|
+
keyManager.setContext("editor");
|
|
254
|
+
[Keys.C, Keys.O, Keys.D, Keys.E].forEach(k => dispatchKeyEvent(k));
|
|
255
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe("Sequence Timeouts", () => {
|
|
259
|
+
const TIMEOUT_MS = 100;
|
|
260
|
+
let originalPerformanceNowForSuite;
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
mock.timers.enable({ apis: ["Date", "setTimeout", "setInterval"], now: 0 });
|
|
263
|
+
// @ts-ignore
|
|
264
|
+
if (!originalPerformanceNowForSuite && global.performance && global.performance.now) {
|
|
265
|
+
// @ts-ignore
|
|
266
|
+
originalPerformanceNowForSuite = global.performance.now;
|
|
267
|
+
}
|
|
268
|
+
// @ts-ignore
|
|
269
|
+
performanceNowMock = mock.method(global.performance, "now", () => Date.now());
|
|
270
|
+
});
|
|
271
|
+
afterEach(() => {
|
|
272
|
+
if (performanceNowMock && performanceNowMock.mock) {
|
|
273
|
+
performanceNowMock.mock.restore();
|
|
274
|
+
}
|
|
275
|
+
else if (originalPerformanceNowForSuite) {
|
|
276
|
+
// @ts-ignore
|
|
277
|
+
global.performance.now = originalPerformanceNowForSuite;
|
|
278
|
+
}
|
|
279
|
+
mock.timers.reset();
|
|
280
|
+
});
|
|
281
|
+
it("should trigger sequence if keys are pressed within specified timeout", () => {
|
|
282
|
+
const config = { id: "seqTimeoutOk", sequence: [Keys.T, Keys.O, Keys.K], callback: mockCallback, sequenceTimeoutMs: TIMEOUT_MS };
|
|
283
|
+
keyManager.addSequence(config);
|
|
284
|
+
dispatchKeyEvent(Keys.T);
|
|
285
|
+
mock.timers.tick(TIMEOUT_MS / 2);
|
|
286
|
+
dispatchKeyEvent(Keys.O);
|
|
287
|
+
mock.timers.tick(TIMEOUT_MS / 2);
|
|
288
|
+
const lastEvent = dispatchKeyEvent(Keys.K);
|
|
289
|
+
assert.strictEqual(mockCallback.calledCount, 1);
|
|
290
|
+
assert.deepStrictEqual(mockCallback.lastArgs, [lastEvent]);
|
|
291
|
+
});
|
|
292
|
+
it("should NOT trigger sequence if a key press is delayed beyond timeout", () => {
|
|
293
|
+
const config = { id: "seqTimeoutFail", sequence: [Keys.D, Keys.E, Keys.L], callback: mockCallback, sequenceTimeoutMs: TIMEOUT_MS };
|
|
294
|
+
keyManager.addSequence(config);
|
|
295
|
+
dispatchKeyEvent(Keys.D);
|
|
296
|
+
mock.timers.tick(TIMEOUT_MS / 2);
|
|
297
|
+
dispatchKeyEvent(Keys.E);
|
|
298
|
+
mock.timers.tick(TIMEOUT_MS + 1);
|
|
299
|
+
dispatchKeyEvent(Keys.L);
|
|
300
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
describe("remove", () => {
|
|
305
|
+
it("should remove a combination shortcut", () => {
|
|
306
|
+
keyManager.addCombination({ id: "remA", keys: { key: Keys.A }, callback: mockCallback });
|
|
307
|
+
assert.strictEqual(keyManager.remove("remA"), true);
|
|
308
|
+
dispatchKeyEvent(Keys.A);
|
|
309
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
310
|
+
});
|
|
311
|
+
it("should remove a sequence shortcut", () => {
|
|
312
|
+
keyManager.addSequence({ id: "remSeq", sequence: [Keys.A, Keys.B], callback: mockCallback });
|
|
313
|
+
assert.strictEqual(keyManager.remove("remSeq"), true);
|
|
314
|
+
dispatchKeyEvent(Keys.A);
|
|
315
|
+
dispatchKeyEvent(Keys.B);
|
|
316
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe("getActiveShortcuts", () => {
|
|
320
|
+
it("should return active combination and sequence shortcuts", () => {
|
|
321
|
+
keyManager.addCombination({ id: "combo1", keys: { key: Keys.A }, callback: createMockFn(), description: "Test A" });
|
|
322
|
+
keyManager.addSequence({ id: "seq1", sequence: [Keys.B, Keys.C], callback: createMockFn(), context: "modal", description: "Test BC" });
|
|
323
|
+
const active = keyManager.getActiveShortcuts();
|
|
324
|
+
assert.strictEqual(active.length, 2);
|
|
325
|
+
const combo = active.find(s => s.id === "combo1");
|
|
326
|
+
assert.ok(combo);
|
|
327
|
+
assert.strictEqual(combo.type, "combination");
|
|
328
|
+
const seq = active.find(s => s.id === "seq1");
|
|
329
|
+
assert.ok(seq);
|
|
330
|
+
assert.strictEqual(seq.type, "sequence");
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
describe("hasShortcut", () => {
|
|
334
|
+
it("should return true for an existing combination shortcut", () => {
|
|
335
|
+
keyManager.addCombination({ id: "existsCombo", keys: { key: Keys.E }, callback: mockCallback });
|
|
336
|
+
assert.strictEqual(keyManager.hasShortcut("existsCombo"), true);
|
|
337
|
+
});
|
|
338
|
+
it("should return false for a shortcut that failed to add (e.g. invalid key object)", () => {
|
|
339
|
+
// This test now relies on the runtime check for !keys.key, as TS would catch `key: null` directly.
|
|
340
|
+
const config = { id: "invalidKeyCombo", keys: { key: null }, callback: mockCallback };
|
|
341
|
+
keyManager.addCombination(config);
|
|
342
|
+
assert.strictEqual(keyManager.hasShortcut("invalidKeyCombo"), false);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
describe("destroy", () => {
|
|
346
|
+
it("should clear active shortcuts and prevent further triggers", () => {
|
|
347
|
+
keyManager.addCombination({ id: "destroyTestCombo", keys: { key: Keys.D }, callback: mockCallback });
|
|
348
|
+
keyManager.addSequence({ id: "destroyTestSeq", sequence: [Keys.X, Keys.Y], callback: mockCallback });
|
|
349
|
+
// @ts-ignore
|
|
350
|
+
assert.strictEqual(keyManager['activeShortcuts'].size, 2);
|
|
351
|
+
keyManager.destroy();
|
|
352
|
+
// @ts-ignore
|
|
353
|
+
assert.strictEqual(keyManager['activeShortcuts'].size, 0);
|
|
354
|
+
dispatchKeyEvent(Keys.D);
|
|
355
|
+
dispatchKeyEvent(Keys.X);
|
|
356
|
+
dispatchKeyEvent(Keys.Y);
|
|
357
|
+
assert.strictEqual(mockCallback.calledCount, 0);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,WAAW,EAChB,IAAI,GACP,MAAM,WAAW,CAAC;AACnB,OAAO,EACH,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,OAAO,GACV,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides a set of common, standard string values for `KeyboardEvent.key`.
|
|
3
|
+
* Using these values can help avoid typos and ensure consistency.
|
|
4
|
+
* These are based on the MDN documentation:
|
|
5
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
6
|
+
*
|
|
7
|
+
* All shortcut configurations should use values from this object.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Keys: {
|
|
10
|
+
readonly Unidentified: "Unidentified";
|
|
11
|
+
readonly Alt: "Alt";
|
|
12
|
+
readonly AltGraph: "AltGraph";
|
|
13
|
+
readonly CapsLock: "CapsLock";
|
|
14
|
+
readonly Control: "Control";
|
|
15
|
+
readonly Fn: "Fn";
|
|
16
|
+
readonly FnLock: "FnLock";
|
|
17
|
+
readonly Hyper: "Hyper";
|
|
18
|
+
readonly Meta: "Meta";
|
|
19
|
+
readonly NumLock: "NumLock";
|
|
20
|
+
readonly ScrollLock: "ScrollLock";
|
|
21
|
+
readonly Shift: "Shift";
|
|
22
|
+
readonly Super: "Super";
|
|
23
|
+
readonly Symbol: "Symbol";
|
|
24
|
+
readonly SymbolLock: "SymbolLock";
|
|
25
|
+
readonly Enter: "Enter";
|
|
26
|
+
readonly Tab: "Tab";
|
|
27
|
+
readonly Space: " ";
|
|
28
|
+
readonly ArrowDown: "ArrowDown";
|
|
29
|
+
readonly ArrowLeft: "ArrowLeft";
|
|
30
|
+
readonly ArrowRight: "ArrowRight";
|
|
31
|
+
readonly ArrowUp: "ArrowUp";
|
|
32
|
+
readonly End: "End";
|
|
33
|
+
readonly Home: "Home";
|
|
34
|
+
readonly PageDown: "PageDown";
|
|
35
|
+
readonly PageUp: "PageUp";
|
|
36
|
+
readonly Backspace: "Backspace";
|
|
37
|
+
readonly Clear: "Clear";
|
|
38
|
+
readonly Copy: "Copy";
|
|
39
|
+
readonly CrSel: "CrSel";
|
|
40
|
+
readonly Cut: "Cut";
|
|
41
|
+
readonly Delete: "Delete";
|
|
42
|
+
readonly EraseEof: "EraseEof";
|
|
43
|
+
readonly ExSel: "ExSel";
|
|
44
|
+
readonly Insert: "Insert";
|
|
45
|
+
readonly Paste: "Paste";
|
|
46
|
+
readonly Redo: "Redo";
|
|
47
|
+
readonly Undo: "Undo";
|
|
48
|
+
readonly Accept: "Accept";
|
|
49
|
+
readonly Again: "Again";
|
|
50
|
+
readonly Attn: "Attn";
|
|
51
|
+
readonly Cancel: "Cancel";
|
|
52
|
+
readonly ContextMenu: "ContextMenu";
|
|
53
|
+
readonly Escape: "Escape";
|
|
54
|
+
readonly Execute: "Execute";
|
|
55
|
+
readonly Find: "Find";
|
|
56
|
+
readonly Finish: "Finish";
|
|
57
|
+
readonly Help: "Help";
|
|
58
|
+
readonly Pause: "Pause";
|
|
59
|
+
readonly Play: "Play";
|
|
60
|
+
readonly Props: "Props";
|
|
61
|
+
readonly Select: "Select";
|
|
62
|
+
readonly ZoomIn: "ZoomIn";
|
|
63
|
+
readonly ZoomOut: "ZoomOut";
|
|
64
|
+
readonly BrightnessDown: "BrightnessDown";
|
|
65
|
+
readonly BrightnessUp: "BrightnessUp";
|
|
66
|
+
readonly Eject: "Eject";
|
|
67
|
+
readonly LogOff: "LogOff";
|
|
68
|
+
readonly Power: "Power";
|
|
69
|
+
readonly PowerOff: "PowerOff";
|
|
70
|
+
readonly PrintScreen: "PrintScreen";
|
|
71
|
+
readonly Hibernate: "Hibernate";
|
|
72
|
+
readonly Standby: "Standby";
|
|
73
|
+
readonly WakeUp: "WakeUp";
|
|
74
|
+
readonly F1: "F1";
|
|
75
|
+
readonly F2: "F2";
|
|
76
|
+
readonly F3: "F3";
|
|
77
|
+
readonly F4: "F4";
|
|
78
|
+
readonly F5: "F5";
|
|
79
|
+
readonly F6: "F6";
|
|
80
|
+
readonly F7: "F7";
|
|
81
|
+
readonly F8: "F8";
|
|
82
|
+
readonly F9: "F9";
|
|
83
|
+
readonly F10: "F10";
|
|
84
|
+
readonly F11: "F11";
|
|
85
|
+
readonly F12: "F12";
|
|
86
|
+
readonly F13: "F13";
|
|
87
|
+
readonly F14: "F14";
|
|
88
|
+
readonly F15: "F15";
|
|
89
|
+
readonly F16: "F16";
|
|
90
|
+
readonly F17: "F17";
|
|
91
|
+
readonly F18: "F18";
|
|
92
|
+
readonly F19: "F19";
|
|
93
|
+
readonly F20: "F20";
|
|
94
|
+
readonly AppSwitch: "AppSwitch";
|
|
95
|
+
readonly Call: "Call";
|
|
96
|
+
readonly Camera: "Camera";
|
|
97
|
+
readonly EndCall: "EndCall";
|
|
98
|
+
readonly GoBack: "GoBack";
|
|
99
|
+
readonly GoHome: "GoHome";
|
|
100
|
+
readonly HeadsetHook: "HeadsetHook";
|
|
101
|
+
readonly MediaPlayPause: "MediaPlayPause";
|
|
102
|
+
readonly MediaStop: "MediaStop";
|
|
103
|
+
readonly MediaTrackNext: "MediaTrackNext";
|
|
104
|
+
readonly MediaTrackPrevious: "MediaTrackPrevious";
|
|
105
|
+
readonly AudioVolumeDown: "AudioVolumeDown";
|
|
106
|
+
readonly AudioVolumeUp: "AudioVolumeUp";
|
|
107
|
+
readonly AudioVolumeMute: "AudioVolumeMute";
|
|
108
|
+
readonly Decimal: ".";
|
|
109
|
+
readonly KeypadMultiply: "*";
|
|
110
|
+
readonly KeypadAdd: "+";
|
|
111
|
+
readonly KeypadSubtract: "-";
|
|
112
|
+
readonly KeypadDivide: "/";
|
|
113
|
+
readonly A: "A";
|
|
114
|
+
readonly B: "B";
|
|
115
|
+
readonly C: "C";
|
|
116
|
+
readonly D: "D";
|
|
117
|
+
readonly E: "E";
|
|
118
|
+
readonly F: "F";
|
|
119
|
+
readonly G: "G";
|
|
120
|
+
readonly H: "H";
|
|
121
|
+
readonly I: "I";
|
|
122
|
+
readonly J: "J";
|
|
123
|
+
readonly K: "K";
|
|
124
|
+
readonly L: "L";
|
|
125
|
+
readonly M: "M";
|
|
126
|
+
readonly N: "N";
|
|
127
|
+
readonly O: "O";
|
|
128
|
+
readonly P: "P";
|
|
129
|
+
readonly Q: "Q";
|
|
130
|
+
readonly R: "R";
|
|
131
|
+
readonly S: "S";
|
|
132
|
+
readonly T: "T";
|
|
133
|
+
readonly U: "U";
|
|
134
|
+
readonly V: "V";
|
|
135
|
+
readonly W: "W";
|
|
136
|
+
readonly X: "X";
|
|
137
|
+
readonly Y: "Y";
|
|
138
|
+
readonly Z: "Z";
|
|
139
|
+
readonly Digit0: "0";
|
|
140
|
+
readonly Digit1: "1";
|
|
141
|
+
readonly Digit2: "2";
|
|
142
|
+
readonly Digit3: "3";
|
|
143
|
+
readonly Digit4: "4";
|
|
144
|
+
readonly Digit5: "5";
|
|
145
|
+
readonly Digit6: "6";
|
|
146
|
+
readonly Digit7: "7";
|
|
147
|
+
readonly Digit8: "8";
|
|
148
|
+
readonly Digit9: "9";
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Represents the set of allowed string literal values for keys, derived from the KeyValues object.
|
|
152
|
+
* This ensures type safety when configuring shortcuts.
|
|
153
|
+
*/
|
|
154
|
+
export type StandardKey = typeof Keys[keyof typeof Keys];
|
|
155
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyHP,CAAC;AAEX;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,CAAC"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides a set of common, standard string values for `KeyboardEvent.key`.
|
|
3
|
+
* Using these values can help avoid typos and ensure consistency.
|
|
4
|
+
* These are based on the MDN documentation:
|
|
5
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
6
|
+
*
|
|
7
|
+
* All shortcut configurations should use values from this object.
|
|
8
|
+
*/
|
|
9
|
+
export const Keys = {
|
|
10
|
+
// Special Values
|
|
11
|
+
Unidentified: "Unidentified",
|
|
12
|
+
// Modifier Keys
|
|
13
|
+
Alt: "Alt",
|
|
14
|
+
AltGraph: "AltGraph",
|
|
15
|
+
CapsLock: "CapsLock",
|
|
16
|
+
Control: "Control",
|
|
17
|
+
Fn: "Fn",
|
|
18
|
+
FnLock: "FnLock",
|
|
19
|
+
Hyper: "Hyper",
|
|
20
|
+
Meta: "Meta", // Command key on Mac, Windows key on Windows
|
|
21
|
+
NumLock: "NumLock",
|
|
22
|
+
ScrollLock: "ScrollLock",
|
|
23
|
+
Shift: "Shift",
|
|
24
|
+
Super: "Super",
|
|
25
|
+
Symbol: "Symbol",
|
|
26
|
+
SymbolLock: "SymbolLock",
|
|
27
|
+
// Whitespace Keys
|
|
28
|
+
Enter: "Enter",
|
|
29
|
+
Tab: "Tab",
|
|
30
|
+
Space: " ", // Standard value for Space Bar
|
|
31
|
+
// Navigation Keys
|
|
32
|
+
ArrowDown: "ArrowDown",
|
|
33
|
+
ArrowLeft: "ArrowLeft",
|
|
34
|
+
ArrowRight: "ArrowRight",
|
|
35
|
+
ArrowUp: "ArrowUp",
|
|
36
|
+
End: "End",
|
|
37
|
+
Home: "Home",
|
|
38
|
+
PageDown: "PageDown",
|
|
39
|
+
PageUp: "PageUp",
|
|
40
|
+
// Editing Keys
|
|
41
|
+
Backspace: "Backspace",
|
|
42
|
+
Clear: "Clear",
|
|
43
|
+
Copy: "Copy",
|
|
44
|
+
CrSel: "CrSel", // Cursor Select
|
|
45
|
+
Cut: "Cut",
|
|
46
|
+
Delete: "Delete",
|
|
47
|
+
EraseEof: "EraseEof", // Erase to End of Field
|
|
48
|
+
ExSel: "ExSel", // Extend Selection
|
|
49
|
+
Insert: "Insert",
|
|
50
|
+
Paste: "Paste",
|
|
51
|
+
Redo: "Redo",
|
|
52
|
+
Undo: "Undo",
|
|
53
|
+
// UI Keys
|
|
54
|
+
Accept: "Accept",
|
|
55
|
+
Again: "Again",
|
|
56
|
+
Attn: "Attn", // Attention
|
|
57
|
+
Cancel: "Cancel",
|
|
58
|
+
ContextMenu: "ContextMenu", // Application key
|
|
59
|
+
Escape: "Escape",
|
|
60
|
+
Execute: "Execute",
|
|
61
|
+
Find: "Find",
|
|
62
|
+
Finish: "Finish",
|
|
63
|
+
Help: "Help",
|
|
64
|
+
Pause: "Pause",
|
|
65
|
+
Play: "Play",
|
|
66
|
+
Props: "Props", // Properties
|
|
67
|
+
Select: "Select",
|
|
68
|
+
ZoomIn: "ZoomIn",
|
|
69
|
+
ZoomOut: "ZoomOut",
|
|
70
|
+
// Device Keys
|
|
71
|
+
BrightnessDown: "BrightnessDown",
|
|
72
|
+
BrightnessUp: "BrightnessUp",
|
|
73
|
+
Eject: "Eject",
|
|
74
|
+
LogOff: "LogOff",
|
|
75
|
+
Power: "Power",
|
|
76
|
+
PowerOff: "PowerOff",
|
|
77
|
+
PrintScreen: "PrintScreen",
|
|
78
|
+
Hibernate: "Hibernate",
|
|
79
|
+
Standby: "Standby", // Suspend or Sleep
|
|
80
|
+
WakeUp: "WakeUp",
|
|
81
|
+
// Function Keys
|
|
82
|
+
F1: "F1", F2: "F2", F3: "F3", F4: "F4",
|
|
83
|
+
F5: "F5", F6: "F6", F7: "F7", F8: "F8",
|
|
84
|
+
F9: "F9", F10: "F10", F11: "F11", F12: "F12",
|
|
85
|
+
F13: "F13", F14: "F14", F15: "F15", F16: "F16",
|
|
86
|
+
F17: "F17", F18: "F18", F19: "F19", F20: "F20",
|
|
87
|
+
// Phone Keys (selection)
|
|
88
|
+
AppSwitch: "AppSwitch",
|
|
89
|
+
Call: "Call",
|
|
90
|
+
Camera: "Camera",
|
|
91
|
+
EndCall: "EndCall",
|
|
92
|
+
GoBack: "GoBack",
|
|
93
|
+
GoHome: "GoHome",
|
|
94
|
+
HeadsetHook: "HeadsetHook",
|
|
95
|
+
// Multimedia Keys (selection)
|
|
96
|
+
MediaPlayPause: "MediaPlayPause",
|
|
97
|
+
MediaStop: "MediaStop",
|
|
98
|
+
MediaTrackNext: "MediaTrackNext",
|
|
99
|
+
MediaTrackPrevious: "MediaTrackPrevious",
|
|
100
|
+
AudioVolumeDown: "AudioVolumeDown",
|
|
101
|
+
AudioVolumeUp: "AudioVolumeUp",
|
|
102
|
+
AudioVolumeMute: "AudioVolumeMute",
|
|
103
|
+
// Numeric Keypad (special characters, numbers 0-9 are via KeyValues.DigitN)
|
|
104
|
+
Decimal: ".", // This is the character for the decimal point
|
|
105
|
+
KeypadMultiply: "*",
|
|
106
|
+
KeypadAdd: "+",
|
|
107
|
+
KeypadSubtract: "-",
|
|
108
|
+
KeypadDivide: "/",
|
|
109
|
+
// Character Keys (Uppercase A-Z for configuration via KeyValues)
|
|
110
|
+
// The library handles case-insensitivity for these when matching browser events.
|
|
111
|
+
A: "A", B: "B", C: "C", D: "D", E: "E", F: "F", G: "G", H: "H", I: "I",
|
|
112
|
+
J: "J", K: "K", L: "L", M: "M", N: "N", O: "O", P: "P", Q: "Q", R: "R",
|
|
113
|
+
S: "S", T: "T", U: "U", V: "V", W: "W", X: "X", Y: "Y", Z: "Z",
|
|
114
|
+
// Digit Keys (0-9 for configuration via KeyValues)
|
|
115
|
+
Digit0: "0", Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4",
|
|
116
|
+
Digit5: "5", Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9",
|
|
117
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function createMockFn(): {
|
|
2
|
+
(...args: any[]): void;
|
|
3
|
+
calledCount: number;
|
|
4
|
+
calls: any[];
|
|
5
|
+
lastArgs: any[];
|
|
6
|
+
mockClear(): void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Dispatches a KeyboardEvent to the document (JSDOM).
|
|
10
|
+
* @param key - The key value, e.g., "a", "Escape", "ArrowUp"
|
|
11
|
+
* @param modifiers - Optional modifier keys
|
|
12
|
+
*/
|
|
13
|
+
export declare function dispatchKeyEvent(key: string, modifiers?: Partial<KeyboardEventInit>): KeyboardEvent;
|
|
14
|
+
//# sourceMappingURL=testutils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testutils.d.ts","sourceRoot":"","sources":["../src/testutils.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY;cACH,GAAG,EAAE;;;;;EAc7B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC5B,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAC3C,aAAa,CASf"}
|