DevUI is a team with both design and engineering perspectives, serving huawei Cloud DevCloud platform and several huawei internal middle and background systems, as well as designers and front-end engineers. Design Ng Component library: Ng-devUI

The introduction

In the world of Web development, the Rich Text Editor is a very versatile and complex component.

It’s not easy to build a good, powerful rich text editor from scratch, and building on existing open source libraries can save a lot of money.

Quill is a great choice.

This article mainly introduces the basic principles related to Quill content rendering, mainly including:

  1. How Quill describes the content of the editor
  2. Quill’s rationale for rendering Delta to the DOM
  3. The Scroll class manages the basic principles of all sub-blotting

How does Quill describe editor content?

Quill profile

Quill is an API-driven, easily extensible and cross-platform rich text editor for the modern Web. Currently, the number of Stars on Github is over 25K.

Quill is also very easy to use, creating a basic editor in just a few lines of code:

<script>
  var quill = new Quill('#editor', {
    theme: 'snow'
  });
</script>
Copy the code

How does Quill describe formatted text

When we insert formatted content into an editor, the traditional approach is to insert the corresponding DOM directly into the editor and compare the DOM tree to record the changes.

There are many disadvantages to manipulating the DOM directly, such as the difficulty of knowing exactly what format certain characters or content are in the editor, especially for custom rich text formats.

Quill takes a layer of abstraction on top of the DOM, using a very concise data structure to describe the content of the editor and its changes: Delta.

Delta is a subset of JSON that contains only one OPS property, whose value is an array of objects, each of which represents an operation on the editor (based on the initial null state of the editor).

For example, the editor has “Hello World” in it:

This is described in terms of Delta:

{
  "ops": [
    { "insert": "Hello " },
    { "insert": "World", "attributes": { "bold": true } },
    { "insert": "\n" }
  ]
}
Copy the code

The meaning is obvious, insert “Hello “into the empty editor, insert the bold “World” after the previous operation, and finally insert a newline “\n”.

How does Quill describe changes in content

Delta is very simple, but very expressive.

It has only three actions and one attribute, but it’s enough to describe any rich text content and any change in content.

Three actions:

  • Insert the insert:
  • Retain: keep
  • Delete: delete

1 attributes:

  • Attributes: format attributes

For example, if we change the bold “World” to red “World”, the action is described by Delta:

{
  "ops": [
    { "retain": 6 },
    { "retain": 5, "attributes": { "color": "#ff0000" } }
  ]
}
Copy the code

Leave the first six characters in the editor as “Hello “and the next five characters “World”, and set them to the font color “#ff0000”.

If you want to delete “World”, you’re smart enough to guess how to use Delta, and yes, you did:

{
  "ops": [
    { "retain": 6 },
    { "delete": 5 }
  ]
}
Copy the code

How does Quill describe rich text content

The most common form of rich text is an image. How does Quill use Delta to describe an image?

In addition to the string format used to describe ordinary characters, the INSERT attribute can also be an object format used to describe rich text content, such as pictures:

{
  "ops": [
    { "insert": { "image": "https://quilljs.com/assets/images/logo.svg" } },
    { "insert": "\n" }
  ]
}
Copy the code

For example, the formula:

{ 
  "ops": [ 
    { "insert": { "formula": "e=mc^2" } }, 
    { "insert": "\n" } 
  ]
}
Copy the code

Quill offers great flexibility and extensibility to customize rich text content and formats such as slides, mind maps, and even 3D models.

How does setContent render Delta data into DOM?

In the last section, we showed how Quill uses Delta to describe editor content and its variations. We learned that Delta is just a normal JSON structure with three actions and one property, but it’s very expressive.

So how does Quill apply Delta data and render it in the editor?

SetContents que

Quill has an API called setContents that allows you to render Delta data into the editor. This installment will focus on how this API is implemented.

Again, take the Delta data from the previous issue as an example:

const delta = {  "ops": [
    { "insert": "Hello " },
    { "insert": "World", "attributes": { "bold": true } },
    { "insert": "\n" } ]
}
Copy the code

When we create an instance of Quill using new Quill(), we can call its API.

const quill = new Quill('#editor', {
  theme: 'snow'
});
Copy the code

Let’s try calling setContents, passing in the Delta:

quill.setContents(delta);
Copy the code

The expected formatted text appears in the editor:

SetContents source

By looking at the source of setContents, we can see that we have called the modify method, passing in a function:

