import { AnyCommand } from '../commandbus'; /** * Service for binding keyboard shortcuts to execute commands * * A keybinding is a sequence or key combination on a computer keyboard which * invokes commands. This service supports key bindings as a way of responding * to individual keys typed by a user. The key combination can be pressing a single * key or a sequence of keys one after the other. * * The service works together with the {@link CommandBus} to execute the commands and * a command needs to already be registered on the command bus in order to be * bound as a keyboard shortcut. * * Keybindings are scoped to the component that creates them and should be * cleaned up when the component is removed from the DOM to prevent memory leaks * and unintended behavior. * * ## Key Combination Format * * Key combinations can be: * - Single keys: `'a'`, `'enter'`, `'escape'` * - Modifier combinations: `'ctrl+s'`, `'alt+shift+n'`, `'meta+k'` * - Sequences: `'g i'` (press 'g' then 'i') * * Supported modifiers: `ctrl`, `alt`, `shift`, `meta` (Command on Mac) * * @example Binding a command to a key combination * ```ts * import { * Command, * LimeObjectContext, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'save-deal' }) * class SaveDealCommand { * public context: LimeObjectContext; * } * * const http = platform.get(PlatformServiceName.Http); * const commandBus = platform.get(PlatformServiceName.CommandBus); * * // A command can only be bound once the command bus knows how to handle it * commandBus.register(SaveDealCommand, { * handle: (command: SaveDealCommand) => * http.post(`deal/${command.context.id}/save`), * }); * * const saveCommand = new SaveDealCommand(); * saveCommand.context = { limetype: 'deal', id: 42 }; * * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * registry.bind('ctrl+s', saveCommand); * ``` * * @example Checking for conflicts before binding * ```ts * import { Command, PlatformServiceName } from '@limetech/lime-web-components'; * * @Command({ id: 'navigate-next' }) * class NavigateNextCommand {} * * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * if (!registry.isBound('j')) { * registry.bind('j', new NavigateNextCommand()); * } * ``` * * @example Vim-style key sequences * ```ts * import { Command, PlatformServiceName } from '@limetech/lime-web-components'; * * @Command({ id: 'go-to-inbox' }) * class GoToInboxCommand {} * * @Command({ id: 'go-to-sent' }) * class GoToSentCommand {} * * @Command({ id: 'go-to-drafts' }) * class GoToDraftsCommand {} * * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * // 'g i' is a sequence, so press 'g' and then 'i' * registry.bind('g i', new GoToInboxCommand()); * registry.bind('g s', new GoToSentCommand()); * registry.bind('g d', new GoToDraftsCommand()); * ``` * * @example Usage in a component * ```ts * import { Prop } from '@stencil/core'; * import { * Command, * LimeWebComponentPlatform, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'save-deal' }) * class SaveDealCommand {} * * class MyComponent { * @Prop() * public platform: LimeWebComponentPlatform; * * private saveCommand = new SaveDealCommand(); * * public connectedCallback() { * this.keybindingRegistry.bind('ctrl+s', this.saveCommand); * } * * public disconnectedCallback() { * this.keybindingRegistry.unbind('ctrl+s', this.saveCommand); * } * * private get keybindingRegistry() { * return this.platform.get(PlatformServiceName.KeybindingRegistry); * } * } * ``` * * @note This service is work in progress * @beta * @group Keyboard shortcuts */ export interface KeybindingRegistry { /** * Bind a command to a specific key combination to add it to the registry * * When a user presses the specified key combination, the command will be * dispatched through the {@link CommandBus}. The command must already have * a handler registered on the command bus before binding. * * If the same key combination is bound to multiple commands, all commands * will be executed in the order they were registered. * * @param keys - the string representation of keys (e.g., 'ctrl+s', 'alt+shift+n', 'g i') * @param command - the command instance to trigger when keys are pressed * @throws Will throw an error if the command is invalid or not registered on the command bus * * @example Bind a command to ctrl+s * ```ts * import { Command, PlatformServiceName } from '@limetech/lime-web-components'; * * @Command({ id: 'save-deal' }) * class SaveDealCommand {} * * const command = new SaveDealCommand(); * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * registry.bind('ctrl+s', command); * ``` */ bind: (keys: string, command: AnyCommand) => void; /** * Unbind a keybinding to remove it from the registry * * Removes the association between a key combination and a command. After * unbinding, pressing the key combination will no longer trigger the command. * This is essential for cleanup when components are removed from the DOM. * * If multiple commands are bound to the same key combination, only the * specified command will be unbound. * * @param keys - the string representation of the key combination (e.g., 'ctrl+s') * @param command - the command instance connected to the key combination * * @example Remove a keybinding * ```ts * import { Command, PlatformServiceName } from '@limetech/lime-web-components'; * * @Command({ id: 'save-deal' }) * class SaveDealCommand {} * * const saveCommand = new SaveDealCommand(); * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * registry.bind('ctrl+s', saveCommand); * * // The same command instance has to be passed back to remove the binding * registry.unbind('ctrl+s', saveCommand); * ``` */ unbind: (keys: string, command: AnyCommand) => void; /** * Get a list of the key bindings that are currently in the registry * * Returns all active keybindings across all components. Useful for debugging, * displaying keyboard shortcuts to users, or detecting conflicts. * * @returns the list of all active keybindings with their commands * * @example List the active keybindings * ```ts * import { * createLogger, * getCommandId, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * const logger = createLogger('my-component'); * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * for (const binding of registry.getKeybindings()) { * logger.debug(`${binding.keys} runs ${getCommandId(binding.command)}`); * } * ``` */ getKeybindings: () => Keybinding[]; /** * Checks if a key combination or a specific keybinding is already bound * * Use this method to detect conflicts before binding new keybindings. * When called with just a key combination, returns true if any command * is bound to those keys. When called with both keys and a command, * returns true only if that specific combination exists. * * @param keys - the string representation of a key combination (e.g., 'ctrl+s') * @param command - optional command instance to check for a specific binding * @returns true if the key combination is bound (to any or the specified command), false otherwise * * @example Check if any command is bound to keys * ```ts * import { * createLogger, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * const logger = createLogger('my-component'); * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * if (registry.isBound('ctrl+s')) { * logger.warn('ctrl+s is already bound'); * } * ``` * * @example Check if a specific command is bound to keys * ```ts * import { * Command, * createLogger, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'save-deal' }) * class SaveDealCommand {} * * const logger = createLogger('my-component'); * const saveCommand = new SaveDealCommand(); * const registry = platform.get(PlatformServiceName.KeybindingRegistry); * * if (registry.isBound('ctrl+s', saveCommand)) { * logger.debug('The save command is bound to ctrl+s'); * } * ``` */ isBound: (keys: string, command?: AnyCommand) => boolean; } /** * A key combination and its corresponding command that will be triggered when * the key combination is pressed * * Represents an active binding in the {@link KeybindingRegistry}. Each keybinding * associates a specific key combination with a command instance that will be * executed via the {@link CommandBus} when the keys are pressed. * * @example Describe a keybinding * ```ts * import { Command, Keybinding } from '@limetech/lime-web-components'; * * @Command({ id: 'save-deal' }) * class SaveDealCommand {} * * const binding: Keybinding = { * keys: 'ctrl+s', * command: new SaveDealCommand(), * }; * ``` * * @beta * @group Keyboard shortcuts */ export interface Keybinding { /** * The string representation of a key combination * * Examples: `'ctrl+s'`, `'alt+shift+n'`, `'g i'`, `'escape'` */ keys: string; /** * The command instance to trigger when the key combination is pressed * * This command must be registered on the {@link CommandBus} before the * keybinding is created. */ command: AnyCommand; }