The history() plugin tracks undo and redo batches for an editor.
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
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.
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[];
Returns true if the passed in value is a History object and acts as a
type guard.
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.
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.
Read the current undo and redo stacks.
Return whether the undo branch has a surviving mapped batch.
Return whether the redo branch has a surviving mapped batch.
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.
Claim and replay the current undo batch.
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 replay leaves the same batch at the
branch 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.
Replace both history branches when the surrounding update commits. Commit and snapshot subscribers observe the restored revision through that one update.
Do not save the current transaction to history.
Merge the current transaction into the previous compatible undo batch.
Start a fresh undo batch for the current transaction.
Run one update without saving it to history.
Run one update that merges into the previous compatible undo batch.
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");Restore decoded history as one direct update.
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.
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.