CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-y-prosemirror

ProseMirror bindings for Yjs that enable real-time collaborative editing with synchronization, cursors, and undo/redo

Overview
Eval results
Files

plugins.mddocs/

Core Plugins

Y-ProseMirror provides three essential ProseMirror plugins that enable collaborative editing: synchronization, cursor tracking, and collaborative undo/redo functionality.

Capabilities

Synchronization Plugin (ySyncPlugin)

Maintains bidirectional synchronization between a Yjs XML fragment and ProseMirror document, ensuring all users see consistent content.

/**
 * Creates a ProseMirror plugin that synchronizes with a Yjs XML fragment
 * @param yXmlFragment - The Yjs XML fragment to bind to
 * @param opts - Optional configuration object with destructured options
 * @returns ProseMirror Plugin instance
 */
function ySyncPlugin(yXmlFragment: Y.XmlFragment, opts?: {
  /** Array of color definitions for user highlighting */
  colors?: Array<ColorDef>;
  /** Mapping of users to specific colors */
  colorMapping?: Map<string, ColorDef>;
  /** Persistent user data for the session */
  permanentUserData?: Y.PermanentUserData | null;
  /** Pre-existing mapping between Yjs and ProseMirror nodes */
  mapping?: ProsemirrorMapping;
  /** Callback fired when content initially renders */
  onFirstRender?: () => void;
}): Plugin;

interface ColorDef {
  light: string;
  dark: string;
}

Usage Example:

import * as Y from "yjs";
import { ySyncPlugin } from "y-prosemirror";

const ydoc = new Y.Doc();
const yXmlFragment = ydoc.getXmlFragment("prosemirror");

const syncPlugin = ySyncPlugin(yXmlFragment, {
  colors: [
    { light: "#ffeb3b", dark: "#f57f17" },
    { light: "#4caf50", dark: "#2e7d32" }
  ],
  onFirstRender: () => console.log("Content rendered")
});

Cursor Plugin (yCursorPlugin)

Displays collaborative cursors and selections from other editing users using Yjs awareness protocol.

/**
 * Creates a ProseMirror plugin for displaying collaborative cursors
 * @param awareness - Yjs awareness instance for cursor tracking
 * @param opts - Optional configuration object with destructured options
 * @param cursorStateField - Awareness field name for cursor data (default: 'cursor')
 * @returns ProseMirror Plugin instance
 */
function yCursorPlugin(
  awareness: Awareness, 
  opts?: {
    /** Filter function for awareness states */
    awarenessStateFilter?: (currentClientId: number, userClientId: number, user: any) => boolean;
    /** Custom cursor element builder */
    cursorBuilder?: (user: any, clientId: number) => HTMLElement;
    /** Custom selection attributes builder */
    selectionBuilder?: (user: any, clientId: number) => DecorationAttrs;
    /** Custom selection getter */
    getSelection?: (state: EditorState) => Selection;
  }, 
  cursorStateField?: string
): Plugin;

Usage Example:

import { Awareness } from "y-protocols/awareness";
import { yCursorPlugin } from "y-prosemirror";

const awareness = new Awareness(ydoc);

const cursorPlugin = yCursorPlugin(awareness, {
  cursorBuilder: (user, clientId) => {
    const cursor = document.createElement("div");
    cursor.className = "user-cursor";
    cursor.style.borderColor = user.color;
    cursor.textContent = user.name;
    return cursor;
  },
  awarenessStateFilter: (currentId, userId, user) => {
    return currentId !== userId && user.cursor;
  }
});

Undo Plugin (yUndoPlugin)

Provides collaborative undo/redo functionality with per-user history tracking that respects collaborative document state.

/**
 * Creates a ProseMirror plugin for collaborative undo/redo operations
 * @param options - Optional configuration object with destructured options
 * @returns ProseMirror Plugin instance
 */
function yUndoPlugin(options?: {
  /** Set of node types that cannot be deleted during undo */
  protectedNodes?: Set<string>;
  /** Origins to track for undo operations */
  trackedOrigins?: any[];
  /** Custom undo manager instance */
  undoManager?: UndoManager | null;
}): Plugin;

Usage Example:

import { yUndoPlugin } from "y-prosemirror";

const undoPlugin = yUndoPlugin({
  protectedNodes: new Set(["doc", "paragraph"]),
  trackedOrigins: ["user-input", "paste"]
});

Plugin Keys

Unique identifiers for each plugin, useful for accessing plugin state or configuration.

/** Plugin key for synchronization plugin */
const ySyncPluginKey: PluginKey;

/** Plugin key for cursor plugin */
const yCursorPluginKey: PluginKey;  

/** Plugin key for undo plugin */
const yUndoPluginKey: PluginKey;

Undo/Redo Commands

Direct command functions for implementing undo/redo in your editor interface.

/**
 * Perform undo operation on editor state
 * @param state - Current editor state
 * @returns true if undo was performed
 */
function undo(state: EditorState): boolean;

/**
 * Perform redo operation on editor state  
 * @param state - Current editor state
 * @returns true if redo was performed
 */
function redo(state: EditorState): boolean;

/**
 * ProseMirror command for undo operation
 * @param state - Current editor state
 * @param dispatch - Dispatch function for state changes
 * @returns true if command was handled
 */
