@hotora/inputs
v1.0.3
Published
Lightweight JavaScript and TypeScript library for handling keyboard shortcuts, gestures and complex sequences with customizable scoped actions.
Downloads
85
Maintainers
Readme
Inputs
Lightweight JavaScript and TypeScript library for handling keyboard shortcuts, gestures and complex sequences with customizable scoped actions.
Features
- Step combinations and sequences
- Scoped handlers (local + global)
- Element binding
- Visibility-aware
- Smart active element resolution
- Scope propagation control (
stopPropagation) - Pluggable event providers (DOM, SSR-safe, custom)
Installation
npm install @hotora/inputsBasic Usage
Default (browser)
import { createInputsManager, Keys } from "@hotora/inputs";
const inputsManager = createInputsManager();
inputsManager.register([Keys.A], {
handler: () => {
console.log("Pressed A");
},
});For correct operation, it is best to use 1 InputsManager instance at a time. Otherwise stopPropagation may not work.
Step Combinations
Register multi-step combinations:
inputsManager.register([Keys.ControlLeft, Keys.A], {
handler: () => {
console.log("Ctrl + A");
},
});The handler fires when all steps are fired simultaneously.
Step Sequences
Register ordered sequences:
inputsManager.register([[Keys.ControlLeft], [Keys.A]], {
handler: () => {
console.log("Ctrl → A");
},
});The handler fires only when steps are fired in order.
Scoped Sequences
Bind sequence to an element and scope:
const element = document.getElementById("editor");
inputsManager.register(
[Keys.S],
{
handler: () => console.log("Save inside editor"),
},
element,
"editor",
);How scopes work
- Scope is attached to an element
- On step fire:
- active element is resolved
- scope chain is built (element → parents →
$global) - handlers execute in order
Global Scope
All handlers fallback to $global:
inputsManager.register([Keys.Escape], {
handler: () => console.log("Global escape"),
});stopPropagation
Prevent execution in parent scopes:
inputsManager.register([Keys.S], {
handler: (e) => {
e.stopPropagation();
console.log("Handled locally only");
},
});Active Element Resolution
InputsManager determines active element using:
- Last pointer interaction (click/touch)
- Only visible elements
- If multiple:
- prefers last active
- otherwise deepest
Providers
InputsManager is provider-based.
Event handling is abstracted via EventProvider.
DOMKeyboardEventProvider (used by default)
Uses real DOM APIs:
keydown/keyupmousedown/touchstartIntersectionObserver
import { createInputsManager, DOMKeyboardEventProvider } from "@hotora/inputs";
const inputsManager = createInputsManager(new DOMKeyboardEventProvider());LazyEventProvider (SSR / async)
Safe for SSR and dynamic environments.
- Buffers subscriptions
- Initializes provider lazily
- Supports async factory
import {
LazyEventProvider,
DOMKeyboardEventProvider,
createInputsManager,
} from "@hotora/inputs";
const provider = new LazyEventProvider(() => {
if (typeof window !== "undefined") {
return new DOMKeyboardEventProvider();
}
return null;
});
const inputsManager = createInputsManager(provider);Custom providers
You can implement your own provider (e.g. canvas, WebGL, game engine):
import type { EventProvider } from "";
class MyProvider implements EventProvider<any, any> {
// implement interface
}EventProvider Interface
Custom providers implement EventProvider<TElement, TStep> to handle events in different environments:
interface EventProvider<TElement, TStep> {
onInputStart(handler: InputHandler<TStep>, signal: AbortSignal): void;
onInputEnd(handler: InputHandler<TStep>, signal: AbortSignal): void;
onPointer(handler: PointerHandler<TElement>, signal: AbortSignal): void;
observe(
element: TElement,
callback: (entries: Entry<TElement>[]) => void,
signal: AbortSignal,
): void;
unobserve(element: TElement): void;
getParentElement(element: TElement): TElement | null;
getIsElementConnected(element: TElement): boolean;
}- Allows InputsManager to work with browser DOM, SSR, or test environments
- Supports lazy initialization for SSR or deferred providers
- Handlers are automatically queued until the provider is ready
API
InputsManager
createInputsManager(provider?)
Creates a new instance with a specific EventProvider.
const inputsManager = createInputsManager(provider);register(sequence, setup, element?, scope?)
Registers a steps or stepsSequence.
register(
Steps | StepsSequence,
{
handler: (event) => void;
clearDuration?: number;
},
element?: TElement,
scope?: string
): ActionIdunregister(id)
Removes a previously registered handlers.
destroy()
Cleans up all subscriptions.
LazyEventProvider
new LazyEventProvider(providerFactory)
Creates new instance with a provider factory
const factory = () => {
import("./CustomEventProvider").then((m) => new m.CustomEventProvider());
};
const provider = new LazyEventProvider(factory);isReady: boolean
Indicates whether the provider has already been initialized or not.
reinitialize()
Restarts the provider initialization.
Event Object
Handler receives:
{
stopPropagation: () => void;
sequence: Sequence;
activeSteps: Set;
timestamp: number;
}SSR Notes
DOMKeyboardEventProvideris not SSR-safe- Use
LazyEventProviderfor:- SSR frameworks (Next.js, Nuxt)
- dynamic imports
- delayed initialization
Example
import { createInputsManager } from "@hotora/inputs";
const modal = document.getElementById("modal");
const inputsManager = createInputsManager();
inputsManager.register(
[Keys.Escape],
{
handler: () => console.log("Close modal"),
},
modal,
"modal",
);
inputsManager.register([Keys.Escape], {
handler: () => console.log("Global escape fallback"),
});License
MIT
