The first sentence of the text – “this article has participated in the good article calling order activities, click to see: back end, big front end of the double track submission, 20,000 yuan prize pool for you to challenge!”

🌹 This article is written by Zi Rong, douyin small program front-end business platform team

The introduction

Small program community as a platform for external developers to communicate, carrying a lot of content, Posting function is the most important part of the community, so the community has a rich function, user experience friendly rich text editor, has become a prerequisite.

In the previous investigation of small program community editor scheme, we chose the rich text editor developed by the internal team of the company, and made plug-in customization according to the needs of the community. Prosemirror was used for secondary development at the bottom.

This article is mainly combined with their own understanding, the editor related knowledge to sort out, share with you.

1. Background

1.1 Editor classification

Editors currently fall into the following three categories:

  1. A primitive editor implementation, starting with the simplest one such as multiple lines of text
  2. The contentEditable browser provides the basic functionality
    • draftjs (react)
    • quilljs (vue)
    • prosemirror(util)
  3. Detached from the browser’s own editing capabilities, independent to do the cursor and typesetting engine
    • Office, WPS, Google Doc

The first category doesn’t support other things like pictures and videos, and the third can be a huge project. Most of the active open source frameworks today fall into the second category. Let’s take a look at both contentEditable and Document. execCommand that come with browsers, and how they relate to editors.

1.2 about contentEditable

The browser itself supports displaying rich text, so we can display images, videos, text, and other elements through HTML, CSS, and so on.

But with the advent of contentEditable, browsers have the ability to edit rich text. You make the DOM editable by setting the contentediable property of the DOM to true, and the cursor appears accordingly.

1.3 about the document. ExecCommand

While contentEditable makes the DOM editable, execCommand provides apis for modifying the DOM from the outside.

When a document is converted to designMode (contentEditable and input), document.execCommand’s command is used to select the cursor (bold, italic, etc.), and may insert a new element. It may also affect the contents of the current line, depending on what command is.

A combination of the editable DOM and the ability to modify the DOM from the outside produces the simplest editor possible.

HTML
1.<div id="contentEditable" contenteditable style="height: 666px"></div>
Copy the code
JavaScript 1. const editor = document.getElementById("contentEditable"); 2. editor.focus(); 3. document.execCommand("formatBlock", false, "h1"); 4. Document. execCommand("bold", false); // Select the text in boldCopy the code

The problem is that contentEditable only defines the operation, not the result. The result is that the same UI may correspond to different DOM structures in different browsers.

For example, for “bold font” user input, the

tag is added on Chrome, and the
tag is added on IE11.

2. prosemirror

As mentioned earlier, contentEditable has some irregularities, so Prosemirror builds on contentEditable by adding a layer of abstraction to the document model on top of DOM. ContentEditable is responsible for consistent actions across browsers, and inconsistent actions are handled through the abstraction layer. The document also has a state, and all changes to the content of the editor are recordable, reflecting changes in the state to the view.

Let’s take a look at Prosemirror.

2.1 introduction

Prosemirror is not an out-of-the-box rich text editor. The prosemirror website explains that prosemirror is more like a Lego building kit for developers to build rich text editors on.

Its main principle is that the developer has control over document and event changes. The document here is a custom data structure that contains only the elements you allow to describe the content itself and its changes, so the changes are traceable.

Its core is to give priority to modularity and customizability, which can be done on the basis of secondary development, which is also the four modules we will talk about below.

2.2 Basic Principles

Prosemirror is made up of four modules:

  • Prosemiror-model: Defines the document model, the data structure that describes the content of the editor, and is part of the description of state
  • Prosemiror-state: Data structure that describes the editor state, including selections, transactions (referring to a change in the editor state)
  • Prosemiror-view: View presentation for the editor, which is used to mount real DOM elements and provide user interaction
  • Prosemirror -transform: Records and replays changes to a document, which is the basis of transactions in the status module for undo rollback, collaborative editing, etc

Combining these four modules, we can implement a simple editor based on Prosemirror:

