title |
---|
Introduction |
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.
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:
{
type: "div",
"data-element": "root",
"contenteditable": "true"
children: [
{
type: "p",
children: [{
text: "Hello world!"
}]
},
{
type: "p",
children: [
{
type: "br",
children: []
}
]
}
]
}
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.
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 the
schemarule. They are composed of different attributes, styles or
html` structures. Certain constraints are imposed on nesting.
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>;
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">
<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">
<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())
...
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.
{
"type": "div",
"children": [
{
"type": "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",
"children": []
},
{
"type": "p",
"data-id": "pd157317-RSLJ4X6g",
"children": [
{
"type": "br",
"children": []
}
]
}
]
}
The value in JSON format is derived from monitoring the changes in the html structure within the editing area (contenteditable root node) using MutationObserver.
We can access this derived data model through engine.model
.
The node type is Element.
{
// Node type
type: "div",
// Child nodes
children: [
...
]
// ... Other custom properties
}
The node type of the text is Text.
{
// Text content of the node
text: "hello world",
}
Similarly, we can use the getJsonValue and setJsonValue provided by the editor to retrieve and process values of the json type.
// 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({
type: 'div',
children: [
{
type: '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',
children: []
},
{
type: 'p',
'data-id': 'pd157317-RSLJ4X6g',
children: [
{
type: 'br',
children: []
}
]
}
]
})
// 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())
...
This open-source library listens to changes in the HTML
structure of the editing area (contenteditable root node), uses MutationObserver
to reverse-engineer the data structure, and connects and interacts with Yjs through WebSocket
to achieve multi-user collaborative editing.
- 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 provide
cardcomponent combined with
React,
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
andVue
. Easily cope with complex architecture - Built-in collaborative editing program, ready to use with lightweight configuration
- Compatible with most of the latest mobile browsers