Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Overview
    • Why This Fork
    • Examples
    • Installing Plite
    • Adding Event Handlers
    • Defining Custom Elements
    • Applying Custom Formatting
    • Executing Commands
    • Saving to a Database
    • Canonical Change Substrate
    • Improving Performance
    • Interfaces
    • Nodes
    • Locations
    • Transforms
    • Document Changes
    • Commands
    • Editor
    • Plugins
    • Rendering
    • Serializing
    • Normalizing
    • Using TypeScript
    • Roots
    • Document State
    • Editing Behavior
    • Selection And DOM
    • Clipboard And Paste
    • Decorations, annotations, and widgets
    • Schema
    • Anchor API
    • Location API
    • Path API
    • PointEntry API
    • Point API
    • Range API
    • Selection API
    • Location Types APIs
    • Span API
    • Editor
    • Element API
    • NodeEntry API
    • Node API
    • Node Types APIs
    • Text API
    • Debug Value Scrubbing
    • Transforms API
    • Plite DOM
    • History Editor API
    • History Plugin Setup
    • History
    • Plite History
    • Plite Hyperscript
    • Plite PaginationExperimental
    • Annotations
    • DOM Coverage Boundaries
    • Editable Component
    • External Text Views
    • Plite React Event Handling
    • Virtualized Rendering
    • Plite React Hooks
    • React Editor Setup
    • React Editor
    • Plite React
    • Plite Component
    • Plite Yjs
    • Plite
    • Migration
    • Contributing
    • FAQ
    • Resources

History

PreviousNext

Understand the undo and redo batch shape stored by the history plugin.

The history() plugin tracks undo and redo batches for an editor.

Usage

import { createEditor } from "plitejs";
import { history } from "plitejs/history";
 
const editor = createEditor({
  plugins: [history()],
});
 
const canUndo = editor.read.history.hasUndo();
const pending = editor.read.history.pending



History Plugin SetupPlite History

On This Page

UsageHistory objectStatic methodsHistory.isHistory(value: unknown): value is HistoryHistory.toJSON(editor): HistoryJSONHistory.fromJSON(editor, json): HistoryEditor APIeditor.read.history(): Historyeditor.read.history.hasUndo(): booleaneditor.read.history.hasRedo(): booleaneditor.read.history.pending(): "undo" | "redo" | nulleditor.api.history.undo(): HistoryResulteditor.api.history.redo(): HistoryResulttx.history.restore(history): voidtx.history.skip(): voidtx.history.merge(): voidtx.history.newBatch(): voideditor.update({ history: "skip" }, fn): R | undefinededitor.update({ history: "merge" }, fn): R | undefinededitor.update({ history: "new-batch" }, fn): R | undefinededitor.update.history.restore(history): voidAuthored changesControlled previews
Build your editor
Production-ready AI template and reusable components.
Get all-access
();
editor.api.history.undo();
editor.update({ history: "skip" }).text.insert("draft");
import { createEditor } from "plitejs";
import { history } from "plitejs/history";
 
const editor = createEditor({
  plugins: [history()],
});
 
const canUndo = editor.read.history.hasUndo();
const pending = editor.read.history.pending();
 
editor.api.history.undo();
 
editor.update({ history: "skip" }).text.insert("draft");

undo() and redo() claim their next batch in call order and return its result. A document batch has applied when the call returns. A batch with a live-session effect returns { status: "pending", settled } and publishes its pending direction while its external owner finishes. Document edits, selection updates, and collaborative imports continue while replay is pending.

const result = editor.api.history.undo();
const outcome = result.status === "pending" ? await result.settled : result;
const result = editor.api.history.undo();
const outcome = result.status === "pending" ? await result.settled : result;

Use hasUndo(), hasRedo(), and pending() for controls. Inspect editor.read.history() when you need the full immutable stacks.

History object

import type {
  DocumentChange,
  EditorEffect,
  EditorSchemaIdentity,
  Selection,
} from "plitejs";
 
export interface History {
  readonly redos: readonly Batch[];
  readonly revision: number;
  readonly schema: EditorSchemaIdentity;
  readonly undos: readonly Batch[];
}
 
interface Batch {
  readonly change: DocumentChange;
  readonly effects: readonly EditorEffect[];
  readonly selectionAfter: Selection;
  readonly selectionAfterRoot?: string;
  readonly selectionBefore: Selection;
  readonly selectionBeforeRoot?: string;
}
import type {
  DocumentChange,
  EditorEffect,
  EditorSchemaIdentity,
  Selection,
} from "plitejs";
 
export interface History {
  readonly redos: readonly Batch[];
  readonly revision: number;
  readonly schema: EditorSchemaIdentity;
  readonly undos: readonly Batch[];
}
 