HTML
<div id=editor style="margin-bottom: 23px"></div>
Copy the code
JavaScript 1. import {schema} from "prosemirror-schema-basic" 2. import {EditorState} from "prosemirror-state" 3. import  {EditorView} from "prosemirror-view" 4. import {baseKeymap} from "prosemirror-commands" 5. import {DOMParser} from "prosemirror-model" 6. 7. let state = EditorState.create({schema}) 8. let view = new EditorView(document.querySelector("#editor"), { 9. state, 10. plugins: [ 11. history(), 12. keymap({"Mod-z": undo, "Mod-y": redo}), 13. keymap(baseKeymap) 14. ] 15. })Copy the code

2.3 modular

2.3.1 prosemirror – model

Of the four modules mentioned above, the document model is the most basic. The document model is described in a rich text.

HTML
1. <p>This is <strong>strong text with <em>emphasis</em></strong></p>
Copy the code

In the browser, it is a DOM tree, as shown in the following figure.

But manipulating the DOM tree itself is a complicated business. If we want to associate dom, state, and view, a more reasonable way should be to abstract the DOM tree through JS objects to form a document model.

JavaScript
1. const model = {
2.     type: 'document',
3.     content: [
4.         {
5.             type: 'paragraph',
6.             content: [
7.                 {
8.                     text: 'This is',
9.                     type: 'text'
10.                 },
11.                 {
12.                     type: 'strong',
13.                     content: [
14.                         {
15.                             type: 'text',
16.                             text: 'strong text with'
17.                         },
18.                         {
19.                             type: 'em',
20.                             content: [
21.                                 {
22.                                     text: 'emphasis',
23.                                     type: 'text'
24.                                 }
25.                             ]
26.                         }
27.                     ]
28.                 }
29.             ]
30.         }
31.     ]
32. };
Copy the code

Content [0]. Content [1]. Content [2]. Content [0].text

But let’s consider a case of tag nesting:

Both display the same UI in the browser, but the path changes as we navigate to the bold and italic text based on the model object.

Manipulating the DOM tree itself is very inconvenient, so we abstracted it into a document model, and one of the characteristics of the document model is to make it as flat as possible. So we can interpret the text above with an object like this

JavaScrpit
1. const model = {
2.     type: 'document',
3.     content: [
4.         {
5.             type: 'paragraph',
6.             content: [
7.                 {
8.                     text: 'This is',
9.                     type: 'text'
10.                 },
11.                 {
12.                     type: 'text',
13.                     text: 'strong text with',
14.                     marks: [{ type: 'strong' }]
15.                 },
16.                 {
17.                     type: 'text',
18.                     text: 'emphasis',
19.                     marks: [{ type: 'strong' }, { type: 'em' }]
20.                 }
21.             ]
22.         }
23.     ]
24. };
Copy the code

We introduce marks to represent additional information attached to the node. In this way, the path to find emphasis is fixed.

So, like the BROWSER’S DOM, the editor maintains its own hierarchy of nodes. The difference is that for inline elements, the editor is flat, which reduces the tree operation.

The document object model also contains a Schema attribute that specifies which rules the document model belongs to. A document schema that describes the types of nodes that can occur in a document and how they can be nested.

JavaScript
1. const schema = new Schema({
2.  nodes: {
3.   doc: {content: "block+"},
4.   paragraph: {group: "block", content: "text*", marks: "_"},
5.   heading: {group: "block", content: "text*", marks: ""},
6.   text: {inline: true}
7.  },
8.  marks: {
9.   strong: {},
10.   em: {}
11.  }
12. })
Copy the code

This code defines a simple skeleton. By using group attributes to create node group, block + on the content expression is equivalent to (com.lowagie.text.paragraph | blockquote) +, marks for empty string said there is no tags, “_” represents a wildcard. A document may contain one or more paragraphs and headings. Each paragraph or title contains any amount of text. The strong/italic emphasis of the text of a paragraph is supported.

Schema limits the rules for nesting tags and what marks are allowed under a node. Because the user’s behavior is unpredictable, but the schema provides a set of rules to constrain user input, tags that do not conform to the schema are removed.

2.3.2 Prosemirror -state, prosemirror-view, and prosemirror-transform

