@tanstack/hotkeys 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_event-target.js +156 -32
- package/dist/_registration.js +4 -3
- package/dist/_target-listeners.js +72 -0
- package/dist/adapter.js +3 -5
- package/dist/app-hotkeys.d.ts +1 -1
- package/dist/conflicts.js +2 -6
- package/dist/hotkey-manager.d.ts +24 -4
- package/dist/hotkey-manager.js +64 -83
- package/dist/hotkey-recorder.js +2 -2
- package/dist/key-state-tracker.js +35 -5
- package/package.json +1 -1
package/dist/_event-target.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
import { normalizeKeyName } from "./constants.js";
|
|
2
|
+
|
|
1
3
|
//#region src/_event-target.ts
|
|
4
|
+
function isNode(target) {
|
|
5
|
+
return typeof target?.nodeType === "number";
|
|
6
|
+
}
|
|
7
|
+
function isElement(target) {
|
|
8
|
+
return isNode(target) && target.nodeType === 1;
|
|
9
|
+
}
|
|
10
|
+
function isWindow(target) {
|
|
11
|
+
return "document" in target;
|
|
12
|
+
}
|
|
13
|
+
/** Returns the document a listener target belongs to, in any frame. */
|
|
14
|
+
function getTargetDocument(target) {
|
|
15
|
+
if (isWindow(target)) return target.document;
|
|
16
|
+
if (target.nodeType === 9) return target;
|
|
17
|
+
return target.ownerDocument;
|
|
18
|
+
}
|
|
2
19
|
/**
|
|
3
20
|
* Checks if an element is an input-like element that should be ignored for hotkeys.
|
|
4
21
|
*
|
|
@@ -12,15 +29,13 @@
|
|
|
12
29
|
* Mod+S and Escape fire when the user has tabbed to a form button.
|
|
13
30
|
*/
|
|
14
31
|
function isInputElement(element) {
|
|
15
|
-
if (!element) return false;
|
|
16
|
-
if (element
|
|
32
|
+
if (!isElement(element)) return false;
|
|
33
|
+
if (element.localName === "input") {
|
|
17
34
|
const type = element.type.toLowerCase();
|
|
18
|
-
|
|
19
|
-
return true;
|
|
35
|
+
return type !== "button" && type !== "submit" && type !== "reset";
|
|
20
36
|
}
|
|
21
|
-
if (element
|
|
22
|
-
|
|
23
|
-
return false;
|
|
37
|
+
if (element.localName === "textarea" || element.localName === "select") return true;
|
|
38
|
+
return element.isContentEditable === true;
|
|
24
39
|
}
|
|
25
40
|
/**
|
|
26
41
|
* Returns the focused element for the document associated with where hotkey
|
|
@@ -30,41 +45,150 @@ function isInputElement(element) {
|
|
|
30
45
|
*/
|
|
31
46
|
function getActiveElementForListenerTarget(target) {
|
|
32
47
|
if (typeof document === "undefined") return null;
|
|
33
|
-
|
|
34
|
-
if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) return target.ownerDocument.activeElement ?? null;
|
|
35
|
-
return target.document.activeElement ?? null;
|
|
48
|
+
return getTargetDocument(target).activeElement;
|
|
36
49
|
}
|
|
50
|
+
const VERTICAL = ["ArrowUp", "ArrowDown"];
|
|
51
|
+
const HORIZONTAL = ["ArrowLeft", "ArrowRight"];
|
|
52
|
+
const ARROWS = [...VERTICAL, ...HORIZONTAL];
|
|
53
|
+
const EDGES = ["Home", "End"];
|
|
54
|
+
const PAGES = ["PageUp", "PageDown"];
|
|
55
|
+
const ACTIVATION = ["Enter", "Space"];
|
|
56
|
+
const TYPEAHEAD = "typeahead";
|
|
37
57
|
/**
|
|
38
|
-
* Returns
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* This checks:
|
|
42
|
-
* - the currently focused element for the listener target
|
|
43
|
-
* - the event's composed path (for shadow DOM)
|
|
44
|
-
* - the event target as a final fallback
|
|
58
|
+
* Returns the keys a control handles itself: activation for buttons and
|
|
59
|
+
* links, and navigation for ARIA widgets, following the WAI-ARIA Authoring
|
|
60
|
+
* Practices keyboard patterns (https://www.w3.org/WAI/ARIA/apg/patterns/).
|
|
45
61
|
*/
|
|
46
|
-
function
|
|
62
|
+
function getOwnedKeys(element) {
|
|
63
|
+
const orientation = element.getAttribute("aria-orientation");
|
|
64
|
+
switch (element.getAttribute("role")?.trim().split(/\s+/)[0]) {
|
|
65
|
+
case "button":
|
|
66
|
+
case "tab":
|
|
67
|
+
case "option":
|
|
68
|
+
case "treeitem":
|
|
69
|
+
case "menuitem":
|
|
70
|
+
case "menuitemcheckbox":
|
|
71
|
+
case "menuitemradio": return ACTIVATION;
|
|
72
|
+
case "link": return ["Enter"];
|
|
73
|
+
case "checkbox":
|
|
74
|
+
case "switch":
|
|
75
|
+
case "radio": return ["Space"];
|
|
76
|
+
case "menu":
|
|
77
|
+
case "menubar":
|
|
78
|
+
case "tree": return [
|
|
79
|
+
...ARROWS,
|
|
80
|
+
...EDGES,
|
|
81
|
+
...ACTIVATION,
|
|
82
|
+
TYPEAHEAD
|
|
83
|
+
];
|
|
84
|
+
case "listbox": return [
|
|
85
|
+
...orientation === "horizontal" ? HORIZONTAL : VERTICAL,
|
|
86
|
+
...EDGES,
|
|
87
|
+
...ACTIVATION,
|
|
88
|
+
TYPEAHEAD
|
|
89
|
+
];
|
|
90
|
+
case "combobox": return [
|
|
91
|
+
...VERTICAL,
|
|
92
|
+
...EDGES,
|
|
93
|
+
...ACTIVATION,
|
|
94
|
+
TYPEAHEAD
|
|
95
|
+
];
|
|
96
|
+
case "tablist": return [
|
|
97
|
+
...orientation === "vertical" ? VERTICAL : HORIZONTAL,
|
|
98
|
+
...EDGES,
|
|
99
|
+
...ACTIVATION
|
|
100
|
+
];
|
|
101
|
+
case "toolbar": return [...orientation === "vertical" ? VERTICAL : HORIZONTAL, ...EDGES];
|
|
102
|
+
case "grid":
|
|
103
|
+
case "treegrid": return [
|
|
104
|
+
...ARROWS,
|
|
105
|
+
...EDGES,
|
|
106
|
+
...PAGES,
|
|
107
|
+
...ACTIVATION
|
|
108
|
+
];
|
|
109
|
+
case "radiogroup": return [...ARROWS, "Space"];
|
|
110
|
+
case "slider": return [
|
|
111
|
+
...ARROWS,
|
|
112
|
+
...EDGES,
|
|
113
|
+
...PAGES
|
|
114
|
+
];
|
|
115
|
+
case "spinbutton": return [
|
|
116
|
+
...VERTICAL,
|
|
117
|
+
...EDGES,
|
|
118
|
+
...PAGES
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
switch (element.localName) {
|
|
122
|
+
case "button":
|
|
123
|
+
case "summary": return ACTIVATION;
|
|
124
|
+
case "input": {
|
|
125
|
+
const type = element.type;
|
|
126
|
+
return type === "button" || type === "submit" || type === "reset" ? ACTIVATION : [];
|
|
127
|
+
}
|
|
128
|
+
case "a":
|
|
129
|
+
case "area": return element.hasAttribute("href") ? ["Enter"] : [];
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether a control handles this key itself. Ctrl, Meta, and Alt combinations
|
|
135
|
+
* are never owned; Enter and Space are owned only without modifiers, while
|
|
136
|
+
* navigation and typeahead keys allow Shift (for example, to extend a selection).
|
|
137
|
+
*/
|
|
138
|
+
function ownsKey(element, event, key) {
|
|
139
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
140
|
+
const owned = getOwnedKeys(element);
|
|
141
|
+
if (key === "Enter" || key === "Space") return !event.shiftKey && owned.includes(key);
|
|
142
|
+
return owned.includes(key) || owned.includes(TYPEAHEAD) && /^\S$/u.test(event.key);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether the focused element or an element on the event's path passes `test`.
|
|
146
|
+
* Only elements inside the registration target count: walking outward from
|
|
147
|
+
* the event's origin stops at the registration target, so a hotkey targeted
|
|
148
|
+
* at a control, or at an element inside it, is not blocked by that control.
|
|
149
|
+
*/
|
|
150
|
+
function someElementInsideTarget(event, listenerTarget, registrationTarget, test) {
|
|
151
|
+
const scoped = !isWindow(registrationTarget) && registrationTarget.nodeType !== 9;
|
|
47
152
|
const focused = getActiveElementForListenerTarget(listenerTarget);
|
|
48
|
-
if (focused &&
|
|
49
|
-
|
|
50
|
-
|
|
153
|
+
if (focused && focused !== registrationTarget && (!scoped || registrationTarget.contains(focused)) && test(focused)) return true;
|
|
154
|
+
const path = event.composedPath();
|
|
155
|
+
for (const element of path.length > 0 ? path : [event.target]) {
|
|
156
|
+
if (element === registrationTarget) break;
|
|
157
|
+
if (isElement(element) && test(element)) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Returns whether a hotkey should skip an event because a focused control
|
|
163
|
+
* inside the registration target handles the key itself:
|
|
164
|
+
* - text inputs, textareas, selects, and contenteditable elements (typing)
|
|
165
|
+
* - buttons, links, and `<summary>` elements (Enter and Space activation)
|
|
166
|
+
* - ARIA widgets such as menus, listboxes, tabs, and grids (navigation keys)
|
|
167
|
+
*/
|
|
168
|
+
function shouldIgnoreInputEvent(event, listenerTarget, registrationTarget) {
|
|
169
|
+
const key = normalizeKeyName(event.key);
|
|
170
|
+
return someElementInsideTarget(event, listenerTarget, registrationTarget, (element) => isInputElement(element) || ownsKey(element, event, key));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns whether an event comes from a text input, textarea, select, or
|
|
174
|
+
* contenteditable element in the target's document. Unlike
|
|
175
|
+
* `shouldIgnoreInputEvent`, buttons, links, and ARIA widgets don't count.
|
|
176
|
+
*/
|
|
177
|
+
function isTypingEvent(event, target) {
|
|
178
|
+
return someElementInsideTarget(event, target, target, isInputElement);
|
|
51
179
|
}
|
|
52
180
|
/**
|
|
53
181
|
* Checks if an event is for the given target (originated from or bubbled to it).
|
|
54
182
|
*
|
|
55
|
-
* For document/window targets, also accepts document
|
|
56
|
-
* to handle Brave and other browsers where currentTarget may be
|
|
57
|
-
* instead of document when listeners are attached to document.
|
|
183
|
+
* For document/window targets, also accepts the document's root element as
|
|
184
|
+
* currentTarget to handle Brave and other browsers where currentTarget may be
|
|
185
|
+
* documentElement instead of document when listeners are attached to document.
|
|
58
186
|
*/
|
|
59
187
|
function isEventForTarget(event, target) {
|
|
60
|
-
if (
|
|
61
|
-
if (target
|
|
62
|
-
|
|
63
|
-
if (event.currentTarget === target) return true;
|
|
64
|
-
if (event.target instanceof Node && target.contains(event.target)) return true;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
188
|
+
if (event.currentTarget === target) return true;
|
|
189
|
+
if (isWindow(target) || target.nodeType === 9) return event.currentTarget === getTargetDocument(target).documentElement;
|
|
190
|
+
return isNode(event.target) && target.contains(event.target);
|
|
67
191
|
}
|
|
68
192
|
|
|
69
193
|
//#endregion
|
|
70
|
-
export { getActiveElementForListenerTarget, isEventForTarget, isInputElement, shouldIgnoreInputEvent };
|
|
194
|
+
export { getActiveElementForListenerTarget, getTargetDocument, isEventForTarget, isInputElement, isTypingEvent, shouldIgnoreInputEvent };
|
package/dist/_registration.js
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
* Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).
|
|
5
5
|
*/
|
|
6
6
|
const defaultHotkeyOptions = {
|
|
7
|
+
capture: false,
|
|
7
8
|
preventDefault: true,
|
|
8
9
|
stopPropagation: true,
|
|
9
10
|
eventType: "keydown",
|
|
10
11
|
enabled: true,
|
|
11
|
-
ignoreInputs:
|
|
12
|
+
ignoreInputs: "auto",
|
|
12
13
|
conflictBehavior: "warn"
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Resolves `ignoreInputs: 'auto'` from the hotkey (the first step of a sequence).
|
|
16
17
|
* Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
|
|
17
18
|
*/
|
|
18
19
|
function getDefaultIgnoreInputs(parsedHotkey) {
|
|
@@ -31,7 +32,7 @@ function getDefaultIgnoreInputs(parsedHotkey) {
|
|
|
31
32
|
function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
|
|
32
33
|
if (conflictBehavior === "allow") return;
|
|
33
34
|
if (conflictBehavior === "warn") {
|
|
34
|
-
console.warn(`'${keyDisplay}' is already registered. Multiple handlers
|
|
35
|
+
console.warn(`'${keyDisplay}' is already registered. Multiple handlers may be triggered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to suppress this warning.`);
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
if (conflictBehavior === "error") throw new Error(`'${keyDisplay}' is already registered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to allow multiple registrations.`);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/_target-listeners.ts
|
|
2
|
+
/** Shares keyboard listeners within each target and capture phase. */
|
|
3
|
+
var TargetListeners = class {
|
|
4
|
+
dispatch;
|
|
5
|
+
#targets = /* @__PURE__ */ new Map();
|
|
6
|
+
#processed = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
constructor(dispatch) {
|
|
8
|
+
this.dispatch = dispatch;
|
|
9
|
+
}
|
|
10
|
+
add(target, capture, id) {
|
|
11
|
+
if (typeof document === "undefined") return;
|
|
12
|
+
let phases = this.#targets.get(target);
|
|
13
|
+
if (!phases) {
|
|
14
|
+
phases = /* @__PURE__ */ new Map();
|
|
15
|
+
this.#targets.set(target, phases);
|
|
16
|
+
}
|
|
17
|
+
let group = phases.get(capture);
|
|
18
|
+
if (!group) {
|
|
19
|
+
const ids = /* @__PURE__ */ new Set();
|
|
20
|
+
const listener = (eventType) => (rawEvent) => {
|
|
21
|
+
const event = rawEvent;
|
|
22
|
+
let processed = this.#processed.get(event);
|
|
23
|
+
if (!processed || processed.groups.has(ids)) {
|
|
24
|
+
processed = {
|
|
25
|
+
ids: /* @__PURE__ */ new Set(),
|
|
26
|
+
groups: /* @__PURE__ */ new Set()
|
|
27
|
+
};
|
|
28
|
+
this.#processed.set(event, processed);
|
|
29
|
+
}
|
|
30
|
+
processed.groups.add(ids);
|
|
31
|
+
const pending = /* @__PURE__ */ new Set();
|
|
32
|
+
for (const registrationId of ids) if (!processed.ids.has(registrationId)) {
|
|
33
|
+
processed.ids.add(registrationId);
|
|
34
|
+
pending.add(registrationId);
|
|
35
|
+
}
|
|
36
|
+
this.dispatch(event, target, eventType, capture, pending);
|
|
37
|
+
};
|
|
38
|
+
group = {
|
|
39
|
+
ids,
|
|
40
|
+
keydown: listener("keydown"),
|
|
41
|
+
keyup: listener("keyup")
|
|
42
|
+
};
|
|
43
|
+
phases.set(capture, group);
|
|
44
|
+
target.addEventListener("keydown", group.keydown, capture);
|
|
45
|
+
target.addEventListener("keyup", group.keyup, capture);
|
|
46
|
+
}
|
|
47
|
+
group.ids.add(id);
|
|
48
|
+
}
|
|
49
|
+
remove(target, capture, id) {
|
|
50
|
+
const phases = this.#targets.get(target);
|
|
51
|
+
const group = phases?.get(capture);
|
|
52
|
+
if (!group) return;
|
|
53
|
+
group.ids.delete(id);
|
|
54
|
+
if (group.ids.size === 0) {
|
|
55
|
+
target.removeEventListener("keydown", group.keydown, capture);
|
|
56
|
+
target.removeEventListener("keyup", group.keyup, capture);
|
|
57
|
+
phases.delete(capture);
|
|
58
|
+
if (phases.size === 0) this.#targets.delete(target);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
destroy() {
|
|
62
|
+
for (const [target, phases] of this.#targets) for (const [capture, group] of phases) {
|
|
63
|
+
target.removeEventListener("keydown", group.keydown, capture);
|
|
64
|
+
target.removeEventListener("keyup", group.keyup, capture);
|
|
65
|
+
}
|
|
66
|
+
this.#targets.clear();
|
|
67
|
+
this.#processed = /* @__PURE__ */ new WeakMap();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { TargetListeners };
|
package/dist/adapter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
|
-
import { isHotkeySequence, normalizeRegisterableHotkey
|
|
3
|
-
import { defaultHotkeyOptions
|
|
2
|
+
import { isHotkeySequence, normalizeRegisterableHotkey } from "./parse.js";
|
|
3
|
+
import { defaultHotkeyOptions } from "./_registration.js";
|
|
4
4
|
import { getHotkeyManager } from "./hotkey-manager.js";
|
|
5
5
|
import { getAppHotkeyRegistrationOptions, isAppHotkey, resolveAppHotkey } from "./app-hotkeys.js";
|
|
6
6
|
|
|
@@ -23,14 +23,12 @@ function createHotkeyBindings() {
|
|
|
23
23
|
const options = {
|
|
24
24
|
...defaultHotkeyOptions,
|
|
25
25
|
...merged,
|
|
26
|
-
ignoreInputs: merged.ignoreInputs,
|
|
26
|
+
ignoreInputs: merged.ignoreInputs ?? "auto",
|
|
27
27
|
target: "target" in merged ? merged.target ?? null : typeof document === "undefined" ? null : document,
|
|
28
28
|
platform: merged.platform ?? detectPlatform()
|
|
29
29
|
};
|
|
30
30
|
if (!options.target || isHotkeySequence(definition.hotkey) && definition.hotkey.length === 0) return;
|
|
31
31
|
const hotkey = normalizeRegisterableHotkey(definition.hotkey, options.platform);
|
|
32
|
-
const first = isHotkeySequence(hotkey) ? hotkey[0] : hotkey;
|
|
33
|
-
options.ignoreInputs ??= getDefaultIgnoreInputs(parseHotkey(first, options.platform));
|
|
34
32
|
if (isHotkeySequence(hotkey)) delete options.requireReset;
|
|
35
33
|
else delete options.timeout;
|
|
36
34
|
prepared.push({
|
package/dist/app-hotkeys.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { HotkeyChordOptions, HotkeyRegistrationView, HotkeySequenceOptions } fro
|
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/app-hotkeys.d.ts
|
|
5
5
|
type Platform = 'mac' | 'windows' | 'linux';
|
|
6
|
-
type DeclaredOptionKeys = 'conflictBehavior' | 'eventType' | 'ignoreInputs' | 'preventDefault' | 'stopPropagation';
|
|
6
|
+
type DeclaredOptionKeys = 'capture' | 'conflictBehavior' | 'eventType' | 'ignoreInputs' | 'preventDefault' | 'stopPropagation';
|
|
7
7
|
/** Declared options for an app hotkey whose default is a chord. */
|
|
8
8
|
export type AppHotkeyChordOptions = Pick<HotkeyChordOptions, DeclaredOptionKeys | 'requireReset'>;
|
|
9
9
|
/** Declared options for an app hotkey whose default is a sequence. */
|
package/dist/conflicts.js
CHANGED
|
@@ -2,18 +2,14 @@ import { detectPlatform } from "./platform.js";
|
|
|
2
2
|
import { keysEqual } from "./_keyboard-event.js";
|
|
3
3
|
import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
|
|
4
4
|
import { matchesKeyboardEvent } from "./match.js";
|
|
5
|
+
import { getTargetDocument } from "./_event-target.js";
|
|
5
6
|
import { getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
6
7
|
|
|
7
8
|
//#region src/conflicts.ts
|
|
8
9
|
/** Whether two targets share a document and one contains the other (or is its window). */
|
|
9
10
|
function targetsOverlap(a, b) {
|
|
10
11
|
if (a === b) return true;
|
|
11
|
-
|
|
12
|
-
if ("document" in target) return target.document;
|
|
13
|
-
if (target.nodeType === 9) return target;
|
|
14
|
-
return target.ownerDocument;
|
|
15
|
-
};
|
|
16
|
-
if (documentOf(a) !== documentOf(b)) return false;
|
|
12
|
+
if (getTargetDocument(a) !== getTargetDocument(b)) return false;
|
|
17
13
|
if (!("contains" in a) || !("contains" in b)) return true;
|
|
18
14
|
return a.contains(b) || b.contains(a);
|
|
19
15
|
}
|
package/dist/hotkey-manager.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export type Target = HTMLElement | Document | Window;
|
|
|
8
8
|
* Options shared by chords and sequences.
|
|
9
9
|
*/
|
|
10
10
|
export interface HotkeyOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Listen during capture, before descendant listeners. Defaults to `false` (bubble).
|
|
13
|
+
* With `stopPropagation: true`, a match on an ancestor prevents the event from
|
|
14
|
+
* reaching descendant widgets. Other listeners on the same target still run.
|
|
15
|
+
* Applies to both `keydown` and `keyup`, for chords and sequences.
|
|
16
|
+
* Changing this option preserves held-key reset state and sequence progress.
|
|
17
|
+
*/
|
|
18
|
+
capture?: boolean;
|
|
11
19
|
/** Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' */
|
|
12
20
|
conflictBehavior?: ConflictBehavior;
|
|
13
21
|
/**
|
|
@@ -18,8 +26,17 @@ export interface HotkeyOptions {
|
|
|
18
26
|
enabled?: boolean;
|
|
19
27
|
/** The event type to listen for. Defaults to 'keydown' */
|
|
20
28
|
eventType?: 'keydown' | 'keyup';
|
|
21
|
-
/**
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Skip keys that a focused control inside the target uses itself: typing in
|
|
31
|
+
* text inputs, textarea, select, and contenteditable; Enter/Space on buttons
|
|
32
|
+
* and summary elements; Enter on links; and navigation keys in ARIA widgets
|
|
33
|
+
* such as menus, listboxes, tabs, and grids. A hotkey targeted at the control
|
|
34
|
+
* itself still receives them. Defaults to `'auto'`, which chooses from the
|
|
35
|
+
* hotkey (the first step of a sequence): true for single keys and Shift/Alt
|
|
36
|
+
* combos; false for Ctrl/Meta shortcuts and Escape. Set false to handle keys
|
|
37
|
+
* even in these controls.
|
|
38
|
+
*/
|
|
39
|
+
ignoreInputs?: boolean | 'auto';
|
|
23
40
|
/** The target platform for resolving 'Mod' */
|
|
24
41
|
platform?: 'mac' | 'windows' | 'linux';
|
|
25
42
|
/** Prevent the default browser action when the hotkey matches. Defaults to true */
|
|
@@ -147,6 +164,9 @@ export interface HotkeyRegistrationHandle<TOptions extends HotkeyOptions = Hotke
|
|
|
147
164
|
/**
|
|
148
165
|
* Update options (merged with existing options).
|
|
149
166
|
* Useful for updating `enabled`, `preventDefault`, etc. without re-registering.
|
|
167
|
+
* Changing `capture` moves the listener phase without clearing `requireReset`
|
|
168
|
+
* state or sequence progress. A registration already processed for an event
|
|
169
|
+
* cannot process it again in another phase.
|
|
150
170
|
*/
|
|
151
171
|
setOptions: (options: Partial<TOptions>) => void;
|
|
152
172
|
/** Unregister this hotkey */
|
|
@@ -156,8 +176,8 @@ export interface HotkeyRegistrationHandle<TOptions extends HotkeyOptions = Hotke
|
|
|
156
176
|
* Singleton manager for hotkey registrations, including sequences.
|
|
157
177
|
*
|
|
158
178
|
* This class provides a centralized way to register and manage keyboard
|
|
159
|
-
* hotkeys. It
|
|
160
|
-
*
|
|
179
|
+
* hotkeys. It shares one keydown and one keyup listener per target and
|
|
180
|
+
* capture phase across registrations.
|
|
161
181
|
*
|
|
162
182
|
* Sequences (Vim-style hotkeys such as `['G', 'G']`) match when every step is
|
|
163
183
|
* pressed within the timeout. Modifier-only events (Shift, Control, Alt, or
|
package/dist/hotkey-manager.js
CHANGED
|
@@ -5,9 +5,10 @@ import { isHotkeySequence, isModifierKey, parseHotkey, rawHotkeyToParsedHotkey,
|
|
|
5
5
|
import { formatHotkey } from "./format.js";
|
|
6
6
|
import { matchKeyboardEvent } from "./_match.js";
|
|
7
7
|
import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
|
|
8
|
+
import { isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
|
|
9
|
+
import { TargetListeners } from "./_target-listeners.js";
|
|
8
10
|
import { isRecordingEvent } from "./_recording-guard.js";
|
|
9
11
|
import { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
|
|
10
|
-
import { isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
|
|
11
12
|
import { Store } from "@tanstack/store";
|
|
12
13
|
|
|
13
14
|
//#region src/hotkey-manager.ts
|
|
@@ -58,6 +59,11 @@ function optionsForKind(kind, options) {
|
|
|
58
59
|
function resolvedSteps(registration) {
|
|
59
60
|
return registration.kind === "chord" ? [registration.parsedHotkey] : registration.parsedSequence;
|
|
60
61
|
}
|
|
62
|
+
/** Whether a registration skips keys that focused controls use, resolving `'auto'`. */
|
|
63
|
+
function ignoresInputs(registration) {
|
|
64
|
+
const { ignoreInputs = "auto" } = registration.options;
|
|
65
|
+
return ignoreInputs === "auto" ? getDefaultIgnoreInputs(resolvedSteps(registration)[0]) : ignoreInputs;
|
|
66
|
+
}
|
|
61
67
|
function sameResolvedSteps(left, right) {
|
|
62
68
|
return left.length === right.length && left.every((step, index) => areHotkeysEqual(step, right[index]));
|
|
63
69
|
}
|
|
@@ -68,8 +74,8 @@ function formatForMessage(hotkey) {
|
|
|
68
74
|
* Singleton manager for hotkey registrations, including sequences.
|
|
69
75
|
*
|
|
70
76
|
* This class provides a centralized way to register and manage keyboard
|
|
71
|
-
* hotkeys. It
|
|
72
|
-
*
|
|
77
|
+
* hotkeys. It shares one keydown and one keyup listener per target and
|
|
78
|
+
* capture phase across registrations.
|
|
73
79
|
*
|
|
74
80
|
* Sequences (Vim-style hotkeys such as `['G', 'G']`) match when every step is
|
|
75
81
|
* pressed within the timeout. Modifier-only events (Shift, Control, Alt, or
|
|
@@ -118,8 +124,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
118
124
|
*/
|
|
119
125
|
appHotkeys = new Store(null);
|
|
120
126
|
#platform;
|
|
121
|
-
#
|
|
122
|
-
#targetRegistrations = /* @__PURE__ */ new Map();
|
|
127
|
+
#listeners = new TargetListeners((...args) => this.#processTargetEvent(...args));
|
|
123
128
|
constructor() {
|
|
124
129
|
this.#platform = detectPlatform();
|
|
125
130
|
}
|
|
@@ -167,28 +172,26 @@ var HotkeyManager = class HotkeyManager {
|
|
|
167
172
|
const platform = options.platform ?? this.#platform;
|
|
168
173
|
const target = options.target ?? document;
|
|
169
174
|
let registration;
|
|
170
|
-
if (isHotkeySequence(hotkey)) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
} else {
|
|
175
|
+
if (isHotkeySequence(hotkey)) registration = {
|
|
176
|
+
kind: "sequence",
|
|
177
|
+
id,
|
|
178
|
+
hotkey,
|
|
179
|
+
parsedSequence: hotkey.map((step) => parseHotkey(step, platform)),
|
|
180
|
+
callback,
|
|
181
|
+
options: {
|
|
182
|
+
...defaultHotkeyOptions,
|
|
183
|
+
timeout: DEFAULT_SEQUENCE_TIMEOUT,
|
|
184
|
+
...optionsForKind("sequence", options),
|
|
185
|
+
platform,
|
|
186
|
+
ignoreInputs: options.ignoreInputs ?? "auto"
|
|
187
|
+
},
|
|
188
|
+
target,
|
|
189
|
+
currentIndex: 0,
|
|
190
|
+
lastKeyTime: 0,
|
|
191
|
+
triggerCount: 0,
|
|
192
|
+
hasFired: false
|
|
193
|
+
};
|
|
194
|
+
else {
|
|
192
195
|
const parsedHotkey = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : rawHotkeyToParsedHotkey(hotkey, platform);
|
|
193
196
|
registration = {
|
|
194
197
|
kind: "chord",
|
|
@@ -201,7 +204,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
201
204
|
requireReset: false,
|
|
202
205
|
...optionsForKind("chord", options),
|
|
203
206
|
platform,
|
|
204
|
-
ignoreInputs: options.ignoreInputs ??
|
|
207
|
+
ignoreInputs: options.ignoreInputs ?? "auto"
|
|
205
208
|
},
|
|
206
209
|
target,
|
|
207
210
|
hasFired: false,
|
|
@@ -211,9 +214,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
211
214
|
const conflicting = this.#findConflictingRegistration(registration);
|
|
212
215
|
if (conflicting) handleConflict(conflicting.id, formatForMessage(registration.hotkey), options.conflictBehavior ?? "warn", (conflictId) => this.#unregister(conflictId));
|
|
213
216
|
this.registrations.setState((prev) => new Map(prev).set(id, registration));
|
|
214
|
-
|
|
215
|
-
this.#targetRegistrations.get(target).add(id);
|
|
216
|
-
this.#ensureListenersForTarget(target);
|
|
217
|
+
this.#listeners.add(target, !!registration.options.capture, id);
|
|
217
218
|
const manager = this;
|
|
218
219
|
return {
|
|
219
220
|
get id() {
|
|
@@ -238,6 +239,11 @@ var HotkeyManager = class HotkeyManager {
|
|
|
238
239
|
...optionsForKind(reg.kind, newOptions)
|
|
239
240
|
};
|
|
240
241
|
if (optionsEqual(reg.options, options)) return prev;
|
|
242
|
+
const capture = !!reg.options.capture;
|
|
243
|
+
if (capture !== !!options.capture) {
|
|
244
|
+
manager.#listeners.remove(reg.target, capture, id);
|
|
245
|
+
manager.#listeners.add(reg.target, !!options.capture, id);
|
|
246
|
+
}
|
|
241
247
|
return new Map(prev).set(id, {
|
|
242
248
|
...reg,
|
|
243
249
|
options
|
|
@@ -261,58 +267,31 @@ var HotkeyManager = class HotkeyManager {
|
|
|
261
267
|
next.delete(id);
|
|
262
268
|
return next;
|
|
263
269
|
});
|
|
264
|
-
|
|
265
|
-
if (targetRegs) {
|
|
266
|
-
targetRegs.delete(id);
|
|
267
|
-
if (targetRegs.size === 0) this.#removeListenersForTarget(target);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Ensures event listeners are attached for a specific target.
|
|
272
|
-
*/
|
|
273
|
-
#ensureListenersForTarget(target) {
|
|
274
|
-
if (typeof document === "undefined") return;
|
|
275
|
-
if (this.#targetListeners.has(target)) return;
|
|
276
|
-
const keydown = (event) => this.#processTargetEvent(event, target, "keydown");
|
|
277
|
-
const keyup = (event) => this.#processTargetEvent(event, target, "keyup");
|
|
278
|
-
target.addEventListener("keydown", keydown);
|
|
279
|
-
target.addEventListener("keyup", keyup);
|
|
280
|
-
this.#targetListeners.set(target, {
|
|
281
|
-
keydown,
|
|
282
|
-
keyup
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Removes event listeners for a specific target.
|
|
287
|
-
*/
|
|
288
|
-
#removeListenersForTarget(target) {
|
|
289
|
-
if (typeof document === "undefined") return;
|
|
290
|
-
const listeners = this.#targetListeners.get(target);
|
|
291
|
-
if (!listeners) return;
|
|
292
|
-
target.removeEventListener("keydown", listeners.keydown);
|
|
293
|
-
target.removeEventListener("keyup", listeners.keyup);
|
|
294
|
-
this.#targetListeners.delete(target);
|
|
295
|
-
this.#targetRegistrations.delete(target);
|
|
270
|
+
this.#listeners.remove(target, !!registration.options.capture, id);
|
|
296
271
|
}
|
|
297
|
-
/** Registrations of one kind
|
|
298
|
-
#
|
|
272
|
+
/** Registrations of one kind in the listener phase, in registration order. */
|
|
273
|
+
#registrationsIn(ids, capture, kind) {
|
|
299
274
|
const result = [];
|
|
300
|
-
for (const id of
|
|
275
|
+
for (const id of ids) {
|
|
301
276
|
const registration = this.registrations.state.get(id);
|
|
302
|
-
if (registration?.kind === kind) result.push(registration);
|
|
277
|
+
if (registration?.kind === kind && !!registration.options.capture === capture) result.push(registration);
|
|
303
278
|
}
|
|
304
279
|
return result;
|
|
305
280
|
}
|
|
281
|
+
/** The live registration, unless it was removed or moved to another phase. */
|
|
282
|
+
#currentIn(registration, capture) {
|
|
283
|
+
const current = this.registrations.state.get(registration.id);
|
|
284
|
+
return current && !!current.options.capture === capture ? current : void 0;
|
|
285
|
+
}
|
|
306
286
|
/**
|
|
307
287
|
* Processes keyboard events for a specific target and event type.
|
|
308
288
|
* Chords and sequences match independently; both may fire for one event.
|
|
309
289
|
*/
|
|
310
|
-
#processTargetEvent(event, target, eventType) {
|
|
311
|
-
this.#processChords(event, target, eventType);
|
|
312
|
-
this.#processSequences(event, target, eventType);
|
|
290
|
+
#processTargetEvent(event, target, eventType, capture, ids) {
|
|
291
|
+
this.#processChords(event, target, eventType, capture, this.#registrationsIn(ids, capture, "chord"));
|
|
292
|
+
this.#processSequences(event, target, eventType, capture, this.#registrationsIn(ids, capture, "sequence"));
|
|
313
293
|
}
|
|
314
|
-
#processChords(event, target, eventType) {
|
|
315
|
-
const registrations = this.#registrationsOn(target, "chord");
|
|
294
|
+
#processChords(event, target, eventType, capture, registrations) {
|
|
316
295
|
if (eventType === "keyup") {
|
|
317
296
|
for (const registration of registrations) if (registration.hasFired && this.#shouldResetRegistration(registration, event)) {
|
|
318
297
|
registration.hasFired = false;
|
|
@@ -323,16 +302,18 @@ var HotkeyManager = class HotkeyManager {
|
|
|
323
302
|
const matches = /* @__PURE__ */ new Map();
|
|
324
303
|
let bestScore = 0;
|
|
325
304
|
for (const registration of registrations) {
|
|
326
|
-
if (!registration.options.enabled || registration.options.eventType !== eventType || !isEventForTarget(event, target) || registration
|
|
305
|
+
if (!registration.options.enabled || registration.options.eventType !== eventType || !isEventForTarget(event, target) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target, registration.target)) continue;
|
|
327
306
|
const result = matchKeyboardEvent(event, registration.parsedHotkey, registration.options.platform);
|
|
328
307
|
if (result.matched) {
|
|
329
308
|
matches.set(registration.id, result);
|
|
330
309
|
bestScore = Math.max(bestScore, result.score);
|
|
331
310
|
}
|
|
332
311
|
}
|
|
333
|
-
for (const
|
|
334
|
-
const match = matches.get(
|
|
312
|
+
for (const snapshot of registrations) {
|
|
313
|
+
const match = matches.get(snapshot.id);
|
|
335
314
|
if (!match?.matched || match.score !== bestScore) continue;
|
|
315
|
+
const registration = this.#currentIn(snapshot, capture);
|
|
316
|
+
if (!registration) continue;
|
|
336
317
|
if (eventType === "keyup") {
|
|
337
318
|
this.#executeCallback(registration, event);
|
|
338
319
|
continue;
|
|
@@ -340,16 +321,15 @@ var HotkeyManager = class HotkeyManager {
|
|
|
340
321
|
if (registration.options.preventDefault) event.preventDefault();
|
|
341
322
|
if (registration.options.stopPropagation) event.stopPropagation();
|
|
342
323
|
if (!registration.options.requireReset || !registration.hasFired) {
|
|
343
|
-
this.#executeCallback(registration, event);
|
|
344
324
|
if (registration.options.requireReset) {
|
|
345
325
|
registration.hasFired = true;
|
|
346
326
|
registration.activeMatch = match.identity;
|
|
347
327
|
}
|
|
328
|
+
this.#executeCallback(registration, event);
|
|
348
329
|
}
|
|
349
330
|
}
|
|
350
331
|
}
|
|
351
|
-
#processSequences(event, target, eventType) {
|
|
352
|
-
const registrations = this.#registrationsOn(target, "sequence");
|
|
332
|
+
#processSequences(event, target, eventType, capture, registrations) {
|
|
353
333
|
if (registrations.length === 0) return;
|
|
354
334
|
if (isRecordingEvent(event)) {
|
|
355
335
|
let changed = false;
|
|
@@ -367,7 +347,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
367
347
|
const candidates = /* @__PURE__ */ new Map();
|
|
368
348
|
let bestScore = 0;
|
|
369
349
|
for (const registration of registrations) {
|
|
370
|
-
if (!isEventForTarget(event, target) || !registration.options.enabled || registration
|
|
350
|
+
if (!isEventForTarget(event, target) || !registration.options.enabled || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target, registration.target) || registration.options.eventType !== eventType) continue;
|
|
371
351
|
const timeout = registration.options.timeout ?? 1e3;
|
|
372
352
|
if (registration.currentIndex > 0 && now - registration.lastKeyTime > timeout) {
|
|
373
353
|
registration.currentIndex = 0;
|
|
@@ -407,7 +387,10 @@ var HotkeyManager = class HotkeyManager {
|
|
|
407
387
|
}
|
|
408
388
|
}
|
|
409
389
|
if (changed && completed.length === 0) this.#notify();
|
|
410
|
-
for (const
|
|
390
|
+
for (const snapshot of completed) {
|
|
391
|
+
const registration = this.#currentIn(snapshot, capture);
|
|
392
|
+
if (registration) this.#executeCallback(registration, event);
|
|
393
|
+
}
|
|
411
394
|
}
|
|
412
395
|
/**
|
|
413
396
|
* Executes a hotkey callback with proper event handling.
|
|
@@ -514,11 +497,9 @@ var HotkeyManager = class HotkeyManager {
|
|
|
514
497
|
* Destroys the manager and removes all listeners.
|
|
515
498
|
*/
|
|
516
499
|
destroy() {
|
|
517
|
-
|
|
500
|
+
this.#listeners.destroy();
|
|
518
501
|
this.registrations.setState(() => /* @__PURE__ */ new Map());
|
|
519
502
|
this.appHotkeys.setState(() => null);
|
|
520
|
-
this.#targetListeners.clear();
|
|
521
|
-
this.#targetRegistrations.clear();
|
|
522
503
|
}
|
|
523
504
|
};
|
|
524
505
|
/**
|
package/dist/hotkey-recorder.js
CHANGED
|
@@ -2,8 +2,8 @@ import { detectPlatform } from "./platform.js";
|
|
|
2
2
|
import { normalizeKeyboardEvent } from "./_keyboard-event.js";
|
|
3
3
|
import { isModifierKey, parseHotkey, simplifyHotkey } from "./parse.js";
|
|
4
4
|
import { validateHotkey } from "./validate.js";
|
|
5
|
+
import { isTypingEvent } from "./_event-target.js";
|
|
5
6
|
import { beginRecording, captureRecordingEvent, endRecording } from "./_recording-guard.js";
|
|
6
|
-
import { shouldIgnoreInputEvent } from "./_event-target.js";
|
|
7
7
|
import { findHotkeyConflicts } from "./conflicts.js";
|
|
8
8
|
import { chordRejection, hotkeyChordFromKeydown } from "./_recorder-chord.js";
|
|
9
9
|
import { Store } from "@tanstack/store";
|
|
@@ -111,7 +111,7 @@ var HotkeyRecorder = class {
|
|
|
111
111
|
}));
|
|
112
112
|
const handler = (event) => {
|
|
113
113
|
if (!this.#keydownHandler) return;
|
|
114
|
-
if (this.#options.ignoreInputs !== false && event.key !== "Escape" &&
|
|
114
|
+
if (this.#options.ignoreInputs !== false && event.key !== "Escape" && isTypingEvent(event, document)) return;
|
|
115
115
|
const platform = this.#options.platform ?? this.#platform;
|
|
116
116
|
if (normalizeKeyboardEvent(event, platform).isComposing) return;
|
|
117
117
|
captureRecordingEvent(event);
|
|
@@ -55,6 +55,7 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
55
55
|
#heldEntries = /* @__PURE__ */ new Map();
|
|
56
56
|
#keydownListener = null;
|
|
57
57
|
#keyupListener = null;
|
|
58
|
+
#mouseListener = null;
|
|
58
59
|
#blurListener = null;
|
|
59
60
|
constructor() {
|
|
60
61
|
this.#setupListeners();
|
|
@@ -81,6 +82,7 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
81
82
|
#setupListeners() {
|
|
82
83
|
if (typeof document === "undefined") return;
|
|
83
84
|
this.#keydownListener = (event) => {
|
|
85
|
+
let changed = this.#reconcileModifiers(event);
|
|
84
86
|
const key = normalizeKeyName(event.key);
|
|
85
87
|
const identity = event.code || `key:${key}`;
|
|
86
88
|
if (!this.#heldEntries.has(identity)) {
|
|
@@ -88,8 +90,9 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
88
90
|
key,
|
|
89
91
|
code: event.code
|
|
90
92
|
});
|
|
91
|
-
|
|
93
|
+
changed = true;
|
|
92
94
|
}
|
|
95
|
+
if (changed) this.#syncState();
|
|
93
96
|
};
|
|
94
97
|
this.#keyupListener = (event) => {
|
|
95
98
|
const key = normalizeKeyName(event.key);
|
|
@@ -98,22 +101,44 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
98
101
|
if (!event.code) {
|
|
99
102
|
for (const [entryIdentity, entry] of this.#heldEntries) if (entry.key === key) this.#heldEntries.delete(entryIdentity);
|
|
100
103
|
}
|
|
104
|
+
this.#reconcileModifiers(event);
|
|
101
105
|
if (MODIFIER_KEYS.has(key)) {
|
|
102
106
|
for (const [entryIdentity, entry] of this.#heldEntries) if (!MODIFIER_KEYS.has(entry.key)) this.#heldEntries.delete(entryIdentity);
|
|
103
107
|
}
|
|
104
108
|
this.#syncState();
|
|
105
109
|
};
|
|
110
|
+
this.#mouseListener = (event) => {
|
|
111
|
+
if (this.#reconcileModifiers(event)) this.#syncState();
|
|
112
|
+
};
|
|
106
113
|
this.#blurListener = () => {
|
|
107
114
|
if (this.#heldEntries.size > 0) {
|
|
108
115
|
this.#heldEntries.clear();
|
|
109
116
|
this.#syncState();
|
|
110
117
|
}
|
|
111
118
|
};
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
window.addEventListener("keydown", this.#keydownListener, true);
|
|
120
|
+
window.addEventListener("keyup", this.#keyupListener, true);
|
|
121
|
+
window.addEventListener("mousemove", this.#mouseListener, true);
|
|
122
|
+
window.addEventListener("mousedown", this.#mouseListener, true);
|
|
114
123
|
window.addEventListener("blur", this.#blurListener);
|
|
115
124
|
}
|
|
116
125
|
/**
|
|
126
|
+
* Removes modifiers whose release was missed without a window blur.
|
|
127
|
+
* Event modifier state is shared by left/right keys, so a true flag keeps
|
|
128
|
+
* both physical entries until their individual keyup events arrive.
|
|
129
|
+
* Only real user input counts: events dispatched by page code report no
|
|
130
|
+
* modifiers and would clear keys the user is still holding.
|
|
131
|
+
*/
|
|
132
|
+
#reconcileModifiers(event) {
|
|
133
|
+
if (!event.isTrusted) return false;
|
|
134
|
+
let changed = false;
|
|
135
|
+
for (const [identity, entry] of this.#heldEntries) if (entry.key === "Control" && !event.ctrlKey || entry.key === "Alt" && !event.altKey || entry.key === "Shift" && !event.shiftKey || entry.key === "Meta" && !event.metaKey) {
|
|
136
|
+
this.#heldEntries.delete(identity);
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
return changed;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
117
142
|
* Syncs the internal Set to the Store state.
|
|
118
143
|
*/
|
|
119
144
|
#syncState() {
|
|
@@ -130,13 +155,18 @@ var KeyStateTracker = class KeyStateTracker {
|
|
|
130
155
|
#removeListeners() {
|
|
131
156
|
if (typeof document === "undefined") return;
|
|
132
157
|
if (this.#keydownListener) {
|
|
133
|
-
|
|
158
|
+
window.removeEventListener("keydown", this.#keydownListener, true);
|
|
134
159
|
this.#keydownListener = null;
|
|
135
160
|
}
|
|
136
161
|
if (this.#keyupListener) {
|
|
137
|
-
|
|
162
|
+
window.removeEventListener("keyup", this.#keyupListener, true);
|
|
138
163
|
this.#keyupListener = null;
|
|
139
164
|
}
|
|
165
|
+
if (this.#mouseListener) {
|
|
166
|
+
window.removeEventListener("mousemove", this.#mouseListener, true);
|
|
167
|
+
window.removeEventListener("mousedown", this.#mouseListener, true);
|
|
168
|
+
this.#mouseListener = null;
|
|
169
|
+
}
|
|
140
170
|
if (this.#blurListener) {
|
|
141
171
|
window.removeEventListener("blur", this.#blurListener);
|
|
142
172
|
this.#blurListener = null;
|