What is it?

A rich text editor that supports collaborative editing, you can freely use React, Vue and other front-end common libraries to extend and define plugins.

Fundamental

Use the contenteditable attribute provided by the browser to make a DOM node editable:

<div contenteditable="true"></div>

So its value looks like this:

<div data-element="root" contenteditable="true">
<p>Hello world!</p>
<p><br /></p>
</div>

Of course, in some scenarios, for the convenience of operation, an API that converts to a JSON type value is also provided:

[
"div", // node name
// All attributes of the node
{
"data-element": "root",
"contenteditable": "true"
},
// child node 1
[
// child node name
"p",
// Child node attributes
{},
// child node of byte point
"Hello world!"
],
// child node 2
["p", {}, ["br", {}]]
]
The editor relies on the input capabilities provided by the contenteditable attribute and cursor control capabilities. Therefore, it has all the default browser behaviors, but the default behavior of the browser has different processing methods under different browser vendors' implementations, so we intercept most of its default behaviors and customize them.

For example, during the input process, beforeinput, input, delete, enter, and shortcut keys related to mousedown, mouseup, click and other events will be intercepted and customized processing will be performed.

After taking over the event, what the editor does is to manage all the child nodes under the root node based on the contenteditable property, such as inserting text, deleting text, inserting pictures, and so on.

In summary, the data structure in editing is a DOM tree structure, and all operations are performed directly on the DOM tree, not a typical MVC mode that drives view rendering with a data model.

Node constraints

In order to manage nodes more conveniently and reduce complexity. The editor abstracts node attributes and functions, and formulates four types of nodes, mark, inline, block, and card. They are composed of different attributes, styles, or html structures, and use the schema uniformly. They are constrained.

A simple schema looks like this:

{
name:'p', // node name
type:'block' // node type
}

In addition, you can also describe attributes, styles, etc., such as:

{
name:'span', // node name
type:'mark', // node type
attributes: {
// The node has a style attribute
style: {
// Must contain a color style
color: {
required: true, // must contain
value:'@color' // The value is a color value that conforms to the css specification. @color is the color validation defined in the editor. Here, methods and regular expressions can also be used to determine whether the required rules are met
}
},
// Optional include a test attribute, its value can be arbitrary, but it is not required
test:'*'
}
}

The following types of nodes conform to the above rules:

<span style="color:#fff"></span>
<span style="color:#fff" test="test123" test1="test1"></span>
<span style="color:#fff;background-color:#000;"></span>
<span style="color:#fff;background-color:#000;" test="test123"></span>

But except that color and test have been defined in schema, other attributes (background-color, test1) will be filtered out by the editor during processing.

