Team Tiny has begun work on a real-time collaboration solution for TinyMCE. This is the second in a series of blog posts documenting our journey and decisions along the way. The first post covered our choice of real-time collaboration algorithm (OT vs. CRDT).
Real-time collaboration in a rich text editing environment is a hard problem. A very hard problem. If you start thinking about adding collaboration to a browser-based rich text editor using ContentEditable, it is bordering on an impossible problem!
It is well established that when building an advanced editing experience, ContentEditable is an insurmountable problem. It's poorly standardised and it leaves control of the content model - the most valuable part of an editor - to the browser. While TinyMCE has proudly stuck with ContentEditable through all the discontent, we recognised that a greater level of control was required for collaboration. The increasingly standardised beforeinput event has also made this process easier than it used to be.
Our path was clear; we had to invest in an editing model.
- Note: this model will only be used for RTC, not the default editing experience
Building a model-based editor, where content is modelled in a consistent way and then rendered to ContentEditable in a style similar to React, has become a popular trend amongst editors for good reason. It allows the editor to provide a more stable user experience (particularly across browsers) and increased flexibility, such as JSON document output.
Given how many open source models are available for editing today, one of the first questions we posed to ourselves was whether to build a new one or join forces with someone else. The wish list for our model was:
- Small and lightweight, with few dependencies
- Established collaboration credentials, or at the very least an obvious way to integrate for collaboration
- Flexible enough to represent arbitrary HTML (as I established in the previous post, we will not sacrifice features for a flat model)
Although we may eventually decide to write our own model, we quickly settled on Slate as the library to use to build a high-quality product.
TinyMCE <3 Slate
Slate is a complete editor built on React, but the core model is made available as a separate package with no React-specific code.
The highly opinionated "why slate" list reads like a kindred spirit building a library with the same goals as us. Slate completely satisfies our wish list while also offering additional helpful features that extend our goals:
- Despite Slate's overall design as a React editor, the core model is so well-designed it has matched well with TinyMCE's editing needs
- The core model has a powerful operation-based abstraction but remains agnostic to collaboration algorithms, so we will be well supported if we later decide to use CRDT
- Although the overall Slate model is agnostic, it uses operation transformation and inversion internally; these APIs give us a big helping hand to deliver our OT solution
- Formatting is based on "marks", similar to flat models, which makes simple collaboration experiences easier to manage by reducing element nesting
Once our proof-of-concept was working, I had a conversation with Slate’s lead developer, Ian Storm Taylor, about our use case. Not only was he open to us including Slate in TinyMCE, he pointed out that our extensive experience with rich text editing would make us valuable contributors. We have not yet contributed much, but our hope is that we can help with Slate's efforts to build a best-in-class editor model.
Slate is unapologetically in beta, and while this has a definite level of risk associated with it, we have found the library to be such a good fit for our needs that we are willing to accept some challenges. We have already had to deal with significant API changes, but every one of them has led to a better and more flexible core.
The biggest of these was the 0.5x release in November. This happened not long after we had built our basic integration; a huge rewrite using TypeScript is a more extreme risk than we expected, but the proposed changes made us happy. We waited until February to migrate, and it has gone better than we expected thanks to the architecture we chose.
Our layer around Slate is built in a functional style using ReasonML (more on that in the next post), and the structure of version 0.5x is close to how we were already using it. As a result, we've been able to delete a lot of the code we used to adapt our style to the 0.47 Slate API.
The Slate core has been greatly simplified in 0.5x; it is even less opinionated than it was and we think it will form a rock solid foundation for our collaboration efforts. We have found that not only are Slate's features a match for our goals, but the project architecture is starting to look like the model we might have created if we built our own.
How we are building on top of Slate
Our approach to large changes has served us very well. In the last two years, the TinyMCE development team has:
- Converted the editor source code to TypeScript
- Converted the project to a monorepo
- Built a completely new user interface for TinyMCE 5
We are confident this new model will be as successful as those efforts. When the project began, we had two strict requirements for the architecture:
- Start fresh and not be biased by any of the ContentEditable TinyMCE code
- Build RTC in a way that doesn't require any UX changes to TinyMCE, other than disabling features not supported by the model
We leveraged a technique we have been using internally for years to help build out the many sub-projects that now comprise TinyMCE. We created a greenfield project built to target plain DOM APIs with complete separation between the model and the layer that adapts the model to TinyMCE.
Our real-time collaboration project has been split into a collection of sub-projects:
- A custom VDOM rendering engine for the Slate model, and reverse-map logic for selection and content input, targeting a pure ContentEditable div so it is not TinyMCE specific
- An implementation of low-level editor features built on Slate core APIs
- A layer that loads TinyMCE configuration and content to set up and compose the low-level features into a working editor
- Collaboration control (transforms, cursors, server interaction)
- Hooks in the TinyMCE core to relinquish control of ContentEditable and redirect all model APIs to the external RTC code
This architecture has been so successful that here, in the first public showing of TinyMCE RTC, I can show you collaboration between TinyMCE and a plain div.
Long term vision
Model-based editing is a big change of direction for TinyMCE. The immediate goal of this model is real-time collaboration, but what we are building is a new foundation for the future. Our vision is to eventually offer this model outside of the RTC feature as a core ContentEditable alternative for all TinyMCE integrations. We anticipate it will enable other big features like Track Changes; maybe one day it can even become the default model. We do not have any plans to announce today, but rest assured this is an active area of research.