@tanstack/hotkeys 0.18.0 → 0.20.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/_app-hotkeys-factory.d.ts +5 -0
- package/dist/_app-hotkeys-factory.js +13 -0
- package/dist/_event-target.js +202 -124
- package/dist/_keyboard-event.js +13 -5
- package/dist/_match.js +23 -1
- package/dist/_named-keys.js +21 -1
- package/dist/_registration.js +1 -1
- package/dist/_store.d.ts +14 -0
- package/dist/_store.js +43 -0
- package/dist/adapter.d.ts +4 -1
- package/dist/adapter.js +3 -1
- package/dist/app-hotkeys.d.ts +1 -1
- package/dist/app-hotkeys.js +12 -9
- package/dist/constants.d.ts +8 -7
- package/dist/constants.js +20 -158
- package/dist/format.d.ts +16 -3
- package/dist/format.js +19 -14
- package/dist/hotkey-manager.d.ts +1 -10
- package/dist/hotkey-manager.js +15 -36
- package/dist/hotkey-recorder.d.ts +1 -1
- package/dist/hotkey-recorder.js +6 -8
- package/dist/key-state-tracker.d.ts +1 -1
- package/dist/key-state-tracker.js +2 -2
- package/dist/validate.d.ts +2 -2
- package/dist/validate.js +8 -11
- package/dist/vendor/@tanstack/store/dist/store.d.ts +23 -0
- package/dist/vendor/@tanstack/store/dist/types.d.ts +11 -0
- package/package.json +5 -2
- package/skills/hotkeys-core/SKILL.md +2 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { AppHotkeys, AppHotkeysStore, AppHotkeysStoreOptions } from "./app-hotkeys.js";
|
|
2
|
+
//#region src/_app-hotkeys-factory.d.ts
|
|
3
|
+
/** Creates the store for app hotkeys from `defineAppHotkeys`. */
|
|
4
|
+
export declare function createAppHotkeysStore<T extends AppHotkeys>(appHotkeys: T, options: AppHotkeysStoreOptions<T>): AppHotkeysStore<T>;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/_app-hotkeys-factory.ts
|
|
2
|
+
let factory;
|
|
3
|
+
function setAppHotkeysStoreFactory(create) {
|
|
4
|
+
factory = create;
|
|
5
|
+
}
|
|
6
|
+
/** Creates the store for app hotkeys from `defineAppHotkeys`. */
|
|
7
|
+
function createAppHotkeysStore(appHotkeys, options) {
|
|
8
|
+
if (!factory) throw new Error("[hotkeys] App hotkeys must come from defineAppHotkeys.");
|
|
9
|
+
return factory(appHotkeys, options);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { createAppHotkeysStore, setAppHotkeysStoreFactory };
|
package/dist/_event-target.js
CHANGED
|
@@ -1,78 +1,169 @@
|
|
|
1
1
|
import { normalizeKeyName } from "./constants.js";
|
|
2
2
|
|
|
3
3
|
//#region src/_event-target.ts
|
|
4
|
-
function
|
|
5
|
-
return
|
|
4
|
+
function isElement(node) {
|
|
5
|
+
return node?.nodeType === 1;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function isWindow(target) {
|
|
11
|
-
return "document" in target;
|
|
7
|
+
/** Whether a target is a document or window, which receive every key press in their document. */
|
|
8
|
+
function isDocumentLevel(target) {
|
|
9
|
+
return "document" in target || target.nodeType === 9;
|
|
12
10
|
}
|
|
13
11
|
/** Returns the document a listener target belongs to, in any frame. */
|
|
14
12
|
function getTargetDocument(target) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
return "document" in target ? target.document : target.ownerDocument ?? target;
|
|
14
|
+
}
|
|
15
|
+
/** The focused element, looking inside open shadow roots. */
|
|
16
|
+
function getDeepActiveElement(doc) {
|
|
17
|
+
let active = doc.activeElement;
|
|
18
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
19
|
+
return active;
|
|
20
|
+
}
|
|
21
|
+
/** The next node a key event bubbles to: a slot, a shadow host, or the parent. */
|
|
22
|
+
function composedParent(node) {
|
|
23
|
+
return node.assignedSlot ?? node.parentNode ?? (node.host || null);
|
|
18
24
|
}
|
|
19
25
|
/**
|
|
20
26
|
* Whether key presses reach a listener on `target` right now. Document and
|
|
21
27
|
* window listeners receive every key press in their document. An element
|
|
22
28
|
* receives them only while focus is inside it, including inside a shadow
|
|
23
|
-
* root it contains.
|
|
29
|
+
* root it contains or on content slotted into it.
|
|
24
30
|
*/
|
|
25
31
|
function targetHasFocus(target) {
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
return
|
|
32
|
+
if (isDocumentLevel(target)) return true;
|
|
33
|
+
for (let node = getDeepActiveElement(target.ownerDocument); node; node = composedParent(node)) if (node === target) return true;
|
|
34
|
+
return false;
|
|
29
35
|
}
|
|
36
|
+
const VERTICAL = ["ArrowUp", "ArrowDown"];
|
|
37
|
+
const HORIZONTAL = ["ArrowLeft", "ArrowRight"];
|
|
38
|
+
const ENTER = ["Enter"];
|
|
39
|
+
const SPACE = ["Space"];
|
|
40
|
+
const ACTIVATION = ["Enter", "Space"];
|
|
41
|
+
const TYPEAHEAD = "typeahead";
|
|
42
|
+
const MENU = [
|
|
43
|
+
"ArrowUp",
|
|
44
|
+
"ArrowDown",
|
|
45
|
+
"ArrowLeft",
|
|
46
|
+
"ArrowRight",
|
|
47
|
+
"Home",
|
|
48
|
+
"End",
|
|
49
|
+
"Enter",
|
|
50
|
+
"Space",
|
|
51
|
+
TYPEAHEAD
|
|
52
|
+
];
|
|
53
|
+
const COMBOBOX = [
|
|
54
|
+
"ArrowUp",
|
|
55
|
+
"ArrowDown",
|
|
56
|
+
"Home",
|
|
57
|
+
"End",
|
|
58
|
+
"Enter",
|
|
59
|
+
"Space",
|
|
60
|
+
TYPEAHEAD
|
|
61
|
+
];
|
|
62
|
+
const GRID = [
|
|
63
|
+
"ArrowUp",
|
|
64
|
+
"ArrowDown",
|
|
65
|
+
"ArrowLeft",
|
|
66
|
+
"ArrowRight",
|
|
67
|
+
"Home",
|
|
68
|
+
"End",
|
|
69
|
+
"PageUp",
|
|
70
|
+
"PageDown",
|
|
71
|
+
"Enter",
|
|
72
|
+
"Space"
|
|
73
|
+
];
|
|
74
|
+
const RADIOS = [
|
|
75
|
+
"ArrowUp",
|
|
76
|
+
"ArrowDown",
|
|
77
|
+
"ArrowLeft",
|
|
78
|
+
"ArrowRight",
|
|
79
|
+
"Space"
|
|
80
|
+
];
|
|
81
|
+
const NAVIGATION = [
|
|
82
|
+
"ArrowUp",
|
|
83
|
+
"ArrowDown",
|
|
84
|
+
"ArrowLeft",
|
|
85
|
+
"ArrowRight",
|
|
86
|
+
"Home",
|
|
87
|
+
"End",
|
|
88
|
+
"PageUp",
|
|
89
|
+
"PageDown"
|
|
90
|
+
];
|
|
91
|
+
const SPIN = [
|
|
92
|
+
"ArrowUp",
|
|
93
|
+
"ArrowDown",
|
|
94
|
+
"Home",
|
|
95
|
+
"End",
|
|
96
|
+
"PageUp",
|
|
97
|
+
"PageDown"
|
|
98
|
+
];
|
|
99
|
+
const CARET = [
|
|
100
|
+
"ArrowLeft",
|
|
101
|
+
"ArrowRight",
|
|
102
|
+
"Home",
|
|
103
|
+
"End"
|
|
104
|
+
];
|
|
30
105
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
106
|
+
* Input types that aren't text fields, with the keys each one keeps. Browsers
|
|
107
|
+
* report unknown types as `'text'`.
|
|
108
|
+
*/
|
|
109
|
+
const INPUT_KEYS = {
|
|
110
|
+
button: ACTIVATION,
|
|
111
|
+
submit: ACTIVATION,
|
|
112
|
+
reset: ACTIVATION,
|
|
113
|
+
image: ACTIVATION,
|
|
114
|
+
color: ACTIVATION,
|
|
115
|
+
file: ACTIVATION,
|
|
116
|
+
checkbox: SPACE,
|
|
117
|
+
radio: RADIOS,
|
|
118
|
+
range: NAVIGATION
|
|
119
|
+
};
|
|
120
|
+
function getRole(element) {
|
|
121
|
+
return element.getAttribute("role")?.trim().split(/\s+/)[0];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Checks if an element is a text field that hotkeys should not type over:
|
|
125
|
+
* - text-entry `<input>` types (text, number, date, and similar) that aren't read-only
|
|
126
|
+
* - `<textarea>` elements that aren't read-only, and `<select>` elements
|
|
127
|
+
* - elements with `role="textbox"` or `role="searchbox"`
|
|
128
|
+
* - contenteditable elements
|
|
38
129
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
130
|
+
* Checkbox, radio, range, color, file, and button-type inputs are not text
|
|
131
|
+
* fields. Hotkeys skip only the keys those controls use themselves.
|
|
41
132
|
*/
|
|
42
133
|
function isInputElement(element) {
|
|
43
134
|
if (!isElement(element)) return false;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
135
|
+
switch (element.localName) {
|
|
136
|
+
case "input": {
|
|
137
|
+
const input = element;
|
|
138
|
+
return !INPUT_KEYS[input.type] && !input.readOnly;
|
|
139
|
+
}
|
|
140
|
+
case "textarea": return !element.readOnly;
|
|
141
|
+
case "select": return true;
|
|
47
142
|
}
|
|
48
|
-
|
|
49
|
-
return element.isContentEditable
|
|
143
|
+
const role = getRole(element);
|
|
144
|
+
return role === "textbox" || role === "searchbox" || element.isContentEditable;
|
|
50
145
|
}
|
|
51
146
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
147
|
+
* Whether the focused custom element may be a text field whose contents can't
|
|
148
|
+
* be seen from outside:
|
|
149
|
+
* - a host with focus inside a closed shadow root. Without a `tabindex` it
|
|
150
|
+
* can't hold focus itself, and an open root would have shown the focused
|
|
151
|
+
* element inside it.
|
|
152
|
+
* - a form-associated element without a `role` attribute naming another control
|
|
56
153
|
*/
|
|
57
|
-
function
|
|
58
|
-
if (
|
|
59
|
-
return
|
|
154
|
+
function isHiddenTextField(element) {
|
|
155
|
+
if (!element.localName.includes("-")) return false;
|
|
156
|
+
return element.constructor.formAssociated === true ? !element.hasAttribute("role") : !element.hasAttribute("tabindex");
|
|
60
157
|
}
|
|
61
|
-
const VERTICAL = ["ArrowUp", "ArrowDown"];
|
|
62
|
-
const HORIZONTAL = ["ArrowLeft", "ArrowRight"];
|
|
63
|
-
const ARROWS = [...VERTICAL, ...HORIZONTAL];
|
|
64
|
-
const EDGES = ["Home", "End"];
|
|
65
|
-
const PAGES = ["PageUp", "PageDown"];
|
|
66
|
-
const ACTIVATION = ["Enter", "Space"];
|
|
67
|
-
const TYPEAHEAD = "typeahead";
|
|
68
158
|
/**
|
|
69
159
|
* Returns the keys a control handles itself: activation for buttons and
|
|
70
160
|
* links, and navigation for ARIA widgets, following the WAI-ARIA Authoring
|
|
71
161
|
* Practices keyboard patterns (https://www.w3.org/WAI/ARIA/apg/patterns/).
|
|
162
|
+
* Editable text fields never get here: they block every key that types text.
|
|
72
163
|
*/
|
|
73
164
|
function getOwnedKeys(element) {
|
|
74
165
|
const orientation = element.getAttribute("aria-orientation");
|
|
75
|
-
switch (element
|
|
166
|
+
switch (getRole(element)) {
|
|
76
167
|
case "button":
|
|
77
168
|
case "tab":
|
|
78
169
|
case "option":
|
|
@@ -80,64 +171,47 @@ function getOwnedKeys(element) {
|
|
|
80
171
|
case "menuitem":
|
|
81
172
|
case "menuitemcheckbox":
|
|
82
173
|
case "menuitemradio": return ACTIVATION;
|
|
83
|
-
case "link": return
|
|
174
|
+
case "link": return ENTER;
|
|
84
175
|
case "checkbox":
|
|
85
176
|
case "switch":
|
|
86
|
-
case "radio": return
|
|
177
|
+
case "radio": return SPACE;
|
|
87
178
|
case "menu":
|
|
88
179
|
case "menubar":
|
|
89
|
-
case "tree": return
|
|
90
|
-
...ARROWS,
|
|
91
|
-
...EDGES,
|
|
92
|
-
...ACTIVATION,
|
|
93
|
-
TYPEAHEAD
|
|
94
|
-
];
|
|
180
|
+
case "tree": return MENU;
|
|
95
181
|
case "listbox": return [
|
|
96
182
|
...orientation === "horizontal" ? HORIZONTAL : VERTICAL,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
case "combobox": return [
|
|
102
|
-
...VERTICAL,
|
|
103
|
-
...EDGES,
|
|
104
|
-
...ACTIVATION,
|
|
183
|
+
"Home",
|
|
184
|
+
"End",
|
|
185
|
+
"Enter",
|
|
186
|
+
"Space",
|
|
105
187
|
TYPEAHEAD
|
|
106
188
|
];
|
|
189
|
+
case "combobox": return COMBOBOX;
|
|
107
190
|
case "tablist": return [
|
|
108
191
|
...orientation === "vertical" ? VERTICAL : HORIZONTAL,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
case "grid":
|
|
114
|
-
case "treegrid": return [
|
|
115
|
-
...ARROWS,
|
|
116
|
-
...EDGES,
|
|
117
|
-
...PAGES,
|
|
118
|
-
...ACTIVATION
|
|
192
|
+
"Home",
|
|
193
|
+
"End",
|
|
194
|
+
"Enter",
|
|
195
|
+
"Space"
|
|
119
196
|
];
|
|
120
|
-
case "
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
...PAGES
|
|
125
|
-
];
|
|
126
|
-
case "spinbutton": return [
|
|
127
|
-
...VERTICAL,
|
|
128
|
-
...EDGES,
|
|
129
|
-
...PAGES
|
|
197
|
+
case "toolbar": return [
|
|
198
|
+
...orientation === "vertical" ? VERTICAL : HORIZONTAL,
|
|
199
|
+
"Home",
|
|
200
|
+
"End"
|
|
130
201
|
];
|
|
202
|
+
case "grid":
|
|
203
|
+
case "treegrid": return GRID;
|
|
204
|
+
case "radiogroup": return RADIOS;
|
|
205
|
+
case "slider": return NAVIGATION;
|
|
206
|
+
case "spinbutton": return SPIN;
|
|
131
207
|
}
|
|
132
208
|
switch (element.localName) {
|
|
133
209
|
case "button":
|
|
134
210
|
case "summary": return ACTIVATION;
|
|
135
|
-
case "input":
|
|
136
|
-
|
|
137
|
-
return type === "button" || type === "submit" || type === "reset" ? ACTIVATION : [];
|
|
138
|
-
}
|
|
211
|
+
case "input": return INPUT_KEYS[element.type] ?? CARET;
|
|
212
|
+
case "textarea": return NAVIGATION;
|
|
139
213
|
case "a":
|
|
140
|
-
case "area": return element.hasAttribute("href") ?
|
|
214
|
+
case "area": return element.hasAttribute("href") ? ENTER : [];
|
|
141
215
|
}
|
|
142
216
|
return [];
|
|
143
217
|
}
|
|
@@ -150,56 +224,60 @@ function ownsKey(element, event, key) {
|
|
|
150
224
|
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
151
225
|
const owned = getOwnedKeys(element);
|
|
152
226
|
if (key === "Enter" || key === "Space") return !event.shiftKey && owned.includes(key);
|
|
153
|
-
return owned.includes(key) || owned.includes(TYPEAHEAD) &&
|
|
227
|
+
return owned.includes(key) || owned.includes(TYPEAHEAD) && /^.$/u.test(event.key);
|
|
154
228
|
}
|
|
155
229
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
230
|
+
* Finds the first element on the event's way out that blocks it, walking out
|
|
231
|
+
* from the focused element. A key event starts there in a browser. One sent
|
|
232
|
+
* by a test may start at an ancestor of the focused element or at the
|
|
233
|
+
* document, and the walk from focus passes through those too. Only an event
|
|
234
|
+
* that starts where focus can't be seen, such as inside a closed shadow root,
|
|
235
|
+
* uses its own path.
|
|
160
236
|
*/
|
|
161
|
-
function
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
237
|
+
function findBlocker(event, doc, test) {
|
|
238
|
+
const focused = getDeepActiveElement(doc);
|
|
239
|
+
let elements = [];
|
|
240
|
+
for (let node = focused; node; node = composedParent(node)) if (isElement(node)) elements.push(node);
|
|
241
|
+
const origin = event.composedPath()[0];
|
|
242
|
+
if (isElement(origin) && !elements.includes(origin)) elements = event.composedPath().filter(isElement);
|
|
243
|
+
const index = elements.findIndex((element) => test(element) || element === focused && isHiddenTextField(element));
|
|
244
|
+
return {
|
|
245
|
+
elements,
|
|
246
|
+
index
|
|
247
|
+
};
|
|
171
248
|
}
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
* - text inputs, textareas, selects, and contenteditable elements (typing)
|
|
176
|
-
* - buttons, links, and `<summary>` elements (Enter and Space activation)
|
|
177
|
-
* - ARIA widgets such as menus, listboxes, tabs, and grids (navigation keys)
|
|
178
|
-
*/
|
|
179
|
-
function shouldIgnoreInputEvent(event, listenerTarget, registrationTarget) {
|
|
180
|
-
const key = normalizeKeyName(event.key);
|
|
181
|
-
return someElementInsideTarget(event, listenerTarget, registrationTarget, (element) => isInputElement(element) || ownsKey(element, event, key));
|
|
249
|
+
/** Whether the blocking element is inside `target`. A target never blocks itself. */
|
|
250
|
+
function blocks({ elements, index }, target) {
|
|
251
|
+
return index >= 0 && (isDocumentLevel(target) || index < elements.indexOf(target));
|
|
182
252
|
}
|
|
253
|
+
const ignoreCache = /* @__PURE__ */ new WeakMap();
|
|
183
254
|
/**
|
|
184
|
-
* Returns whether
|
|
185
|
-
*
|
|
186
|
-
*
|
|
255
|
+
* Returns whether a hotkey on `target` should skip an event because a control
|
|
256
|
+
* inside the target handles the key itself:
|
|
257
|
+
* - text fields, including ARIA text roles and custom elements that hide one (typing)
|
|
258
|
+
* - buttons, links, `<summary>`, and button-like inputs (Enter and Space activation)
|
|
259
|
+
* - checkbox, radio, and range inputs, and ARIA widgets such as menus,
|
|
260
|
+
* listboxes, tabs, and grids (the keys they use)
|
|
261
|
+
*
|
|
262
|
+
* A hotkey targeted at a control receives that control's keys.
|
|
187
263
|
*/
|
|
188
|
-
function
|
|
189
|
-
|
|
264
|
+
function shouldIgnoreInputEvent(event, target) {
|
|
265
|
+
let blocker = ignoreCache.get(event);
|
|
266
|
+
if (!blocker) {
|
|
267
|
+
const key = normalizeKeyName(event.key);
|
|
268
|
+
blocker = findBlocker(event, getTargetDocument(target), (element) => isInputElement(element) || ownsKey(element, event, key));
|
|
269
|
+
ignoreCache.set(event, blocker);
|
|
270
|
+
}
|
|
271
|
+
return blocks(blocker, target);
|
|
190
272
|
}
|
|
191
273
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* currentTarget to handle Brave and other browsers where currentTarget may be
|
|
196
|
-
* documentElement instead of document when listeners are attached to document.
|
|
274
|
+
* Returns whether an event comes from a text field inside `target`, as
|
|
275
|
+
* `isInputElement` and hidden custom text fields define them. Buttons, links,
|
|
276
|
+
* and other controls don't count.
|
|
197
277
|
*/
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
if (isWindow(target) || target.nodeType === 9) return event.currentTarget === getTargetDocument(target).documentElement;
|
|
201
|
-
return isNode(event.target) && target.contains(event.target);
|
|
278
|
+
function isTypingEvent(event, target) {
|
|
279
|
+
return blocks(findBlocker(event, getTargetDocument(target), isInputElement), target);
|
|
202
280
|
}
|
|
203
281
|
|
|
204
282
|
//#endregion
|
|
205
|
-
export {
|
|
283
|
+
export { getTargetDocument, isDocumentLevel, isInputElement, isTypingEvent, shouldIgnoreInputEvent, targetHasFocus };
|
package/dist/_keyboard-event.js
CHANGED
|
@@ -34,9 +34,8 @@ function codeToLogicalKey(code) {
|
|
|
34
34
|
if (/^Key[A-Z]$/i.test(code)) return code.slice(3).toUpperCase();
|
|
35
35
|
if (/^Digit[0-9]$/i.test(code)) return code.slice(5);
|
|
36
36
|
if (/^Numpad[0-9]$/i.test(code)) return code.slice(6);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return {
|
|
37
|
+
if (Object.hasOwn(PUNCTUATION_CODE_MAP, code)) return PUNCTUATION_CODE_MAP[code];
|
|
38
|
+
const named = {
|
|
40
39
|
NumpadAdd: "+",
|
|
41
40
|
NumpadSubtract: "-",
|
|
42
41
|
NumpadMultiply: "*",
|
|
@@ -44,7 +43,8 @@ function codeToLogicalKey(code) {
|
|
|
44
43
|
NumpadDecimal: ".",
|
|
45
44
|
NumpadEnter: "Enter",
|
|
46
45
|
Space: "Space"
|
|
47
|
-
}
|
|
46
|
+
};
|
|
47
|
+
return Object.hasOwn(named, code) ? named[code] : void 0;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Whether AltGr changed what a key typed, as in AltGr+Q typing `@`. False when
|
|
@@ -102,6 +102,14 @@ function keysEqual(left, right) {
|
|
|
102
102
|
function assertLogicalKey(key) {
|
|
103
103
|
if (/^\[.*\]$/s.test(key.trim())) throw new Error("Logical key must not contain a bracketed physical code. Use the code field instead.");
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether a key is written like a key the library knows: one character,
|
|
107
|
+
* F1-F24, or a capitalized name such as `Escape`. Key names are
|
|
108
|
+
* case-sensitive, so `escape` and `f1` don't qualify.
|
|
109
|
+
*/
|
|
110
|
+
function isKeyNameShape(key) {
|
|
111
|
+
return /^(?:.|F(?:[1-9]|1\d|2[0-4])|[A-Z][a-z][A-Za-z\d]*)$/su.test(key);
|
|
112
|
+
}
|
|
105
113
|
|
|
106
114
|
//#endregion
|
|
107
|
-
export { altGraphTypedCharacter, assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts };
|
|
115
|
+
export { altGraphTypedCharacter, assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isKeyNameShape, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts };
|
package/dist/_match.js
CHANGED
|
@@ -22,7 +22,7 @@ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
|
|
|
22
22
|
const direct = keysEqual(normalized.key, parsed.key);
|
|
23
23
|
const implicitShift = direct && isPrintableKey(normalized.key) && !/^\p{Letter}$/u.test(normalized.key) && !parsed.shift;
|
|
24
24
|
if (direct && modifiersMatch(normalized, parsed, implicitShift)) return matched("key", implicitShift ? 2 : 3);
|
|
25
|
-
if (
|
|
25
|
+
if (!positionStandsIn(normalized)) return noMatch;
|
|
26
26
|
if (normalized.altGraph) return noMatch;
|
|
27
27
|
if (!modifiersMatch(normalized, parsed, false)) return noMatch;
|
|
28
28
|
const fallbackKey = codeToLogicalKey(normalized.code);
|
|
@@ -39,6 +39,28 @@ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a key's conventional position may stand in for what it typed. Only
|
|
44
|
+
* output that can't name a shortcut falls back:
|
|
45
|
+
* - non-Latin letters and symbols, such as Russian `ы` or macOS Option's `–`
|
|
46
|
+
* - dead keys
|
|
47
|
+
* - anything on the digit row, where AZERTY types `&` and `é` for 1 and 2
|
|
48
|
+
* - ASCII symbols typed with Shift, such as `!` for Shift+1
|
|
49
|
+
* - Latin letters on punctuation keys with Control or Command, so German
|
|
50
|
+
* Cmd+ü still fires `Mod+[` (German types `[` with Option)
|
|
51
|
+
*
|
|
52
|
+
* A named key (`End` from a NumLock-off numpad), any other ASCII character,
|
|
53
|
+
* or a Latin letter typed on its own is the key the user meant, wherever it
|
|
54
|
+
* sits. AZERTY's `+` on the US `/` position is `+`, not `/`.
|
|
55
|
+
*/
|
|
56
|
+
function positionStandsIn({ key, code, ctrl, meta, shift, alt }) {
|
|
57
|
+
if (key !== "Dead" && !isPrintableKey(key)) return false;
|
|
58
|
+
if (code.startsWith("Digit")) return true;
|
|
59
|
+
if (isAsciiLetter(key)) return false;
|
|
60
|
+
if (/^[!-~]$/.test(key)) return shift;
|
|
61
|
+
if (!isLatinLetter(key) || alt) return true;
|
|
62
|
+
return (ctrl || meta) && !code.startsWith("Key");
|
|
63
|
+
}
|
|
42
64
|
/** Compares modifiers, excluding synthetic AltGr flags and optionally allowing Shift used to type a glyph. */
|
|
43
65
|
function modifiersMatch(event, parsed, allowImplicitShift) {
|
|
44
66
|
const ctrl = event.altGraph ? false : event.ctrl;
|
package/dist/_named-keys.js
CHANGED
|
@@ -60,6 +60,26 @@ const LOGICAL_ONLY_NAMED_KEYS = [
|
|
|
60
60
|
"Hankaku",
|
|
61
61
|
"ZenkakuHankaku"
|
|
62
62
|
];
|
|
63
|
+
/** Arrow, Home/End, and PageUp/PageDown keys. */
|
|
64
|
+
const NAVIGATION_KEY_NAMES = [
|
|
65
|
+
"ArrowUp",
|
|
66
|
+
"ArrowDown",
|
|
67
|
+
"ArrowLeft",
|
|
68
|
+
"ArrowRight",
|
|
69
|
+
"Home",
|
|
70
|
+
"End",
|
|
71
|
+
"PageUp",
|
|
72
|
+
"PageDown"
|
|
73
|
+
];
|
|
74
|
+
/** Editing and special keys. */
|
|
75
|
+
const EDITING_KEY_NAMES = [
|
|
76
|
+
"Enter",
|
|
77
|
+
"Escape",
|
|
78
|
+
"Space",
|
|
79
|
+
"Tab",
|
|
80
|
+
"Backspace",
|
|
81
|
+
"Delete"
|
|
82
|
+
];
|
|
63
83
|
|
|
64
84
|
//#endregion
|
|
65
|
-
export { LOGICAL_ONLY_NAMED_KEYS, SHARED_NAMED_KEYS };
|
|
85
|
+
export { EDITING_KEY_NAMES, LOGICAL_ONLY_NAMED_KEYS, NAVIGATION_KEY_NAMES, SHARED_NAMED_KEYS };
|
package/dist/_registration.js
CHANGED
|
@@ -35,7 +35,7 @@ function getDefaultIgnoreInputs(parsedChord) {
|
|
|
35
35
|
function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
|
|
36
36
|
if (conflictBehavior === "allow") return;
|
|
37
37
|
if (conflictBehavior === "warn") {
|
|
38
|
-
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.`);
|
|
38
|
+
if (process.env.NODE_ENV !== "production") 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.`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
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.`);
|
package/dist/_store.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Store as Store$1 } from "./vendor/@tanstack/store/dist/store.js";
|
|
2
|
+
//#region src/_store.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The part of TanStack Store's `Store` that TanStack Hotkeys uses. Its stores
|
|
5
|
+
* work with TanStack Store's framework adapters, such as `useSelector` from
|
|
6
|
+
* `@tanstack/react-store`.
|
|
7
|
+
*/
|
|
8
|
+
export type Store<T> = Pick<Store$1<T>, 'state' | 'get' | 'setState' | 'subscribe'>;
|
|
9
|
+
/**
|
|
10
|
+
* Creates a store. Listeners run synchronously after each change, and an
|
|
11
|
+
* update to an equal value (by `Object.is`) notifies no one.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createStore<T>(initialState: T): Store<T>;
|
|
14
|
+
//#endregion
|
package/dist/_store.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/_store.ts
|
|
2
|
+
const queue = /* @__PURE__ */ new Set();
|
|
3
|
+
function notify(listeners) {
|
|
4
|
+
for (const listener of listeners) queue.add(listener);
|
|
5
|
+
for (const listener of queue) {
|
|
6
|
+
queue.delete(listener);
|
|
7
|
+
listener();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Creates a store. Listeners run synchronously after each change, and an
|
|
12
|
+
* update to an equal value (by `Object.is`) notifies no one.
|
|
13
|
+
*/
|
|
14
|
+
function createStore(initialState) {
|
|
15
|
+
let state = initialState;
|
|
16
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
17
|
+
return {
|
|
18
|
+
get state() {
|
|
19
|
+
return state;
|
|
20
|
+
},
|
|
21
|
+
get: () => state,
|
|
22
|
+
setState(updater) {
|
|
23
|
+
const next = updater(state);
|
|
24
|
+
if (Object.is(state, next)) return;
|
|
25
|
+
state = next;
|
|
26
|
+
notify(listeners);
|
|
27
|
+
},
|
|
28
|
+
subscribe(observerOrFn) {
|
|
29
|
+
const listener = () => {
|
|
30
|
+
if (typeof observerOrFn === "function") observerOrFn(state);
|
|
31
|
+
else observerOrFn.next?.(state);
|
|
32
|
+
};
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
return { unsubscribe() {
|
|
35
|
+
listeners.delete(listener);
|
|
36
|
+
queue.delete(listener);
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { createStore };
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Store, createStore } from "./_store.js";
|
|
1
2
|
import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
|
|
2
3
|
import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
|
|
3
4
|
import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
|
|
5
|
+
import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
|
|
4
6
|
//#region src/adapter.d.ts
|
|
5
7
|
/**
|
|
6
8
|
* A hotkey and its callback, with optional per-binding options. Chords accept
|
|
@@ -160,4 +162,5 @@ export declare function getHotkeysProviderScopeKey(options: {
|
|
|
160
162
|
* Returns a function that releases the activation.
|
|
161
163
|
*/
|
|
162
164
|
export declare function activateHotkeysProviderScope(key: string): () => void;
|
|
163
|
-
//#endregion
|
|
165
|
+
//#endregion
|
|
166
|
+
export { type Store, createAppHotkeysStore, createStore };
|
package/dist/adapter.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
2
|
import { isHotkeySequence, normalizeHotkey } from "./parse.js";
|
|
3
|
+
import { createStore } from "./_store.js";
|
|
3
4
|
import { defaultHotkeyOptions } from "./_registration.js";
|
|
4
5
|
import { getHotkeyManager } from "./hotkey-manager.js";
|
|
6
|
+
import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
|
|
5
7
|
import { getAppHotkeyRegistrationOptions, isAppHotkey, resolveAppHotkey } from "./app-hotkeys.js";
|
|
6
8
|
|
|
7
9
|
//#region src/adapter.ts
|
|
@@ -171,4 +173,4 @@ function activateHotkeysProviderScope(key) {
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
//#endregion
|
|
174
|
-
export { activateHotkeysProviderScope, createHotkeyBindings, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
|
|
176
|
+
export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
|
package/dist/app-hotkeys.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Store } from "./_store.js";
|
|
1
2
|
import { Hotkey, HotkeyChord, HotkeyMeta, HotkeySequence } from "./hotkey.types.js";
|
|
2
3
|
import { HotkeyChordOptions, HotkeyRegistrationView, HotkeySequenceOptions } from "./hotkey-manager.js";
|
|
3
|
-
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/app-hotkeys.d.ts
|
|
5
5
|
type Platform = 'mac' | 'windows' | 'linux';
|
|
6
6
|
type DeclaredOptionKeys = 'capture' | 'conflictBehavior' | 'eventType' | 'ignoreInputs' | 'preventDefault' | 'stopPropagation';
|