From ebfc96d8c5d26a1f8b8dde710391fb6880cb9ce6 Mon Sep 17 00:00:00 2001 From: Jamie Kyle Date: Tue, 20 Aug 2024 16:21:23 -0700 Subject: [PATCH] Add regex matching and export more --- README.md | 17 ++++++++++++---- example/index.html | 19 +++++++++--------- example/index.tsx | 7 ++++--- package.json | 2 +- src/tinykeys.ts | 50 +++++++++++++++++++++++++++++----------------- 5 files changed, 60 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index a519d5c..1177fb4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # `tinykeys` -> A tiny (~400 B) & modern library for keybindings. +> A tiny (~650 B) & modern library for keybindings. > [See Demo](https://jamiebuilds.github.io/tinykeys/) ## Install @@ -23,9 +23,9 @@ tinykeys(window, { "y e e t": () => { alert("The keys 'y', 'e', 'e', and 't' were pressed in order") }, - "$mod+KeyD": event => { + "$mod+([0-9])": event => { event.preventDefault() - alert("Either 'Control+d' or 'Meta+d' were pressed") + alert(`Either 'Control+${event.key}' or 'Meta+${event.key}' were pressed`) }, }) ``` @@ -173,6 +173,14 @@ platform keybindings: "$mod+Shift+D" // Meta/Control+Shift+D ``` +Alternatively, you can use parenthesis to use case-sensitive regular expressions +to match multiple keys. + +```js +"$mod+([0-9])" // $mod+0, $mod+1, $mod+2, etc... +// equivalent regex: /^[0-9]$/ +``` + ### Keybinding Sequences Keybindings can also consist of several key presses in a row: @@ -221,13 +229,14 @@ You can configure the behavior of tinykeys in a couple ways using a third `options` parameter. ```js -tinykey( +tinykeys( window, { M: toggleMute, }, { event: "keyup", + capture: true, }, ) ``` diff --git a/example/index.html b/example/index.html index adc86e0..e74c525 100644 --- a/example/index.html +++ b/example/index.html @@ -155,15 +155,16 @@

Examples

