tinykeys 3.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `tinykeys`
2
2
 
3
- > A tiny (~650 B) & modern library for keybindings.
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,8 +180,8 @@ platform keybindings:
173
180
  "$mod+Shift+D" // Meta/Control+Shift+D
174
181
  ```
175
182
 
176
- Alternatively, you can use parenthesis to use case-sensitive regular expressions
177
- to match multiple keys.
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...
@@ -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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"}
@@ -1,2 +1,2 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e||self).tinykeys={})}(this,function(e){var t=["Shift","Meta","Alt","Control"],n="object"==typeof navigator?navigator.platform:"",i=/Mac|iPod|iPhone|iPad/.test(n),o=i?"Meta":"Control",r="Win32"===n?["Control","Alt"]:i?["Alt"]:[];function a(e,t){return"function"==typeof e.getModifierState&&(e.getModifierState(t)||r.includes(t)&&e.getModifierState("AltGraph"))}function u(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]+")$","iv")),[t=t.map(function(e){return"$mod"===e?o:e}),n]})}function f(e,n){var i=n[0],o=n[1];return!((o instanceof RegExp?!o.test(e.key)&&!o.test(e.code):o.toUpperCase()!==e.key.toUpperCase()&&o!==e.code)||i.find(function(t){return!a(e,t)})||t.find(function(t){return!i.includes(t)&&o!==t&&a(e,t)}))}function c(e,t){var n;void 0===t&&(t={});var i=null!=(n=t.timeout)?n:1e3,o=Object.keys(e).map(function(t){return[u(t),e[t]]}),r=new Map,c=null;return function(e){e instanceof KeyboardEvent&&(o.forEach(function(t){var n=t[0],i=t[1],o=r.get(n)||n;f(e,o[0])?o.length>1?r.set(n,o.slice(1)):(r.delete(n),i(e)):a(e,e.key)||r.delete(n)}),c&&clearTimeout(c),c=setTimeout(r.clear.bind(r),i))}}e.createKeybindingsHandler=c,e.matchKeyBindingPress=f,e.parseKeybinding=u,e.tinykeys=function(e,t,n){var i=void 0===n?{}:n,o=i.event,r=void 0===o?"keydown":o,a=i.capture,u=c(t,{timeout:i.timeout});return e.addEventListener(r,u,a),function(){e.removeEventListener(r,u,a)}}});
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
@@ -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]})$`, \"iv\")\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,cAAcD,EAAM,QAAQ,OAGhC,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,25 +1,35 @@
1
1
  {
2
2
  "name": "tinykeys",
3
- "version": "3.1.0",
4
- "description": "A tiny (~650 B) & modern library for keybindings.",
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.js",
10
- "module": "dist/tinykeys.module.js",
11
+ "main": "dist/tinykeys.cjs",
12
+ "module": "dist/tinykeys.mjs",
11
13
  "unpkg": "dist/tinykeys.umd.js",
12
- "types": "dist/tinykeys.d.ts",
14
+ "types": "dist/tinykeys.d.mts",
13
15
  "files": [
14
16
  "dist"
15
17
  ],
16
18
  "exports": {
17
19
  ".": {
18
- "types": "./dist/tinykeys.d.ts",
19
- "import": "./dist/tinykeys.module.js",
20
- "require": "./dist/tinykeys.js"
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
+ }
21
28
  }
22
29
  },
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
23
33
  "keywords": [
24
34
  "key",
25
35
  "keys",
@@ -40,48 +50,45 @@
40
50
  "shortcuts"
41
51
  ],
42
52
  "scripts": {
43
- "format": "prettier --write '**'",
53
+ "format": "prettier --write .",
44
54
  "check": "npm run -s check:types && npm run -s lint && npm run -s check:format",
45
55
  "check:types": "tsc --noEmit",
46
- "check:lint": "eslint '**'",
47
- "check:format": "prettier --check '**'",
48
- "test": "TS_NODE_PROJECT=./tsconfig.test.json nyc --reporter=lcov --reporter=text-summary ava",
49
- "build": "rm -rf dist && microbundle --inline none",
50
- "build:example": "rm -rf example-dist && parcel build example/index.html -d example-dist --public-url ./",
51
- "start": "rm -rf example-dist && parcel example/index.html -d example-dist",
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 ./",
52
62
  "precommit": "lint-staged",
53
63
  "prepublishOnly": "npm run -s build"
54
64
  },
