CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tiptap--pm

Comprehensive wrapper around ProseMirror packages providing unified entry point for rich text editing functionality in Tiptap framework

Pending
Overview
Eval results
Files

history.mddocs/

History

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.

Capabilities

History Plugin

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;
}

History Commands

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;

History State Queries

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;

History Control

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);
}

Advanced History Management

Manual History Grouping

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}] `)
  );
}

Custom History Behavior

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);
  }
}

History Visualization

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;
  }
}

Collaborative History

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);
  }
}

Types

/**
 * 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

docs

collaboration.md

commands-and-editing.md

cursors-and-enhancements.md

history.md

index.md

input-and-keymaps.md

markdown.md

menus-and-ui.md

model-and-schema.md

schema-definitions.md

state-management.md

tables.md

transformations.md

view-and-rendering.md

tile.json