rx-hotkeys 2.6.0 → 3.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 +136 -98
- package/dist/hotkeys.d.ts +88 -37
- package/dist/hotkeys.d.ts.map +1 -1
- package/dist/hotkeys.js +228 -97
- package/dist/hotkeys.test.js +402 -238
- package/dist/keys.d.ts +6 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +21 -0
- package/dist/testutils.d.ts +7 -4
- package/dist/testutils.d.ts.map +1 -1
- package/dist/testutils.js +9 -6
- package/package.json +1 -1
- package/dist/tt.d.ts +0 -125
- package/dist/tt.d.ts.map +0 -1
- package/dist/tt.js +0 -384
- package/dist/ttt.d.ts +0 -164
- package/dist/ttt.d.ts.map +0 -1
- package/dist/ttt.js +0 -439
- package/dist/tttt.d.ts +0 -67
- package/dist/tttt.d.ts.map +0 -1
- package/dist/tttt.js +0 -301
package/dist/keys.d.ts
CHANGED
|
@@ -152,4 +152,10 @@ export declare const Keys: {
|
|
|
152
152
|
* This ensures type safety when configuring shortcuts.
|
|
153
153
|
*/
|
|
154
154
|
export type StandardKey = typeof Keys[keyof typeof Keys];
|
|
155
|
+
/**
|
|
156
|
+
* A map of common aliases for keys to their standard `StandardKey` value.
|
|
157
|
+
* Used internally for parsing string-based shortcut definitions.
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
export declare const KeyAliases: Record<string, StandardKey>;
|
|
155
161
|
//# sourceMappingURL=keys.d.ts.map
|
package/dist/keys.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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;AAEzD;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAelD,CAAC"}
|
package/dist/keys.js
CHANGED
|
@@ -115,3 +115,24 @@ export const Keys = {
|
|
|
115
115
|
Digit0: "0", Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4",
|
|
116
116
|
Digit5: "5", Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9",
|
|
117
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* A map of common aliases for keys to their standard `StandardKey` value.
|
|
120
|
+
* Used internally for parsing string-based shortcut definitions.
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
export const KeyAliases = {
|
|
124
|
+
'esc': Keys.Escape, 'escape': Keys.Escape,
|
|
125
|
+
'enter': Keys.Enter, 'return': Keys.Enter,
|
|
126
|
+
'tab': Keys.Tab,
|
|
127
|
+
'space': Keys.Space, 'spacebar': Keys.Space,
|
|
128
|
+
'backspace': Keys.Backspace,
|
|
129
|
+
'del': Keys.Delete, 'delete': Keys.Delete,
|
|
130
|
+
'ins': Keys.Insert, 'insert': Keys.Insert,
|
|
131
|
+
'up': Keys.ArrowUp, 'down': Keys.ArrowDown, 'left': Keys.ArrowLeft, 'right': Keys.ArrowRight,
|
|
132
|
+
'pageup': Keys.PageUp, 'pagedown': Keys.PageDown,
|
|
133
|
+
'cmd': Keys.Meta, 'command': Keys.Meta, 'win': Keys.Meta, 'windows': Keys.Meta,
|
|
134
|
+
'ctrl': Keys.Control, 'control': Keys.Control,
|
|
135
|
+
'alt': Keys.Alt, 'option': Keys.Alt,
|
|
136
|
+
'shift': Keys.Shift,
|
|
137
|
+
// Add any other desired aliases here
|
|
138
|
+
};
|
package/dist/testutils.d.ts
CHANGED
|
@@ -6,9 +6,12 @@ export declare function createMockFn(): {
|
|
|
6
6
|
mockClear(): void;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
* Dispatches a KeyboardEvent to
|
|
10
|
-
* @param
|
|
11
|
-
* @param
|
|
9
|
+
* Dispatches a KeyboardEvent to a specified target element (or document).
|
|
10
|
+
* @param target - The EventTarget to dispatch the event on (e.g., document or an HTMLElement).
|
|
11
|
+
* @param key - The key value, e.g., "a", "Escape", "ArrowUp".
|
|
12
|
+
* @param eventType - The type of event to dispatch, 'keydown' or 'keyup'.
|
|
13
|
+
* @param modifiers - Optional modifier keys for the event.
|
|
14
|
+
* @returns The dispatched KeyboardEvent.
|
|
12
15
|
*/
|
|
13
|
-
export declare function dispatchKeyEvent(key: string, modifiers?: Partial<KeyboardEventInit>): KeyboardEvent;
|
|
16
|
+
export declare function dispatchKeyEvent(target: EventTarget, key: string, eventType?: 'keydown' | 'keyup', modifiers?: Partial<KeyboardEventInit>): KeyboardEvent;
|
|
14
17
|
//# sourceMappingURL=testutils.d.ts.map
|
package/dist/testutils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testutils.d.ts","sourceRoot":"","sources":["../src/testutils.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY;cACH,GAAG,EAAE;;;;;EAc7B;AAED
|
|
1
|
+
{"version":3,"file":"testutils.d.ts","sourceRoot":"","sources":["../src/testutils.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY;cACH,GAAG,EAAE;;;;;EAc7B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC5B,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,SAAS,GAAG,OAAmB,EAC1C,SAAS,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAC3C,aAAa,CASf"}
|
package/dist/testutils.js
CHANGED
|
@@ -15,17 +15,20 @@ export function createMockFn() {
|
|
|
15
15
|
return fn;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Dispatches a KeyboardEvent to
|
|
19
|
-
* @param
|
|
20
|
-
* @param
|
|
18
|
+
* Dispatches a KeyboardEvent to a specified target element (or document).
|
|
19
|
+
* @param target - The EventTarget to dispatch the event on (e.g., document or an HTMLElement).
|
|
20
|
+
* @param key - The key value, e.g., "a", "Escape", "ArrowUp".
|
|
21
|
+
* @param eventType - The type of event to dispatch, 'keydown' or 'keyup'.
|
|
22
|
+
* @param modifiers - Optional modifier keys for the event.
|
|
23
|
+
* @returns The dispatched KeyboardEvent.
|
|
21
24
|
*/
|
|
22
|
-
export function dispatchKeyEvent(key, modifiers = {}) {
|
|
23
|
-
const event = new KeyboardEvent(
|
|
25
|
+
export function dispatchKeyEvent(target, key, eventType = 'keydown', modifiers = {}) {
|
|
26
|
+
const event = new KeyboardEvent(eventType, {
|
|
24
27
|
key,
|
|
25
28
|
bubbles: true,
|
|
26
29
|
cancelable: true,
|
|
27
30
|
...modifiers,
|
|
28
31
|
});
|
|
29
|
-
|
|
32
|
+
target.dispatchEvent(event);
|
|
30
33
|
return event;
|
|
31
34
|
}
|
package/package.json
CHANGED
package/dist/tt.d.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
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
|
-
* **Only applicable if the shortcut has no top-level `context` defined.**
|
|
15
|
-
* If `true`, this shortcut is **strictly global** and will only fire when the active
|
|
16
|
-
* hotkey context is `null`.
|
|
17
|
-
* If `false` or `undefined` (the default), the shortcut can fire in *any* context,
|
|
18
|
-
* but will be suppressed by an identical shortcut that belongs to the active context.
|
|
19
|
-
* @default false
|
|
20
|
-
*/
|
|
21
|
-
strict?: boolean;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Defines a single key trigger, which can be a StandardKey (for simple presses like "Escape")
|
|
25
|
-
* or an object specifying the main key and its modifiers (e.g., { key: Keys.S, ctrlKey: true }).
|
|
26
|
-
*/
|
|
27
|
-
type KeyCombinationTrigger = {
|
|
28
|
-
/**
|
|
29
|
-
* The main key for the combination.
|
|
30
|
-
* This MUST be a value from the exported `Keys` object
|
|
31
|
-
* (e.g., `Keys.A`, `Keys.Enter`, `Keys.Escape`).
|
|
32
|
-
*/
|
|
33
|
-
key: StandardKey;
|
|
34
|
-
ctrlKey?: boolean;
|
|
35
|
-
altKey?: boolean;
|
|
36
|
-
shiftKey?: boolean;
|
|
37
|
-
metaKey?: boolean;
|
|
38
|
-
} | StandardKey;
|
|
39
|
-
export interface KeyCombinationConfig extends ShortcutConfigBase {
|
|
40
|
-
/**
|
|
41
|
-
* Defines the key or key combination(s) that trigger the shortcut.
|
|
42
|
-
*/
|
|
43
|
-
keys: KeyCombinationTrigger | KeyCombinationTrigger[];
|
|
44
|
-
}
|
|
45
|
-
export interface KeySequenceConfig extends ShortcutConfigBase {
|
|
46
|
-
/**
|
|
47
|
-
* An array of keys that form the sequence.
|
|
48
|
-
*/
|
|
49
|
-
sequence: StandardKey[];
|
|
50
|
-
/**
|
|
51
|
-
* Optional: Timeout in milliseconds between consecutive key presses in the sequence.
|
|
52
|
-
* @default undefined
|
|
53
|
-
*/
|
|
54
|
-
sequenceTimeoutMs?: number;
|
|
55
|
-
}
|
|
56
|
-
type ShortcutConfig = KeyCombinationConfig | KeySequenceConfig;
|
|
57
|
-
export interface ActiveShortcut {
|
|
58
|
-
id: string;
|
|
59
|
-
config: ShortcutConfig;
|
|
60
|
-
subscription: Subscription;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Manages keyboard shortcuts for web applications.
|
|
64
|
-
*/
|
|
65
|
-
export declare class Hotkeys {
|
|
66
|
-
private static readonly KEYDOWN_EVENT;
|
|
67
|
-
private static readonly LOG_PREFIX;
|
|
68
|
-
private keydown$;
|
|
69
|
-
private activeContext$;
|
|
70
|
-
private activeShortcuts;
|
|
71
|
-
private debugMode;
|
|
72
|
-
/**
|
|
73
|
-
* Creates an instance of Hotkeys.
|
|
74
|
-
* @param initialContext - Optional initial context name.
|
|
75
|
-
* @param debugMode - Optional. If true, debug messages will be logged to the console.
|
|
76
|
-
*/
|
|
77
|
-
constructor(initialContext?: string | null, debugMode?: boolean);
|
|
78
|
-
/**
|
|
79
|
-
* Sets the active context for shortcuts.
|
|
80
|
-
* @param contextName - The name of the context (e.g., "editor"). Pass `null` to clear the context.
|
|
81
|
-
* @returns `true` if the context was changed, `false` otherwise.
|
|
82
|
-
*/
|
|
83
|
-
setContext(contextName: string | null): boolean;
|
|
84
|
-
/**
|
|
85
|
-
* Gets the current active context.
|
|
86
|
-
*/
|
|
87
|
-
getContext(): string | null;
|
|
88
|
-
/**
|
|
89
|
-
* Enables or disables debug logging.
|
|
90
|
-
*/
|
|
91
|
-
setDebugMode(enable: boolean): void;
|
|
92
|
-
/**
|
|
93
|
-
* Checks if a shortcut with the given ID is registered.
|
|
94
|
-
*/
|
|
95
|
-
hasShortcut(id: string): boolean;
|
|
96
|
-
/**
|
|
97
|
-
* An Observable that emits the new context name whenever it changes.
|
|
98
|
-
*/
|
|
99
|
-
get onContextChange$(): Observable<string | null>;
|
|
100
|
-
private _areSequencesIdentical;
|
|
101
|
-
private _shortcutMatchesEvent;
|
|
102
|
-
private _registerShortcut;
|
|
103
|
-
private _parseKeyTrigger;
|
|
104
|
-
addCombination(config: KeyCombinationConfig): string | undefined;
|
|
105
|
-
addSequence(config: KeySequenceConfig): string | undefined;
|
|
106
|
-
/**
|
|
107
|
-
* Removes a registered shortcut by its ID.
|
|
108
|
-
*/
|
|
109
|
-
remove(id: string): boolean;
|
|
110
|
-
/**
|
|
111
|
-
* Retrieves a list of all currently active shortcuts.
|
|
112
|
-
*/
|
|
113
|
-
getActiveShortcuts(): {
|
|
114
|
-
id: string;
|
|
115
|
-
description?: string;
|
|
116
|
-
context?: string | null;
|
|
117
|
-
type: ShortcutTypes;
|
|
118
|
-
}[];
|
|
119
|
-
/**
|
|
120
|
-
* Cleans up all active subscriptions.
|
|
121
|
-
*/
|
|
122
|
-
destroy(): void;
|
|
123
|
-
}
|
|
124
|
-
export {};
|
|
125
|
-
//# sourceMappingURL=tt.d.ts.map
|
package/dist/tt.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tt.d.ts","sourceRoot":"","sources":["../src/tt.ts"],"names":[],"mappings":"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"}
|
package/dist/tt.js
DELETED
|
@@ -1,384 +0,0 @@
|
|
|
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
|
-
}
|