update: docs & parserHtml
This commit is contained in:
parent
6477a0ac38
commit
7e113181f2
121
README.md
121
README.md
|
@ -1,7 +1,7 @@
|
||||||
# am-editor
|
# am-editor
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A rich text <em>collaborative</em> editor framework that can use <em>React</em> and <em>Vue</em> custom plug-ins
|
A rich text editor that supports collaborative editing, you can freely use React, Vue and other front-end common libraries to extend and define plug-ins.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -34,12 +34,6 @@
|
||||||
|
|
||||||
> Thanks to Google Translate
|
> Thanks to Google Translate
|
||||||
|
|
||||||
Use the `contenteditable` attribute provided by the browser to make a DOM node editable.
|
|
||||||
|
|
||||||
The engine takes over most of the browser's default behaviors such as cursors and events.
|
|
||||||
|
|
||||||
Monitor the changes of the `DOM` tree in the editing area through `MutationObserver`, and generate a data format of `json0` type to interact with the [ShareDB](https://github.com/share/sharedb) library to achieve collaborative editing Needs.
|
|
||||||
|
|
||||||
**`Vue2`** example [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
**`Vue2`** example [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
||||||
|
|
||||||
**`Vue3`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
**`Vue3`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
||||||
|
@ -50,6 +44,104 @@ Monitor the changes of the `DOM` tree in the editing area through `MutationObser
|
||||||
|
|
||||||
**`Vue2 Nuxt DEMO`** [https://github.com/yanmao-cc/am-editor-nuxt](https://github.com/yanmao-cc/am-editor-nuxt)
|
**`Vue2 Nuxt DEMO`** [https://github.com/yanmao-cc/am-editor-nuxt](https://github.com/yanmao-cc/am-editor-nuxt)
|
||||||
|
|
||||||
|
## Fundamental
|
||||||
|
|
||||||
|
Use the `contenteditable` attribute provided by the browser to make a DOM node editable:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div contenteditable="true"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
So its value looks like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"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", {}]]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
The editor relies on the input capabilities provided by the <strong>contenteditable</strong> 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.
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
name:'p', // node name
|
||||||
|
type:'block' // node type
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, you can also describe attributes, styles, etc., such as:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
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:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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 `card`through the`schema`rule. They are composed of different attributes, styles or`html` structures. Certain constraints are imposed on nesting.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Out of the box, it provides dozens of rich plug-ins to meet most needs
|
- Out of the box, it provides dozens of rich plug-ins to meet most needs
|
||||||
|
@ -147,7 +239,8 @@ const EngineDemo = () => {
|
||||||
//Set the editor value
|
//Set the editor value
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//Listen to the editor value change event
|
//Listen to the editor value change event
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -245,7 +338,7 @@ For more complex toolbar configuration, please check the document [https://edito
|
||||||
|
|
||||||
### Collaborative editing
|
### Collaborative editing
|
||||||
|
|
||||||
Collaborative editing is implemented based on the [ShareDB](https://github.com/share/sharedb) open source library. Those who are unfamiliar can learn about it first.
|
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](https://github.com/share/sharedb) through `Websocket`, and then use commands to add, delete, modify, and check the data saved in ShareDB.
|
||||||
|
|
||||||
#### Interactive mode
|
#### Interactive mode
|
||||||
|
|
||||||
|
@ -285,18 +378,18 @@ otClient.connect(
|
||||||
|
|
||||||
### React
|
### React
|
||||||
|
|
||||||
Need to install dependencies separately in `am-editor root directory` `site-ssr` `ot-server`
|
Need to install dependencies in `am-editor
|
||||||
|
|
||||||
```base
|
```base
|
||||||
//After the dependencies are installed, you only need to execute the following commands in the root directory
|
//After the dependencies are installed, you only need to execute the following commands in the root directory
|
||||||
|
|
||||||
yarn ssr
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
- `packages` engine and toolbar
|
- `packages` engine and toolbar
|
||||||
- `plugins` all plugins
|
- `plugins` all plugins
|
||||||
- `site-ssr` All backend API and SSR configuration. The egg used. Use yarn ssr in the am-editor root directory to automatically start `site-ssr`
|
- `api` supports api access required by some plugins. By default, https://editor.aomao.com is used as the api service
|
||||||
- `ot-server` collaborative server. Start: yarn start
|
- `ot-server` collaborative server. Start: yarn dev
|
||||||
|
|
||||||
Visit localhost:7001 after startup
|
Visit localhost:7001 after startup
|
||||||
|
|
||||||
|
@ -317,7 +410,7 @@ In the Vue runtime environment, the default is the installed code that has been
|
||||||
- Execute and install all dependent commands in the root directory of am-editor, for example: `yarn`
|
- Execute and install all dependent commands in the root directory of am-editor, for example: `yarn`
|
||||||
- Finally restart in examples/vue
|
- Finally restart in examples/vue
|
||||||
|
|
||||||
There is no backend API configured in the `Vue` case. For details, please refer to `React` and `site-ssr`
|
No back-end API is configured in the `Vue` case. For details, please refer to `React` and `api` to set reverse proxy
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|
||||||
|
|
123
README.zh-CN.md
123
README.zh-CN.md
|
@ -1,7 +1,7 @@
|
||||||
# am-editor
|
# am-editor
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
一个富文本<em>协同</em>编辑器框架,可以使用<em>React</em>和<em>Vue</em>自定义插件
|
一个支持协同编辑的富文本编辑器,可以自由的使用React、Vue 等前端常用库扩展定义插件。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -34,14 +34,6 @@
|
||||||
|
|
||||||
`广告`:[科学上网,方便、快捷的上网冲浪](https://xiyou4you.us/r/?s=18517120) 稳定、可靠,访问 Github 或者其它外网资源很方便。
|
`广告`:[科学上网,方便、快捷的上网冲浪](https://xiyou4you.us/r/?s=18517120) 稳定、可靠,访问 Github 或者其它外网资源很方便。
|
||||||
|
|
||||||
使用浏览器提供的 `contenteditable` 属性让一个 DOM 节点具有可编辑能力。
|
|
||||||
|
|
||||||
引擎接管了浏览器大部分光标、事件等默认行为。
|
|
||||||
|
|
||||||
可编辑器区域内的节点通过 `schema` 规则,制定了 `mark` `inline` `block` `card` 4 种组合节点,他们由不同的属性、样式或 `html` 结构组成,并对它们的嵌套进行了一定的约束。
|
|
||||||
|
|
||||||
通过 `MutationObserver` 监听编辑区域内的 `DOM` 树的改变,并生成 `json0` 类型的数据格式与 [ShareDB](https://github.com/share/sharedb) 库进行交互,从而达到协同编辑的需要。
|
|
||||||
|
|
||||||
**`Vue2`** 案例 [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
**`Vue2`** 案例 [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
||||||
|
|
||||||
**`Vue3`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
**`Vue3`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
||||||
|
@ -52,6 +44,104 @@
|
||||||
|
|
||||||
**`Vue2 Nuxt DEMO`** [https://github.com/yanmao-cc/am-editor-nuxt](https://github.com/yanmao-cc/am-editor-nuxt)
|
**`Vue2 Nuxt DEMO`** [https://github.com/yanmao-cc/am-editor-nuxt](https://github.com/yanmao-cc/am-editor-nuxt)
|
||||||
|
|
||||||
|
## 基本原理
|
||||||
|
|
||||||
|
使用浏览器提供的 `contenteditable` 属性让一个 DOM 节点具有可编辑能力:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div contenteditable="true"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
所以它的值看起来像是这样的:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div data-element="root" contenteditable="true">
|
||||||
|
<p>Hello world!</p>
|
||||||
|
<p><br /></p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
当然,有些场景下为了方便操作,也提供了转换为 JSON 类型值的 API:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"div", // 节点名称
|
||||||
|
// 节点所有的属性
|
||||||
|
{
|
||||||
|
"data-element": "root",
|
||||||
|
"contenteditable": "true"
|
||||||
|
},
|
||||||
|
// 子节点1
|
||||||
|
[
|
||||||
|
// 子节点名称
|
||||||
|
"p",
|
||||||
|
// 子节点属性
|
||||||
|
{},
|
||||||
|
// 字节点的子节点
|
||||||
|
"Hello world!"
|
||||||
|
],
|
||||||
|
// 子节点2
|
||||||
|
["p", {}, ["br", {}]]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
编辑器依赖 <strong>contenteditable</strong> 属性提供的输入能力以及光标的控制能力。因此,它拥有所有的默认浏览器行为,但是浏览器的默认行为在不同的浏览器厂商实现下存在不同的处理方式,所以我们其大部分默认行为进行了拦截并进行自定义的处理。
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
比如输入的过程中 `beforeinput` `input`, 删除、回车以及快捷键涉及到的 `mousedown` `mouseup` `click` 等事件都会被拦截,并进行自定义的处理。
|
||||||
|
|
||||||
|
在对事件进行接管后,编辑器所做的事情就是管理好基于 `contenteditable` 属性根节点下的所有子节点了,比如插入文本、删除文本、插入图片等等。
|
||||||
|
|
||||||
|
综上所述,编辑中的数据结构是一个 DOM 树结构,所有的操作都是对 DOM 树直接进行操作,不是典型的以数据模型驱动视图渲染的 MVC 模式。
|
||||||
|
|
||||||
|
## 节点约束
|
||||||
|
|
||||||
|
为了更方便的管理节点,降低复杂性。编辑器抽象化了节点属性和功能,制定了 `mark` `inline` `block` `card` 4 种类型节点,他们由不同的属性、样式或 `html` 结构组成,并统一使用 `schema` 对它们进行约束。
|
||||||
|
|
||||||
|
一个简单的 `schema` 看起来像是这样:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
name: 'p', // 节点名称
|
||||||
|
type: 'block' // 节点类型
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
除此之外,还可以描述属性、样式等,比如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
name: 'span', // 节点名称
|
||||||
|
type: 'mark', // 节点类型
|
||||||
|
attributes: {
|
||||||
|
// 节点有一个 style 属性
|
||||||
|
style: {
|
||||||
|
// 必须包含一个color的样式
|
||||||
|
color: {
|
||||||
|
required: true, // 必须包含
|
||||||
|
value: '@color' // 值是一个符合css规范的颜色值,@color 是编辑器内部定义的颜色效验,此处也可以使用方法、正则表达式去判断是否符合需要的规则
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 可选的包含一个 test 属性,他的值可以是任意的,但不是必须的
|
||||||
|
test: '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
下面这几种节点都符合上面的规则:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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` 结构组成,并对它们的嵌套进行了一定的约束。
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- 开箱即用,提供几十种丰富的插件来满足大部分需求
|
- 开箱即用,提供几十种丰富的插件来满足大部分需求
|
||||||
|
@ -149,7 +239,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -247,7 +338,7 @@ return (
|
||||||
|
|
||||||
### 协同编辑
|
### 协同编辑
|
||||||
|
|
||||||
协同编辑基于 [ShareDB](https://github.com/share/sharedb) 开源库实现,比较陌生的朋友可以先了解它。
|
通过 `MutationObserver` 监听编辑区域(contenteditable 根节点)内的 `html` 结构的突变反推 OT。通过`Websocket`与 [ShareDB](https://github.com/share/sharedb) 连接,然后使用命令对 ShareDB 保存的数据进行增、删、改、查。
|
||||||
|
|
||||||
#### 交互模式
|
#### 交互模式
|
||||||
|
|
||||||
|
@ -287,18 +378,18 @@ otClient.connect(
|
||||||
|
|
||||||
### React
|
### React
|
||||||
|
|
||||||
需要在 `am-editor 根目录` `site-ssr` `ot-server` 中分别安装依赖
|
需要在 `am-editor 安装依赖
|
||||||
|
|
||||||
```base
|
```base
|
||||||
//依赖安装好后,只需要在根目录执行以下命令
|
//依赖安装好后,只需要在根目录执行以下命令
|
||||||
|
|
||||||
yarn ssr
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
- `packages` 引擎和工具栏
|
- `packages` 引擎和工具栏
|
||||||
- `plugins` 所有的插件
|
- `plugins` 所有的插件
|
||||||
- `site-ssr` 所有的后端 API 和 SSR 配置。使用的 egg 。在 am-editor 根目录下使用 yarn ssr 自动启动 `site-ssr`
|
- `api` 支持一些插件所需要的 api 访问,默认使用 https://editor.aomao.com 作为 api 服务
|
||||||
- `ot-server` 协同服务端。启动:yarn start
|
- `ot-server` 协同服务端。启动:yarn dev
|
||||||
|
|
||||||
启动后访问 localhost:7001
|
启动后访问 localhost:7001
|
||||||
|
|
||||||
|
@ -319,7 +410,7 @@ yarn serve
|
||||||
- 在 am-editor 根目录下执行安装所有依赖命令,例如:`yarn`
|
- 在 am-editor 根目录下执行安装所有依赖命令,例如:`yarn`
|
||||||
- 最后在 examples/vue 中重新启动
|
- 最后在 examples/vue 中重新启动
|
||||||
|
|
||||||
`Vue` 案例中没有配置任何后端 API,具体可以参考 `React` 和 `site-ssr`
|
`Vue` 案例中没有配置任何后端 API,具体可以参考 `React` 和 `api` 设置反向代理
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,12 @@ export default (opts: {
|
||||||
'/docs': [
|
'/docs': [
|
||||||
{
|
{
|
||||||
title: 'Introduction',
|
title: 'Introduction',
|
||||||
'title_zh-CN': '介绍',
|
'title_zh-CN': '基础',
|
||||||
children: ['/docs/README', '/docs/getting-started'],
|
children: ['/docs/README', '/docs/getting-started'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Basis',
|
title: 'Basis',
|
||||||
'title_zh-CN': '基础',
|
'title_zh-CN': '概念',
|
||||||
children: [
|
children: [
|
||||||
'/docs/concepts-node',
|
'/docs/concepts-node',
|
||||||
'/docs/concepts-schema',
|
'/docs/concepts-schema',
|
||||||
|
@ -24,7 +24,7 @@ export default (opts: {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Resource',
|
title: 'Resource',
|
||||||
'title_zh-CN': '资源文件',
|
'title_zh-CN': '资源',
|
||||||
children: ['/docs/resources-icon'],
|
children: ['/docs/resources-icon'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,26 +4,114 @@ title: Introduction
|
||||||
|
|
||||||
## What is it?
|
## What is it?
|
||||||
|
|
||||||
> Thanks to Google Translate
|
A rich text editor that supports collaborative editing, you can freely use React, Vue and other front-end common libraries to extend and define plug-ins.
|
||||||
|
|
||||||
Use the `contenteditable` attribute provided by the browser to make a DOM node editable.
|
## Fundamental
|
||||||
|
|
||||||
The engine takes over most of the browser's default behaviors such as cursors and events.
|
Use the `contenteditable` attribute provided by the browser to make a DOM node editable:
|
||||||
|
|
||||||
The nodes in the editor area have four types of combined nodes of `mark`, `inline`, `block` and `card` through the `schema` rule. They are composed of different attributes, styles or `html` structures. Certain constraints are imposed on nesting.
|
```html
|
||||||
|
<div contenteditable="true"></div>
|
||||||
|
```
|
||||||
|
|
||||||
Use the `MutationObserver` to monitor the changes of the `html` structure in the editing area, and generate a `json0` type data format to interact with the [ShareDB](https://github.com/share/sharedb) library to meet the needs of collaborative editing .
|
So its value looks like this:
|
||||||
|
|
||||||
**`Vue2`** example [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
```html
|
||||||
|
<div data-element="root" contenteditable="true">
|
||||||
|
<p>Hello world!</p>
|
||||||
|
<p><br /></p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
**`Vue3`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
Of course, in some scenarios, for the convenience of operation, an API that converts to a JSON type value is also provided:
|
||||||
|
|
||||||
**`React`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
|
```json
|
||||||
|
[
|
||||||
|
"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", {}]]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
The editor relies on the input capabilities provided by the <strong>contenteditable</strong> 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.
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
name:'p', // node name
|
||||||
|
type:'block' // node type
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, you can also describe attributes, styles, etc., such as:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
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:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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 `card`through the`schema`rule. They are composed of different attributes, styles or`html` structures. Certain constraints are imposed on nesting.
|
||||||
|
|
||||||
|
## 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](https://github.com/share/sharedb) through `Websocket`, and then use commands to add, delete, modify, and check the data saved in ShareDB.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Out of the box, it provides dozens of rich plug-ins to meet most needs
|
- Out of the box, it provides dozens of rich plug-ins to meet most needs
|
||||||
- High extensibility, in addition to the basic plug-in of `mark`, inline`and`block`type, we also provide`card`component combined with`React`, `Vue` and other front-end libraries to render the plug-in UI
|
- High extensibility, in addition to the basic plug-in of `mark`, inline`, and `block`type, we also provide`card`component combined with`React`, `Vue` and other front-end libraries to render the plug-in UI
|
||||||
- Rich multimedia support, not only supports pictures, audio and video, but also supports insertion of embedded multimedia content
|
- Rich multimedia support, not only supports pictures, audio and video, but also supports insertion of embedded multimedia content
|
||||||
- Support Markdown syntax
|
- Support Markdown syntax
|
||||||
- The engine is written in pure JavaScript and does not rely on any front-end libraries. Plug-ins can be rendered using front-end libraries such as `React` and `Vue`. Easily cope with complex architecture
|
- The engine is written in pure JavaScript and does not rely on any front-end libraries. Plug-ins can be rendered using front-end libraries such as `React` and `Vue`. Easily cope with complex architecture
|
||||||
|
|
|
@ -4,23 +4,111 @@ title: 介绍
|
||||||
|
|
||||||
## 是什么?
|
## 是什么?
|
||||||
|
|
||||||
一个富文本<em>协同</em>编辑器框架,可以使用<em>React</em>和<em>Vue</em>自定义插件
|
一个支持协同编辑的富文本编辑器,可以自由的使用 React、Vue 等前端常用库扩展定义插件。
|
||||||
|
|
||||||
`广告`:[科学上网,方便、快捷的上网冲浪](https://xiyou4you.us/r/?s=18517120) 稳定、可靠,访问 Github 或者其它外网资源很方便。
|
`广告`:[科学上网,方便、快捷的上网冲浪](https://xiyou4you.us/r/?s=18517120) 稳定、可靠,访问 Github 或者其它外网资源很方便。
|
||||||
|
|
||||||
使用浏览器提供的 `contenteditable` 属性让一个 DOM 节点具有可编辑能力。
|
## 基本原理
|
||||||
|
|
||||||
引擎接管了浏览器大部分光标、事件等默认行为。
|
使用浏览器提供的 `contenteditable` 属性让一个 DOM 节点具有可编辑能力:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div contenteditable="true"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
所以它的值看起来像是这样的:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div data-element="root" contenteditable="true">
|
||||||
|
<p>Hello world!</p>
|
||||||
|
<p><br /></p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
当然,有些场景下为了方便操作,也提供了转换为 JSON 类型值的 API:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"div", // 节点名称
|
||||||
|
// 节点所有的属性
|
||||||
|
{
|
||||||
|
"data-element": "root",
|
||||||
|
"contenteditable": "true"
|
||||||
|
},
|
||||||
|
// 子节点1
|
||||||
|
[
|
||||||
|
// 子节点名称
|
||||||
|
"p",
|
||||||
|
// 子节点属性
|
||||||
|
{},
|
||||||
|
// 字节点的子节点
|
||||||
|
"Hello world!"
|
||||||
|
],
|
||||||
|
// 子节点2
|
||||||
|
["p", {}, ["br", {}]]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
编辑器依赖 <strong>contenteditable</strong> 属性提供的输入能力以及光标的控制能力。因此,它拥有所有的默认浏览器行为,但是浏览器的默认行为在不同的浏览器厂商实现下存在不同的处理方式,所以我们其大部分默认行为进行了拦截并进行自定义的处理。
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
比如输入的过程中 `beforeinput` `input`, 删除、回车以及快捷键涉及到的 `mousedown` `mouseup` `click` 等事件都会被拦截,并进行自定义的处理。
|
||||||
|
|
||||||
|
在对事件进行接管后,编辑器所做的事情就是管理好基于 `contenteditable` 属性根节点下的所有子节点了,比如插入文本、删除文本、插入图片等等。
|
||||||
|
|
||||||
|
综上所述,编辑中的数据结构是一个 DOM 树结构,所有的操作都是对 DOM 树直接进行操作,不是典型的以数据模型驱动视图渲染的 MVC 模式。
|
||||||
|
|
||||||
|
## 节点约束
|
||||||
|
|
||||||
|
为了更方便的管理节点,降低复杂性。编辑器抽象化了节点属性和功能,制定了 `mark` `inline` `block` `card` 4 种类型节点,他们由不同的属性、样式或 `html` 结构组成,并统一使用 `schema` 对它们进行约束。
|
||||||
|
|
||||||
|
一个简单的 `schema` 看起来像是这样:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
name: 'p', // 节点名称
|
||||||
|
type: 'block' // 节点类型
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
除此之外,还可以描述属性、样式等,比如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
name: 'span', // 节点名称
|
||||||
|
type: 'mark', // 节点类型
|
||||||
|
attributes: {
|
||||||
|
// 节点有一个 style 属性
|
||||||
|
style: {
|
||||||
|
// 必须包含一个color的样式
|
||||||
|
color: {
|
||||||
|
required: true, // 必须包含
|
||||||
|
value: '@color' // 值是一个符合css规范的颜色值,@color 是编辑器内部定义的颜色效验,此处也可以使用方法、正则表达式去判断是否符合需要的规则
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 可选的包含一个 test 属性,他的值可以是任意的,但不是必须的
|
||||||
|
test: '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
下面这几种节点都符合上面的规则:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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` 结构组成,并对它们的嵌套进行了一定的约束。
|
可编辑器区域内的节点通过 `schema` 规则,制定了 `mark` `inline` `block` `card` 4 种组合节点,他们由不同的属性、样式或 `html` 结构组成,并对它们的嵌套进行了一定的约束。
|
||||||
|
|
||||||
通过 `MutationObserver` 监听编辑区域内的 `html` 结构的改变,并生成 `json0` 类型的数据格式与 [ShareDB](https://github.com/share/sharedb) 库进行交互达到协同编辑的需要。
|
## 协同
|
||||||
|
|
||||||
**`Vue2`** 案例 [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
通过 `MutationObserver` 监听编辑区域(contenteditable 根节点)内的 `html` 结构的突变反推 OT。通过`Websocket`与 [ShareDB](https://github.com/share/sharedb) 连接,然后使用命令对 ShareDB 保存的数据进行增、删、改、查。
|
||||||
|
|
||||||
**`Vue3`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
|
||||||
|
|
||||||
**`React`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
|
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,11 @@ The following three plugins are different
|
||||||
|
|
||||||
- `@aomao/plugin-link` link input, text input, using the existing UI of the front-end library is a better choice
|
- `@aomao/plugin-link` link input, text input, using the existing UI of the front-end library is a better choice
|
||||||
|
|
||||||
[React case](https://github.com/yanmao-cc/am-editor/blob/master/docs/demo/engine.tsx)
|
**`Vue2`** example [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
||||||
|
|
||||||
[Vue case](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
**`Vue3`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
||||||
|
|
||||||
|
**`React`** example [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
@ -54,7 +56,8 @@ const EngineDemo = () => {
|
||||||
//Set the editor value
|
//Set the editor value
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//Listen to the editor value change event
|
//Listen to the editor value change event
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -110,7 +113,8 @@ const EngineDemo = () => {
|
||||||
//Set the editor value
|
//Set the editor value
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//Listen to the editor value change event
|
//Listen to the editor value change event
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -170,7 +174,8 @@ const EngineDemo = () => {
|
||||||
//Set the editor value
|
//Set the editor value
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//Listen to the editor value change event
|
//Listen to the editor value change event
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -253,7 +258,8 @@ const EngineDemo = () => {
|
||||||
//Set the editor value
|
//Set the editor value
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//Listen to the editor value change event
|
//Listen to the editor value change event
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -304,7 +310,8 @@ const EngineDemo = () => {
|
||||||
//Set the editor value
|
//Set the editor value
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//Listen to the editor value change event
|
//Listen to the editor value change event
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 快速上手
|
title: 安装
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速上手
|
## 介绍
|
||||||
|
|
||||||
除引擎库纯`javascript`编写外,我们所提供的插件中,小部分插件 UI 比较复杂,使用前端库来渲染 UI 是一项比较轻松的工作。
|
除引擎库纯`javascript`编写外,我们所提供的插件中,小部分插件 UI 比较复杂,使用前端库来渲染 UI 是一项比较轻松的工作。
|
||||||
|
|
||||||
|
@ -14,9 +14,11 @@ title: 快速上手
|
||||||
|
|
||||||
- `@aomao/plugin-link` 链接输入、文本输入,使用前端库现有的 UI 是比较好的选择
|
- `@aomao/plugin-link` 链接输入、文本输入,使用前端库现有的 UI 是比较好的选择
|
||||||
|
|
||||||
[React 案例](https://github.com/yanmao-cc/am-editor/blob/master/docs/demo/engine.tsx)
|
**`Vue2`** 案例 [https://github.com/zb201307/am-editor-vue2](https://github.com/zb201307/am-editor-vue2)
|
||||||
|
|
||||||
[Vue 案例](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
**`Vue3`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/vue](https://github.com/yanmao-cc/am-editor/tree/master/examples/vue)
|
||||||
|
|
||||||
|
**`React`** 案例 [https://github.com/yanmao-cc/am-editor/tree/master/examples/react](https://github.com/yanmao-cc/am-editor/tree/master/examples/react)
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
|
@ -54,7 +56,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -110,7 +113,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -170,7 +174,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -253,7 +258,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
@ -304,7 +310,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -308,7 +308,8 @@ const EngineDemo = () => {
|
||||||
//Set the editor value
|
//Set the editor value
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//Listen to the editor value change event
|
//Listen to the editor value change event
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -308,7 +308,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -235,9 +235,9 @@ export const mathOptions: MathOptions = {
|
||||||
|
|
||||||
export const mentionOptions: MentionOptions = {
|
export const mentionOptions: MentionOptions = {
|
||||||
action: '/api/user/search',
|
action: '/api/user/search',
|
||||||
onLoading: (root: NodeInterface) => {
|
// onLoading: (root: NodeInterface) => {
|
||||||
return ReactDOM.render(<Loading />, root.get<HTMLElement>()!);
|
// return ReactDOM.render(<Loading />, root.get<HTMLElement>()!);
|
||||||
},
|
// },
|
||||||
onEmpty: (root: NodeInterface) => {
|
onEmpty: (root: NodeInterface) => {
|
||||||
return ReactDOM.render(<Empty />, root.get<HTMLElement>()!);
|
return ReactDOM.render(<Empty />, root.get<HTMLElement>()!);
|
||||||
},
|
},
|
||||||
|
|
|
@ -108,14 +108,15 @@ const EditorComponent: React.FC<EditorProps> = ({
|
||||||
},
|
},
|
||||||
// 编辑器值改变事件
|
// 编辑器值改变事件
|
||||||
onChange: useCallback(
|
onChange: useCallback(
|
||||||
(value: string, trigger: 'remote' | 'local' | 'both') => {
|
(trigger: 'remote' | 'local' | 'both') => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
setValue(value);
|
|
||||||
//自动保存,非远程更改,触发保存
|
//自动保存,非远程更改,触发保存
|
||||||
if (trigger !== 'remote') autoSave();
|
if (trigger !== 'remote') autoSave();
|
||||||
if (props.onChange) props.onChange(value, trigger);
|
if (props.onChange) props.onChange(trigger);
|
||||||
|
const value = engine.current?.getValue();
|
||||||
// 获取编辑器的值
|
// 获取编辑器的值
|
||||||
console.log(`value ${trigger} update:`, value);
|
console.log(`value ${trigger} update:`, value);
|
||||||
|
setValue(value || '');
|
||||||
// 获取当前所有at插件中的名单
|
// 获取当前所有at插件中的名单
|
||||||
// console.log(
|
// console.log(
|
||||||
// 'mention:',
|
// 'mention:',
|
||||||
|
|
|
@ -12,7 +12,7 @@ import 'antd/es/modal/style';
|
||||||
|
|
||||||
export type EngineProps = EngineOptions & {
|
export type EngineProps = EngineOptions & {
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onChange?: (content: string, trigger: 'remote' | 'local' | 'both') => void;
|
onChange?: (trigger: 'remote' | 'local' | 'both') => void;
|
||||||
ref?: React.Ref<EngineInterface | null>;
|
ref?: React.Ref<EngineInterface | null>;
|
||||||
};
|
};
|
||||||
message.config({
|
message.config({
|
||||||
|
@ -57,8 +57,8 @@ const EngineComponent: React.FC<EngineProps> = forwardRef<
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const change = (value: string, trigger: 'remote' | 'local' | 'both') => {
|
const change = (trigger: 'remote' | 'local' | 'both') => {
|
||||||
if (onChange) onChange(value, trigger);
|
if (onChange) onChange(trigger);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -205,8 +205,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听编辑器值改变事件
|
// 监听编辑器值改变事件
|
||||||
engineInstance.on('change', value => {
|
engineInstance.on('change', () => {
|
||||||
console.log('value', value);
|
console.log('value', engineInstance.getValue());
|
||||||
console.log('html:', engineInstance.getHtml());
|
console.log('html:', engineInstance.getHtml());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,8 @@ const EngineDemo = () => {
|
||||||
//设置编辑器值
|
//设置编辑器值
|
||||||
engine.setValue(content);
|
engine.setValue(content);
|
||||||
//监听编辑器值改变事件
|
//监听编辑器值改变事件
|
||||||
engine.on('change', (value) => {
|
engine.on('change', () => {
|
||||||
|
const value = engine.getValue();
|
||||||
setContent(value);
|
setContent(value);
|
||||||
console.log(`value:${value}`);
|
console.log(`value:${value}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,6 +55,7 @@ abstract class CardEntry<T extends CardValue = CardValue>
|
||||||
static readonly lazyRender: boolean = false;
|
static readonly lazyRender: boolean = false;
|
||||||
private defaultMaximize: MaximizeInterface;
|
private defaultMaximize: MaximizeInterface;
|
||||||
isMaximize: boolean = false;
|
isMaximize: boolean = false;
|
||||||
|
private _id: string;
|
||||||
|
|
||||||
get isEditable() {
|
get isEditable() {
|
||||||
return this.contenteditable.length > 0;
|
return this.contenteditable.length > 0;
|
||||||
|
@ -81,6 +82,7 @@ abstract class CardEntry<T extends CardValue = CardValue>
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
|
if (this._id) return this._id;
|
||||||
const value = this.getValue();
|
const value = this.getValue();
|
||||||
return typeof value === 'object' ? value?.id || '' : '';
|
return typeof value === 'object' ? value?.id || '' : '';
|
||||||
}
|
}
|
||||||
|
@ -122,6 +124,7 @@ abstract class CardEntry<T extends CardValue = CardValue>
|
||||||
if (typeof value === 'string') value = decodeCardValue(value);
|
if (typeof value === 'string') value = decodeCardValue(value);
|
||||||
value = value || ({} as T);
|
value = value || ({} as T);
|
||||||
value.id = this.getId(value.id);
|
value.id = this.getId(value.id);
|
||||||
|
this._id = value.id;
|
||||||
value.type = type;
|
value.type = type;
|
||||||
this.setValue(value);
|
this.setValue(value);
|
||||||
this.defaultMaximize = new Maximize(this.editor, this);
|
this.defaultMaximize = new Maximize(this.editor, this);
|
||||||
|
|
|
@ -26,7 +26,6 @@ class ChangeEvent implements ChangeEventInterface {
|
||||||
private dragoverHelper: DragoverHelper;
|
private dragoverHelper: DragoverHelper;
|
||||||
private options: ChangeEventOptions;
|
private options: ChangeEventOptions;
|
||||||
private keydownRange: RangeInterface | null = null;
|
private keydownRange: RangeInterface | null = null;
|
||||||
private inputAtBeforeText = '';
|
|
||||||
|
|
||||||
constructor(engine: EngineInterface, options: ChangeEventOptions = {}) {
|
constructor(engine: EngineInterface, options: ChangeEventOptions = {}) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
|
@ -106,21 +105,10 @@ class ChangeEvent implements ChangeEventInterface {
|
||||||
// safari 组合输入法会直接插入@字符,这里统一全部拦截输入@字符的时候再去触发@事件
|
// safari 组合输入法会直接插入@字符,这里统一全部拦截输入@字符的时候再去触发@事件
|
||||||
const { change, card, node, block, list } = this.engine;
|
const { change, card, node, block, list } = this.engine;
|
||||||
if (event.data === '@') {
|
if (event.data === '@') {
|
||||||
const { startNode } = change.range.get();
|
|
||||||
// 搜狗这种输入法,中文状态下会触发两次,第二次的text是单独的@节点
|
|
||||||
const text = startNode.text();
|
|
||||||
let isHandleAt = false;
|
|
||||||
if (startNode.isText()) {
|
|
||||||
if (this.inputAtBeforeText !== text && text === '@') {
|
|
||||||
event.preventDefault();
|
|
||||||
isHandleAt = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果没有要对 @ 字符处理的就不拦截
|
// 如果没有要对 @ 字符处理的就不拦截
|
||||||
const result = !isHandleAt
|
const result = this.engine.trigger('keydown:at', event);
|
||||||
? this.engine.trigger('keydown:at', event)
|
|
||||||
: true;
|
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
|
this.engine.ot.submitMutationCache();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,21 +286,8 @@ class ChangeEvent implements ChangeEventInterface {
|
||||||
return callback(e);
|
return callback(e);
|
||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
this.onContainer('keydown', (event: KeyboardEvent) => {
|
this.onContainer('keydown', () => {
|
||||||
const range = Range.from(this.engine);
|
const range = Range.from(this.engine);
|
||||||
if (
|
|
||||||
range &&
|
|
||||||
(event.key === '@' ||
|
|
||||||
(event.shiftKey &&
|
|
||||||
event.keyCode === 229 &&
|
|
||||||
event.code === 'Digit2'))
|
|
||||||
) {
|
|
||||||
const { startNode } = range;
|
|
||||||
if (startNode.isText()) {
|
|
||||||
const text = startNode.text();
|
|
||||||
this.inputAtBeforeText = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.keydownRange = range;
|
this.keydownRange = range;
|
||||||
});
|
});
|
||||||
// 补齐通过键盘选中的情况
|
// 补齐通过键盘选中的情况
|
||||||
|
|
|
@ -24,7 +24,7 @@ class ChangeModel implements ChangeInterface {
|
||||||
private changeTimer: NodeJS.Timeout | null = null;
|
private changeTimer: NodeJS.Timeout | null = null;
|
||||||
event: ChangeEvent;
|
event: ChangeEvent;
|
||||||
valueCached: string | null = null;
|
valueCached: string | null = null;
|
||||||
onChange: (value: string, trigger: 'remote' | 'local' | 'both') => void;
|
onChange: (trigger: 'remote' | 'local' | 'both') => void;
|
||||||
onRealtimeChange: (trigger: 'remote' | 'local') => void;
|
onRealtimeChange: (trigger: 'remote' | 'local') => void;
|
||||||
onSelect: (range?: RangeInterface) => void;
|
onSelect: (range?: RangeInterface) => void;
|
||||||
onSetValue: () => void;
|
onSetValue: () => void;
|
||||||
|
@ -72,20 +72,14 @@ class ChangeModel implements ChangeInterface {
|
||||||
private _change() {
|
private _change() {
|
||||||
if (!this.isComposing()) {
|
if (!this.isComposing()) {
|
||||||
this.engine.card.gc();
|
this.engine.card.gc();
|
||||||
const value = this.getValue({
|
const trigger =
|
||||||
ignoreCursor: true,
|
this.changeTrigger.length === 2
|
||||||
});
|
? 'both'
|
||||||
if (!this.valueCached || value !== this.valueCached) {
|
: this.changeTrigger[0] === 'remote'
|
||||||
const trigger =
|
? 'remote'
|
||||||
this.changeTrigger.length === 2
|
: 'local';
|
||||||
? 'both'
|
this.onChange(trigger);
|
||||||
: this.changeTrigger[0] === 'remote'
|
this.changeTrigger = [];
|
||||||
? 'remote'
|
|
||||||
: 'local';
|
|
||||||
this.onChange(value, trigger);
|
|
||||||
this.changeTrigger = [];
|
|
||||||
this.valueCached = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,7 @@ import Language from './language';
|
||||||
import {
|
import {
|
||||||
BlockModelInterface,
|
BlockModelInterface,
|
||||||
CardEntry,
|
CardEntry,
|
||||||
CardInterface,
|
|
||||||
CardModelInterface,
|
CardModelInterface,
|
||||||
CardValue,
|
|
||||||
ClipboardInterface,
|
ClipboardInterface,
|
||||||
CommandInterface,
|
CommandInterface,
|
||||||
ConversionInterface,
|
ConversionInterface,
|
||||||
|
|
|
@ -86,8 +86,7 @@ class Engine<T extends EngineOptions = EngineOptions>
|
||||||
this._container.init();
|
this._container.init();
|
||||||
// 编辑器改变时
|
// 编辑器改变时
|
||||||
this.change = new Change(this, {
|
this.change = new Change(this, {
|
||||||
onChange: (value, trigger) =>
|
onChange: (trigger) => this.trigger('change', trigger),
|
||||||
this.trigger('change', value, trigger),
|
|
||||||
onSelect: () => this.trigger('select'),
|
onSelect: () => this.trigger('select'),
|
||||||
onRealtimeChange: (trigger) => {
|
onRealtimeChange: (trigger) => {
|
||||||
if (this.isEmpty()) {
|
if (this.isEmpty()) {
|
||||||
|
|
|
@ -103,7 +103,7 @@ export type ChangeOptions = {
|
||||||
/**
|
/**
|
||||||
* 值改变事件
|
* 值改变事件
|
||||||
*/
|
*/
|
||||||
onChange?: (value: string, trigger: 'remote' | 'local' | 'both') => void;
|
onChange?: (trigger: 'remote' | 'local' | 'both') => void;
|
||||||
/**
|
/**
|
||||||
* 光标选择事件
|
* 光标选择事件
|
||||||
*/
|
*/
|
||||||
|
@ -185,7 +185,7 @@ export interface ChangeInterface {
|
||||||
/**
|
/**
|
||||||
* 编辑器值改变触发
|
* 编辑器值改变触发
|
||||||
*/
|
*/
|
||||||
onChange: (value: string, trigger: 'remote' | 'local' | 'both') => void;
|
onChange: (trigger: 'remote' | 'local' | 'both') => void;
|
||||||
/**
|
/**
|
||||||
* 编辑器中光标改变触发
|
* 编辑器中光标改变触发
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default class<T extends MarkRangeOptions> extends MarkPlugin<T> {
|
||||||
|
|
||||||
if (isEngine(this.editor)) {
|
if (isEngine(this.editor)) {
|
||||||
const { change } = this.editor;
|
const { change } = this.editor;
|
||||||
this.editor.on('change', (_, trigger) => {
|
this.editor.on('change', (trigger) => {
|
||||||
this.triggerChange(trigger !== 'local');
|
this.triggerChange(trigger !== 'local');
|
||||||
});
|
});
|
||||||
this.editor.on('select', this.onSelectionChange);
|
this.editor.on('select', this.onSelectionChange);
|
||||||
|
|
|
@ -210,15 +210,36 @@ class CollapseComponent implements CollapseComponentInterface {
|
||||||
return this.root?.find('.data-mention-component-body');
|
return this.root?.find('.data-mention-component-body');
|
||||||
}
|
}
|
||||||
|
|
||||||
render(target: NodeInterface, data: Array<MentionItem> | true) {
|
createRoot() {
|
||||||
this.remove();
|
|
||||||
this.root = $(
|
this.root = $(
|
||||||
`<div class="data-mention-component-list" ${DATA_ELEMENT}="${UI}"><div class="data-mention-component-body"></div></div>`,
|
`<div class="data-mention-component-list" ${DATA_ELEMENT}="${UI}"><div class="data-mention-component-body"></div></div>`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRootEmpty() {
|
||||||
|
const body = this.getBody();
|
||||||
|
const children = body?.children();
|
||||||
|
if (
|
||||||
|
body &&
|
||||||
|
body.length > 0 &&
|
||||||
|
(children?.length === 0 ||
|
||||||
|
(children?.length === 1 &&
|
||||||
|
children.eq(0)?.hasClass('data-scrollbar')))
|
||||||
|
) {
|
||||||
|
this.root?.addClass('data-mention-component-empty');
|
||||||
|
} else {
|
||||||
|
this.root?.removeClass('data-mention-component-empty');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(target: NodeInterface, data: Array<MentionItem> | true) {
|
||||||
|
if (!this.root) this.createRoot();
|
||||||
|
if (!this.root) return;
|
||||||
|
|
||||||
this.target = target;
|
this.target = target;
|
||||||
|
|
||||||
let body = this.getBody();
|
let body = this.getBody();
|
||||||
|
|
||||||
let result = null;
|
let result = null;
|
||||||
const options = this.getPluginOptions();
|
const options = this.getPluginOptions();
|
||||||
if (typeof data === 'boolean' && data === true) {
|
if (typeof data === 'boolean' && data === true) {
|
||||||
|
@ -257,8 +278,14 @@ class CollapseComponent implements CollapseComponentInterface {
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.#scrollbar?.refresh();
|
this.#scrollbar?.refresh();
|
||||||
});
|
});
|
||||||
|
this.renderRootEmpty();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
if (!body || body.length === 0) {
|
||||||
|
this.createRoot();
|
||||||
|
body = this.getBody();
|
||||||
|
}
|
||||||
|
body?.empty();
|
||||||
data.forEach((data) => {
|
data.forEach((data) => {
|
||||||
const triggerResult = this.engine.trigger(
|
const triggerResult = this.engine.trigger(
|
||||||
'mention:render-item',
|
'mention:render-item',
|
||||||
|
@ -267,8 +294,8 @@ class CollapseComponent implements CollapseComponentInterface {
|
||||||
);
|
);
|
||||||
const result = triggerResult
|
const result = triggerResult
|
||||||
? triggerResult
|
? triggerResult
|
||||||
: options?.renderItem
|
: options?.onRenderItem
|
||||||
? options.renderItem(data, this.root!)
|
? options.onRenderItem(data, this.root!)
|
||||||
: this.renderTemplate(data);
|
: this.renderTemplate(data);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
|
@ -276,6 +303,7 @@ class CollapseComponent implements CollapseComponentInterface {
|
||||||
});
|
});
|
||||||
this.select(0);
|
this.select(0);
|
||||||
}
|
}
|
||||||
|
this.renderRootEmpty();
|
||||||
if (body) this.#scrollbar = new Scrollbar(body, false, true, false);
|
if (body) this.#scrollbar = new Scrollbar(body, false, true, false);
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.#scrollbar?.refresh();
|
this.#scrollbar?.refresh();
|
||||||
|
|
|
@ -25,6 +25,12 @@
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-mention-component-empty {
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.data-mention-component-body {
|
.data-mention-component-body {
|
||||||
max-height: calc(40vh);
|
max-height: calc(40vh);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
UI,
|
UI,
|
||||||
SelectStyleType,
|
SelectStyleType,
|
||||||
CardValue,
|
CardValue,
|
||||||
|
unescape,
|
||||||
AjaxInterface,
|
AjaxInterface,
|
||||||
} from '@aomao/engine';
|
} from '@aomao/engine';
|
||||||
import CollapseComponent, { CollapseComponentInterface } from './collapse';
|
import CollapseComponent, { CollapseComponentInterface } from './collapse';
|
||||||
|
@ -120,7 +121,15 @@ class Mention<T extends MentionValue = MentionValue> extends Card<T> {
|
||||||
card.removeNode(this);
|
card.removeNode(this);
|
||||||
this.editor.trigger('mention:insert', component);
|
this.editor.trigger('mention:insert', component);
|
||||||
if (options?.onInsert) options.onInsert(component);
|
if (options?.onInsert) options.onInsert(component);
|
||||||
card.focus(component, false);
|
if (isEngine(this.editor)) {
|
||||||
|
const { change } = this.editor;
|
||||||
|
const range = change.range.get().cloneRange();
|
||||||
|
range.setStartAfter(component.root.get()!);
|
||||||
|
range.collapse(true);
|
||||||
|
change.range.select(range);
|
||||||
|
} else {
|
||||||
|
card.focus(component, false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -344,7 +353,9 @@ class Mention<T extends MentionValue = MentionValue> extends Card<T> {
|
||||||
this.resetPlaceHolder();
|
this.resetPlaceHolder();
|
||||||
// 在 Windows 上使用中文输入法,在 keydown 事件里无法阻止用户的输入,所以在这里删除用户的输入
|
// 在 Windows 上使用中文输入法,在 keydown 事件里无法阻止用户的输入,所以在这里删除用户的输入
|
||||||
if (Date.now() - renderTime < 200) {
|
if (Date.now() - renderTime < 200) {
|
||||||
const textNode = this.#keyword?.first();
|
const textNode = this.#keyword
|
||||||
|
?.allChildren()
|
||||||
|
.find((child) => child.isText());
|
||||||
if (
|
if (
|
||||||
textNode &&
|
textNode &&
|
||||||
textNode.isText() &&
|
textNode.isText() &&
|
||||||
|
@ -370,12 +381,6 @@ class Mention<T extends MentionValue = MentionValue> extends Card<T> {
|
||||||
selection?.addRange(range.toRange());
|
selection?.addRange(range.toRange());
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
this.component?.render(
|
|
||||||
this.root,
|
|
||||||
this.editor.trigger('mention:default') ||
|
|
||||||
options?.defaultData ||
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
!(options?.defaultData
|
!(options?.defaultData
|
||||||
? options?.defaultData
|
? options?.defaultData
|
||||||
|
@ -384,6 +389,13 @@ class Mention<T extends MentionValue = MentionValue> extends Card<T> {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.handleInput();
|
this.handleInput();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
} else {
|
||||||
|
this.component?.render(
|
||||||
|
this.root,
|
||||||
|
this.editor.trigger('mention:default') ||
|
||||||
|
options?.defaultData ||
|
||||||
|
[],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 可编辑下,展示模式
|
// 可编辑下,展示模式
|
||||||
|
|
|
@ -47,12 +47,14 @@ class MentionPlugin<
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
private renderTime = Date.now();
|
||||||
onAt(event: KeyboardEvent) {
|
onAt(event: KeyboardEvent) {
|
||||||
if (!isEngine(this.editor)) return;
|
if (!isEngine(this.editor)) return;
|
||||||
|
if (Date.now() - this.renderTime < 200) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const { change } = this.editor;
|
const { change } = this.editor;
|
||||||
let range = change.range.get();
|
let range = change.range.get();
|
||||||
|
|
||||||
// 空格触发
|
// 空格触发
|
||||||
if (this.options.spaceTrigger) {
|
if (this.options.spaceTrigger) {
|
||||||
const selection = range.createSelection();
|
const selection = range.createSelection();
|
||||||
|
@ -82,6 +84,7 @@ class MentionPlugin<
|
||||||
range.collapse(false);
|
range.collapse(false);
|
||||||
change.range.select(range);
|
change.range.select(range);
|
||||||
}
|
}
|
||||||
|
this.renderTime = Date.now();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,12 @@ import {
|
||||||
NodeInterface,
|
NodeInterface,
|
||||||
Plugin,
|
Plugin,
|
||||||
SchemaBlock,
|
SchemaBlock,
|
||||||
PluginOptions,
|
|
||||||
SchemaInterface,
|
SchemaInterface,
|
||||||
getDocument,
|
getDocument,
|
||||||
Parser,
|
|
||||||
READY_CARD_KEY,
|
READY_CARD_KEY,
|
||||||
decodeCardValue,
|
decodeCardValue,
|
||||||
CARD_VALUE_KEY,
|
CARD_VALUE_KEY,
|
||||||
|
CARD_SELECTOR,
|
||||||
} from '@aomao/engine';
|
} from '@aomao/engine';
|
||||||
import TableComponent, { Template, Helper } from './component';
|
import TableComponent, { Template, Helper } from './component';
|
||||||
import locales from './locale';
|
import locales from './locale';
|
||||||
|
@ -378,10 +377,9 @@ class Table<T extends TableOptions = TableOptions> extends Plugin<T> {
|
||||||
`[${CARD_KEY}="${TableComponent.cardName}"],[${READY_CARD_KEY}="${TableComponent.cardName}"]`,
|
`[${CARD_KEY}="${TableComponent.cardName}"],[${READY_CARD_KEY}="${TableComponent.cardName}"]`,
|
||||||
).each((tableNode) => {
|
).each((tableNode) => {
|
||||||
const node = $(tableNode);
|
const node = $(tableNode);
|
||||||
const card = this.editor.card.find<TableValue>(node);
|
const value = decodeCardValue<TableValue>(
|
||||||
const value =
|
node.attributes(CARD_VALUE_KEY),
|
||||||
card?.getValue() ||
|
);
|
||||||
decodeCardValue(node.attributes(CARD_VALUE_KEY));
|
|
||||||
if (value && value.html) {
|
if (value && value.html) {
|
||||||
let table = node.find('table');
|
let table = node.find('table');
|
||||||
if (table.length === 0) {
|
if (table.length === 0) {
|
||||||
|
@ -391,7 +389,10 @@ class Table<T extends TableOptions = TableOptions> extends Plugin<T> {
|
||||||
node.remove();
|
node.remove();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
table = $(new Parser(table, this.editor).toHTML());
|
const cards = table.find(CARD_SELECTOR).toArray();
|
||||||
|
cards.forEach((componentNode) => {
|
||||||
|
this.editor.trigger('parse:html', componentNode);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const width = table.attributes('width') || table.css('width');
|
const width = table.attributes('width') || table.css('width');
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
max-width: 16px !important;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -204,7 +204,7 @@ class VideoComponent<T extends VideoValue = VideoValue> extends Card<T> {
|
||||||
|
|
||||||
const url = sanitizeUrl(this.onBeforeRender('query', value.url));
|
const url = sanitizeUrl(this.onBeforeRender('query', value.url));
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.preload = 'none';
|
video.preload = 'metadata';
|
||||||
video.setAttribute('src', url);
|
video.setAttribute('src', url);
|
||||||
video.setAttribute('webkit-playsinline', 'webkit-playsinline');
|
video.setAttribute('webkit-playsinline', 'webkit-playsinline');
|
||||||
video.setAttribute('playsinline', 'playsinline');
|
video.setAttribute('playsinline', 'playsinline');
|
||||||
|
@ -222,7 +222,20 @@ class VideoComponent<T extends VideoValue = VideoValue> extends Card<T> {
|
||||||
video.oncontextmenu = function () {
|
video.oncontextmenu = function () {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
if (!value.naturalWidth) {
|
||||||
|
value.naturalWidth = video.videoWidth || this.video?.width();
|
||||||
|
this.setValue({
|
||||||
|
naturalWidth: value.naturalWidth,
|
||||||
|
} as T);
|
||||||
|
}
|
||||||
|
if (value.naturalWidth && !value.naturalHeight) {
|
||||||
|
this.rate =
|
||||||
|
(video.videoHeight || this.video?.height() || 1) /
|
||||||
|
value.naturalWidth;
|
||||||
|
}
|
||||||
|
this.resetSize();
|
||||||
|
};
|
||||||
this.video = $(video);
|
this.video = $(video);
|
||||||
this.title = this.container?.find('.data-video-title');
|
this.title = this.container?.find('.data-video-title');
|
||||||
this.resetSize();
|
this.resetSize();
|
||||||
|
@ -386,11 +399,15 @@ class VideoComponent<T extends VideoValue = VideoValue> extends Card<T> {
|
||||||
initResizer() {
|
initResizer() {
|
||||||
const value = this.getValue();
|
const value = this.getValue();
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const { naturalHeight, naturalWidth, status } = value;
|
let { naturalHeight, naturalWidth, status } = value;
|
||||||
if (!naturalHeight || !naturalWidth || status !== 'done') return;
|
if (!naturalWidth || status !== 'done') return;
|
||||||
const { width, height, cover } = value;
|
const { width, height, cover } = value;
|
||||||
this.maxWidth = this.getMaxWidth();
|
this.maxWidth = this.getMaxWidth();
|
||||||
this.rate = naturalHeight / naturalWidth;
|
if (!naturalHeight) {
|
||||||
|
naturalHeight = Math.round(this.rate * naturalWidth);
|
||||||
|
} else {
|
||||||
|
this.rate = naturalHeight / naturalWidth;
|
||||||
|
}
|
||||||
window.removeEventListener('resize', this.onWindowResize);
|
window.removeEventListener('resize', this.onWindowResize);
|
||||||
window.addEventListener('resize', this.onWindowResize);
|
window.addEventListener('resize', this.onWindowResize);
|
||||||
// 拖动调整视频大小
|
// 拖动调整视频大小
|
||||||
|
|
|
@ -297,6 +297,16 @@ export default class<
|
||||||
...data,
|
...data,
|
||||||
cover: customizeResult.data.cover,
|
cover: customizeResult.data.cover,
|
||||||
};
|
};
|
||||||
|
if (customizeResult.data.width !== undefined)
|
||||||
|
data = {
|
||||||
|
...data,
|
||||||
|
width: customizeResult.data.width,
|
||||||
|
};
|
||||||
|
if (customizeResult.data.height !== undefined)
|
||||||
|
data = {
|
||||||
|
...data,
|
||||||
|
height: customizeResult.data.height,
|
||||||
|
};
|
||||||
result.data = { ...data };
|
result.data = { ...data };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue