rx-hotkeys 2.5.0 → 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 +9 -0
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +33 -17
- package/dist/hotkeys.test.js +94 -0
- package/dist/tt.d.ts +125 -0
- package/dist/tt.d.ts.map +1 -0
- package/dist/tt.js +384 -0
- package/dist/ttt.d.ts +164 -0
- package/dist/ttt.d.ts.map +1 -0
- package/dist/ttt.js +439 -0
- package/dist/tttt.d.ts +67 -0
- package/dist/tttt.d.ts.map +1 -0
- package/dist/tttt.js +301 -0
- package/package.json +1 -1
package/dist/tt.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
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
|
+
*/
|
|
30
|
+
export class Hotkeys {
|
|
31
|
+
static KEYDOWN_EVENT = "keydown";
|
|
32
|
+
static LOG_PREFIX = "Hotkeys:";
|
|
33
|
+
keydown$;
|
|
34
|
+
activeContext$;
|
|
35
|
+
activeShortcuts;
|
|
36
|
+
debugMode;
|
|
37
|
+
/**
|
|
38
|
+
* Creates an instance of Hotkeys.
|
|
39
|
+
* @param initialContext - Optional initial context name.
|
|
40
|
+
* @param debugMode - Optional. If true, debug messages will be logged to the console.
|
|
41
|
+
*/
|
|
42
|
+
constructor(initialContext = null, debugMode = false) {
|
|
43
|
+
this.debugMode = debugMode;
|
|
44
|
+
if (typeof document === "undefined" || typeof performance === "undefined") {
|
|
45
|
+
throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment with global "document" and "performance" objects.`);
|
|
46
|
+
}
|
|
47
|
+
this.keydown$ = fromEvent(document, Hotkeys.KEYDOWN_EVENT);
|
|
48
|
+
this.activeContext$ = new BehaviorSubject(initialContext);
|
|
49
|
+
this.activeShortcuts = new Map();
|
|
50
|
+
if (this.debugMode) {
|
|
51
|
+
console.log(`${Hotkeys.LOG_PREFIX} Library initialized. Initial context: "${initialContext}". Debug mode: ${debugMode}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sets the active context for shortcuts.
|
|
56
|
+
* @param contextName - The name of the context (e.g., "editor"). Pass `null` to clear the context.
|
|
57
|
+
* @returns `true` if the context was changed, `false` otherwise.
|
|
58
|
+
*/
|
|
59
|
+
setContext(contextName) {
|
|
60
|
+
const currentContext = this.activeContext$.getValue();
|
|
61
|
+
if (currentContext === contextName) {
|
|
62
|
+
if (this.debugMode) {
|
|
63
|
+
console.log(`${Hotkeys.LOG_PREFIX} setContext called with the same context "${contextName}". No change made.`);
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (this.debugMode) {
|
|
68
|
+
console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${currentContext}" to "${contextName}".`);
|
|
69
|
+
}
|
|
70
|
+
this.activeContext$.next(contextName);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Gets the current active context.
|
|
75
|
+
*/
|
|
76
|
+
getContext() {
|
|
77
|
+
return this.activeContext$.getValue();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Enables or disables debug logging.
|
|
81
|
+
*/
|
|
82
|
+
setDebugMode(enable) {
|
|
83
|
+
if (this.debugMode === enable)
|
|
84
|
+
return;
|
|
85
|
+
this.debugMode = enable;
|
|
86
|
+
console.log(`${Hotkeys.LOG_PREFIX} Debug mode ${enable ? "enabled" : "disabled"}.`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Checks if a shortcut with the given ID is registered.
|
|
90
|
+
*/
|
|
91
|
+
hasShortcut(id) {
|
|
92
|
+
return this.activeShortcuts.has(id);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* An Observable that emits the new context name whenever it changes.
|
|
96
|
+
*/
|
|
97
|
+
get onContextChange$() {
|
|
98
|
+
return this.activeContext$.asObservable();
|
|
99
|
+
}
|
|
100
|
+
_areSequencesIdentical(seq1, seq2) {
|
|
101
|
+
if (seq1.length !== seq2.length)
|
|
102
|
+
return false;
|
|
103
|
+
for (let i = 0; i < seq1.length; i++) {
|
|
104
|
+
if (seq1[i] !== seq2[i])
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
_shortcutMatchesEvent(shortcutConfig, event) {
|
|
110
|
+
const keyTriggers = Array.isArray(shortcutConfig.keys) ? shortcutConfig.keys : [shortcutConfig.keys];
|
|
111
|
+
for (const keyInput of keyTriggers) {
|
|
112
|
+
let configuredMainKey;
|
|
113
|
+
let ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig;
|
|
114
|
+
if (typeof keyInput === "string") {
|
|
115
|
+
if (keyInput === "")
|
|
116
|
+
continue;
|
|
117
|
+
configuredMainKey = keyInput;
|
|
118
|
+
[ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig] = [false, false, false, false];
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
if (!keyInput.key)
|
|
122
|
+
continue;
|
|
123
|
+
configuredMainKey = keyInput.key;
|
|
124
|
+
({ ctrlKey: ctrlKeyConfig, altKey: altKeyConfig, shiftKey: shiftKeyConfig, metaKey: metaKeyConfig } = keyInput);
|
|
125
|
+
}
|
|
126
|
+
const keyMatch = compareKey(event.key, configuredMainKey);
|
|
127
|
+
if (!keyMatch)
|
|
128
|
+
continue;
|
|
129
|
+
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
130
|
+
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
131
|
+
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
132
|
+
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
133
|
+
if (ctrlMatch && altMatch && shiftMatch && metaMatch) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
_registerShortcut(config, sub, type, log) {
|
|
140
|
+
const existing = this.activeShortcuts.get(config.id);
|
|
141
|
+
if (existing) {
|
|
142
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
|
|
143
|
+
existing.subscription.unsubscribe();
|
|
144
|
+
}
|
|
145
|
+
this.activeShortcuts.set(config.id, { id: config.id, config, subscription: sub });
|
|
146
|
+
if (this.debugMode) {
|
|
147
|
+
const contextLog = config.context ? `"${config.context}"` : config.strict ? "global (strict)" : "global (default)";
|
|
148
|
+
console.log(`${Hotkeys.LOG_PREFIX} ${type} shortcut "${config.id}" added. ${log}, Context: ${contextLog}`);
|
|
149
|
+
}
|
|
150
|
+
return config.id;
|
|
151
|
+
}
|
|
152
|
+
_parseKeyTrigger(keyInput, shortcutId) {
|
|
153
|
+
if (typeof keyInput === "string") {
|
|
154
|
+
if (keyInput === "") {
|
|
155
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key (shorthand) in shortcut "${shortcutId}". Key must not be empty.`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
configuredMainKey: keyInput, ctrlKeyConfig: false, altKeyConfig: false,
|
|
160
|
+
shiftKeyConfig: false, metaKeyConfig: false,
|
|
161
|
+
logDetails: `key: "${keyInput}" (no mods)`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
if (!keyInput.key) {
|
|
166
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid "key" property in shortcut "${shortcutId}". Key must be a non-empty string.`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const logDetails = `key: "${keyInput.key}"` +
|
|
170
|
+
(keyInput.ctrlKey !== undefined ? `, ctrl: ${keyInput.ctrlKey}` : "") +
|
|
171
|
+
(keyInput.altKey !== undefined ? `, alt: ${keyInput.altKey}` : "") +
|
|
172
|
+
(keyInput.shiftKey !== undefined ? `, shift: ${keyInput.shiftKey}` : "") +
|
|
173
|
+
(keyInput.metaKey !== undefined ? `, meta: ${keyInput.metaKey}` : "");
|
|
174
|
+
return {
|
|
175
|
+
configuredMainKey: keyInput.key, ctrlKeyConfig: keyInput.ctrlKey, altKeyConfig: keyInput.altKey,
|
|
176
|
+
shiftKeyConfig: keyInput.shiftKey, metaKeyConfig: keyInput.metaKey,
|
|
177
|
+
logDetails,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
addCombination(config) {
|
|
182
|
+
const { keys, callback, context, preventDefault = false, id, strict = false } = config;
|
|
183
|
+
const keyTriggers = Array.isArray(keys) ? keys : [keys];
|
|
184
|
+
if (keyTriggers.length === 0) {
|
|
185
|
+
console.warn(`${Hotkeys.LOG_PREFIX} "keys" array for combination shortcut "${id}" is empty. Shortcut not added.`);
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
if (context != null && strict) {
|
|
189
|
+
console.warn(`${Hotkeys.LOG_PREFIX} In shortcut "${id}", the "strict" flag is redundant because the shortcut has a top-level context "${context}". Flag will be ignored.`);
|
|
190
|
+
}
|
|
191
|
+
const baseStream$ = (context == null && strict)
|
|
192
|
+
? this.keydown$.pipe(// Strictly global, only listen when context is null
|
|
193
|
+
withLatestFrom(this.activeContext$), filter(([, activeCtx]) => activeCtx === null), map(([event]) => event))
|
|
194
|
+
: this.keydown$; // Default global or context-specific, listen to all keys
|
|
195
|
+
const observables = [];
|
|
196
|
+
const logParts = [];
|
|
197
|
+
for (const keyInput of keyTriggers) {
|
|
198
|
+
const parsedTrigger = this._parseKeyTrigger(keyInput, id);
|
|
199
|
+
if (!parsedTrigger)
|
|
200
|
+
return undefined;
|
|
201
|
+
const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig, logDetails } = parsedTrigger;
|
|
202
|
+
logParts.push(`{ ${logDetails} }`);
|
|
203
|
+
const stream = baseStream$.pipe(
|
|
204
|
+
// Filter by specific context if the shortcut is not strictly global
|
|
205
|
+
filter(() => {
|
|
206
|
+
if (context != null) {
|
|
207
|
+
return context === this.activeContext$.getValue();
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}),
|
|
211
|
+
// Match the key and modifiers for this specific trigger
|
|
212
|
+
filter(event => {
|
|
213
|
+
const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
|
|
214
|
+
const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
|
|
215
|
+
const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
|
|
216
|
+
const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
|
|
217
|
+
return ctrlMatch && altMatch && shiftMatch && metaMatch && compareKey(event.key, configuredMainKey);
|
|
218
|
+
}),
|
|
219
|
+
// Priority filter for global shortcuts to prevent them from firing over specific ones
|
|
220
|
+
filter(event => {
|
|
221
|
+
if (context != null || strict)
|
|
222
|
+
return true;
|
|
223
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
224
|
+
if (currentSpecificContext == null)
|
|
225
|
+
return true;
|
|
226
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
227
|
+
if (otherAS.config.id !== config.id && 'keys' in otherAS.config &&
|
|
228
|
+
otherAS.config.context === currentSpecificContext && this._shortcutMatchesEvent(otherAS.config, event)) {
|
|
229
|
+
if (this.debugMode) {
|
|
230
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${config.id}" (key: "${event.key}") suppressed by specific context shortcut "${otherAS.config.id}".`);
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}));
|
|
237
|
+
observables.push(stream);
|
|
238
|
+
}
|
|
239
|
+
if (observables.length === 0)
|
|
240
|
+
return undefined;
|
|
241
|
+
const finalShortcut$ = merge(...observables);
|
|
242
|
+
const overallLogDetails = `Triggers: [ ${logParts.join(", ")} ]`;
|
|
243
|
+
const subscription = finalShortcut$.pipe(tap(event => {
|
|
244
|
+
if (this.debugMode)
|
|
245
|
+
console.log(`${Hotkeys.LOG_PREFIX} Combination "${id}" triggered by key "${event.key}".`);
|
|
246
|
+
if (preventDefault)
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
}), catchError(err => {
|
|
249
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in stream for combination "${id}":`, err);
|
|
250
|
+
return EMPTY;
|
|
251
|
+
})).subscribe(event => {
|
|
252
|
+
try {
|
|
253
|
+
callback(event);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in callback for combination "${id}":`, e);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Combination, overallLogDetails);
|
|
260
|
+
}
|
|
261
|
+
addSequence(config) {
|
|
262
|
+
const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs, strict = false } = config;
|
|
263
|
+
if (!Array.isArray(sequence) || sequence.length === 0) {
|
|
264
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty. Not added.`);
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
if (sequence.some(key => typeof key !== "string" || key === "")) {
|
|
268
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Invalid key in sequence for shortcut "${id}". Not added.`);
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
if (context != null && strict) {
|
|
272
|
+
console.warn(`${Hotkeys.LOG_PREFIX} In sequence shortcut "${id}", the "strict" flag is redundant because it has a top-level context "${context}". Flag will be ignored.`);
|
|
273
|
+
}
|
|
274
|
+
const sequenceLength = sequence.length;
|
|
275
|
+
const baseKeydownStream$ = (context == null && strict)
|
|
276
|
+
? this.keydown$.pipe(// Strictly global
|
|
277
|
+
withLatestFrom(this.activeContext$), filter(([, activeCtx]) => activeCtx === null), map(([event]) => event))
|
|
278
|
+
: this.keydown$.pipe(// Default global or context-specific
|
|
279
|
+
withLatestFrom(this.activeContext$), filter(([, activeCtx]) => context == null || context === activeCtx), map(([event]) => event));
|
|
280
|
+
let shortcut$;
|
|
281
|
+
if (sequenceTimeoutMs && sequenceTimeoutMs > 0) {
|
|
282
|
+
shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
|
|
283
|
+
let { matchedEvents, lastEventTime } = acc;
|
|
284
|
+
const currentTime = performance.now();
|
|
285
|
+
if (acc.emitState === EmitStates.Emit)
|
|
286
|
+
matchedEvents = [];
|
|
287
|
+
if (matchedEvents.length > 0 && (currentTime - lastEventTime > sequenceTimeoutMs)) {
|
|
288
|
+
matchedEvents = [];
|
|
289
|
+
}
|
|
290
|
+
if (compareKey(event.key, sequence[matchedEvents.length])) {
|
|
291
|
+
const newMatched = [...matchedEvents, event];
|
|
292
|
+
if (newMatched.length === sequenceLength) {
|
|
293
|
+
return { matchedEvents: newMatched, lastEventTime: currentTime, emitState: EmitStates.Emit };
|
|
294
|
+
}
|
|
295
|
+
return { matchedEvents: newMatched, lastEventTime: currentTime, emitState: EmitStates.InProgress };
|
|
296
|
+
}
|
|
297
|
+
const newSequenceStartsWithKey = compareKey(event.key, sequence[0]);
|
|
298
|
+
if (matchedEvents.length > 0 && this.debugMode && !newSequenceStartsWithKey) {
|
|
299
|
+
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" broken by key "${event.key}". Resetting.`);
|
|
300
|
+
}
|
|
301
|
+
return { matchedEvents: newSequenceStartsWithKey ? [event] : [], lastEventTime: currentTime, emitState: EmitStates.InProgress };
|
|
302
|
+
}, { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore }), filter(state => state.emitState === EmitStates.Emit), map(state => state.matchedEvents));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
shortcut$ = baseKeydownStream$.pipe(bufferCount(sequenceLength, 1), filter(events => events.every((e, i) => compareKey(e.key, sequence[i]))));
|
|
306
|
+
}
|
|
307
|
+
const finalShortcutWithPriority$ = shortcut$.pipe(filter(() => {
|
|
308
|
+
if (context != null || strict)
|
|
309
|
+
return true;
|
|
310
|
+
const currentSpecificContext = this.activeContext$.getValue();
|
|
311
|
+
if (currentSpecificContext == null)
|
|
312
|
+
return true;
|
|
313
|
+
for (const [, otherAS] of this.activeShortcuts) {
|
|
314
|
+
if (otherAS.config.id !== config.id && "sequence" in otherAS.config &&
|
|
315
|
+
otherAS.config.context === currentSpecificContext && this._areSequencesIdentical(config.sequence, otherAS.config.sequence)) {
|
|
316
|
+
if (this.debugMode) {
|
|
317
|
+
console.log(`${Hotkeys.LOG_PREFIX} Global sequence "${config.id}" suppressed by specific context shortcut "${otherAS.config.id}".`);
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
}), tap(events => {
|
|
324
|
+
if (this.debugMode)
|
|
325
|
+
console.log(`${Hotkeys.LOG_PREFIX} Sequence "${id}" triggered.`);
|
|
326
|
+
if (preventDefault && events.length > 0)
|
|
327
|
+
events[events.length - 1].preventDefault();
|
|
328
|
+
}), catchError(err => {
|
|
329
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in stream for sequence "${id}":`, err);
|
|
330
|
+
return EMPTY;
|
|
331
|
+
}));
|
|
332
|
+
const subscription = finalShortcutWithPriority$.subscribe(events => {
|
|
333
|
+
try {
|
|
334
|
+
if (events.length > 0)
|
|
335
|
+
callback(events[events.length - 1]);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
console.error(`${Hotkeys.LOG_PREFIX} Error in callback for sequence "${id}":`, e);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const logDetails = `Sequence: ${sequence.join(" -> ")}`;
|
|
342
|
+
return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, logDetails);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Removes a registered shortcut by its ID.
|
|
346
|
+
*/
|
|
347
|
+
remove(id) {
|
|
348
|
+
const shortcut = this.activeShortcuts.get(id);
|
|
349
|
+
if (shortcut) {
|
|
350
|
+
shortcut.subscription.unsubscribe();
|
|
351
|
+
this.activeShortcuts.delete(id);
|
|
352
|
+
if (this.debugMode)
|
|
353
|
+
console.log(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" removed.`);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${id}" not found for removal.`);
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Retrieves a list of all currently active shortcuts.
|
|
361
|
+
*/
|
|
362
|
+
getActiveShortcuts() {
|
|
363
|
+
const shortcuts = [];
|
|
364
|
+
for (const [id, activeShortcut] of this.activeShortcuts.entries()) {
|
|
365
|
+
shortcuts.push({
|
|
366
|
+
id,
|
|
367
|
+
description: activeShortcut.config.description,
|
|
368
|
+
context: activeShortcut.config.context,
|
|
369
|
+
type: "sequence" in activeShortcut.config ? ShortcutTypes.Sequence : ShortcutTypes.Combination
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return shortcuts;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Cleans up all active subscriptions.
|
|
376
|
+
*/
|
|
377
|
+
destroy() {
|
|
378
|
+
if (this.debugMode)
|
|
379
|
+
console.log(`${Hotkeys.LOG_PREFIX} Destroying library instance.`);
|
|
380
|
+
this.activeShortcuts.forEach(shortcut => shortcut.subscription.unsubscribe());
|
|
381
|
+
this.activeShortcuts.clear();
|
|
382
|
+
this.activeContext$.complete();
|
|
383
|
+
}
|
|
384
|
+
}
|
package/dist/ttt.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
/**
|
|
15
|
+
* Defines a single key trigger, which can be a StandardKey (for simple presses like "Escape")
|
|
16
|
+
* or an object specifying the main key and its modifiers (e.g., { key: Keys.S, ctrlKey: true }).
|
|
17
|
+
*/
|
|
18
|
+
type KeyCombinationTrigger = {
|
|
19
|
+
/**
|
|
20
|
+
* The main key for the combination.
|
|
21
|
+
* This MUST be a value from the exported `Keys` object
|
|
22
|
+
* (e.g., `Keys.A`, `Keys.Enter`, `Keys.Escape`).
|
|
23
|
+
* The library handles case-insensitivity for single character keys (like A-Z, 0-9)
|
|
24
|
+
* automatically when comparing with the actual browser event's `event.key`.
|
|
25
|
+
* For special, multi-character keys (e.g. "ArrowUp", "Escape"), the value from
|
|
26
|
+
* `Keys` ensures the correct case-sensitive string is used.
|
|
27
|
+
* Refer to: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
28
|
+
*/
|
|
29
|
+
key: StandardKey;
|
|
30
|
+
ctrlKey?: boolean;
|
|
31
|
+
altKey?: boolean;
|
|
32
|
+
shiftKey?: boolean;
|
|
33
|
+
metaKey?: boolean;
|
|
34
|
+
} | StandardKey;
|
|
35
|
+
export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
36
|
+
/**
|
|
37
|
+
* Defines the key or key combination(s) that trigger the shortcut.
|
|
38
|
+
* Can be a single trigger or an array of triggers.
|
|
39
|
+
* Each trigger can be an object specifying the main `key` (from `StandardKey`) and optional
|
|
40
|
+
* modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
|
|
41
|
+
* Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
|
|
42
|
+
*
|
|
43
|
+
* Alternatively, for a simple key press without any modifiers, a trigger can be
|
|
44
|
+
* a `StandardKey` directly.
|
|
45
|
+
* Example: `Keys.Escape` for the Escape key. When using this shorthand,
|
|
46
|
+
* it implies that no modifier keys (Ctrl, Alt, Shift, Meta) should be active.
|
|
47
|
+
*
|
|
48
|
+
* To define multiple triggers for the same action:
|
|
49
|
+
* Example: `keys: [Keys.Enter, { key: Keys.Space, ctrlKey: true }]`
|
|
50
|
+
*/
|
|
51
|
+
keys: KeyCombinationTrigger | KeyCombinationTrigger[];
|
|
52
|
+
}
|
|
53
|
+
export interface KeySequenceConfig extends ShortcutConfigBase {
|
|
54
|
+
/**
|
|
55
|
+
* An array of keys that form the sequence.
|
|
56
|
+
* Each key in the sequence MUST be a value from the exported `Keys` object
|
|
57
|
+
* (e.g., `Keys.ArrowUp`, `Keys.G`, `Keys.Digit1`).
|
|
58
|
+
* The library handles case-insensitivity for single character keys automatically
|
|
59
|
+
* when comparing with the actual browser event's `event.key`.
|
|
60
|
+
* Refer to: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
|
61
|
+
* Example: [Keys.Control, Keys.Alt, Keys.Delete] or [Keys.G, Keys.I]
|
|
62
|
+
*/
|
|
63
|
+
sequence: StandardKey[];
|
|
64
|
+
/**
|
|
65
|
+
* Optional: Timeout in milliseconds between consecutive key presses in the sequence.
|
|
66
|
+
* If the time between two keys in the sequence exceeds this value, the sequence attempt is reset.
|
|
67
|
+
* Set to 0 or undefined to disable inter-key timeout behavior (uses simpler buffer-based matching).
|
|
68
|
+
*/
|
|
69
|
+
sequenceTimeoutMs?: number;
|
|
70
|
+
}
|
|
71
|
+
type ShortcutConfig = KeyCombinationConfig | KeySequenceConfig;
|
|
72
|
+
export interface ActiveShortcut {
|
|
73
|
+
id: string;
|
|
74
|
+
config: ShortcutConfig;
|
|
75
|
+
subscription: Subscription;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Manages keyboard shortcuts for web applications.
|
|
79
|
+
* Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
|
|
80
|
+
* Supports contexts to enable/disable shortcuts based on application state.
|
|
81
|
+
*/
|
|
82
|
+
export declare class Hotkeys {
|
|
83
|
+
private static readonly KEYDOWN_EVENT;
|
|
84
|
+
private static readonly LOG_PREFIX;
|
|
85
|
+
private keydown$;
|
|
86
|
+
private activeContext$;
|
|
87
|
+
private activeShortcuts;
|
|
88
|
+
private debugMode;
|
|
89
|
+
/**
|
|
90
|
+
* Creates an instance of Hotkeys.
|
|
91
|
+
* @param initialContext - Optional initial context name. Shortcuts will only trigger if their context matches this, or if they have no context defined.
|
|
92
|
+
* @param debugMode - Optional. If true, debug messages will be logged to the console. Defaults to false.
|
|
93
|
+
* @throws Error if not in a browser environment (i.e., `document` or `performance` is undefined).
|
|
94
|
+
*/
|
|
95
|
+
constructor(initialContext?: string | null, debugMode?: boolean);
|
|
96
|
+
/**
|
|
97
|
+
* Sets the active context for shortcuts.
|
|
98
|
+
* Only shortcuts matching this context (or shortcuts with no specific context defined)
|
|
99
|
+
* will be active and can be triggered.
|
|
100
|
+
* @param contextName - The name of the context (e.g., "modal", "editor", "global").
|
|
101
|
+
* Pass `null` to activate shortcuts with no context or to deactivate context-specific shortcuts.
|
|
102
|
+
* @returns `true` if the context was changed, `false` if the new context was the same as the current one.
|
|
103
|
+
*/
|
|
104
|
+
setContext(contextName: string | null): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Gets the current active context.
|
|
107
|
+
* @returns The current context name as a string, or `null` if no context is set.
|
|
108
|
+
*/
|
|
109
|
+
getContext(): string | null;
|
|
110
|
+
/**
|
|
111
|
+
* Enables or disables debug logging for the Hotkeys instance.
|
|
112
|
+
* When enabled, various internal actions and shortcut triggers will be logged to the console.
|
|
113
|
+
* @param enable - True to enable debug logs, false to disable.
|
|
114
|
+
*/
|
|
115
|
+
setDebugMode(enable: boolean): void;
|
|
116
|
+
/**
|
|
117
|
+
* Checks if a shortcut with the given ID is currently registered and active.
|
|
118
|
+
* @param id - The unique ID of the shortcut to check.
|
|
119
|
+
* @returns True if a shortcut with the specified ID exists, false otherwise.
|
|
120
|
+
*/
|
|
121
|
+
hasShortcut(id: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* An Observable that emits the new context name (or null) whenever the active context changes.
|
|
124
|
+
*/
|
|
125
|
+
get onContextChange$(): Observable<string | null>;
|
|
126
|
+
private _areSequencesIdentical;
|
|
127
|
+
private _shortcutMatchesEvent;
|
|
128
|
+
private filterByContext;
|
|
129
|
+
private _registerShortcut;
|
|
130
|
+
private _parseKeyTrigger;
|
|
131
|
+
/**
|
|
132
|
+
* Registers a key combination shortcut.
|
|
133
|
+
* @param config - Configuration object for the key combination.
|
|
134
|
+
* @param strict - If `true` and the shortcut has no context, it will only fire when no context is active. Defaults to `false`.
|
|
135
|
+
* @returns The ID of the registered shortcut, or `undefined` if invalid.
|
|
136
|
+
*/
|
|
137
|
+
addCombination(config: KeyCombinationConfig, strict?: boolean): string | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Registers a key sequence shortcut.
|
|
140
|
+
* @param config - Configuration object for the key sequence.
|
|
141
|
+
* @param strict - If `true` and the shortcut has no context, it will only fire when no context is active. Defaults to `false`.
|
|
142
|
+
* @returns The ID of the registered shortcut, or `undefined` if invalid.
|
|
143
|
+
*/
|
|
144
|
+
addSequence(config: KeySequenceConfig, strict?: boolean): string | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* Removes a registered shortcut by its ID.
|
|
147
|
+
*/
|
|
148
|
+
remove(id: string): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Retrieves a list of all currently active (registered) shortcut configurations.
|
|
151
|
+
*/
|
|
152
|
+
getActiveShortcuts(): {
|
|
153
|
+
id: string;
|
|
154
|
+
description?: string;
|
|
155
|
+
context?: string | null;
|
|
156
|
+
type: ShortcutTypes;
|
|
157
|
+
}[];
|
|
158
|
+
/**
|
|
159
|
+
* Cleans up all active subscriptions and resources used by the Hotkeys instance.
|
|
160
|
+
*/
|
|
161
|
+
destroy(): void;
|
|
162
|
+
}
|
|
163
|
+
export {};
|
|
164
|
+
//# sourceMappingURL=ttt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ttt.d.ts","sourceRoot":"","sources":["../src/ttt.ts"],"names":[],"mappings":"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"}
|