Home Doh Ref
Dohballs
  • 📁 doh_chat
    • 📁 chat_extensions
  • 📁 doh_modules
    • 📦 dataforge
    • 📦 express
    • 📁 sso
    • 📁 user

Collaborative Rich Text

Real-time collaborative rich text editing with TinyMCE, Yjs CRDTs, and the collab_docs backend.

Overview

collab_richtext provides a Google Docs-style WYSIWYG editing experience on top of the collab_docs persistence and permission system. The server is intentionally thin (two page routes), delegating all document lifecycle, Yjs sync, and permissions to collab_docs_server. The client pairs TinyMCE with a custom TinyMCEBinding class that bridges the editor's HTML content model to Yjs text CRDTs for real-time multi-user collaboration.

Architecture

collab_richtext.doh.yaml
├── collab_docs              (nodejs — shared backend: Yjs lifecycle, permissions, DB)
├── collab_richtext_server   (nodejs — page routes only)
└── collab_richtext_client   (browser)
    ├── TinyMCEBinding       (Yjs ↔ TinyMCE HTML sync)
    ├── CollabRichtextEditor  (editor container + provider lifecycle)
    ├── CollabRichtextToolbar (title, export, print, delete, user avatars)
    ├── CollabRichtextStatusBar (sync indicator, client count)
    └── CollabRichtextList    (document list + create)

Depends on: collab_docs (server), collab_shared (browser), tinymce (browser) Loaded by: chat.doh.yaml

File Structure

File Purpose
collab_richtext.doh.yaml Package manifest
collab_richtext_server.js Two page routes (~20 lines)
collab_richtext_client.js TinyMCE binding, 4 Patterns, 2 bootstrap modules (~620 lines)
collab_richtext.css Editor, list, toolbar, status bar styles

Page Routes

Method Path Auth Description
GET /richtext Yes Document list and create page
GET /richtext/:id Yes Fullscreen rich text editor

Both routes serve HTML pages that bootstrap browser modules. All document CRUD, permissions, and Yjs sync are handled by collab_docs_server.

TinyMCEBinding

Bridges TinyMCE's HTML content model to a Yjs Y.Text instance using character-level diff.

Constructor

new TinyMCEBinding(ytext, editor, awareness)
Param Type Description
ytext Y.Text Yjs text type from ydoc.getText('content')
editor TinyMCE Editor Initialized TinyMCE instance
awareness awarenessProtocol.Awareness Cursor/presence tracking

Properties

Property Type Description
ytext Y.Text Yjs text instance
editor TinyMCE Editor TinyMCE editor
awareness Awareness Yjs awareness
_lastHtml string Last known HTML content (for diffing)
_updating boolean Lock flag during remote updates
_destroyed boolean Destruction flag

Sync Algorithm

Local → Remote (_applyLocalChange):

  1. Get current HTML from editor via getContent({ format: 'raw' })
  2. Compute longest common prefix and suffix with _lastHtml
  3. Calculate delete range and insert text from the diff
  4. Apply as Yjs transaction: ytext.delete() + ytext.insert()

Remote → Local (_applyRemoteChange):

  1. Read ytext.toString() for new content
  2. Save cursor position via TinyMCE bookmark
  3. Set editor content with setContent(newContent, { format: 'raw' })
  4. Restore cursor via moveToBookmark()

TinyMCE Events Listened

Event Purpose
input Text input changes
change Content changes
ExecCommand Toolbar formatting commands
Undo Undo action
Redo Redo action

Methods

destroy() — Unobserves Yjs text, removes all TinyMCE event listeners.

Socket Events

All socket events are handled by collab_docs_server. The richtext client uses the same protocol as collab_docs_client.

Emitted by Client

Event Payload Response Used By
collab:doc:open { doc_id } { doc_meta, readOnly, syncStep1, syncStep2, awarenessStates, clientCount } Editor
collab:doc:update_meta { doc_id, updates: { title } } { error? } Toolbar
collab:doc:delete { doc_id } { error? } Toolbar
collab:doc:list { room_id? } { docs: [...] } List
collab:doc:create { title, room_id?, type: 'richtext', language: 'html' } { error?, doc } List

Listened by Client

Event Payload Action
collab:doc:meta_updated { doc } Update toolbar title
collab:doc:deleted { doc_id } Show "deleted" message, destroy binding
collab:doc:client_count { doc_id, count } Update status bar collaborator count
collab:doc:created { doc } Refresh document list
collab:doc:sync { doc_id, message: number[] } Yjs sync (via CollabSocketProvider)
collab:doc:awareness { doc_id, message: number[] } Cursor/presence (via CollabSocketProvider)

