[]
        
(Showing Draft Content)

Tutorial: Add History Functionality

This tutorial guides developers through the server-side getOps and fetchHistorySnapshot APIs to implement history management. You will:

  • View the list of operations

  • Preview specific versions

The history will be displayed in a sidebar on the right side of the editor. While previewing, the editor will disable editing. Editing capabilities will be restored after exiting the preview.

Prerequisity

Complete the Real-Time Collaborative Text Editor tutorial.

Preview

Enter the string "Hello!" in the input field and click the ​​Refresh​​ button. The right sidebar will display the operation history list, which records all operations. Click the ​​Preview​​ button for a specific version to view the corresponding editor state in the input field.


text_editor_history

Implementation Approach

Server-Side Extensions:

  • Add a /history/:roomId endpoint, using the getOps method to retrieve operation history.

  • Add a /snapshot/:roomId/:version endpoint, using the fetchHistorySnapshot method to retrieve historical version snapshots.

Client-Side Enhancements:

  • Add a sidebar on the right to display the history.

  • Implement the "Preview" functionality to load snapshots and disable editing.

  • Add the "Exit Preview" functionality to restore the current document and editing capabilities.

Step 1: Update the Server

Modify server.js to support the history API.

import express from "express";
import { createServer } from "http";
import { Server } from "@mescius/js-collaboration";
import OT from "@mescius/js-collaboration-ot";
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 services
const documentServices = new OT.DocumentServices();
server.useFeature(OT.documentFeature(documentServices));

// Serve static files
app.use(express.static("public"));

