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.
When to use
Section titled “When to use”Use History when you need:
- Undo (Cmd+Z) and redo (Cmd+Shift+Z) for the editor
- Configurable depth (default 100 steps) for long-form documents
- Configurable
newGroupDelayto control how rapid keystrokes are grouped
Notes:
- History is strongly recommended for any editable instance. StarterKit includes it automatically
- For read-only or display-only editors, omit History to save a small amount of state overhead
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.
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 { Document, Paragraph, Text, History } from '@domternal/core';import { DomternalEditor, DomternalToolbar } from '@domternal/vanilla';import '@domternal/theme';
const editorEl = document.getElementById('editor')!;const toolbarEl = document.createElement('div');editorEl.before(toolbarEl);
const dm = new DomternalEditor(editorEl, { extensions: [Document, Paragraph, Text, History], content: '<p>Type something, then undo/redo.</p>',});
new DomternalToolbar(toolbarEl, { editor: dm.editor });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> );}<script setup lang="ts">import { Domternal } from '@domternal/vue';import { Document, Paragraph, Text, History } from '@domternal/core';
const extensions = [Document, Paragraph, Text, History];</script>
<template> <Domternal :extensions="extensions" content="<p>Type something, then undo/redo.</p>"> <Domternal.Toolbar /> <Domternal.Content /> </Domternal></template>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.
Disabling the built-in history
Section titled “Disabling the built-in history”Some setups bring their own undo/redo. The typical case is collaborative editing: a shared-document binding must undo only the local user’s changes, so it ships its own undo manager, and running the built-in History next to it would corrupt the undo stacks.
With the core Editor class, History is opt-in like every other extension: leave it out of extensions, or disable the StarterKit module:
StarterKit.configure({ history: false })The framework wrappers include History in their default extensions, so they expose a history option (NEW in v0.12.0) that removes it:
// React and Vueconst { editor, editorRef } = useEditor({ history: false, extensions: [...] });
// Vanillaconst dm = new DomternalEditor(host, { history: false, extensions: [...] });<!-- Angular --><domternal-editor [history]="false" [extensions]="extensions" />The undo/redo toolbar buttons come from the History extension, so disabling it also removes them; the replacement undo manager provides its own commands.
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
See also
Section titled “See also”- Base Keymap - baseline ProseMirror keymap
- Starter Kit - all-in-one preset of common nodes/marks