tinykeys 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -4
- package/dist/tinykeys.cjs +205 -0
- package/dist/tinykeys.cjs.map +1 -0
- package/dist/tinykeys.d.cts +138 -0
- package/dist/tinykeys.d.mts +138 -0
- package/dist/tinykeys.mjs +200 -0
- package/dist/tinykeys.mjs.map +1 -0
- package/dist/tinykeys.umd.js +2 -2
- package/dist/tinykeys.umd.js.map +1 -1
- package/package.json +48 -40
- package/dist/tinykeys.d.ts +0 -94
- package/dist/tinykeys.js +0 -2
- package/dist/tinykeys.js.map +0 -1
- package/dist/tinykeys.modern.js +0 -2
- package/dist/tinykeys.modern.js.map +0 -1
- package/dist/tinykeys.module.js +0 -2
- package/dist/tinykeys.module.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `tinykeys`
|
|
2
2
|
|
|
3
|
-
> A tiny (~
|
|
3
|
+
> A tiny (~1KB) & modern library for keybindings.
|
|
4
4
|
> [See Demo](https://jamiebuilds.github.io/tinykeys/)
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -162,6 +162,13 @@ valid value to
|
|
|
162
162
|
"Meta+Shift+D"
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
Modifiers can be made optional by wrapping them with `[brackets]`
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
"[Shift]+?"
|
|
169
|
+
"Control+[Shift]+D"
|
|
170
|
+
```
|
|
171
|
+
|
|
165
172
|
There is also a special `$mod` modifier that makes it easy to support cross
|
|
166
173
|
platform keybindings:
|
|
167
174
|
|
|
@@ -173,12 +180,12 @@ platform keybindings:
|
|
|
173
180
|
"$mod+Shift+D" // Meta/Control+Shift+D
|
|
174
181
|
```
|
|
175
182
|
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
You can also use parenthesis to use case-insensitive regular expressions to
|
|
184
|
+
match multiple keys (Note: This does not work for modifiers).
|
|
178
185
|
|
|
179
186
|
```js
|
|
180
187
|
"$mod+([0-9])" // $mod+0, $mod+1, $mod+2, etc...
|
|
181
|
-
// equivalent regex: /^[0-9]$/
|
|
188
|
+
// equivalent regex: /^(?:[0-9])$/iv
|
|
182
189
|
```
|
|
183
190
|
|
|
184
191
|
### Keybinding Sequences
|
|
@@ -201,6 +208,72 @@ Each press can optionally be prefixed with modifier keys:
|
|
|
201
208
|
|
|
202
209
|
Each press in the sequence must be pressed within 1000ms of the last.
|
|
203
210
|
|
|
211
|
+
#### Keybinding Priorities
|
|
212
|
+
|
|
213
|
+
It should rarely come up, but the order of your keybindings matters. So if you
|
|
214
|
+
were to create two keybindings that would match the same event, only the first
|
|
215
|
+
will fire.
|
|
216
|
+
|
|
217
|
+
This mainly comes up when you create bindings for both `KeyboardEvent#key` and
|
|
218
|
+
`KeyboardEvent#code` in order to support more keyboard layouts:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
tinykeys(window, {
|
|
222
|
+
"$mod+b": () => console.log("make bold"),
|
|
223
|
+
"$mod+KeyB": () => console.log("also bold"),
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
> In this case, if the user holds `$mod` and types `b (KeyB)` it will only
|
|
228
|
+
> trigger `$mod+B` and log `"make bold"`
|
|
229
|
+
|
|
230
|
+
This can also come up when you have sequences that overlap with other
|
|
231
|
+
keybindings.
|
|
232
|
+
|
|
233
|
+
<!-- prettier-ignore -->
|
|
234
|
+
```js
|
|
235
|
+
tinykeys(window, {
|
|
236
|
+
"g a": () => console.log("goto archive"),
|
|
237
|
+
"a": () => console.log("archive item"),
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
> In this case, if the user types `g a` it will only log `"goto archive"`
|
|
242
|
+
|
|
243
|
+
However, you can also break later keybindings by declaring earlier keybindings
|
|
244
|
+
that will always win:
|
|
245
|
+
|
|
246
|
+
<!-- prettier-ignore -->
|
|
247
|
+
```js
|
|
248
|
+
tinykeys(window, {
|
|
249
|
+
"g": () => console.log("show goto indicator"),
|
|
250
|
+
"g a": () => console.log("goto archive"),
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
> In this case, if the user types `g a` it will only log `"show goto indicator"`
|
|
255
|
+
> (after the first `g`)
|
|
256
|
+
|
|
257
|
+
#### Keybinding Sequence Conflicts
|
|
258
|
+
|
|
259
|
+
In some circumstances, overlapping keybindings can cause "conflicts" where a
|
|
260
|
+
keybinding has been completely typed, but there is another keybinding declared
|
|
261
|
+
earlier that might still match.
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
tinykeys(window, {
|
|
265
|
+
"a b c": () => console.log("abc"),
|
|
266
|
+
"a b": () => console.log("ab"),
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
> In this case, if the user types `a b` it will not trigger either keybinding,
|
|
271
|
+
> but it will print this warning to the console:
|
|
272
|
+
>
|
|
273
|
+
> ```
|
|
274
|
+
> warning: tinykeys: Conflict found, "a b" did not fire, waiting for: ["a b c"]
|
|
275
|
+
> ```
|
|
276
|
+
|
|
204
277
|
### Display the keyboard sequence
|
|
205
278
|
|
|
206
279
|
You can use the `parseKeybinding` method to get a structured representation of a
|
|
@@ -256,3 +329,10 @@ Keybinding sequences will wait this long between key presses before cancelling
|
|
|
256
329
|
|
|
257
330
|
> **Note:** Setting this value too low (i.e. `300`) will be too fast for many of
|
|
258
331
|
> your users.
|
|
332
|
+
|
|
333
|
+
### `options.ignore`
|
|
334
|
+
|
|
335
|
+
Add a filter for which keyboard events should be ignored.
|
|
336
|
+
|
|
337
|
+
By default, tinykeys will ignore keyboard events from `[contenteditable]`,
|
|
338
|
+
`input`, `textarea`, and `select` unless they are the `event.currentTarget`.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/tinykeys.ts
|
|
3
|
+
/**
|
|
4
|
+
* These are the modifier keys that change the meaning of keybindings.
|
|
5
|
+
*
|
|
6
|
+
* Note: Ignoring "AltGraph" because it is covered by the others.
|
|
7
|
+
*/
|
|
8
|
+
let KEYBINDING_MODIFIER_KEYS = [
|
|
9
|
+
"Shift",
|
|
10
|
+
"Meta",
|
|
11
|
+
"Alt",
|
|
12
|
+
"Control"
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Keybinding sequences should timeout if individual key presses are more than
|
|
16
|
+
* 1s apart by default.
|
|
17
|
+
*/
|
|
18
|
+
let DEFAULT_TIMEOUT = 1e3;
|
|
19
|
+
/**
|
|
20
|
+
* Keybinding sequences should bind to this event by default.
|
|
21
|
+
*/
|
|
22
|
+
let DEFAULT_EVENT = "keydown";
|
|
23
|
+
/**
|
|
24
|
+
* Platform detection code.
|
|
25
|
+
* @see https://github.com/jamiebuilds/tinykeys/issues/184
|
|
26
|
+
*/
|
|
27
|
+
let PLATFORM = typeof navigator === "object" ? navigator.platform : "";
|
|
28
|
+
/**
|
|
29
|
+
* An alias for creating platform-specific keybinding aliases.
|
|
30
|
+
*/
|
|
31
|
+
let MOD = /Mac|iPod|iPhone|iPad/.test(PLATFORM) ? "Meta" : "Control";
|
|
32
|
+
/**
|
|
33
|
+
* Meaning of `AltGraph`, from MDN:
|
|
34
|
+
* - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed
|
|
35
|
+
* - Mac: ⌥ Option key pressed
|
|
36
|
+
* - Linux: Level 3 Shift key (or Level 5 Shift key) pressed
|
|
37
|
+
* - Android: Not supported
|
|
38
|
+
* @see https://github.com/jamiebuilds/tinykeys/issues/185
|
|
39
|
+
*/
|
|
40
|
+
let ALT_GRAPH_ALIASES = PLATFORM === "Win32" ? ["Control", "Alt"] : ["Alt"];
|
|
41
|
+
/**
|
|
42
|
+
* Ensure and stop any event that isn't a full keyboard event.
|
|
43
|
+
* Autocomplete option navigation and selection would fire an Event,
|
|
44
|
+
* instead of the expected KeyboardEvent
|
|
45
|
+
*/
|
|
46
|
+
function isKeyboardEvent(event) {
|
|
47
|
+
return !!(event.key && event.code && event.getModifierState);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Ignores keyboard events from contenteditable and form elements unless they
|
|
51
|
+
* are the current target.
|
|
52
|
+
*/
|
|
53
|
+
function defaultKeybindingsHandlerIgnore(event) {
|
|
54
|
+
let target = event.target;
|
|
55
|
+
return event.repeat || event.isComposing || target !== event.currentTarget && target.matches("[contenteditable],input,select,textarea");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* There's a bug in Chrome that causes event.getModifierState not to exist on
|
|
59
|
+
* KeyboardEvent's for F1/F2/etc keys.
|
|
60
|
+
*/
|
|
61
|
+
function getModifierState(event, mod) {
|
|
62
|
+
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parses a keybinding string into its parts.
|
|
66
|
+
*
|
|
67
|
+
* ```
|
|
68
|
+
* grammar = `<sequence>`
|
|
69
|
+
* <sequence> = `<press> <press> <press> ...`
|
|
70
|
+
* <press> = `<key>` or `<mods>+<key>`
|
|
71
|
+
* <mods> = `<mod>+<mod>+...`
|
|
72
|
+
* <mod> = `<modifier>` (required) or `[<modifier>]` (optional)
|
|
73
|
+
* <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)
|
|
74
|
+
* <key> = `(<regex>)` -> `/^(?:<regex>)$/iy` (case-insensitive)
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
function parseKeybinding(str) {
|
|
78
|
+
return str.trim().split(" ").map((press) => {
|
|
79
|
+
let parts = press.split(/(?<=\w|\])\+/);
|
|
80
|
+
let last = parts.pop();
|
|
81
|
+
let regex = last.match(/^\((.+)\)$/);
|
|
82
|
+
let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
|
|
83
|
+
let requiredModifiers = [];
|
|
84
|
+
let optionalModifiers = [];
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
let optional = part.match(/^\[(.*)\]$/);
|
|
87
|
+
let mod = optional?.[1] ?? part;
|
|
88
|
+
mod = mod === "$mod" ? MOD : mod;
|
|
89
|
+
if (optional) optionalModifiers.push(mod);
|
|
90
|
+
else requiredModifiers.push(mod);
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
requiredModifiers,
|
|
94
|
+
optionalModifiers,
|
|
95
|
+
key
|
|
96
|
+
];
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* This tells us if a single keyboard event matches a single keybinding press.
|
|
101
|
+
*/
|
|
102
|
+
function matchKeybindingPress(event, [requiredModifiers, optionalModifiers, key]) {
|
|
103
|
+
const hasAltGraph = requiredModifiers.includes("AltGraph");
|
|
104
|
+
return !((key instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) : key.toUpperCase() !== event.key.toUpperCase() && key !== event.code) || requiredModifiers.find((mod) => {
|
|
105
|
+
return !getModifierState(event, mod);
|
|
106
|
+
}) || KEYBINDING_MODIFIER_KEYS.find((mod) => {
|
|
107
|
+
return !requiredModifiers.includes(mod) && !optionalModifiers.includes(mod) && key !== mod && getModifierState(event, mod) && !(hasAltGraph && ALT_GRAPH_ALIASES.includes(mod));
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Creates an event listener for handling keybindings.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```js
|
|
115
|
+
* import { createKeybindingsHandler } from "../src/keybindings"
|
|
116
|
+
*
|
|
117
|
+
* let handler = createKeybindingsHandler({
|
|
118
|
+
* "Shift+d": () => {
|
|
119
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
120
|
+
* },
|
|
121
|
+
* "y e e t": () => {
|
|
122
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
123
|
+
* },
|
|
124
|
+
* "$mod+d": () => {
|
|
125
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
126
|
+
* },
|
|
127
|
+
* })
|
|
128
|
+
*
|
|
129
|
+
* window.addEventListener("keydown", handler)
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
function createKeybindingsHandler(keybindingsMap, options = {}) {
|
|
133
|
+
let timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
134
|
+
let ignore = options.ignore ?? defaultKeybindingsHandlerIgnore;
|
|
135
|
+
let keybindings = Object.keys(keybindingsMap).map((input) => {
|
|
136
|
+
return [
|
|
137
|
+
input,
|
|
138
|
+
parseKeybinding(input),
|
|
139
|
+
keybindingsMap[input]
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
let pending = /* @__PURE__ */ new Map();
|
|
143
|
+
let timer = null;
|
|
144
|
+
return (event) => {
|
|
145
|
+
if (!isKeyboardEvent(event) || ignore(event)) return;
|
|
146
|
+
let conflicts = [];
|
|
147
|
+
for (let [input, sequence, handler] of keybindings) {
|
|
148
|
+
let prev = pending.get(input);
|
|
149
|
+
let [current, ...rest] = prev ? prev : sequence;
|
|
150
|
+
if (!matchKeybindingPress(event, current)) {
|
|
151
|
+
if (!getModifierState(event, event.key)) pending.delete(input);
|
|
152
|
+
} else if (rest.length > 0) {
|
|
153
|
+
pending.set(input, rest);
|
|
154
|
+
conflicts.push(input);
|
|
155
|
+
} else {
|
|
156
|
+
pending.delete(input);
|
|
157
|
+
if (conflicts.length) console.warn(`tinykeys: Conflict found, "${input}" did not fire, waiting for:`, conflicts);
|
|
158
|
+
else {
|
|
159
|
+
handler(event);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (timer) clearTimeout(timer);
|
|
165
|
+
timer = setTimeout(() => pending.clear(), timeout);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Subscribes to keybindings.
|
|
170
|
+
*
|
|
171
|
+
* Returns an unsubscribe method.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```js
|
|
175
|
+
* import { tinykeys } from "../src/tinykeys"
|
|
176
|
+
*
|
|
177
|
+
* tinykeys(window, {
|
|
178
|
+
* "Shift+d": () => {
|
|
179
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
180
|
+
* },
|
|
181
|
+
* "y e e t": () => {
|
|
182
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
183
|
+
* },
|
|
184
|
+
* "$mod+d": () => {
|
|
185
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
186
|
+
* },
|
|
187
|
+
* })
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
function tinykeys(target, keybindingMap, options = {}) {
|
|
191
|
+
let event = options.event ?? DEFAULT_EVENT;
|
|
192
|
+
let onKeyEvent = createKeybindingsHandler(keybindingMap, options);
|
|
193
|
+
target.addEventListener(event, onKeyEvent, options.capture);
|
|
194
|
+
return () => {
|
|
195
|
+
target.removeEventListener(event, onKeyEvent, options.capture);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
exports.createKeybindingsHandler = createKeybindingsHandler;
|
|
200
|
+
exports.defaultKeybindingsHandlerIgnore = defaultKeybindingsHandlerIgnore;
|
|
201
|
+
exports.matchKeybindingPress = matchKeybindingPress;
|
|
202
|
+
exports.parseKeybinding = parseKeybinding;
|
|
203
|
+
exports.tinykeys = tinykeys;
|
|
204
|
+
|
|
205
|
+
//# sourceMappingURL=tinykeys.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinykeys.cjs","names":[],"sources":["../src/tinykeys.ts"],"sourcesContent":["/**\n * A single press of a keybinding sequence.\n */\nexport type KeybindingPress = readonly [\n\trequiredModifiers: ReadonlyArray<string>,\n\toptionalModifiers: ReadonlyArray<string>,\n\tkey: string | RegExp,\n]\n\n/**\n * Keyboard event callback fired when keybinding is triggered.\n */\nexport type KeybindingHandler = (event: KeyboardEvent) => void\n\n/**\n * A map of keybinding strings to event handlers.\n */\nexport type KeybindingsMap = Record<string, KeybindingHandler>\n\n/**\n * Predicate that returns true if a keyboard event should be ignored.\n */\nexport type KeybindingFilter = (event: KeyboardEvent) => boolean\n\nexport interface KeybindingHandlerOptions {\n\t/**\n\t * Keybinding sequences will wait this long between key presses before\n\t * cancelling (default: 1000).\n\t *\n\t * **Note:** Setting this value too low (i.e. `300`) will be too fast for many\n\t * of your users.\n\t */\n\ttimeout?: number\n\n\t/**\n\t * Customize the behavior of which keyboard events will be ignored/skipped.\n\t *\n\t * By default this uses the behavior of {@link defaultKeybindingsHandlerIgnore}.\n\t *\n\t * @example Allow all events\n\t * ```tsx\n\t * tinykeys(window, {...}, {\n\t * ignore: () => false\n\t * })\n\t * ```\n\t *\n\t * @example Extend the default ignore\n\t * ```tsx\n\t * tinykeys(window, {...}, {\n\t * ignore: event => {\n\t * return (\n\t * // Also ignore events inside a dialog\n\t * event.target.closest(\"dialog\") != null &&\n\t * defaultKeybindingsHandlerIgnore(event)\n\t * );\n\t * }\n\t * })\n\t * ```\n\t */\n\tignore?: KeybindingFilter\n}\n\n/**\n * Options to configure the behavior of keybindings.\n */\nexport interface KeybindingOptions extends KeybindingHandlerOptions {\n\t/**\n\t * Key presses will listen to this event (default: \"keydown\").\n\t */\n\tevent?: \"keydown\" | \"keyup\"\n\n\t/**\n\t * Key presses will use a capture listener (default: false)\n\t */\n\tcapture?: boolean\n}\n\n/**\n * These are the modifier keys that change the meaning of keybindings.\n *\n * Note: Ignoring \"AltGraph\" because it is covered by the others.\n */\nlet KEYBINDING_MODIFIER_KEYS = [\"Shift\", \"Meta\", \"Alt\", \"Control\"]\n\n/**\n * Keybinding sequences should timeout if individual key presses are more than\n * 1s apart by default.\n */\nlet DEFAULT_TIMEOUT = 1000\n\n/**\n * Keybinding sequences should bind to this event by default.\n */\nlet DEFAULT_EVENT = \"keydown\" as const\n\n/**\n * Platform detection code.\n * @see https://github.com/jamiebuilds/tinykeys/issues/184\n */\nlet PLATFORM = typeof navigator === \"object\" ? navigator.platform : \"\"\nlet APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM)\n\n/**\n * An alias for creating platform-specific keybinding aliases.\n */\nlet MOD = APPLE_DEVICE ? \"Meta\" : \"Control\"\n\n/**\n * Meaning of `AltGraph`, from MDN:\n * - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed\n * - Mac: ⌥ Option key pressed\n * - Linux: Level 3 Shift key (or Level 5 Shift key) pressed\n * - Android: Not supported\n * @see https://github.com/jamiebuilds/tinykeys/issues/185\n */\nlet ALT_GRAPH_ALIASES = PLATFORM === \"Win32\" ? [\"Control\", \"Alt\"] : [\"Alt\"]\n\n/**\n * Ensure and stop any event that isn't a full keyboard event.\n * Autocomplete option navigation and selection would fire an Event,\n * instead of the expected KeyboardEvent\n */\nfunction isKeyboardEvent(\n\tevent: Partial<KeyboardEvent>,\n): event is KeyboardEvent {\n\treturn !!(event.key && event.code && event.getModifierState)\n}\n\n/**\n * Ignores keyboard events from contenteditable and form elements unless they\n * are the current target.\n */\nexport function defaultKeybindingsHandlerIgnore(event: KeyboardEvent) {\n\tlet target = event.target as HTMLElement\n\treturn (\n\t\t// Always ignore repeated keyboard events\n\t\tevent.repeat ||\n\t\t// Always ignore keyboard events during composition input\n\t\tevent.isComposing ||\n\t\t// Always allow the current target\n\t\t(target !== event.currentTarget &&\n\t\t\t// Ignore contenteditable and form elements\n\t\t\ttarget.matches(\"[contenteditable],input,select,textarea\"))\n\t)\n}\n\n/**\n * There's a bug in Chrome that causes event.getModifierState not to exist on\n * KeyboardEvent's for F1/F2/etc keys.\n */\nfunction getModifierState(event: KeyboardEvent, mod: string) {\n\treturn typeof event.getModifierState === \"function\"\n\t\t? event.getModifierState(mod) ||\n\t\t\t\t(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState(\"AltGraph\"))\n\t\t: false\n}\n\n/**\n * Parses a keybinding string into its parts.\n *\n * ```\n * grammar = `<sequence>`\n * <sequence> = `<press> <press> <press> ...`\n * <press> = `<key>` or `<mods>+<key>`\n * <mods> = `<mod>+<mod>+...`\n * <mod> = `<modifier>` (required) or `[<modifier>]` (optional)\n * <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)\n * <key> = `(<regex>)` -> `/^(?:<regex>)$/iy` (case-insensitive)\n * ```\n */\nexport function parseKeybinding(str: string): KeybindingPress[] {\n\treturn str\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.map(press => {\n\t\t\tlet parts = press.split(/(?<=\\w|\\])\\+/)\n\n\t\t\tlet last: string | RegExp = parts.pop() as string\n\t\t\tlet regex = last.match(/^\\((.+)\\)$/)\n\t\t\tlet key = regex ? new RegExp(`^(?:${regex[1]})$`, \"iv\") : last\n\n\t\t\tlet requiredModifiers: string[] = []\n\t\t\tlet optionalModifiers: string[] = []\n\n\t\t\tfor (const part of parts) {\n\t\t\t\tlet optional = part.match(/^\\[(.*)\\]$/)\n\t\t\t\tlet mod = optional?.[1] ?? part\n\t\t\t\tmod = mod === \"$mod\" ? MOD : mod\n\t\t\t\tif (optional) {\n\t\t\t\t\toptionalModifiers.push(mod)\n\t\t\t\t} else {\n\t\t\t\t\trequiredModifiers.push(mod)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn [requiredModifiers, optionalModifiers, key]\n\t\t})\n}\n\n/**\n * This tells us if a single keyboard event matches a single keybinding press.\n */\nexport function matchKeybindingPress(\n\tevent: KeyboardEvent,\n\t[requiredModifiers, optionalModifiers, key]: KeybindingPress,\n): boolean {\n\tconst hasAltGraph = requiredModifiers.includes(\"AltGraph\")\n\t// prettier-ignore\n\treturn !(\n\t\t// Allow either the `event.key` or the `event.code`\n\t\t// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n\t\t// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\t\t(\n\t\t\tkey instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :\n\t\t\t(key.toUpperCase() !== event.key.toUpperCase() &&\n\t\t\tkey !== event.code)\n\t\t) ||\n\n\t\t// Ensure all required modifiers in the keybinding are pressed.\n\t\trequiredModifiers.find(mod => {\n\t\t\treturn !getModifierState(event, mod)\n\t\t}) ||\n\n\t\t// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a\n\t\t// keybinding. So if they are pressed but aren't part of the current\n\t\t// keybinding press, then we don't have a match.\n\t\tKEYBINDING_MODIFIER_KEYS.find(mod => {\n\t\t\treturn (\n\t\t\t\t!requiredModifiers.includes(mod) &&\n\t\t\t\t!optionalModifiers.includes(mod) &&\n\t\t\t\tkey !== mod &&\n\t\t\t\tgetModifierState(event, mod) &&\n\t\t\t\t// When AltGraph is required, its alias modifiers (e.g. Alt, Control)\n\t\t\t\t// being active is expected — don't treat them as unexpected modifiers.\n\t\t\t\t!(hasAltGraph && ALT_GRAPH_ALIASES.includes(mod))\n\t\t\t);\n\t\t})\n\t)\n}\n\n/**\n * Creates an event listener for handling keybindings.\n *\n * @example\n * ```js\n * import { createKeybindingsHandler } from \"../src/keybindings\"\n *\n * let handler = createKeybindingsHandler({\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n *\n * window.addEventListener(\"keydown\", handler)\n * ```\n */\nexport function createKeybindingsHandler(\n\tkeybindingsMap: KeybindingsMap,\n\toptions: KeybindingHandlerOptions = {},\n): EventListener {\n\tlet timeout = options.timeout ?? DEFAULT_TIMEOUT\n\tlet ignore = options.ignore ?? defaultKeybindingsHandlerIgnore\n\n\tlet keybindings = Object.keys(keybindingsMap).map(input => {\n\t\treturn [input, parseKeybinding(input), keybindingsMap[input]] as const\n\t})\n\n\tlet pending = new Map<string, KeybindingPress[]>()\n\tlet timer: number | null = null\n\n\treturn event => {\n\t\tif (!isKeyboardEvent(event) || ignore(event)) {\n\t\t\treturn\n\t\t}\n\n\t\tlet conflicts: Array<string> = []\n\t\tfor (let [input, sequence, handler] of keybindings) {\n\t\t\tlet prev = pending.get(input)\n\t\t\tlet expected = prev ? prev : sequence\n\t\t\tlet [current, ...rest] = expected\n\n\t\t\tlet matches = matchKeybindingPress(event, current)\n\n\t\t\tif (!matches) {\n\t\t\t\t// Modifier keydown events shouldn't break sequences\n\t\t\t\t// Note: This works because:\n\t\t\t\t// - non-modifiers will always return false\n\t\t\t\t// - if the current keypress is a modifier then it will return true when we check its state\n\t\t\t\t// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState\n\t\t\t\tif (!getModifierState(event, event.key)) {\n\t\t\t\t\tpending.delete(input)\n\t\t\t\t}\n\t\t\t} else if (rest.length > 0) {\n\t\t\t\tpending.set(input, rest)\n\t\t\t\tconflicts.push(input)\n\t\t\t} else {\n\t\t\t\tpending.delete(input)\n\t\t\t\tif (conflicts.length) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`tinykeys: Conflict found, \"${input}\" did not fire, waiting for:`,\n\t\t\t\t\t\tconflicts,\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\thandler(event)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (timer) {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\ttimer = setTimeout(() => pending.clear(), timeout)\n\t}\n}\n\n/**\n * Subscribes to keybindings.\n *\n * Returns an unsubscribe method.\n *\n * @example\n * ```js\n * import { tinykeys } from \"../src/tinykeys\"\n *\n * tinykeys(window, {\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n * ```\n */\nexport function tinykeys(\n\ttarget: Window | HTMLElement,\n\tkeybindingMap: KeybindingsMap,\n\toptions: KeybindingOptions = {},\n): () => void {\n\tlet event = options.event ?? DEFAULT_EVENT\n\tlet onKeyEvent = createKeybindingsHandler(keybindingMap, options)\n\ttarget.addEventListener(event, onKeyEvent, options.capture)\n\treturn () => {\n\t\ttarget.removeEventListener(event, onKeyEvent, options.capture)\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//#region src/tinykeys.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A single press of a keybinding sequence.
|
|
4
|
+
*/
|
|
5
|
+
type KeybindingPress = readonly [requiredModifiers: ReadonlyArray<string>, optionalModifiers: ReadonlyArray<string>, key: string | RegExp];
|
|
6
|
+
/**
|
|
7
|
+
* Keyboard event callback fired when keybinding is triggered.
|
|
8
|
+
*/
|
|
9
|
+
type KeybindingHandler = (event: KeyboardEvent) => void;
|
|
10
|
+
/**
|
|
11
|
+
* A map of keybinding strings to event handlers.
|
|
12
|
+
*/
|
|
13
|
+
type KeybindingsMap = Record<string, KeybindingHandler>;
|
|
14
|
+
/**
|
|
15
|
+
* Predicate that returns true if a keyboard event should be ignored.
|
|
16
|
+
*/
|
|
17
|
+
type KeybindingFilter = (event: KeyboardEvent) => boolean;
|
|
18
|
+
interface KeybindingHandlerOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Keybinding sequences will wait this long between key presses before
|
|
21
|
+
* cancelling (default: 1000).
|
|
22
|
+
*
|
|
23
|
+
* **Note:** Setting this value too low (i.e. `300`) will be too fast for many
|
|
24
|
+
* of your users.
|
|
25
|
+
*/
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Customize the behavior of which keyboard events will be ignored/skipped.
|
|
29
|
+
*
|
|
30
|
+
* By default this uses the behavior of {@link defaultKeybindingsHandlerIgnore}.
|
|
31
|
+
*
|
|
32
|
+
* @example Allow all events
|
|
33
|
+
* ```tsx
|
|
34
|
+
* tinykeys(window, {...}, {
|
|
35
|
+
* ignore: () => false
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example Extend the default ignore
|
|
40
|
+
* ```tsx
|
|
41
|
+
* tinykeys(window, {...}, {
|
|
42
|
+
* ignore: event => {
|
|
43
|
+
* return (
|
|
44
|
+
* // Also ignore events inside a dialog
|
|
45
|
+
* event.target.closest("dialog") != null &&
|
|
46
|
+
* defaultKeybindingsHandlerIgnore(event)
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
ignore?: KeybindingFilter;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Options to configure the behavior of keybindings.
|
|
56
|
+
*/
|
|
57
|
+
interface KeybindingOptions extends KeybindingHandlerOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Key presses will listen to this event (default: "keydown").
|
|
60
|
+
*/
|
|
61
|
+
event?: "keydown" | "keyup";
|
|
62
|
+
/**
|
|
63
|
+
* Key presses will use a capture listener (default: false)
|
|
64
|
+
*/
|
|
65
|
+
capture?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ignores keyboard events from contenteditable and form elements unless they
|
|
69
|
+
* are the current target.
|
|
70
|
+
*/
|
|
71
|
+
declare function defaultKeybindingsHandlerIgnore(event: KeyboardEvent): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Parses a keybinding string into its parts.
|
|
74
|
+
*
|
|
75
|
+
* ```
|
|
76
|
+
* grammar = `<sequence>`
|
|
77
|
+
* <sequence> = `<press> <press> <press> ...`
|
|
78
|
+
* <press> = `<key>` or `<mods>+<key>`
|
|
79
|
+
* <mods> = `<mod>+<mod>+...`
|
|
80
|
+
* <mod> = `<modifier>` (required) or `[<modifier>]` (optional)
|
|
81
|
+
* <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)
|
|
82
|
+
* <key> = `(<regex>)` -> `/^(?:<regex>)$/iy` (case-insensitive)
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function parseKeybinding(str: string): KeybindingPress[];
|
|
86
|
+
/**
|
|
87
|
+
* This tells us if a single keyboard event matches a single keybinding press.
|
|
88
|
+
*/
|
|
89
|
+
declare function matchKeybindingPress(event: KeyboardEvent, [requiredModifiers, optionalModifiers, key]: KeybindingPress): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Creates an event listener for handling keybindings.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```js
|
|
95
|
+
* import { createKeybindingsHandler } from "../src/keybindings"
|
|
96
|
+
*
|
|
97
|
+
* let handler = createKeybindingsHandler({
|
|
98
|
+
* "Shift+d": () => {
|
|
99
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
100
|
+
* },
|
|
101
|
+
* "y e e t": () => {
|
|
102
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
103
|
+
* },
|
|
104
|
+
* "$mod+d": () => {
|
|
105
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
106
|
+
* },
|
|
107
|
+
* })
|
|
108
|
+
*
|
|
109
|
+
* window.addEventListener("keydown", handler)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
declare function createKeybindingsHandler(keybindingsMap: KeybindingsMap, options?: KeybindingHandlerOptions): EventListener;
|
|
113
|
+
/**
|
|
114
|
+
* Subscribes to keybindings.
|
|
115
|
+
*
|
|
116
|
+
* Returns an unsubscribe method.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```js
|
|
120
|
+
* import { tinykeys } from "../src/tinykeys"
|
|
121
|
+
*
|
|
122
|
+
* tinykeys(window, {
|
|
123
|
+
* "Shift+d": () => {
|
|
124
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
125
|
+
* },
|
|
126
|
+
* "y e e t": () => {
|
|
127
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
128
|
+
* },
|
|
129
|
+
* "$mod+d": () => {
|
|
130
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
131
|
+
* },
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare function tinykeys(target: Window | HTMLElement, keybindingMap: KeybindingsMap, options?: KeybindingOptions): () => void;
|
|
136
|
+
//#endregion
|
|
137
|
+
export { KeybindingFilter, KeybindingHandler, KeybindingHandlerOptions, KeybindingOptions, KeybindingPress, KeybindingsMap, createKeybindingsHandler, defaultKeybindingsHandlerIgnore, matchKeybindingPress, parseKeybinding, tinykeys };
|
|
138
|
+
//# sourceMappingURL=tinykeys.d.cts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//#region src/tinykeys.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A single press of a keybinding sequence.
|
|
4
|
+
*/
|
|
5
|
+
type KeybindingPress = readonly [requiredModifiers: ReadonlyArray<string>, optionalModifiers: ReadonlyArray<string>, key: string | RegExp];
|
|
6
|
+
/**
|
|
7
|
+
* Keyboard event callback fired when keybinding is triggered.
|
|
8
|
+
*/
|
|
9
|
+
type KeybindingHandler = (event: KeyboardEvent) => void;
|
|
10
|
+
/**
|
|
11
|
+
* A map of keybinding strings to event handlers.
|
|
12
|
+
*/
|
|
13
|
+
type KeybindingsMap = Record<string, KeybindingHandler>;
|
|
14
|
+
/**
|
|
15
|
+
* Predicate that returns true if a keyboard event should be ignored.
|
|
16
|
+
*/
|
|
17
|
+
type KeybindingFilter = (event: KeyboardEvent) => boolean;
|
|
18
|
+
interface KeybindingHandlerOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Keybinding sequences will wait this long between key presses before
|
|
21
|
+
* cancelling (default: 1000).
|
|
22
|
+
*
|
|
23
|
+
* **Note:** Setting this value too low (i.e. `300`) will be too fast for many
|
|
24
|
+
* of your users.
|
|
25
|
+
*/
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Customize the behavior of which keyboard events will be ignored/skipped.
|
|
29
|
+
*
|
|
30
|
+
* By default this uses the behavior of {@link defaultKeybindingsHandlerIgnore}.
|
|
31
|
+
*
|
|
32
|
+
* @example Allow all events
|
|
33
|
+
* ```tsx
|
|
34
|
+
* tinykeys(window, {...}, {
|
|
35
|
+
* ignore: () => false
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example Extend the default ignore
|
|
40
|
+
* ```tsx
|
|
41
|
+
* tinykeys(window, {...}, {
|
|
42
|
+
* ignore: event => {
|
|
43
|
+
* return (
|
|
44
|
+
* // Also ignore events inside a dialog
|
|
45
|
+
* event.target.closest("dialog") != null &&
|
|
46
|
+
* defaultKeybindingsHandlerIgnore(event)
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
ignore?: KeybindingFilter;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Options to configure the behavior of keybindings.
|
|
56
|
+
*/
|
|
57
|
+
interface KeybindingOptions extends KeybindingHandlerOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Key presses will listen to this event (default: "keydown").
|
|
60
|
+
*/
|
|
61
|
+
event?: "keydown" | "keyup";
|
|
62
|
+
/**
|
|
63
|
+
* Key presses will use a capture listener (default: false)
|
|
64
|
+
*/
|
|
65
|
+
capture?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ignores keyboard events from contenteditable and form elements unless they
|
|
69
|
+
* are the current target.
|
|
70
|
+
*/
|
|
71
|
+
declare function defaultKeybindingsHandlerIgnore(event: KeyboardEvent): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Parses a keybinding string into its parts.
|
|
74
|
+
*
|
|
75
|
+
* ```
|
|
76
|
+
* grammar = `<sequence>`
|
|
77
|
+
* <sequence> = `<press> <press> <press> ...`
|
|
78
|
+
* <press> = `<key>` or `<mods>+<key>`
|
|
79
|
+
* <mods> = `<mod>+<mod>+...`
|
|
80
|
+
* <mod> = `<modifier>` (required) or `[<modifier>]` (optional)
|
|
81
|
+
* <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)
|
|
82
|
+
* <key> = `(<regex>)` -> `/^(?:<regex>)$/iy` (case-insensitive)
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function parseKeybinding(str: string): KeybindingPress[];
|
|
86
|
+
/**
|
|
87
|
+
* This tells us if a single keyboard event matches a single keybinding press.
|
|
88
|
+
*/
|
|
89
|
+
declare function matchKeybindingPress(event: KeyboardEvent, [requiredModifiers, optionalModifiers, key]: KeybindingPress): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Creates an event listener for handling keybindings.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```js
|
|
95
|
+
* import { createKeybindingsHandler } from "../src/keybindings"
|
|
96
|
+
*
|
|
97
|
+
* let handler = createKeybindingsHandler({
|
|
98
|
+
* "Shift+d": () => {
|
|
99
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
100
|
+
* },
|
|
101
|
+
* "y e e t": () => {
|
|
102
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
103
|
+
* },
|
|
104
|
+
* "$mod+d": () => {
|
|
105
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
106
|
+
* },
|
|
107
|
+
* })
|
|
108
|
+
*
|
|
109
|
+
* window.addEventListener("keydown", handler)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
declare function createKeybindingsHandler(keybindingsMap: KeybindingsMap, options?: KeybindingHandlerOptions): EventListener;
|
|
113
|
+
/**
|
|
114
|
+
* Subscribes to keybindings.
|
|
115
|
+
*
|
|
116
|
+
* Returns an unsubscribe method.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```js
|
|
120
|
+
* import { tinykeys } from "../src/tinykeys"
|
|
121
|
+
*
|
|
122
|
+
* tinykeys(window, {
|
|
123
|
+
* "Shift+d": () => {
|
|
124
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
125
|
+
* },
|
|
126
|
+
* "y e e t": () => {
|
|
127
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
128
|
+
* },
|
|
129
|
+
* "$mod+d": () => {
|
|
130
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
131
|
+
* },
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare function tinykeys(target: Window | HTMLElement, keybindingMap: KeybindingsMap, options?: KeybindingOptions): () => void;
|
|
136
|
+
//#endregion
|
|
137
|
+
export { KeybindingFilter, KeybindingHandler, KeybindingHandlerOptions, KeybindingOptions, KeybindingPress, KeybindingsMap, createKeybindingsHandler, defaultKeybindingsHandlerIgnore, matchKeybindingPress, parseKeybinding, tinykeys };
|
|
138
|
+
//# sourceMappingURL=tinykeys.d.mts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
//#region src/tinykeys.ts
|
|
2
|
+
/**
|
|
3
|
+
* These are the modifier keys that change the meaning of keybindings.
|
|
4
|
+
*
|
|
5
|
+
* Note: Ignoring "AltGraph" because it is covered by the others.
|
|
6
|
+
*/
|
|
7
|
+
let KEYBINDING_MODIFIER_KEYS = [
|
|
8
|
+
"Shift",
|
|
9
|
+
"Meta",
|
|
10
|
+
"Alt",
|
|
11
|
+
"Control"
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Keybinding sequences should timeout if individual key presses are more than
|
|
15
|
+
* 1s apart by default.
|
|
16
|
+
*/
|
|
17
|
+
let DEFAULT_TIMEOUT = 1e3;
|
|
18
|
+
/**
|
|
19
|
+
* Keybinding sequences should bind to this event by default.
|
|
20
|
+
*/
|
|
21
|
+
let DEFAULT_EVENT = "keydown";
|
|
22
|
+
/**
|
|
23
|
+
* Platform detection code.
|
|
24
|
+
* @see https://github.com/jamiebuilds/tinykeys/issues/184
|
|
25
|
+
*/
|
|
26
|
+
let PLATFORM = typeof navigator === "object" ? navigator.platform : "";
|
|
27
|
+
/**
|
|
28
|
+
* An alias for creating platform-specific keybinding aliases.
|
|
29
|
+
*/
|
|
30
|
+
let MOD = /Mac|iPod|iPhone|iPad/.test(PLATFORM) ? "Meta" : "Control";
|
|
31
|
+
/**
|
|
32
|
+
* Meaning of `AltGraph`, from MDN:
|
|
33
|
+
* - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed
|
|
34
|
+
* - Mac: ⌥ Option key pressed
|
|
35
|
+
* - Linux: Level 3 Shift key (or Level 5 Shift key) pressed
|
|
36
|
+
* - Android: Not supported
|
|
37
|
+
* @see https://github.com/jamiebuilds/tinykeys/issues/185
|
|
38
|
+
*/
|
|
39
|
+
let ALT_GRAPH_ALIASES = PLATFORM === "Win32" ? ["Control", "Alt"] : ["Alt"];
|
|
40
|
+
/**
|
|
41
|
+
* Ensure and stop any event that isn't a full keyboard event.
|
|
42
|
+
* Autocomplete option navigation and selection would fire an Event,
|
|
43
|
+
* instead of the expected KeyboardEvent
|
|
44
|
+
*/
|
|
45
|
+
function isKeyboardEvent(event) {
|
|
46
|
+
return !!(event.key && event.code && event.getModifierState);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Ignores keyboard events from contenteditable and form elements unless they
|
|
50
|
+
* are the current target.
|
|
51
|
+
*/
|
|
52
|
+
function defaultKeybindingsHandlerIgnore(event) {
|
|
53
|
+
let target = event.target;
|
|
54
|
+
return event.repeat || event.isComposing || target !== event.currentTarget && target.matches("[contenteditable],input,select,textarea");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* There's a bug in Chrome that causes event.getModifierState not to exist on
|
|
58
|
+
* KeyboardEvent's for F1/F2/etc keys.
|
|
59
|
+
*/
|
|
60
|
+
function getModifierState(event, mod) {
|
|
61
|
+
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parses a keybinding string into its parts.
|
|
65
|
+
*
|
|
66
|
+
* ```
|
|
67
|
+
* grammar = `<sequence>`
|
|
68
|
+
* <sequence> = `<press> <press> <press> ...`
|
|
69
|
+
* <press> = `<key>` or `<mods>+<key>`
|
|
70
|
+
* <mods> = `<mod>+<mod>+...`
|
|
71
|
+
* <mod> = `<modifier>` (required) or `[<modifier>]` (optional)
|
|
72
|
+
* <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)
|
|
73
|
+
* <key> = `(<regex>)` -> `/^(?:<regex>)$/iy` (case-insensitive)
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
function parseKeybinding(str) {
|
|
77
|
+
return str.trim().split(" ").map((press) => {
|
|
78
|
+
let parts = press.split(/(?<=\w|\])\+/);
|
|
79
|
+
let last = parts.pop();
|
|
80
|
+
let regex = last.match(/^\((.+)\)$/);
|
|
81
|
+
let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
|
|
82
|
+
let requiredModifiers = [];
|
|
83
|
+
let optionalModifiers = [];
|
|
84
|
+
for (const part of parts) {
|
|
85
|
+
let optional = part.match(/^\[(.*)\]$/);
|
|
86
|
+
let mod = optional?.[1] ?? part;
|
|
87
|
+
mod = mod === "$mod" ? MOD : mod;
|
|
88
|
+
if (optional) optionalModifiers.push(mod);
|
|
89
|
+
else requiredModifiers.push(mod);
|
|
90
|
+
}
|
|
91
|
+
return [
|
|
92
|
+
requiredModifiers,
|
|
93
|
+
optionalModifiers,
|
|
94
|
+
key
|
|
95
|
+
];
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* This tells us if a single keyboard event matches a single keybinding press.
|
|
100
|
+
*/
|
|
101
|
+
function matchKeybindingPress(event, [requiredModifiers, optionalModifiers, key]) {
|
|
102
|
+
const hasAltGraph = requiredModifiers.includes("AltGraph");
|
|
103
|
+
return !((key instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) : key.toUpperCase() !== event.key.toUpperCase() && key !== event.code) || requiredModifiers.find((mod) => {
|
|
104
|
+
return !getModifierState(event, mod);
|
|
105
|
+
}) || KEYBINDING_MODIFIER_KEYS.find((mod) => {
|
|
106
|
+
return !requiredModifiers.includes(mod) && !optionalModifiers.includes(mod) && key !== mod && getModifierState(event, mod) && !(hasAltGraph && ALT_GRAPH_ALIASES.includes(mod));
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Creates an event listener for handling keybindings.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```js
|
|
114
|
+
* import { createKeybindingsHandler } from "../src/keybindings"
|
|
115
|
+
*
|
|
116
|
+
* let handler = createKeybindingsHandler({
|
|
117
|
+
* "Shift+d": () => {
|
|
118
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
119
|
+
* },
|
|
120
|
+
* "y e e t": () => {
|
|
121
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
122
|
+
* },
|
|
123
|
+
* "$mod+d": () => {
|
|
124
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
125
|
+
* },
|
|
126
|
+
* })
|
|
127
|
+
*
|
|
128
|
+
* window.addEventListener("keydown", handler)
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
function createKeybindingsHandler(keybindingsMap, options = {}) {
|
|
132
|
+
let timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
133
|
+
let ignore = options.ignore ?? defaultKeybindingsHandlerIgnore;
|
|
134
|
+
let keybindings = Object.keys(keybindingsMap).map((input) => {
|
|
135
|
+
return [
|
|
136
|
+
input,
|
|
137
|
+
parseKeybinding(input),
|
|
138
|
+
keybindingsMap[input]
|
|
139
|
+
];
|
|
140
|
+
});
|
|
141
|
+
let pending = /* @__PURE__ */ new Map();
|
|
142
|
+
let timer = null;
|
|
143
|
+
return (event) => {
|
|
144
|
+
if (!isKeyboardEvent(event) || ignore(event)) return;
|
|
145
|
+
let conflicts = [];
|
|
146
|
+
for (let [input, sequence, handler] of keybindings) {
|
|
147
|
+
let prev = pending.get(input);
|
|
148
|
+
let [current, ...rest] = prev ? prev : sequence;
|
|
149
|
+
if (!matchKeybindingPress(event, current)) {
|
|
150
|
+
if (!getModifierState(event, event.key)) pending.delete(input);
|
|
151
|
+
} else if (rest.length > 0) {
|
|
152
|
+
pending.set(input, rest);
|
|
153
|
+
conflicts.push(input);
|
|
154
|
+
} else {
|
|
155
|
+
pending.delete(input);
|
|
156
|
+
if (conflicts.length) console.warn(`tinykeys: Conflict found, "${input}" did not fire, waiting for:`, conflicts);
|
|
157
|
+
else {
|
|
158
|
+
handler(event);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (timer) clearTimeout(timer);
|
|
164
|
+
timer = setTimeout(() => pending.clear(), timeout);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Subscribes to keybindings.
|
|
169
|
+
*
|
|
170
|
+
* Returns an unsubscribe method.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```js
|
|
174
|
+
* import { tinykeys } from "../src/tinykeys"
|
|
175
|
+
*
|
|
176
|
+
* tinykeys(window, {
|
|
177
|
+
* "Shift+d": () => {
|
|
178
|
+
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
179
|
+
* },
|
|
180
|
+
* "y e e t": () => {
|
|
181
|
+
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
182
|
+
* },
|
|
183
|
+
* "$mod+d": () => {
|
|
184
|
+
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
185
|
+
* },
|
|
186
|
+
* })
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
function tinykeys(target, keybindingMap, options = {}) {
|
|
190
|
+
let event = options.event ?? DEFAULT_EVENT;
|
|
191
|
+
let onKeyEvent = createKeybindingsHandler(keybindingMap, options);
|
|
192
|
+
target.addEventListener(event, onKeyEvent, options.capture);
|
|
193
|
+
return () => {
|
|
194
|
+
target.removeEventListener(event, onKeyEvent, options.capture);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
export { createKeybindingsHandler, defaultKeybindingsHandlerIgnore, matchKeybindingPress, parseKeybinding, tinykeys };
|
|
199
|
+
|
|
200
|
+
//# sourceMappingURL=tinykeys.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinykeys.mjs","names":[],"sources":["../src/tinykeys.ts"],"sourcesContent":["/**\n * A single press of a keybinding sequence.\n */\nexport type KeybindingPress = readonly [\n\trequiredModifiers: ReadonlyArray<string>,\n\toptionalModifiers: ReadonlyArray<string>,\n\tkey: string | RegExp,\n]\n\n/**\n * Keyboard event callback fired when keybinding is triggered.\n */\nexport type KeybindingHandler = (event: KeyboardEvent) => void\n\n/**\n * A map of keybinding strings to event handlers.\n */\nexport type KeybindingsMap = Record<string, KeybindingHandler>\n\n/**\n * Predicate that returns true if a keyboard event should be ignored.\n */\nexport type KeybindingFilter = (event: KeyboardEvent) => boolean\n\nexport interface KeybindingHandlerOptions {\n\t/**\n\t * Keybinding sequences will wait this long between key presses before\n\t * cancelling (default: 1000).\n\t *\n\t * **Note:** Setting this value too low (i.e. `300`) will be too fast for many\n\t * of your users.\n\t */\n\ttimeout?: number\n\n\t/**\n\t * Customize the behavior of which keyboard events will be ignored/skipped.\n\t *\n\t * By default this uses the behavior of {@link defaultKeybindingsHandlerIgnore}.\n\t *\n\t * @example Allow all events\n\t * ```tsx\n\t * tinykeys(window, {...}, {\n\t * ignore: () => false\n\t * })\n\t * ```\n\t *\n\t * @example Extend the default ignore\n\t * ```tsx\n\t * tinykeys(window, {...}, {\n\t * ignore: event => {\n\t * return (\n\t * // Also ignore events inside a dialog\n\t * event.target.closest(\"dialog\") != null &&\n\t * defaultKeybindingsHandlerIgnore(event)\n\t * );\n\t * }\n\t * })\n\t * ```\n\t */\n\tignore?: KeybindingFilter\n}\n\n/**\n * Options to configure the behavior of keybindings.\n */\nexport interface KeybindingOptions extends KeybindingHandlerOptions {\n\t/**\n\t * Key presses will listen to this event (default: \"keydown\").\n\t */\n\tevent?: \"keydown\" | \"keyup\"\n\n\t/**\n\t * Key presses will use a capture listener (default: false)\n\t */\n\tcapture?: boolean\n}\n\n/**\n * These are the modifier keys that change the meaning of keybindings.\n *\n * Note: Ignoring \"AltGraph\" because it is covered by the others.\n */\nlet KEYBINDING_MODIFIER_KEYS = [\"Shift\", \"Meta\", \"Alt\", \"Control\"]\n\n/**\n * Keybinding sequences should timeout if individual key presses are more than\n * 1s apart by default.\n */\nlet DEFAULT_TIMEOUT = 1000\n\n/**\n * Keybinding sequences should bind to this event by default.\n */\nlet DEFAULT_EVENT = \"keydown\" as const\n\n/**\n * Platform detection code.\n * @see https://github.com/jamiebuilds/tinykeys/issues/184\n */\nlet PLATFORM = typeof navigator === \"object\" ? navigator.platform : \"\"\nlet APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM)\n\n/**\n * An alias for creating platform-specific keybinding aliases.\n */\nlet MOD = APPLE_DEVICE ? \"Meta\" : \"Control\"\n\n/**\n * Meaning of `AltGraph`, from MDN:\n * - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed\n * - Mac: ⌥ Option key pressed\n * - Linux: Level 3 Shift key (or Level 5 Shift key) pressed\n * - Android: Not supported\n * @see https://github.com/jamiebuilds/tinykeys/issues/185\n */\nlet ALT_GRAPH_ALIASES = PLATFORM === \"Win32\" ? [\"Control\", \"Alt\"] : [\"Alt\"]\n\n/**\n * Ensure and stop any event that isn't a full keyboard event.\n * Autocomplete option navigation and selection would fire an Event,\n * instead of the expected KeyboardEvent\n */\nfunction isKeyboardEvent(\n\tevent: Partial<KeyboardEvent>,\n): event is KeyboardEvent {\n\treturn !!(event.key && event.code && event.getModifierState)\n}\n\n/**\n * Ignores keyboard events from contenteditable and form elements unless they\n * are the current target.\n */\nexport function defaultKeybindingsHandlerIgnore(event: KeyboardEvent) {\n\tlet target = event.target as HTMLElement\n\treturn (\n\t\t// Always ignore repeated keyboard events\n\t\tevent.repeat ||\n\t\t// Always ignore keyboard events during composition input\n\t\tevent.isComposing ||\n\t\t// Always allow the current target\n\t\t(target !== event.currentTarget &&\n\t\t\t// Ignore contenteditable and form elements\n\t\t\ttarget.matches(\"[contenteditable],input,select,textarea\"))\n\t)\n}\n\n/**\n * There's a bug in Chrome that causes event.getModifierState not to exist on\n * KeyboardEvent's for F1/F2/etc keys.\n */\nfunction getModifierState(event: KeyboardEvent, mod: string) {\n\treturn typeof event.getModifierState === \"function\"\n\t\t? event.getModifierState(mod) ||\n\t\t\t\t(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState(\"AltGraph\"))\n\t\t: false\n}\n\n/**\n * Parses a keybinding string into its parts.\n *\n * ```\n * grammar = `<sequence>`\n * <sequence> = `<press> <press> <press> ...`\n * <press> = `<key>` or `<mods>+<key>`\n * <mods> = `<mod>+<mod>+...`\n * <mod> = `<modifier>` (required) or `[<modifier>]` (optional)\n * <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)\n * <key> = `(<regex>)` -> `/^(?:<regex>)$/iy` (case-insensitive)\n * ```\n */\nexport function parseKeybinding(str: string): KeybindingPress[] {\n\treturn str\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.map(press => {\n\t\t\tlet parts = press.split(/(?<=\\w|\\])\\+/)\n\n\t\t\tlet last: string | RegExp = parts.pop() as string\n\t\t\tlet regex = last.match(/^\\((.+)\\)$/)\n\t\t\tlet key = regex ? new RegExp(`^(?:${regex[1]})$`, \"iv\") : last\n\n\t\t\tlet requiredModifiers: string[] = []\n\t\t\tlet optionalModifiers: string[] = []\n\n\t\t\tfor (const part of parts) {\n\t\t\t\tlet optional = part.match(/^\\[(.*)\\]$/)\n\t\t\t\tlet mod = optional?.[1] ?? part\n\t\t\t\tmod = mod === \"$mod\" ? MOD : mod\n\t\t\t\tif (optional) {\n\t\t\t\t\toptionalModifiers.push(mod)\n\t\t\t\t} else {\n\t\t\t\t\trequiredModifiers.push(mod)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn [requiredModifiers, optionalModifiers, key]\n\t\t})\n}\n\n/**\n * This tells us if a single keyboard event matches a single keybinding press.\n */\nexport function matchKeybindingPress(\n\tevent: KeyboardEvent,\n\t[requiredModifiers, optionalModifiers, key]: KeybindingPress,\n): boolean {\n\tconst hasAltGraph = requiredModifiers.includes(\"AltGraph\")\n\t// prettier-ignore\n\treturn !(\n\t\t// Allow either the `event.key` or the `event.code`\n\t\t// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n\t\t// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\t\t(\n\t\t\tkey instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :\n\t\t\t(key.toUpperCase() !== event.key.toUpperCase() &&\n\t\t\tkey !== event.code)\n\t\t) ||\n\n\t\t// Ensure all required modifiers in the keybinding are pressed.\n\t\trequiredModifiers.find(mod => {\n\t\t\treturn !getModifierState(event, mod)\n\t\t}) ||\n\n\t\t// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a\n\t\t// keybinding. So if they are pressed but aren't part of the current\n\t\t// keybinding press, then we don't have a match.\n\t\tKEYBINDING_MODIFIER_KEYS.find(mod => {\n\t\t\treturn (\n\t\t\t\t!requiredModifiers.includes(mod) &&\n\t\t\t\t!optionalModifiers.includes(mod) &&\n\t\t\t\tkey !== mod &&\n\t\t\t\tgetModifierState(event, mod) &&\n\t\t\t\t// When AltGraph is required, its alias modifiers (e.g. Alt, Control)\n\t\t\t\t// being active is expected — don't treat them as unexpected modifiers.\n\t\t\t\t!(hasAltGraph && ALT_GRAPH_ALIASES.includes(mod))\n\t\t\t);\n\t\t})\n\t)\n}\n\n/**\n * Creates an event listener for handling keybindings.\n *\n * @example\n * ```js\n * import { createKeybindingsHandler } from \"../src/keybindings\"\n *\n * let handler = createKeybindingsHandler({\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n *\n * window.addEventListener(\"keydown\", handler)\n * ```\n */\nexport function createKeybindingsHandler(\n\tkeybindingsMap: KeybindingsMap,\n\toptions: KeybindingHandlerOptions = {},\n): EventListener {\n\tlet timeout = options.timeout ?? DEFAULT_TIMEOUT\n\tlet ignore = options.ignore ?? defaultKeybindingsHandlerIgnore\n\n\tlet keybindings = Object.keys(keybindingsMap).map(input => {\n\t\treturn [input, parseKeybinding(input), keybindingsMap[input]] as const\n\t})\n\n\tlet pending = new Map<string, KeybindingPress[]>()\n\tlet timer: number | null = null\n\n\treturn event => {\n\t\tif (!isKeyboardEvent(event) || ignore(event)) {\n\t\t\treturn\n\t\t}\n\n\t\tlet conflicts: Array<string> = []\n\t\tfor (let [input, sequence, handler] of keybindings) {\n\t\t\tlet prev = pending.get(input)\n\t\t\tlet expected = prev ? prev : sequence\n\t\t\tlet [current, ...rest] = expected\n\n\t\t\tlet matches = matchKeybindingPress(event, current)\n\n\t\t\tif (!matches) {\n\t\t\t\t// Modifier keydown events shouldn't break sequences\n\t\t\t\t// Note: This works because:\n\t\t\t\t// - non-modifiers will always return false\n\t\t\t\t// - if the current keypress is a modifier then it will return true when we check its state\n\t\t\t\t// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState\n\t\t\t\tif (!getModifierState(event, event.key)) {\n\t\t\t\t\tpending.delete(input)\n\t\t\t\t}\n\t\t\t} else if (rest.length > 0) {\n\t\t\t\tpending.set(input, rest)\n\t\t\t\tconflicts.push(input)\n\t\t\t} else {\n\t\t\t\tpending.delete(input)\n\t\t\t\tif (conflicts.length) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`tinykeys: Conflict found, \"${input}\" did not fire, waiting for:`,\n\t\t\t\t\t\tconflicts,\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\thandler(event)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (timer) {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\ttimer = setTimeout(() => pending.clear(), timeout)\n\t}\n}\n\n/**\n * Subscribes to keybindings.\n *\n * Returns an unsubscribe method.\n *\n * @example\n * ```js\n * import { tinykeys } from \"../src/tinykeys\"\n *\n * tinykeys(window, {\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n * ```\n */\nexport function tinykeys(\n\ttarget: Window | HTMLElement,\n\tkeybindingMap: KeybindingsMap,\n\toptions: KeybindingOptions = {},\n): () => void {\n\tlet event = options.event ?? DEFAULT_EVENT\n\tlet onKeyEvent = createKeybindingsHandler(keybindingMap, options)\n\ttarget.addEventListener(event, onKeyEvent, options.capture)\n\treturn () => {\n\t\ttarget.removeEventListener(event, onKeyEvent, options.capture)\n\t}\n}\n"],"mappings":"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"}
|
package/dist/tinykeys.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
//# sourceMappingURL=tinykeys.umd.js.map
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.tinykeys={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});let t=[`Shift`,`Meta`,`Alt`,`Control`],n=typeof navigator==`object`?navigator.platform:``,r=/Mac|iPod|iPhone|iPad/.test(n)?`Meta`:`Control`,i=n===`Win32`?[`Control`,`Alt`]:[`Alt`];function a(e){return!!(e.key&&e.code&&e.getModifierState)}function o(e){let t=e.target;return e.repeat||e.isComposing||t!==e.currentTarget&&t.matches(`[contenteditable],input,select,textarea`)}function s(e,t){return typeof e.getModifierState==`function`?e.getModifierState(t)||i.includes(t)&&e.getModifierState(`AltGraph`):!1}function c(e){return e.trim().split(` `).map(e=>{let t=e.split(/(?<=\w|\])\+/),n=t.pop(),i=n.match(/^\((.+)\)$/),a=i?RegExp(`^(?:${i[1]})$`,`iv`):n,o=[],s=[];for(let e of t){let t=e.match(/^\[(.*)\]$/),n=t?.[1]??e;n=n===`$mod`?r:n,t?s.push(n):o.push(n)}return[o,s,a]})}function l(e,[n,r,a]){let o=n.includes(`AltGraph`);return!((a instanceof RegExp?!(a.test(e.key)||a.test(e.code)):a.toUpperCase()!==e.key.toUpperCase()&&a!==e.code)||n.find(t=>!s(e,t))||t.find(t=>!n.includes(t)&&!r.includes(t)&&a!==t&&s(e,t)&&!(o&&i.includes(t))))}function u(e,t={}){let n=t.timeout??1e3,r=t.ignore??o,i=Object.keys(e).map(t=>[t,c(t),e[t]]),u=new Map,d=null;return e=>{if(!a(e)||r(e))return;let t=[];for(let[n,r,a]of i){let[i,...o]=u.get(n)||r;if(!l(e,i))s(e,e.key)||u.delete(n);else if(o.length>0)u.set(n,o),t.push(n);else if(u.delete(n),t.length)console.warn(`tinykeys: Conflict found, "${n}" did not fire, waiting for:`,t);else{a(e);break}}d&&clearTimeout(d),d=setTimeout(()=>u.clear(),n)}}function d(e,t,n={}){let r=n.event??`keydown`,i=u(t,n);return e.addEventListener(r,i,n.capture),()=>{e.removeEventListener(r,i,n.capture)}}e.createKeybindingsHandler=u,e.defaultKeybindingsHandlerIgnore=o,e.matchKeybindingPress=l,e.parseKeybinding=c,e.tinykeys=d});
|
|
2
|
+
//# sourceMappingURL=tinykeys.umd.js.map
|
package/dist/tinykeys.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinykeys.umd.js","sources":["../src/tinykeys.ts"],"sourcesContent":["/**\n * A single press of a keybinding sequence\n */\nexport type KeyBindingPress = [mods: string[], key: string | RegExp]\n\n/**\n * A map of keybinding strings to event handlers.\n */\nexport interface KeyBindingMap {\n\t[keybinding: string]: (event: KeyboardEvent) => void\n}\n\nexport interface KeyBindingHandlerOptions {\n\t/**\n\t * Keybinding sequences will wait this long between key presses before\n\t * cancelling (default: 1000).\n\t *\n\t * **Note:** Setting this value too low (i.e. `300`) will be too fast for many\n\t * of your users.\n\t */\n\ttimeout?: number\n}\n\n/**\n * Options to configure the behavior of keybindings.\n */\nexport interface KeyBindingOptions extends KeyBindingHandlerOptions {\n\t/**\n\t * Key presses will listen to this event (default: \"keydown\").\n\t */\n\tevent?: \"keydown\" | \"keyup\"\n\n\t/**\n\t * Key presses will use a capture listener (default: false)\n\t */\n\tcapture?: boolean\n}\n\n/**\n * These are the modifier keys that change the meaning of keybindings.\n *\n * Note: Ignoring \"AltGraph\" because it is covered by the others.\n */\nlet KEYBINDING_MODIFIER_KEYS = [\"Shift\", \"Meta\", \"Alt\", \"Control\"]\n\n/**\n * Keybinding sequences should timeout if individual key presses are more than\n * 1s apart by default.\n */\nlet DEFAULT_TIMEOUT = 1000\n\n/**\n * Keybinding sequences should bind to this event by default.\n */\nlet DEFAULT_EVENT = \"keydown\" as const\n\n/**\n * Platform detection code.\n * @see https://github.com/jamiebuilds/tinykeys/issues/184\n */\nlet PLATFORM = typeof navigator === \"object\" ? navigator.platform : \"\"\nlet APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM)\n\n/**\n * An alias for creating platform-specific keybinding aliases.\n */\nlet MOD = APPLE_DEVICE ? \"Meta\" : \"Control\"\n\n/**\n * Meaning of `AltGraph`, from MDN:\n * - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed\n * - Mac: ⌥ Option key pressed\n * - Linux: Level 3 Shift key (or Level 5 Shift key) pressed\n * - Android: Not supported\n * @see https://github.com/jamiebuilds/tinykeys/issues/185\n */\nlet ALT_GRAPH_ALIASES =\n\tPLATFORM === \"Win32\" ? [\"Control\", \"Alt\"] : APPLE_DEVICE ? [\"Alt\"] : []\n\n/**\n * There's a bug in Chrome that causes event.getModifierState not to exist on\n * KeyboardEvent's for F1/F2/etc keys.\n */\nfunction getModifierState(event: KeyboardEvent, mod: string) {\n\treturn typeof event.getModifierState === \"function\"\n\t\t? event.getModifierState(mod) ||\n\t\t\t\t(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState(\"AltGraph\"))\n\t\t: false\n}\n\n/**\n * Parses a \"Key Binding String\" into its parts\n *\n * grammar = `<sequence>`\n * <sequence> = `<press> <press> <press> ...`\n * <press> = `<key>` or `<mods>+<key>`\n * <mods> = `<mod>+<mod>+...`\n * <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)\n * <key> = `(<regex>)` -> `/^<regex>$/` (case-sensitive)\n */\nexport function parseKeybinding(str: string): KeyBindingPress[] {\n\treturn str\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.map(press => {\n\t\t\tlet mods = press.split(/\\b\\+/)\n\t\t\tlet key: string | RegExp = mods.pop() as string\n\t\t\tlet match = key.match(/^\\((.+)\\)$/)\n\t\t\tif (match) {\n\t\t\t\tkey = new RegExp(`^${match[1]}$`)\n\t\t\t}\n\t\t\tmods = mods.map(mod => (mod === \"$mod\" ? MOD : mod))\n\t\t\treturn [mods, key]\n\t\t})\n}\n\n/**\n * This tells us if a single keyboard event matches a single keybinding press.\n */\nexport function matchKeyBindingPress(\n\tevent: KeyboardEvent,\n\t[mods, key]: KeyBindingPress,\n): boolean {\n\t// prettier-ignore\n\treturn !(\n\t\t// Allow either the `event.key` or the `event.code`\n\t\t// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n\t\t// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\t\t(\n\t\t\tkey instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :\n\t\t\t(key.toUpperCase() !== event.key.toUpperCase() &&\n\t\t\tkey !== event.code)\n\t\t) ||\n\n\t\t// Ensure all the modifiers in the keybinding are pressed.\n\t\tmods.find(mod => {\n\t\t\treturn !getModifierState(event, mod)\n\t\t}) ||\n\n\t\t// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a\n\t\t// keybinding. So if they are pressed but aren't part of the current\n\t\t// keybinding press, then we don't have a match.\n\t\tKEYBINDING_MODIFIER_KEYS.find(mod => {\n\t\t\treturn !mods.includes(mod) && key !== mod && getModifierState(event, mod)\n\t\t})\n\t)\n}\n\n/**\n * Creates an event listener for handling keybindings.\n *\n * @example\n * ```js\n * import { createKeybindingsHandler } from \"../src/keybindings\"\n *\n * let handler = createKeybindingsHandler({\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n *\n * window.addEvenListener(\"keydown\", handler)\n * ```\n */\nexport function createKeybindingsHandler(\n\tkeyBindingMap: KeyBindingMap,\n\toptions: KeyBindingHandlerOptions = {},\n): EventListener {\n\tlet timeout = options.timeout ?? DEFAULT_TIMEOUT\n\n\tlet keyBindings = Object.keys(keyBindingMap).map(key => {\n\t\treturn [parseKeybinding(key), keyBindingMap[key]] as const\n\t})\n\n\tlet possibleMatches = new Map<KeyBindingPress[], KeyBindingPress[]>()\n\tlet timer: number | null = null\n\n\treturn event => {\n\t\t// Ensure and stop any event that isn't a full keyboard event.\n\t\t// Autocomplete option navigation and selection would fire a instanceof Event,\n\t\t// instead of the expected KeyboardEvent\n\t\tif (!(event instanceof KeyboardEvent)) {\n\t\t\treturn\n\t\t}\n\n\t\tkeyBindings.forEach(keyBinding => {\n\t\t\tlet sequence = keyBinding[0]\n\t\t\tlet callback = keyBinding[1]\n\n\t\t\tlet prev = possibleMatches.get(sequence)\n\t\t\tlet remainingExpectedPresses = prev ? prev : sequence\n\t\t\tlet currentExpectedPress = remainingExpectedPresses[0]\n\n\t\t\tlet matches = matchKeyBindingPress(event, currentExpectedPress)\n\n\t\t\tif (!matches) {\n\t\t\t\t// Modifier keydown events shouldn't break sequences\n\t\t\t\t// Note: This works because:\n\t\t\t\t// - non-modifiers will always return false\n\t\t\t\t// - if the current keypress is a modifier then it will return true when we check its state\n\t\t\t\t// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState\n\t\t\t\tif (!getModifierState(event, event.key)) {\n\t\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\t}\n\t\t\t} else if (remainingExpectedPresses.length > 1) {\n\t\t\t\tpossibleMatches.set(sequence, remainingExpectedPresses.slice(1))\n\t\t\t} else {\n\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\tcallback(event)\n\t\t\t}\n\t\t})\n\n\t\tif (timer) {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\ttimer = setTimeout(possibleMatches.clear.bind(possibleMatches), timeout)\n\t}\n}\n\n/**\n * Subscribes to keybindings.\n *\n * Returns an unsubscribe method.\n *\n * @example\n * ```js\n * import { tinykeys } from \"../src/tinykeys\"\n *\n * tinykeys(window, {\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n * ```\n */\nexport function tinykeys(\n\ttarget: Window | HTMLElement,\n\tkeyBindingMap: KeyBindingMap,\n\t{ event = DEFAULT_EVENT, capture, timeout }: KeyBindingOptions = {},\n): () => void {\n\tlet onKeyEvent = createKeybindingsHandler(keyBindingMap, { timeout })\n\ttarget.addEventListener(event, onKeyEvent, capture)\n\treturn () => {\n\t\ttarget.removeEventListener(event, onKeyEvent, capture)\n\t}\n}\n"],"names":["KEYBINDING_MODIFIER_KEYS","PLATFORM","navigator","platform","APPLE_DEVICE","test","MOD","ALT_GRAPH_ALIASES","getModifierState","event","mod","includes","parseKeybinding","str","trim","split","map","press","mods","key","pop","match","RegExp","matchKeyBindingPress","_ref","code","toUpperCase","find","createKeybindingsHandler","keyBindingMap","options","timeout","_options$timeout","keyBindings","Object","keys","possibleMatches","Map","timer","KeyboardEvent","forEach","keyBinding","sequence","callback","remainingExpectedPresses","get","length","set","slice","clearTimeout","setTimeout","clear","bind","target","_temp","_ref2$event","_ref2","capture","onKeyEvent","addEventListener","removeEventListener"],"mappings":"kOA2CA,IAAIA,EAA2B,CAAC,QAAS,OAAQ,MAAO,WAiBpDC,EAAgC,iBAAdC,UAAyBA,UAAUC,SAAW,GAChEC,EAAe,uBAAuBC,KAAKJ,GAK3CK,EAAMF,EAAe,OAAS,UAU9BG,EACU,UAAbN,EAAuB,CAAC,UAAW,OAASG,EAAe,CAAC,OAAS,GAMtE,SAASI,EAAiBC,EAAsBC,GAC/C,MAAyC,mBAA3BD,EAAMD,mBACjBC,EAAMD,iBAAiBE,IACtBH,EAAkBI,SAASD,IAAQD,EAAMD,iBAAiB,sBAc/CI,EAAgBC,GAC/B,OAAOA,EACLC,OACAC,MAAM,KACNC,IAAI,SAAAC,GACJ,IAAIC,EAAOD,EAAMF,MAAM,QACnBI,EAAuBD,EAAKE,MAC5BC,EAAQF,EAAIE,MAAM,cAKtB,OAJIA,IACHF,EAAM,IAAIG,WAAWD,EAAM,SAGrB,CADPH,EAAOA,EAAKF,IAAI,SAAAN,SAAgB,SAARA,EAAiBJ,EAAMI,IACjCS,cAODI,EACfd,EAAoBe,OACnBN,EAAIM,KAAEL,EAAGK,KAGV,SAKEL,aAAeG,QAAWH,EAAId,KAAKI,EAAMU,OAAQA,EAAId,KAAKI,EAAMgB,MAC/DN,EAAIO,gBAAkBjB,EAAMU,IAAIO,eACjCP,IAAQV,EAAMgB,OAIfP,EAAKS,KAAK,SAAAjB,GACT,OAAQF,EAAiBC,EAAOC,MAMjCV,EAAyB2B,KAAK,SAAAjB,GAC7B,OAAQQ,EAAKP,SAASD,IAAQS,IAAQT,GAAOF,EAAiBC,EAAOC,eA2BxDkB,EACfC,EACAC,kBAAAA,IAAAA,EAAoC,IAEpC,IAAIC,SAAOC,EAAGF,EAAQC,SAAOC,EA7HR,IA+HjBC,EAAcC,OAAOC,KAAKN,GAAeb,IAAI,SAAAG,GAChD,MAAO,CAACP,EAAgBO,GAAMU,EAAcV,MAGzCiB,EAAkB,IAAIC,IACtBC,EAAuB,KAE3B,gBAAO7B,GAIAA,aAAiB8B,gBAIvBN,EAAYO,QAAQ,SAAAC,GACnB,IAAIC,EAAWD,EAAW,GACtBE,EAAWF,EAAW,GAGtBG,EADOR,EAAgBS,IAAIH,IACcA,EAG/BnB,EAAqBd,EAFRmC,EAAyB,IAazCA,EAAyBE,OAAS,EAC5CV,EAAgBW,IAAIL,EAAUE,EAAyBI,MAAM,KAE7DZ,SAAuBM,GACvBC,EAASlC,IAPJD,EAAiBC,EAAOA,EAAMU,MAClCiB,SAAuBM,KAUtBJ,GACHW,aAAaX,GAGdA,EAAQY,WAAWd,EAAgBe,MAAMC,KAAKhB,GAAkBL,mGA2BjEsB,EACAxB,EAA4ByB,oBACqC,GAAEA,EAAAC,EAAAC,EAAjE/C,MAAAA,WAAK8C,EArMY,UAqMIA,EAAEE,EAAOD,EAAPC,QAErBC,EAAa9B,EAAyBC,EAAe,CAAEE,QAFlByB,EAAPzB,UAIlC,OADAsB,EAAOM,iBAAiBlD,EAAOiD,EAAYD,cAE1CJ,EAAOO,oBAAoBnD,EAAOiD,EAAYD"}
|
|
1
|
+
{"version":3,"file":"tinykeys.umd.js","names":[],"sources":["../src/tinykeys.ts"],"sourcesContent":["/**\n * A single press of a keybinding sequence.\n */\nexport type KeybindingPress = readonly [\n\trequiredModifiers: ReadonlyArray<string>,\n\toptionalModifiers: ReadonlyArray<string>,\n\tkey: string | RegExp,\n]\n\n/**\n * Keyboard event callback fired when keybinding is triggered.\n */\nexport type KeybindingHandler = (event: KeyboardEvent) => void\n\n/**\n * A map of keybinding strings to event handlers.\n */\nexport type KeybindingsMap = Record<string, KeybindingHandler>\n\n/**\n * Predicate that returns true if a keyboard event should be ignored.\n */\nexport type KeybindingFilter = (event: KeyboardEvent) => boolean\n\nexport interface KeybindingHandlerOptions {\n\t/**\n\t * Keybinding sequences will wait this long between key presses before\n\t * cancelling (default: 1000).\n\t *\n\t * **Note:** Setting this value too low (i.e. `300`) will be too fast for many\n\t * of your users.\n\t */\n\ttimeout?: number\n\n\t/**\n\t * Customize the behavior of which keyboard events will be ignored/skipped.\n\t *\n\t * By default this uses the behavior of {@link defaultKeybindingsHandlerIgnore}.\n\t *\n\t * @example Allow all events\n\t * ```tsx\n\t * tinykeys(window, {...}, {\n\t * ignore: () => false\n\t * })\n\t * ```\n\t *\n\t * @example Extend the default ignore\n\t * ```tsx\n\t * tinykeys(window, {...}, {\n\t * ignore: event => {\n\t * return (\n\t * // Also ignore events inside a dialog\n\t * event.target.closest(\"dialog\") != null &&\n\t * defaultKeybindingsHandlerIgnore(event)\n\t * );\n\t * }\n\t * })\n\t * ```\n\t */\n\tignore?: KeybindingFilter\n}\n\n/**\n * Options to configure the behavior of keybindings.\n */\nexport interface KeybindingOptions extends KeybindingHandlerOptions {\n\t/**\n\t * Key presses will listen to this event (default: \"keydown\").\n\t */\n\tevent?: \"keydown\" | \"keyup\"\n\n\t/**\n\t * Key presses will use a capture listener (default: false)\n\t */\n\tcapture?: boolean\n}\n\n/**\n * These are the modifier keys that change the meaning of keybindings.\n *\n * Note: Ignoring \"AltGraph\" because it is covered by the others.\n */\nlet KEYBINDING_MODIFIER_KEYS = [\"Shift\", \"Meta\", \"Alt\", \"Control\"]\n\n/**\n * Keybinding sequences should timeout if individual key presses are more than\n * 1s apart by default.\n */\nlet DEFAULT_TIMEOUT = 1000\n\n/**\n * Keybinding sequences should bind to this event by default.\n */\nlet DEFAULT_EVENT = \"keydown\" as const\n\n/**\n * Platform detection code.\n * @see https://github.com/jamiebuilds/tinykeys/issues/184\n */\nlet PLATFORM = typeof navigator === \"object\" ? navigator.platform : \"\"\nlet APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM)\n\n/**\n * An alias for creating platform-specific keybinding aliases.\n */\nlet MOD = APPLE_DEVICE ? \"Meta\" : \"Control\"\n\n/**\n * Meaning of `AltGraph`, from MDN:\n * - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed\n * - Mac: ⌥ Option key pressed\n * - Linux: Level 3 Shift key (or Level 5 Shift key) pressed\n * - Android: Not supported\n * @see https://github.com/jamiebuilds/tinykeys/issues/185\n */\nlet ALT_GRAPH_ALIASES = PLATFORM === \"Win32\" ? [\"Control\", \"Alt\"] : [\"Alt\"]\n\n/**\n * Ensure and stop any event that isn't a full keyboard event.\n * Autocomplete option navigation and selection would fire an Event,\n * instead of the expected KeyboardEvent\n */\nfunction isKeyboardEvent(\n\tevent: Partial<KeyboardEvent>,\n): event is KeyboardEvent {\n\treturn !!(event.key && event.code && event.getModifierState)\n}\n\n/**\n * Ignores keyboard events from contenteditable and form elements unless they\n * are the current target.\n */\nexport function defaultKeybindingsHandlerIgnore(event: KeyboardEvent) {\n\tlet target = event.target as HTMLElement\n\treturn (\n\t\t// Always ignore repeated keyboard events\n\t\tevent.repeat ||\n\t\t// Always ignore keyboard events during composition input\n\t\tevent.isComposing ||\n\t\t// Always allow the current target\n\t\t(target !== event.currentTarget &&\n\t\t\t// Ignore contenteditable and form elements\n\t\t\ttarget.matches(\"[contenteditable],input,select,textarea\"))\n\t)\n}\n\n/**\n * There's a bug in Chrome that causes event.getModifierState not to exist on\n * KeyboardEvent's for F1/F2/etc keys.\n */\nfunction getModifierState(event: KeyboardEvent, mod: string) {\n\treturn typeof event.getModifierState === \"function\"\n\t\t? event.getModifierState(mod) ||\n\t\t\t\t(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState(\"AltGraph\"))\n\t\t: false\n}\n\n/**\n * Parses a keybinding string into its parts.\n *\n * ```\n * grammar = `<sequence>`\n * <sequence> = `<press> <press> <press> ...`\n * <press> = `<key>` or `<mods>+<key>`\n * <mods> = `<mod>+<mod>+...`\n * <mod> = `<modifier>` (required) or `[<modifier>]` (optional)\n * <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)\n * <key> = `(<regex>)` -> `/^(?:<regex>)$/iy` (case-insensitive)\n * ```\n */\nexport function parseKeybinding(str: string): KeybindingPress[] {\n\treturn str\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.map(press => {\n\t\t\tlet parts = press.split(/(?<=\\w|\\])\\+/)\n\n\t\t\tlet last: string | RegExp = parts.pop() as string\n\t\t\tlet regex = last.match(/^\\((.+)\\)$/)\n\t\t\tlet key = regex ? new RegExp(`^(?:${regex[1]})$`, \"iv\") : last\n\n\t\t\tlet requiredModifiers: string[] = []\n\t\t\tlet optionalModifiers: string[] = []\n\n\t\t\tfor (const part of parts) {\n\t\t\t\tlet optional = part.match(/^\\[(.*)\\]$/)\n\t\t\t\tlet mod = optional?.[1] ?? part\n\t\t\t\tmod = mod === \"$mod\" ? MOD : mod\n\t\t\t\tif (optional) {\n\t\t\t\t\toptionalModifiers.push(mod)\n\t\t\t\t} else {\n\t\t\t\t\trequiredModifiers.push(mod)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn [requiredModifiers, optionalModifiers, key]\n\t\t})\n}\n\n/**\n * This tells us if a single keyboard event matches a single keybinding press.\n */\nexport function matchKeybindingPress(\n\tevent: KeyboardEvent,\n\t[requiredModifiers, optionalModifiers, key]: KeybindingPress,\n): boolean {\n\tconst hasAltGraph = requiredModifiers.includes(\"AltGraph\")\n\t// prettier-ignore\n\treturn !(\n\t\t// Allow either the `event.key` or the `event.code`\n\t\t// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n\t\t// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\t\t(\n\t\t\tkey instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :\n\t\t\t(key.toUpperCase() !== event.key.toUpperCase() &&\n\t\t\tkey !== event.code)\n\t\t) ||\n\n\t\t// Ensure all required modifiers in the keybinding are pressed.\n\t\trequiredModifiers.find(mod => {\n\t\t\treturn !getModifierState(event, mod)\n\t\t}) ||\n\n\t\t// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a\n\t\t// keybinding. So if they are pressed but aren't part of the current\n\t\t// keybinding press, then we don't have a match.\n\t\tKEYBINDING_MODIFIER_KEYS.find(mod => {\n\t\t\treturn (\n\t\t\t\t!requiredModifiers.includes(mod) &&\n\t\t\t\t!optionalModifiers.includes(mod) &&\n\t\t\t\tkey !== mod &&\n\t\t\t\tgetModifierState(event, mod) &&\n\t\t\t\t// When AltGraph is required, its alias modifiers (e.g. Alt, Control)\n\t\t\t\t// being active is expected — don't treat them as unexpected modifiers.\n\t\t\t\t!(hasAltGraph && ALT_GRAPH_ALIASES.includes(mod))\n\t\t\t);\n\t\t})\n\t)\n}\n\n/**\n * Creates an event listener for handling keybindings.\n *\n * @example\n * ```js\n * import { createKeybindingsHandler } from \"../src/keybindings\"\n *\n * let handler = createKeybindingsHandler({\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n *\n * window.addEventListener(\"keydown\", handler)\n * ```\n */\nexport function createKeybindingsHandler(\n\tkeybindingsMap: KeybindingsMap,\n\toptions: KeybindingHandlerOptions = {},\n): EventListener {\n\tlet timeout = options.timeout ?? DEFAULT_TIMEOUT\n\tlet ignore = options.ignore ?? defaultKeybindingsHandlerIgnore\n\n\tlet keybindings = Object.keys(keybindingsMap).map(input => {\n\t\treturn [input, parseKeybinding(input), keybindingsMap[input]] as const\n\t})\n\n\tlet pending = new Map<string, KeybindingPress[]>()\n\tlet timer: number | null = null\n\n\treturn event => {\n\t\tif (!isKeyboardEvent(event) || ignore(event)) {\n\t\t\treturn\n\t\t}\n\n\t\tlet conflicts: Array<string> = []\n\t\tfor (let [input, sequence, handler] of keybindings) {\n\t\t\tlet prev = pending.get(input)\n\t\t\tlet expected = prev ? prev : sequence\n\t\t\tlet [current, ...rest] = expected\n\n\t\t\tlet matches = matchKeybindingPress(event, current)\n\n\t\t\tif (!matches) {\n\t\t\t\t// Modifier keydown events shouldn't break sequences\n\t\t\t\t// Note: This works because:\n\t\t\t\t// - non-modifiers will always return false\n\t\t\t\t// - if the current keypress is a modifier then it will return true when we check its state\n\t\t\t\t// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState\n\t\t\t\tif (!getModifierState(event, event.key)) {\n\t\t\t\t\tpending.delete(input)\n\t\t\t\t}\n\t\t\t} else if (rest.length > 0) {\n\t\t\t\tpending.set(input, rest)\n\t\t\t\tconflicts.push(input)\n\t\t\t} else {\n\t\t\t\tpending.delete(input)\n\t\t\t\tif (conflicts.length) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`tinykeys: Conflict found, \"${input}\" did not fire, waiting for:`,\n\t\t\t\t\t\tconflicts,\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\thandler(event)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (timer) {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\ttimer = setTimeout(() => pending.clear(), timeout)\n\t}\n}\n\n/**\n * Subscribes to keybindings.\n *\n * Returns an unsubscribe method.\n *\n * @example\n * ```js\n * import { tinykeys } from \"../src/tinykeys\"\n *\n * tinykeys(window, {\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n * ```\n */\nexport function tinykeys(\n\ttarget: Window | HTMLElement,\n\tkeybindingMap: KeybindingsMap,\n\toptions: KeybindingOptions = {},\n): () => void {\n\tlet event = options.event ?? DEFAULT_EVENT\n\tlet onKeyEvent = createKeybindingsHandler(keybindingMap, options)\n\ttarget.addEventListener(event, onKeyEvent, options.capture)\n\treturn () => {\n\t\ttarget.removeEventListener(event, onKeyEvent, options.capture)\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinykeys",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A tiny (~
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "A tiny (~1KB) & modern library for keybindings.",
|
|
5
5
|
"author": "Jamie Kyle <me@thejameskyle.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "jamiebuilds/tinykeys",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"sideEffects": false,
|
|
8
10
|
"source": "src/tinykeys.ts",
|
|
9
|
-
"main": "dist/tinykeys.
|
|
10
|
-
"module": "dist/tinykeys.
|
|
11
|
+
"main": "dist/tinykeys.cjs",
|
|
12
|
+
"module": "dist/tinykeys.mjs",
|
|
11
13
|
"unpkg": "dist/tinykeys.umd.js",
|
|
12
|
-
"types": "dist/tinykeys.d.
|
|
14
|
+
"types": "dist/tinykeys.d.mts",
|
|
13
15
|
"files": [
|
|
14
16
|
"dist"
|
|
15
17
|
],
|
|
16
18
|
"exports": {
|
|
17
19
|
".": {
|
|
18
|
-
"import":
|
|
19
|
-
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/tinykeys.d.mts",
|
|
22
|
+
"default": "./dist/tinykeys.mjs"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/tinykeys.d.cts",
|
|
26
|
+
"default": "./dist/tinykeys.cjs"
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
22
33
|
"keywords": [
|
|
23
34
|
"key",
|
|
24
35
|
"keys",
|
|
@@ -39,48 +50,45 @@
|
|
|
39
50
|
"shortcuts"
|
|
40
51
|
],
|
|
41
52
|
"scripts": {
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"lint": "eslint
|
|
46
|
-
"format": "prettier --
|
|
47
|
-
"
|
|
48
|
-
"build": "
|
|
49
|
-
"
|
|
50
|
-
"start": "
|
|
53
|
+
"format": "prettier --write .",
|
|
54
|
+
"check": "npm run -s check:types && npm run -s lint && npm run -s check:format",
|
|
55
|
+
"check:types": "tsc --noEmit",
|
|
56
|
+
"check:lint": "eslint .",
|
|
57
|
+
"check:format": "prettier --check .",
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"build:example": "vite build ./example --base ./",
|
|
60
|
+
"test": "vitest",
|
|
61
|
+
"start": "vite example --base ./",
|
|
51
62
|
"precommit": "lint-staged",
|
|
52
63
|
"prepublishOnly": "npm run -s build"
|
|
53
64
|
},
|
|
54
65
|
"devDependencies": {
|
|
55
|
-
"@
|
|
56
|
-
"@
|
|
57
|
-
"@
|
|
58
|
-
"
|
|
59
|
-
"canvas-confetti": "^1.9.
|
|
60
|
-
"eslint": "^
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
66
|
+
"@arethetypeswrong/core": "^0.18.2",
|
|
67
|
+
"@eslint/js": "^10.0.1",
|
|
68
|
+
"@types/canvas-confetti": "^1.9.0",
|
|
69
|
+
"@vitest/browser-playwright": "^4.1.6",
|
|
70
|
+
"canvas-confetti": "^1.9.4",
|
|
71
|
+
"eslint": "^10.4.0",
|
|
72
|
+
"husky": "^9.1.7",
|
|
73
|
+
"lint-staged": "^17.0.5",
|
|
74
|
+
"prettier": "^3.8.3",
|
|
75
|
+
"prettier-plugin-tailwindcss": "^0.8.0",
|
|
76
|
+
"publint": "^0.3.21",
|
|
77
|
+
"tsdown": "^0.22.0",
|
|
78
|
+
"typescript": "^6.0.3",
|
|
79
|
+
"typescript-eslint": "^8.59.4",
|
|
80
|
+
"vite": "^8.0.13",
|
|
81
|
+
"vitest": "^4.1.6"
|
|
82
|
+
},
|
|
83
|
+
"overrides": {
|
|
84
|
+
"@arethetypeswrong/core": {
|
|
85
|
+
"fflate": "0.8.2"
|
|
86
|
+
}
|
|
70
87
|
},
|
|
71
88
|
"lint-staged": {
|
|
72
89
|
"*": [
|
|
73
90
|
"prettier --write",
|
|
74
91
|
"git add"
|
|
75
92
|
]
|
|
76
|
-
},
|
|
77
|
-
"ava": {
|
|
78
|
-
"extensions": [
|
|
79
|
-
"ts",
|
|
80
|
-
"tsx"
|
|
81
|
-
],
|
|
82
|
-
"require": [
|
|
83
|
-
"ts-node/register"
|
|
84
|
-
]
|
|
85
93
|
}
|
|
86
94
|
}
|
package/dist/tinykeys.d.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A single press of a keybinding sequence
|
|
3
|
-
*/
|
|
4
|
-
export declare type KeyBindingPress = [mods: string[], key: string | RegExp];
|
|
5
|
-
/**
|
|
6
|
-
* A map of keybinding strings to event handlers.
|
|
7
|
-
*/
|
|
8
|
-
export interface KeyBindingMap {
|
|
9
|
-
[keybinding: string]: (event: KeyboardEvent) => void;
|
|
10
|
-
}
|
|
11
|
-
export interface KeyBindingHandlerOptions {
|
|
12
|
-
/**
|
|
13
|
-
* Keybinding sequences will wait this long between key presses before
|
|
14
|
-
* cancelling (default: 1000).
|
|
15
|
-
*
|
|
16
|
-
* **Note:** Setting this value too low (i.e. `300`) will be too fast for many
|
|
17
|
-
* of your users.
|
|
18
|
-
*/
|
|
19
|
-
timeout?: number;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Options to configure the behavior of keybindings.
|
|
23
|
-
*/
|
|
24
|
-
export interface KeyBindingOptions extends KeyBindingHandlerOptions {
|
|
25
|
-
/**
|
|
26
|
-
* Key presses will listen to this event (default: "keydown").
|
|
27
|
-
*/
|
|
28
|
-
event?: "keydown" | "keyup";
|
|
29
|
-
/**
|
|
30
|
-
* Key presses will use a capture listener (default: false)
|
|
31
|
-
*/
|
|
32
|
-
capture?: boolean;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Parses a "Key Binding String" into its parts
|
|
36
|
-
*
|
|
37
|
-
* grammar = `<sequence>`
|
|
38
|
-
* <sequence> = `<press> <press> <press> ...`
|
|
39
|
-
* <press> = `<key>` or `<mods>+<key>`
|
|
40
|
-
* <mods> = `<mod>+<mod>+...`
|
|
41
|
-
* <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)
|
|
42
|
-
* <key> = `(<regex>)` -> `/^<regex>$/` (case-sensitive)
|
|
43
|
-
*/
|
|
44
|
-
export declare function parseKeybinding(str: string): KeyBindingPress[];
|
|
45
|
-
/**
|
|
46
|
-
* This tells us if a single keyboard event matches a single keybinding press.
|
|
47
|
-
*/
|
|
48
|
-
export declare function matchKeyBindingPress(event: KeyboardEvent, [mods, key]: KeyBindingPress): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Creates an event listener for handling keybindings.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```js
|
|
54
|
-
* import { createKeybindingsHandler } from "../src/keybindings"
|
|
55
|
-
*
|
|
56
|
-
* let handler = createKeybindingsHandler({
|
|
57
|
-
* "Shift+d": () => {
|
|
58
|
-
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
59
|
-
* },
|
|
60
|
-
* "y e e t": () => {
|
|
61
|
-
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
62
|
-
* },
|
|
63
|
-
* "$mod+d": () => {
|
|
64
|
-
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
65
|
-
* },
|
|
66
|
-
* })
|
|
67
|
-
*
|
|
68
|
-
* window.addEvenListener("keydown", handler)
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export declare function createKeybindingsHandler(keyBindingMap: KeyBindingMap, options?: KeyBindingHandlerOptions): EventListener;
|
|
72
|
-
/**
|
|
73
|
-
* Subscribes to keybindings.
|
|
74
|
-
*
|
|
75
|
-
* Returns an unsubscribe method.
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* ```js
|
|
79
|
-
* import { tinykeys } from "../src/tinykeys"
|
|
80
|
-
*
|
|
81
|
-
* tinykeys(window, {
|
|
82
|
-
* "Shift+d": () => {
|
|
83
|
-
* alert("The 'Shift' and 'd' keys were pressed at the same time")
|
|
84
|
-
* },
|
|
85
|
-
* "y e e t": () => {
|
|
86
|
-
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
|
|
87
|
-
* },
|
|
88
|
-
* "$mod+d": () => {
|
|
89
|
-
* alert("Either 'Control+d' or 'Meta+d' were pressed")
|
|
90
|
-
* },
|
|
91
|
-
* })
|
|
92
|
-
* ```
|
|
93
|
-
*/
|
|
94
|
-
export declare function tinykeys(target: Window | HTMLElement, keyBindingMap: KeyBindingMap, { event, capture, timeout }?: KeyBindingOptions): () => void;
|
package/dist/tinykeys.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
var e=["Shift","Meta","Alt","Control"],t="object"==typeof navigator?navigator.platform:"",n=/Mac|iPod|iPhone|iPad/.test(t),r=n?"Meta":"Control",i="Win32"===t?["Control","Alt"]:n?["Alt"]:[];function o(e,t){return"function"==typeof e.getModifierState&&(e.getModifierState(t)||i.includes(t)&&e.getModifierState("AltGraph"))}function a(e){return e.trim().split(" ").map(function(e){var t=e.split(/\b\+/),n=t.pop(),i=n.match(/^\((.+)\)$/);return i&&(n=new RegExp("^"+i[1]+"$")),[t=t.map(function(e){return"$mod"===e?r:e}),n]})}function u(t,n){var r=n[0],i=n[1];return!((i instanceof RegExp?!i.test(t.key)&&!i.test(t.code):i.toUpperCase()!==t.key.toUpperCase()&&i!==t.code)||r.find(function(e){return!o(t,e)})||e.find(function(e){return!r.includes(e)&&i!==e&&o(t,e)}))}function c(e,t){var n;void 0===t&&(t={});var r=null!=(n=t.timeout)?n:1e3,i=Object.keys(e).map(function(t){return[a(t),e[t]]}),c=new Map,d=null;return function(e){e instanceof KeyboardEvent&&(i.forEach(function(t){var n=t[0],r=t[1],i=c.get(n)||n;u(e,i[0])?i.length>1?c.set(n,i.slice(1)):(c.delete(n),r(e)):o(e,e.key)||c.delete(n)}),d&&clearTimeout(d),d=setTimeout(c.clear.bind(c),r))}}exports.createKeybindingsHandler=c,exports.matchKeyBindingPress=u,exports.parseKeybinding=a,exports.tinykeys=function(e,t,n){var r=void 0===n?{}:n,i=r.event,o=void 0===i?"keydown":i,a=r.capture,u=c(t,{timeout:r.timeout});return e.addEventListener(o,u,a),function(){e.removeEventListener(o,u,a)}};
|
|
2
|
-
//# sourceMappingURL=tinykeys.js.map
|
package/dist/tinykeys.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tinykeys.js","sources":["../src/tinykeys.ts"],"sourcesContent":["/**\n * A single press of a keybinding sequence\n */\nexport type KeyBindingPress = [mods: string[], key: string | RegExp]\n\n/**\n * A map of keybinding strings to event handlers.\n */\nexport interface KeyBindingMap {\n\t[keybinding: string]: (event: KeyboardEvent) => void\n}\n\nexport interface KeyBindingHandlerOptions {\n\t/**\n\t * Keybinding sequences will wait this long between key presses before\n\t * cancelling (default: 1000).\n\t *\n\t * **Note:** Setting this value too low (i.e. `300`) will be too fast for many\n\t * of your users.\n\t */\n\ttimeout?: number\n}\n\n/**\n * Options to configure the behavior of keybindings.\n */\nexport interface KeyBindingOptions extends KeyBindingHandlerOptions {\n\t/**\n\t * Key presses will listen to this event (default: \"keydown\").\n\t */\n\tevent?: \"keydown\" | \"keyup\"\n\n\t/**\n\t * Key presses will use a capture listener (default: false)\n\t */\n\tcapture?: boolean\n}\n\n/**\n * These are the modifier keys that change the meaning of keybindings.\n *\n * Note: Ignoring \"AltGraph\" because it is covered by the others.\n */\nlet KEYBINDING_MODIFIER_KEYS = [\"Shift\", \"Meta\", \"Alt\", \"Control\"]\n\n/**\n * Keybinding sequences should timeout if individual key presses are more than\n * 1s apart by default.\n */\nlet DEFAULT_TIMEOUT = 1000\n\n/**\n * Keybinding sequences should bind to this event by default.\n */\nlet DEFAULT_EVENT = \"keydown\" as const\n\n/**\n * Platform detection code.\n * @see https://github.com/jamiebuilds/tinykeys/issues/184\n */\nlet PLATFORM = typeof navigator === \"object\" ? navigator.platform : \"\"\nlet APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM)\n\n/**\n * An alias for creating platform-specific keybinding aliases.\n */\nlet MOD = APPLE_DEVICE ? \"Meta\" : \"Control\"\n\n/**\n * Meaning of `AltGraph`, from MDN:\n * - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed\n * - Mac: ⌥ Option key pressed\n * - Linux: Level 3 Shift key (or Level 5 Shift key) pressed\n * - Android: Not supported\n * @see https://github.com/jamiebuilds/tinykeys/issues/185\n */\nlet ALT_GRAPH_ALIASES =\n\tPLATFORM === \"Win32\" ? [\"Control\", \"Alt\"] : APPLE_DEVICE ? [\"Alt\"] : []\n\n/**\n * There's a bug in Chrome that causes event.getModifierState not to exist on\n * KeyboardEvent's for F1/F2/etc keys.\n */\nfunction getModifierState(event: KeyboardEvent, mod: string) {\n\treturn typeof event.getModifierState === \"function\"\n\t\t? event.getModifierState(mod) ||\n\t\t\t\t(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState(\"AltGraph\"))\n\t\t: false\n}\n\n/**\n * Parses a \"Key Binding String\" into its parts\n *\n * grammar = `<sequence>`\n * <sequence> = `<press> <press> <press> ...`\n * <press> = `<key>` or `<mods>+<key>`\n * <mods> = `<mod>+<mod>+...`\n * <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)\n * <key> = `(<regex>)` -> `/^<regex>$/` (case-sensitive)\n */\nexport function parseKeybinding(str: string): KeyBindingPress[] {\n\treturn str\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.map(press => {\n\t\t\tlet mods = press.split(/\\b\\+/)\n\t\t\tlet key: string | RegExp = mods.pop() as string\n\t\t\tlet match = key.match(/^\\((.+)\\)$/)\n\t\t\tif (match) {\n\t\t\t\tkey = new RegExp(`^${match[1]}$`)\n\t\t\t}\n\t\t\tmods = mods.map(mod => (mod === \"$mod\" ? MOD : mod))\n\t\t\treturn [mods, key]\n\t\t})\n}\n\n/**\n * This tells us if a single keyboard event matches a single keybinding press.\n */\nexport function matchKeyBindingPress(\n\tevent: KeyboardEvent,\n\t[mods, key]: KeyBindingPress,\n): boolean {\n\t// prettier-ignore\n\treturn !(\n\t\t// Allow either the `event.key` or the `event.code`\n\t\t// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n\t\t// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\t\t(\n\t\t\tkey instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :\n\t\t\t(key.toUpperCase() !== event.key.toUpperCase() &&\n\t\t\tkey !== event.code)\n\t\t) ||\n\n\t\t// Ensure all the modifiers in the keybinding are pressed.\n\t\tmods.find(mod => {\n\t\t\treturn !getModifierState(event, mod)\n\t\t}) ||\n\n\t\t// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a\n\t\t// keybinding. So if they are pressed but aren't part of the current\n\t\t// keybinding press, then we don't have a match.\n\t\tKEYBINDING_MODIFIER_KEYS.find(mod => {\n\t\t\treturn !mods.includes(mod) && key !== mod && getModifierState(event, mod)\n\t\t})\n\t)\n}\n\n/**\n * Creates an event listener for handling keybindings.\n *\n * @example\n * ```js\n * import { createKeybindingsHandler } from \"../src/keybindings\"\n *\n * let handler = createKeybindingsHandler({\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n *\n * window.addEvenListener(\"keydown\", handler)\n * ```\n */\nexport function createKeybindingsHandler(\n\tkeyBindingMap: KeyBindingMap,\n\toptions: KeyBindingHandlerOptions = {},\n): EventListener {\n\tlet timeout = options.timeout ?? DEFAULT_TIMEOUT\n\n\tlet keyBindings = Object.keys(keyBindingMap).map(key => {\n\t\treturn [parseKeybinding(key), keyBindingMap[key]] as const\n\t})\n\n\tlet possibleMatches = new Map<KeyBindingPress[], KeyBindingPress[]>()\n\tlet timer: number | null = null\n\n\treturn event => {\n\t\t// Ensure and stop any event that isn't a full keyboard event.\n\t\t// Autocomplete option navigation and selection would fire a instanceof Event,\n\t\t// instead of the expected KeyboardEvent\n\t\tif (!(event instanceof KeyboardEvent)) {\n\t\t\treturn\n\t\t}\n\n\t\tkeyBindings.forEach(keyBinding => {\n\t\t\tlet sequence = keyBinding[0]\n\t\t\tlet callback = keyBinding[1]\n\n\t\t\tlet prev = possibleMatches.get(sequence)\n\t\t\tlet remainingExpectedPresses = prev ? prev : sequence\n\t\t\tlet currentExpectedPress = remainingExpectedPresses[0]\n\n\t\t\tlet matches = matchKeyBindingPress(event, currentExpectedPress)\n\n\t\t\tif (!matches) {\n\t\t\t\t// Modifier keydown events shouldn't break sequences\n\t\t\t\t// Note: This works because:\n\t\t\t\t// - non-modifiers will always return false\n\t\t\t\t// - if the current keypress is a modifier then it will return true when we check its state\n\t\t\t\t// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState\n\t\t\t\tif (!getModifierState(event, event.key)) {\n\t\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\t}\n\t\t\t} else if (remainingExpectedPresses.length > 1) {\n\t\t\t\tpossibleMatches.set(sequence, remainingExpectedPresses.slice(1))\n\t\t\t} else {\n\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\tcallback(event)\n\t\t\t}\n\t\t})\n\n\t\tif (timer) {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\ttimer = setTimeout(possibleMatches.clear.bind(possibleMatches), timeout)\n\t}\n}\n\n/**\n * Subscribes to keybindings.\n *\n * Returns an unsubscribe method.\n *\n * @example\n * ```js\n * import { tinykeys } from \"../src/tinykeys\"\n *\n * tinykeys(window, {\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n * ```\n */\nexport function tinykeys(\n\ttarget: Window | HTMLElement,\n\tkeyBindingMap: KeyBindingMap,\n\t{ event = DEFAULT_EVENT, capture, timeout }: KeyBindingOptions = {},\n): () => void {\n\tlet onKeyEvent = createKeybindingsHandler(keyBindingMap, { timeout })\n\ttarget.addEventListener(event, onKeyEvent, capture)\n\treturn () => {\n\t\ttarget.removeEventListener(event, onKeyEvent, capture)\n\t}\n}\n"],"names":["KEYBINDING_MODIFIER_KEYS","PLATFORM","navigator","platform","APPLE_DEVICE","test","MOD","ALT_GRAPH_ALIASES","getModifierState","event","mod","includes","parseKeybinding","str","trim","split","map","press","mods","key","pop","match","RegExp","matchKeyBindingPress","_ref","code","toUpperCase","find","createKeybindingsHandler","keyBindingMap","options","timeout","_options$timeout","keyBindings","Object","keys","possibleMatches","Map","timer","KeyboardEvent","forEach","keyBinding","sequence","callback","remainingExpectedPresses","get","length","set","slice","clearTimeout","setTimeout","clear","bind","target","_temp","_ref2$event","_ref2","capture","onKeyEvent","addEventListener","removeEventListener"],"mappings":"AA2CA,IAAIA,EAA2B,CAAC,QAAS,OAAQ,MAAO,WAiBpDC,EAAgC,iBAAdC,UAAyBA,UAAUC,SAAW,GAChEC,EAAe,uBAAuBC,KAAKJ,GAK3CK,EAAMF,EAAe,OAAS,UAU9BG,EACU,UAAbN,EAAuB,CAAC,UAAW,OAASG,EAAe,CAAC,OAAS,GAMtE,SAASI,EAAiBC,EAAsBC,GAC/C,MAAyC,mBAA3BD,EAAMD,mBACjBC,EAAMD,iBAAiBE,IACtBH,EAAkBI,SAASD,IAAQD,EAAMD,iBAAiB,sBAc/CI,EAAgBC,GAC/B,OAAOA,EACLC,OACAC,MAAM,KACNC,IAAI,SAAAC,GACJ,IAAIC,EAAOD,EAAMF,MAAM,QACnBI,EAAuBD,EAAKE,MAC5BC,EAAQF,EAAIE,MAAM,cAKtB,OAJIA,IACHF,EAAM,IAAIG,WAAWD,EAAM,SAGrB,CADPH,EAAOA,EAAKF,IAAI,SAAAN,SAAgB,SAARA,EAAiBJ,EAAMI,IACjCS,cAODI,EACfd,EAAoBe,OACnBN,EAAIM,KAAEL,EAAGK,KAGV,SAKEL,aAAeG,QAAWH,EAAId,KAAKI,EAAMU,OAAQA,EAAId,KAAKI,EAAMgB,MAC/DN,EAAIO,gBAAkBjB,EAAMU,IAAIO,eACjCP,IAAQV,EAAMgB,OAIfP,EAAKS,KAAK,SAAAjB,GACT,OAAQF,EAAiBC,EAAOC,MAMjCV,EAAyB2B,KAAK,SAAAjB,GAC7B,OAAQQ,EAAKP,SAASD,IAAQS,IAAQT,GAAOF,EAAiBC,EAAOC,eA2BxDkB,EACfC,EACAC,kBAAAA,IAAAA,EAAoC,IAEpC,IAAIC,SAAOC,EAAGF,EAAQC,SAAOC,EA7HR,IA+HjBC,EAAcC,OAAOC,KAAKN,GAAeb,IAAI,SAAAG,GAChD,MAAO,CAACP,EAAgBO,GAAMU,EAAcV,MAGzCiB,EAAkB,IAAIC,IACtBC,EAAuB,KAE3B,gBAAO7B,GAIAA,aAAiB8B,gBAIvBN,EAAYO,QAAQ,SAAAC,GACnB,IAAIC,EAAWD,EAAW,GACtBE,EAAWF,EAAW,GAGtBG,EADOR,EAAgBS,IAAIH,IACcA,EAG/BnB,EAAqBd,EAFRmC,EAAyB,IAazCA,EAAyBE,OAAS,EAC5CV,EAAgBW,IAAIL,EAAUE,EAAyBI,MAAM,KAE7DZ,SAAuBM,GACvBC,EAASlC,IAPJD,EAAiBC,EAAOA,EAAMU,MAClCiB,SAAuBM,KAUtBJ,GACHW,aAAaX,GAGdA,EAAQY,WAAWd,EAAgBe,MAAMC,KAAKhB,GAAkBL,2HA2BjEsB,EACAxB,EAA4ByB,oBACqC,GAAEA,EAAAC,EAAAC,EAAjE/C,MAAAA,WAAK8C,EArMY,UAqMIA,EAAEE,EAAOD,EAAPC,QAErBC,EAAa9B,EAAyBC,EAAe,CAAEE,QAFlByB,EAAPzB,UAIlC,OADAsB,EAAOM,iBAAiBlD,EAAOiD,EAAYD,cAE1CJ,EAAOO,oBAAoBnD,EAAOiD,EAAYD"}
|
package/dist/tinykeys.modern.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
let e=["Shift","Meta","Alt","Control"],t="object"==typeof navigator?navigator.platform:"",n=/Mac|iPod|iPhone|iPad/.test(t),o=n?"Meta":"Control",i="Win32"===t?["Control","Alt"]:n?["Alt"]:[];function r(e,t){return"function"==typeof e.getModifierState&&(e.getModifierState(t)||i.includes(t)&&e.getModifierState("AltGraph"))}function a(e){return e.trim().split(" ").map(e=>{let t=e.split(/\b\+/),n=t.pop(),i=n.match(/^\((.+)\)$/);return i&&(n=new RegExp(`^${i[1]}$`)),t=t.map(e=>"$mod"===e?o:e),[t,n]})}function l(t,[n,o]){return!((o instanceof RegExp?!o.test(t.key)&&!o.test(t.code):o.toUpperCase()!==t.key.toUpperCase()&&o!==t.code)||n.find(e=>!r(t,e))||e.find(e=>!n.includes(e)&&o!==e&&r(t,e)))}function u(e,t={}){var n;let o=null!=(n=t.timeout)?n:1e3,i=Object.keys(e).map(t=>[a(t),e[t]]),u=new Map,c=null;return e=>{e instanceof KeyboardEvent&&(i.forEach(t=>{let n=t[0],o=t[1],i=u.get(n)||n;l(e,i[0])?i.length>1?u.set(n,i.slice(1)):(u.delete(n),o(e)):r(e,e.key)||u.delete(n)}),c&&clearTimeout(c),c=setTimeout(u.clear.bind(u),o))}}function c(e,t,{event:n="keydown",capture:o,timeout:i}={}){let r=u(t,{timeout:i});return e.addEventListener(n,r,o),()=>{e.removeEventListener(n,r,o)}}export{u as createKeybindingsHandler,l as matchKeyBindingPress,a as parseKeybinding,c as tinykeys};
|
|
2
|
-
//# sourceMappingURL=tinykeys.modern.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tinykeys.modern.js","sources":["../src/tinykeys.ts"],"sourcesContent":["/**\n * A single press of a keybinding sequence\n */\nexport type KeyBindingPress = [mods: string[], key: string | RegExp]\n\n/**\n * A map of keybinding strings to event handlers.\n */\nexport interface KeyBindingMap {\n\t[keybinding: string]: (event: KeyboardEvent) => void\n}\n\nexport interface KeyBindingHandlerOptions {\n\t/**\n\t * Keybinding sequences will wait this long between key presses before\n\t * cancelling (default: 1000).\n\t *\n\t * **Note:** Setting this value too low (i.e. `300`) will be too fast for many\n\t * of your users.\n\t */\n\ttimeout?: number\n}\n\n/**\n * Options to configure the behavior of keybindings.\n */\nexport interface KeyBindingOptions extends KeyBindingHandlerOptions {\n\t/**\n\t * Key presses will listen to this event (default: \"keydown\").\n\t */\n\tevent?: \"keydown\" | \"keyup\"\n\n\t/**\n\t * Key presses will use a capture listener (default: false)\n\t */\n\tcapture?: boolean\n}\n\n/**\n * These are the modifier keys that change the meaning of keybindings.\n *\n * Note: Ignoring \"AltGraph\" because it is covered by the others.\n */\nlet KEYBINDING_MODIFIER_KEYS = [\"Shift\", \"Meta\", \"Alt\", \"Control\"]\n\n/**\n * Keybinding sequences should timeout if individual key presses are more than\n * 1s apart by default.\n */\nlet DEFAULT_TIMEOUT = 1000\n\n/**\n * Keybinding sequences should bind to this event by default.\n */\nlet DEFAULT_EVENT = \"keydown\" as const\n\n/**\n * Platform detection code.\n * @see https://github.com/jamiebuilds/tinykeys/issues/184\n */\nlet PLATFORM = typeof navigator === \"object\" ? navigator.platform : \"\"\nlet APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM)\n\n/**\n * An alias for creating platform-specific keybinding aliases.\n */\nlet MOD = APPLE_DEVICE ? \"Meta\" : \"Control\"\n\n/**\n * Meaning of `AltGraph`, from MDN:\n * - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed\n * - Mac: ⌥ Option key pressed\n * - Linux: Level 3 Shift key (or Level 5 Shift key) pressed\n * - Android: Not supported\n * @see https://github.com/jamiebuilds/tinykeys/issues/185\n */\nlet ALT_GRAPH_ALIASES =\n\tPLATFORM === \"Win32\" ? [\"Control\", \"Alt\"] : APPLE_DEVICE ? [\"Alt\"] : []\n\n/**\n * There's a bug in Chrome that causes event.getModifierState not to exist on\n * KeyboardEvent's for F1/F2/etc keys.\n */\nfunction getModifierState(event: KeyboardEvent, mod: string) {\n\treturn typeof event.getModifierState === \"function\"\n\t\t? event.getModifierState(mod) ||\n\t\t\t\t(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState(\"AltGraph\"))\n\t\t: false\n}\n\n/**\n * Parses a \"Key Binding String\" into its parts\n *\n * grammar = `<sequence>`\n * <sequence> = `<press> <press> <press> ...`\n * <press> = `<key>` or `<mods>+<key>`\n * <mods> = `<mod>+<mod>+...`\n * <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)\n * <key> = `(<regex>)` -> `/^<regex>$/` (case-sensitive)\n */\nexport function parseKeybinding(str: string): KeyBindingPress[] {\n\treturn str\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.map(press => {\n\t\t\tlet mods = press.split(/\\b\\+/)\n\t\t\tlet key: string | RegExp = mods.pop() as string\n\t\t\tlet match = key.match(/^\\((.+)\\)$/)\n\t\t\tif (match) {\n\t\t\t\tkey = new RegExp(`^${match[1]}$`)\n\t\t\t}\n\t\t\tmods = mods.map(mod => (mod === \"$mod\" ? MOD : mod))\n\t\t\treturn [mods, key]\n\t\t})\n}\n\n/**\n * This tells us if a single keyboard event matches a single keybinding press.\n */\nexport function matchKeyBindingPress(\n\tevent: KeyboardEvent,\n\t[mods, key]: KeyBindingPress,\n): boolean {\n\t// prettier-ignore\n\treturn !(\n\t\t// Allow either the `event.key` or the `event.code`\n\t\t// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n\t\t// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\t\t(\n\t\t\tkey instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :\n\t\t\t(key.toUpperCase() !== event.key.toUpperCase() &&\n\t\t\tkey !== event.code)\n\t\t) ||\n\n\t\t// Ensure all the modifiers in the keybinding are pressed.\n\t\tmods.find(mod => {\n\t\t\treturn !getModifierState(event, mod)\n\t\t}) ||\n\n\t\t// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a\n\t\t// keybinding. So if they are pressed but aren't part of the current\n\t\t// keybinding press, then we don't have a match.\n\t\tKEYBINDING_MODIFIER_KEYS.find(mod => {\n\t\t\treturn !mods.includes(mod) && key !== mod && getModifierState(event, mod)\n\t\t})\n\t)\n}\n\n/**\n * Creates an event listener for handling keybindings.\n *\n * @example\n * ```js\n * import { createKeybindingsHandler } from \"../src/keybindings\"\n *\n * let handler = createKeybindingsHandler({\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n *\n * window.addEvenListener(\"keydown\", handler)\n * ```\n */\nexport function createKeybindingsHandler(\n\tkeyBindingMap: KeyBindingMap,\n\toptions: KeyBindingHandlerOptions = {},\n): EventListener {\n\tlet timeout = options.timeout ?? DEFAULT_TIMEOUT\n\n\tlet keyBindings = Object.keys(keyBindingMap).map(key => {\n\t\treturn [parseKeybinding(key), keyBindingMap[key]] as const\n\t})\n\n\tlet possibleMatches = new Map<KeyBindingPress[], KeyBindingPress[]>()\n\tlet timer: number | null = null\n\n\treturn event => {\n\t\t// Ensure and stop any event that isn't a full keyboard event.\n\t\t// Autocomplete option navigation and selection would fire a instanceof Event,\n\t\t// instead of the expected KeyboardEvent\n\t\tif (!(event instanceof KeyboardEvent)) {\n\t\t\treturn\n\t\t}\n\n\t\tkeyBindings.forEach(keyBinding => {\n\t\t\tlet sequence = keyBinding[0]\n\t\t\tlet callback = keyBinding[1]\n\n\t\t\tlet prev = possibleMatches.get(sequence)\n\t\t\tlet remainingExpectedPresses = prev ? prev : sequence\n\t\t\tlet currentExpectedPress = remainingExpectedPresses[0]\n\n\t\t\tlet matches = matchKeyBindingPress(event, currentExpectedPress)\n\n\t\t\tif (!matches) {\n\t\t\t\t// Modifier keydown events shouldn't break sequences\n\t\t\t\t// Note: This works because:\n\t\t\t\t// - non-modifiers will always return false\n\t\t\t\t// - if the current keypress is a modifier then it will return true when we check its state\n\t\t\t\t// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState\n\t\t\t\tif (!getModifierState(event, event.key)) {\n\t\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\t}\n\t\t\t} else if (remainingExpectedPresses.length > 1) {\n\t\t\t\tpossibleMatches.set(sequence, remainingExpectedPresses.slice(1))\n\t\t\t} else {\n\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\tcallback(event)\n\t\t\t}\n\t\t})\n\n\t\tif (timer) {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\ttimer = setTimeout(possibleMatches.clear.bind(possibleMatches), timeout)\n\t}\n}\n\n/**\n * Subscribes to keybindings.\n *\n * Returns an unsubscribe method.\n *\n * @example\n * ```js\n * import { tinykeys } from \"../src/tinykeys\"\n *\n * tinykeys(window, {\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n * ```\n */\nexport function tinykeys(\n\ttarget: Window | HTMLElement,\n\tkeyBindingMap: KeyBindingMap,\n\t{ event = DEFAULT_EVENT, capture, timeout }: KeyBindingOptions = {},\n): () => void {\n\tlet onKeyEvent = createKeybindingsHandler(keyBindingMap, { timeout })\n\ttarget.addEventListener(event, onKeyEvent, capture)\n\treturn () => {\n\t\ttarget.removeEventListener(event, onKeyEvent, capture)\n\t}\n}\n"],"names":["KEYBINDING_MODIFIER_KEYS","PLATFORM","navigator","platform","APPLE_DEVICE","test","MOD","ALT_GRAPH_ALIASES","getModifierState","event","mod","includes","parseKeybinding","str","trim","split","map","press","mods","key","pop","match","RegExp","matchKeyBindingPress","code","toUpperCase","find","createKeybindingsHandler","keyBindingMap","options","timeout","_options$timeout","keyBindings","Object","keys","possibleMatches","Map","timer","KeyboardEvent","forEach","keyBinding","sequence","callback","remainingExpectedPresses","get","length","set","slice","delete","clearTimeout","setTimeout","clear","bind","tinykeys","target","capture","onKeyEvent","addEventListener","removeEventListener"],"mappings":"AA2CA,IAAIA,EAA2B,CAAC,QAAS,OAAQ,MAAO,WAiBpDC,EAAgC,iBAAdC,UAAyBA,UAAUC,SAAW,GAChEC,EAAe,uBAAuBC,KAAKJ,GAK3CK,EAAMF,EAAe,OAAS,UAU9BG,EACU,UAAbN,EAAuB,CAAC,UAAW,OAASG,EAAe,CAAC,OAAS,GAMtE,SAASI,EAAiBC,EAAsBC,GAC/C,MAAyC,mBAA3BD,EAAMD,mBACjBC,EAAMD,iBAAiBE,IACtBH,EAAkBI,SAASD,IAAQD,EAAMD,iBAAiB,sBAc/CI,EAAgBC,GAC/B,OAAOA,EACLC,OACAC,MAAM,KACNC,IAAIC,IACJ,IAAIC,EAAOD,EAAMF,MAAM,QACnBI,EAAuBD,EAAKE,MAC5BC,EAAQF,EAAIE,MAAM,cAKtB,OAJIA,IACHF,EAAM,IAAIG,OAAO,IAAID,EAAM,QAE5BH,EAAOA,EAAKF,IAAIN,GAAgB,SAARA,EAAiBJ,EAAMI,GACxC,CAACQ,EAAMC,cAODI,EACfd,GACCS,EAAMC,IAGP,SAKEA,aAAeG,QAAWH,EAAId,KAAKI,EAAMU,OAAQA,EAAId,KAAKI,EAAMe,MAC/DL,EAAIM,gBAAkBhB,EAAMU,IAAIM,eACjCN,IAAQV,EAAMe,OAIfN,EAAKQ,KAAKhB,IACDF,EAAiBC,EAAOC,KAMjCV,EAAyB0B,KAAKhB,IACrBQ,EAAKP,SAASD,IAAQS,IAAQT,GAAOF,EAAiBC,EAAOC,cA2BxDiB,EACfC,EACAC,EAAoC,UAEpC,IAAIC,SAAOC,EAAGF,EAAQC,SAAOC,EA7HR,IA+HjBC,EAAcC,OAAOC,KAAKN,GAAeZ,IAAIG,GACzC,CAACP,EAAgBO,GAAMS,EAAcT,KAGzCgB,EAAkB,IAAIC,IACtBC,EAAuB,KAE3B,OAAO5B,IAIAA,aAAiB6B,gBAIvBN,EAAYO,QAAQC,IACnB,IAAIC,EAAWD,EAAW,GACtBE,EAAWF,EAAW,GAGtBG,EADOR,EAAgBS,IAAIH,IACcA,EAG/BlB,EAAqBd,EAFRkC,EAAyB,IAazCA,EAAyBE,OAAS,EAC5CV,EAAgBW,IAAIL,EAAUE,EAAyBI,MAAM,KAE7DZ,EAAgBa,OAAOP,GACvBC,EAASjC,IAPJD,EAAiBC,EAAOA,EAAMU,MAClCgB,EAAgBa,OAAOP,KAUtBJ,GACHY,aAAaZ,GAGdA,EAAQa,WAAWf,EAAgBgB,MAAMC,KAAKjB,GAAkBL,cA0BlDuB,EACfC,EACA1B,GACAnB,MAAEA,EArMiB,UAqMI8C,QAAEA,EAAOzB,QAAEA,GAA+B,IAEjE,IAAI0B,EAAa7B,EAAyBC,EAAe,CAAEE,QAAAA,IAE3D,OADAwB,EAAOG,iBAAiBhD,EAAO+C,EAAYD,GACpC,KACND,EAAOI,oBAAoBjD,EAAO+C,EAAYD"}
|
package/dist/tinykeys.module.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
var t=["Shift","Meta","Alt","Control"],e="object"==typeof navigator?navigator.platform:"",n=/Mac|iPod|iPhone|iPad/.test(e),o=n?"Meta":"Control",r="Win32"===e?["Control","Alt"]:n?["Alt"]:[];function i(t,e){return"function"==typeof t.getModifierState&&(t.getModifierState(e)||r.includes(e)&&t.getModifierState("AltGraph"))}function a(t){return t.trim().split(" ").map(function(t){var e=t.split(/\b\+/),n=e.pop(),r=n.match(/^\((.+)\)$/);return r&&(n=new RegExp("^"+r[1]+"$")),[e=e.map(function(t){return"$mod"===t?o:t}),n]})}function u(e,n){var o=n[0],r=n[1];return!((r instanceof RegExp?!r.test(e.key)&&!r.test(e.code):r.toUpperCase()!==e.key.toUpperCase()&&r!==e.code)||o.find(function(t){return!i(e,t)})||t.find(function(t){return!o.includes(t)&&r!==t&&i(e,t)}))}function c(t,e){var n;void 0===e&&(e={});var o=null!=(n=e.timeout)?n:1e3,r=Object.keys(t).map(function(e){return[a(e),t[e]]}),c=new Map,f=null;return function(t){t instanceof KeyboardEvent&&(r.forEach(function(e){var n=e[0],o=e[1],r=c.get(n)||n;u(t,r[0])?r.length>1?c.set(n,r.slice(1)):(c.delete(n),o(t)):i(t,t.key)||c.delete(n)}),f&&clearTimeout(f),f=setTimeout(c.clear.bind(c),o))}}function f(t,e,n){var o=void 0===n?{}:n,r=o.event,i=void 0===r?"keydown":r,a=o.capture,u=c(e,{timeout:o.timeout});return t.addEventListener(i,u,a),function(){t.removeEventListener(i,u,a)}}export{c as createKeybindingsHandler,u as matchKeyBindingPress,a as parseKeybinding,f as tinykeys};
|
|
2
|
-
//# sourceMappingURL=tinykeys.module.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tinykeys.module.js","sources":["../src/tinykeys.ts"],"sourcesContent":["/**\n * A single press of a keybinding sequence\n */\nexport type KeyBindingPress = [mods: string[], key: string | RegExp]\n\n/**\n * A map of keybinding strings to event handlers.\n */\nexport interface KeyBindingMap {\n\t[keybinding: string]: (event: KeyboardEvent) => void\n}\n\nexport interface KeyBindingHandlerOptions {\n\t/**\n\t * Keybinding sequences will wait this long between key presses before\n\t * cancelling (default: 1000).\n\t *\n\t * **Note:** Setting this value too low (i.e. `300`) will be too fast for many\n\t * of your users.\n\t */\n\ttimeout?: number\n}\n\n/**\n * Options to configure the behavior of keybindings.\n */\nexport interface KeyBindingOptions extends KeyBindingHandlerOptions {\n\t/**\n\t * Key presses will listen to this event (default: \"keydown\").\n\t */\n\tevent?: \"keydown\" | \"keyup\"\n\n\t/**\n\t * Key presses will use a capture listener (default: false)\n\t */\n\tcapture?: boolean\n}\n\n/**\n * These are the modifier keys that change the meaning of keybindings.\n *\n * Note: Ignoring \"AltGraph\" because it is covered by the others.\n */\nlet KEYBINDING_MODIFIER_KEYS = [\"Shift\", \"Meta\", \"Alt\", \"Control\"]\n\n/**\n * Keybinding sequences should timeout if individual key presses are more than\n * 1s apart by default.\n */\nlet DEFAULT_TIMEOUT = 1000\n\n/**\n * Keybinding sequences should bind to this event by default.\n */\nlet DEFAULT_EVENT = \"keydown\" as const\n\n/**\n * Platform detection code.\n * @see https://github.com/jamiebuilds/tinykeys/issues/184\n */\nlet PLATFORM = typeof navigator === \"object\" ? navigator.platform : \"\"\nlet APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM)\n\n/**\n * An alias for creating platform-specific keybinding aliases.\n */\nlet MOD = APPLE_DEVICE ? \"Meta\" : \"Control\"\n\n/**\n * Meaning of `AltGraph`, from MDN:\n * - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed\n * - Mac: ⌥ Option key pressed\n * - Linux: Level 3 Shift key (or Level 5 Shift key) pressed\n * - Android: Not supported\n * @see https://github.com/jamiebuilds/tinykeys/issues/185\n */\nlet ALT_GRAPH_ALIASES =\n\tPLATFORM === \"Win32\" ? [\"Control\", \"Alt\"] : APPLE_DEVICE ? [\"Alt\"] : []\n\n/**\n * There's a bug in Chrome that causes event.getModifierState not to exist on\n * KeyboardEvent's for F1/F2/etc keys.\n */\nfunction getModifierState(event: KeyboardEvent, mod: string) {\n\treturn typeof event.getModifierState === \"function\"\n\t\t? event.getModifierState(mod) ||\n\t\t\t\t(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState(\"AltGraph\"))\n\t\t: false\n}\n\n/**\n * Parses a \"Key Binding String\" into its parts\n *\n * grammar = `<sequence>`\n * <sequence> = `<press> <press> <press> ...`\n * <press> = `<key>` or `<mods>+<key>`\n * <mods> = `<mod>+<mod>+...`\n * <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)\n * <key> = `(<regex>)` -> `/^<regex>$/` (case-sensitive)\n */\nexport function parseKeybinding(str: string): KeyBindingPress[] {\n\treturn str\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.map(press => {\n\t\t\tlet mods = press.split(/\\b\\+/)\n\t\t\tlet key: string | RegExp = mods.pop() as string\n\t\t\tlet match = key.match(/^\\((.+)\\)$/)\n\t\t\tif (match) {\n\t\t\t\tkey = new RegExp(`^${match[1]}$`)\n\t\t\t}\n\t\t\tmods = mods.map(mod => (mod === \"$mod\" ? MOD : mod))\n\t\t\treturn [mods, key]\n\t\t})\n}\n\n/**\n * This tells us if a single keyboard event matches a single keybinding press.\n */\nexport function matchKeyBindingPress(\n\tevent: KeyboardEvent,\n\t[mods, key]: KeyBindingPress,\n): boolean {\n\t// prettier-ignore\n\treturn !(\n\t\t// Allow either the `event.key` or the `event.code`\n\t\t// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n\t\t// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code\n\t\t(\n\t\t\tkey instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) :\n\t\t\t(key.toUpperCase() !== event.key.toUpperCase() &&\n\t\t\tkey !== event.code)\n\t\t) ||\n\n\t\t// Ensure all the modifiers in the keybinding are pressed.\n\t\tmods.find(mod => {\n\t\t\treturn !getModifierState(event, mod)\n\t\t}) ||\n\n\t\t// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a\n\t\t// keybinding. So if they are pressed but aren't part of the current\n\t\t// keybinding press, then we don't have a match.\n\t\tKEYBINDING_MODIFIER_KEYS.find(mod => {\n\t\t\treturn !mods.includes(mod) && key !== mod && getModifierState(event, mod)\n\t\t})\n\t)\n}\n\n/**\n * Creates an event listener for handling keybindings.\n *\n * @example\n * ```js\n * import { createKeybindingsHandler } from \"../src/keybindings\"\n *\n * let handler = createKeybindingsHandler({\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n *\n * window.addEvenListener(\"keydown\", handler)\n * ```\n */\nexport function createKeybindingsHandler(\n\tkeyBindingMap: KeyBindingMap,\n\toptions: KeyBindingHandlerOptions = {},\n): EventListener {\n\tlet timeout = options.timeout ?? DEFAULT_TIMEOUT\n\n\tlet keyBindings = Object.keys(keyBindingMap).map(key => {\n\t\treturn [parseKeybinding(key), keyBindingMap[key]] as const\n\t})\n\n\tlet possibleMatches = new Map<KeyBindingPress[], KeyBindingPress[]>()\n\tlet timer: number | null = null\n\n\treturn event => {\n\t\t// Ensure and stop any event that isn't a full keyboard event.\n\t\t// Autocomplete option navigation and selection would fire a instanceof Event,\n\t\t// instead of the expected KeyboardEvent\n\t\tif (!(event instanceof KeyboardEvent)) {\n\t\t\treturn\n\t\t}\n\n\t\tkeyBindings.forEach(keyBinding => {\n\t\t\tlet sequence = keyBinding[0]\n\t\t\tlet callback = keyBinding[1]\n\n\t\t\tlet prev = possibleMatches.get(sequence)\n\t\t\tlet remainingExpectedPresses = prev ? prev : sequence\n\t\t\tlet currentExpectedPress = remainingExpectedPresses[0]\n\n\t\t\tlet matches = matchKeyBindingPress(event, currentExpectedPress)\n\n\t\t\tif (!matches) {\n\t\t\t\t// Modifier keydown events shouldn't break sequences\n\t\t\t\t// Note: This works because:\n\t\t\t\t// - non-modifiers will always return false\n\t\t\t\t// - if the current keypress is a modifier then it will return true when we check its state\n\t\t\t\t// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState\n\t\t\t\tif (!getModifierState(event, event.key)) {\n\t\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\t}\n\t\t\t} else if (remainingExpectedPresses.length > 1) {\n\t\t\t\tpossibleMatches.set(sequence, remainingExpectedPresses.slice(1))\n\t\t\t} else {\n\t\t\t\tpossibleMatches.delete(sequence)\n\t\t\t\tcallback(event)\n\t\t\t}\n\t\t})\n\n\t\tif (timer) {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\ttimer = setTimeout(possibleMatches.clear.bind(possibleMatches), timeout)\n\t}\n}\n\n/**\n * Subscribes to keybindings.\n *\n * Returns an unsubscribe method.\n *\n * @example\n * ```js\n * import { tinykeys } from \"../src/tinykeys\"\n *\n * tinykeys(window, {\n * \t\"Shift+d\": () => {\n * \t\talert(\"The 'Shift' and 'd' keys were pressed at the same time\")\n * \t},\n * \t\"y e e t\": () => {\n * \t\talert(\"The keys 'y', 'e', 'e', and 't' were pressed in order\")\n * \t},\n * \t\"$mod+d\": () => {\n * \t\talert(\"Either 'Control+d' or 'Meta+d' were pressed\")\n * \t},\n * })\n * ```\n */\nexport function tinykeys(\n\ttarget: Window | HTMLElement,\n\tkeyBindingMap: KeyBindingMap,\n\t{ event = DEFAULT_EVENT, capture, timeout }: KeyBindingOptions = {},\n): () => void {\n\tlet onKeyEvent = createKeybindingsHandler(keyBindingMap, { timeout })\n\ttarget.addEventListener(event, onKeyEvent, capture)\n\treturn () => {\n\t\ttarget.removeEventListener(event, onKeyEvent, capture)\n\t}\n}\n"],"names":["KEYBINDING_MODIFIER_KEYS","PLATFORM","navigator","platform","APPLE_DEVICE","test","MOD","ALT_GRAPH_ALIASES","getModifierState","event","mod","includes","parseKeybinding","str","trim","split","map","press","mods","key","pop","match","RegExp","matchKeyBindingPress","_ref","code","toUpperCase","find","createKeybindingsHandler","keyBindingMap","options","timeout","_options$timeout","keyBindings","Object","keys","possibleMatches","Map","timer","KeyboardEvent","forEach","keyBinding","sequence","callback","remainingExpectedPresses","get","length","set","slice","clearTimeout","setTimeout","clear","bind","tinykeys","target","_temp","_ref2$event","_ref2","capture","onKeyEvent","addEventListener","removeEventListener"],"mappings":"AA2CA,IAAIA,EAA2B,CAAC,QAAS,OAAQ,MAAO,WAiBpDC,EAAgC,iBAAdC,UAAyBA,UAAUC,SAAW,GAChEC,EAAe,uBAAuBC,KAAKJ,GAK3CK,EAAMF,EAAe,OAAS,UAU9BG,EACU,UAAbN,EAAuB,CAAC,UAAW,OAASG,EAAe,CAAC,OAAS,GAMtE,SAASI,EAAiBC,EAAsBC,GAC/C,MAAyC,mBAA3BD,EAAMD,mBACjBC,EAAMD,iBAAiBE,IACtBH,EAAkBI,SAASD,IAAQD,EAAMD,iBAAiB,sBAc/CI,EAAgBC,GAC/B,OAAOA,EACLC,OACAC,MAAM,KACNC,IAAI,SAAAC,GACJ,IAAIC,EAAOD,EAAMF,MAAM,QACnBI,EAAuBD,EAAKE,MAC5BC,EAAQF,EAAIE,MAAM,cAKtB,OAJIA,IACHF,EAAM,IAAIG,WAAWD,EAAM,SAGrB,CADPH,EAAOA,EAAKF,IAAI,SAAAN,SAAgB,SAARA,EAAiBJ,EAAMI,IACjCS,cAODI,EACfd,EAAoBe,OACnBN,EAAIM,KAAEL,EAAGK,KAGV,SAKEL,aAAeG,QAAWH,EAAId,KAAKI,EAAMU,OAAQA,EAAId,KAAKI,EAAMgB,MAC/DN,EAAIO,gBAAkBjB,EAAMU,IAAIO,eACjCP,IAAQV,EAAMgB,OAIfP,EAAKS,KAAK,SAAAjB,GACT,OAAQF,EAAiBC,EAAOC,MAMjCV,EAAyB2B,KAAK,SAAAjB,GAC7B,OAAQQ,EAAKP,SAASD,IAAQS,IAAQT,GAAOF,EAAiBC,EAAOC,eA2BxDkB,EACfC,EACAC,kBAAAA,IAAAA,EAAoC,IAEpC,IAAIC,SAAOC,EAAGF,EAAQC,SAAOC,EA7HR,IA+HjBC,EAAcC,OAAOC,KAAKN,GAAeb,IAAI,SAAAG,GAChD,MAAO,CAACP,EAAgBO,GAAMU,EAAcV,MAGzCiB,EAAkB,IAAIC,IACtBC,EAAuB,KAE3B,gBAAO7B,GAIAA,aAAiB8B,gBAIvBN,EAAYO,QAAQ,SAAAC,GACnB,IAAIC,EAAWD,EAAW,GACtBE,EAAWF,EAAW,GAGtBG,EADOR,EAAgBS,IAAIH,IACcA,EAG/BnB,EAAqBd,EAFRmC,EAAyB,IAazCA,EAAyBE,OAAS,EAC5CV,EAAgBW,IAAIL,EAAUE,EAAyBI,MAAM,KAE7DZ,SAAuBM,GACvBC,EAASlC,IAPJD,EAAiBC,EAAOA,EAAMU,MAClCiB,SAAuBM,KAUtBJ,GACHW,aAAaX,GAGdA,EAAQY,WAAWd,EAAgBe,MAAMC,KAAKhB,GAAkBL,cA0BlDsB,EACfC,EACAzB,EAA4B0B,oBACqC,GAAEA,EAAAC,EAAAC,EAAjEhD,MAAAA,WAAK+C,EArMY,UAqMIA,EAAEE,EAAOD,EAAPC,QAErBC,EAAa/B,EAAyBC,EAAe,CAAEE,QAFlB0B,EAAP1B,UAIlC,OADAuB,EAAOM,iBAAiBnD,EAAOkD,EAAYD,cAE1CJ,EAAOO,oBAAoBpD,EAAOkD,EAAYD"}
|