@waradu/keyboard
Version:
A keyboard manager compatible with JavaScript, TypeScript and Nuxt
603 lines (466 loc) • 11.6 kB
Markdown
# Keyboard Manager
A keyboard manager compatible with JavaScript, TypeScript and Nuxt.
[](https://github.com/Waradu/keyboard/actions/workflows/validate.yml)
- [Install](#install)
- [Get Started](#get-started)
- [Nuxt](#nuxt)
- [Usage](#usage)
- [Key Strings](#key-strings)
- [Handler](#handler)
- [Layers](#layers)
- [Config](#config)
- [Directives](#directives)
- [Changes](#changes)
- [Development](#development)
- [Examples](#examples)
## Install
```bash
bun install @waradu/keyboard
```
## Get Started
Create a keyboard instance and bind a shortcut:
```ts
import { Keyboard } from "@waradu/keyboard";
const keyboard = new Keyboard();
const off = keyboard.bind({
keys: "ctrl+k",
run({ event }) {
console.log("pressed", event.key);
},
});
off();
```
`Keyboard` initializes itself when `window` is available. If you create an instance before `window` exists, call `keyboard.init()` later.
## Nuxt
Add the Nuxt module to `nuxt.config.ts`:
```ts
export default defineNuxtConfig({
modules: ["@waradu/keyboard/nuxt"],
});
```
The module provides `$keyboard`, auto-imports composables, registers directives and initializes the keyboard on `app:mounted`.
```ts
const off = useKeybind({
keys: "a",
run() {
console.log("A key pressed");
},
});
```
`useKeybind` mirrors `keyboard.bind` and automatically removes the binding when the component unmounts.
```ts
useKeybind([
{
keys: "ctrl+z",
run() {
console.log("undo");
},
},
{
keys: "ctrl+shift+z",
run() {
console.log("redo");
},
},
]);
```
Create a layer in Nuxt:
```ts
const editor = useKeybindLayer("editor");
editor.bind({
keys: "escape",
run() {
editor.disable();
},
});
```
Inspect active handlers:
```ts
const { handlers, unsubscribe } = useKeyboardInspector();
console.log(handlers.value);
unsubscribe();
```
Record keybinds:
```ts
const stop = useKeybindRecorder((keybind) => {
console.log(keybind.toString());
});
stop();
```
Access the provided instance directly:
```ts
const { $keyboard } = useNuxtApp();
$keyboard.destroy();
```
The module accepts a `debug` option:
```ts
export default defineNuxtConfig({
modules: ["@waradu/keyboard/nuxt"],
keyboard: {
debug: true,
},
});
```
## Usage
### Bind One Handler
```ts
const off = keyboard.bind({
keys: ["ctrl+y", "ctrl+shift+z"],
run() {
console.log("redo");
},
config: {
prevent: true,
},
});
off();
```
`keys` can be a key string, a `Keybind`, a plain keybind shape, or an array of them.
```ts
keyboard.bind({
keys: {
platform: "macos",
key: "a",
modifiers: {
alt: true,
},
},
run() {},
});
```
### Bind Multiple Handlers
```ts
keyboard.bind([
{
keys: "ctrl+z",
run() {
console.log("undo");
},
},
{
keys: "ctrl+shift+z",
run() {
console.log("redo");
},
},
]);
```
You can pass shared config as the second argument:
```ts
keyboard.bind(
[
{
keys: "ctrl+s",
run() {
console.log("save");
},
},
],
{
prevent: true,
},
);
```
### Inspect Handlers
```ts
const unsubscribe = keyboard.subscribe((handlers) => {
console.log("active handlers", handlers);
});
unsubscribe();
```
### Record Keybinds
```ts
const stop = keyboard.record((keybind) => {
console.log(keybind.toString());
});
stop();
```
### Keybinds
Use `Keybind` when you need to parse, compare, format, or display shortcuts.
```ts
import { Keybind } from "@waradu/keyboard";
const keybind = Keybind.fromString("ctrl-cmd+shift+k");
keybind?.toString(); // "ctrl-cmd+shift+k"
keybind?.toShape(); // { key, modifiers, platform? }
keybind?.equals("ctrl-cmd+shift+k"); // true
Keybind.equals("ctrl+k", "ctrl+k"); // true
```
Readable methods return parts so you can join them into text or render them as separate keycaps.
```ts
keybind?.toReadable(); // ["Ctrl-Cmd", "Shift", "K"]
keybind?.toLocalReadable({ platform: "macos" }); // ["Cmd", "Shift", "K"]
keybind?.toLocalReadable({ platform: "windows" }); // ["Ctrl", "Shift", "K"]
Keybind.fromString("no-macos:alt+enter")?.toReadablePlatform(); // "Linux & Windows"
```
## Key Strings
Key strings describe the key and modifiers that must be active for a handler to run.
The structure is:
```txt
(platform:)?(meta+)?(ctrl+)?(ctrl-cmd+)?(alt+)?(shift+)?key
```
Special keys:
- `$any`: match any key and ignores modifiers
- `$num`: match any number key and expose it as `context.template`
Platform prefixes:
- `macos`
- `win`
- `linux`
- `no-macos`
- `no-win`
- `no-linux`
The modifier order is fixed: `meta`, `ctrl`, `ctrl-cmd`, `alt`, `shift`, then the key.
`ctrl-cmd` maps to Cmd (`meta`) on macOS and Ctrl (`ctrl`) on other platforms.
It can be combined with `alt` and `shift`, but not with `meta` or `ctrl`.
In `KeybindShape` / `CreateKeybindShape` objects, the same modifier field is named `ctrlCmd`.
Examples:
- `"ctrl+x"`: valid
- `"ctrl-cmd+k"`: valid
- `"ctrl-cmd+shift+k"`: valid
- `"meta+ctrl+alt+shift+arrow-up"`: valid
- `"c"`: valid
- `"macos:x"`: valid
- `"alt+$num"`: valid
- `"$any"`: valid
- `""`: invalid
- `"shift+alt+y"`: invalid, because `shift` comes after `alt`
- `"meta+ctrl"`: invalid, because the key is missing
- `"meta+ctrl-cmd+k"`: invalid, because `ctrl-cmd` cannot be mixed with `meta` or `ctrl`
- `"lunix:x"`: invalid platform
- `"xy"`: invalid, because only one key can be used
## Handler
The handler receives a context object:
```ts
keyboard.bind({
keys: "enter",
run(context) {
context.event;
context.handler;
context.template;
},
});
```
Context fields:
- `context.event`: the original `KeyboardEvent`
- `context.handler`: the registered handler
- `context.template`: the matched template value, currently used by `$num`
Handler return values are ignored.
## Layers
Layers group handlers so they can be enabled or disabled together.
```ts
const editor = keyboard.layers.create("editor");
editor.bind({
keys: "escape",
run() {
console.log("escape in editor layer");
},
});
editor.disable();
editor.enable();
editor.toggle();
editor.off();
```
Create a disabled layer:
```ts
const modal = keyboard.layers.create("modal", true);
```
Manage layers globally:
```ts
keyboard.layers.enable("editor");
keyboard.layers.disable(["modal", "editor"]);
keyboard.layers.set("modal");
keyboard.layers.all();
keyboard.layers.none();
```
You can also assign layers directly through config:
```ts
keyboard.bind({
keys: "escape",
run() {},
config: {
layers: ["modal"],
},
});
```
## Config
Keyboard-level config:
```ts
const keyboard = new Keyboard({
debug: true,
platform: "macos",
signal: abortController.signal,
});
```
Handler config:
```ts
keyboard.bind({
keys: "enter",
run() {},
config: {
once: true,
ignoreIfEditable: true,
runIfFocused: [document.getElementById("email")],
prevent: true,
stop: true,
when: true,
signal: abortController.signal,
},
});
```
Available handler config:
- `once`: remove the handler after the first run
- `ignoreIfEditable`: skip while an editable element is focused
- `runIfFocused`: only run if one of the provided elements is focused
- `prevent`: call `event.preventDefault()`
- `stop`: call `stopPropagation`, `stopImmediatePropagation`, or both
- `when`: boolean or predicate that controls whether the handler runs
- `layers`: layer names assigned to the handler
- `signal`: abort signal that removes the handler
`stop` accepts:
- `true`: call `event.stopPropagation()`
- `"immediate"`: call `event.stopImmediatePropagation()`
- `"both"`: call both propagation methods
## Directives
Directives are Nuxt-only. Pass the handler to `v-keybind` and use the directive arg as the key sequence.
```html
<input
type="text"
v-keybind:enter="
() => {
console.log('Hello, directive!');
}
"
/>
```
Put keyboard modifiers in the key sequence. `prevent` and `stop` are directive modifiers:
```html
<input type="text" v-keybind:ctrl+shift+enter.prevent.stop="onEnter" />
```
Use `ctrl-cmd` in the key sequence for the cross-platform Cmd/Ctrl key:
```html
<input type="text" v-keybind:ctrl-cmd+k="openCommandPalette" />
```
For platform-aware or multi-key bindings, use `useKeybind`.
The directive automatically limits the handler to that focused element.
## Changes
### v8 -> v9
- Replaced `useKeyboard()` with the `Keyboard` class
- Renamed `keyboard.listen` to `keyboard.bind`
- Renamed the `control` modifier to `ctrl`
- Changed key-string separators from `_` to `+`
- Changed catch-all key from `any` to `$any`
- Added the `Keybind` class for parsing, formatting, comparing, and converting keybinds
- Added readable `Keybind` labels for rendering shortcuts in UI
- Added automatic initialization when `window` is available
- Added `ctrl-cmd` as a cross-platform Cmd/Ctrl modifier
- Added `pause()`, `resume()`, and `toggle()` for temporarily pausing the keyboard
- Redesigned the Nuxt directive API to use `v-keybind:sequence` with `.prevent` and `.stop`
- Fixed run return type by ignoring it
- Better and faster OS detection
- Removed stats
### v7.4 -> v8
- Added `parseKeyData` to parse key data into a key string
- Renamed `FormattedKeySequence` to `KeyData`
- `KeyData` can now also be used to define keys
- Changed `KeyData` format
### v7.3 -> v7.4
- Added `keyboard.exists` to check if a key-string handler already exists
- Added `config.when` to control whether a handler runs
### v7.2 -> v7.3
- Added `keyboard.layers` to create and manage layers
- Added Nuxt-only `useKeybindLayer` composable
### v7.1 -> v7.2
- Added `keyboard.subscribe` for inspecting active handlers
- Added `keyboard.record` to record a keybind
- Added `parseKeyString` to parse a key string into key data
- Added Nuxt-only `useKeyboardInspector` and `useKeybindRecorder`
### v7 -> v7.1
- Added `v-keybind` and `v-run` directives
- Allowed passing a single sequence as the `keys` argument instead of requiring an array
## Development
You need [Bun](https://bun.sh).
```bash
bun install
```
Commands:
- `bun test`: run tests
- `bunx tsc --noEmit`: run type checking
- `bun run lint`: run linting
- `bun run format`: format files
- `bun run format:check`: check formatting
- `bun playground:prepare`: install playground dependencies
- `bun playground`: start the playground
## Examples
Catch any key press:
```ts
keyboard.bind({
keys: "$any",
run(ctx) {
console.log("Key pressed:", ctx.event.key);
},
});
```
Run only when an input is focused:
```ts
const input = document.getElementById("myInput");
keyboard.bind({
keys: "enter",
run() {
console.log("Enter pressed while input is focused");
},
config: {
runIfFocused: [input],
},
});
```
Prevent default behavior:
```ts
keyboard.bind({
keys: "ctrl+r",
run() {
console.log("Refresh prevented");
},
config: {
prevent: true,
},
});
```
Run a handler only once:
```ts
keyboard.bind({
keys: "escape",
run() {
console.log("Escape pressed once");
},
config: {
once: true,
},
});
```
Platform-aware undo and redo:
```ts
keyboard.bind([
{
// With ctrl-cmd
keys: "ctrl-cmd+z",
run() {
console.log("undo");
},
},
{
// Manually
keys: ["no-macos:ctrl+shift+z", "macos:meta+shift+z"],
run() {
console.log("redo");
},
},
]);
```
Catch Alt plus any number:
```ts
keyboard.bind({
keys: "alt+$num",
run(ctx) {
console.log("Number pressed:", ctx.template);
},
});
```