interface Batch {
  readonly change: DocumentChange;
  readonly effects: readonly EditorEffect[];




Static methods

History.isHistory(value: unknown): value is History

Returns true if the passed in value is a History object and acts as a type guard.

History.toJSON(editor): HistoryJSON

Encode the installed undo and redo stacks as validated version 4 JSON. The envelope records the editor's schema identity, and each batch stores its canonical DocumentChange, registered effects, and selection envelopes.

Compatible typing and same-node property updates coalesce from canonical change classifications and changed ranges. Grouping fingerprints are runtime-only; loading persisted history starts a fresh automatic group.

History.fromJSON(editor, json): History

Decode and validate history without mutating the editor. Effect codecs are discovered from installed editor-plugin resources, including state fields and standalone effects. Install the returned immutable value through tx.history.restore(...).

Decoding fails before reading any batch when the persisted schema identity does not match the editor. A matching schema ID and version with a different fingerprint means the schema semantics need a version bump.

import { History } from "plitejs/history";
 
const json = History.toJSON(editor);
 
localStorage.setItem("plite.history", JSON.stringify(json));
 
const decoded = History.fromJSON(
  editor,
  JSON.parse(localStorage.getItem("plite.history")!)
);
 
editor.update((tx) => {
  tx.history.restore(decoded);
});
import { History } from "plitejs/history";
 
const json = History.toJSON(editor);
 
localStorage.setItem("plite.history", JSON.stringify(json));
 
const decoded = History.fromJSON(
  editor,
  JSON.parse(localStorage.getItem("plite.history")!)
);
 
editor.update((tx) => {
  tx.history.restore(decoded);
});

Schema identity is always derived or named; null is invalid in memory and in version 4 JSON. Schema identity, codec versions, effect keys, built-in selection payloads, and JSON values are validated during decoding. Failed decoding leaves editor state untouched.

Editor API

editor.read.history(): History

Read the current undo and redo stacks.

editor.read.history.hasUndo(): boolean

Return whether the undo branch has a surviving mapped batch.

editor.read.history.hasRedo(): boolean

Return whether the redo branch has a surviving mapped batch.

editor.read.history.pending(): "undo" | "redo" | null

Return the replay direction currently waiting for a live-session owner. Pending state belongs to this editor's runtime. It is absent from History JSON, snapshots, and collaboration.

editor.api.history.undo(): HistoryResult

Claim and replay the current undo batch.

editor.api.history.redo(): HistoryResult

Claim and replay the current redo batch.

Both methods return { status: "applied" } after a replay and { status: "empty" } when no surviving batch exists. A replay requested while another live-session replay is pending returns { status: "busy" } and does not claim another batch.

A live-session batch returns { status: "pending", settled } while its owner works, unless the owner answers synchronously. settled never rejects; it resolves to { status: "applied" }, to { status: "blocked", reason } when the owner refuses, or to { status: "failed" } when the owner or the settlement threw. A failure goes to the editor's lifecycleErrorSink. With no sink, it goes to reportError, or to console.error where reportError does not exist. A blocked or failed live-session replay leaves the same batch at the branch head. A document replay that throws while it resolves or applies its batch, for example because the schema rejects the restored document, reports the error the same way, drops that batch and returns { status: "failed" }, so the next call replays the batch below. A batch below the head that can no longer map onto the document is dropped and reported the same way. An error raised when the replay commits, such as a Yjs admission error, also reports and returns { status: "failed" }, but the batch stays at the head. Invalid runtime use throws.

Call these services outside editor.read(...) and editor.update(...). A history batch can cover several document roots; replay always applies the complete batch. A live-session batch is effect-only, local to the current session, excluded from serialized History, and never merged with another batch.

tx.history.restore(history): void

Replace both history branches when the surrounding update commits. Commit and snapshot subscribers observe the restored revision through that one update.

tx.history.skip(): void

Do not save the current transaction to history.

tx.history.merge(): void

Merge the current transaction into the previous compatible undo batch.

tx.history.newBatch(): void

Start a fresh undo batch for the current transaction.

editor.update({ history: "skip" }, fn): R | undefined

Run one update without saving it to history.

editor.update({ history: "merge" }, fn): R | undefined

Run one update that merges into the previous compatible undo batch.

editor.update({ history: "new-batch" }, fn): R | undefined

Run one update where the first write starts a fresh history batch, then the rest of the callback merges into that batch.

For one direct write, configure the update facade instead:

editor.update({ history: "skip" }).text.insert("draft");
editor.update({ history: "skip" }).text.insert("draft");

editor.update.history.restore(history): void

Restore decoded history as one direct update.

Authored changes

Install authored with History when the document needs attributed accepted edits or review proposals:

import { authored } from "plitejs/authored";
import { history } from "plitejs/history";
 
const editor = createEditor({
  plugins: [
    authored({ authorId: () => session.user.id }),
    history(),
  ],
});
import { authored } from "plitejs/authored";
import { history } from "plitejs/history";
 
const editor = createEditor({
  plugins: [
    authored({ authorId: () => session.user.id }),
    history(),
  ],
});

Undo and redo reverse local interaction batches, including the authored operation and mapped selection recorded with that batch. They do not accept or reject collaborative proposals.

Use update.authored.decide(...) for review decisions. Pass the AuthoredChange values read from the installed authored plugin.

Persist History with History.toJSON only when the application needs local undo stacks after reload. Save editor.read.value() separately to persist document content, authored review marks, and document records.

See Plite Authored Changes.

Controlled previews

Render ephemeral previews from local state, decorations, or sidecar UI until the user commits them. Do not mutate document content for a preview and then try to make that preview history later.

Use a local defineStateField without persist and with history: 'skip' for preview state. Cancel by clearing that field. Accept by clearing the preview and applying the real document edit in one normal update.

const previewReplacement = defineStateField<string | null>({
  key: "local.preview.replacement",
  history: "skip",
  initial: () => null,
});
 
editor.update((tx) => {
  tx.setField(previewReplacement, null);
  tx.text.delete({ at: selectedRange });
  tx.text.insert(acceptedText);
});
const previewReplacement = defineStateField<string | null>({
  key: "local.preview.replacement",
  history: "skip",
  initial: () => null,
});
 
editor.update((tx) => {
  tx.setField(previewReplacement, null);
  tx.text.delete({ at: selectedRange });
  tx.text.insert(acceptedText);
});

Undo then restores the document content without resurrecting preview UI.

readonly selectionAfter: Selection;
readonly selectionAfterRoot?: string;
readonly selectionBefore: Selection;
readonly selectionBeforeRoot?: string;
}