86

0

am-editor

一个支持协同编辑的富文本编辑器,可以自由的使用React、Vue 等前端常用库扩展定义插件。

过去两年中,am-editor 编辑器基于 contenteditable 属性上做了很多功能和扩展,也遇到了很多问题。当然,有些问题从一开始的架构设计上就注定了的。所以,现在大胆一些,尝试抛弃 contenteditable属性,使用自绘光标的模式开发的下一个版本的富文本编辑器

am-editor

一个支持协同编辑的富文本编辑器,可以自由的使用React、Vue 等前端常用库扩展定义插件。

English · Demo · 文档 · 插件 · QQ群 907664876 ·

aomao-preview

Vue2 DEMO https://github.com/zb201307/am-editor-vue2

Vue3 DEMO https://github.com/red-axe/am-editor-vue3-demo

React DEMO https://github.com/yanmao-cc/am-editor/tree/master/examples/react

Vue2 DEMO https://github.com/yanmao-cc/am-editor-demo-vue2

Vue2 Nuxt DEMO https://github.com/yanmao-cc/am-editor-nuxt

基本原理

使用浏览器提供的 contenteditable 属性让一个 DOM 节点具有可编辑能力:

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

所以它的值看起来像是这样的:

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

当然,有些场景下为了方便操作,也提供了转换为 JSON 类型值的 API:

[
    'div', // 节点名称
    // 节点所有的属性
    {
        'data-element': 'root',
        contenteditable: 'true',
    },
    // 子节点1
    [
        // 子节点名称
        'p',
        // 子节点属性
        {},
        // 字节点的子节点
        'Hello world!',
    ],
    // 子节点2
    ['p', {}, ['br', {}]],
];
编辑器依赖 contenteditable 属性提供的输入能力以及光标的控制能力。因此,它拥有所有的默认浏览器行为,但是浏览器的默认行为在不同的浏览器厂商实现下存在不同的处理方式,所以我们其大部分默认行为进行了拦截并进行自定义的处理。

比如输入的过程中 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 结构组成,并对它们的嵌套进行了一定的约束。

特性

  • 开箱即用,提供几十种丰富的插件来满足大部分需求
  • 高扩展性,除了 mark inline block 类型基础插件外,我们还提供 card 组件结合 React Vue等前端库渲染插件 UI
  • 丰富的多媒体支持,不仅支持图片和音视频,更支持插入嵌入式多媒体内容
  • 支持 Markdown 语法
  • 支持国际化
  • 引擎纯 JavaScript 编写,不依赖任何前端库,插件可以使用 React Vue 等前端库渲染。复杂架构轻松应对
  • 内置协同编辑方案,轻量配置即可使用
  • 兼容大部分最新移动端浏览器

插件

版本 大小 描述
@aomao/toolbar 工具栏, 适用于 React
@aomao/toolbar-vue 工具栏, 适用于 Vue3
am-editor-toolbar-vue2 工具栏, 适用于 Vue2
@aomao/plugin-alignment 对齐方式
@aomao/plugin-embed 嵌入网址
@aomao/plugin-backcolor 背景色
@aomao/plugin-bold 加粗
@aomao/plugin-code 行内代码
@aomao/plugin-codeblock 代码块, 适用于 React
@aomao/plugin-codeblock-vue 代码块, 适用于 Vue3
am-editor-codeblock-vue2 代码块, 适用于 Vue2
@aomao/plugin-fontcolor 前景色
@aomao/plugin-fontfamily 字体
@aomao/plugin-fontsize 字体大小
@aomao/plugin-heading 标题
@aomao/plugin-hr 分割线
@aomao/plugin-indent 缩进
@aomao/plugin-italic 斜体
@aomao/plugin-link 链接, 适用于 React
@aomao/plugin-link-vue 链接, 适用于 Vue3
am-editor-link-vue2 链接, 适用于 Vue2
@aomao/plugin-line-height 行高
@aomao/plugin-mark 标记
@aomao/plugin-mention 提及
@aomao/plugin-orderedlist 有序列表
@aomao/plugin-paintformat 格式刷
@aomao/plugin-quote 引用块
@aomao/plugin-redo 重做
@aomao/plugin-removeformat 移除样式
@aomao/plugin-selectall 全选
@aomao/plugin-status 状态
@aomao/plugin-strikethrough 删除线
@aomao/plugin-sub 下标
@aomao/plugin-sup 上标
@aomao/plugin-tasklist 任务列表
@aomao/plugin-underline 下划线
@aomao/plugin-undo 撤销
@aomao/plugin-unorderedlist 无序列表
@aomao/plugin-image 图片
@aomao/plugin-table 表格
@aomao/plugin-file 文件
@aomao/plugin-mark-range 标记光标, 例如: 批注.
@aomao/plugin-math 数学公式
@aomao/plugin-video 视频

快速上手

安装

编辑器由 引擎工具栏插件 组成。引擎 为我们提供了核心的编辑能力。

使用 npm 或者 yarn 安装引擎包

$ npm install @aomao/engine
# or
$ yarn add @aomao/engine

