Skip to content

History

History provides undo and redo functionality using ProseMirror’s history plugin. It tracks document changes as events and lets users step backward and forward through them. Changes made close together in time are grouped into a single undo step.

Included in StarterKit by default.

Type some text, then use the undo/redo buttons or keyboard shortcuts (Cmd+Z / Ctrl+Z and Cmd+Shift+Z / Ctrl+Shift+Z).

With the default theme enabled. The toolbar shows undo and redo buttons with disabled state.

Click to try it out

History is included in StarterKit, so it works out of the box. To use it standalone:

import { Editor, Document, Paragraph, Text, History, defaultIcons } from '@domternal/core';
import '@domternal/theme';
const editorEl = document.getElementById('editor')!;
// Toolbar
const toolbar = document.createElement('div');
toolbar.className = 'dm-toolbar';
toolbar.innerHTML = `<div class="dm-toolbar-group">
<button class="dm-toolbar-button" id="undo-btn">${defaultIcons.arrowCounterClockwise}</button>
<button class="dm-toolbar-button" id="redo-btn">${defaultIcons.arrowClockwise}</button>
</div>`;
editorEl.before(toolbar);
// Editor
const editor = new Editor({
element: editorEl,
extensions: [Document, Paragraph, Text, History],
content: '<p>Type something, then undo/redo.</p>',
});
// Wire undo/redo buttons
document.getElementById('undo-btn')!.addEventListener('click', () => {
editor.chain().focus().undo().run();
});
document.getElementById('redo-btn')!.addEventListener('click', () => {
editor.chain().focus().redo().run();
});

To configure History in StarterKit:

StarterKit.configure({
history: { depth: 50, newGroupDelay: 250 },
})

To disable History in StarterKit:

StarterKit.configure({ history: false })
OptionTypeDefaultDescription
depthnumber100Maximum number of undo events to keep. When exceeded, the oldest events are discarded.
newGroupDelaynumber500Time in milliseconds between changes before a new undo group is started. Changes within this delay are combined into a single undo step.
import { History } from '@domternal/core';
const editor = new Editor({
extensions: [
History.configure({
depth: 200, // keep more undo history
newGroupDelay: 1000, // group changes within 1 second
}),
],
});

The history plugin decides whether to merge consecutive changes into a single undo event or start a new one. A new group is started when any of these conditions are true:

  • It is the first change since the editor was created
  • There was a composition (IME) input
  • The time since the last change exceeds newGroupDelay (default 500ms)
  • The current change is not adjacent to the previous change (i.e., editing in a different part of the document)

Otherwise, consecutive adjacent changes within the time window are merged into one event. This means rapid typing in the same location produces a single undo step, while pausing or moving the cursor starts a new one.

Reverts the last undo event. The editor scrolls the affected content into view. Returns false if there is nothing to undo.

editor.commands.undo();
// With chaining
editor.chain().focus().undo().run();

Reapplies the last undone event. The editor scrolls the affected content into view. Returns false if there is nothing to redo.

editor.commands.redo();
// With chaining
editor.chain().focus().redo().run();
KeyCommandDescription
Mod-ZundoUndo the last change
Mod-Shift-ZredoRedo the last undone change
Mod-YredoRedo (Windows/Linux alternative)

Mod is Cmd on macOS and Ctrl on Windows/Linux.

The history plugin also intercepts native browser undo/redo events (beforeinput with inputType of historyUndo / historyRedo) to prevent the browser from interfering with the editor’s own history.

History does not register any input rules.

History registers two buttons in the toolbar:

ItemTypeCommandIconShortcutGroupPriority
undobuttonundoarrowCounterClockwiseMod-Zhistory200
redobuttonredoarrowClockwiseMod-Shift-Zhistory190

Both buttons reflect their disabled state automatically: undo is disabled when there is nothing to undo, redo is disabled when there is nothing to redo.

Some transactions should not be recorded in the undo history, for example UI-only state changes or decoration updates. Use setMeta to exclude them:

const tr = editor.state.tr;
tr.setMeta('addToHistory', false);
// ... apply changes to tr ...
editor.view.dispatch(tr);

This transaction will modify the document (or state) but will not appear in the undo stack.

History creates a ProseMirror history() plugin that maintains two internal branches:

  • done - the stack of completed (undoable) events
  • redo - the stack of undone (redoable) events

Each branch stores a sequence of inverted steps (the reverse of each change) along with position maps for remapping across concurrent edits. An “event” is one or more steps grouped together by the timing and adjacency logic described above.

When undo is called, the most recent event is popped from the done branch, its steps are applied in reverse to restore the previous state, and the event is pushed onto the redo branch. redo does the opposite.

The depth option caps the done branch. When the number of events exceeds the configured depth, the oldest events are discarded in batches (overflow threshold of 20) to avoid per-transaction cleanup overhead.

import { History } from '@domternal/core';
import type { HistoryOptions } from '@domternal/core';
ExportTypeDescription
HistoryExtensionThe history extension
HistoryOptionsTypeScript typeOptions for History.configure()

For low-level ProseMirror history utilities:

import {
undo, redo, undoNoScroll, redoNoScroll,
undoDepth, redoDepth,
closeHistory, isHistoryTransaction,
history,
} from '@domternal/pm/history';

@domternal/core - History.ts