UNPKG

rx-hotkeys

Version:

Advanced Keyboard Shortcut Management library using rxjs

346 lines 15.4 kB
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