Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Undo and redo

How Edytor groups edits into undo steps, the undo and redo APIs, and how undo behaves when several people edit the same document.

Every document has one local history. Undo reverts your own edits only, never a collaborator’s, and restores the selection you had. This page covers the shortcuts and APIs, what one undo step contains, and the rules that apply when others edit at the same time.

Undo and redo

Action Keys
Undo Mod+Z
Redo Mod+Shift+Z, or Ctrl+Y on Windows and Linux

Mod is Cmd on macOS and Ctrl elsewhere. Undo and redo from the browser’s Edit menu or a context menu go through the same history.

From code, call the view’s methods:

<button onclick={() => edytor?.historyUndo()}>Undo</button>
<button onclick={() => edytor?.historyRedo()}>Redo</button>

historyUndo() restores the selection this view had before the step; historyRedo() restores the one it had after. If a collaborator deleted the place a recorded caret pointed to, the view keeps its current selection instead.

Without a view, use the document’s history directly. It restores no selection:

document.history.undo();
document.history.redo();
document.history.canUndo(); // Yjs UndoManager API
document.clearHistory(); // drop both stacks

All views of one document share its history. An edit made in one view can be undone from another; only the view that issues the undo restores its selection.

What one step contains

Edytor groups edits the way a text editor does:

  • Typing at the caret joins the previous step if it continues where that step left the caret, and follows it within the capture window (500 ms by default). Moving the caret and typing again starts a new step, however short the pause.
  • Deletions (Backspace, Delete, cut, deleting a selection or blocks) start a new step.
  • Enter is a step of its own.
  • Paste and drop each start a new step.
  • Structural edits (inserting, duplicating, converting, removing, nesting, unnesting and moving blocks, formatting) start a new step, whether they come from a key, a menu, a toolbar or a plugin calling a block command.
  • Markdown and slash conversions are a step of their own. Undo gives back what you typed as text: ## becomes a paragraph ##, **b** becomes **b*, and /h2 + Enter becomes the /h2 text.
  • An IME composition (Japanese, Chinese, Korean input, and so on) is one step with its commit.
  • Copy records nothing.

A command that plugins refuse records nothing. One edytor.transact call is never split across steps.

These rules do not depend on how fast you work: a menu action a few milliseconds after typing is still its own step, and so is the block handle’s +, even when it only types / into an empty block. Commands you call yourself are one step each: block commands (block.insertBlockAfter, block.setBlock, block.addChildBlock, block.setInlineData, deletions and the rest) and text commands such as text.markText. Only text.insertText and block.addInlineBlock can join the previous step, and only when they continue at the caret that step left, as typing does. To make several commands one step, run them in one edytor.transact call: that is the only guarantee. edytor.dispatcher.run(kind, body) makes what body runs synchronously one step, as long as its commands commit within the capture window. It starts no step between them, but each command still commits on its own, so two steps result when captureTimeout is 0 or when body takes longer than the window, and commands after an await in an async body are grouped as if you had called them on their own. Do the async work before calling run, or wrap the commands in edytor.transact. kind names the edit (an input type such as insertText, or format, insertBlock, insertFromPaste), and the rules above apply by it: a kind they do not cover (run('myConvert', …)) starts a new step, as a structural edit does, so it never joins the typing before it.

Set the capture window when you create the document. 0 makes every commit its own step:

import { createDocument } from 'edytor';

const document = createDocument({ value, history: { captureTimeout: 300 } });

History stacks are not trimmed automatically. For long sessions, call document.clearHistory() at points where undo no longer makes sense, such as after a save.

Undo with collaborators

Remote edits never enter your history, and your undo only takes back what you did. When edits overlap, these rules apply:

  • Your undo removes only your own contributions. If you create a block (or press Enter) and a collaborator types into it, undoing your creation leaves the block with their text, even if their text arrives after your undo. A block stays while it holds another person’s text or a child.
  • Undoing a split puts the text of the second half back into the first line, including anything a collaborator typed there.
  • Concurrent deletes. Each person’s delete of a piece of text is their own. If two people delete the same text at the same time and one of them undoes, the text stays deleted until the other undoes too; then it comes back once, not twice.
  • Explicit deletes win. When you delete a block while someone types into it, the block stays deleted.
  • Deleting a selected parent moves its unselected children into its place; undo puts them back under it, with any edits collaborators made to them meanwhile.
  • An emptied document shows a local empty paragraph when concurrent edits delete every block. Nothing is written for it until someone types, so undo never has to remove it, and two people typing into an emptied document each keep their line.

Concurrent editing explains these rules in detail.

Was this page helpful?