rx-hotkeys 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -8
- package/dist/hotkeys.d.ts +39 -11
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +180 -44
- package/dist/hotkeys.test.js +173 -1
- 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/dist/ttt.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { fromEvent, BehaviorSubject, EMPTY, filter, map, bufferCount, withLatestFrom, tap, catchError, scan, merge, } 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 = {}));
|
|
8
|
+
var EmitStates;
|
|
9
|
+
(function (EmitStates) {
|
|
10
|
+
EmitStates[EmitStates["Emit"] = 0] = "Emit";
|
|
11
|
+
EmitStates[EmitStates["Ignore"] = 1] = "Ignore";
|
|
12
|
+
EmitStates[EmitStates["InProgress"] = 2] = "InProgress";
|
|
13
|
+
})(EmitStates || (EmitStates = {}));
|
|
14
|
+
// --- Helper function to compare keys ---
|
|
15
|
+
/**
|
|
16
|
+
* Compares a browser event's key with a configured key.
|
|
17
|
+
* - For single character keys (e.g., "a", "A", "7"), comparison is case-insensitive.
|
|
18
|
+
* - For multi-character special keys (e.g., "Enter", "ArrowUp"), comparison is case-sensitive.
|
|
19
|
+
*/
|
|
20
|
+
function compareKey(eventKey, configuredKey) {
|
|
21
|
+
if (configuredKey.length === 1 && eventKey.length === 1) {
|
|
22
|
+
return eventKey.toLowerCase() === configuredKey.toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
return eventKey === configuredKey;
|
|
25
|
+
}
|
|
26
|
+
// --- Hotkeys Library ---
|
|
27
|
+
/**
|
|
28
|
+
* Manages keyboard shortcuts for web applications.
|
|
29
|
+
* Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
|
|
30
|
+
* Supports contexts to enable/disable shortcuts based on application state.
|
|
31
|
+
*/
|
|
32
|
+
export class Hotkeys {
|
|
33
|
+
static KEYDOWN_EVENT = "keydown";
|
|
34
|
+
static LOG_PREFIX = "Hotkeys:";
|
|
35
|
+
keydown$;
|
|
36
|
+
activeContext$;
|
|
37
|
+
activeShortcuts;
|
|
38
|
+
debugMode;
|
|
39
|
+
/**
|
|
40
|
+
* Creates an instance of Hotkeys.
|
|
41
|
+
* @param initialContext - Optional initial context name. Shortcuts will only trigger if their context matches this, or if they have no context defined.
|
|
42
|
+
* @param debugMode - Optional. If true, debug messages will be logged to the console. Defaults to false.
|
|
43
|
+
* @throws Error if not in a browser environment (i.e., `document` or `performance` is undefined).
|
|
44
|
+
*/
|
|
45
|
+
constructor(initialContext = null, debugMode = false) {
|
|
46
|
+
this.debugMode = debugMode;
|
|
47
|
+
if (typeof document === "undefined" || typeof performance === "undefined") {
|
|
48
|
+
throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment with global "document" and "performance" objects.`);
|
|
49
|
+
}
|
|
50
|
+
this.keydown$ = fromEvent(document, Hotkeys.KEYDOWN_EVENT);
|
|
51
|
+
this.activeContext$ = new BehaviorSubject(initialContext);
|
|
52
|
+
this.activeShortcuts = new Map();
|
|
53
|
+
if (this.debugMode) {
|
|
54
|
+
console.log(`${Hotkeys.LOG_PREFIX} Library initialized. Initial context: "${initialContext}". Debug mode: ${debugMode}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sets the active context for shortcuts.
|
|
59
|
+
* Only shortcuts matching this context (or shortcuts with no specific context defined)
|
|
60
|
+
* will be active and can be triggered.
|
|
61
|
+
* @param contextName - The name of the context (e.g., "modal", "editor", "global").
|
|
62
|
+
* Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
|
|
63
|
+
* @returns `true` if the context was changed, `false` if the new context was the same as the current one.
|
|
64
|
+
*/
|
|
65
|
+
setContext(contextName) {
|
|
66
|
+
const currentContext = this.activeContext$.getValue();
|
|
67
|
+
if (currentContext === contextName) {
|
|
68
|
+
if (this.debugMode) {
|
|
69
|
+
// Optional: Log that no change is happening, or simply do nothing.
|
|
70
|
+
console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
|
|
71
|
+
}
|
|
72
|
+
return false; // Context was NOT updated
|
|
73
|
+
}
|
|
74
|
+
// If we reach here, the context is actually changing.
|
|
75
|
+
if (this.debugMode) {
|
|
76
|
+
console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${currentContext}" to "${contextName}".`);
|
|
77
|
+
}
|
|
78
|
+
this.activeContext$.next(contextName);
|
|
79
|
+
return true; // Context WAS updated
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Gets the current active context.
|
|
83
|
+
* @returns The current context name as a string, or `null` if no context is set.
|
|
84
|
+
*/
|
|
85
|
+
getContext() {
|
|
86
|
+
return this.activeContext$.getValue();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Enables or disables debug logging for the Hotkeys instance.
|
|
90
|
+
* When enabled, various internal actions and shortcut triggers will be logged to the console.
|
|
91
|
+
* @param enable - True to enable debug logs, false to disable.
|
|
92
|
+
*/
|
|
93
|
+
setDebugMode(enable) {
|
|
94
|
+
if (this.debugMode === enable) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.debugMode = enable;
|
|
98
|
+
if (enable) {
|
|
99
|
+
console.log(`${Hotkeys.LOG_PREFIX} Debug mode enabled.`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log(`${Hotkeys.LOG_PREFIX} Debug mode disabled.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Checks if a shortcut with the given ID is currently registered and active.
|
|
107
|
+
* @param id - The unique ID of the shortcut to check.
|
|
108
|
+
* @returns True if a shortcut with the specified ID exists, false otherwise.
|
|
109
|
+
*/
|
|
110
|
+
hasShortcut(id) {
|
|
111
|
+
return this.activeShortcuts.has(id);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* An Observable that emits the new context name (or null) whenever the active context changes.
|
|
115
|
+
*/
|
|
116
|
+
get onContextChange$() {
|
|
117
|
+
return this.activeContext$.asObservable();
|
|
118
|
+
}
|
|
119
|
+
_areSequencesIdentical(seq1, seq2) {
|
|
120
|
+
if (seq1.length !== seq2.length) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
for (let i = 0; i < seq1.length; i++) {
|
|
124
|
+
if (seq1[i] !== seq2[i]) { // Direct comparison for canonical StandardKey values
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
_shortcutMatchesEvent(shortcutConfig, event) {
|
|
131
|
+
const keyTriggers = Array.isArray(shortcutConfig.keys) ? shortcutConfig.keys : [shortcutConfig.keys];
|
|
132
|
+
for (const keyInput of keyTriggers) {
|
|
133
|
+
let configuredMainKey;
|
|
134
|
+
let ctrlKeyConfig;
|
|
135
|
+
let altKeyConfig;
|
|
136
|
+
let shiftKeyConfig;
|
|
137
|
+
let metaKeyConfig;
|
|
138
|
+
if (typeof keyInput === "string") {
|
|
139
|
+
if (keyInput === "")
|
|
140
|
+
continue; // Invalid trigger, skip
|
|
141
|
+
configuredMainKey = keyInput;
|
|
142
|
+
ctrlKeyConfig = false;
|
|
143
|
+
altKeyConfig = false;
|
|
144
|
+
shiftKeyConfig = false;
|
|
145
|
+
metaKeyConfig = false;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
if (!keyInput.key || keyInput.key === "")
|
|
149
|
+
continue; // Invalid trigger, skip
|
|
150
|
+
configuredMainKey = keyInput.key;
|
|
151
|
+
ctrlKeyConfig = keyInput.ctrlKey;
|
|
152
|
+
altKeyConfig = keyInput.altKey;
|
|
153
|
+
shiftKeyConfig = keyInput.shiftKey;
|
|
154
|
+
metaKeyConfig = keyInput.metaKey;
|
|
155
|
+
}
|
|
156
|
+
const keyMatch = compareKey(event.key, configuredMainKey);
|
|
157
|
+
if (!keyMatch)
|
|
158
|
+
continue;
|
|
159
|
+
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
160
|
+
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
161
|
+
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
162
|
+
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
163
|
+
if (ctrlMatch && altMatch && shiftMatch && metaMatch) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
filterByContext(source$, context) {
|
|
170
|
+
return source$.pipe(withLatestFrom(this.activeContext$), filter(([/* event */ , activeCtx]) => {
|
|
171
|
+
// If context is explicitly null, only match when active context is also null.
|
|
172
|
+
if (context === null) {
|
|
173
|
+
return activeCtx === null;
|
|
174
|
+
}
|
|
175
|
+
// If context is undefined (truly global), it can run alongside a specific context.
|
|
176
|
+
// If context is a string, it must match the active context.
|
|
177
|
+
return context === undefined || context === activeCtx;
|
|
178
|
+
}), map(([event, /* _activeCtx */]) => event));
|
|
179
|
+
}
|
|
180
|
+
_registerShortcut(config, subscription, type, detailsForLog) {
|
|
181
|
+
const existingShortcut = this.activeShortcuts.get(config.id);
|
|
182
|
+
if (existingShortcut) {
|
|
183
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
|
|
184
|
+
existingShortcut.subscription.unsubscribe();
|
|
185
|
+
}
|
|
186
|
+
this.activeShortcuts.set(config.id, { id: config.id, config, subscription });
|
|
187
|
+
if (this.debugMode) {
|
|
188
|
+
console.log(`${Hotkeys.LOG_PREFIX} ${type} shortcut "${config.id}" added. ${detailsForLog}, Context: ${config.context ?? "any"}`);
|
|
189
|
+
}
|
|
190
|
+
return config.id;
|
|
191
|
+
}
|
|
192
|
+
_parseKeyTrigger(keyInput, shortcutId) {
|
|
193
|
+
if (typeof keyInput === "string") {
|
|
194
|
+
if (keyInput === "") {
|
|
195
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key (shorthand) in shortcut "${shortcutId}". Key string must not be empty.`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
configuredMainKey: keyInput,
|
|
200
|
+
ctrlKeyConfig: false,
|
|
201
|
+
altKeyConfig: false,
|
|
202
|
+
shiftKeyConfig: false,
|
|
203
|
+
metaKeyConfig: false,
|
|
204
|
+
logDetails: `key: "${keyInput}" (no mods)`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
if (!keyInput.key || typeof keyInput.key !== "string" || keyInput.key === "") {
|
|
209
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "key" property in shortcut "${shortcutId}". Key must be a non-empty string value from Keys.`);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const logDetails = `key: "${keyInput.key}"` +
|
|
213
|
+
(keyInput.ctrlKey !== undefined ? `, ctrl: ${keyInput.ctrlKey}` : "") +
|
|
214
|
+
(keyInput.altKey !== undefined ? `, alt: ${keyInput.altKey}` : "") +
|
|
215
|
+
(keyInput.shiftKey !== undefined ? `, shift: ${keyInput.shiftKey}` : "") +
|
|
216
|
+
(keyInput.metaKey !== undefined ? `, meta: ${keyInput.metaKey}` : "");
|
|
217
|
+
return {
|
|
218
|
+
configuredMainKey: keyInput.key,
|
|
219
|
+
ctrlKeyConfig: keyInput.ctrlKey,
|
|
220
|
+
altKeyConfig: keyInput.altKey,
|
|
221
|
+
shiftKeyConfig: keyInput.shiftKey,
|
|
222
|
+
metaKeyConfig: keyInput.metaKey,
|
|
223
|
+
logDetails,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Registers a key combination shortcut.
|
|
229
|
+
* @param config - Configuration object for the key combination.
|
|
230
|
+
* @param strict - If `true` and the shortcut has no context, it will only fire when no context is active. Defaults to `false`.
|
|
231
|
+
* @returns The ID of the registered shortcut, or `undefined` if invalid.
|
|
232
|
+
*/
|
|
233
|
+
addCombination(config, strict = false) {
|
|
234
|
+
const { keys, callback, context, preventDefault = false, id } = config;
|
|
235
|
+
if (context && strict) {
|
|
236
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context and the 'strict' flag. The 'strict' flag will be ignored.`);
|
|
237
|
+
}
|
|
238
|
+
const keyTriggers = Array.isArray(keys) ? keys : [keys];
|
|
239
|
+
if (keyTriggers.length === 0) {
|
|
240
|
+
console.warn(`${Hotkeys.LOG_PREFIX} "keys" array for combination shortcut "${id}" is empty. Shortcut not added.`);
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
const observables = [];
|
|
244
|
+
const logParts = [];
|
|
245
|
+
// Determine the base stream based on the strict flag
|
|
246
|
+
const baseStream$ = (context == null && strict)
|
|
247
|
+
? this.keydown$.pipe(withLatestFrom(this.activeContext$), filter(([, activeCtx]) => activeCtx === null), map(([event]) => event))
|
|
248
|
+
: this.keydown$;
|
|
249
|
+
for (const keyInput of keyTriggers) {
|
|
250
|
+
const parsedTrigger = this._parseKeyTrigger(keyInput, id);
|
|
251
|
+
if (!parsedTrigger)
|
|
252
|
+
return undefined;
|
|
253
|
+
const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig, logDetails } = parsedTrigger;
|
|
254
|
+
logParts.push(`{ ${logDetails} }`);
|
|
255
|
+
const stream = baseStream$.pipe(
|
|
256
|
+
// For non-strict shortcuts, filter by context here
|
|
257
|
+
filter(() => {
|
|
258
|
+
if (context == null && strict)
|
|
259
|
+
return true; // Already filtered by baseStream$
|
|
260
|
+
return context === this.activeContext$.getValue();
|
|
261
|
+
}), filter(event => {
|
|
262
|
+
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
263
|
+
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
264
|
+
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
265
|
+
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
266
|
+
return ctrlMatch && altMatch && shiftMatch && metaMatch;
|
|
267
|
+
}), filter(event => compareKey(event.key, configuredMainKey)),
|
|
268
|
+
// Original priority filter for non-strict global shortcuts
|
|
269
|
+
filter(event => {
|
|
270
|
+
if (context != null || strict)
|
|
271
|
+
return true;
|
|
272
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
273
|
+
if (currentSpecificContext == null)
|
|
274
|
+
return true;
|
|
275
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
276
|
+
if (otherAS.config.id !== config.id &&
|
|
277
|
+
'keys' in otherAS.config &&
|
|
278
|
+
otherAS.config.context === currentSpecificContext &&
|
|
279
|
+
this._shortcutMatchesEvent(otherAS.config, event)) {
|
|
280
|
+
if (this.debugMode) {
|
|
281
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${config.id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}));
|
|
288
|
+
observables.push(stream);
|
|
289
|
+
}
|
|
290
|
+
if (observables.length === 0)
|
|
291
|
+
return undefined;
|
|
292
|
+
const finalShortcut$ = merge(...observables);
|
|
293
|
+
const overallLogDetails = `Triggers: [ ${logParts.join(", ")} ]`;
|
|
294
|
+
const subscription = finalShortcut$.pipe(tap(event => {
|
|
295
|
+
if (this.debugMode) {
|
|
296
|
+
console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered by key "${event.key}".`);
|
|
297
|
+
}
|
|
298
|
+
if (preventDefault)
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
}), catchError(err => {
|
|
301
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in stream for shortcut "${id}":`, err);
|
|
302
|
+
return EMPTY;
|
|
303
|
+
})).subscribe(event => {
|
|
304
|
+
try {
|
|
305
|
+
callback(event);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in callback for combination shortcut "${id}":`, e);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Combination, overallLogDetails);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Registers a key sequence shortcut.
|
|
315
|
+
* @param config - Configuration object for the key sequence.
|
|
316
|
+
* @param strict - If `true` and the shortcut has no context, it will only fire when no context is active. Defaults to `false`.
|
|
317
|
+
* @returns The ID of the registered shortcut, or `undefined` if invalid.
|
|
318
|
+
*/
|
|
319
|
+
addSequence(config, strict = false) {
|
|
320
|
+
const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs } = config;
|
|
321
|
+
if (!Array.isArray(sequence) || sequence.length === 0) {
|
|
322
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty or invalid. Shortcut not added.`);
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
if (sequence.some(key => typeof key !== "string" || key === "")) {
|
|
326
|
+
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.`);
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
if (context && strict) {
|
|
330
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context and the 'strict' flag. The 'strict' flag will be ignored.`);
|
|
331
|
+
}
|
|
332
|
+
const sequenceLength = sequence.length;
|
|
333
|
+
const baseKeydownStream$ = (context == null && strict)
|
|
334
|
+
? this.keydown$.pipe(withLatestFrom(this.activeContext$), filter(([, activeCtx]) => activeCtx === null), map(([event]) => event))
|
|
335
|
+
: this.filterByContext(this.keydown$, context);
|
|
336
|
+
let shortcut$;
|
|
337
|
+
if (sequenceTimeoutMs && sequenceTimeoutMs > 0) {
|
|
338
|
+
shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
|
|
339
|
+
// (Scan logic remains the same as original)
|
|
340
|
+
let { matchedEvents, lastEventTime } = acc;
|
|
341
|
+
const currentTime = performance.now();
|
|
342
|
+
if (acc.emitState === EmitStates.Emit)
|
|
343
|
+
matchedEvents = [];
|
|
344
|
+
if (matchedEvents.length > 0 && (currentTime - lastEventTime > sequenceTimeoutMs))
|
|
345
|
+
matchedEvents = [];
|
|
346
|
+
if (compareKey(event.key, sequence[matchedEvents.length])) {
|
|
347
|
+
const newMatched = [...matchedEvents, event];
|
|
348
|
+
if (newMatched.length === sequenceLength)
|
|
349
|
+
return { matchedEvents: newMatched, lastEventTime: currentTime, emitState: EmitStates.Emit };
|
|
350
|
+
return { matchedEvents: newMatched, lastEventTime: currentTime, emitState: EmitStates.InProgress };
|
|
351
|
+
}
|
|
352
|
+
if (compareKey(event.key, sequence[0]))
|
|
353
|
+
return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
|
|
354
|
+
return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
|
|
355
|
+
}, { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore }), filter(state => state.emitState === EmitStates.Emit), map(state => state.matchedEvents));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
shortcut$ = baseKeydownStream$.pipe(bufferCount(sequenceLength, 1), filter((events) => events.every((event, index) => compareKey(event.key, sequence[index]))));
|
|
359
|
+
}
|
|
360
|
+
const finalShortcutWithPriority$ = shortcut$.pipe(filter(() => {
|
|
361
|
+
if (context != null || strict)
|
|
362
|
+
return true;
|
|
363
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
364
|
+
if (currentSpecificContext == null)
|
|
365
|
+
return true;
|
|
366
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
367
|
+
if (otherAS.config.id !== config.id &&
|
|
368
|
+
"sequence" in otherAS.config &&
|
|
369
|
+
otherAS.config.context === currentSpecificContext &&
|
|
370
|
+
this._areSequencesIdentical(config.sequence, otherAS.config.sequence)) {
|
|
371
|
+
if (this.debugMode) {
|
|
372
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global sequence shortcut "${config.id}" suppressed by identical specific-context shortcut "${otherAS.config.id}".`);
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}), tap(events => {
|
|
379
|
+
if (preventDefault && events.length > 0)
|
|
380
|
+
events[events.length - 1].preventDefault();
|
|
381
|
+
}), catchError(err => {
|
|
382
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in sequence stream for shortcut "${id}":`, err);
|
|
383
|
+
return EMPTY;
|
|
384
|
+
}));
|
|
385
|
+
const subscription = finalShortcutWithPriority$.subscribe((events) => {
|
|
386
|
+
try {
|
|
387
|
+
if (events.length > 0)
|
|
388
|
+
callback(events[events.length - 1]);
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in user callback for sequence shortcut "${id}":`, e);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const logDetails = `Sequence: ${sequence.join(" -> ")}`;
|
|
395
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Removes a registered shortcut by its ID.
|
|
399
|
+
*/
|
|
400
|
+
remove(id) {
|
|
401
|
+
const shortcut = this.activeShortcuts.get(id);
|
|
402
|
+
if (shortcut) {
|
|
403
|
+
shortcut.subscription.unsubscribe();
|
|
404
|
+
this.activeShortcuts.delete(id);
|
|
405
|
+
if (this.debugMode)
|
|
406
|
+
console.log(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" removed.`);
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${id}" not found for removal.`);
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Retrieves a list of all currently active (registered) shortcut configurations.
|
|
414
|
+
*/
|
|
415
|
+
getActiveShortcuts() {
|
|
416
|
+
const shortcuts = [];
|
|
417
|
+
for (const [id, activeShortcut] of this.activeShortcuts.entries()) {
|
|
418
|
+
shortcuts.push({
|
|
419
|
+
id,
|
|
420
|
+
description: activeShortcut.config.description,
|
|
421
|
+
context: activeShortcut.config.context,
|
|
422
|
+
type: ("sequence" in activeShortcut.config) ? ShortcutTypes.Sequence : ShortcutTypes.Combination
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return shortcuts;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Cleans up all active subscriptions and resources used by the Hotkeys instance.
|
|
429
|
+
*/
|
|
430
|
+
destroy() {
|
|
431
|
+
if (this.debugMode)
|
|
432
|
+
console.log(`${Hotkeys.LOG_PREFIX} Destroying library instance and unsubscribing all shortcuts.`);
|
|
433
|
+
this.activeShortcuts.forEach(shortcut => shortcut.subscription.unsubscribe());
|
|
434
|
+
this.activeShortcuts.clear();
|
|
435
|
+
this.activeContext$.complete();
|
|
436
|
+
if (this.debugMode)
|
|
437
|
+
console.log(`${Hotkeys.LOG_PREFIX} Library destroyed.`);
|
|
438
|
+
}
|
|
439
|
+
}
|
package/dist/tttt.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
type KeyCombinationTrigger = {
|
|
15
|
+
key: StandardKey;
|
|
16
|
+
ctrlKey?: boolean;
|
|
17
|
+
altKey?: boolean;
|
|
18
|
+
shiftKey?: boolean;
|
|
19
|
+
metaKey?: boolean;
|
|
20
|
+
} | StandardKey;
|
|
21
|
+
export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
22
|
+
keys: KeyCombinationTrigger | KeyCombinationTrigger[];
|
|
23
|
+
}
|
|
24
|
+
export interface KeySequenceConfig extends ShortcutConfigBase {
|
|
25
|
+
sequence: StandardKey[];
|
|
26
|
+
sequenceTimeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
type ShortcutConfig = KeyCombinationConfig | KeySequenceConfig;
|
|
29
|
+
export interface ActiveShortcut {
|
|
30
|
+
id: string;
|
|
31
|
+
config: ShortcutConfig;
|
|
32
|
+
subscription: Subscription;
|
|
33
|
+
}
|
|
34
|
+
export declare class Hotkeys {
|
|
35
|
+
private static readonly KEYDOWN_EVENT;
|
|
36
|
+
private static readonly LOG_PREFIX;
|
|
37
|
+
private keydown$;
|
|
38
|
+
private activeContext$;
|
|
39
|
+
private activeShortcuts;
|
|
40
|
+
private debugMode;
|
|
41
|
+
constructor(initialContext?: string | null, debugMode?: boolean);
|
|
42
|
+
setContext(contextName: string | null): boolean;
|
|
43
|
+
getContext(): string | null;
|
|
44
|
+
setDebugMode(enable: boolean): void;
|
|
45
|
+
hasShortcut(id: string): boolean;
|
|
46
|
+
get onContextChange$(): Observable<string | null>;
|
|
47
|
+
private _areSequencesIdentical;
|
|
48
|
+
private _shortcutMatchesEvent;
|
|
49
|
+
/**
|
|
50
|
+
* Pre-filters a stream of keyboard events based on context and strictness rules.
|
|
51
|
+
*/
|
|
52
|
+
private filterByContext;
|
|
53
|
+
private _registerShortcut;
|
|
54
|
+
private _parseKeyTrigger;
|
|
55
|
+
addCombination(config: KeyCombinationConfig, strict?: boolean): string | undefined;
|
|
56
|
+
addSequence(config: KeySequenceConfig, strict?: boolean): string | undefined;
|
|
57
|
+
remove(id: string): boolean;
|
|
58
|
+
getActiveShortcuts(): {
|
|
59
|
+
id: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
context?: string | null;
|
|
62
|
+
type: ShortcutTypes;
|
|
63
|
+
}[];
|
|
64
|
+
destroy(): void;
|
|
65
|
+
}
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=tttt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tttt.d.ts","sourceRoot":"","sources":["../src/tttt.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;CACxB;AAED,KAAK,qBAAqB,GAAG;IACzB,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,IAAI,EAAE,qBAAqB,GAAG,qBAAqB,EAAE,CAAC;CACzD;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;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;AASD,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;gBAEf,cAAc,GAAE,MAAM,GAAG,IAAW,EAAE,SAAS,GAAE,OAAe;IAarE,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAY/C,UAAU,IAAI,MAAM,GAAG,IAAI;IAI3B,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAMnC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIvC,IAAW,gBAAgB,IAAI,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,CAEvD;IAED,OAAO,CAAC,sBAAsB;IAK9B,OAAO,CAAC,qBAAqB;IAsB7B;;OAEG;IACH,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,gBAAgB;IA6BjB,cAAc,CAAC,MAAM,EAAE,oBAAoB,EAAE,MAAM,UAAQ,GAAG,MAAM,GAAG,SAAS;IA8EhF,WAAW,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,UAAQ,GAAG,MAAM,GAAG,SAAS;IAmE1E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAU3B,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;IASxG,OAAO,IAAI,IAAI;CAKzB"}
|