Client Patterns

CollabRichtextEditor

Main editor container managing the Yjs provider lifecycle.

Key Properties:

Property Type Description
doc_id string Document ID
doc_meta object Document metadata from server
read_only boolean Read-only mode flag
mode string 'embedded' or 'fullscreen'
_ydoc Y.Doc Yjs document instance
_provider CollabSocketProvider Socket.IO ↔ Yjs bridge
_binding TinyMCEBinding TinyMCE ↔ Yjs binding
_tinymce Editor TinyMCE editor reference

Sub-objects:

Name Pattern Purpose
toolbar CollabRichtextToolbar Title, actions, user avatars
editorContainer html TinyMCE iframe host
statusBar CollabRichtextStatusBar Sync/connection indicators

Lifecycle:

  1. object_phase — Create Y.Doc and CollabSocketProvider, wire socket listeners
  2. post_builder_phase — Connect toolbar to editor instance
  3. html_phase — Emit collab:doc:open, initialize TinyMCE on response, create binding

TinyMCE Configuration:

  • Plugins: lists, link, table, code, wordcount, searchreplace, autolink, image, charmap, emoticons
  • Toolbar: undo/redo, blocks, fonts, text formatting, colors, alignment, lists, media, search
  • Custom File menu items: Rename, Download as HTML, Print, Delete document
  • Google Docs-inspired content styling

Key Methods:

Method Description
requestClose() Navigate to /richtext or postMessage to parent
showError(message) Display error in editor container
onDocumentDeleted() Show deleted message, destroy binding and editor
destroy() Clean up binding, provider, ydoc, and TinyMCE

CollabRichtextToolbar

Title editing, document actions, and collaborator avatars.

Sub-objects: back_btn, title_display, spacer, users_container, fullscreen_btn

Key Methods:

Method Description
startTitleEdit() Make title contenteditable with select-all
saveTitle(newTitle) Emit collab:doc:update_meta with new title
downloadHtml() Build full HTML document and trigger download
printDoc() Access editor iframe and call contentWindow.print()
deleteDocument() Confirm and emit collab:doc:delete
updateMeta(doc) Update displayed title from metadata
updateUsers(awareness) Render up to 5 user avatars with overflow counter
toggleFullscreen() Toggle fullscreen class or postMessage to parent

CollabRichtextStatusBar

Sync state indicator, collaborator count, read-only badge.

Sub-objects: sync_status, client_count, readonly_indicator

Methods:

Method Params Description
setSyncStatus(status) 'syncing' | 'synced' | 'disconnected' Update sync dot and label
setClientCount(count) number Show "N collaborators" or empty
setReadOnly(readOnly) boolean Show "View only" badge

CollabRichtextList

Document browser with create functionality.

Key Properties:

Property Type Description
room_id string | null Optional room filter
docs array Loaded document list
onDocSelected function Callback when a document is clicked

Sub-objects: header (with title_area and create_btn), list_container

Key Methods:

Method Description
loadDocs() Emit collab:doc:list, sort by updated_at descending
renderList() Render document items with owner avatar, type badge, active indicator
createDocument() Prompt for title, emit collab:doc:create with type: 'richtext'

Bootstrap Modules

Module Route Behavior
collab_richtext_list_page /richtext Full-page CollabRichtextList, navigates to /richtext/:id or /collab/:id
collab_richtext_page /richtext/:id Full-page CollabRichtextEditor in fullscreen mode

Module Exports

return { TinyMCEBinding };

Integration Notes

  • All document persistence, permissions, and Yjs sync are delegated to collab_docs_server
  • Documents created with type: 'richtext' use this editor; type: 'plaintext' uses the Monaco editor from collab_docs
  • The document list shows both richtext and plaintext documents, routing to the appropriate editor
  • CollabSocketProvider from collab_shared handles the Yjs ↔ Socket.IO bridge

Testing

  1. Open /richtext — verify document list loads
  2. Create a new document — verify it appears in the list
  3. Open document — verify TinyMCE loads with toolbar
  4. Type formatted text — verify it persists on refresh
  5. Open in two browsers — verify real-time sync
  6. Test toolbar actions: rename, download HTML, print, delete
  7. Verify collaborator avatars appear when multiple users edit
  8. Check status bar: sync dot (gold → green), client count, read-only badge
Last updated: 2/21/2026