55
65
  "devDependencies": {
56
- "@types/canvas-confetti": "^1.6.4",
57
- "@typescript-eslint/eslint-plugin": "^4.0.0",
58
- "@typescript-eslint/parser": "^3.7.1",
59
- "ava": "^3.11.0",
60
- "canvas-confetti": "^1.9.3",
61
- "eslint": "^7.5.0",
62
- "eslint-plugin-ava": "^12.0.0",
63
- "husky": "^6.0.0",
64
- "lint-staged": "^11.0.0",
65
- "microbundle": "^0.13.0",
66
- "nyc": "^15.1.0",
67
- "parcel": "^1.12.4",
68
- "prettier": "^2.0.5",
69
- "ts-node": "^9.0.0",
70
- "typescript": "^4.0.2"
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
+ }
71
87
  },
72
88
  "lint-staged": {
73
89
  "*": [
74
90
  "prettier --write",
75
91
  "git add"
76
92
  ]
77
- },
78
- "ava": {
79
- "extensions": [
80
- "ts",
81
- "tsx"
82
- ],
83
- "require": [
84
- "ts-node/register"
85
- ]
86
93
  }
87
94
  }
@@ -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),i=n?"Meta":"Control",r="Win32"===t?["Control","Alt"]:n?["Alt"]:[];function o(e,t){return"function"==typeof e.getModifierState&&(e.getModifierState(t)||r.includes(t)&&e.getModifierState("AltGraph"))}function a(e){return e.trim().split(" ").map(function(e){var t=e.split(/\b\+/),n=t.pop(),r=n.match(/^\((.+)\)$/);return r&&(n=new RegExp("^(?:"+r[1]+")$","iv")),[t=t.map(function(e){return"$mod"===e?i:e}),n]})}function u(t,n){var i=n[0],r=n[1];return!((r instanceof RegExp?!r.test(t.key)&&!r.test(t.code):r.toUpperCase()!==t.key.toUpperCase()&&r!==t.code)||i.find(function(e){return!o(t,e)})||e.find(function(e){return!i.includes(e)&&r!==e&&o(t,e)}))}function c(e,t){var n;void 0===t&&(t={});var i=null!=(n=t.timeout)?n:1e3,r=Object.keys(e).map(function(t){return[a(t),e[t]]}),c=new Map,d=null;return function(e){e instanceof KeyboardEvent&&(r.forEach(function(t){var n=t[0],i=t[1],r=c.get(n)||n;u(e,r[0])?r.length>1?c.set(n,r.slice(1)):(c.delete(n),i(e)):o(e,e.key)||c.delete(n)}),d&&clearTimeout(d),d=setTimeout(c.clear.bind(c),i))}}exports.createKeybindingsHandler=c,exports.matchKeyBindingPress=u,exports.parseKeybinding=a,exports.tinykeys=function(e,t,n){var i=void 0===n?{}:n,r=i.event,o=void 0===r?"keydown":r,a=i.capture,u=c(t,{timeout:i.timeout});return e.addEventListener(o,u,a),function(){e.removeEventListener(o,u,a)}};
2
- //# sourceMappingURL=tinykeys.js.map
@@ -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]})$`, \"iv\")\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,cAAcD,EAAM,QAAQ,OAGhC,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"}
@@ -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]})$`,"iv")),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]})$`, \"iv\")\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,OAAOD,EAAM,OAAQ,OAEvCH,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"}
@@ -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",i="Win32"===e?["Control","Alt"]:n?["Alt"]:[];function r(t,e){return"function"==typeof t.getModifierState&&(t.getModifierState(e)||i.includes(e)&&t.getModifierState("AltGraph"))}function a(t){return t.trim().split(" ").map(function(t){var e=t.split(/\b\+/),n=e.pop(),i=n.match(/^\((.+)\)$/);return i&&(n=new RegExp("^(?:"+i[1]+")$","iv")),[e=e.map(function(t){return"$mod"===t?o:t}),n]})}function u(e,n){var o=n[0],i=n[1];return!((i instanceof RegExp?!i.test(e.key)&&!i.test(e.code):i.toUpperCase()!==e.key.toUpperCase()&&i!==e.code)||o.find(function(t){return!r(e,t)})||t.find(function(t){return!o.includes(t)&&i!==t&&r(e,t)}))}function c(t,e){var n;void 0===e&&(e={});var o=null!=(n=e.timeout)?n:1e3,i=Object.keys(t).map(function(e){return[a(e),t[e]]}),c=new Map,f=null;return function(t){t instanceof KeyboardEvent&&(i.forEach(function(e){var n=e[0],o=e[1],i=c.get(n)||n;u(t,i[0])?i.length>1?c.set(n,i.slice(1)):(c.delete(n),o(t)):r(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,i=o.event,r=void 0===i?"keydown":i,a=o.capture,u=c(e,{timeout:o.timeout});return t.addEventListener(r,u,a),function(){t.removeEventListener(r,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]})$`, \"iv\")\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,cAAcD,EAAM,QAAQ,OAGhC,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"}