Integrations

Your shortcuts and editing events

Try it in the demo →

An editor app has its own keyboard shortcuts: Delete removes the selected object, the arrow keys move it. While someone types in a text box, those keys belong to the text, so Delete removes a letter instead of the box. This page shows how your shortcuts step aside during editing, how your app hears when editing starts and stops, and how it removes empty boxes and saves as people work. It ends with every event the library offers.

Let the keys go to the text while someone types

Check whether text is being edited at the top of your key handler. On Konva, isEditingText() answers for the whole page: any stage, any context. On a plain canvas, rt.editing() returns the box being edited, or null.

// Konva
import { isEditingText } from "@mycreativeshop/rich-text/konva";

window.addEventListener("keydown", (e) => {
  if (isEditingText()) return; // the keys are the text's
  if (e.key === "Delete" || e.key === "Backspace") removeSelected();
  if (e.key.startsWith("Arrow")) nudgeSelected(e.key, e.shiftKey ? 16 : 4);
});

// Plain canvas: if (rt.editing()) return;

While a box is being edited, the library listens for keys on the window. It uses the typing keys, Backspace, Delete, Enter, Tab, the arrow keys, Home and End, Esc, and Ctrl (Cmd on a Mac) with A, C, X, V, Z and Y. While the keyboard focus is in an input, a textarea, a select or an element with contenteditable, the library leaves every key alone.

Note: Esc ends editing and leaves the box selected. The library calls preventDefault() on the Esc press, so if your app also uses Esc (to deselect, for example), check e.defaultPrevented to let that press only end editing.

Bind Bold, Italic and Underline

The library does not bind Ctrl+B, Ctrl+I or Ctrl+U (Cmd on a Mac). While a box is being edited, a letter key pressed with Ctrl or Cmd is not typed, and your app's handler receives it. Bind the keys to the commands, and call preventDefault() so the browser's own action for the key does not run:

window.addEventListener("keydown", (e) => {
  if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
  if (/^(INPUT|TEXTAREA|SELECT)$/.test((e.target as HTMLElement).tagName)) return; // a form field keeps its keys
  const command = { b: "toggleBold", i: "toggleItalic", u: "toggleUnderline" }[e.key.toLowerCase()];
  if (!command || !(rt.manager.editing() || rt.manager.selected())) return; // plain canvas: rt.editing() || rt.selected()
  e.preventDefault();
  rt.manager[command](); // plain canvas: rt[command]()
});

With a box selected and not being edited, the command formats all of its text, as a toolbar button does.

End editing from your app

stopEditingText() (Konva) ends editing wherever it is on the page and returns true if a box was being edited. rt.manager.stopEditing() ends it on that stage, and rt.stopEditing() on a plain canvas. The box stays selected. Use it when people pick another tool or open a dialog.

import { stopEditingText } from "@mycreativeshop/rich-text/konva";

shapeTool.onclick = () => {
  stopEditingText();
  startShapeTool();
};

Hear when editing starts and stops

Pass onEnterEditMode and onExitEditMode to show a text toolbar while someone types, or to hide other tools.

// Konva
const rt = enableRichText(stage, {
  onEnterEditMode: (box) => showTextToolbar(box),
  onExitEditMode: () => hideTextToolbar(),
});

// Plain canvas
const rt = createCanvasRichText({
  onChange: render,
  onEnterEditMode: (box) => showTextToolbar(box),
  onExitEditMode: () => hideTextToolbar(),
});

Editing has already ended everywhere when onExitEditMode is called: isEditingText() is false inside it.

Remove boxes left empty

onEmptyBlur is called when editing ends on a box with no words in it (only spaces or line breaks), just before onExitEditMode. It is also called when Story Mode closes and leaves a box empty. Remove the box there:

// Konva: a destroyed box is released by the library
const rt = enableRichText(stage, { onEmptyBlur: (box) => box.destroy() });

// Plain canvas
const rt = createCanvasRichText({ onChange: render, onEmptyBlur: (box) => rt.removeBox(box) });

box.getLastNonEmptyDocument() returns the box's last document that had words in it, or null. Keep it in your own undo history if people should be able to bring the box back.

Save as people work

onDocumentChange is called after every change people make to a box's text or formatting: typing, a formatting command, a resize, a Story Mode edit. It is also called after every undo and redo, and after every restore in host history. Use it to save as people work, or to show that there are unsaved changes.

It is called once per change, which means once for each keystroke, so wait for a pause before saving:

let timer: ReturnType<typeof setTimeout>;
const rt = enableRichText(stage, {
  onDocumentChange: (box) => {
    markUnsaved();
    clearTimeout(timer);
    timer = setTimeout(save, 1000);
  },
});
// Plain canvas: createCanvasRichText({ onChange: render, onDocumentChange: (box) => ... })
  • Commands your own code calls, such as rt.manager.setText(...), are changes too and are reported.
  • Changes your code makes directly on a box, such as box.setDocument(...) or replaceVariables, are not reported: your code already knows about them.
  • On Konva, each stage's onDocumentChange hears only its own boxes. To hear every box of an editing session with several stages, listen on the context: rt.manager.context.onDocumentChange((box) => ...). It returns a function that stops listening.

Take over the undo keys

In the default history, the library undoes on Ctrl+Z and redoes on Ctrl+Shift+Z and Ctrl+Y (Cmd on a Mac) while a box is being edited. On Konva, pass disableInternalUndoRedoShortcuts: true to enableRichText to bind those keys yourself; your handler then calls rt.manager.undo() and rt.manager.redo().

You don't need it in "host" history mode, where the library does nothing with the undo keys and your app's own undo handles them. A plain canvas has no such option: use host history to own the undo keys there. See Your app's undo.

Every event

Pass these callbacks to enableRichText(stage, { ... }) on Konva, or to createCanvasRichText({ ... }) on a plain canvas, unless the table says otherwise. Most apps use the first table; the second is for particular needs.

What most apps use

Callback When it is called Typical use
onFormattingChange The selection's formatting changes Keep your toolbar in step. On the context on Konva (defaultRichTextContext.onFormattingChange(...)), on rt on a plain canvas. See Your own toolbar
onDocumentChange After every change people make, and every undo, redo and restore Save as people work (wait for a pause), or show an "unsaved changes" mark
onEnterEditMode(box), onExitEditMode(box) Editing starts and stops Show a text toolbar while someone types
onEmptyBlur(box) Editing ends on a box with no words in it Remove the empty box
onChange (plain canvas only) The canvas needs drawing again Required on a plain canvas: schedule your render

For particular needs

Callback When it is called Typical use
onHistoryStep(step) Each undo step, in "host" history mode. On Konva, pass it to createRichTextContext One undo history for your whole app. See Your app's undo
onStoryModeEnter(box), onStoryModeExit(box, wasSaved) Story Mode opens and closes Hide your own UI while it is open. See Story Mode
fonts.onFontError (in configureRichText) A font can't load Report it. See Fonts
onSelectionChange(boxes) (Konva) The selected text boxes change UI that depends on the selection
onClipboardAccessDenied (Konva) The browser refused to let the page read the clipboard Suggest Ctrl+V
onBeforeMouseDown(e, box) (Konva) Before a press on a box reaches the library Return true to let another tool take the press

Konva node events

On Konva each box also fires events you can listen to with box.on(...): richtext:enterEditMode, richtext:exitEditMode, richtext:emptyBlur, richtext:transformStart and richtext:transformEnd (a resize with the transformer starts and ends).

In the API reference