Keyboard shortcuts

Bind keys to intents or commands from your application.

Bind application shortcuts to an Intent, a CommandId, or a DOM runtime action. The runtime already owns native editing input: typing, delete, paragraph breaks, paste, cut, copy, IME composition, selection sync, pointer placement, and drag selection. Host shortcuts should cover product actions such as opening a link dialog, toggling a sidebar, applying a saved style, or invoking a custom command.

Examples assume an engine and mounted runtime from Quickstart.

const host = runtime.rootElement;

Rules of thumb

  • Let the DOM runtime handle native editing keys while host code owns product shortcuts.
  • Call event.preventDefault() only after you have matched the shortcut.
  • Dispatch a semantic intent or command from the shortcut.
  • Prefer a command when the same behavior is used by keyboard, toolbar, menu, or plugin code.
  • Prefer runtime.invokeAction(...) when the action already exists in the runtime catalog.

Bind a key to an intent

host.addEventListener("keydown", (event) => {
  const isMod = event.metaKey || event.ctrlKey;
  if (!isMod || event.shiftKey || event.altKey) return;
  if (event.key.toLowerCase() !== "k") return;

  event.preventDefault();
  engine.dispatch({
    kind: "setLink",
    href: "https://spineeditor.com",
  });
});

This path is useful when the host already has the exact semantic edit. The intent still goes through planning, transaction apply, normalization, commit, history, render, and selection projection.

Bind a key to a custom command

Register a CommandId once, invoke it from any UI surface.

import {
  createCommandRegistry,
  createEditingCommands,
  createHistoryCommands,
  extendCommandRegistry,
  type CommandId,
  type NodeTypeName,
} from "@spine-editor/core";

const SetTitleHeading = "setTitleHeading" as CommandId;

const commandRegistry = extendCommandRegistry(
  createCommandRegistry()
    .registerMany(createHistoryCommands())
    .registerMany(createEditingCommands()),
  [
    {
      command: {
        id: SetTitleHeading,
        run() {
          return {
            kind: "setSelectedBlockType",
            toType: "heading" as NodeTypeName,
            attrs: { level: 1 },
          };
        },
      },
    },
  ],
);

// Pass commandRegistry to createEditorEngine, then:

host.addEventListener("keydown", (event) => {
  const isMod = event.metaKey || event.ctrlKey;
  if (!isMod || !event.altKey) return;
  if (event.key !== "1") return;

  event.preventDefault();
  engine.dispatchCommand(SetTitleHeading);
});

run() returns an Intent or null. The engine dispatches the returned intent.

Invoke a DOM runtime action

For toolbar-equivalent shortcuts, use the action catalog instead of duplicating enabled and active state:

host.addEventListener("keydown", (event) => {
  const isMod = event.metaKey || event.ctrlKey;
  if (!isMod || event.shiftKey || event.altKey) return;
  if (event.key.toLowerCase() !== "b") return;

  event.preventDefault();
  runtime.invokeAction("inline.bold");
});

Action ids are exposed by runtime.getActions(). Host UI can render from that metadata so readonly and selection-aware action state stays in one place.

Map several shortcuts to runtime actions

const shortcutActions = new Map([
  ["mod+b", "inline.bold"],
  ["mod+i", "inline.italic"],
  ["mod+shift+7", "block.list.ordered"],
  ["mod+shift+8", "block.list.bullet"],
  ["mod+alt+2", "block.heading.2"],
]);

function shortcutId(event: KeyboardEvent): string | null {
  const isMod = event.metaKey || event.ctrlKey;
  if (!isMod) return null;

  return [
    "mod",
    event.altKey ? "alt" : null,
    event.shiftKey ? "shift" : null,
    event.key.toLowerCase(),
  ]
    .filter(Boolean)
    .join("+");
}

host.addEventListener("keydown", (event) => {
  const actionId = shortcutActions.get(shortcutId(event) ?? "");
  if (actionId === undefined) return;

  const action = runtime.getActions().find((item) => item.id === actionId);
  if (action?.enabled !== true) return;

  event.preventDefault();
  runtime.invokeAction(actionId);
});

Built-in command ids

import { CoreCommandIds } from "@spine-editor/core";

engine.dispatchCommand(CoreCommandIds.undo);
engine.dispatchCommand(CoreCommandIds.redo);
engine.dispatchCommand(CoreCommandIds.selectAll);

The built-in command set covers deletion, paragraph breaks, paste text, marks, links, block type, block alignment, list commands, media row commands, select all, undo, and redo. Commands produce intents or invoke engine-owned history operations; the engine applies the resulting transaction.

Keep native editing keys with the runtime

Plain Backspace, Delete, Enter, printable keys, Tab, selection arrows, and paste/copy/cut shortcuts belong to the runtime substrate. Product shortcuts should sit above that layer and call commands or runtime actions after a precise match.