Inject the Schema in some form into the state generation process, and the editor will show only the content that conforms to the defined rules. In addition to document objects, the contents that make up the editor state include selection, plugin System, and so on. Selection contains information such as cursor position and selected area, and plugin System provides additional features such as keyboard binding for the editor.

When we initialize the state, the Promisemirror view module will display the state according to the existing state, and start to process the user input. Since the state of Prosemirror is immutable by design, a new editor state can only be described by triggering the creation of a transaction, which then updates the view with the new state. To handle user input and view interaction. This creates a unidirectional data flow, as shown in the figure below.

We can do some interesting things with transactions. For example, we can intercept the dispatch of the transaction at initialization, print the log and update the state

2. DispatchTransaction (transaction) {3. Console. log(" document content size changed by ", Transaction. Before. The content, size, "change into", transaction. Doc. Content. the size); 4. let newState = view.state.apply(transaction); 5. view.updateState(newState); 6.}Copy the code

Or we can manually inject content into the editor

// Insert hello World into the editor 2. Let tr = view.state.tr; 3. tr.insertText("hello world"); 4. let newState = view.state.apply(tr); 5. view.updateState(newState);Copy the code

2.3.3 Relationships between Modules

Combined with the analysis above, you can see that each module of Prosemirror is not independent. Prosemirror -Model forms the basis of the editor and is part of Prosemirror -State, Prosemirror -transform handles changes to state, and Prosemirror -State initializes the entire editor view.

Now let’s understand the official sample code:

HTML 1. <div id=editor style="margin-bottom: 23px"></div> 2. 3. <div style="display: None "id="content"> 4. <p> < div>Copy the code
JavaScript 1. import {EditorState} from "prosemirror-state" 2. import {EditorView} from "prosemirror-view" 3. import {Schema, DOMParser} from "prosemirror-model" 4. import {schema} from "prosemirror-schema-basic" 5. import {addListNodes} from "prosemirror-schema-list" 6. import {exampleSetup} from "prosemirror-example-setup" 7. 8. // Mix the nodes from prosemirror-schema-list into the basic schema to 9. // create a schema with list support. 10. const mySchema = new Schema({ 11. nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"), 12. marks: schema.spec.marks 13. }) 14. 15. window.view = new EditorView(document.querySelector("#editor"), { 16. state: EditorState.create({ 17. doc: Domparse.fromschema (mySchema).parse(document.querySelector("#content")), // Retrieve the HTML and generate the document object model according to the schema rules. exampleSetup({schema: mySchema}) 19. }) 20. })Copy the code

This code can be interpreted as:

  • First, a document is created after the document model is abstracted according to a specified schema based on a browser DOM
  • This creates an empty document that follows the schema, with the cursor at the start point of the document
  • The document Object model and the official default plug-in system constitute the initial editor state
  • Create an editor view component based on the editor state and mount it to the real DOM node
  • This renders the stateful document as an editable DOM Node node, with corresponding state transactions created whenever there is user input
  • A state transaction describes a change in state and is applied to create a new state, which will be used to update the view.
  • Subsequent updates to each editor are made by dispatching a transaction.

3. Afterword.

Editors have always been a difficult and sinkhole in the front end, and building one from scratch is a huge project, so I have a lot of respect for open source editors like Prosemirror and Quilljs. Standing on the shoulders of giants, understanding the basic structure and model of the editor, and having a preliminary understanding of the editor, may be a certain inspiration to us, which is also one of the original intention of writing this article.

Ps: here you can experience to our editor (forum.microapp.bytedance.com/mini-app/po…).

Welcome to the small program community (forum.microapp.bytedance.com/mini-app) walk ~

4. References

  • Contenteditable developer.mozilla.org/en-US/docs/…

  • Document. ExecCommand developer.mozilla.org/zh-CN/docs/…

  • Why ContentEditable horrible www.oschina.net/translate/w…

  • prosemirror prosemirror.net/docs/guide/

  • Youdao cloud notes cross-platform rich text editor technology evolution www.cnblogs.com/163yun/p/92…

  • Independently developed a text editor: how long does it take to www.zhihu.com/question/26…

  • Mainstream rich text editor what defect www.zhihu.com/question/40…