setContents(delta, source = Emitter.sources.API) { return modify.call( this, () => { delta = new Delta(delta); const length = this.getLength(); const deleted = this.editor.deleteText(0, length); const applied = this.editor.applyDelta(delta); . // Return deleted.compose(applied); }, source, ); }Copy the code

Use the call method to call modify in order to change its internal this reference, which in this case refers to the current instance of Quill, because modify is not defined in the Quill class, so you need to do this.

Let’s skip the modify method and look at the anonymous function passed in to modify.

This function does three main things:

  1. Delete all the content in the editor
  2. Apply the incoming Delta data and render it to the editor
  3. Returns the Delta data after the combination of 1 and 2

Let’s focus on step 2, which involves the applyDelta method of the Editor class.

The applyDelta method is resolved

As you can probably guess from the name, the purpose of this method is to apply and render the incoming Delta data to the editor.

The implementation, as we can probably guess, is that the ops array in the loop Delta is applied one by one to the editor.

The source code is 54 lines long and looks something like this:

applyDelta(delta) { let consumeNextNewline = false; this.scroll.update(); let scrollLength = this.scroll.length(); this.scroll.batchStart(); const normalizedDelta = normalizeDelta(delta); normalizedDelta.reduce((index, op) => { const length = op.retain || op.delete || op.insert.length || 1; let attributes = op.attributes || {}; // 1. Insert text if (op.insert! = null) {if (typeof op.insert === 'string') {// let text = op.insert; . This.scroll. InsertAt (index, text); . } else if (typeof op.insert === 'object') {// Rich text content const key = object.keys (op.insert)[0]; // There should only be one key if (key == null) return index; this.scroll.insertAt(index, key, op.insert[key]); } scrollLength += length; Object.keys(attributes).foreach (name => {this.scroll.formatat (index, length, name, attributes[name]); // 2. }); return index + length; }, 0); . This.scroll. BatchEnd (); this.scroll. this.scroll.optimize(); return this.update(normalizedDelta); }Copy the code

As we guessed, this method is to iterate the incoming Delta data with the Reduce method of Delta, separating the logic of inserting content and deleting content. In the iteration of inserting content, we mainly do two things:

  1. Insert plain text or rich text content: insertAt

  2. Format the text: formatAt

At this point, we have parsed the logic to apply the Delta data and render it into the editor. Here’s a summary:

  1. The setContents method itself doesn’t have any logic, it just calls the modify method, right

  2. The applyDelta method of the Editor object is called in the anonymous function passed in the modify method

  3. The applyDelta method iterates over the incoming Delta data and in turn inserts/formats/deletes the editor contents described by the Delta data

How does Scroll manage all Blot types?

In the previous section, we explained how Quill applies and renders Delta data into the editor by iterating over the OPS data in Delta to render Delta rows one by one into the editor.

Knowing that the insertion and formatting of the final content is done by calling methods on the Scroll object, who is the Scroll object? What role does it play in the operation of the editor?

Creation of Scroll object

The parsing in the previous section ends with the applyDelta method, which eventually calls this.scroll. InsertAt to insert the Delta contents into the editor.

The applyDelta method is defined in the Editor class and is called in the setContents method of Quill class. Looking at the source code, we can see that this.scroll was originally assigned in Quill’s constructor.

this.scroll = Parchment.create(this.root, {
  emitter: this.emitter,
  whitelist: this.options.formats
});
Copy the code

The Scroll object was created by calling the create method of Parchment.

In the last two issues, we briefly introduced Quill’s data model, Delta, but what about Parchment? How does it relate to Quill and Delta? We’ll leave these questions unanswered until we get to them.

Here’s a quick look at how the create method is used to create the Scroll object. The create method was finally defined in the Registry. Ts file in the parchment library source code.

export function create(input: Node | string | Scope, value? : any): {// The input is the editor body DOM element (.ql-editor), which contains all the actual editable content of the editor // match is the blotting class queried by the query method, Let match = query(input); if (match == null) { throw new ParchmentError(`Unable to create ${input} blot`); } let BlotClass = <BlotConstructor>match; let node = input instanceof Node || input['nodeType'] === Node.TEXT_NODE ? input : BlotClass.create(value); Return new BlotClass(<Node> Node, value); }Copy the code

Create (); create (); create (); create (); create (); create (); create (); Finally, we return the instance of the Scroll object by new Scroll(), and assign the value to this.scroll.

{ql-cursor: ƒ Scroll(domNode, selection), qL-editor: ƒ Scroll(domNode, config), ƒ FormulaBlot(), QL-syntax: ƒ SyntaxCodeBlock(), QL-video: ƒ video (),}Copy the code

Scroll type,

The Scroll class is the first blotting format that we parse, and we’ll come across various blotting formats later on, and we’ll define our own blotting formats for inserting custom content into the editor, all of which have similar structures.

It can be simply understood that the Blot format is an abstraction of the DOM node while Parchment is an abstraction of the HTML document, just as THE DOM node is the basic unit of the HTML document, the Blot is the basic unit of the Parchment document.

For example, the DOM node is

, which is encapsulated into

And encapsulate some properties and methods inside it, which becomes the Scroll class.

The Scroll class is the root Blot of all the blotting, and its CORRESPONDING DOM node is also the outermost node of the editor content, and all the editor content is wrapped under it, Scroll can be assumed to coordinate other blotting objects (actual Scroll’s parent class ContainerBlot is the BOSS behind the scenes, responsible for overall scheduling).

<div class="ql-editor" contenteditable="true"> <p> Hello <strong>World</strong> </p> ... // Other editor content </div>Copy the code

The Scroll class is defined in the blots/scroll.js file in Quill source code. Insert/formatAt/deleteAt/update/batchStart/batchEnd/optimize/applyDelta Scroll in the class.

Here is the definition of the Scroll class:

class Scroll extends ScrollBlot { constructor(domNode, config) { super(domNode); . } batchStart() {this.batch = true; // the optimize will not be the actual update. } batchEnd() {this.batch = false; this.optimize(); } // Delete the specified length in the specified position // for example: DeleteAt (index, length, source); deleteAt(index, length, source); Length) {} / / set the editor's editable state enable (enabled = true) {this. DomNode. SetAttribute (' contenteditable, enabled); } // Format content of specified length in specified position with specified format // For example: FormatText (index, length, name, value, value) FormatAt (index, length, format, value) {if (this.whitelist! = null && ! this.whitelist[format]) return; super.formatAt(index, length, format, value); this.optimize(); } // insert into the specified location // for example: insertAt(11, '\n hello, world '); // Insert (index, text, name, value, InsertAt (index, value, value) = insertAt(index, value, value) Def) {} insertBefore(Blot, Blot ref) {} / / pop up the current location path the outermost leaf Blot (will change the original array) leaf (index) {return this. The path (index). Pop () | | (null, 1), } line(index) {if (index === this.length()) {return (index === this.length())  this.line(index - 1); } return this.descendant(isLine, index); Lines (index = 0, length = number.max_value) {} TODO optimize(mutations = [], context = {}) { if (this.batch === true) return; super.optimize(mutations, context); if (mutations.length > 0) { this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations, context); }} / / is actually call the superclass ContainerBlot path method / / the purpose is to get the Blot path of the current position, and eliminate Scroll / / Blot path and DOM node path is / / such as: Div.ql -editor -> p -> strong, [[Scroll div.ql-editor, 0], [Block p, 0], [Bold strong, 6]] path(index) { return super.path(index).slice(1); // Exclude self } // TODO update(mutations) { if (this.batch === true) return; . } } Scroll.blotName = 'scroll'; Scroll.className = 'ql-editor'; Scroll.tagName = 'DIV'; Scroll.defaultChild = 'block'; Scroll.allowedChildren = [Block, BlockEmbed, Container]; export default Scroll;Copy the code

The static properties blotName and tagName, defined on the Scroll class, are required. The former uniquely identifies the blotting format, and the latter corresponds to a specific DOM tag. The latter also defines a className. Generally, allowedChildren is defined to restrict the whitelist of allowed child blotting. The DOM corresponding to the child blotting that is not in the whitelist cannot be inserted into the DOM structure corresponding to the parent blotting.

In addition to methods to insert/format/delete content, the Scroll class defines some useful methods to get the current location Blot path and Blot object, as well as events that trigger editor content updates.

The optimize and Update methods deal with Quill’s event and state-change logic and will be resolved separately later.

A specification definition document for the Blot format can be found in the following article:

Github.com/quilljs/par…

I am also the first time to use Quill for the development of rich text editor, it is inevitable that there are not enough places to understand, welcome your comments and suggestions.

Join us

We are DevUI team, welcome to come here and build elegant and efficient man-machine design/R&D system with us. Recruitment email: [email protected].

The text/DevUI Kagol

Previous article recommended

Modularity in Quill, a Modern Rich Text Editor

How to Build a Grayscale Publishing Environment

Practice of Micro Front End in Enterprise Application (part 1)