[]
This tutorial builds on the "Real-Time Collaborative Text Editor tutorial" by adding Presence functionality, which is used to display online users and their cursor positions. Users can see the real-time status of others in the editor, such as usernames and selection positions, enhancing the collaborative experience.
When one user inputs characters in the editor, other users can see the cursor position, selected area, username and input content.
Familiarity with JavaScript and HTML/CSS basics
Run the following command in the project root directory to install the Presence-related npm packages:
npm install @mescius/js-collaboration-presence @mescius/js-collaboration-presence-client
npm install quill-cursors
Modify server.js
to add the Presence feature.
import express from "express";
import { createServer } from "http";
import { Server } from "@mescius/js-collaboration";
import OT from "@mescius/js-collaboration-ot";
import { presenceFeature } from "@mescius/js-collaboration-presence";
import richText from "rich-text";
const app = express();
const httpServer = createServer(app);
const server = new Server({ httpServer });
// Register rich-text type
OT.TypesManager.register(richText.type);
// Initialize OT document service
server.useFeature(OT.documentFeature());
// Initialize presence service
server.useFeature(presenceFeature());
// Serve static files
app.use(express.static("public"));
// Start the server
httpServer.listen(8080, () => {
console.log("Server running at http://localhost:8080");
});
We will modify the client files to add the online user cursor display functionality.
Update public/client.js
import { Client } from "@mescius/js-collaboration-client";
import * as OT from "@mescius/js-collaboration-ot-client";
import { Presence } from "@mescius/js-collaboration-presence-client";
import richText from "rich-text";
import Quill from "quill";
import QuillCursors from "quill-cursors";
Quill.register('modules/cursors', QuillCursors);
// Register rich-text type
OT.TypesManager.register(richText.type);
// Connect to server and join room
const connection = new Client().connect("room-id");
const doc = new OT.SharedDoc(connection);
const quill = new Quill("#editor", { theme: "bubble", modules: { cursors: true } });
const cursors = quill.getModule('cursors');
// Initialize Presence
const presence = new Presence(connection);
const userId = `user-${Math.random().toString(36).substring(2, 9)}`; // Generate a random user ID
const DEFAULT_COLOR_SCHEME = ['#0000ff', '#008000', '#9900cc', '#800000', '#00cc33', '#cc6600', '#cc0099'];
// Subscribe to document
doc.subscribe().then(async () => {
if (!doc.type) {
try {
await doc.create([{ insert: "Hi!" }], richText.type.uri, {});
} catch (err) {
console.error("Create Document Error:", err);
}
}
quill.setContents(doc.data);
quill.on("text-change", (delta, oldDelta, source) => {
if (source !== "user") return;
doc.submitOp(delta, { source: connection.id });
});
doc.on("op", (op, source) => {
if (source === connection.id) return;
quill.updateContents(op);
// Update the content will cause the selection to change, but Quill will not trigger the selection-change event.
presence.submitLocalStateField('selection', quill.getSelection());
});
});
// Display remote presences
function updatePresenceList() {
cursors.clearCursors();
for (const id in presence.otherStates) {
const data = presence.otherStates[id];
cursors.createCursor(id, data.userId, data.color);
cursors.moveCursor(id, data.selection);
}
}
// Subscribe to presence updates
presence.subscribe().then(() => {
updatePresenceList();
let color = DEFAULT_COLOR_SCHEME[Object.keys(presence.otherStates).length % DEFAULT_COLOR_SCHEME.length];
presence.submitLocalState({ userId, selection: quill.getSelection(), color });
// Update local presence on selection change
quill.on("selection-change", (range) => {
presence.submitLocalStateField('selection', range);
});
presence.on("add", () => updatePresenceList());
presence.on("update", () => updatePresenceList());
presence.on("remove", () => updatePresenceList());
})
// Error handling
connection.on("error", (err) => console.error("Connection error:", err.message));
doc.on("error", (err) => console.error("Document error:", err.message));
Bundle the client code
npm run build
Start the server
npm run start
You should see the output: Server running at http://localhost:8080.
Test the functionality
Open a browser and visit http://localhost:8080.
Open the same address in multiple windows, edit the content, and observe real-time synchronization.