rx-hotkeys
Version:
Advanced Keyboard Shortcut Management library using rxjs
346 lines • 15.4 kB
TypeScript
import { Observable, Subject } from "rxjs";
import { type StandardKey } from "./keys.js";
export declare enum ShortcutTypes {
Combination = "combination",
Sequence = "sequence"
}
interface ShortcutConfigBase {
id: string;
context?: string | null;
preventDefault?: boolean;
description?: string;
/**
* **Only applicable if the shortcut has no top-level `context` defined.**
* If `true`, this shortcut is **strictly global** and will only fire when the active
* hotkey context is `null`.
* If `false` or `undefined` (the default), the shortcut can fire in *any* context,
* but will be suppressed by an identical shortcut that belongs to the active context.
* @default false
*/
strict?: boolean;
/**
* The DOM element to which the event listener for this shortcut will be attached.
* If not provided, the listener will be attached to the `document`.
* Useful for creating shortcuts that are only active within a specific component or area.
* @default document
*/
target?: HTMLElement;
/**
* The type of keyboard event to listen for.
* Use "keydown" for actions that should happen immediately upon pressing a key.
* Use "keyup" for actions that should happen upon releasing a key.
* @default "keydown"
*/
event?: "keydown" | "keyup";
/**
* Optional: Advanced options to pass directly to the underlying `addEventListener` call.
* Use this to control behaviors like `capture`, `passive`, or `once`.
* See: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
* @default undefined
*/
options?: AddEventListenerOptions;
}
/**
* Defines a single key trigger, which can be a StandardKey (for simple presses like "Escape")
* or an object specifying the main key and its modifiers (e.g., { key: Keys.S, ctrlKey: true }).
*/
export type KeyCombinationTrigger = {
/**
* The main key for the combination.
* This MUST be a value from the exported `Keys` object
* (e.g., `Keys.A`, `Keys.Enter`, `Keys.Escape`).
* The library handles case-insensitivity for single character keys (like A-Z, 0-9)
* automatically when comparing with the actual browser event's `event.key`.
* For special, multi-character keys (e.g. "ArrowUp", "Escape"), the value from
* `Keys` ensures the correct case-sensitive string is used.
* Refer to: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
*/
key: StandardKey;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
metaKey?: boolean;
} | StandardKey | string;
/**
* A fully parsed, canonical representation of a single key trigger.
* All modifier keys are explicitly defined as booleans.
*/
interface ParsedTrigger {
key: StandardKey;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
}
export interface KeyCombinationConfig extends ShortcutConfigBase {
/**
* Defines the key or key combination(s) that trigger the shortcut.
* Can be a single trigger, an array of triggers, or a string representation.
*
* **Object/Array:** Each trigger can be an object specifying the main `key` (from `StandardKey`) and optional
* modifiers (`ctrlKey`, `altKey`, `shiftKey`, `metaKey`).
* Example: `{ key: Keys.S, ctrlKey: true }` for Ctrl+S.
*
* **Shorthand:** For a simple key press without modifiers, a trigger can be a `StandardKey` directly.
* Example: `Keys.Escape` for the Escape key.
*
* **String:** A human-readable string like `"ctrl+s"` or `"shift+alt+k"`. Modifiers are joined by `+`.
* Example: `"meta+k"`, `"ctrl+shift+?"`
*
* To define multiple triggers for the same action:
* Example: `keys: [Keys.Enter, { key: Keys.Space, ctrlKey: true }]`
*/
keys: KeyCombinationTrigger | KeyCombinationTrigger[];
}
export interface KeySequenceConfig extends ShortcutConfigBase {
/**
* An array of keys or a string defining the sequence.
*
* **Array:** Each key in the sequence MUST be a value from `Keys`.
* Example: `[Keys.G, Keys.I]`
*
* **String:** A string where keys are separated by `->`.
* Example: `"g -> i"`, `"up -> up -> down -> down"`
*/
sequence: StandardKey[] | string;
/**
* Optional: Timeout in milliseconds between consecutive key presses in the sequence.
* If the time between two keys in the sequence exceeds this value, the sequence attempt is reset.
* Set to 0 or undefined to disable inter-key timeout behavior (uses simpler buffer-based matching).
*/
sequenceTimeoutMs?: number;
}
type ShortcutConfig = KeyCombinationConfig | KeySequenceConfig;
export interface ActiveShortcut {
id: string;
config: ShortcutConfig;
terminator$: Subject<void>;
parsedTriggers?: ParsedTrigger[];
}
/**
* Manages keyboard shortcuts for web applications.
* Allows registration of single key combinations (e.g., Ctrl+S) and key sequences (e.g., g -> i).
* Supports contexts to enable/disable shortcuts based on application state.
*/
export declare class Hotkeys {
private static readonly KEYDOWN_EVENT;
private static readonly KEYUP_EVENT;
private static readonly LOG_PREFIX;
private static readonly NO_OVERRIDE;
private eventStreams;
private activeShortcuts;
private debugMode;
private contextStack$;
private overrideContext$;
/**
* An Observable that emits the new active context name (or null) whenever it changes.
* The active context is the override context if one is set, otherwise it's the context
* from the top of the stack.
*/
private readonly activeContext$;
/**
* Creates an instance of Hotkeys.
* @param initialContext - Optional initial context name. This forms the base of the context stack.
* @param debugMode - Optional. If true, debug messages will be logged to the console. Defaults to false.
* @throws Error if not in a browser environment (i.e., `document` or `performance` is undefined).
*/
constructor(initialContext?: string | null, debugMode?: boolean);
/**
* Helper method to determine the active context based on override and stack.
*/
private _resolveActiveContext;
private _normalizeAndParseTriggers;
private _normalizeSequence;
/**
* [PRIVATE] Generates a unique key for the stream cache based on event type and options.
*/
private _getStreamCacheKey;
/**
* Gets or creates a shared event stream for a given event type, target, and options.
* @param eventType The type of event ("keydown" or "keyup").
* @param target The DOM element to attach the listener to.
* @param options The AddEventListenerOptions.
* @returns A shared Observable for the specified event.
*/
private _getEventStream;
/**
* Sets a temporary, high-priority override context that takes precedence over the context stack.
* @param contextName The override context to activate (can be a string or `null`).
* @returns A `restore` function that, when called, clears the override context, reverting to the stack.
*/
setContext(contextName: string | null): () => void;
/**
* @deprecated Rename to `getActiveContext`
* Gets the current active context, considering any override.
* @returns The current context name as a string, or `null` if no context is set.
*/
getContext(): string | null;
/**
* Gets the current active context, considering any override.
* @returns The current context name as a string, or `null` if no context is set.
*/
getActiveContext(): string | null;
/**
* Pushes a new context onto the context stack. It will become active if no override context is set.
* @param contextName The name of the context to enter (e.g., "modal", "editor").
*/
enterContext(contextName: string | null): void;
/**
* Pops the current context from the stack.
* @returns The context that was just left from the stack, or `undefined` if at the base.
*/
leaveContext(): string | null | undefined;
/**
* Enables or disables debug logging for the Hotkeys instance.
* When enabled, various internal actions and shortcut triggers will be logged to the console.
* @param enable - True to enable debug logs, false to disable.
*/
setDebugMode(enable: boolean): void;
/**
* Checks if a shortcut with the given ID is currently registered and active.
* @param id - The unique ID of the shortcut to check.
* @returns True if a shortcut with the specified ID exists, false otherwise.
*/
hasShortcut(id: string): boolean;
/**
* An Observable that emits the new context name (or null) whenever the active context changes.
*
* @example
* ```typescript
* const hotkeys = new Hotkeys();
* const subscription = hotkeys.onContextChange$.subscribe(newContext => {
* console.log("Hotkey context changed to:", newContext);
* // Update UI or perform other actions
* });
* // To unsubscribe when no longer needed:
* // subscription.unsubscribe();
* ```
*/
get onContextChange$(): Observable<string | null>;
/**
* Compares two sequences of StandardKey arrays to see if they are identical.
* @param seq1 - The first sequence array.
* @param seq2 - The second sequence array.
* @returns True if the sequences are identical, false otherwise.
*/
private _areSequencesIdentical;
/**
* Checks if a given KeyCombinationConfig matches a given KeyboardEvent.
* This is used internally for priority checking.
* @param shortcutConfig The KeyCombinationConfig to check.
* @param event The KeyboardEvent to match against.
* @returns True if the shortcutConfig matches the event, false otherwise.
*/
private _shortcutMatchesEvent;
private filterByContext;
private _registerShortcut;
/**
* Parses a single key trigger definition (either shorthand StandardKey or an object with modifiers)
* into its constituent parts: main key and modifier states.
* @param keyInput - The KeyCombinationTrigger to parse.
* @param shortcutId - The ID of the shortcut this key trigger belongs to (for logging).
* @returns An object containing configuredMainKey and modifier states, or null if parsing fails.
*/
private _parseKeyTrigger;
private _parseCombinationString;
private _parseSequenceString;
/**
* Registers a key combination shortcut (e.g., Ctrl+S, Shift+Enter, or a single key like Escape)
* and returns an Observable that emits the `KeyboardEvent` when the combination is triggered.
* @param config - Configuration object for the key combination.
* See {@link KeyCombinationConfig} for details.
* @returns An `Observable<KeyboardEvent>` that you can subscribe to. The stream will be automatically
* completed if the shortcut is removed via `remove(id)` or `destroy()`, or if it's overwritten.
* If the configuration is invalid, an empty Observable is returned and a warning is logged.
* @example
* ```typescript
* import { Keys } from "./keys";
* // For Ctrl+S
* const save$ = keyManager.addCombination({
* id: "saveFile",
* keys: { key: Keys.S, ctrlKey: true },
* context: "editor"
* });
* save$.subscribe(event => console.log("File saved!", event));
*
* // For Ctrl+S using a string
* const save$ = keyManager.addCombination({ id: "saveFile", keys: "ctrl+s" });
* save$.subscribe(event => console.log("File saved!", event));
*
* // For just the Escape key, or Ctrl+Space
* const close$ = keyManager.addCombination({
* id: "closeModal",
* keys: [Keys.Escape, {key: Keys.Space, ctrlKey: true}],
* });
* close$.subscribe(() => console.log("Modal closed!"));
*
* // For the Escape key on a specific element
* const myModal = document.getElementById("my-modal");
* const close$ = keyManager.addCombination({ id: "closeModal", keys: Keys.Escape, target: myModal });
* close$.subscribe(() => console.log("Modal closed!"));
* ```
*/
addCombination(config: KeyCombinationConfig): Observable<KeyboardEvent>;
/**
* Registers a key sequence shortcut (e.g., g -> i, or ArrowUp -> ArrowUp -> ArrowDown)
* and returns an Observable that emits the final `KeyboardEvent` of the sequence when it's completed.
* An optional timeout can be specified for the time allowed between key presses in the sequence.
* @param config - Configuration object for the key sequence.
* See {@link KeySequenceConfig} for details.
* Each key in the `sequence` array must be a value from the `Keys` object.
* Or using string for `sequence`.
* @returns An `Observable<KeyboardEvent>` that you can subscribe to. The stream will be automatically
* completed if the shortcut is removed via `remove(id)` or `destroy()`, or if it's overwritten.
* If the configuration is invalid, an empty Observable is returned and a warning is logged.
* @example
* ```typescript
* import { Keys } from "./keys";
* const konami$ = keyManager.addSequence({
* id: "konamiCode",
* sequence: [Keys.ArrowUp, Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowDown, Keys.A, Keys.B],
* sequenceTimeoutMs: 2000 // 2 seconds between keys
* });
* konami$.subscribe(event => console.log("Konami!", event));
* ```
* ```typescript
* // Using a string for the sequence
* const konami$ = keyManager.addSequence({
* id: "konamiCode",
* sequence: "up -> up -> down -> down -> a -> b",
* sequenceTimeoutMs: 2000
* });
* konami$.subscribe(event => console.log("Konami!", event));
* ```
*/
addSequence(config: KeySequenceConfig): Observable<KeyboardEvent>;
/**
* Removes a registered shortcut by its ID.
* This will complete the corresponding Observable stream for any subscribers.
* @param id - The unique ID of the shortcut to remove.
* @returns True if the shortcut was found and removed, false otherwise.
* A warning is logged to the console if no shortcut with the given ID is found.
*/
remove(id: string): boolean;
/**
* Retrieves a list of all currently active (registered) shortcut configurations.
* This can be useful for displaying available shortcuts to the user or for debugging.
* @returns An array of objects, where each object represents an active shortcut
* and includes its `id`, `description` (if provided), `context` (if any),
* and `type` (from `ShortcutTypes` enum).
*/
getActiveShortcuts(): {
id: string;
description?: string;
context?: string | null;
type: ShortcutTypes;
}[];
/**
* Cleans up all active subscriptions and resources used by the Hotkeys instance.
* This method should be called when the Hotkeys instance is no longer needed
* (e.g., when a component unmounts or the application is shutting down) to prevent memory leaks.
* After calling `destroy()`, the instance should not be used further.
*/
destroy(): void;
}
export {};
//# sourceMappingURL=hotkeys.d.ts.map