@rocketc/shortcuts
v0.0.1
Published
pure javascript shortcut solution
Readme
@rocketc/shortcuts
Pure JavaScript shortcut solution without any framework dependencies.
Features
- Framework-agnostic: Works with any JavaScript framework or vanilla JavaScript
- Strict/Loose mode: Support both strict and loose matching modes
- Dynamic register/unregister: Register and unregister shortcuts at runtime
- Dynamic enable/disable: Enable or disable registered shortcuts dynamically
- Flexible key combinations: Support complex modifier and normal key combinations
- Modern browser API: Uses modern browser APIs (
KeyboardEvent.code) - Full TypeScript support: Complete type definitions included
- Shortcut validation: Built-in validation for accelerator strings
- Custom filters: Filter keyboard events based on your needs
- Custom aliases: Define custom key name aliases
- Event listeners: Support
keydownandkeyupevent listeners
Installation
# npm
npm install @rocketc/shortcuts
# yarn
yarn add @rocketc/shortcuts
# pnpm
pnpm add @rocketc/shortcutsSupported Keys
Modifiers
| Key | Alias | Notes |
| -------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| ControlLeft | Ctrl CtrlLeft Control ControlLeft | |
| ControlRight | Ctrl CtrlRight Control ControlRight | |
| MetaLeft | Meta MetaLeft Super SuperLeft Command CommandLeft Cmd CmdLeft Win WinLeft | Windows on Windows, Command on MacOS |
| MetaRight | Meta MetaRight Super SuperRight Command CommandRight Cmd CmdRight Win WinRight | Windows on Windows, Command on MacOS |
| ShiftLeft | Shift ShiftLeft | |
| ShiftRight | Shift ShiftRight | |
| AltLeft | Alt AltLeft Option OptionLeft | Option is only available on MacOS. |
| AltRight | Alt AltRight Option OptionRight | Option is only available on MacOS. |
| OSLeft | Meta MetaLeft Super SuperLeft | Firefox only |
| OSRight | Meta MetaRight Super SuperRight | Firefox only |
Normal Keys
Function Keys
| Key | Notes |
| ----- | ------------ |
| F1 | Function key |
| F2 | Function key |
| F3 | Function key |
| F4 | Function key |
| F5 | Function key |
| F6 | Function key |
| F7 | Function key |
| F8 | Function key |
| F9 | Function key |
| F10 | Function key |
| F11 | Function key |
| F12 | Function key |
Number Keys
| Key | Notes |
| --- | --------------------------------------- |
| 0 | Zero (main keyboard) |
| 1 | One (main keyboard) |
| 2 | Two (main keyboard) |
| 3 | Three (main keyboard) |
| 4 | Four (main keyboard) |
| 5 | Five (main keyboard) |
| 6 | Six (main keyboard) |
| 7 | Seven (main keyboard) |
| 8 | Eight (main keyboard) |
| 9 | Nine (main keyboard) |
| - | Minus (main keyboard) |
| = | Equal (main keyboard) |
| + | Plus (main keyboard, not Shift+=) |
| * | Multiply (main keyboard, not Shift+8) |
Numpad Keys
| Key | Notes |
| ---------- | --------------------- |
| num0 | Numpad zero |
| num1 | Numpad one |
| num2 | Numpad two |
| num3 | Numpad three |
| num4 | Numpad four |
| num5 | Numpad five |
| num6 | Numpad six |
| num7 | Numpad seven |
| num8 | Numpad eight |
| num9 | Numpad nine |
| numadd | Numpad add (+) |
| numsub | Numpad subtract (-) |
| nummult | Numpad multiply (*) |
| numdiv | Numpad divide (/) |
| numenter | Numpad enter |
| numdec | Numpad decimal (.) |
| numLock | NumLock |
Alphabet Keys
| Key | Notes |
| --- | ----------- |
| a | Lowercase a |
| b | Lowercase b |
| c | Lowercase c |
| d | Lowercase d |
| e | Lowercase e |
| f | Lowercase f |
| g | Lowercase g |
| h | Lowercase h |
| i | Lowercase i |
| j | Lowercase j |
| k | Lowercase k |
| l | Lowercase l |
| m | Lowercase m |
| n | Lowercase n |
| o | Lowercase o |
| p | Lowercase p |
| q | Lowercase q |
| r | Lowercase r |
| s | Lowercase s |
| t | Lowercase t |
| u | Lowercase u |
| v | Lowercase v |
| w | Lowercase w |
| x | Lowercase x |
| y | Lowercase y |
| z | Lowercase z |
| A | Uppercase A |
| B | Uppercase B |
| C | Uppercase C |
| D | Uppercase D |
| E | Uppercase E |
| F | Uppercase F |
| G | Uppercase G |
| H | Uppercase H |
| I | Uppercase I |
| J | Uppercase J |
| K | Uppercase K |
| L | Uppercase L |
| M | Uppercase M |
| N | Uppercase N |
| O | Uppercase O |
| P | Uppercase P |
| Q | Uppercase Q |
| R | Uppercase R |
| S | Uppercase S |
| T | Uppercase T |
| U | Uppercase U |
| V | Uppercase V |
| W | Uppercase W |
| X | Uppercase X |
| Y | Uppercase Y |
| Z | Uppercase Z |
Punctuation Keys
| Key | Notes |
| ------- | ------------ |
| , | Comma |
| . | Period |
| / | Slash |
| ; | Semicolon |
| ' | Quote |
| [ | BracketLeft |
| ] | BracketRight |
| \ | Backslash |
| ` | Backquote |
Other Keys
| Key | Notes |
| ------------ | ------------------------------ |
| Space | Space bar |
| Escape | Escape key (alias: Esc) |
| Esc | Escape key (alias of Escape) |
| Backspace | Backspace |
| Delete | Delete key |
| Tab | Tab |
| CapsLock | Caps Lock |
| Enter | Enter key |
| ArrowUp | Up arrow |
| ArrowDown | Down arrow |
| ArrowLeft | Left arrow |
| ArrowRight | Right arrow |
| Home | Home |
| End | End |
| PageUp | Page Up |
| PageDown | Page Down |
| Insert | Insert |
Quick Start
Basic Usage
import { ShortcutRegistry } from '@rocketc/shortcuts';
// Create a registry instance
const registry = new ShortcutRegistry();
// Attach to window
const dispose = registry.attachElement(window);
// Register a shortcut
registry.registerShortcut('Ctrl+a', (event) => {
console.log('Ctrl+A pressed!');
event.preventDefault();
});
// Clean up when done
dispose();With Options
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry({
strict: false, // Enable loose mode (default: false)
debug: true, // Enable debug logging
filter: (event) => {
// Custom filter logic
return !event.repeat;
},
alias: {
Save: 'Ctrl',
},
});
registry.attachElement(window);
registry.registerShortcut('Save+s', () => {
console.log('Save shortcut triggered');
});Examples
1. Register Single Key Shortcut
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry();
registry.attachElement(window);
registry.registerShortcut('a', (event) => {
console.log('Key A pressed');
event.preventDefault();
});2. Register Shortcut with Modifiers
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry();
registry.attachElement(window);
registry.registerShortcut('Ctrl+Shift+a', (event) => {
console.log('Ctrl+Shift+A pressed');
event.preventDefault();
});3. Dynamic Enable/Disable
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry();
registry.attachElement(window);
// Register shortcut
registry.registerShortcut('Ctrl+a', () => {
console.log('Ctrl+A pressed');
});
// Disable it
registry.disableShortcut('Ctrl+a');
// Enable it again
registry.enableShortcut('Ctrl+a');4. Multiple Callbacks for Same Shortcut
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry();
registry.attachElement(window);
const handler1 = () => console.log('Handler 1');
const handler2 = () => console.log('Handler 2');
// Register multiple handlers
registry.registerShortcut('Ctrl+a', handler1);
registry.registerShortcut('Ctrl+a', handler2);
// Disable only handler1
registry.disableShortcut('Ctrl+a', handler1);
// Unregister only handler1
registry.unregisterShortcut('Ctrl+a', handler1);5. Scoped Shortcuts (Attach to Specific Element)
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry();
const element = document.getElementById('my-element');
// Attach to specific element
const dispose = registry.attachElement(element);
// Register shortcut (only works when element is focused)
registry.registerShortcut('Ctrl+a', () => {
console.log('Ctrl+A pressed in element');
});
// Clean up
dispose();6. Event Listeners
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry();
registry.attachElement(window);
// Listen to key state changes (triggered on both keydown and keyup)
const disposeListener = registry.onKeyPressedChanged((event) => {
// event.detail indicates the event type: 'keydown' or 'keyup'
console.log('Key state changed:', event.detail); // 'keydown' or 'keyup'
// Get current pressed keys when state changes
const currentKeys = registry.getCurrentKeyPressed();
console.log('Current keys:', currentKeys);
});
// Get current pressed keys
const currentKeys = registry.getCurrentKeyPressed();
console.log('Current keys:', currentKeys);
// Clean up
disposeListener();6.1. Clear State
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry();
registry.attachElement(window);
// Press some keys
dispatchEvent('keydown', 'ControlLeft');
dispatchEvent('keydown', 'KeyA');
console.log(registry.getCurrentKeyPressed()); // 'ControlLeft+A'
// Clear the pressed keys state
registry.clear();
console.log(registry.getCurrentKeyPressed()); // ''7. Custom Filter
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry({
filter: (event) => {
// Only handle events from non-input elements
if (event.target instanceof HTMLElement) {
return !['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName);
}
return true;
},
});
registry.attachElement(window);
registry.registerShortcut('Ctrl+a', () => {
console.log('Ctrl+A pressed');
});8. Custom Aliases
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry({
alias: {
Save: 'Ctrl',
Quit: 'Ctrl+Shift',
},
});
registry.attachElement(window);
// Use custom alias
registry.registerShortcut('Save+s', () => {
console.log('Save shortcut');
});
registry.registerShortcut('Quit+q', () => {
console.log('Quit shortcut');
});9. Loose Mode
import { ShortcutRegistry } from '@rocketc/shortcuts';
const registry = new ShortcutRegistry({
strict: false, // Enable loose mode
});
registry.attachElement(window);
// In loose mode, getCurrentKeyPressed returns normalized keys
registry.onKeyPressedChanged(() => {
// If ControlLeft+A is pressed, returns 'Ctrl+a' instead of 'ControlLeft+A'
console.log(registry.getCurrentKeyPressed()); // 'Ctrl+a'
});10. Using Accelerator Parser
import { acceleratorParser } from '@rocketc/shortcuts';
// Validate accelerator string
const isValid = acceleratorParser.validate('Ctrl+a');
console.log(isValid); // true
// Convert to loose mode
const loose = acceleratorParser.convertAcceleratorToLooseMode('ControlLeft+a');
console.log(loose); // 'Ctrl+a'
// Parse accelerator
const parsed = acceleratorParser.parse('Ctrl+Shift+a');
console.log(parsed); // ['Ctrl', 'Shift', 'a']
// Check if key code name is supported
const isSupported = acceleratorParser.isKeyCodeNameSupported('Ctrl');
console.log(isSupported); // true
// Check if accelerators match
const isMatched = acceleratorParser.isAcceleratorMatched(
'Ctrl+a',
'ControlLeft+KeyA',
);
console.log(isMatched); // true
// Get default separator
const separator = acceleratorParser.defaultSeparator;
console.log(separator); // '+'API Reference
ShortcutRegistry
Main class for managing keyboard shortcuts.
Constructor
new ShortcutRegistry(options?: ShortcutRegisterOptions)Options:
interface ShortcutRegisterOptions {
separator?: string; // Default: '+'
strict?: boolean; // Default: false (loose mode by default)
debug?: boolean | ((...args: any[]) => void); // Default: false
filter?: Filter; // Custom event filter
alias?: Record<string, string>; // Custom key aliases
}Methods
attachElement(ele: Window | HTMLElement): Dispose
Attach keyboard event listeners to the specified element. Returns a dispose function to detach listeners.
Reference Counting:
- If the same element is attached multiple times, the library uses reference counting to track the number of attachments.
- Each call to
attachElementon the same element returns a different dispose function to avoid interference between different parts of your code. - Each dispose function can only be called once (subsequent calls are no-ops).
- The dispose functions must be called the same number of times as
attachElementwas called to fully detach the listeners. - This design allows multiple parts of your code to attach to the same element independently, and each part can safely dispose its own reference without affecting others.
Note:
- Automatically attaches a global
windowblur event listener to clear the internal state when the window loses focus. This listener is attached only once when the first element is attached, and removed when the last element is disposed. - All listeners (keydown, keyup, and window blur) are properly cleaned up when all dispose functions have been called (reference count reaches zero).
registerShortcut(accelerator: Accelerator, callback: KeyboardEventListener): boolean
Register a shortcut handler. Returns false if the shortcut is invalid.
unregisterShortcut(accelerator: Accelerator, cb?: KeyboardEventListener): boolean
Unregister a shortcut handler. If cb is provided, only unregister the specific callback. Returns false if the shortcut is not registered.
enableShortcut(accelerator: Accelerator, cb?: KeyboardEventListener): boolean
Enable a shortcut. If cb is provided, only enable the specific callback. Returns false if the shortcut is not registered.
disableShortcut(accelerator: Accelerator, cb?: KeyboardEventListener): boolean
Disable a shortcut. If cb is provided, only disable the specific callback. Returns false if the shortcut is not registered.
isShortcutRegistered(accelerator: Accelerator): boolean
Check if a shortcut is registered.
getCurrentKeyPressed(): Accelerator
Get the current pressed keys as an accelerator string.
Note:
- Returns an empty string (
'') when no keys are currently pressed (no modifiers and no normal key). - The internal state (
modifiersPressedandnormalKeyPressed) is automatically cleared when the window loses focus (via a globalwindowblur event listener attached byattachElement). If you need to manually clear the state, you can callclear().
onKeyPressedChanged(cb: (event: CustomEvent<'keydown' | 'keyup'>) => void): Dispose
Register a listener for key state changes. This listener is triggered whenever the pressed keys state changes, which occurs on both keydown and keyup events (after filtering). Returns a dispose function.
Event Details:
event.detail: Indicates the event type, either'keydown'or'keyup'event.type: Always'keyPressedChanged'
Note:
- The listener is triggered when the internal state (
modifiersPressedornormalKeyPressed) changes. - In
handleKeyup, the internal state is updated before the filter check. This ensures that the state remains consistent even if the filter returnsfalsefor the event. However, thekeyPressedChangedevent only occurs if the filter returnstrue. This design ensures state consistency while respecting the filter configuration. - To get the current pressed keys when the state changes, call
getCurrentKeyPressed()inside the listener callback.
Example:
registry.onKeyPressedChanged((event) => {
if (event.detail === 'keydown') {
console.log('Key pressed');
} else if (event.detail === 'keyup') {
console.log('Key released');
}
const currentKeys = registry.getCurrentKeyPressed();
console.log('Current keys:', currentKeys);
});getOptions(): ShortcutRegisterOptions
Get current options.
setOptions(options: ShortcutRegisterOptions): void
Update options. Note: This replaces all options, not merges them.
getShortcutRegisters(accelerator?: Accelerator): Array<ShortcutRegister>
Get all registered shortcuts, or shortcuts for a specific accelerator.
clear(): void
Clear the current pressed keys state (modifiers and normal keys). This is useful for resetting the state when needed.
Note: The library automatically clears the state when the window loses focus (via a global window blur event listener attached by attachElement). You can also manually call clear() if you need to reset the state at other times.
Example:
const registry = new ShortcutRegistry();
registry.attachElement(window);
// Press some keys
dispatchEvent('keydown', 'ControlLeft');
dispatchEvent('keydown', 'KeyA');
console.log(registry.getCurrentKeyPressed()); // 'ControlLeft+A'
// Clear the state
registry.clear();
console.log(registry.getCurrentKeyPressed()); // ''acceleratorParser
Utility object for parsing and validating accelerator strings.
Methods
parse(accelerator: Accelerator, options?): Array<KeyCodeName>
Parse an accelerator string into an array of key code names.
Options:
{
separator?: string; // Default: '+'
alias?: Record<string, string>; // Custom key aliases
}validate(accelerator: Accelerator, options?): boolean
Validate if an accelerator string is valid. Returns true if valid, false otherwise.
Options:
{
separator?: string; // Default: '+'
alias?: Record<string, string>; // Custom key aliases
}convertAcceleratorToLooseMode(accelerator: Accelerator, options?): Accelerator
Convert an accelerator to loose mode format. In loose mode, modifier keys are normalized (e.g., ControlLeft and ControlRight both become Ctrl).
Options:
{
separator?: string; // Default: '+'
alias?: Record<string, string>; // Custom key aliases
}isKeyCodeNameSupported(keyCodeName: string): boolean
Check if a key code name is valid.
isAcceleratorMatched(source: Accelerator, target: Accelerator, options?): boolean
Check if two accelerators match each other.
Options:
{
separator?: string; // Default: '+'
alias?: Record<string, string>; // Custom key aliases
}defaultSeparator: string
The default separator used for accelerator strings (default: '+').
Types
type Accelerator = string;
type Dispose = () => void;
type Filter = (event: KeyboardEvent) => boolean;
type KeyboardEventListener = (event: KeyboardEvent) => void;
type KeyCodeName = ModifierKeyCodeName | NormalKeyCodeName;
interface ShortcutRegister {
accelerator: Accelerator;
enabled: boolean;
callback: KeyboardEventListener;
}Note: KeyCodeName is exported from the package and can be imported directly:
import type { KeyCodeName } from '@rocketc/shortcuts';Behavior Notes
State Management
Window Focus: The internal state (
modifiersPressedandnormalKeyPressed) is automatically cleared when the window loses focus. This is handled by a globalwindowblur event listener that is automatically attached when you callattachElement()for the first time, and removed when the last element is disposed. You don't need to manually handle this unless you're not usingattachElement().Key State Change Events: The
onKeyPressedChangedlistener is triggered whenever the pressed keys state changes, which occurs on bothkeydownandkeyupevents (after filtering). InhandleKeyup, the internal state is updated before the filter check. This ensures that the state remains consistent even if the filter returnsfalsefor the event. However, thekeyPressedChangedevent only occurs if the filter returnstrue. This design ensures state consistency while respecting the filter configuration.Empty State: When no keys are pressed,
getCurrentKeyPressed()returns an empty string (''). This is the expected behavior and indicates that no keys are currently being tracked.Rapid Key Presses: When multiple normal keys are pressed rapidly, the
normalKeyPressedstate will be overwritten by the latest key. This is the normal behavior for keyboard events, as only one normal key can be pressed at a time. If a previous key'skeyupevent arrives after it has been overwritten, the state will not be cleared for that key (since it no longer matches the current state). This is expected behavior and reflects the actual keyboard state.
Shortcut Match Rules
| Actions | Accelerator | Matched |
| :------------------------------------------------------------- | ----------------------- | ----------- |
| pressControlLeft press AltLeft release AltLeft press A | Control+a | ✅ |
| pressControlLeft press AltLeft press A | Control+a | ❌ |
| pressControlRight press A | Control+a | ✅ |
| pressControlRight press B release B press A | Control+a | ✅ |
| pressControlLeft press A | Control+a | ✅ |
| pressMetaLeft press A | Command+a or Meta+a | ✅ |
Browser Compatibility
- Chrome ≥ 48
- Firefox ≥ 38
- Safari ≥ 10.1
- Edge ≥ 79
React Integration
For React applications, use @rocketc/react-use-shortcuts which provides React hooks and context providers built on top of this library.
License
Distributed under the MIT License. See LICENSE for more information.
