Integrations

Your app's undo

Try it in the demo →

By default the library keeps its own undo history, so Ctrl+Z works with no setup. That suits an app that only edits text. A design app has other changes too, such as moving a shape or changing a background, and people expect one Undo for all of them. In "host" history mode the library hands each text change to your app as a step. Your app keeps it in the same history as its own changes, and restore puts a text step back when people undo or redo it.

The library's own history

This is the default ("library" mode). Nothing needs to be set up:

  • While a box is being edited, Ctrl+Z undoes, and Ctrl+Shift+Z or Ctrl+Y redoes (Cmd on a Mac).
  • From your own buttons, call rt.manager.undo() and rt.manager.redo() on Konva, or rt.undo() and rt.redo() on a plain canvas. Enable the buttons from canUndo and canRedo in the formatting report (see Your own toolbar).
  • The undo keys work only while a box is being edited. When no box is being edited, the keys are your app's: bind them to the same undo() and redo() if text is all your app undoes.

The history keeps up to 50 steps. Each formatting command is one step, even when it changes several boxes. Quick changes of the same kind to the same box, less than a second apart, join one step, so a burst of typing is undone at once. A resize and a Story Mode edit are steps too. Undo and redo never start or end editing of the box they change.

On Konva, stages that share a context share one history, for example an editor with one stage per page. The history belongs to the context, so canUndo and canRedo in the formatting report answer for all of them.

Keep text changes in your app's history

Create the editing session in "host" mode and receive each step in onHistoryStep:

// Konva: a context in "host" mode, given to the stage
import { createRichTextContext, enableRichText } from "@mycreativeshop/rich-text/konva";

const context = createRichTextContext({ history: "host", onHistoryStep: onTextStep });
const rt = enableRichText(stage, { context });

// Plain canvas: the same two options
const rt = createCanvasRichText({ onChange: render, history: "host", onHistoryStep: onTextStep });

onTextStep is your app's function that keeps the step. The sections below build it.

Note: On Konva, listen for formatting on this context (context.onFormattingChange), not on defaultRichTextContext: the stage no longer belongs to the default context.

In host mode the library keeps no steps. Its undo keys do nothing, rt.manager.undo() and rt.undo() do nothing, and canUndo and canRedo in the formatting report are always false. Your app binds the keys, and its own history decides whether its Undo and Redo buttons are enabled.

What a step holds

Each step is a HistoryStep:

Field What it holds
boxes One BoxChange for each box the step changed: the box's id, its state before the change (what undo puts back) and after it (what redo puts back). Bold applied to two selected boxes is one step with two changes.
kind "typing", "deletion", "formatting" or "structural". Structural steps include a new paragraph, a paste, a whole-document replacement such as setDocument, a resize, and closing Story Mode.
group A number. Consecutive steps with the same number are one step to the person using your app.

A box's state is a DocumentSnapshot: its document, size, position, scale, curve and cursor. Treat it as data to keep and hand back, not to change.

Undo and redo a step

Hand the step back with restore. "before" undoes it; "after" redoes it.

context.restore(step, "before"); // Konva
rt.restore(step, "before"); // plain canvas

Each box in the step goes back to that state, exactly as the library's own undo would put it. restore returns how many boxes it restored. A box that is no longer in the editor is skipped.

Restoring never starts or ends editing of the box it changes. If people are typing in that box, the cursor moves to where the change was and they can go on typing. If another box is being edited, that editing ends. Each restore is reported to onDocumentChange, as an undo is.

Note: Steps find their boxes by id. If your app's own undo puts back a box it had deleted, give the box its old id, so that the text steps recorded for it apply again.

Merge steps that share a group

The library sends a step for every keystroke. Steps with the same group are one burst of typing in one box: keep one entry for them, with the first step's before and the latest step's after.

const history = [];
let at = 0; // entries before this index are done; entries after it can be redone

function onTextStep(step) {
  history.length = at; // a new change drops what was undone
  const last = history[at - 1];
  if (last?.text && last.text.group === step.group) {
    for (const change of step.boxes) {
      const kept = last.text.boxes.find((b) => b.id === change.id);
      if (kept) kept.after = change.after; // keep the first "before", take the latest "after"
      else last.text.boxes.push({ ...change });
    }
  } else {
    history.push({ text: { ...step, boxes: step.boxes.map((b) => ({ ...b })) } });
  }
  at = history.length;
}

One history for your app's changes and the text's

Your app's own changes go into the same list as the text steps, each with its own undo and redo. Undo walks back through both, in the order they happened:

function add(entry) {
  history.length = at;
  history.push(entry);
  at = history.length;
}

// Your app's own change
function setBackground(color) {
  const before = background;
  paint(color);
  add({ undo: () => paint(before), redo: () => paint(color) });
}

function undo() {
  if (at === 0) return;
  const entry = history[--at];
  entry.text ? context.restore(entry.text, "before") : entry.undo(); // plain canvas: rt.restore
}

function redo() {
  if (at === history.length) return;
  const entry = history[at++];
  entry.text ? context.restore(entry.text, "after") : entry.redo();
}

// The undo keys are your app's in host mode, while someone types too
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 own undo
  const key = e.key.toLowerCase();
  if (key !== "z" && key !== "y") return;
  e.preventDefault();
  if (key === "y" || e.shiftKey) redo();
  else undo();
});

onTextStep from the previous section keeps the text steps in the same history list. To keep your buttons in step, set undoButton.disabled = at === 0 and redoButton.disabled = at === history.length after each change, undo and redo.

On Konva, dragging a box to move it is your app's change, as it is for any Konva shape: record it in your history yourself. A resize with the transformer is a text step. On a plain canvas with the built-in handles, moves and resizes are both text steps.

No history: previews and print

"none" records nothing. Use it for offscreen copies, previews and print rendering, so their changes never reach the history people undo through:

// Konva: a preview stage with its own context, which nothing can undo
enableRichText(previewStage, { context: createRichTextContext({ history: "none" }), interactive: false });

// Plain canvas
const preview = createCanvasRichText({ onChange: renderPreview, history: "none" });

onDocumentChange is still called in "none" mode.

In the API reference