The nodes in the editable area have four types of combined nodes of mark, inline, block, and cardthrough theschemarule. They are composed of different attributes, styles orhtml` structures. Certain constraints are imposed on nesting.

Definition of value in editor

With the custom value in the card node am-editor, the card can be rendered asynchronously, and React can be rendered in the card to do more interaction

<card type="block" name="codeblock" editable="false" value="data:%7B%22id%22%3A%22ArADP%22%2C%22type%22%3A%22block%22%2C%22mode %22%3A%22javascript%22%2C%22code%22%3A%22const%20a%20%3D%200%3B%22%7D"></card>
<p data-id="pd157317-RSLJ4X6g">
</p>

card node main attributes

  • type card type, block (separate line) or inline (embedded in line)
  • Name card name is the same as the imported CodeBlockComponent.cardName name
import { CodeBlockComponent } from '@aomao/plugin-codeblock';
  • Value The value of the card, used for card rendering, the type and structure of the value are defined and rendered by the card plugin when the card plugin is defined The card value is a data string + json , taking the above code block as an example, after decoding it looks like this
data:{"id":"ArADP","type":"block","mode":"javascript","code":"const a = 0;"}

A data fixed string is followed by a json, the id in the json is the unique id generated by the editor, and the type is the type of the card, which is consistent with its attribute type. The latter properties are customized by the card.

After we encode a json value, we can assign it to the card

// Use js for demonstration, back-end processing is also the same logic
const value = encodeURIComponent(JSON.stringify({"id":"ArADP","type":"block","mode":"javascript","code":"const a = 0;"}));
const cardValue = `data:${value}`
<card type="block" name="codeblock" editable="false" value=`data:${value}`></card>

Get and assign such custom values ​​with cards in am-editor

...
// import editor
import Engine from '@aomao/engine'
// import the code block plugin
import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock'
...
// editor render node
const container = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// instantiate the engine
const engine = new Engine(container.current, {
plugins: [CodeBlock], // Pass in the plugins that need to be supported
cards: [CodeBlockComponent] // Pass in the cards that need to be supported
});
// Listen for editor value changes
engine.on('change', value => {
// print the current changed value
console.log('am-editor value:', value)
// or you can get the value via engine.getValue()
})
// assign value to editor
engine.setValue('<card type="block" name="codeblock" editable="false" value="data:%7B%22id%22%3A%22ArADP%22%2C%22type%22%3A%22block% 22%2C%22mode%22%3A%22javascript%22%2C%22code%22%3A%22const%20a%20%3D%200%3B%22%7D"></card>')
return() => {
engine.destroy();
};
}, []);
return <div ref={container}></div>;

The editor value obtained through engine.getValue() needs to be rendered through the View component when displayed. The advantage of this rendering is that it can restore various interactions in the card and asynchronous rendering, or asynchronously obtain data and other operational experiences

...
// import view renderer
import { View } from '@aomao/engine';
// import the code block plugin
import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock'
...
const container = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// instantiate the view renderer
const view = new View (container.current, {
plugins: [CodeBlock], // Pass in the plugins that need to be supported
cards: [CodeBlockComponent] // Pass in the cards that need to be supported
});
// render to the container
view.render('<card type="block" name="codeblock" editable="false" value="data:%7B%22id%22%3A%22ArADP%22%2C%22type%22%3A%22block% 22%2C%22mode%22%3A%22javascript%22%2C%22code%22%3A%22const%20a%20%3D%200%3B%22%7D"></card>')
return() => {
view.destroy();
};
}, []);
return <div ref={container}></div>;

static html

Compared with the value of the card, Html cannot provide asynchronous rendering, cannot use other ui libraries, it is only static The value of the card node in the previous paragraph, we can get the following html through the method provided by the engine

<div
data-element="root"
class="am-engine"
data-selection-5118985c-3395-3365-8228-d08540d1293e="%7B%22path%22%3A%7B%22start%22%3A%7B% 22path%22%3A%5B1%2C0%5D%2C%22id%22%3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%2C%22end%22%3A%7B%22path%22% 3A%5B1%2C0%5D%2C%22id%22%3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%7D%2C%22uuid%22%3A%225118985c-3395-3365-8228- d08540d1293e%22%2C%22active%22%3Atrue%7D"
>
<div
data-id="de4bd68e-VhAUT2WQ"
data-card-editable="false"
class=""
data-syntax="javascript"
>
<div
class="data-codeblock-content"
style="border: 1px solid rgb(232, 232, 232); max-width: 750px; color: rgb(38, 38, 38); margin: 0px; padding: 0px; background: rgb(249, 249, 249);"
>
<div
class="CodeMirror"
style="color: rgb(89, 89, 89); margin: 0px; padding: 16px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0 , 0);"
>
<pre
class="cm-s-default"
style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);"
><span class="cm-keyword" style="color: rgb(215, 58, 73); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">const</span> <span class="cm-def" style="color: rgb (0, 92, 197); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">a</span > <span class="cm-operator" s tyle="color: rgb(215, 58, 73); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); ">=</span> <span class="cm-number" style="color: rgb(0, 92, 197); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding -box border-box rgba(0, 0, 0, 0);">0</span>;</pre>
</div>
</div>
</div>
<p data-id="pd157317-RSLJ4X6g"><br /></p>
</div>

The card is converted into static html, so that we can copy it into a .html and open it without react and engine

It is also easier to restore a piece of html to a value with a card. The instantiated Engine is the same as the card, the difference lies in setting the value and getting the value

...
// We set this html to the editor through the setHtml method, and the editor will automatically parse it into the corresponding card and render it
engine.setHtml(`<div data-element="root" class="am-engine" data-selection-5118985c-3395-3365-8228-d08540d1293e="%7B%22path%22%3A%7B%22start%22%3A%7B%22path%22%3A%5B1%2C0%5D%2C%22id%22%3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%2C%22end%22%3A%7B%22path%22%3A%5B1%2C0%5D%2C%22id%22%3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%7D%2C%22uuid%22%3A%225118985c-3395-3365-8228-d08540d1293e%22%2C%22active%22%3Atrue%7D">
<div data-id="de4bd68e-VhAUT2WQ" data-card-editable="false" class="" data-syntax="javascript"><div class="data-codeblock-content" style="border: 1px solid rgb(232, 232, 232); max-width: 750px; color: rgb(38, 38, 38); margin: 0px; padding: 0px; background: rgb(249, 249, 249);"><div class="CodeMirror" style="color: rgb(89, 89, 89); margin: 0px; padding: 16px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);"><pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);"><span class="cm-keyword" style="color: rgb(215, 58, 73); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">const</span> <span class="cm-def" style="color: rgb(0, 92, 197); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">a</span> <span class="cm-operator" style="color: rgb(215, 58, 73); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">=</span> <span class="cm-number" style="color: rgb(0, 92, 197); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">0</span>;</pre></div></div></div>
<p data-id="pd157317-RSLJ4X6g"><br></p>
</div> `)
// Through the getHtml method, we can get the corresponding html in the current editor. At this time, we don't need to consider whether the value set by setHtml or setValue is used in our editor, we can get the corresponding html through getHtml
console.log(engine.getHtml())
...

JSON format

In addition to the values ​​of the above two DOM nodes, JSON-type values ​​are also provided. Compared with the above two values, JSON will be easier to traverse and operate.

[
"div",
{
"data-selection-5118985c-3395-3365-8228-d08540d1293e": "%7B%22path%22%3A%7B%22start%22%3A%7B%22path%22%3A%5B1%2C0%5D%2C% 22id%22%3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%2C%22end%22%3A%7B%22path%22%3A%5B1%2C0%5D%2C%22id%22% 3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%7D%2C%22uuid%22%3A%225118985c-3395-3365-8228-d08540d1293e%22%2C%22active%22%3Atrue%7D"
},
[
"div",
{
"data-card-value": "data:%7B%22id%22%3A%22ArADP%22%2C%22type%22%3A%22block%22%2C%22mode%22%3A%22javascript%22%2C% 22code%22%3A%22const%20a%20%3D%200%3B%22%7D",
"data-card-type": "block",
"data-card-key": "codeblock",
"data-id": "de4bd68e-VhAUT2WQ"
}
],
[
"p",
{
"data-id": "pd157317-RSLJ4X6g"
},
["br", {}]
]
]

The value in JSON format is a json array.

[
//Index 0 is the name of the node
"div",
// The position of index 1 is all the properties of the node
{
"data-id": "de4bd68e-VhAUT2WQ"
},
// The position of index 2 represents the child node under this node
[
...
]
]

Similarly, we can obtain and process the value of json type through the getJsonValue and setJsonValue provided by the editor

...
// We set this html to the editor through the setHtml method, and the editor will automatically parse it into the corresponding card and render it
engine.setJsonValue([
"div",
{
"data-selection-5118985c-3395-3365-8228-d08540d1293e": "%7B%22path%22%3A%7B%22start%22%3A%7B%22path%22%3A%5B1%2C0%5D%2C% 22id%22%3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%2C%22end%22%3A%7B%22path%22%3A%5B1%2C0%5D%2C%22id%22% 3A%22pd157317-RSLJ4X6g%22%2C%22bi%22%3A1%7D%7D%2C%22uuid%22%3A%225118985c-3395-3365-8228-d08540d1293e%22%2C%22active%22%3Atrue%7D"
},
[
"div",
{
"data-card-value": "data:%7B%22id%22%3A%22ArADP%22%2C%22type%22%3A%22block%22%2C%22mode%22%3A%22javascript%22%2C% 22code%22%3A%22const%20a%20%3D%200%3B%22%7D",
"data-card-type": "block",
"data-card-key": "codeblock",
"data-id": "de4bd68e-VhAUT2WQ"
}
],
[
"p",
{
"data-id": "pd157317-RSLJ4X6g"
},
[
"br",
{}
]
]
])
// Through the getJsonValue method, we can get the corresponding json in the current editor. At this time, we don't need to consider whether the value set by setHtml or setValue is used in our editor. We can get the corresponding json through getJsonValue.
console.log(engine.getJsonValue())
...

Collaboration

Use the MutationObserver to monitor the mutation of the html structure in the editable area (contenteditable root node) to reverse infer OT. Connect to ShareDB through Websocket, and then use commands to add, delete, modify, and check the data saved in ShareDB.

Features

  • Out of the box, it provides dozens of rich plugins to meet most needs
  • High extensibility, in addition to the basic plugin of mark, inline, and blocktype, we also providecardcomponent combined withReact, Vue` and other front-end libraries to render the plugin UI
  • Rich multimedia support, not only supports pictures, audio and video, but also supports insertion of embedded multimedia content
  • Support Markdown syntax
  • The engine is written in pure JavaScript and does not rely on any front-end libraries. Plugins can be rendered using front-end libraries such as React and Vue. Easily cope with complex architecture
  • Built-in collaborative editing program, ready to use with lightweight configuration
  • Compatible with most of the latest mobile browsers