一个支持协同编辑的富文本编辑器,可以自由的使用 React、Vue 等前端常用库扩展定义插件。
广告
:科学上网,方便、快捷的上网冲浪 稳定、可靠,访问 Github 或者其它外网资源很方便。
使用浏览器提供的 contenteditable
属性让一个 DOM 节点具有可编辑能力:
<div contenteditable="true"></div>
所以它的值看起来像是这样的:
<div data-element="root" contenteditable="true"><p>Hello world!</p><p><br /></p></div>
当然,有些场景下为了方便操作,也提供了转换为 JSON 类型值的 API:
{type: "div","data-element": "root","contenteditable": "true"children: [{type: "p",children: [{text: "Hello world!"}]},{type: "p",children: [{type: "br",children: []}]}]}
比如输入的过程中 beforeinput
input
, 删除、回车以及快捷键涉及到的 mousedown
mouseup
click
等事件都会被拦截,并进行自定义的处理。
在对事件进行接管后,编辑器所做的事情就是管理好基于 contenteditable
属性根节点下的所有子节点了,比如插入文本、删除文本、插入图片等等。
综上所述,编辑中的数据结构是一个 DOM 树结构,所有的操作都是对 DOM 树直接进行操作,不是典型的以数据模型驱动视图渲染的 MVC 模式。
为了更方便的管理节点,降低复杂性。编辑器抽象化了节点属性和功能,制定了 mark
inline
block
card
4 种类型节点,他们由不同的属性、样式或 html
结构组成,并统一使用 schema
对它们进行约束。
一个简单的 schema
看起来像是这样:
{name: 'p', // 节点名称type: 'block' // 节点类型}
除此之外,还可以描述属性、样式等,比如:
{name: 'span', // 节点名称type: 'mark', // 节点类型attributes: {// 节点有一个 style 属性style: {// 必须包含一个color的样式color: {required: true, // 必须包含value: '@color' // 值是一个符合css规范的颜色值,@color 是编辑器内部定义的颜色效验,此处也可以使用方法、正则表达式去判断是否符合需要的规则}},// 可选的包含一个 test 属性,他的值可以是任意的,但不是必须的test: '*'}}
下面这几种节点都符合上面的规则:
<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>
但是除了在 color 和 test 已经在 schema
中定义外,其它的属性(background-color、test1)在处理时都会被编辑器过滤掉。
可编辑器区域内的节点通过 schema
规则,制定了 mark
inline
block
card
4 种组合节点,他们由不同的属性、样式或 html
结构组成,并对它们的嵌套进行了一定的约束。
<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 节点主要属性
import { CodeBlockComponent } from '@aomao/plugin-codeblock';
data:{"id":"ArADP","type":"block","mode":"javascript","code":"const a = 0;"}
一个 data 固定字符串后面跟一个 json,json 中 id 由编辑器生成的唯一 id,type 为卡片的类型,与它的属性 type 是一致的。后面的属性由卡片自定义。
我们对一个 json 值进行编码后就可以赋值给卡片了
// 使用js做演示,后端处理也是这个逻辑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>
在 am-editor 中对这类带卡片的自定义值获取和赋值
...// 导入编辑器import Engine from '@aomao/engine'// 导入代码块插件import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock'...// 编辑器渲染节点const container = useRef<HTMLDivElement | null>(null);useEffect(() => {// 实例化引擎const engine = new Engine(container.current, {plugins: [CodeBlock], // 传入需要支持的插件cards: [CodeBlockComponent] // 传入需要支持的卡片});// 监听编辑器值改变engine.on('change', value => {// 打印当前变更的值console.log('am-editor value:', value)// 或者可以通过 engine.getValue() 获取值})// 给编辑器赋值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>;
通过 engine.getValue() 获取的编辑器值在展示的时候需要通过 View 组件渲染。这样渲染的好处是可以还原卡片内的各种交互以及异步渲染,或者异步获取数据等操作体验
...// 导入视图渲染器import { View } from '@aomao/engine';// 导入代码块插件import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock'...const container = useRef<HTMLDivElement | null>(null);useEffect(() => {// 实例化视图渲染器const view = new View (container.current, {plugins: [CodeBlock], // 传入需要支持的插件cards: [CodeBlockComponent] // 传入需要支持的卡片});// 渲染到容器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>;
Html 相对于卡片那样的值,是无法提供异步渲染、无法使用其它 ui 库,仅仅是静态的 上一段带卡片节点的值,我们通过引擎提供的方法可以获取到以下 html
<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" 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>
卡片转换成了静态 html,这样我们可以自己复制到一个.html 中脱离 react、engine 就可以打开了
把一段 html 再还原成带卡片的值也比较容易。实例化 Engine 与卡片一致,区别在于设置值和获取值
...// 我们把这段html通过setHtml方法设置给编辑器,编辑器会自动解析成对应的卡片并且渲染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> `)// 通过 getHtml 方法,我们可以获取到当前编辑器中对应的 html,此时我们不需要考虑我们编辑器中是使用 setHtml 还是 setValue 设置的值,我们都能通过 getHtml 获取到对应的 htmlconsole.log(engine.getHtml())...
除了以上两种 DOM 节点的值之外,还提供了 JSON 类型的值,JSON 相比较以上两种值会比较跟容易遍历和操作
{"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": []}]}]}
JSON 格式的值是通过监听编辑区域(contenteditable 根节点)内的 html 结构的变化,使用 MutationObserver
反推出来的。
我们可以通过 engine.model
来访问这个反推出来的数据模型
节点的类型为 Element
{// 节点的类型type: "div",// 子节点children: [...]// ... 其它自定义属性}
文本节点的类型为 Text
{// 节点的文本text: "hello world",}
同样的我们可以通过编辑器提供的 getJsonValue 和 setJsonValue 对 json 类型的值进行获取和处理
...// 我们把这段html通过setHtml方法设置给编辑器,编辑器会自动解析成对应的卡片并且渲染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: []}]}]})// 通过 getJsonValue 方法,我们可以获取到当前编辑器中对应的 json,此时我们不需要考虑我们编辑器中是使用 setHtml 还是 setValue 设置的值,我们都能通过 getJsonValue 获取到对应的 jsonconsole.log(engine.getJsonValue())...
该开源库通过监听编辑区域(contenteditable 根节点)内的 html 结构的变化,使用 MutationObserver
反推数据结构,并通过 WebSocket
与 Yjs 连接交互,实现多用户协同编辑的功能。
mark
inline
block
类型基础插件外,我们还提供 card
组件结合React
Vue
等前端库渲染插件 UIReact
Vue
等前端库渲染。复杂架构轻松应对