A simple yet powerful keybind manager compatible with vanilla and nuxt js.
- Install
- Get Started
- Nuxt
- Usage
- Key Sequence
- Handler
- Layers
- Config
- Directives (Nuxt)
- Changes
- Development
- Examples
bun install @waradu/keyboardStart by importing useKeyboard and create a new keyboard instance.
import { useKeyboard } from "@waradu/keyboard";
const keyboard = useKeyboard();Nuxt users can use the built-in module that automatically creates and initializes a keyboard instance. It also cleans up listeners when the component unmounts.
First add the package to the modules in nuxt.config.ts:
export default defineNuxtConfig({
modules: ["@waradu/keyboard/nuxt"],
});And then use it like this:
useKeybind({
keys: ["a"],
run() {
console.log("A key pressed");
},
});You can also use directives if you want.
It is also possible to set up your own composable and plugin for more control. Just copy the templates from the links below and skip adding @waradu/keyboard/nuxt to your nuxt.config.ts:
You can also use the helper composable which returns a ref that stays in sync with $keyboard.subscribe:
const { listeners, unsubscribe } = useKeyboardInspector(); // auto unsubscribes on unmountTo record sequences in Nuxt, use the helper that wraps keyboard.record and cleans up on unmount:
// auto stops on unmount
const stop = useKeybindRecorder((sequence) => {
console.log("pressed", sequence); // e.g. "control_shift_k"
});If you need to access the useKeyboard instance use the Nuxt plugin.
const { $keyboard } = useNuxtApp();
$keyboard.destroy();Do not forget to call keyboard.init(); once window is available.
A listener can be really simple. You just need one or more key sequences, a handler and the config (optional).
const unlisten = keyboard.listen({
keys: ["control_y", "control_shift_z"], // key sequences
run() {
// handler
console.log("redo");
},
config: {
// config
},
});
// with key data
keyboard.listen({
keys: [
{
key: "a",
modifiers: ["alt"],
platform: "macos",
},
],
run() {},
});keyboard.listen returns a unlisten function that can be called to remove the listener.
const unlisten = keyboard.listen(...);
unlisten();You can inspect all active listeners by subscribing:
const { listeners, unsubscribe } = keyboard.subscribe((handlers) => {
console.log("changed", handlers);
});
console.log("current listeners", listeners);
unsubscribe();You can also record key sequences to help users configure shortcuts:
const stop = keyboard.record((sequence) => {
console.log("pressed", sequence); // e.g. "control_shift_k"
});
stop();Parse a key string into parts:
import { parseKeyString } from "@waradu/keyboard";
parseKeyString("meta_shift_k");
// { modifiers: ["meta", "shift"], key: "k" }
parseKeyString("macos:meta_k");
// { platform: "macos", modifiers: ["meta"], key: "k" }
// invalid strings return undefined
parseKeyString("unknown_mod_k"); // undefinedOr reverse:
import { parseKeyData } from "@waradu/keyboard";
parseKeyData({ modifiers: ["meta", "shift"], key: "k" });
// "meta_shift_k"
parseKeyData({ platform: "macos", modifiers: ["meta"], key: "k" });
// "macos:meta_k"It is also possible to define multiple keybinds in one listen call.
keyboard.listen([
{
keys: ["control_z"],
run() {
console.log("undo");
},
},
{
keys: ["control_shift_z"],
run() {
console.log("redo");
},
},
]);Key sequences are just strings of characters defining the key that needs to be pressed to activate the listener. A listener can have multiple key sequences.
Details:
The structure looks like this (? = optional, ! = required):
"(platform:)?(meta_)?(control_)?(alt_)?(shift_)?(key)!" or "any"
platform: Optionally include or exclude certain platforms, for examplemacosorno-linux. (experimental)modifiers: Keys likecontrolorshift. They have a fixed order but are optional.key: The actual key. Supports letters, numbers, symbols, templates and more (f4,dollar,arrow-up,$numetc.). This part is required. If you notice a missing character or symbol you need, please open an issue.
Meta is the equivalent of windows key on windows or cmd on macos.
The order is fixed, the key will always come last, control always after meta etc. The modifiers are not required.
Platform detection is not always reliable. Use it at your own risk, or create your own platform detector and set it through the config.
Patterns:
Currently there is only one template:
$num: Match any number
Examples:
Some examples to get a better understanding:
"control_x": โ"meta_control_alt_shift_arrow-up": โ"c": โ"macos:x": โ"$num": โ (number pattern)"any": โ (catch all)"": โ (empty string)"shift_alt_y": โ (shiftcomes afteralt)"meta_control": โ (keyis required)"lunix:x": โ (lunixis not a valid platform)"xy": โ (only onekeyat a time)
The handler is a function that runs when the key sequence is pressed. It can be written in multiple ways.
keyboard.listen({
...
run(context) { ... } // object method (preferred)
run: (context) => { ... } // arrow function
run: function (context) { ... } // function expression
run: handleEvent // external function
...
});context.event: The unchanged event from the event listenercontext.listener: The listenercontext.template: The result of the template if matched
Layers are used to toggle multiple keybinds together.
// can also be a list like ["editor", "anotherlayer"]
const editor = keyboard.layers.create("editor", {
enabled: true, // enabled by default
});
// these only apply to the layers defined above
editor.disable(); // disable the layer(s)
editor.enable(); // enable the layer(s)
editor.toggle(); // toggles each layer individually
editor.listen(...); // the same as keyboard.listen
editor.off(); // disables all listenersYou can also manage layers globally.
keyboard.layers.enable("editor");
keyboard.layers.disable(["anotherlayer", "editor"]);
keyboard.layers.set("anotherlayer"); // Only enable these and disable every other layers
keyboard.layers.all(); // Enable all layers
keyboard.layers.none(); // Disable all layersin Nuxt there is a useKeybindLayer composable that auto unlistens on unmount.
You can set your own platform and skip the built-in detection from keyboard.init. Just pass one of these values as the platform option: "macos" | "linux" | "windows" | "unknown". This is needed for the key sequences platform prefix.
const detectedPlatform = await yourOwnPlatformDetection();
const keyboard = useKeyboard({
platform: detectedPlatform,
});Each listener can also be configure separately. All keys are optional.
const emailInput = document.getElementById("emailInput"); // Normal
const passwordInput = useTemplateRef("passwordInput"); // Nuxt
keyboard.listen({
...
config: {
// Remove the listener after one run.
once: true,
// Ignore the listener if any text element like input is focused.
ignoreIfEditable: true,
// A list of elements which one has to be focused for the listener to run.
// The DOM needs to be ready ("DOMContentLoaded" or onMounted (nuxt)).
runIfFocused: [emailInput, passwordInput],
// Call preventDefault() before run.
prevent: true,
// Call stopPropagation() before run. (use "immediate" for stopImmediatePropagation() and "both" for both).
stop: true,
// Boolean value or predicate function before each run whether the listener should run.
when: true,
}
});Also you can pass a signal to the config or the useKeyboard to abort them with a signal.
This is Nuxt only.
Add a keybind listener to any element by combining v-keybind and v-run on the same element.
<input
type="text"
v-keybind="'enter'"
v-run="
() => {
console.log('Hello, Directive!');
}
"
/>You can also use modifiers to prevent the default browser behavior and/or run the handler only once:
<input type="text" v-keybind.prevent.once="'enter'" v-run="onEnter" />Both v-keybind and v-run must be defined on the same element. If one of them is missing, the keybind will not be registered.
The function passed to v-run behaves the same as the run callback in keyboard.listen or useKeybind (Nuxt). This means you can also use the HandlerContext parameter:
<input
type="text"
v-keybind="['no-macos:control_$num', 'macos:meta_$num']"
v-run="
(ctx: HandlerContext) => {
console.log(ctx.template);
}
"
/>- Added
parseKeyDatato parse a key data into a key string - Renamed
FormattedKeySequencetoKeyData KeyDatacan now also be used to define keys- Changed
KeyDataformat
- Added
keyboard.existsto check if a Key String listener already exists. - Added
config.whento control whether a listener runs, using either a boolean or a predicate function.
- Added
keyboard.layersto create and manage layers - Added Nuxt-only
useKeybindLayercomposable
- Added
keyboard.subscribefor inspecting active listeners - Added
keyboard.recordto record a key sequence - Added
parseKeyStringto parse a key string into key data - Added Nuxt-only
useKeyboardInspectoranduseKeybindRecorderhelper
- Added
v-keybindandv-rundirectives - Allow passing a single sequence as the
keysargument instead of requiring an array
- Added
contextto handler $numkey template- Fixed a bug that ignored the order of key presses
anywill no longer trigger when a modifier is pressed
- Use
e.keyinstead ofe.code - Support multiple keybinds per listener
- No longer need to use
Key.* - Rewrite
runIfFocusedto allow multiple targets - Ignore
event.isComposingand Dead keys - Remove
ignoreCase - Platform specific keybinds.
- Restructure
You need bun.
bun install- ๐
Start Playground:
bun playground:preparebun playground- ๐
Commands:
bun test: run testsbun test-types: run type testsbun playground: start playground
Catch any key press:
keyboard.listen({
keys: ["any"],
run(ctx) {
console.log("Key pressed:", ctx.event.key);
},
});Run only when an input is focused:
const input = document.getElementById("myInput");
keyboard.listen({
keys: ["enter"],
run() {
console.log("Enter pressed while input is focused");
},
config: {
runIfFocused: [input],
},
});Prevent default behavior (disable refresh with Ctrl+R):
keyboard.listen({
keys: ["control_r"],
run() {
console.log("Refresh prevented!");
},
config: {
prevent: true,
},
});Run a listener only once:
keyboard.listen({
keys: ["escape"],
run() {
console.log("Escape pressed, this will only log once.");
},
config: {
once: true,
},
});Platform aware undo/redo:
keyboard.listen([
{
keys: ["no-macos:control_z", "macos:meta_z"],
run() {
console.log("undo");
},
},
{
keys: ["no-macos:control_shift_z", "macos:meta_shift_z"],
run() {
console.log("redo");
},
},
]);Catch alt with any number:
keyboard.listen({
keys: ["alt_$num"],
run(ctx) {
console.log("Key pressed:", ctx.template!); // 0..9
},
});