Examples
Tiptap
Tiptap is a headless text editor, that’s fully customizable and has a first-class collaborative editing integration that’s compatible with Hocuspocus.
The below example code shows everything you need to create an instance of Tiptap, with all default extension, start your collaboration backend with Hocuspocus and connect everything.
Add an element to your HTML document where Tiptap should be initialized:
<div class="element"></div>
Install the required extensions:
npm install @hocuspocus/provider @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor yjs y-prosemirror
And create your Tiptap instance:
import { Editor } from '@tiptap/core'
import { StarterKit } from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import * as Y from 'yjs'
import { HocuspocusProvider } from '@hocuspocus/provider'
const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({
url: "ws://127.0.0.1",
name: "example-document",
document: ydoc,
});
new Editor({
element: document.querySelector(".element"),
extensions: [
StarterKit.configure({
history: false,
}),
Collaboration.configure({
document: ydoc,
}),
CollaborationCursor.configure({
provider,
user: { name: "John Doe", color: "#ffcc00" },
}),
],
});
CodeMirror
import * as Y from "yjs";
import { CodemirrorBinding } from "y-codemirror";
import { WebsocketProvider } from "y-websocket";
import CodeMirror from "codemirror";
const ydoc = new Y.Doc();
var provider = new WebsocketProvider(
"wss://websocket.tiptap.dev",
"hocuspocus-demos-codemirror",
ydoc
);
const yText = ydoc.getText("codemirror");
const yUndoManager = new Y.UndoManager(yText);
const editor = CodeMirror(document.querySelector(".editor"), {
mode: "javascript",
lineNumbers: true,
});
const binding = new CodemirrorBinding(yText, editor, provider.awareness, { yUndoManager });
Learn more: https://github.com/yjs/y-codemirror
Monaco
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { MonacoBinding } from "y-monaco";
import * as monaco from "monaco-editor";
window.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
if (label === "json") {
return "/monaco/dist/json.worker.bundle.js";
}
if (label === "css") {
return "/monaco/dist/css.worker.bundle.js";
}
if (label === "html") {
return "/monaco/dist/html.worker.bundle.js";
}
if (label === "typescript" || label === "javascript") {
return "/monaco/dist/ts.worker.bundle.js";
}
return "/monaco/dist/editor.worker.bundle.js";
},
};
window.addEventListener("load", () => {
const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
"wss://websocket.tiptap.dev",
"hocuspocus-demos-monaco",
ydoc
);
const type = ydoc.getText("monaco");
const editor = monaco.editor.create(document.querySelector(".editor"), {
value: "",
language: "javascript",
theme: "vs-dark",
});
const monacoBinding = new MonacoBinding(
type,
editor.getModel(),
new Set([editor]),
provider.awareness
);
window.example = { provider, ydoc, type, monacoBinding };
});
Learn more: https://github.com/yjs/y-monaco
Quill
import Quill from "quill";
import QuillCursors from "quill-cursors";
import * as Y from "yjs";
import { QuillBinding } from "y-quill";
import { WebsocketProvider } from "y-websocket";
Quill.register("modules/cursors", QuillCursors);
var ydoc = new Y.Doc();
var type = ydoc.getText("quill");
var provider = new WebsocketProvider("wss://websocket.tiptap.dev", "hocuspocus-demos-quill", ydoc);
var quill = new Quill(".editor", {
theme: "snow",
modules: {
cursors: true,
history: {
userOnly: true,
},
},
});
new QuillBinding(type, quill, provider.awareness);
Learn more: https://github.com/yjs/y-quill
Lexical
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { CollaborationPlugin } from "@lexical/react/LexicalCollaborationPlugin";
import * as Y from "yjs";
import { TiptapCollabProvider } from "@hocuspocus/provider";
export default function Editor({
initialEditorState,
key
}: {
initialEditorState: string | null;
key: string;
}) {
return (
<LexicalComposer
key={key}
initialConfig={{
editorState: null,
namespace: "test",
}}
>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<CollaborationPlugin
id={key}
providerFactory={createWebsocketProvider}
initialEditorState={initialEditorState}
shouldBootstrap={true}
/>
</LexicalComposer>
);
}
function createWebsocketProvider(
id: string,
yjsDocMap: Map<string, Y.Doc>
): Provider {
const doc = new Y.Doc();
yjsDocMap.set(id, doc);
// @TODO: REPLACE APP ID
// @TODO: PUT PROPER TOKEN
// @TODO: OR USE `HocuspocusProvider` with Hocuspocus URL
const hocuspocusProvider = new TiptapCollabProvider({
appId: 'YOUR_APP_ID',
name: `lexical-${id}`,
token: 'YOUR_TOKEN',
document: doc,
});
return hocuspocusProvider;
}
Slate (Draft)
Learn more: https://github.com/BitPhinix/slate-yjs
Multiplexing
In order to use multiplexing (i.e. opening multiple documents over the same websocket connection) with TiptapCollab or Hocuspocus, you'll need to create the socket and the provider separately.
The example below will show how it works with TiptapCollab, but you can just replace TiptapCollabProviderWebsocket
with HocuspocusProviderWebsocket
and TiptapCollabProvider
with HocuspocusProvider
for use with Hocuspocus.
Note that the authentication has to be taken care of per document, thus token
is part of the Provider, not the ProviderWebsocket.
import {
TiptapCollabProvider,
TiptapCollabProviderWebsocket
} from "@hocuspocus/provider";
const socket = new TiptapCollabProviderWebsocket({
appId: '', // or `url` if using `HocuspocusProviderWebsocket`
})
const provider1 = new TiptapCollabProvider({
websocketProvider: socket,
name: 'document1',
token: '',
})
const provider2 = new TiptapCollabProvider({
websocketProvider: socket,
name: 'document2',
token: '',
})