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/dist/tttt.js ADDED
@@ -0,0 +1,301 @@
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
+ function compareKey(eventKey, configuredKey) {
15
+ if (configuredKey.length === 1 && eventKey.length === 1) {
16
+ return eventKey.toLowerCase() === configuredKey.toLowerCase();
17
+ }
18
+ return eventKey === configuredKey;
19
+ }
20
+ export class Hotkeys {
21
+ static KEYDOWN_EVENT = "keydown";
22
+ static LOG_PREFIX = "Hotkeys:";
23
+ keydown$;
24
+ activeContext$;
25
+ activeShortcuts;
26
+ debugMode;
27
+ constructor(initialContext = null, debugMode = false) {
28
+ this.debugMode = debugMode;
29
+ if (typeof document === "undefined" || typeof performance === "undefined") {
30
+ throw new Error(`${Hotkeys.LOG_PREFIX} Hotkeys can only be used in a browser environment.`);
31
+ }
32
+ this.keydown$ = fromEvent(document, Hotkeys.KEYDOWN_EVENT);
33
+ this.activeContext$ = new BehaviorSubject(initialContext);
34
+ this.activeShortcuts = new Map();
35
+ if (this.debugMode) {
36
+ console.log(`${Hotkeys.LOG_PREFIX} Library initialized. Initial context: "${initialContext}".`);
37
+ }
38
+ }
39
+ setContext(contextName) {
40
+ const currentContext = this.activeContext$.getValue();
41
+ if (currentContext === contextName) {
42
+ return false;
43
+ }
44
+ if (this.debugMode) {
45
+ console.log(`${Hotkeys.LOG_PREFIX} Context changed from "${currentContext}" to "${contextName}".`);
46
+ }
47
+ this.activeContext$.next(contextName);
48
+ return true;
49
+ }
50
+ getContext() {
51
+ return this.activeContext$.getValue();
52
+ }
53
+ setDebugMode(enable) {
54
+ if (this.debugMode === enable)
55
+ return;
56
+ this.debugMode = enable;
57
+ console.log(`${Hotkeys.LOG_PREFIX} Debug mode ${enable ? "enabled" : "disabled"}.`);
58
+ }
59
+ hasShortcut(id) {
60
+ return this.activeShortcuts.has(id);
61
+ }
62
+ get onContextChange$() {
63
+ return this.activeContext$.asObservable();
64
+ }
65
+ _areSequencesIdentical(seq1, seq2) {
66
+ if (seq1.length !== seq2.length)
67
+ return false;
68
+ return seq1.every((key, index) => key === seq2[index]);
69
+ }
70
+ _shortcutMatchesEvent(shortcutConfig, event) {
71
+ const keyTriggers = Array.isArray(shortcutConfig.keys) ? shortcutConfig.keys : [shortcutConfig.keys];
72
+ for (const keyInput of keyTriggers) {
73
+ const parsed = this._parseKeyTrigger(keyInput, shortcutConfig.id);
74
+ if (!parsed)
75
+ continue;
76
+ const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig } = parsed;
77
+ const keyMatch = compareKey(event.key, configuredMainKey);
78
+ if (!keyMatch)
79
+ continue;
80
+ const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
81
+ const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
82
+ const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
83
+ const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
84
+ if (ctrlMatch && altMatch && shiftMatch && metaMatch) {
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+ /**
91
+ * Pre-filters a stream of keyboard events based on context and strictness rules.
92
+ */
93
+ filterByContext(source$, context, strict = false) {
94
+ return source$.pipe(withLatestFrom(this.activeContext$), filter(([, activeCtx]) => {
95
+ // Case 1: Strictly global shortcut. Only allow if no context is active.
96
+ if (context == null && strict) {
97
+ return activeCtx === null;
98
+ }
99
+ // Case 2: Shortcut with a specific context. Only allow if context matches.
100
+ if (context != null) {
101
+ return activeCtx === context;
102
+ }
103
+ // Case 3: Default global shortcut (context: null, strict: false).
104
+ // Always allow the event to pass through to the priority filter.
105
+ return true;
106
+ }), map(([event]) => event));
107
+ }
108
+ _registerShortcut(config, subscription, type, detailsForLog) {
109
+ const existingShortcut = this.activeShortcuts.get(config.id);
110
+ if (existingShortcut) {
111
+ console.warn(`${Hotkeys.LOG_PREFIX} Shortcut with ID "${config.id}" already exists. It will be overwritten.`);
112
+ existingShortcut.subscription.unsubscribe();
113
+ }
114
+ this.activeShortcuts.set(config.id, { id: config.id, config, subscription });
115
+ if (this.debugMode) {
116
+ console.log(`${Hotkeys.LOG_PREFIX} ${type} shortcut "${config.id}" added. ${detailsForLog}, Context: ${config.context ?? "any"}`);
117
+ }
118
+ return config.id;
119
+ }
120
+ _parseKeyTrigger(keyInput, shortcutId) {
121
+ if (typeof keyInput === "string") {
122
+ if (!keyInput) {
123
+ console.warn(`${Hotkeys.LOG_PREFIX} Invalid key (shorthand) in shortcut "${shortcutId}".`);
124
+ return null;
125
+ }
126
+ return {
127
+ configuredMainKey: keyInput, ctrlKeyConfig: false, altKeyConfig: false,
128
+ shiftKeyConfig: false, metaKeyConfig: false,
129
+ logDetails: `key: "${keyInput}" (no mods)`,
130
+ };
131
+ }
132
+ else {
133
+ if (!keyInput.key) {
134
+ console.warn(`${Hotkeys.LOG_PREFIX} Invalid "key" property in shortcut "${shortcutId}".`);
135
+ return null;
136
+ }
137
+ const logDetails = `key: "${keyInput.key}"` +
138
+ (keyInput.ctrlKey !== undefined ? `, ctrl: ${keyInput.ctrlKey}` : "") +
139
+ (keyInput.altKey !== undefined ? `, alt: ${keyInput.altKey}` : "") +
140
+ (keyInput.shiftKey !== undefined ? `, shift: ${keyInput.shiftKey}` : "") +
141
+ (keyInput.metaKey !== undefined ? `, meta: ${keyInput.metaKey}` : "");
142
+ return {
143
+ configuredMainKey: keyInput.key, ctrlKeyConfig: keyInput.ctrlKey, altKeyConfig: keyInput.altKey,
144
+ shiftKeyConfig: keyInput.shiftKey, metaKeyConfig: keyInput.metaKey,
145
+ logDetails,
146
+ };
147
+ }
148
+ }
149
+ addCombination(config, strict = false) {
150
+ const { keys, callback, context, preventDefault = false, id } = config;
151
+ if (context && strict) {
152
+ console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context and the 'strict' flag. The flag will be ignored.`);
153
+ }
154
+ const keyTriggers = Array.isArray(keys) ? keys : [keys];
155
+ if (keyTriggers.length === 0) {
156
+ console.warn(`${Hotkeys.LOG_PREFIX} "keys" array for shortcut "${id}" is empty.`);
157
+ return undefined;
158
+ }
159
+ // The base stream is now created with our centralized helper
160
+ const baseStream$ = this.filterByContext(this.keydown$, context, strict);
161
+ const observables = [];
162
+ const logParts = [];
163
+ for (const keyInput of keyTriggers) {
164
+ const parsedTrigger = this._parseKeyTrigger(keyInput, id);
165
+ if (!parsedTrigger)
166
+ return undefined;
167
+ const { configuredMainKey, ctrlKeyConfig, altKeyConfig, shiftKeyConfig, metaKeyConfig, logDetails } = parsedTrigger;
168
+ logParts.push(`{ ${logDetails} }`);
169
+ const stream = baseStream$.pipe(filter(event => {
170
+ const ctrlMatch = (ctrlKeyConfig === undefined) ? true : (event.ctrlKey === ctrlKeyConfig);
171
+ const altMatch = (altKeyConfig === undefined) ? true : (event.altKey === altKeyConfig);
172
+ const shiftMatch = (shiftKeyConfig === undefined) ? true : (event.shiftKey === shiftKeyConfig);
173
+ const metaMatch = (metaKeyConfig === undefined) ? true : (event.metaKey === metaKeyConfig);
174
+ return ctrlMatch && altMatch && shiftMatch && metaMatch && compareKey(event.key, configuredMainKey);
175
+ }), filter(event => {
176
+ if (context != null || strict)
177
+ return true;
178
+ const currentSpecificContext = this.activeContext$.getValue();
179
+ if (currentSpecificContext == null)
180
+ return true;
181
+ for (const [, otherAS] of this.activeShortcuts) {
182
+ if (otherAS.config.id !== config.id &&
183
+ 'keys' in otherAS.config &&
184
+ otherAS.config.context === currentSpecificContext &&
185
+ this._shortcutMatchesEvent(otherAS.config, event)) {
186
+ if (this.debugMode) {
187
+ console.log(`${Hotkeys.LOG_PREFIX} Global shortcut "${config.id}" suppressed by "${otherAS.config.id}".`);
188
+ }
189
+ return false;
190
+ }
191
+ }
192
+ return true;
193
+ }));
194
+ observables.push(stream);
195
+ }
196
+ if (observables.length === 0)
197
+ return undefined;
198
+ const finalShortcut$ = merge(...observables);
199
+ const overallLogDetails = `Triggers: [ ${logParts.join(", ")} ]`;
200
+ const subscription = finalShortcut$.pipe(tap(event => {
201
+ if (preventDefault)
202
+ event.preventDefault();
203
+ }), catchError(err => {
204
+ console.error(`${Hotkeys.LOG_PREFIX} Error in stream for shortcut "${id}":`, err);
205
+ return EMPTY;
206
+ })).subscribe(event => {
207
+ try {
208
+ callback(event);
209
+ }
210
+ catch (e) {
211
+ console.error(`${Hotkeys.LOG_PREFIX} Error in callback for shortcut "${id}":`, e);
212
+ }
213
+ });
214
+ return this._registerShortcut(config, subscription, ShortcutTypes.Combination, overallLogDetails);
215
+ }
216
+ addSequence(config, strict = false) {
217
+ const { sequence, callback, context, preventDefault = false, id, sequenceTimeoutMs } = config;
218
+ if (!Array.isArray(sequence) || sequence.length === 0) {
219
+ console.warn(`${Hotkeys.LOG_PREFIX} Sequence for shortcut "${id}" is empty.`);
220
+ return undefined;
221
+ }
222
+ if (context && strict) {
223
+ console.warn(`${Hotkeys.LOG_PREFIX} Shortcut "${id}" has both a context and 'strict' flag. Flag ignored.`);
224
+ }
225
+ const baseKeydownStream$ = this.filterByContext(this.keydown$, context, strict);
226
+ let shortcut$;
227
+ if (sequenceTimeoutMs && sequenceTimeoutMs > 0) {
228
+ shortcut$ = baseKeydownStream$.pipe(scan((acc, event) => {
229
+ let { matchedEvents, lastEventTime } = acc;
230
+ const currentTime = performance.now();
231
+ if (acc.emitState === EmitStates.Emit)
232
+ matchedEvents = [];
233
+ if (matchedEvents.length > 0 && (currentTime - lastEventTime > sequenceTimeoutMs))
234
+ matchedEvents = [];
235
+ if (compareKey(event.key, sequence[matchedEvents.length])) {
236
+ const newMatched = [...matchedEvents, event];
237
+ if (newMatched.length === sequence.length)
238
+ return { matchedEvents: newMatched, lastEventTime: currentTime, emitState: EmitStates.Emit };
239
+ return { matchedEvents: newMatched, lastEventTime: currentTime, emitState: EmitStates.InProgress };
240
+ }
241
+ if (compareKey(event.key, sequence[0]))
242
+ return { matchedEvents: [event], lastEventTime: currentTime, emitState: EmitStates.InProgress };
243
+ return { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore };
244
+ }, { matchedEvents: [], lastEventTime: 0, emitState: EmitStates.Ignore }), filter(state => state.emitState === EmitStates.Emit), map(state => state.matchedEvents));
245
+ }
246
+ else {
247
+ shortcut$ = baseKeydownStream$.pipe(bufferCount(sequence.length, 1), filter((events) => events.every((event, index) => compareKey(event.key, sequence[index]))));
248
+ }
249
+ const finalShortcutWithPriority$ = shortcut$.pipe(filter(() => {
250
+ if (context != null || strict)
251
+ return true;
252
+ const currentSpecificContext = this.activeContext$.getValue();
253
+ if (currentSpecificContext == null)
254
+ return true;
255
+ for (const [, otherAS] of this.activeShortcuts) {
256
+ if (otherAS.config.id !== config.id &&
257
+ "sequence" in otherAS.config &&
258
+ otherAS.config.context === currentSpecificContext &&
259
+ this._areSequencesIdentical(config.sequence, otherAS.config.sequence)) {
260
+ return false;
261
+ }
262
+ }
263
+ return true;
264
+ }), tap(events => {
265
+ if (preventDefault && events.length > 0)
266
+ events[events.length - 1].preventDefault();
267
+ }));
268
+ const subscription = finalShortcutWithPriority$.subscribe((events) => {
269
+ try {
270
+ if (events.length > 0)
271
+ callback(events[events.length - 1]);
272
+ }
273
+ catch (e) {
274
+ console.error(`${Hotkeys.LOG_PREFIX} Error in callback for sequence shortcut "${id}":`, e);
275
+ }
276
+ });
277
+ return this._registerShortcut(config, subscription, ShortcutTypes.Sequence, `Sequence: ${sequence.join(" -> ")}`);
278
+ }
279
+ remove(id) {
280
+ const shortcut = this.activeShortcuts.get(id);
281
+ if (shortcut) {
282
+ shortcut.subscription.unsubscribe();
283
+ this.activeShortcuts.delete(id);
284
+ return true;
285
+ }
286
+ return false;
287
+ }
288
+ getActiveShortcuts() {
289
+ return Array.from(this.activeShortcuts.values()).map(activeShortcut => ({
290
+ id: activeShortcut.id,
291
+ description: activeShortcut.config.description,
292
+ context: activeShortcut.config.context,
293
+ type: "sequence" in activeShortcut.config ? ShortcutTypes.Sequence : ShortcutTypes.Combination
294
+ }));
295
+ }
296
+ destroy() {
297
+ this.activeShortcuts.forEach(shortcut => shortcut.subscription.unsubscribe());
298
+ this.activeShortcuts.clear();
299
+ this.activeContext$.complete();
300
+ }
301
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rx-hotkeys",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsc",