@hotora/hotkeys 1.0.0 → 1.0.1
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 +203 -1
- package/dist/index.d.mts +213 -1
- package/dist/index.d.ts +213 -1
- package/dist/index.js +326 -0
- package/dist/index.mjs +299 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1 +1,203 @@
|
|
|
1
|
-
#
|
|
1
|
+
# HotKeys
|
|
2
|
+
|
|
3
|
+
Lightweight hotkey manager with support for key sequences, scoped handlers, and DOM-based context resolution.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Key combinations and sequences
|
|
8
|
+
- Scoped handlers (local + global)
|
|
9
|
+
- DOM element binding
|
|
10
|
+
- Visibility-aware (via `IntersectionObserver`)
|
|
11
|
+
- Smart active element resolution
|
|
12
|
+
- Scope propagation control (`stopPropagation`)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @hotora/hotkeys
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Basic Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { hotKeys, Keys } from "@hotora/hotkeys";
|
|
28
|
+
|
|
29
|
+
hotKeys.register([Keys.A], {
|
|
30
|
+
handler: () => {
|
|
31
|
+
console.log("Pressed A");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Key Combinations
|
|
39
|
+
|
|
40
|
+
You can register multi-step combination:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
hotKeys.register([Keys.ControlLeft, Keys.A], {
|
|
44
|
+
handler: () => {
|
|
45
|
+
console.log("Ctrl + A");
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The handler will only work after you press Ctrl and the A key together.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Key Sequences
|
|
55
|
+
|
|
56
|
+
You can register multi-step sequences:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
hotKeys.register([[Keys.ControlLeft], [Keys.A]], {
|
|
60
|
+
handler: () => {
|
|
61
|
+
console.log("Ctrl → A");
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The handler only works after pressing Ctrl and then the A key.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Scoped Hotkeys
|
|
71
|
+
|
|
72
|
+
Bind hotkeys to a specific DOM element and scope:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const element = document.getElementById("editor");
|
|
76
|
+
|
|
77
|
+
hotKeys.register(
|
|
78
|
+
[Keys.S],
|
|
79
|
+
{
|
|
80
|
+
handler: () => console.log("Save inside editor"),
|
|
81
|
+
},
|
|
82
|
+
element,
|
|
83
|
+
"editor",
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### How scopes work
|
|
88
|
+
|
|
89
|
+
- Scope is attached to a DOM element
|
|
90
|
+
- When a key is pressed, HotKeys:
|
|
91
|
+
1. Resolves active element
|
|
92
|
+
2. Builds scope chain (element → parents → `$global`)
|
|
93
|
+
3. Executes handlers in order
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Global Scope
|
|
98
|
+
|
|
99
|
+
All handlers fallback to `$global` if no scoped handler stops propagation.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { Keys } from "@hotora/hotkeys/dist";
|
|
103
|
+
|
|
104
|
+
hotKeys.register([Keys.Escape], {
|
|
105
|
+
handler: () => console.log("Global escape"),
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## stopPropagation
|
|
112
|
+
|
|
113
|
+
Prevent execution of handlers in parent scopes:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
hotKeys.register([Keys.S], {
|
|
117
|
+
handler: (e) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
console.log("Handled locally only");
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Active Element Resolution
|
|
127
|
+
|
|
128
|
+
HotKeys determines active element using:
|
|
129
|
+
|
|
130
|
+
1. Last pointer interaction (click/touch)
|
|
131
|
+
2. Only visible elements are considered
|
|
132
|
+
3. If multiple visible elements:
|
|
133
|
+
|
|
134
|
+
- prefers last active
|
|
135
|
+
- otherwise chooses deepest in DOM
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## API
|
|
140
|
+
|
|
141
|
+
### `register(sequence, setup, element?, scope?)`
|
|
142
|
+
|
|
143
|
+
Registers a hotkey or sequence.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
register(
|
|
147
|
+
Combo | Sequence,
|
|
148
|
+
{
|
|
149
|
+
handler: (event) => void
|
|
150
|
+
clearDuration?: number
|
|
151
|
+
},
|
|
152
|
+
element: HTMLElement,
|
|
153
|
+
scope?: string
|
|
154
|
+
): ActionId
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### `unregister(id)`
|
|
160
|
+
|
|
161
|
+
Removes a previously registered hotkey.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Event
|
|
166
|
+
|
|
167
|
+
Handler receives:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
{
|
|
171
|
+
stopPropagation: ()=> void;
|
|
172
|
+
sequence: Sequence;
|
|
173
|
+
activeSteps: Set;
|
|
174
|
+
timestamp: number;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Example
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const modal = document.getElementById("modal");
|
|
184
|
+
|
|
185
|
+
hotKeys.register(
|
|
186
|
+
[Keys.Escape],
|
|
187
|
+
{
|
|
188
|
+
handler: () => console.log("Close modal"),
|
|
189
|
+
},
|
|
190
|
+
modal,
|
|
191
|
+
"modal",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
hotKeys.register([Keys.Escape], {
|
|
195
|
+
handler: () => console.log("Global escape fallback"),
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,214 @@
|
|
|
1
|
+
import { SequenceEvent, Stage, SequenceAction, ActionId } from '@hotora/core';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
declare enum Keys {
|
|
4
|
+
A = "KeyA",
|
|
5
|
+
B = "KeyB",
|
|
6
|
+
C = "KeyC",
|
|
7
|
+
D = "KeyD",
|
|
8
|
+
E = "KeyE",
|
|
9
|
+
F = "KeyF",
|
|
10
|
+
G = "KeyG",
|
|
11
|
+
H = "KeyH",
|
|
12
|
+
I = "KeyI",
|
|
13
|
+
J = "KeyJ",
|
|
14
|
+
K = "KeyK",
|
|
15
|
+
L = "KeyL",
|
|
16
|
+
M = "KeyM",
|
|
17
|
+
N = "KeyN",
|
|
18
|
+
O = "KeyO",
|
|
19
|
+
P = "KeyP",
|
|
20
|
+
Q = "KeyQ",
|
|
21
|
+
R = "KeyR",
|
|
22
|
+
S = "KeyS",
|
|
23
|
+
T = "KeyT",
|
|
24
|
+
U = "KeyU",
|
|
25
|
+
V = "KeyV",
|
|
26
|
+
W = "KeyW",
|
|
27
|
+
X = "KeyX",
|
|
28
|
+
Y = "KeyY",
|
|
29
|
+
Z = "KeyZ",
|
|
30
|
+
Digit0 = "Digit0",
|
|
31
|
+
Digit1 = "Digit1",
|
|
32
|
+
Digit2 = "Digit2",
|
|
33
|
+
Digit3 = "Digit3",
|
|
34
|
+
Digit4 = "Digit4",
|
|
35
|
+
Digit5 = "Digit5",
|
|
36
|
+
Digit6 = "Digit6",
|
|
37
|
+
Digit7 = "Digit7",
|
|
38
|
+
Digit8 = "Digit8",
|
|
39
|
+
Digit9 = "Digit9",
|
|
40
|
+
Numpad0 = "Numpad0",
|
|
41
|
+
Numpad1 = "Numpad1",
|
|
42
|
+
Numpad2 = "Numpad2",
|
|
43
|
+
Numpad3 = "Numpad3",
|
|
44
|
+
Numpad4 = "Numpad4",
|
|
45
|
+
Numpad5 = "Numpad5",
|
|
46
|
+
Numpad6 = "Numpad6",
|
|
47
|
+
Numpad7 = "Numpad7",
|
|
48
|
+
Numpad8 = "Numpad8",
|
|
49
|
+
Numpad9 = "Numpad9",
|
|
50
|
+
NumpadAdd = "NumpadAdd",
|
|
51
|
+
NumpadSubtract = "NumpadSubtract",
|
|
52
|
+
NumpadMultiply = "NumpadMultiply",
|
|
53
|
+
NumpadDivide = "NumpadDivide",
|
|
54
|
+
NumpadDecimal = "NumpadDecimal",
|
|
55
|
+
NumpadEnter = "NumpadEnter",
|
|
56
|
+
F1 = "F1",
|
|
57
|
+
F2 = "F2",
|
|
58
|
+
F3 = "F3",
|
|
59
|
+
F4 = "F4",
|
|
60
|
+
F5 = "F5",
|
|
61
|
+
F6 = "F6",
|
|
62
|
+
F7 = "F7",
|
|
63
|
+
F8 = "F8",
|
|
64
|
+
F9 = "F9",
|
|
65
|
+
F10 = "F10",
|
|
66
|
+
F11 = "F11",
|
|
67
|
+
F12 = "F12",
|
|
68
|
+
F13 = "F13",
|
|
69
|
+
F14 = "F14",
|
|
70
|
+
F15 = "F15",
|
|
71
|
+
F16 = "F16",
|
|
72
|
+
F17 = "F17",
|
|
73
|
+
F18 = "F18",
|
|
74
|
+
F19 = "F19",
|
|
75
|
+
F20 = "F20",
|
|
76
|
+
Escape = "Escape",
|
|
77
|
+
Tab = "Tab",
|
|
78
|
+
CapsLock = "CapsLock",
|
|
79
|
+
ShiftLeft = "ShiftLeft",
|
|
80
|
+
ShiftRight = "ShiftRight",
|
|
81
|
+
ControlLeft = "ControlLeft",
|
|
82
|
+
ControlRight = "ControlRight",
|
|
83
|
+
AltLeft = "AltLeft",
|
|
84
|
+
AltRight = "AltRight",
|
|
85
|
+
MetaLeft = "MetaLeft",
|
|
86
|
+
MetaRight = "MetaRight",
|
|
87
|
+
Space = "Space",
|
|
88
|
+
Enter = "Enter",
|
|
89
|
+
Backspace = "Backspace",
|
|
90
|
+
Delete = "Delete",
|
|
91
|
+
Insert = "Insert",
|
|
92
|
+
Home = "Home",
|
|
93
|
+
End = "End",
|
|
94
|
+
PageUp = "PageUp",
|
|
95
|
+
PageDown = "PageDown",
|
|
96
|
+
ArrowUp = "ArrowUp",
|
|
97
|
+
ArrowDown = "ArrowDown",
|
|
98
|
+
ArrowLeft = "ArrowLeft",
|
|
99
|
+
ArrowRight = "ArrowRight",
|
|
100
|
+
Minus = "Minus",
|
|
101
|
+
Equal = "Equal",
|
|
102
|
+
BracketLeft = "BracketLeft",
|
|
103
|
+
BracketRight = "BracketRight",
|
|
104
|
+
Backslash = "Backslash",
|
|
105
|
+
Semicolon = "Semicolon",
|
|
106
|
+
Quote = "Quote",
|
|
107
|
+
Backquote = "Backquote",
|
|
108
|
+
Comma = "Comma",
|
|
109
|
+
Period = "Period",
|
|
110
|
+
Slash = "Slash"
|
|
111
|
+
}
|
|
112
|
+
interface HotkeysEvent extends SequenceEvent<Keys> {
|
|
113
|
+
stopPropagation: () => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* HotKeys manager that supports:
|
|
118
|
+
* - key combinations and sequences (via SequenceController)
|
|
119
|
+
* - scoped handlers (scopes)
|
|
120
|
+
* - binding to DOM elements
|
|
121
|
+
*
|
|
122
|
+
* Features:
|
|
123
|
+
* - processes only visible elements (via IntersectionObserver)
|
|
124
|
+
* - resolves "active" element based on:
|
|
125
|
+
* - last pointer interaction (mouse/touch)
|
|
126
|
+
* - DOM depth (if multiple elements are visible)
|
|
127
|
+
* - builds scope chain (from element up to root + $global fallback)
|
|
128
|
+
* - allows stopping propagation between scopes
|
|
129
|
+
*/
|
|
130
|
+
declare class HotKeys {
|
|
131
|
+
/** Internal controller for key sequences */
|
|
132
|
+
private sequenceController;
|
|
133
|
+
/**
|
|
134
|
+
* Maps DOM elements to their scope
|
|
135
|
+
* WeakMap is used to avoid memory leaks
|
|
136
|
+
*/
|
|
137
|
+
private elements;
|
|
138
|
+
/** Set of currently visible elements (tracked by IntersectionObserver) */
|
|
139
|
+
private visibleElements;
|
|
140
|
+
/** Last active element determined by pointer interaction */
|
|
141
|
+
private activeElement;
|
|
142
|
+
/**
|
|
143
|
+
* Observer that tracks element visibility
|
|
144
|
+
* Adds/removes elements from visibleElements
|
|
145
|
+
*/
|
|
146
|
+
private observer;
|
|
147
|
+
/** Subscribes to global keyboard and pointer events */
|
|
148
|
+
constructor();
|
|
149
|
+
/**
|
|
150
|
+
* Registers a hotkey or key sequence
|
|
151
|
+
*
|
|
152
|
+
* @param sequence - key combination or sequence
|
|
153
|
+
* @param setup - handler configuration (without id and sequence)
|
|
154
|
+
* @param element - optional DOM element to bind scope to
|
|
155
|
+
* @param scope - optional scope name
|
|
156
|
+
*
|
|
157
|
+
* @returns action identifier
|
|
158
|
+
*/
|
|
159
|
+
register(sequence: Stage<Keys> | Stage<Keys>[], setup: Omit<SequenceAction<Keys, HotkeysEvent>, "id" | "sequence">, element?: HTMLElement, scope?: string): ActionId;
|
|
160
|
+
/**
|
|
161
|
+
* Unregisters a previously registered hotkey
|
|
162
|
+
*
|
|
163
|
+
* @param id - action identifier
|
|
164
|
+
*/
|
|
165
|
+
unregister(id: ActionId): void;
|
|
166
|
+
/**
|
|
167
|
+
* Pointer event handler
|
|
168
|
+
* Stores the closest registered element as active
|
|
169
|
+
*/
|
|
170
|
+
private onPointer;
|
|
171
|
+
/**
|
|
172
|
+
* Key down handler
|
|
173
|
+
* - adds step to sequence
|
|
174
|
+
* - resolves active element
|
|
175
|
+
* - walks through scope chain
|
|
176
|
+
* - executes matched handlers
|
|
177
|
+
*/
|
|
178
|
+
private onKeyDown;
|
|
179
|
+
/**
|
|
180
|
+
* Key up handler
|
|
181
|
+
* Removes step from current sequence state
|
|
182
|
+
*/
|
|
183
|
+
private onKeyUp;
|
|
184
|
+
/**
|
|
185
|
+
* Resolves the currently active element
|
|
186
|
+
*
|
|
187
|
+
* Priority:
|
|
188
|
+
* 1. Only visible elements are considered
|
|
189
|
+
* 2. If one element → return it
|
|
190
|
+
* 3. If activeElement is still visible → return it
|
|
191
|
+
* 4. Otherwise → return the deepest element in DOM
|
|
192
|
+
*/
|
|
193
|
+
private resolveActiveElement;
|
|
194
|
+
/**
|
|
195
|
+
* Builds scope chain from element to root
|
|
196
|
+
* Always includes "$global" as fallback
|
|
197
|
+
*/
|
|
198
|
+
private getScopeChain;
|
|
199
|
+
/**
|
|
200
|
+
* Finds the closest ancestor element that has a registered scope
|
|
201
|
+
*/
|
|
202
|
+
private findClosestRegistered;
|
|
203
|
+
/**
|
|
204
|
+
* Calculates DOM depth of an element
|
|
205
|
+
*/
|
|
206
|
+
private getDepth;
|
|
207
|
+
/**
|
|
208
|
+
* Cleans up elements that are no longer in the DOM
|
|
209
|
+
*/
|
|
210
|
+
private cleanup;
|
|
211
|
+
}
|
|
212
|
+
declare const hotKeys: HotKeys;
|
|
213
|
+
|
|
214
|
+
export { type HotkeysEvent, Keys, hotKeys };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,214 @@
|
|
|
1
|
+
import { SequenceEvent, Stage, SequenceAction, ActionId } from '@hotora/core';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
declare enum Keys {
|
|
4
|
+
A = "KeyA",
|
|
5
|
+
B = "KeyB",
|
|
6
|
+
C = "KeyC",
|
|
7
|
+
D = "KeyD",
|
|
8
|
+
E = "KeyE",
|
|
9
|
+
F = "KeyF",
|
|
10
|
+
G = "KeyG",
|
|
11
|
+
H = "KeyH",
|
|
12
|
+
I = "KeyI",
|
|
13
|
+
J = "KeyJ",
|
|
14
|
+
K = "KeyK",
|
|
15
|
+
L = "KeyL",
|
|
16
|
+
M = "KeyM",
|
|
17
|
+
N = "KeyN",
|
|
18
|
+
O = "KeyO",
|
|
19
|
+
P = "KeyP",
|
|
20
|
+
Q = "KeyQ",
|
|
21
|
+
R = "KeyR",
|
|
22
|
+
S = "KeyS",
|
|
23
|
+
T = "KeyT",
|
|
24
|
+
U = "KeyU",
|
|
25
|
+
V = "KeyV",
|
|
26
|
+
W = "KeyW",
|
|
27
|
+
X = "KeyX",
|
|
28
|
+
Y = "KeyY",
|
|
29
|
+
Z = "KeyZ",
|
|
30
|
+
Digit0 = "Digit0",
|
|
31
|
+
Digit1 = "Digit1",
|
|
32
|
+
Digit2 = "Digit2",
|
|
33
|
+
Digit3 = "Digit3",
|
|
34
|
+
Digit4 = "Digit4",
|
|
35
|
+
Digit5 = "Digit5",
|
|
36
|
+
Digit6 = "Digit6",
|
|
37
|
+
Digit7 = "Digit7",
|
|
38
|
+
Digit8 = "Digit8",
|
|
39
|
+
Digit9 = "Digit9",
|
|
40
|
+
Numpad0 = "Numpad0",
|
|
41
|
+
Numpad1 = "Numpad1",
|
|
42
|
+
Numpad2 = "Numpad2",
|
|
43
|
+
Numpad3 = "Numpad3",
|
|
44
|
+
Numpad4 = "Numpad4",
|
|
45
|
+
Numpad5 = "Numpad5",
|
|
46
|
+
Numpad6 = "Numpad6",
|
|
47
|
+
Numpad7 = "Numpad7",
|
|
48
|
+
Numpad8 = "Numpad8",
|
|
49
|
+
Numpad9 = "Numpad9",
|
|
50
|
+
NumpadAdd = "NumpadAdd",
|
|
51
|
+
NumpadSubtract = "NumpadSubtract",
|
|
52
|
+
NumpadMultiply = "NumpadMultiply",
|
|
53
|
+
NumpadDivide = "NumpadDivide",
|
|
54
|
+
NumpadDecimal = "NumpadDecimal",
|
|
55
|
+
NumpadEnter = "NumpadEnter",
|
|
56
|
+
F1 = "F1",
|
|
57
|
+
F2 = "F2",
|
|
58
|
+
F3 = "F3",
|
|
59
|
+
F4 = "F4",
|
|
60
|
+
F5 = "F5",
|
|
61
|
+
F6 = "F6",
|
|
62
|
+
F7 = "F7",
|
|
63
|
+
F8 = "F8",
|
|
64
|
+
F9 = "F9",
|
|
65
|
+
F10 = "F10",
|
|
66
|
+
F11 = "F11",
|
|
67
|
+
F12 = "F12",
|
|
68
|
+
F13 = "F13",
|
|
69
|
+
F14 = "F14",
|
|
70
|
+
F15 = "F15",
|
|
71
|
+
F16 = "F16",
|
|
72
|
+
F17 = "F17",
|
|
73
|
+
F18 = "F18",
|
|
74
|
+
F19 = "F19",
|
|
75
|
+
F20 = "F20",
|
|
76
|
+
Escape = "Escape",
|
|
77
|
+
Tab = "Tab",
|
|
78
|
+
CapsLock = "CapsLock",
|
|
79
|
+
ShiftLeft = "ShiftLeft",
|
|
80
|
+
ShiftRight = "ShiftRight",
|
|
81
|
+
ControlLeft = "ControlLeft",
|
|
82
|
+
ControlRight = "ControlRight",
|
|
83
|
+
AltLeft = "AltLeft",
|
|
84
|
+
AltRight = "AltRight",
|
|
85
|
+
MetaLeft = "MetaLeft",
|
|
86
|
+
MetaRight = "MetaRight",
|
|
87
|
+
Space = "Space",
|
|
88
|
+
Enter = "Enter",
|
|
89
|
+
Backspace = "Backspace",
|
|
90
|
+
Delete = "Delete",
|
|
91
|
+
Insert = "Insert",
|
|
92
|
+
Home = "Home",
|
|
93
|
+
End = "End",
|
|
94
|
+
PageUp = "PageUp",
|
|
95
|
+
PageDown = "PageDown",
|
|
96
|
+
ArrowUp = "ArrowUp",
|
|
97
|
+
ArrowDown = "ArrowDown",
|
|
98
|
+
ArrowLeft = "ArrowLeft",
|
|
99
|
+
ArrowRight = "ArrowRight",
|
|
100
|
+
Minus = "Minus",
|
|
101
|
+
Equal = "Equal",
|
|
102
|
+
BracketLeft = "BracketLeft",
|
|
103
|
+
BracketRight = "BracketRight",
|
|
104
|
+
Backslash = "Backslash",
|
|
105
|
+
Semicolon = "Semicolon",
|
|
106
|
+
Quote = "Quote",
|
|
107
|
+
Backquote = "Backquote",
|
|
108
|
+
Comma = "Comma",
|
|
109
|
+
Period = "Period",
|
|
110
|
+
Slash = "Slash"
|
|
111
|
+
}
|
|
112
|
+
interface HotkeysEvent extends SequenceEvent<Keys> {
|
|
113
|
+
stopPropagation: () => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* HotKeys manager that supports:
|
|
118
|
+
* - key combinations and sequences (via SequenceController)
|
|
119
|
+
* - scoped handlers (scopes)
|
|
120
|
+
* - binding to DOM elements
|
|
121
|
+
*
|
|
122
|
+
* Features:
|
|
123
|
+
* - processes only visible elements (via IntersectionObserver)
|
|
124
|
+
* - resolves "active" element based on:
|
|
125
|
+
* - last pointer interaction (mouse/touch)
|
|
126
|
+
* - DOM depth (if multiple elements are visible)
|
|
127
|
+
* - builds scope chain (from element up to root + $global fallback)
|
|
128
|
+
* - allows stopping propagation between scopes
|
|
129
|
+
*/
|
|
130
|
+
declare class HotKeys {
|
|
131
|
+
/** Internal controller for key sequences */
|
|
132
|
+
private sequenceController;
|
|
133
|
+
/**
|
|
134
|
+
* Maps DOM elements to their scope
|
|
135
|
+
* WeakMap is used to avoid memory leaks
|
|
136
|
+
*/
|
|
137
|
+
private elements;
|
|
138
|
+
/** Set of currently visible elements (tracked by IntersectionObserver) */
|
|
139
|
+
private visibleElements;
|
|
140
|
+
/** Last active element determined by pointer interaction */
|
|
141
|
+
private activeElement;
|
|
142
|
+
/**
|
|
143
|
+
* Observer that tracks element visibility
|
|
144
|
+
* Adds/removes elements from visibleElements
|
|
145
|
+
*/
|
|
146
|
+
private observer;
|
|
147
|
+
/** Subscribes to global keyboard and pointer events */
|
|
148
|
+
constructor();
|
|
149
|
+
/**
|
|
150
|
+
* Registers a hotkey or key sequence
|
|
151
|
+
*
|
|
152
|
+
* @param sequence - key combination or sequence
|
|
153
|
+
* @param setup - handler configuration (without id and sequence)
|
|
154
|
+
* @param element - optional DOM element to bind scope to
|
|
155
|
+
* @param scope - optional scope name
|
|
156
|
+
*
|
|
157
|
+
* @returns action identifier
|
|
158
|
+
*/
|
|
159
|
+
register(sequence: Stage<Keys> | Stage<Keys>[], setup: Omit<SequenceAction<Keys, HotkeysEvent>, "id" | "sequence">, element?: HTMLElement, scope?: string): ActionId;
|
|
160
|
+
/**
|
|
161
|
+
* Unregisters a previously registered hotkey
|
|
162
|
+
*
|
|
163
|
+
* @param id - action identifier
|
|
164
|
+
*/
|
|
165
|
+
unregister(id: ActionId): void;
|
|
166
|
+
/**
|
|
167
|
+
* Pointer event handler
|
|
168
|
+
* Stores the closest registered element as active
|
|
169
|
+
*/
|
|
170
|
+
private onPointer;
|
|
171
|
+
/**
|
|
172
|
+
* Key down handler
|
|
173
|
+
* - adds step to sequence
|
|
174
|
+
* - resolves active element
|
|
175
|
+
* - walks through scope chain
|
|
176
|
+
* - executes matched handlers
|
|
177
|
+
*/
|
|
178
|
+
private onKeyDown;
|
|
179
|
+
/**
|
|
180
|
+
* Key up handler
|
|
181
|
+
* Removes step from current sequence state
|
|
182
|
+
*/
|
|
183
|
+
private onKeyUp;
|
|
184
|
+
/**
|
|
185
|
+
* Resolves the currently active element
|
|
186
|
+
*
|
|
187
|
+
* Priority:
|
|
188
|
+
* 1. Only visible elements are considered
|
|
189
|
+
* 2. If one element → return it
|
|
190
|
+
* 3. If activeElement is still visible → return it
|
|
191
|
+
* 4. Otherwise → return the deepest element in DOM
|
|
192
|
+
*/
|
|
193
|
+
private resolveActiveElement;
|
|
194
|
+
/**
|
|
195
|
+
* Builds scope chain from element to root
|
|
196
|
+
* Always includes "$global" as fallback
|
|
197
|
+
*/
|
|
198
|
+
private getScopeChain;
|
|
199
|
+
/**
|
|
200
|
+
* Finds the closest ancestor element that has a registered scope
|
|
201
|
+
*/
|
|
202
|
+
private findClosestRegistered;
|
|
203
|
+
/**
|
|
204
|
+
* Calculates DOM depth of an element
|
|
205
|
+
*/
|
|
206
|
+
private getDepth;
|
|
207
|
+
/**
|
|
208
|
+
* Cleans up elements that are no longer in the DOM
|
|
209
|
+
*/
|
|
210
|
+
private cleanup;
|
|
211
|
+
}
|
|
212
|
+
declare const hotKeys: HotKeys;
|
|
213
|
+
|
|
214
|
+
export { type HotkeysEvent, Keys, hotKeys };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,327 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Keys: () => Keys,
|
|
24
|
+
hotKeys: () => hotKeys
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/HotKeys.ts
|
|
29
|
+
var import_core = require("@hotora/core");
|
|
30
|
+
var HotKeys = class {
|
|
31
|
+
/** Internal controller for key sequences */
|
|
32
|
+
sequenceController = new import_core.SequenceController();
|
|
33
|
+
/**
|
|
34
|
+
* Maps DOM elements to their scope
|
|
35
|
+
* WeakMap is used to avoid memory leaks
|
|
36
|
+
*/
|
|
37
|
+
elements = /* @__PURE__ */ new WeakMap();
|
|
38
|
+
/** Set of currently visible elements (tracked by IntersectionObserver) */
|
|
39
|
+
visibleElements = /* @__PURE__ */ new Set();
|
|
40
|
+
/** Last active element determined by pointer interaction */
|
|
41
|
+
activeElement = null;
|
|
42
|
+
/**
|
|
43
|
+
* Observer that tracks element visibility
|
|
44
|
+
* Adds/removes elements from visibleElements
|
|
45
|
+
*/
|
|
46
|
+
observer = new IntersectionObserver((entries) => {
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const el = entry.target;
|
|
49
|
+
if (entry.isIntersecting) {
|
|
50
|
+
this.visibleElements.add(el);
|
|
51
|
+
} else {
|
|
52
|
+
this.visibleElements.delete(el);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.cleanup();
|
|
56
|
+
});
|
|
57
|
+
/** Subscribes to global keyboard and pointer events */
|
|
58
|
+
constructor() {
|
|
59
|
+
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
|
60
|
+
document.addEventListener("keyup", this.onKeyUp.bind(this));
|
|
61
|
+
document.addEventListener("mousedown", this.onPointer.bind(this));
|
|
62
|
+
document.addEventListener("touchstart", this.onPointer.bind(this));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Registers a hotkey or key sequence
|
|
66
|
+
*
|
|
67
|
+
* @param sequence - key combination or sequence
|
|
68
|
+
* @param setup - handler configuration (without id and sequence)
|
|
69
|
+
* @param element - optional DOM element to bind scope to
|
|
70
|
+
* @param scope - optional scope name
|
|
71
|
+
*
|
|
72
|
+
* @returns action identifier
|
|
73
|
+
*/
|
|
74
|
+
register(sequence, setup, element, scope) {
|
|
75
|
+
if (element && scope) {
|
|
76
|
+
this.elements.set(element, scope);
|
|
77
|
+
this.observer.observe(element);
|
|
78
|
+
}
|
|
79
|
+
return this.sequenceController.register(sequence, setup, scope);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Unregisters a previously registered hotkey
|
|
83
|
+
*
|
|
84
|
+
* @param id - action identifier
|
|
85
|
+
*/
|
|
86
|
+
unregister(id) {
|
|
87
|
+
this.sequenceController.unregister(id);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Pointer event handler
|
|
91
|
+
* Stores the closest registered element as active
|
|
92
|
+
*/
|
|
93
|
+
onPointer = (e) => {
|
|
94
|
+
const target = e.target;
|
|
95
|
+
const el = this.findClosestRegistered(target);
|
|
96
|
+
if (el) {
|
|
97
|
+
this.activeElement = el;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Key down handler
|
|
102
|
+
* - adds step to sequence
|
|
103
|
+
* - resolves active element
|
|
104
|
+
* - walks through scope chain
|
|
105
|
+
* - executes matched handlers
|
|
106
|
+
*/
|
|
107
|
+
onKeyDown = (event) => {
|
|
108
|
+
if (event.repeat) return;
|
|
109
|
+
const step = event.code;
|
|
110
|
+
const activeEl = this.resolveActiveElement();
|
|
111
|
+
const scopes = this.getScopeChain(activeEl);
|
|
112
|
+
this.sequenceController.addStep(step);
|
|
113
|
+
let stopPropagation = false;
|
|
114
|
+
for (const scope of scopes) {
|
|
115
|
+
if (stopPropagation) break;
|
|
116
|
+
const fired = this.sequenceController.process(scope);
|
|
117
|
+
fired.forEach(([evt, handler]) => {
|
|
118
|
+
handler({
|
|
119
|
+
stopPropagation: () => {
|
|
120
|
+
stopPropagation = true;
|
|
121
|
+
},
|
|
122
|
+
...evt
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Key up handler
|
|
129
|
+
* Removes step from current sequence state
|
|
130
|
+
*/
|
|
131
|
+
onKeyUp = (event) => {
|
|
132
|
+
const step = event.code;
|
|
133
|
+
this.sequenceController.removeStep(step);
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Resolves the currently active element
|
|
137
|
+
*
|
|
138
|
+
* Priority:
|
|
139
|
+
* 1. Only visible elements are considered
|
|
140
|
+
* 2. If one element → return it
|
|
141
|
+
* 3. If activeElement is still visible → return it
|
|
142
|
+
* 4. Otherwise → return the deepest element in DOM
|
|
143
|
+
*/
|
|
144
|
+
resolveActiveElement() {
|
|
145
|
+
const visible = [...this.visibleElements];
|
|
146
|
+
if (visible.length === 0) return null;
|
|
147
|
+
if (visible.length === 1) return visible[0] ?? null;
|
|
148
|
+
if (this.activeElement && this.visibleElements.has(this.activeElement)) {
|
|
149
|
+
return this.activeElement;
|
|
150
|
+
}
|
|
151
|
+
return visible.reduce((deepest, el) => {
|
|
152
|
+
return this.getDepth(el) > this.getDepth(deepest) ? el : deepest;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Builds scope chain from element to root
|
|
157
|
+
* Always includes "$global" as fallback
|
|
158
|
+
*/
|
|
159
|
+
getScopeChain(element) {
|
|
160
|
+
const chain = [];
|
|
161
|
+
let current = element;
|
|
162
|
+
while (current) {
|
|
163
|
+
const scope = this.elements.get(current);
|
|
164
|
+
if (scope) chain.push(scope);
|
|
165
|
+
current = current.parentElement;
|
|
166
|
+
}
|
|
167
|
+
if (!chain.includes("$global")) {
|
|
168
|
+
chain.push("$global");
|
|
169
|
+
}
|
|
170
|
+
return chain;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Finds the closest ancestor element that has a registered scope
|
|
174
|
+
*/
|
|
175
|
+
findClosestRegistered(el) {
|
|
176
|
+
let current = el;
|
|
177
|
+
while (current) {
|
|
178
|
+
if (this.elements.has(current)) return current;
|
|
179
|
+
current = current.parentElement;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Calculates DOM depth of an element
|
|
185
|
+
*/
|
|
186
|
+
getDepth(el) {
|
|
187
|
+
let depth = 0;
|
|
188
|
+
let current = el;
|
|
189
|
+
while (current) {
|
|
190
|
+
depth++;
|
|
191
|
+
current = current.parentElement;
|
|
192
|
+
}
|
|
193
|
+
return depth;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Cleans up elements that are no longer in the DOM
|
|
197
|
+
*/
|
|
198
|
+
cleanup() {
|
|
199
|
+
for (const el of [...this.visibleElements]) {
|
|
200
|
+
if (!el.isConnected) {
|
|
201
|
+
this.visibleElements.delete(el);
|
|
202
|
+
this.observer.unobserve(el);
|
|
203
|
+
if (this.activeElement === el) {
|
|
204
|
+
this.activeElement = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
var hotKeys = new HotKeys();
|
|
211
|
+
|
|
212
|
+
// src/types.ts
|
|
213
|
+
var Keys = /* @__PURE__ */ ((Keys2) => {
|
|
214
|
+
Keys2["A"] = "KeyA";
|
|
215
|
+
Keys2["B"] = "KeyB";
|
|
216
|
+
Keys2["C"] = "KeyC";
|
|
217
|
+
Keys2["D"] = "KeyD";
|
|
218
|
+
Keys2["E"] = "KeyE";
|
|
219
|
+
Keys2["F"] = "KeyF";
|
|
220
|
+
Keys2["G"] = "KeyG";
|
|
221
|
+
Keys2["H"] = "KeyH";
|
|
222
|
+
Keys2["I"] = "KeyI";
|
|
223
|
+
Keys2["J"] = "KeyJ";
|
|
224
|
+
Keys2["K"] = "KeyK";
|
|
225
|
+
Keys2["L"] = "KeyL";
|
|
226
|
+
Keys2["M"] = "KeyM";
|
|
227
|
+
Keys2["N"] = "KeyN";
|
|
228
|
+
Keys2["O"] = "KeyO";
|
|
229
|
+
Keys2["P"] = "KeyP";
|
|
230
|
+
Keys2["Q"] = "KeyQ";
|
|
231
|
+
Keys2["R"] = "KeyR";
|
|
232
|
+
Keys2["S"] = "KeyS";
|
|
233
|
+
Keys2["T"] = "KeyT";
|
|
234
|
+
Keys2["U"] = "KeyU";
|
|
235
|
+
Keys2["V"] = "KeyV";
|
|
236
|
+
Keys2["W"] = "KeyW";
|
|
237
|
+
Keys2["X"] = "KeyX";
|
|
238
|
+
Keys2["Y"] = "KeyY";
|
|
239
|
+
Keys2["Z"] = "KeyZ";
|
|
240
|
+
Keys2["Digit0"] = "Digit0";
|
|
241
|
+
Keys2["Digit1"] = "Digit1";
|
|
242
|
+
Keys2["Digit2"] = "Digit2";
|
|
243
|
+
Keys2["Digit3"] = "Digit3";
|
|
244
|
+
Keys2["Digit4"] = "Digit4";
|
|
245
|
+
Keys2["Digit5"] = "Digit5";
|
|
246
|
+
Keys2["Digit6"] = "Digit6";
|
|
247
|
+
Keys2["Digit7"] = "Digit7";
|
|
248
|
+
Keys2["Digit8"] = "Digit8";
|
|
249
|
+
Keys2["Digit9"] = "Digit9";
|
|
250
|
+
Keys2["Numpad0"] = "Numpad0";
|
|
251
|
+
Keys2["Numpad1"] = "Numpad1";
|
|
252
|
+
Keys2["Numpad2"] = "Numpad2";
|
|
253
|
+
Keys2["Numpad3"] = "Numpad3";
|
|
254
|
+
Keys2["Numpad4"] = "Numpad4";
|
|
255
|
+
Keys2["Numpad5"] = "Numpad5";
|
|
256
|
+
Keys2["Numpad6"] = "Numpad6";
|
|
257
|
+
Keys2["Numpad7"] = "Numpad7";
|
|
258
|
+
Keys2["Numpad8"] = "Numpad8";
|
|
259
|
+
Keys2["Numpad9"] = "Numpad9";
|
|
260
|
+
Keys2["NumpadAdd"] = "NumpadAdd";
|
|
261
|
+
Keys2["NumpadSubtract"] = "NumpadSubtract";
|
|
262
|
+
Keys2["NumpadMultiply"] = "NumpadMultiply";
|
|
263
|
+
Keys2["NumpadDivide"] = "NumpadDivide";
|
|
264
|
+
Keys2["NumpadDecimal"] = "NumpadDecimal";
|
|
265
|
+
Keys2["NumpadEnter"] = "NumpadEnter";
|
|
266
|
+
Keys2["F1"] = "F1";
|
|
267
|
+
Keys2["F2"] = "F2";
|
|
268
|
+
Keys2["F3"] = "F3";
|
|
269
|
+
Keys2["F4"] = "F4";
|
|
270
|
+
Keys2["F5"] = "F5";
|
|
271
|
+
Keys2["F6"] = "F6";
|
|
272
|
+
Keys2["F7"] = "F7";
|
|
273
|
+
Keys2["F8"] = "F8";
|
|
274
|
+
Keys2["F9"] = "F9";
|
|
275
|
+
Keys2["F10"] = "F10";
|
|
276
|
+
Keys2["F11"] = "F11";
|
|
277
|
+
Keys2["F12"] = "F12";
|
|
278
|
+
Keys2["F13"] = "F13";
|
|
279
|
+
Keys2["F14"] = "F14";
|
|
280
|
+
Keys2["F15"] = "F15";
|
|
281
|
+
Keys2["F16"] = "F16";
|
|
282
|
+
Keys2["F17"] = "F17";
|
|
283
|
+
Keys2["F18"] = "F18";
|
|
284
|
+
Keys2["F19"] = "F19";
|
|
285
|
+
Keys2["F20"] = "F20";
|
|
286
|
+
Keys2["Escape"] = "Escape";
|
|
287
|
+
Keys2["Tab"] = "Tab";
|
|
288
|
+
Keys2["CapsLock"] = "CapsLock";
|
|
289
|
+
Keys2["ShiftLeft"] = "ShiftLeft";
|
|
290
|
+
Keys2["ShiftRight"] = "ShiftRight";
|
|
291
|
+
Keys2["ControlLeft"] = "ControlLeft";
|
|
292
|
+
Keys2["ControlRight"] = "ControlRight";
|
|
293
|
+
Keys2["AltLeft"] = "AltLeft";
|
|
294
|
+
Keys2["AltRight"] = "AltRight";
|
|
295
|
+
Keys2["MetaLeft"] = "MetaLeft";
|
|
296
|
+
Keys2["MetaRight"] = "MetaRight";
|
|
297
|
+
Keys2["Space"] = "Space";
|
|
298
|
+
Keys2["Enter"] = "Enter";
|
|
299
|
+
Keys2["Backspace"] = "Backspace";
|
|
300
|
+
Keys2["Delete"] = "Delete";
|
|
301
|
+
Keys2["Insert"] = "Insert";
|
|
302
|
+
Keys2["Home"] = "Home";
|
|
303
|
+
Keys2["End"] = "End";
|
|
304
|
+
Keys2["PageUp"] = "PageUp";
|
|
305
|
+
Keys2["PageDown"] = "PageDown";
|
|
306
|
+
Keys2["ArrowUp"] = "ArrowUp";
|
|
307
|
+
Keys2["ArrowDown"] = "ArrowDown";
|
|
308
|
+
Keys2["ArrowLeft"] = "ArrowLeft";
|
|
309
|
+
Keys2["ArrowRight"] = "ArrowRight";
|
|
310
|
+
Keys2["Minus"] = "Minus";
|
|
311
|
+
Keys2["Equal"] = "Equal";
|
|
312
|
+
Keys2["BracketLeft"] = "BracketLeft";
|
|
313
|
+
Keys2["BracketRight"] = "BracketRight";
|
|
314
|
+
Keys2["Backslash"] = "Backslash";
|
|
315
|
+
Keys2["Semicolon"] = "Semicolon";
|
|
316
|
+
Keys2["Quote"] = "Quote";
|
|
317
|
+
Keys2["Backquote"] = "Backquote";
|
|
318
|
+
Keys2["Comma"] = "Comma";
|
|
319
|
+
Keys2["Period"] = "Period";
|
|
320
|
+
Keys2["Slash"] = "Slash";
|
|
321
|
+
return Keys2;
|
|
322
|
+
})(Keys || {});
|
|
323
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
324
|
+
0 && (module.exports = {
|
|
325
|
+
Keys,
|
|
326
|
+
hotKeys
|
|
327
|
+
});
|
package/dist/index.mjs
CHANGED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// src/HotKeys.ts
|
|
2
|
+
import { SequenceController } from "@hotora/core";
|
|
3
|
+
var HotKeys = class {
|
|
4
|
+
/** Internal controller for key sequences */
|
|
5
|
+
sequenceController = new SequenceController();
|
|
6
|
+
/**
|
|
7
|
+
* Maps DOM elements to their scope
|
|
8
|
+
* WeakMap is used to avoid memory leaks
|
|
9
|
+
*/
|
|
10
|
+
elements = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
/** Set of currently visible elements (tracked by IntersectionObserver) */
|
|
12
|
+
visibleElements = /* @__PURE__ */ new Set();
|
|
13
|
+
/** Last active element determined by pointer interaction */
|
|
14
|
+
activeElement = null;
|
|
15
|
+
/**
|
|
16
|
+
* Observer that tracks element visibility
|
|
17
|
+
* Adds/removes elements from visibleElements
|
|
18
|
+
*/
|
|
19
|
+
observer = new IntersectionObserver((entries) => {
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const el = entry.target;
|
|
22
|
+
if (entry.isIntersecting) {
|
|
23
|
+
this.visibleElements.add(el);
|
|
24
|
+
} else {
|
|
25
|
+
this.visibleElements.delete(el);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
this.cleanup();
|
|
29
|
+
});
|
|
30
|
+
/** Subscribes to global keyboard and pointer events */
|
|
31
|
+
constructor() {
|
|
32
|
+
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
|
33
|
+
document.addEventListener("keyup", this.onKeyUp.bind(this));
|
|
34
|
+
document.addEventListener("mousedown", this.onPointer.bind(this));
|
|
35
|
+
document.addEventListener("touchstart", this.onPointer.bind(this));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Registers a hotkey or key sequence
|
|
39
|
+
*
|
|
40
|
+
* @param sequence - key combination or sequence
|
|
41
|
+
* @param setup - handler configuration (without id and sequence)
|
|
42
|
+
* @param element - optional DOM element to bind scope to
|
|
43
|
+
* @param scope - optional scope name
|
|
44
|
+
*
|
|
45
|
+
* @returns action identifier
|
|
46
|
+
*/
|
|
47
|
+
register(sequence, setup, element, scope) {
|
|
48
|
+
if (element && scope) {
|
|
49
|
+
this.elements.set(element, scope);
|
|
50
|
+
this.observer.observe(element);
|
|
51
|
+
}
|
|
52
|
+
return this.sequenceController.register(sequence, setup, scope);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Unregisters a previously registered hotkey
|
|
56
|
+
*
|
|
57
|
+
* @param id - action identifier
|
|
58
|
+
*/
|
|
59
|
+
unregister(id) {
|
|
60
|
+
this.sequenceController.unregister(id);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pointer event handler
|
|
64
|
+
* Stores the closest registered element as active
|
|
65
|
+
*/
|
|
66
|
+
onPointer = (e) => {
|
|
67
|
+
const target = e.target;
|
|
68
|
+
const el = this.findClosestRegistered(target);
|
|
69
|
+
if (el) {
|
|
70
|
+
this.activeElement = el;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Key down handler
|
|
75
|
+
* - adds step to sequence
|
|
76
|
+
* - resolves active element
|
|
77
|
+
* - walks through scope chain
|
|
78
|
+
* - executes matched handlers
|
|
79
|
+
*/
|
|
80
|
+
onKeyDown = (event) => {
|
|
81
|
+
if (event.repeat) return;
|
|
82
|
+
const step = event.code;
|
|
83
|
+
const activeEl = this.resolveActiveElement();
|
|
84
|
+
const scopes = this.getScopeChain(activeEl);
|
|
85
|
+
this.sequenceController.addStep(step);
|
|
86
|
+
let stopPropagation = false;
|
|
87
|
+
for (const scope of scopes) {
|
|
88
|
+
if (stopPropagation) break;
|
|
89
|
+
const fired = this.sequenceController.process(scope);
|
|
90
|
+
fired.forEach(([evt, handler]) => {
|
|
91
|
+
handler({
|
|
92
|
+
stopPropagation: () => {
|
|
93
|
+
stopPropagation = true;
|
|
94
|
+
},
|
|
95
|
+
...evt
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Key up handler
|
|
102
|
+
* Removes step from current sequence state
|
|
103
|
+
*/
|
|
104
|
+
onKeyUp = (event) => {
|
|
105
|
+
const step = event.code;
|
|
106
|
+
this.sequenceController.removeStep(step);
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Resolves the currently active element
|
|
110
|
+
*
|
|
111
|
+
* Priority:
|
|
112
|
+
* 1. Only visible elements are considered
|
|
113
|
+
* 2. If one element → return it
|
|
114
|
+
* 3. If activeElement is still visible → return it
|
|
115
|
+
* 4. Otherwise → return the deepest element in DOM
|
|
116
|
+
*/
|
|
117
|
+
resolveActiveElement() {
|
|
118
|
+
const visible = [...this.visibleElements];
|
|
119
|
+
if (visible.length === 0) return null;
|
|
120
|
+
if (visible.length === 1) return visible[0] ?? null;
|
|
121
|
+
if (this.activeElement && this.visibleElements.has(this.activeElement)) {
|
|
122
|
+
return this.activeElement;
|
|
123
|
+
}
|
|
124
|
+
return visible.reduce((deepest, el) => {
|
|
125
|
+
return this.getDepth(el) > this.getDepth(deepest) ? el : deepest;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Builds scope chain from element to root
|
|
130
|
+
* Always includes "$global" as fallback
|
|
131
|
+
*/
|
|
132
|
+
getScopeChain(element) {
|
|
133
|
+
const chain = [];
|
|
134
|
+
let current = element;
|
|
135
|
+
while (current) {
|
|
136
|
+
const scope = this.elements.get(current);
|
|
137
|
+
if (scope) chain.push(scope);
|
|
138
|
+
current = current.parentElement;
|
|
139
|
+
}
|
|
140
|
+
if (!chain.includes("$global")) {
|
|
141
|
+
chain.push("$global");
|
|
142
|
+
}
|
|
143
|
+
return chain;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Finds the closest ancestor element that has a registered scope
|
|
147
|
+
*/
|
|
148
|
+
findClosestRegistered(el) {
|
|
149
|
+
let current = el;
|
|
150
|
+
while (current) {
|
|
151
|
+
if (this.elements.has(current)) return current;
|
|
152
|
+
current = current.parentElement;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Calculates DOM depth of an element
|
|
158
|
+
*/
|
|
159
|
+
getDepth(el) {
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let current = el;
|
|
162
|
+
while (current) {
|
|
163
|
+
depth++;
|
|
164
|
+
current = current.parentElement;
|
|
165
|
+
}
|
|
166
|
+
return depth;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Cleans up elements that are no longer in the DOM
|
|
170
|
+
*/
|
|
171
|
+
cleanup() {
|
|
172
|
+
for (const el of [...this.visibleElements]) {
|
|
173
|
+
if (!el.isConnected) {
|
|
174
|
+
this.visibleElements.delete(el);
|
|
175
|
+
this.observer.unobserve(el);
|
|
176
|
+
if (this.activeElement === el) {
|
|
177
|
+
this.activeElement = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var hotKeys = new HotKeys();
|
|
184
|
+
|
|
185
|
+
// src/types.ts
|
|
186
|
+
var Keys = /* @__PURE__ */ ((Keys2) => {
|
|
187
|
+
Keys2["A"] = "KeyA";
|
|
188
|
+
Keys2["B"] = "KeyB";
|
|
189
|
+
Keys2["C"] = "KeyC";
|
|
190
|
+
Keys2["D"] = "KeyD";
|
|
191
|
+
Keys2["E"] = "KeyE";
|
|
192
|
+
Keys2["F"] = "KeyF";
|
|
193
|
+
Keys2["G"] = "KeyG";
|
|
194
|
+
Keys2["H"] = "KeyH";
|
|
195
|
+
Keys2["I"] = "KeyI";
|
|
196
|
+
Keys2["J"] = "KeyJ";
|
|
197
|
+
Keys2["K"] = "KeyK";
|
|
198
|
+
Keys2["L"] = "KeyL";
|
|
199
|
+
Keys2["M"] = "KeyM";
|
|
200
|
+
Keys2["N"] = "KeyN";
|
|
201
|
+
Keys2["O"] = "KeyO";
|
|
202
|
+
Keys2["P"] = "KeyP";
|
|
203
|
+
Keys2["Q"] = "KeyQ";
|
|
204
|
+
Keys2["R"] = "KeyR";
|
|
205
|
+
Keys2["S"] = "KeyS";
|
|
206
|
+
Keys2["T"] = "KeyT";
|
|
207
|
+
Keys2["U"] = "KeyU";
|
|
208
|
+
Keys2["V"] = "KeyV";
|
|
209
|
+
Keys2["W"] = "KeyW";
|
|
210
|
+
Keys2["X"] = "KeyX";
|
|
211
|
+
Keys2["Y"] = "KeyY";
|
|
212
|
+
Keys2["Z"] = "KeyZ";
|
|
213
|
+
Keys2["Digit0"] = "Digit0";
|
|
214
|
+
Keys2["Digit1"] = "Digit1";
|
|
215
|
+
Keys2["Digit2"] = "Digit2";
|
|
216
|
+
Keys2["Digit3"] = "Digit3";
|
|
217
|
+
Keys2["Digit4"] = "Digit4";
|
|
218
|
+
Keys2["Digit5"] = "Digit5";
|
|
219
|
+
Keys2["Digit6"] = "Digit6";
|
|
220
|
+
Keys2["Digit7"] = "Digit7";
|
|
221
|
+
Keys2["Digit8"] = "Digit8";
|
|
222
|
+
Keys2["Digit9"] = "Digit9";
|
|
223
|
+
Keys2["Numpad0"] = "Numpad0";
|
|
224
|
+
Keys2["Numpad1"] = "Numpad1";
|
|
225
|
+
Keys2["Numpad2"] = "Numpad2";
|
|
226
|
+
Keys2["Numpad3"] = "Numpad3";
|
|
227
|
+
Keys2["Numpad4"] = "Numpad4";
|
|
228
|
+
Keys2["Numpad5"] = "Numpad5";
|
|
229
|
+
Keys2["Numpad6"] = "Numpad6";
|
|
230
|
+
Keys2["Numpad7"] = "Numpad7";
|
|
231
|
+
Keys2["Numpad8"] = "Numpad8";
|
|
232
|
+
Keys2["Numpad9"] = "Numpad9";
|
|
233
|
+
Keys2["NumpadAdd"] = "NumpadAdd";
|
|
234
|
+
Keys2["NumpadSubtract"] = "NumpadSubtract";
|
|
235
|
+
Keys2["NumpadMultiply"] = "NumpadMultiply";
|
|
236
|
+
Keys2["NumpadDivide"] = "NumpadDivide";
|
|
237
|
+
Keys2["NumpadDecimal"] = "NumpadDecimal";
|
|
238
|
+
Keys2["NumpadEnter"] = "NumpadEnter";
|
|
239
|
+
Keys2["F1"] = "F1";
|
|
240
|
+
Keys2["F2"] = "F2";
|
|
241
|
+
Keys2["F3"] = "F3";
|
|
242
|
+
Keys2["F4"] = "F4";
|
|
243
|
+
Keys2["F5"] = "F5";
|
|
244
|
+
Keys2["F6"] = "F6";
|
|
245
|
+
Keys2["F7"] = "F7";
|
|
246
|
+
Keys2["F8"] = "F8";
|
|
247
|
+
Keys2["F9"] = "F9";
|
|
248
|
+
Keys2["F10"] = "F10";
|
|
249
|
+
Keys2["F11"] = "F11";
|
|
250
|
+
Keys2["F12"] = "F12";
|
|
251
|
+
Keys2["F13"] = "F13";
|
|
252
|
+
Keys2["F14"] = "F14";
|
|
253
|
+
Keys2["F15"] = "F15";
|
|
254
|
+
Keys2["F16"] = "F16";
|
|
255
|
+
Keys2["F17"] = "F17";
|
|
256
|
+
Keys2["F18"] = "F18";
|
|
257
|
+
Keys2["F19"] = "F19";
|
|
258
|
+
Keys2["F20"] = "F20";
|
|
259
|
+
Keys2["Escape"] = "Escape";
|
|
260
|
+
Keys2["Tab"] = "Tab";
|
|
261
|
+
Keys2["CapsLock"] = "CapsLock";
|
|
262
|
+
Keys2["ShiftLeft"] = "ShiftLeft";
|
|
263
|
+
Keys2["ShiftRight"] = "ShiftRight";
|
|
264
|
+
Keys2["ControlLeft"] = "ControlLeft";
|
|
265
|
+
Keys2["ControlRight"] = "ControlRight";
|
|
266
|
+
Keys2["AltLeft"] = "AltLeft";
|
|
267
|
+
Keys2["AltRight"] = "AltRight";
|
|
268
|
+
Keys2["MetaLeft"] = "MetaLeft";
|
|
269
|
+
Keys2["MetaRight"] = "MetaRight";
|
|
270
|
+
Keys2["Space"] = "Space";
|
|
271
|
+
Keys2["Enter"] = "Enter";
|
|
272
|
+
Keys2["Backspace"] = "Backspace";
|
|
273
|
+
Keys2["Delete"] = "Delete";
|
|
274
|
+
Keys2["Insert"] = "Insert";
|
|
275
|
+
Keys2["Home"] = "Home";
|
|
276
|
+
Keys2["End"] = "End";
|
|
277
|
+
Keys2["PageUp"] = "PageUp";
|
|
278
|
+
Keys2["PageDown"] = "PageDown";
|
|
279
|
+
Keys2["ArrowUp"] = "ArrowUp";
|
|
280
|
+
Keys2["ArrowDown"] = "ArrowDown";
|
|
281
|
+
Keys2["ArrowLeft"] = "ArrowLeft";
|
|
282
|
+
Keys2["ArrowRight"] = "ArrowRight";
|
|
283
|
+
Keys2["Minus"] = "Minus";
|
|
284
|
+
Keys2["Equal"] = "Equal";
|
|
285
|
+
Keys2["BracketLeft"] = "BracketLeft";
|
|
286
|
+
Keys2["BracketRight"] = "BracketRight";
|
|
287
|
+
Keys2["Backslash"] = "Backslash";
|
|
288
|
+
Keys2["Semicolon"] = "Semicolon";
|
|
289
|
+
Keys2["Quote"] = "Quote";
|
|
290
|
+
Keys2["Backquote"] = "Backquote";
|
|
291
|
+
Keys2["Comma"] = "Comma";
|
|
292
|
+
Keys2["Period"] = "Period";
|
|
293
|
+
Keys2["Slash"] = "Slash";
|
|
294
|
+
return Keys2;
|
|
295
|
+
})(Keys || {});
|
|
296
|
+
export {
|
|
297
|
+
Keys,
|
|
298
|
+
hotKeys
|
|
299
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotora/hotkeys",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Lightweight JavaScript and TypeScript library for handling keyboard shortcuts, hotkeys, and complex key sequences with customizable scoped actions.",
|
|
5
5
|
"author": "egorkk1211@gmail.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"exports": {
|
|
42
42
|
".": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
43
44
|
"require": "./dist/index.js",
|
|
44
45
|
"import": "./dist/index.mjs"
|
|
45
46
|
}
|
|
@@ -48,6 +49,6 @@
|
|
|
48
49
|
"access": "public"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"@hotora/core": "^
|
|
52
|
+
"@hotora/core": "^2.0.2"
|
|
52
53
|
}
|
|
53
54
|
}
|