@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 CHANGED
@@ -1 +1,203 @@
1
- # Hotkeys
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
- export { }
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
- export { }
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.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": "^1.0.0"
52
+ "@hotora/core": "^2.0.2"
52
53
  }
53
54
  }