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: true snapshot mode is still supported for back-compat but would clobber one user's
keystrokes on the same line.
Welcome Alice. Type into the same paragraph as Bob — the CRDT will merge your edits.
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 — 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>