ProseMirror bindings for Yjs that enable real-time collaborative editing with synchronization, cursors, and undo/redo
npx @tessl/cli install tessl/npm-y-prosemirror@1.3.0Y-ProseMirror provides ProseMirror bindings for Yjs that enable real-time collaborative editing. It offers essential collaborative features including document synchronization, shared cursor visibility, and individual undo/redo history for each user. The library handles concurrent editing conflicts and integrates seamlessly with ProseMirror's plugin system.
npm install y-prosemirrorimport { ySyncPlugin, yCursorPlugin, yUndoPlugin } from "y-prosemirror";For CommonJS:
const { ySyncPlugin, yCursorPlugin, yUndoPlugin } = require("y-prosemirror");Utility functions:
import {
prosemirrorToYDoc,
yXmlFragmentToProseMirrorRootNode,
absolutePositionToRelativePosition
} from "y-prosemirror";import * as Y from "yjs";
import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
import { DOMParser, Schema } from "prosemirror-model";
import { Awareness } from "y-protocols/awareness";
import {
ySyncPlugin,
yCursorPlugin,
yUndoPlugin,
undo,
redo,
initProseMirrorDoc
} from "y-prosemirror";
import { keymap } from "prosemirror-keymap";
// Define your ProseMirror schema
const schema = new Schema({
nodes: {
doc: { content: "paragraph+" },
paragraph: { content: "text*", toDOM: () => ["p", 0] },
text: {}
}
});
// Create Yjs document and awareness
const ydoc = new Y.Doc();
const yXmlFragment = ydoc.getXmlFragment("prosemirror");
const awareness = new Awareness(ydoc);
// Initialize document from Yjs content (or create empty)
const { doc, mapping } = initProseMirrorDoc(yXmlFragment, schema);
// Create ProseMirror editor with collaborative plugins
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
doc,
schema,
plugins: [
ySyncPlugin(yXmlFragment, { mapping }),
yCursorPlugin(awareness),
yUndoPlugin(),
keymap({
'Mod-z': undo,
'Mod-y': redo,
'Mod-Shift-z': redo
})
]
})
});
// Set user information for collaborative cursors
awareness.setLocalStateField('user', {
name: 'User Name',
color: '#ff6b6b'
});Y-ProseMirror is built around several key components:
ProsemirrorBinding class managing the synchronization lifecycleEssential ProseMirror plugins that enable collaborative editing with synchronization, cursors, and undo functionality.
function ySyncPlugin(yXmlFragment: Y.XmlFragment, opts?: {
colors?: Array<ColorDef>;
colorMapping?: Map<string, ColorDef>;
permanentUserData?: Y.PermanentUserData | null;
mapping?: ProsemirrorMapping;
onFirstRender?: () => void;
}): Plugin;
function yCursorPlugin(awareness: Awareness, opts?: {
awarenessStateFilter?: (currentClientId: number, userClientId: number, user: any) => boolean;
cursorBuilder?: (user: any, clientId: number) => HTMLElement;
selectionBuilder?: (user: any, clientId: number) => DecorationAttrs;
getSelection?: (state: EditorState) => Selection;
}, cursorStateField?: string): Plugin;
function yUndoPlugin(options?: {
protectedNodes?: Set<string>;
trackedOrigins?: any[];
undoManager?: UndoManager | null;
}): Plugin;Utilities for converting between ProseMirror and Yjs document formats, enabling data migration and persistence.
function prosemirrorToYDoc(doc: Node, xmlFragment?: string): Y.Doc;
function yXmlFragmentToProseMirrorRootNode(yXmlFragment: Y.XmlFragment, schema: Schema): Node;
function prosemirrorJSONToYDoc(schema: Schema, state: object, xmlFragment?: string): Y.Doc;Functions for managing positions and selections in collaborative editing contexts with relative position support.
function absolutePositionToRelativePosition(pos: number, type: Y.AbstractType, mapping: ProsemirrorMapping): Y.RelativePosition;
function relativePositionToAbsolutePosition(y: Y.Doc, documentType: Y.AbstractType, relPos: Y.RelativePosition, mapping: ProsemirrorMapping): number | null;
function getRelativeSelection(pmbinding: ProsemirrorBinding, state: EditorState): {
type: string;
anchor: Y.RelativePosition;
head: Y.RelativePosition;
};Plugin keys for accessing plugin state and configuration.
const ySyncPluginKey: PluginKey;
const yCursorPluginKey: PluginKey;
const yUndoPluginKey: PluginKey;interface ColorDef {
light: string;
dark: string;
}
type ProsemirrorMapping = Map<Y.AbstractType<any>, PModel.Node | Array<PModel.Node>>;
interface BindingMetadata {
mapping: ProsemirrorMapping;
isOMark: Map<import('prosemirror-model').MarkType, boolean>;
}
interface UndoPluginState {
undoManager: import('yjs').UndoManager;
prevSel: ReturnType<typeof getRelativeSelection> | null;
hasUndoOps: boolean;
hasRedoOps: boolean;
}
interface YSyncOpts {
colors?: Array<ColorDef>;
colorMapping?: Map<string, ColorDef>;
permanentUserData?: Y.PermanentUserData | null;
mapping?: ProsemirrorMapping;
onFirstRender?: () => void;
}
class ProsemirrorBinding {
type: Y.XmlFragment;
prosemirrorView: EditorView | null;
mapping: ProsemirrorMapping;
doc: Y.Doc;
mux: any;
isOMark: Map<MarkType, boolean>;
isDestroyed: boolean;
beforeTransactionSelection: any;
constructor(yXmlFragment: Y.XmlFragment, mapping?: Map);
initView(prosemirrorView: EditorView): void;
destroy(): void;
renderSnapshot(snapshot: Snapshot, prevSnapshot?: Snapshot): void;
unrenderSnapshot(): void;
}