使用

我们按照惯例先输出一个 Hello word!

import React, { useEffect, useRef, useState } from 'react';
import Engine, { EngineInterface } from '@aomao/engine';

const EngineDemo = () => {
    //编辑器容器
    const ref = useRef<HTMLDivElement | null>(null);
    //引擎实例
    const [engine, setEngine] = useState<EngineInterface>();
    //编辑器内容
    const [content, setContent] = useState<string>('<p>Hello word!</p>');

    useEffect(() => {
        if (!ref.current) return;
        //实例化引擎
        const engine = new Engine(ref.current);
        //设置编辑器值
        engine.setValue(content);
        //监听编辑器值改变事件
        engine.on('change', () => {
            const value = engine.getValue();
            setContent(value);
            console.log(`value:${value}`);
        });
        //设置引擎实例
        setEngine(engine);
    }, []);

    return <div ref={ref} />;
};
export default EngineDemo;

插件

引入 @aomao/plugin-bold 加粗插件

import Bold from '@aomao/plugin-bold';

Bold 插件加入引擎

//实例化引擎
const engine = new Engine(ref.current, {
    plugins: [Bold],
});

卡片

卡片是编辑器中单独划分的一个区域,其 UI 以及逻辑在卡片内部可以使用 React、Vue 或其它前端库自定义渲染内容,最后再挂载到编辑器上。

引入 @aomao/plugin-codeblock 代码块插件,这个插件的 语言下拉框 使用 React 渲染,所以有区分。 Vue3 使用 @aomao/plugin-codeblock-vue

import CodeBlock, { CodeBlockComponent } from '@aomao/plugin-codeblock';

CodeBlock 插件和 CodeBlockComponent 卡片组件加入引擎

//实例化引擎
const engine = new Engine(ref.current, {
    plugins: [CodeBlock],
    cards: [CodeBlockComponent],
});

CodeBlock 插件默认支持 markdown,在编辑器一行开头位置输入代码块语法 ```javascript 空格后即可触发。

工具栏

引入 @aomao/toolbar 工具栏,工具栏由于交互复杂,基本上都是使用 React + Antd UI 组件渲染,Vue3 使用 @aomao/toolbar-vue

工具栏除了 UI 交互外,大部分工作只是对不同的按钮事件触发后调用了引擎执行对应的插件命令,在需求比较复杂或需要重新定制 UI 的情况下,Fork 后修改起来也比较容易。

import Toolbar, { ToolbarPlugin, ToolbarComponent } from '@aomao/toolbar';

ToolbarPlugin 插件和 ToolbarComponent 卡片组件加入引擎,它可以让我们在编辑器中可以使用快捷键 / 唤醒出卡片工具栏

//实例化引擎
const engine = new Engine(ref.current, {
    plugins: [ToolbarPlugin],
    cards: [ToolbarComponent],
});

渲染工具栏,工具栏已配置好所有插件,这里我们只需要传入插件名称即可

return (
    ...
    {
        engine && (
            <Toolbar
                engine={engine}
                items={[
                    ['collapse'],
                    [
                        'bold',
                    ],
                ]}
            />
        )
    }
    ...
)

更复杂的工具栏配置请查看文档 https://editor.aomao.com/zh-CN/config/toolbar

协同编辑

通过 MutationObserver 监听编辑区域(contenteditable 根节点)内的 html 结构的突变反推 OT。通过 WebsocketShareDB 连接,然后使用命令对 ShareDB 保存的数据进行增、删、改、查。

交互模式

每位编辑者作为 客户端 通过 WebSocket服务端 通信交换由编辑器生成的 json0 格式的数据。

服务端会保留一份 json 格式的 html 结构数据,接收到来自客户端的指令后,再去修改这份数据,最后再转发到每个客户端。

在启用协同编辑前,我们需要配置好 客户端服务端

服务端是 NodeJs 环境,使用 express + WebSocket 搭建的网络服务。

DEMO

DEMO 中我们已经一份比较基础的客户端代码

查看 React 完整 DEMO

查看 Vue3 完整 DEMO

查看 Vue2 完整 DEMO

//实例化协作编辑客户端,传入当前编辑器引擎实例
const otClient = new OTClient(engine);
//连接到协作服务端,`demo` 与服务端文档ID相同
otClient.connect(
    `ws://127.0.0.1:8080${currentMember ? '?uid=' + currentMember.id : ''}`,
    'demo',
);

项目图标

Iconfont

开发

React

需要在 `am-editor 安装依赖

//依赖安装好后,只需要在根目录执行以下命令

yarn start
  • packages 引擎和工具栏
  • plugins 所有的插件
  • api 支持一些插件所需要的 api 访问,默认使用 https://editor.aomao.com 作为 api 服务
  • ot-server 协同服务端。启动:yarn dev

Vue

am-editor vue example

贡献

感谢 pleasedmiElena211314zb201307cheon 的捐赠

如果您愿意,可以在这里留下你的名字。

支付宝

alipay

微信支付

wechat

PayPal

https://paypal.me/aomaocom