// Get operation history
app.get("/history/:roomId", async (req, res) => {
  const roomId = req.params.roomId;
  try {
    const ops = await documentServices.getOps(roomId, 0);
    res.json({ operations: ops });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Get snapshot for a specific version
app.get("/snapshot/:roomId/:version", async (req, res) => {
  const roomId = req.params.roomId;
  const version = parseInt(req.params.version, 10);
  try {
    const snapshot = await documentServices.fetchHistorySnapshot(roomId, version);
    res.json(snapshot);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Start the server
httpServer.listen(8080, () => {
  console.log("Server running at http://localhost:8080");
});

Code Explanation

The bolded sections are the newly added code, introducing two REST endpoints:

  • /history/:roomId: Returns the full operation history of a room.

  • /snapshot/:roomId/:version: Returns the document state at a specific version.

Step 2: Update the Client

Modify files index.html, styles.css, and client.js to add functionalities of history viewing and preview.

  1. Update public/index.html

    Replace the HTML from the basic tutorial with the following content:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Real-Time Collaborative Text Editor with History</title>
      <link href="https://cdn.quilljs.com/1.3.6/quill.bubble.css" rel="stylesheet">
      <link rel="stylesheet" href="./styles.css">
      <script src="./client.bundle.js" defer></script>
    </head>
    <body>
      <div class="container">
        <h1>Real-Time Collaborative Text Editor with History</h1>
        <div class="main-content">
          <div class="editor-area">
            <div id="editor"></div>
            <div id="preview-editor" style="display: none;"></div>
          </div>
          <div class="sidebar">
            <h3>History</h3>
            <div class="history-controls">
              <button onclick="loadHistory()">Refresh</button>
              <button id="exit-preview" onclick="exitPreview()" style="display: none;">Exit Preview</button>
            </div>
            <ul id="history-list"></ul>
          </div>
        </div>
      </div>
    </body>
    </html>
  2. Update public/client.js

    Replace the code from the basic tutorial with the following content:

    import { Client } from "@mescius/js-collaboration-client";
    import * as OT from "@mescius/js-collaboration-ot-client";
    import richText from "rich-text";
    import Quill from "quill";
    
    // 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" });
    const previewQuill = new Quill("#preview-editor", { theme: "bubble", readOnly: true });
    
    // Subscribe to document
    doc.subscribe().then(async () => {
        if (!doc.type) {
            try {
                await doc.create([{ insert: "Hi!" }], richText.type.uri, {});
            } catch (err) {
                if (err) console.error("Error: " + err); // Fixed: Pass single error object
            }
        }
        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);
        });
        loadHistory(); // Load history on start
    })
    
    
    // Error handling (fixed: consolidated and corrected)
    connection.on("error", (err) => console.error("Connection error:", err.message));
    doc.on("error", (err) => console.error("Document error:", err.message));
    
    // Load history
    async function loadHistory() {
        const response = await fetch("/history/room-id");
        const data = await response.json();
        const historyList = document.getElementById("history-list");
        historyList.innerHTML = "";
        data.operations.forEach((op, index) => {
            const li = document.createElement("li");
            const infoDiv = document.createElement("div");
            infoDiv.classList.add("info");
            infoDiv.textContent = `Version ${index}: ${JSON.stringify(op.create || op.op)}`;
            const previewBtn = document.createElement("button");
            previewBtn.textContent = "Preview";
            previewBtn.addEventListener("click", () => previewVersion(index + 1));
            li.appendChild(infoDiv);
            li.appendChild(previewBtn);
            historyList.appendChild(li);
        });
    }
    
    // Preview a specific version
    async function previewVersion(version) {
        const response = await fetch(`/snapshot/room-id/${version}`);
        const snapshot = await response.json();
        document.getElementById("editor").style.display = "none";
        document.getElementById("preview-editor").style.display = "block";
        previewQuill.setContents(snapshot.data);
        document.getElementById("exit-preview").style.display = "inline-block";
    }
    
    // Exit preview mode
    function exitPreview() {
        document.getElementById("editor").style.display = "block";
        document.getElementById("preview-editor").style.display = "none";
        previewQuill.setContents([]);
        document.getElementById("exit-preview").style.display = "none";
    }
    
    // Global functions
    window.loadHistory = loadHistory;
    window.exitPreview = exitPreview;
  3. Update public/styles.css

    html,
    body {
        height: 100%;
        margin: 0;
        font-family: Arial, sans-serif;
        background: #f0f2f5;
    }
    
    .container {
        height: 100vh;
        display: flex;
        flex-direction: column;
        background: #fff;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    }
    
    h1 {
        padding: 15px;
        margin: 0;
        color: #333;
        text-align: center;
        border-bottom: 1px solid #ddd;
    }
    
    .main-content {
        display: flex;
        flex: 1;
        padding: 15px;
    }
    
    .editor-area {
        flex: 2;
        display: flex;
        flex-direction: column;
    }
    
    #editor,
    #preview-editor {
        height: 100%;
        border: 1px solid #ccc;
        font-size: large;
    }
    
    .sidebar {
        flex: 1;
        background: #f5f5f5;
        border: 1px solid #ddd;
        padding: 15px;
        display: flex;
        flex-direction: column;
    }
    
    .sidebar h3 {
        margin: 0 0 10px 0;
        font-size: 16px;
        color: #333;
    }
    
    .history-controls {
        margin-bottom: 15px;
    }
    
    #history-list {
        list-style: none;
        padding: 0;
        overflow-y: auto;
        flex: 1;
    }
    
    #history-list li {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10px;
        border-bottom: 1px solid #eee;
        transition: background 0.2s;
    }
    
    #history-list li:hover {
        background: #f0f0f0;
    }
    
    .info {
        flex: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        margin-right: 10px;
        font-size: 14px;
        max-width: 400px;
    }
    
    button {
        padding: 8px 16px;
        background: #007bff;
        color: #fff;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        font-size: 14px;
    }
    
    button:hover {
        background: #0056b3;
    }
    
    #exit-preview {
        margin-left: 10px;
    }

Step 3: Build and Run

  1. Build the Client Code

    npm run build
  2. Start the Server

    npm run start
  3. Test the Functionality

    1. Open http://127.0.0.1:8080/index.html.

    2. Edit the document and click "Refresh" to view the history on the right.

    3. Click "Preview" to view a historical version (editing disabled), then click "Exit Preview" to restore.