Comprehensive wrapper around ProseMirror packages providing unified entry point for rich text editing functionality in Tiptap framework
—
The history system provides undo and redo functionality by tracking document changes and maintaining reversible state transitions. It intelligently groups related changes and manages the undo stack.
Create and configure the undo/redo system.
/**
* Create a history plugin with configurable options
*/
function history(config?: HistoryOptions): Plugin;
/**
* History plugin configuration options
*/
interface HistoryOptions {
/**
* Maximum number of events in the history (default: 100)
*/
depth?: number;
/**
* Delay in milliseconds for grouping events (default: 500)
*/
newGroupDelay?: number;
}Commands for navigating through the undo/redo stack.
/**
* Undo the last change and scroll to the affected area
*/
function undo(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;
/**
* Undo the last change without scrolling
*/
function undoNoScroll(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;
/**
* Redo the last undone change and scroll to the affected area
*/
function redo(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;
/**
* Redo the last undone change without scrolling
*/
function redoNoScroll(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;Functions to inspect the current history state.
/**
* Get the number of undoable events in the history
*/
function undoDepth(state: EditorState): number;
/**
* Get the number of redoable events in the history
*/
function redoDepth(state: EditorState): number;Functions to control history behavior and grouping.
/**
* Prevent the next steps from being grouped with previous ones
*/
function closeHistory(tr: Transaction): Transaction;Usage Examples:
import {
history,
undo,
redo,
undoDepth,
redoDepth,
closeHistory
} from "@tiptap/pm/history";
import { keymap } from "@tiptap/pm/keymap";
// Basic history setup
const historyPlugin = history({
depth: 50, // Keep 50 events
newGroupDelay: 1000 // Group events within 1 second
});
// History keymap
const historyKeymap = keymap({
"Mod-z": undo,
"Mod-y": redo,
"Mod-Shift-z": redo // Alternative redo binding
});
// Create editor with history
const state = EditorState.create({
schema: mySchema,
plugins: [
historyPlugin,
historyKeymap
]
});
// Check history state
function canUndo(state: EditorState): boolean {
return undoDepth(state) > 0;
}
function canRedo(state: EditorState): boolean {
return redoDepth(state) > 0;
}
// Custom undo/redo with UI updates
function createHistoryActions(view: EditorView) {
return {
undo: () => {
if (canUndo(view.state)) {
undo(view.state, view.dispatch);
updateHistoryButtons(view);
}
},
redo: () => {
if (canRedo(view.state)) {
redo(view.state, view.dispatch);
updateHistoryButtons(view);
}
}
};
}
function updateHistoryButtons(view: EditorView) {
const undoButton = document.getElementById("undo-btn");
const redoButton = document.getElementById("redo-btn");
undoButton.disabled = !canUndo(view.state);
redoButton.disabled = !canRedo(view.state);
}Control when history events are grouped together.
// Group related operations
function performComplexEdit(view: EditorView) {
let tr = view.state.tr;
// Start a new history group
tr = closeHistory(tr);
// Perform multiple related operations
tr = tr.insertText("New content at ", 10);
tr = tr.addMark(10, 25, view.state.schema.marks.strong.create());
tr = tr.insertText(" with formatting", 25);
// Dispatch as single undoable unit
view.dispatch(tr);
}
// Prevent grouping with previous edits
function insertTimestamp(view: EditorView) {
const tr = closeHistory(view.state.tr);
const timestamp = new Date().toLocaleString();
view.dispatch(
tr.insertText(`[${timestamp}] `)
);
}Create specialized history handling for specific operations.
// Auto-save with history preservation
class AutoSaveManager {
private saveTimeout: NodeJS.Timeout | null = null;
constructor(private view: EditorView, private saveInterval: number = 30000) {
this.setupAutoSave();
}
private setupAutoSave() {
const plugin = new Plugin({
state: {
init: () => null,
apply: (tr, value) => {
if (tr.docChanged) {
this.scheduleAutoSave();
}
return value;
}
}
});
// Add plugin to existing plugins
const newState = this.view.state.reconfigure({
plugins: this.view.state.plugins.concat(plugin)
});
this.view.updateState(newState);
}
private scheduleAutoSave() {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
this.saveTimeout = setTimeout(() => {
this.performAutoSave();
}, this.saveInterval);
}
private async performAutoSave() {
try {
// Close history group before save
const tr = closeHistory(this.view.state.tr);
tr.setMeta("auto-save", true);
this.view.dispatch(tr);
// Perform save operation
await this.saveDocument();
// Clear undo history if save successful and history is deep
if (undoDepth(this.view.state) > 100) {
this.clearOldHistory();
}
} catch (error) {
console.error("Auto-save failed:", error);
}
}
private clearOldHistory() {
// Create new state with fresh history
const historyPlugin = history({ depth: 20 });
const newState = this.view.state.reconfigure({
plugins: this.view.state.plugins
.filter(p => p.spec !== history().spec)
.concat(historyPlugin)
});
this.view.updateState(newState);
}
private async saveDocument(): Promise<void> {
// Implement your save logic here
const doc = this.view.state.doc.toJSON();
// await api.save(doc);
}
}Create UI components that show history state.
// History timeline component
class HistoryTimeline {
private element: HTMLElement;
constructor(private view: EditorView) {
this.element = this.createElement();
this.updateTimeline();
// Listen for state changes
this.view.setProps({
dispatchTransaction: (tr) => {
this.view.updateState(this.view.state.apply(tr));
this.updateTimeline();
}
});
}
private createElement(): HTMLElement {
const container = document.createElement("div");
container.className = "history-timeline";
return container;
}
private updateTimeline() {
const undoCount = undoDepth(this.view.state);
const redoCount = redoDepth(this.view.state);
this.element.innerHTML = "";
// Add undo items
for (let i = undoCount - 1; i >= 0; i--) {
const item = document.createElement("div");
item.className = "history-item undo-item";
item.textContent = `Undo ${i + 1}`;
item.onclick = () => this.undoToStep(i);
this.element.appendChild(item);
}
// Add current state marker
const current = document.createElement("div");
current.className = "history-item current-item";
current.textContent = "Current";
this.element.appendChild(current);
// Add redo items
for (let i = 0; i < redoCount; i++) {
const item = document.createElement("div");
item.className = "history-item redo-item";
item.textContent = `Redo ${i + 1}`;
item.onclick = () => this.redoToStep(i);
this.element.appendChild(item);
}
}
private undoToStep(step: number) {
for (let i = 0; i <= step; i++) {
if (canUndo(this.view.state)) {
undo(this.view.state, this.view.dispatch);
}
}
}
private redoToStep(step: number) {
for (let i = 0; i <= step; i++) {
if (canRedo(this.view.state)) {
redo(this.view.state, this.view.dispatch);
}
}
}
public getElement(): HTMLElement {
return this.element;
}
}Handle history in collaborative editing scenarios.
// History manager for collaborative editing
class CollaborativeHistory {
constructor(private view: EditorView) {
this.setupCollaborativeHistory();
}
private setupCollaborativeHistory() {
const plugin = new Plugin({
state: {
init: () => ({
localUndoDepth: 0,
remoteChanges: []
}),
apply: (tr, value) => {
// Track local vs remote changes
const isLocal = !tr.getMeta("remote");
const isUndo = tr.getMeta("history$") === "undo";
const isRedo = tr.getMeta("history$") === "redo";
if (isLocal && !isUndo && !isRedo) {
// Local change - can be undone
return {
...value,
localUndoDepth: value.localUndoDepth + 1
};
} else if (tr.getMeta("remote")) {
// Remote change - affects undo stack
return {
...value,
remoteChanges: [...value.remoteChanges, tr]
};
}
return value;
}
},
props: {
handleKeyDown: (view, event) => {
// Custom undo/redo for collaborative context
if (event.key === "z" && (event.metaKey || event.ctrlKey)) {
if (event.shiftKey) {
return this.collaborativeRedo(view);
} else {
return this.collaborativeUndo(view);
}
}
return false;
}
}
});
const newState = this.view.state.reconfigure({
plugins: this.view.state.plugins.concat(plugin)
});
this.view.updateState(newState);
}
private collaborativeUndo(view: EditorView): boolean {
// Only undo local changes
const state = view.state;
const pluginState = this.getPluginState(state);
if (pluginState.localUndoDepth > 0) {
return undo(state, view.dispatch);
}
return false;
}
private collaborativeRedo(view: EditorView): boolean {
// Only redo local changes
const state = view.state;
if (redoDepth(state) > 0) {
return redo(state, view.dispatch);
}
return false;
}
private getPluginState(state: EditorState) {
// Get plugin state helper
return state.plugins.find(p => p.spec.key === "collaborativeHistory")?.getState(state);
}
}/**
* History plugin configuration
*/
interface HistoryOptions {
depth?: number;
newGroupDelay?: number;
}
/**
* History metadata for transactions
*/
interface HistoryMeta {
"history$"?: "undo" | "redo";
addToHistory?: boolean;
preserveItems?: number;
}Install with Tessl CLI
npx tessl i tessl/npm-tiptap--pm