function undoCommand(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * ProseMirror command for redo operation
 * @param state - Current editor state  
 * @param dispatch - Dispatch function for state changes
 * @returns true if command was handled
 */
function redoCommand(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

ProseMirror Binding Class

Core binding class that manages the synchronization lifecycle between Yjs and ProseMirror.

/**
 * Main binding class managing synchronization between Yjs and ProseMirror
 */
class ProsemirrorBinding {
  /** The bound Yjs XML fragment */
  type: Y.XmlFragment;
  /** The ProseMirror editor view */
  prosemirrorView: EditorView | null;
  /** Mapping between Yjs and ProseMirror nodes */
  mapping: ProsemirrorMapping;
  /** The Yjs document */
  doc: Y.Doc;
  /** Mutex for preventing concurrent operations */
  mux: Mutex;
  /** Map tracking overlapping marks */
  isOMark: Map<MarkType, boolean>;
  /** Whether this binding has been destroyed */
  isDestroyed: boolean;
  /** Current selection as relative positions in the Yjs model */
  beforeTransactionSelection: any;

  /**
   * Create a new ProseMirror binding
   * @param yXmlFragment - Yjs XML fragment to bind
   * @param mapping - Optional initial mapping
   */
  constructor(yXmlFragment: Y.XmlFragment, mapping?: Map);

  /**
   * Initialize the binding with a ProseMirror view
   * @param prosemirrorView - ProseMirror editor view
   */
  initView(prosemirrorView: EditorView): void;

  /**
   * Clean up the binding and remove event listeners
   */
  destroy(): void;

  /**
   * Render a specific document snapshot
   * @param snapshot - Document snapshot to render
   * @param prevSnapshot - Previous snapshot for comparison
   */
  renderSnapshot(snapshot: Snapshot, prevSnapshot?: Snapshot): void;

  /**
   * Remove snapshot rendering and return to current state
   */
  unrenderSnapshot(): void;

  /**
   * Get current transaction with addToHistory set to false
   * @returns Transaction for internal operations
   */
  get _tr(): Transaction;

  /**
   * Check if local cursor is visible in view
   * @returns true if local cursor is in viewport
   */
  _isLocalCursorInView(): boolean;

  /**
   * Force re-render of the binding mapping
   */
  _forceRerender(): void;
}

Default Builders and Filters

Default implementations for cursor display and awareness filtering that can be used or customized.

/**
 * Default cursor element builder
 * @param user - User information from awareness
 * @param clientId - Client ID of the user
 * @returns HTML element for cursor display
 */
function defaultCursorBuilder(user: any, clientId: number): HTMLElement;

/**
 * Default selection attributes builder
 * @param user - User information from awareness
 * @param clientId - Client ID of the user
 * @returns Decoration attributes for selection display
 */
function defaultSelectionBuilder(user: any, clientId: number): DecorationAttrs;

/**
 * Default awareness state filter
 * @param currentClientId - Current user's client ID
 * @param userClientId - Other user's client ID
 * @param user - User data from awareness
 * @returns true if user should be displayed
 */
function defaultAwarenessStateFilter(
  currentClientId: number, 
  userClientId: number, 
  user: any
): boolean;

/**
 * Default filter for undo delete operations
 * @param item - Yjs item being deleted
 * @param protectedNodes - Set of protected node types
 * @returns true if item should be deleted
 */
function defaultDeleteFilter(item: any, protectedNodes: Set<string>): boolean;

/** Default set of protected node types for undo operations */
const defaultProtectedNodes: Set<string>;

Internal Utilities

Functions that are exported but primarily intended for internal use or advanced scenarios.

/**
 * Update Yjs fragment from ProseMirror node
 * @param y - Yjs document
 * @param yDomFragment - Yjs fragment to update
 * @param pNode - ProseMirror node
 * @param meta - Binding metadata
 */
function updateYFragment(y: Y.Doc, yDomFragment: Y.XmlFragment, pNode: Node, meta: BindingMetadata): void;

/**
 * Create ProseMirror node from Yjs element
 * @param yElement - Yjs element
 * @param schema - ProseMirror schema
 * @param meta - Binding metadata
 * @returns ProseMirror node
 */
function createNodeFromYElement(yElement: Y.XmlElement, schema: Schema, meta: BindingMetadata): Node;

/**
 * Check if Yjs item is visible in snapshot
 * @param item - Yjs item to check
 * @param snapshot - Snapshot to check against
 * @returns true if item is visible
 */
function isVisible(item: any, snapshot?: Snapshot): boolean;

/**
 * Create empty binding metadata
 * @returns Empty binding metadata object
 */
function createEmptyMeta(): BindingMetadata;

/**
 * Get current selection as relative positions for collaboration
 * @param pmbinding - ProseMirror binding instance
 * @param state - Current editor state
 * @returns Selection object with relative positions
 */
function getRelativeSelection(pmbinding: ProsemirrorBinding, state: EditorState): {
  type: string;
  anchor: Y.RelativePosition;
  head: Y.RelativePosition;
};

/**
 * Convert Yjs attribute name to ProseMirror mark name
 * @param attrName - Yjs attribute name
 * @returns ProseMirror mark name
 */
function yattr2markname(attrName: string): string;

/**
 * Convert attributes to ProseMirror marks
 * @param attrs - Attribute object
 * @param schema - ProseMirror schema
 * @returns Array of ProseMirror marks
 */
function attributesToMarks(attrs: object, schema: Schema): Mark[];

/**
 * Create cursor and selection decorations
 * @param awarenessStates - Map of awareness states
 * @param cursorBuilder - Function to build cursor elements
 * @param selectionBuilder - Function to build selection attributes
 * @param schema - ProseMirror schema
 * @returns DecorationSet with cursor decorations
 */
function createDecorations(
  awarenessStates: Map<number, any>,
  cursorBuilder: (user: any, clientId: number) => HTMLElement,
  selectionBuilder: (user: any, clientId: number) => DecorationAttrs,
  schema: Schema
): DecorationSet;

Install with Tessl CLI

npx tessl i tessl/npm-y-prosemirror

docs

index.md

plugins.md

utilities.md

tile.json