Skip to content

Commit

Permalink
Add regex matching and export more
Browse files Browse the repository at this point in the history
  • Loading branch information
jamiebuilds committed Aug 20, 2024
1 parent c366447 commit ebfc96d
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 35 deletions.
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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`)
},
})
```
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
},
)
```
Expand Down
19 changes: 10 additions & 9 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,16 @@ <h2 class="font-bold text-2xl">Examples</h2>
import tinykeys from "tinykeys"

tinykeys(window, {
<strong class="text-emerald-400">"Shift+D"</strong>: () => {
<em class="text-pink-400">alert("The 'Shift' and 'd' keys were pressed at the same time")</em>
},
<strong class="text-emerald-400">"y e e t"</strong>: () => {
<em class="text-pink-400">alert("The keys 'y', 'e', 'e', and 't' were pressed in order")</em>
},
<strong class="text-emerald-400">"$mod+KeyU"</strong>: () => {
<em class="text-pink-400">alert("Either 'Control+u' or 'Meta+u' were pressed")</em>
},
<strong class="text-emerald-400">"Shift+D"</strong>: () => {
<em class="text-pink-400">alert("The 'Shift' and 'd' keys were pressed at the same time")</em>
},
<strong class="text-emerald-400">"y e e t"</strong>: () => {
<em class="text-pink-400">alert("The keys 'y', 'e', 'e', and 't' were pressed in order")</em>
},
<strong class="text-emerald-400">"$mod+([1-9])"</strong>: event => {
event.preventDefault()
<em class="text-pink-400">alert(`Either 'Control+${event.key}' or 'Meta+${event.key}' were pressed`)</em>
},
})
<details><summary class="text-slate-500 bg-slate-800 rounded p-1 my-1">what does this button do</summary>import confetti from "canvas-confetti"

Expand Down
7 changes: 4 additions & 3 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
},
})

Expand All @@ -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 }

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <me@thejameskyle.com>",
"license": "MIT",
"repository": "jamiebuilds/tinykeys",
Expand Down
50 changes: 32 additions & 18 deletions src/tinykeys.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -87,44 +95,53 @@ function getModifierState(event: KeyboardEvent, mod: string) {
* <sequence> = `<press> <press> <press> ...`
* <press> = `<key>` or `<mods>+<key>`
* <mods> = `<mod>+<mod>+...`
* <key> = `<KeyboardEvent.key>` or `<KeyboardEvent.code>` (case-insensitive)
* <key> = `(<regex>)` -> `/^<regex>$/` (case-sensitive)
*/
export function parseKeybinding(str: string): KeyBindingPress[] {
return str
.trim()
.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)
}) ||

// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a
// 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)
})
)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

0 comments on commit ebfc96d

Please sign in to comment.