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 nametype:'block' // node type}
In addition, you can also describe attributes, styles, etc., such as:
{name:'span', // node nametype:'mark', // node typeattributes: {// The node has a style attributestyle: {// Must contain a color stylecolor: {required: true, // must containvalue:'@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 requiredtest:'*'}}
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.
<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
import { CodeBlockComponent } from '@aomao/plugin-codeblock';
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 logicconst 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 editorimport Engine from '@aomao/engine'// import the code block pluginimport CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock'...// editor render nodeconst container = useRef<HTMLDivElement | null>(null);useEffect(() => {// instantiate the engineconst engine = new Engine(container.current, {plugins: [CodeBlock], // Pass in the plugins that need to be supportedcards: [CodeBlockComponent] // Pass in the cards that need to be supported});// Listen for editor value changesengine.on('change', value => {// print the current changed valueconsole.log('am-editor value:', value)// or you can get the value via engine.getValue()})// assign value to editorengine.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 rendererimport { View } from '@aomao/engine';// import the code block pluginimport CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock'...const container = useRef<HTMLDivElement | null>(null);useEffect(() => {// instantiate the view rendererconst view = new View (container.current, {plugins: [CodeBlock], // Pass in the plugins that need to be supportedcards: [CodeBlockComponent] // Pass in the cards that need to be supported});// render to the containerview.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"><divdata-id="de4bd68e-VhAUT2WQ"data-card-editable="false"class=""data-syntax="javascript"><divclass="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);"><divclass="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);"><preclass="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 itengine.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 getHtmlconsole.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 typetype: "div",// Child nodeschildren: [...]// ... Other custom properties}
The node type of the text is Text.
{// Text content of the nodetext: "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 itengine.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.
mark
, inline, and
blocktype, we also provide
cardcomponent combined with
React,
Vue` and other front-end libraries to render the plugin UIReact
and Vue
. Easily cope with complex architecture