import tinykeys from "tinykeys" tinykeys(window, { - "Shift+D": () => { - alert("The 'Shift' and 'd' keys were pressed at the same time") - }, - "y e e t": () => { - alert("The keys 'y', 'e', 'e', and 't' were pressed in order") - }, - "$mod+KeyU": () => { - alert("Either 'Control+u' or 'Meta+u' were pressed") - }, + "Shift+D": () => { + alert("The 'Shift' and 'd' keys were pressed at the same time") + }, + "y e e t": () => { + alert("The keys 'y', 'e', 'e', and 't' were pressed in order") + }, + "$mod+([1-9])": event => { + event.preventDefault() + alert(`Either 'Control+${event.key}' or 'Meta+${event.key}' were pressed`) + }, })
what does this button doimport confetti from "canvas-confetti" diff --git a/example/index.tsx b/example/index.tsx index c99157c..00ac1f4 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -8,8 +8,9 @@ tinykeys(window, { "y e e t": () => { alert("The keys 'y', 'e', 'e', and 't' were pressed in order") }, - "$mod+KeyU": () => { - alert("Either 'Control+u' or 'Meta+u' were pressed") + "$mod+([1-9])": event => { + event.preventDefault() + alert(`Either 'Control+${event.key}' or 'Meta+${event.key}' were pressed`) }, }) @@ -29,7 +30,7 @@ const KonamiCode = [ tinykeys(window, { [KonamiCode]: () => { - const duration = 15 * 1000 + const duration = 5 * 1000 const animationEnd = Date.now() + duration const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 } diff --git a/package.json b/package.json index b60a38a..3683564 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tinykeys", "version": "2.1.0", - "description": "A tiny (~400 B) & modern library for keybindings.", + "description": "A tiny (~650 B) & modern library for keybindings.", "author": "Jamie Kyle ", "license": "MIT", "repository": "jamiebuilds/tinykeys", diff --git a/src/tinykeys.ts b/src/tinykeys.ts index ebd2b22..942f15d 100644 --- a/src/tinykeys.ts +++ b/src/tinykeys.ts @@ -1,4 +1,7 @@ -type KeyBindingPress = [string[], string] +/** + * A single press of a keybinding sequence + */ +export type KeyBindingPress = [mods: string[], key: string | RegExp] /** * A map of keybinding strings to event handlers. @@ -26,6 +29,11 @@ export interface KeyBindingOptions extends KeyBindingHandlerOptions { * Key presses will listen to this event (default: "keydown"). */ event?: "keydown" | "keyup" + + /** + * Key presses will use a capture listener (default: false) + */ + capture?: boolean } /** @@ -44,7 +52,7 @@ let DEFAULT_TIMEOUT = 1000 /** * Keybinding sequences should bind to this event by default. */ -let DEFAULT_EVENT = "keydown" +let DEFAULT_EVENT = "keydown" as const /** * Platform detection code. @@ -87,6 +95,8 @@ function getModifierState(event: KeyboardEvent, mod: string) { * = ` ...` * = `` or `+` * = `++...` + * = `` or `` (case-insensitive) + * = `()` -> `/^$/` (case-sensitive) */ export function parseKeybinding(str: string): KeyBindingPress[] { return str @@ -94,29 +104,36 @@ export function parseKeybinding(str: string): KeyBindingPress[] { .split(" ") .map(press => { let mods = press.split(/\b\+/) - let key = mods.pop() as string + let key: string | RegExp = mods.pop() as string + let match = key.match(/^\((.+)\)$/) + if (match) { + key = new RegExp(`^${match[1]}$`) + } mods = mods.map(mod => (mod === "$mod" ? MOD : mod)) return [mods, key] }) } /** - * This tells us if a series of events matches a key binding sequence either - * partially or exactly. + * This tells us if a single keyboard event matches a single keybinding press. */ -function match(event: KeyboardEvent, press: KeyBindingPress): boolean { +export function matchKeyBindingPress( + event: KeyboardEvent, + [mods, key]: KeyBindingPress, +): boolean { // prettier-ignore return !( // Allow either the `event.key` or the `event.code` // MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key // MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code ( - press[1].toUpperCase() !== event.key.toUpperCase() && - press[1] !== event.code + key instanceof RegExp ? !(key.test(event.key) || key.test(event.code)) : + (key.toUpperCase() !== event.key.toUpperCase() && + key !== event.code) ) || // Ensure all the modifiers in the keybinding are pressed. - press[0].find(mod => { + mods.find(mod => { return !getModifierState(event, mod) }) || @@ -124,7 +141,7 @@ function match(event: KeyboardEvent, press: KeyBindingPress): boolean { // keybinding. So if they are pressed but aren't part of the current // keybinding press, then we don't have a match. KEYBINDING_MODIFIER_KEYS.find(mod => { - return !press[0].includes(mod) && press[1] !== mod && getModifierState(event, mod) + return !mods.includes(mod) && key !== mod && getModifierState(event, mod) }) ) } @@ -180,7 +197,7 @@ export function createKeybindingsHandler( let remainingExpectedPresses = prev ? prev : sequence let currentExpectedPress = remainingExpectedPresses[0] - let matches = match(event, currentExpectedPress) + let matches = matchKeyBindingPress(event, currentExpectedPress) if (!matches) { // Modifier keydown events shouldn't break sequences @@ -232,14 +249,11 @@ export function createKeybindingsHandler( export function tinykeys( target: Window | HTMLElement, keyBindingMap: KeyBindingMap, - options: KeyBindingOptions = {}, + { event = DEFAULT_EVENT, capture, timeout }: KeyBindingOptions = {}, ): () => void { - let event = options.event ?? DEFAULT_EVENT - let onKeyEvent = createKeybindingsHandler(keyBindingMap, options) - - target.addEventListener(event, onKeyEvent) - + let onKeyEvent = createKeybindingsHandler(keyBindingMap, { timeout }) + target.addEventListener(event, onKeyEvent, capture) return () => { - target.removeEventListener(event, onKeyEvent) + target.removeEventListener(event, onKeyEvent, capture) } }