Real-time Collaboration (per-node Yjs CRDT)

Two editors bridged by Yjs — awareness gives you live cursors, the shared review ledger replicates comments / track changes / AI suggestions, and textSync: "crdt" activates the per-node CRDT engine for character-level concurrent typing on the same paragraph (no last-write-wins).

Try it:click into both editors and type at the same time, on the same paragraph, in overlapping positions. Both insertions land — the CRDT merges them deterministically. The legacy textSync: truesnapshot mode is still supported for back-compat but would clobber one user’s keystrokes on the same line.

Alice

Welcome Alice. Type into the same paragraph as Bob — the CRDT will merge your edits.

Bob

Welcome Bob. You and Alice share the review ledger.

Example code

<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css" />
<script type="text/javascript" src="/richtexteditor/rte.js"></script>
<script type="text/javascript" src='/richtexteditor/plugins/all_plugins.js'></script>
<!-- Load the CRDT engine bundle so textSync:"crdt" mode is available. -->
<script type="text/javascript" src='/richtexteditor/plugins/crdt-engine.min.js'></script>

<div style="display:grid; gap:14px; grid-template-columns:1fr; margin-top:10px;">
    <div><strong>Alice</strong><div id="yjs_a"><p>Welcome Alice. Type into the same paragraph as Bob &mdash; the CRDT will merge your edits.</p></div></div>
    <div><strong>Bob</strong><div id="yjs_b"><p>Welcome Bob. You and Alice share the review ledger.</p></div></div>
</div>

<script type="module">
    // Peer-dependency imports — customers use npm or a provider of their choice.
    import * as Y from "https://esm.sh/yjs@13.6.27";
    import { Awareness } from "https://esm.sh/y-protocols@1.0.6/awareness?deps=yjs@13.6.27";

    var edA = new RichTextEditor("#yjs_a", { commentsEnabled: true, trackChangesEnabled: true, currentUser: { id: "alice", name: "Alice", color: "#2563eb" } });
    var edB = new RichTextEditor("#yjs_b", { commentsEnabled: true, trackChangesEnabled: true, currentUser: { id: "bob", name: "Bob", color: "#dc2626" } });

    // Two Y.Docs bridged in-memory so this single-tab demo behaves like two browsers.
    const docA = new Y.Doc(), docB = new Y.Doc();
    const awA = new Awareness(docA), awB = new Awareness(docB);
    docA.on("update", function (u, o) { if (o !== "remote") Y.applyUpdate(docB, u, "remote"); });
    docB.on("update", function (u, o) { if (o !== "remote") Y.applyUpdate(docA, u, "remote"); });

    function waitAttach(ed, opts) {
        if (!ed.collab) { setTimeout(function () { waitAttach(ed, opts); }, 50); return; }
        ed.collab.attach(opts);
    }
    // textSync: "crdt" routes to the bundled per-node engine.
    // textSync: true would route to the legacy snapshot-mode MVP.
    waitAttach(edA, { doc: docA, provider: { awareness: awA }, user: { id: "alice", name: "Alice", color: "#2563eb" }, textSync: "crdt" });
    waitAttach(edB, { doc: docB, provider: { awareness: awB }, user: { id: "bob", name: "Bob", color: "#dc2626" }, textSync: "crdt" });
</script>