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.
Live Playground
Section titled “Live Playground”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.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Plain editor without the theme. The buttons above call undo() and redo() directly.
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')!;
// Toolbarconst 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);
// Editorconst editor = new Editor({ element: editorEl, extensions: [Document, Paragraph, Text, History], content: '<p>Type something, then undo/redo.</p>',});
// Wire undo/redo buttonsdocument.getElementById('undo-btn')!.addEventListener('click', () => { editor.chain().focus().undo().run();});document.getElementById('redo-btn')!.addEventListener('click', () => { editor.chain().focus().redo().run();});import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text, History } from '@domternal/core';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [Document, Paragraph, Text, History]; content = '<p>Type something, then undo/redo.</p>';}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>import { Domternal } from '@domternal/react';import { Document, Paragraph, Text, History } from '@domternal/core';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, History]} content="<p>Type something, then undo/redo.</p>" > <Domternal.Toolbar /> <Domternal.Content /> </Domternal> );}import { Editor, Document, Paragraph, Text, History } from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, History], content: '<p>Type something, then undo/redo.</p>',});
// Undo/redo programmaticallyeditor.chain().focus().undo().run();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 })Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
depth | number | 100 | Maximum number of undo events to keep. When exceeded, the oldest events are discarded. |
newGroupDelay | number | 500 | Time 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 }), ],});How grouping works
Section titled “How grouping works”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.
Commands
Section titled “Commands”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 chainingeditor.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 chainingeditor.chain().focus().redo().run();Keyboard shortcuts
Section titled “Keyboard shortcuts”| Key | Command | Description |
|---|---|---|
Mod-Z | undo | Undo the last change |
Mod-Shift-Z | redo | Redo the last undone change |
Mod-Y | redo | Redo (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.
Input rules
Section titled “Input rules”History does not register any input rules.
Toolbar items
Section titled “Toolbar items”History registers two buttons in the toolbar:
| Item | Type | Command | Icon | Shortcut | Group | Priority |
|---|---|---|---|---|---|---|
undo | button | undo | arrowCounterClockwise | Mod-Z | history | 200 |
redo | button | redo | arrowClockwise | Mod-Shift-Z | history | 190 |
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.
Preventing history recording
Section titled “Preventing history recording”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.
How it works
Section titled “How it works”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.
Exports
Section titled “Exports”import { History } from '@domternal/core';import type { HistoryOptions } from '@domternal/core';| Export | Type | Description |
|---|---|---|
History | Extension | The history extension |
HistoryOptions | TypeScript type | Options for History.configure() |
For low-level ProseMirror history utilities:
import { undo, redo, undoNoScroll, redoNoScroll, undoDepth, redoDepth, closeHistory, isHistoryTransaction, history,} from '@domternal/pm/history';Source
Section titled “Source”@domternal/core - History.ts