bridgerte 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +936 -855
- package/dist/bridge.cjs.map +1 -1
- package/dist/bridge.d.ts +8 -1
- package/dist/bridge.js.map +1 -1
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.ts +8 -1
- package/dist/core.js +44 -54
- package/dist/core.js.map +1 -1
- package/dist/dom.cjs +1 -1
- package/dist/dom.d.ts +8 -1
- package/dist/dom.js +6 -5
- package/dist/index-5d8qaSP5.cjs +36 -0
- package/dist/index-5d8qaSP5.cjs.map +1 -0
- package/dist/{index-KRuLtGv9.js → index-BzpD9bI2.js} +1800 -1661
- package/dist/index-BzpD9bI2.js.map +1 -0
- package/dist/index-C26bdJ7I.js +319 -0
- package/dist/index-C26bdJ7I.js.map +1 -0
- package/dist/index-CuNKUHed.js.map +1 -1
- package/dist/index-DF8OhKI4.cjs +2 -0
- package/dist/index-DF8OhKI4.cjs.map +1 -0
- package/dist/index-DKalD8mx.cjs +4 -0
- package/dist/index-DKalD8mx.cjs.map +1 -0
- package/dist/index-GaS65GL0.cjs.map +1 -1
- package/dist/index-sbZNOcCB.js +54 -0
- package/dist/index-sbZNOcCB.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +24 -21
- package/dist/index.js.map +1 -1
- package/dist/native-spec.cjs +1 -1
- package/dist/native-spec.cjs.map +1 -1
- package/dist/native-spec.d.ts +8 -1
- package/dist/native-spec.js +50 -42
- package/dist/native-spec.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/webview.cjs +1 -1
- package/dist/webview.d.ts +8 -1
- package/dist/webview.js +1 -1
- package/package.json +2 -1
- package/dist/index-CkgUKPh3.cjs +0 -3
- package/dist/index-CkgUKPh3.cjs.map +0 -1
- package/dist/index-CqOH1_5N.cjs +0 -2
- package/dist/index-CqOH1_5N.cjs.map +0 -1
- package/dist/index-DRWIM218.js +0 -262
- package/dist/index-DRWIM218.js.map +0 -1
- package/dist/index-KRuLtGv9.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,554 +1,611 @@
|
|
|
1
|
-
# bridgerte
|
|
1
|
+
# bridgerte
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
`bridgerte` 是 BridgeRTE 对外发布给业务项目使用的唯一 npm 包。它面向 Web、PC、H5、
|
|
6
|
+
React Native WebView 和 Flutter WebView 场景,内置 Lexical 编辑器、DOM toolbar/tabbar、
|
|
7
|
+
参数面板、上传入口、WebView bridge 协议和跨端菜单 schema。
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
业务项目通常只需要安装 `bridgerte`,再按使用场景从 `bridgerte/dom`、`bridgerte/core`、
|
|
10
|
+
`bridgerte/native-spec`、`bridgerte/bridge` 或 `bridgerte/webview` 导入能力,不需要额外安装
|
|
11
|
+
Lexical 相关包,也不需要直接依赖仓库里的 `@bridgerte/*` 内部包。
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
业务项目只需要安装 `bridgerte`,不需要额外安装 Lexical 相关包。
|
|
7
|
-
|
|
8
|
-
它提供三类接入方式:
|
|
13
|
+
这个包适合三类接入方式:
|
|
9
14
|
|
|
10
15
|
- DOM 编辑器:直接在 Web、PC、H5 页面里创建富文本编辑器。
|
|
11
16
|
- 编辑器和菜单分离:editor 负责内容,toolbar/tabbar 由业务按布局单独挂载。
|
|
12
17
|
- WebView / Native:WebView 内运行编辑器,RN/Flutter 原生侧自己渲染菜单并通过 bridge 发命令。
|
|
13
|
-
|
|
14
|
-
## 安装
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
pnpm add bridgerte
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
使用 npm:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install bridgerte
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
DOM 编辑器必须显式导入样式:
|
|
27
|
-
|
|
28
|
-
```ts
|
|
29
|
-
import 'bridgerte/style.css';
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## 推荐导入方式
|
|
33
|
-
|
|
34
|
-
推荐按能力从 subpath 导入,方便业务打包器按使用边界 tree-shaking:
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import {
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add bridgerte
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
使用 npm:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install bridgerte
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
DOM 编辑器必须显式导入样式:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import 'bridgerte/style.css';
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 推荐导入方式
|
|
38
|
+
|
|
39
|
+
推荐按能力从 subpath 导入,方便业务打包器按使用边界 tree-shaking:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import {
|
|
43
|
+
createRichTextEditor,
|
|
44
|
+
createRichTextToolbar,
|
|
45
|
+
hasMeaningfulHtmlContent
|
|
46
|
+
} from 'bridgerte/dom';
|
|
38
47
|
import { createWebViewBridgeRuntime } from 'bridgerte/webview';
|
|
39
|
-
import { BRIDGERTE_CONTENT_VERSION } from 'bridgerte/core';
|
|
48
|
+
import { BRIDGERTE_CONTENT_VERSION, isEditorContentEmpty } from 'bridgerte/core';
|
|
40
49
|
import { isBridgeMessage } from 'bridgerte/bridge';
|
|
41
50
|
import { defaultMenuSchema, resolveToolbarMenu } from 'bridgerte/native-spec';
|
|
42
51
|
import 'bridgerte/style.css';
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
入口说明:
|
|
46
|
-
|
|
47
|
-
- `bridgerte/dom`:DOM 编辑器、独立 toolbar/tabbar、WebView runtime 的 DOM 实现。
|
|
48
|
-
- `bridgerte/webview`:WebView 页面内的 bridge runtime。
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
入口说明:
|
|
55
|
+
|
|
56
|
+
- `bridgerte/dom`:DOM 编辑器、独立 toolbar/tabbar、WebView runtime 的 DOM 实现。
|
|
57
|
+
- `bridgerte/webview`:WebView 页面内的 bridge runtime。
|
|
49
58
|
- `bridgerte/core`:内容模型、命令、上传、菜单、参数面板和 `EditorAPI` 类型。
|
|
50
|
-
- `bridgerte/bridge`:WebView 双向消息类型、默认事件节流配置和消息判断工具。
|
|
51
|
-
- `bridgerte/native-spec`:RN/Flutter 原生菜单 schema、toolbar 解析和命令状态匹配工具。
|
|
52
|
-
- `bridgerte/style.css`:DOM 默认样式。
|
|
53
|
-
- `bridgerte`:聚合入口,适合迁移期或小型项目;生产示例优先使用 subpath。
|
|
54
|
-
|
|
55
|
-
## 快速开始
|
|
56
|
-
|
|
57
|
-
页面准备两个容器:一个给 toolbar/tabbar,一个给编辑器内容区。`createRichTextEditor()`
|
|
58
|
-
只创建 editor;菜单必须用 `createRichTextToolbar()` 单独挂载,并绑定同一个 `EditorAPI`。
|
|
59
|
-
|
|
60
|
-
```html
|
|
61
|
-
<div id="toolbar"></div>
|
|
62
|
-
<div id="editor"></div>
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
import { createRichTextEditor, createRichTextToolbar } from 'bridgerte/dom';
|
|
67
|
-
import 'bridgerte/style.css';
|
|
68
|
-
|
|
69
|
-
const editorContainer = document.querySelector<HTMLElement>('#editor');
|
|
70
|
-
const toolbarContainer = document.querySelector<HTMLElement>('#toolbar');
|
|
71
|
-
|
|
72
|
-
if (!editorContainer) throw new Error('editor container not found');
|
|
73
|
-
if (!toolbarContainer) throw new Error('toolbar container not found');
|
|
74
|
-
|
|
75
|
-
const editor = createRichTextEditor(editorContainer, {
|
|
76
|
-
placeholder: '开始输入',
|
|
77
|
-
onContentChange(change) {
|
|
78
|
-
counter.textContent = `${change.plainTextLength}/${change.maxLength ?? '∞'}`;
|
|
79
|
-
saveButton.disabled = !change.dirty;
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const toolbar = createRichTextToolbar(toolbarContainer, {
|
|
84
|
-
editor,
|
|
85
|
-
placement: 'top'
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
toolbar.update();
|
|
89
|
-
|
|
90
|
-
window.addEventListener('beforeunload', () => {
|
|
91
|
-
toolbar.destroy();
|
|
92
|
-
editor.destroy();
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`placement: 'top' | 'bottom'` 只影响菜单语义、默认样式状态和无障碍命名;DOM 放在哪里、
|
|
97
|
-
是否吸顶、吸底或跟随键盘,由业务自己的布局决定。销毁时建议先销毁 toolbar,再销毁 editor。
|
|
98
|
-
|
|
99
|
-
WebView / RN / Flutter 也是同一心智:WebView 内的 editor 只提供内容和命令能力;原生菜单先读取
|
|
100
|
-
ready payload 中的 `menuSchema`,再订阅后续 `editor.commandStateChange` 自行渲染,并通过
|
|
101
|
-
bridge 发送命令。
|
|
102
|
-
|
|
103
|
-
## 基础配置
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import { createRichTextEditor } from 'bridgerte/dom';
|
|
107
|
-
import type { EditorContent } from 'bridgerte/core';
|
|
108
|
-
|
|
109
|
-
const initialValue: Partial<EditorContent> = {
|
|
110
|
-
html: '<p>Hello BridgeRTE</p>',
|
|
111
|
-
plainText: 'Hello BridgeRTE'
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const editor = createRichTextEditor(container, {
|
|
115
|
-
value: initialValue,
|
|
116
|
-
readonly: false,
|
|
117
|
-
placeholder: '写点什么',
|
|
118
|
-
maxLength: 10000,
|
|
119
|
-
keyboardShortcuts: true,
|
|
120
|
-
onReady(api) {
|
|
121
|
-
api.focus();
|
|
122
|
-
},
|
|
123
|
-
onError(error) {
|
|
124
|
-
reportError(error);
|
|
125
|
-
},
|
|
126
|
-
onFocus() {
|
|
127
|
-
console.log('focus');
|
|
128
|
-
},
|
|
129
|
-
onBlur() {
|
|
130
|
-
console.log('blur');
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
常用选项:
|
|
136
|
-
|
|
137
|
-
- `value`:初始内容,传 `Partial<EditorContent>`。
|
|
138
|
-
- `readonly`:只读状态。
|
|
139
|
-
- `placeholder`:空态提示。
|
|
140
|
-
- `maxLength`:最大纯文本长度。
|
|
141
|
-
- `keyboardShortcuts`:是否启用 DOM 基础快捷键,默认关闭。
|
|
142
|
-
- `onReady(api)`:编辑器初始化完成后回传 `EditorAPI`。
|
|
143
|
-
- `onContentChange(change)`:高频轻量内容摘要。
|
|
144
|
-
- `onChange(content)`:兼容旧项目的完整内容回调,大文档不建议逐字依赖。
|
|
145
|
-
- `onError(error)`:运行时错误。
|
|
146
|
-
- `onFocus()` / `onBlur()`:焦点变化。
|
|
147
|
-
|
|
148
|
-
Deprecated 兼容字段:
|
|
149
|
-
|
|
150
|
-
- `toolbarMode`:历史字段,当前在 `createRichTextEditor()` 中是 no-op,不再创建或控制 DOM
|
|
151
|
-
toolbar/tabbar。
|
|
152
|
-
- `toolbarConfig`:历史 editor 字段,当前不再影响 editor 创建。菜单显示结构请传给
|
|
153
|
-
`createRichTextToolbar()`。
|
|
154
|
-
|
|
155
|
-
## 内容读写
|
|
156
|
-
|
|
157
|
-
主动读取完整内容:
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
const content = editor.getContent();
|
|
161
|
-
|
|
162
|
-
console.log(content.version);
|
|
163
|
-
console.log(content.html);
|
|
164
|
-
console.log(content.json);
|
|
165
|
-
console.log(content.plainText);
|
|
166
|
-
console.log(content.assets);
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
写入内容:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
editor.setContent({
|
|
173
|
-
html: '<h1>标题</h1><p>正文</p>',
|
|
174
|
-
plainText: '标题\n正文'
|
|
175
|
-
});
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
高频变化使用轻量摘要:
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
createRichTextEditor(container, {
|
|
182
|
-
maxLength: 5000,
|
|
183
|
-
onContentChange(change) {
|
|
184
|
-
saveButton.disabled = !change.dirty;
|
|
185
|
-
counter.textContent = `${change.plainTextLength}/${change.maxLength ?? '∞'}`;
|
|
186
|
-
warning.hidden = !change.isOverMaxLength;
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
重要边界:
|
|
192
|
-
|
|
193
|
-
- `onContentChange` 不携带完整 `html`、`json`、`plainText`,适合驱动 dirty、字数和保存按钮。
|
|
194
|
-
- 保存、提交、离开页面确认时,再主动调用 `getContent()`。
|
|
195
|
-
- `onChange(content)` 会回传完整内容,主要用于兼容旧项目;10w 内容场景不要逐字依赖它保存。
|
|
196
|
-
- WebView 高频 `editor.contentChange` 也只应依赖轻量摘要;完整内容通过 `requestContent` 获取。
|
|
197
|
-
|
|
198
|
-
## EditorAPI
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
editor.focus();
|
|
202
|
-
editor.blur();
|
|
203
|
-
editor.setReadonly(true);
|
|
204
|
-
|
|
205
|
-
editor.executeCommand({ type: 'format.bold' });
|
|
206
|
-
|
|
207
|
-
const states = editor.getCommandStates();
|
|
208
|
-
const unsubscribe = editor.subscribeCommandStateChange((nextStates) => {
|
|
209
|
-
renderToolbarState(nextStates);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
unsubscribe();
|
|
213
|
-
editor.destroy();
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
`EditorAPI` 方法:
|
|
217
|
-
|
|
59
|
+
- `bridgerte/bridge`:WebView 双向消息类型、默认事件节流配置和消息判断工具。
|
|
60
|
+
- `bridgerte/native-spec`:RN/Flutter 原生菜单 schema、toolbar 解析和命令状态匹配工具。
|
|
61
|
+
- `bridgerte/style.css`:DOM 默认样式。
|
|
62
|
+
- `bridgerte`:聚合入口,适合迁移期或小型项目;生产示例优先使用 subpath。
|
|
63
|
+
|
|
64
|
+
## 快速开始
|
|
65
|
+
|
|
66
|
+
页面准备两个容器:一个给 toolbar/tabbar,一个给编辑器内容区。`createRichTextEditor()`
|
|
67
|
+
只创建 editor;菜单必须用 `createRichTextToolbar()` 单独挂载,并绑定同一个 `EditorAPI`。
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<div id="toolbar"></div>
|
|
71
|
+
<div id="editor"></div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createRichTextEditor, createRichTextToolbar } from 'bridgerte/dom';
|
|
76
|
+
import 'bridgerte/style.css';
|
|
77
|
+
|
|
78
|
+
const editorContainer = document.querySelector<HTMLElement>('#editor');
|
|
79
|
+
const toolbarContainer = document.querySelector<HTMLElement>('#toolbar');
|
|
80
|
+
|
|
81
|
+
if (!editorContainer) throw new Error('editor container not found');
|
|
82
|
+
if (!toolbarContainer) throw new Error('toolbar container not found');
|
|
83
|
+
|
|
84
|
+
const editor = createRichTextEditor(editorContainer, {
|
|
85
|
+
placeholder: '开始输入',
|
|
86
|
+
onContentChange(change) {
|
|
87
|
+
counter.textContent = `${change.plainTextLength}/${change.maxLength ?? '∞'}`;
|
|
88
|
+
saveButton.disabled = !change.dirty;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const toolbar = createRichTextToolbar(toolbarContainer, {
|
|
93
|
+
editor,
|
|
94
|
+
placement: 'top'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
toolbar.update();
|
|
98
|
+
|
|
99
|
+
window.addEventListener('beforeunload', () => {
|
|
100
|
+
toolbar.destroy();
|
|
101
|
+
editor.destroy();
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`placement: 'top' | 'bottom'` 只影响菜单语义、默认样式状态和无障碍命名;DOM 放在哪里、
|
|
106
|
+
是否吸顶、吸底或跟随键盘,由业务自己的布局决定。销毁时建议先销毁 toolbar,再销毁 editor。
|
|
107
|
+
|
|
108
|
+
WebView / RN / Flutter 也是同一心智:WebView 内的 editor 只提供内容和命令能力;原生菜单先读取
|
|
109
|
+
ready payload 中的 `menuSchema`,再订阅后续 `editor.commandStateChange` 自行渲染,并通过
|
|
110
|
+
bridge 发送命令。
|
|
111
|
+
|
|
112
|
+
## 基础配置
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { createRichTextEditor } from 'bridgerte/dom';
|
|
116
|
+
import type { EditorContent } from 'bridgerte/core';
|
|
117
|
+
|
|
118
|
+
const initialValue: Partial<EditorContent> = {
|
|
119
|
+
html: '<p>Hello BridgeRTE</p>',
|
|
120
|
+
plainText: 'Hello BridgeRTE'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const editor = createRichTextEditor(container, {
|
|
124
|
+
value: initialValue,
|
|
125
|
+
readonly: false,
|
|
126
|
+
placeholder: '写点什么',
|
|
127
|
+
maxLength: 10000,
|
|
128
|
+
keyboardShortcuts: true,
|
|
129
|
+
onReady(api) {
|
|
130
|
+
api.focus();
|
|
131
|
+
},
|
|
132
|
+
onError(error) {
|
|
133
|
+
reportError(error);
|
|
134
|
+
},
|
|
135
|
+
onFocus() {
|
|
136
|
+
console.log('focus');
|
|
137
|
+
},
|
|
138
|
+
onBlur() {
|
|
139
|
+
console.log('blur');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
常用选项:
|
|
145
|
+
|
|
146
|
+
- `value`:初始内容,传 `Partial<EditorContent>`。
|
|
147
|
+
- `readonly`:只读状态。
|
|
148
|
+
- `placeholder`:空态提示。
|
|
149
|
+
- `maxLength`:最大纯文本长度。
|
|
150
|
+
- `keyboardShortcuts`:是否启用 DOM 基础快捷键,默认关闭。
|
|
151
|
+
- `onReady(api)`:编辑器初始化完成后回传 `EditorAPI`。
|
|
152
|
+
- `onContentChange(change)`:高频轻量内容摘要。
|
|
153
|
+
- `onChange(content)`:兼容旧项目的完整内容回调,大文档不建议逐字依赖。
|
|
154
|
+
- `onError(error)`:运行时错误。
|
|
155
|
+
- `onFocus()` / `onBlur()`:焦点变化。
|
|
156
|
+
|
|
157
|
+
Deprecated 兼容字段:
|
|
158
|
+
|
|
159
|
+
- `toolbarMode`:历史字段,当前在 `createRichTextEditor()` 中是 no-op,不再创建或控制 DOM
|
|
160
|
+
toolbar/tabbar。
|
|
161
|
+
- `toolbarConfig`:历史 editor 字段,当前不再影响 editor 创建。菜单显示结构请传给
|
|
162
|
+
`createRichTextToolbar()`。
|
|
163
|
+
|
|
164
|
+
## 内容读写
|
|
165
|
+
|
|
166
|
+
主动读取完整内容:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const content = editor.getContent();
|
|
170
|
+
|
|
171
|
+
console.log(content.version);
|
|
172
|
+
console.log(content.html);
|
|
173
|
+
console.log(content.json);
|
|
174
|
+
console.log(content.plainText);
|
|
175
|
+
console.log(content.assets);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
写入内容:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
editor.setContent({
|
|
182
|
+
html: '<h1>标题</h1><p>正文</p>',
|
|
183
|
+
plainText: '标题\n正文'
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
高频变化使用轻量摘要:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
createRichTextEditor(container, {
|
|
191
|
+
maxLength: 5000,
|
|
192
|
+
onContentChange(change) {
|
|
193
|
+
saveButton.disabled = !change.dirty;
|
|
194
|
+
counter.textContent = `${change.plainTextLength}/${change.maxLength ?? '∞'}`;
|
|
195
|
+
warning.hidden = !change.isOverMaxLength;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
重要边界:
|
|
201
|
+
|
|
202
|
+
- `onContentChange` 不携带完整 `html`、`json`、`plainText`,适合驱动 dirty、字数和保存按钮。
|
|
203
|
+
- 保存、提交、离开页面确认时,再主动调用 `getContent()`。
|
|
204
|
+
- `onChange(content)` 会回传完整内容,主要用于兼容旧项目;10w 内容场景不要逐字依赖它保存。
|
|
205
|
+
- WebView 高频 `editor.contentChange` 也只应依赖轻量摘要;完整内容通过 `requestContent` 获取。
|
|
206
|
+
|
|
207
|
+
## EditorAPI
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
editor.focus();
|
|
211
|
+
editor.blur();
|
|
212
|
+
editor.setReadonly(true);
|
|
213
|
+
|
|
214
|
+
editor.executeCommand({ type: 'format.bold' });
|
|
215
|
+
|
|
216
|
+
const states = editor.getCommandStates();
|
|
217
|
+
const unsubscribe = editor.subscribeCommandStateChange((nextStates) => {
|
|
218
|
+
renderToolbarState(nextStates);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
unsubscribe();
|
|
222
|
+
editor.destroy();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`EditorAPI` 方法:
|
|
226
|
+
|
|
218
227
|
- `getContent()`:读取完整内容。
|
|
219
228
|
- `setContent(content)`:写入内容。
|
|
220
|
-
- `executeCommand(command)`:执行命令。
|
|
221
|
-
- `requestPayloadPanel(request)`:打开参数面板请求,供自绘菜单复用。
|
|
222
|
-
- `getCommandStates()`:读取当前命令状态。
|
|
223
|
-
- `subscribeCommandStateChange(listener)`:订阅命令状态变化。
|
|
224
|
-
- `setReadonly(readonly)`:切换只读。
|
|
225
|
-
- `focus()` / `blur()`:焦点控制。
|
|
229
|
+
- `executeCommand(command)`:执行命令。
|
|
230
|
+
- `requestPayloadPanel(request)`:打开参数面板请求,供自绘菜单复用。
|
|
231
|
+
- `getCommandStates()`:读取当前命令状态。
|
|
232
|
+
- `subscribeCommandStateChange(listener)`:订阅命令状态变化。
|
|
233
|
+
- `setReadonly(readonly)`:切换只读。
|
|
234
|
+
- `focus()` / `blur()`:焦点控制。
|
|
226
235
|
- `destroy()`:销毁实例。
|
|
227
236
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
所有菜单最终都会落到 `EditorCommand`。业务也可以直接调用命令 API。
|
|
231
|
-
|
|
232
|
-
文本格式:
|
|
237
|
+
保存、提交或离开确认前可以用 `isEditorContentEmpty()` 判断富文本是否为空。它暴露在
|
|
238
|
+
`bridgerte/core`,只读取已经生成的 `EditorContent` 字段,不会触发 DOM 解析:
|
|
233
239
|
|
|
234
240
|
```ts
|
|
235
|
-
|
|
236
|
-
editor.executeCommand({ type: 'format.italic' });
|
|
237
|
-
editor.executeCommand({ type: 'format.underline' });
|
|
238
|
-
editor.executeCommand({ type: 'format.strike' });
|
|
239
|
-
editor.executeCommand({ type: 'format.inlineCode' });
|
|
240
|
-
editor.executeCommand({ type: 'format.superscript' });
|
|
241
|
-
editor.executeCommand({ type: 'format.subscript' });
|
|
242
|
-
editor.executeCommand({ type: 'format.clear' });
|
|
243
|
-
editor.executeCommand({ type: 'format.color', value: '#1677ff' });
|
|
244
|
-
editor.executeCommand({ type: 'format.backgroundColor', value: '#e8f3ff' });
|
|
245
|
-
editor.executeCommand({ type: 'format.fontSize', value: '18px' });
|
|
246
|
-
editor.executeCommand({ type: 'format.fontFamily', value: 'Arial' });
|
|
247
|
-
editor.executeCommand({ type: 'format.lineHeight', value: '1.75' });
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
段落、列表、对齐:
|
|
241
|
+
import { isEditorContentEmpty } from 'bridgerte/core';
|
|
251
242
|
|
|
252
|
-
|
|
253
|
-
editor.executeCommand({ type: 'block.paragraph' });
|
|
254
|
-
editor.executeCommand({ type: 'block.heading', level: 1 });
|
|
255
|
-
editor.executeCommand({ type: 'block.quote' });
|
|
256
|
-
editor.executeCommand({ type: 'block.divider' });
|
|
257
|
-
editor.executeCommand({ type: 'block.code', language: 'typescript' });
|
|
258
|
-
editor.executeCommand({ type: 'block.setCodeLanguage', language: 'json' });
|
|
259
|
-
|
|
260
|
-
editor.executeCommand({ type: 'list.ordered' });
|
|
261
|
-
editor.executeCommand({ type: 'list.unordered' });
|
|
262
|
-
editor.executeCommand({ type: 'list.todo' });
|
|
263
|
-
|
|
264
|
-
editor.executeCommand({ type: 'align', value: 'left' });
|
|
265
|
-
editor.executeCommand({ type: 'align', value: 'center' });
|
|
266
|
-
editor.executeCommand({ type: 'align', value: 'right' });
|
|
267
|
-
editor.executeCommand({ type: 'align', value: 'justify' });
|
|
268
|
-
editor.executeCommand({ type: 'indent.increase' });
|
|
269
|
-
editor.executeCommand({ type: 'indent.decrease' });
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
链接、表格、媒体、历史:
|
|
273
|
-
|
|
274
|
-
```ts
|
|
275
|
-
editor.executeCommand({ type: 'link.set', href: 'https://example.com', text: 'Example' });
|
|
276
|
-
editor.executeCommand({ type: 'link.unset' });
|
|
277
|
-
editor.executeCommand({ type: 'link.open' });
|
|
278
|
-
|
|
279
|
-
editor.executeCommand({ type: 'table.insert', rows: 3, cols: 4 });
|
|
280
|
-
editor.executeCommand({ type: 'table.insertRow', direction: 'after', count: 1 });
|
|
281
|
-
editor.executeCommand({ type: 'table.insertColumn', direction: 'after', count: 1 });
|
|
282
|
-
editor.executeCommand({ type: 'table.deleteRow' });
|
|
283
|
-
editor.executeCommand({ type: 'table.deleteColumn' });
|
|
284
|
-
editor.executeCommand({ type: 'table.delete' });
|
|
285
|
-
|
|
286
|
-
editor.executeCommand({
|
|
287
|
-
type: 'media.insertImage',
|
|
288
|
-
url: 'https://example.com/image.png',
|
|
289
|
-
alt: '示例图片',
|
|
290
|
-
width: 1200,
|
|
291
|
-
height: 800,
|
|
292
|
-
displayWidthPercent: 50,
|
|
293
|
-
align: 'center'
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
editor.executeCommand({
|
|
297
|
-
type: 'media.insertVideo',
|
|
298
|
-
url: 'https://example.com/video.mp4',
|
|
299
|
-
poster: 'https://example.com/poster.png',
|
|
300
|
-
displayWidthPercent: 100
|
|
301
|
-
});
|
|
243
|
+
const content = editor.getContent();
|
|
302
244
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
245
|
+
if (isEditorContentEmpty(content)) {
|
|
246
|
+
showToast('请输入内容');
|
|
247
|
+
} else {
|
|
248
|
+
await saveArticle(content);
|
|
249
|
+
}
|
|
307
250
|
```
|
|
308
251
|
|
|
309
|
-
|
|
310
|
-
`media.insertVideo` 命令。主动链接编辑也不是当前默认内置重点,业务可以通过自定义菜单调用
|
|
311
|
-
`link.*` 命令。
|
|
312
|
-
|
|
313
|
-
## Toolbar / Tabbar 菜单配置
|
|
314
|
-
|
|
315
|
-
DOM toolbar/tabbar 只通过 `createRichTextToolbar()` 创建。它使用同一套菜单 schema/config,
|
|
316
|
-
并通过传入的 `EditorAPI` 订阅状态、执行命令。
|
|
252
|
+
如果业务只拿到 HTML 字符串,可以用 DOM 入口的 `hasMeaningfulHtmlContent()` 做低频判断:
|
|
317
253
|
|
|
318
254
|
```ts
|
|
319
|
-
|
|
320
|
-
editor,
|
|
321
|
-
placement: 'bottom',
|
|
322
|
-
toolbarConfig: {
|
|
323
|
-
toolbarKeys: [
|
|
324
|
-
'bold',
|
|
325
|
-
'italic',
|
|
326
|
-
'underline',
|
|
327
|
-
'|',
|
|
328
|
-
'heading-1',
|
|
329
|
-
'quote',
|
|
330
|
-
'|',
|
|
331
|
-
{
|
|
332
|
-
key: 'history',
|
|
333
|
-
title: '历史',
|
|
334
|
-
icon: 'history',
|
|
335
|
-
menuKeys: ['undo', 'redo']
|
|
336
|
-
}
|
|
337
|
-
],
|
|
338
|
-
excludeKeys: ['quote']
|
|
339
|
-
}
|
|
340
|
-
});
|
|
255
|
+
import { hasMeaningfulHtmlContent } from 'bridgerte/dom';
|
|
341
256
|
|
|
342
|
-
|
|
257
|
+
submitButton.disabled = !hasMeaningfulHtmlContent(html);
|
|
343
258
|
```
|
|
344
259
|
|
|
260
|
+
## 命令 API
|
|
261
|
+
|
|
262
|
+
所有菜单最终都会落到 `EditorCommand`。业务也可以直接调用命令 API。
|
|
263
|
+
|
|
264
|
+
文本格式:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
editor.executeCommand({ type: 'format.bold' });
|
|
268
|
+
editor.executeCommand({ type: 'format.italic' });
|
|
269
|
+
editor.executeCommand({ type: 'format.underline' });
|
|
270
|
+
editor.executeCommand({ type: 'format.strike' });
|
|
271
|
+
editor.executeCommand({ type: 'format.inlineCode' });
|
|
272
|
+
editor.executeCommand({ type: 'format.superscript' });
|
|
273
|
+
editor.executeCommand({ type: 'format.subscript' });
|
|
274
|
+
editor.executeCommand({ type: 'format.clear' });
|
|
275
|
+
editor.executeCommand({ type: 'format.color', value: '#1677ff' });
|
|
276
|
+
editor.executeCommand({ type: 'format.backgroundColor', value: '#e8f3ff' });
|
|
277
|
+
editor.executeCommand({ type: 'format.fontSize', value: '18px' });
|
|
278
|
+
editor.executeCommand({ type: 'format.fontFamily', value: 'Arial' });
|
|
279
|
+
editor.executeCommand({ type: 'format.lineHeight', value: '1.75' });
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
段落、列表、对齐:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
editor.executeCommand({ type: 'block.paragraph' });
|
|
286
|
+
editor.executeCommand({ type: 'block.heading', level: 1 });
|
|
287
|
+
editor.executeCommand({ type: 'block.quote' });
|
|
288
|
+
editor.executeCommand({ type: 'block.divider' });
|
|
289
|
+
editor.executeCommand({ type: 'block.code', language: 'typescript' });
|
|
290
|
+
editor.executeCommand({ type: 'block.setCodeLanguage', language: 'json' });
|
|
291
|
+
|
|
292
|
+
editor.executeCommand({ type: 'list.ordered' });
|
|
293
|
+
editor.executeCommand({ type: 'list.unordered' });
|
|
294
|
+
editor.executeCommand({ type: 'list.todo' });
|
|
295
|
+
|
|
296
|
+
editor.executeCommand({ type: 'align', value: 'left' });
|
|
297
|
+
editor.executeCommand({ type: 'align', value: 'center' });
|
|
298
|
+
editor.executeCommand({ type: 'align', value: 'right' });
|
|
299
|
+
editor.executeCommand({ type: 'align', value: 'justify' });
|
|
300
|
+
editor.executeCommand({ type: 'indent.increase' });
|
|
301
|
+
editor.executeCommand({ type: 'indent.decrease' });
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
链接、表格、媒体、历史:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
editor.executeCommand({ type: 'link.set', href: 'https://example.com', text: 'Example' });
|
|
308
|
+
editor.executeCommand({ type: 'link.unset' });
|
|
309
|
+
editor.executeCommand({ type: 'link.open' });
|
|
310
|
+
|
|
311
|
+
editor.executeCommand({ type: 'table.insert', rows: 3, cols: 4 });
|
|
312
|
+
editor.executeCommand({ type: 'table.insertRow', direction: 'after', count: 1 });
|
|
313
|
+
editor.executeCommand({ type: 'table.insertColumn', direction: 'after', count: 1 });
|
|
314
|
+
editor.executeCommand({ type: 'table.deleteRow' });
|
|
315
|
+
editor.executeCommand({ type: 'table.deleteColumn' });
|
|
316
|
+
editor.executeCommand({ type: 'table.delete' });
|
|
317
|
+
|
|
318
|
+
editor.executeCommand({
|
|
319
|
+
type: 'media.insertImage',
|
|
320
|
+
url: 'https://example.com/image.png',
|
|
321
|
+
alt: '示例图片',
|
|
322
|
+
width: 1200,
|
|
323
|
+
height: 800,
|
|
324
|
+
displayWidthPercent: 50,
|
|
325
|
+
align: 'center'
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
editor.executeCommand({
|
|
329
|
+
type: 'media.insertVideo',
|
|
330
|
+
url: 'https://example.com/video.mp4',
|
|
331
|
+
poster: 'https://example.com/poster.png',
|
|
332
|
+
displayWidthPercent: 100
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
editor.executeCommand({ type: 'history.undo' });
|
|
336
|
+
editor.executeCommand({ type: 'history.redo' });
|
|
337
|
+
editor.executeCommand({ type: 'fullscreen.toggle' });
|
|
338
|
+
editor.executeCommand({ type: 'content.clear' });
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
默认 toolbar 不提供图片/视频 URL 插入入口,但保留 `media.insertImage` 和
|
|
342
|
+
`media.insertVideo` 命令。主动链接编辑也不是当前默认内置重点,业务可以通过自定义菜单调用
|
|
343
|
+
`link.*` 命令。
|
|
344
|
+
|
|
345
|
+
## Toolbar / Tabbar 菜单配置
|
|
346
|
+
|
|
347
|
+
DOM toolbar/tabbar 只通过 `createRichTextToolbar()` 创建。它使用同一套菜单 schema/config,
|
|
348
|
+
并通过传入的 `EditorAPI` 订阅状态、执行命令。
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
const toolbar = createRichTextToolbar(toolbarContainer, {
|
|
352
|
+
editor,
|
|
353
|
+
placement: 'bottom',
|
|
354
|
+
toolbarConfig: {
|
|
355
|
+
toolbarKeys: [
|
|
356
|
+
'bold',
|
|
357
|
+
'italic',
|
|
358
|
+
'underline',
|
|
359
|
+
'|',
|
|
360
|
+
'heading-1',
|
|
361
|
+
'quote',
|
|
362
|
+
'|',
|
|
363
|
+
{
|
|
364
|
+
key: 'history',
|
|
365
|
+
title: '历史',
|
|
366
|
+
icon: 'history',
|
|
367
|
+
menuKeys: ['undo', 'redo']
|
|
368
|
+
}
|
|
369
|
+
],
|
|
370
|
+
excludeKeys: ['quote']
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
toolbar.update();
|
|
375
|
+
```
|
|
376
|
+
|
|
345
377
|
`toolbarConfig` 规则:
|
|
346
378
|
|
|
347
379
|
- `toolbarKeys`:完整控制显示顺序。
|
|
348
|
-
- 字符串项使用 `MenuItem.id`;`'|'`
|
|
380
|
+
- 字符串项使用 `MenuItem.id`;`'|'` 是唯一可见分割线来源,不执行命令;
|
|
381
|
+
`MenuItem.group` 只保留语义分组,不会自动画线。
|
|
349
382
|
- 分组项使用 `{ key, title, icon, menuKeys }`;DOM toolbar 会渲染一个分组按钮,点击后打开
|
|
350
383
|
Y 轴纵向收纳菜单,例如上面的配置会显示 `[历史]` 按钮,菜单里是 `[撤销]`、`[重做]`。
|
|
351
|
-
- `key` 是分组入口的稳定 id,`title` 是按钮文案和 `aria-label`,`icon`
|
|
384
|
+
- `key` 是分组入口的稳定 id,`title` 是按钮文案和 `aria-label`,`icon` 用于分组按钮图标;
|
|
385
|
+
`menuKeys` 继续引用已有 `MenuItem.id`,不要为收纳菜单重新声明一套命令。
|
|
352
386
|
- `insertKeys`:在默认菜单基础上插入。
|
|
353
387
|
- `excludeKeys`:隐藏默认菜单。
|
|
354
388
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
```ts
|
|
358
|
-
const toolbar = createRichTextToolbar(toolbarContainer, {
|
|
359
|
-
editor,
|
|
360
|
-
placement: 'top',
|
|
361
|
-
toolbarConfig: {
|
|
362
|
-
toolbarKeys: ['bold', 'italic', '|', 'undo', 'redo']
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
toolbar.update();
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
## 自定义菜单、Icon 和文案
|
|
389
|
+
把几个功能收进“更多样式”菜单时,可以这样声明:
|
|
370
390
|
|
|
371
391
|
```ts
|
|
372
|
-
|
|
373
|
-
import { defaultMenuSchema, type MenuItem } from 'bridgerte/native-spec';
|
|
374
|
-
|
|
375
|
-
const customMenu: MenuItem = {
|
|
376
|
-
id: 'custom-clear',
|
|
377
|
-
command: { type: 'content.clear' },
|
|
378
|
-
label: '清空',
|
|
379
|
-
icon: 'custom-clear',
|
|
380
|
-
group: 'history'
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
const menuSchema = [...defaultMenuSchema, customMenu];
|
|
384
|
-
const icons = {
|
|
385
|
-
'custom-clear': '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 6h16"/></svg>'
|
|
386
|
-
};
|
|
387
|
-
const menuLabels = {
|
|
388
|
-
'custom-clear': '清空文档',
|
|
389
|
-
bold: '加粗文本'
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const editor = createRichTextEditor(editorContainer, {
|
|
393
|
-
menuSchema,
|
|
394
|
-
icons,
|
|
395
|
-
menuLabels
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const toolbar = createRichTextToolbar(toolbarContainer, {
|
|
392
|
+
createRichTextToolbar(toolbarContainer, {
|
|
399
393
|
editor,
|
|
400
|
-
menuSchema,
|
|
401
394
|
toolbarConfig: {
|
|
402
|
-
insertKeys: {
|
|
403
|
-
index: 0,
|
|
404
|
-
keys: ['custom-clear', '|']
|
|
405
|
-
}
|
|
406
|
-
},
|
|
407
|
-
icons,
|
|
408
|
-
menuLabels
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
toolbar.update();
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
稳定约束:
|
|
415
|
-
|
|
416
|
-
- `MenuItem.id` 是菜单配置使用的稳定 key。
|
|
417
|
-
- `MenuItem.icon` 是稳定 icon key,不是 SVG 字符串。
|
|
418
|
-
- 业务覆盖 icon 只能通过 `icons` map。
|
|
419
|
-
- 业务覆盖文案只能通过 `menuLabels`。
|
|
420
|
-
- `menuLabels` 影响按钮文本、tooltip 和 `aria-label`,不改变命令语义。
|
|
421
|
-
- 缺失 icon 时,DOM 菜单使用 label 文本兜底。
|
|
422
|
-
|
|
423
|
-
## Hoverbar
|
|
424
|
-
|
|
425
|
-
选中文本 hoverbar 默认开启,复用 `menuSchema`、`icons` 和 `menuLabels`。它有独立的
|
|
426
|
-
`hoverbarConfig`,可以和 toolbar 展示不同菜单。
|
|
427
|
-
|
|
428
|
-
```ts
|
|
429
|
-
createRichTextEditor(container, {
|
|
430
|
-
hoverbarConfig: {
|
|
431
395
|
toolbarKeys: [
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
396
|
+
{
|
|
397
|
+
key: 'more-style-group',
|
|
398
|
+
title: '更多样式',
|
|
399
|
+
icon: 'more',
|
|
400
|
+
menuKeys: ['inline-code', 'clear-style']
|
|
401
|
+
},
|
|
437
402
|
'|',
|
|
438
|
-
'
|
|
439
|
-
'
|
|
403
|
+
'undo',
|
|
404
|
+
'redo'
|
|
440
405
|
]
|
|
441
406
|
}
|
|
442
407
|
});
|
|
443
408
|
```
|
|
444
409
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
```ts
|
|
448
|
-
createRichTextEditor(container, {
|
|
449
|
-
floatingMenus: {
|
|
450
|
-
hoverbar: false
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
关闭后只是不显示 DOM hoverbar,不影响底层命令 API。H5/WebView 或品牌化项目可以关闭内置
|
|
456
|
-
hoverbar 后自绘选区菜单。
|
|
457
|
-
|
|
458
|
-
## 参数面板
|
|
459
|
-
|
|
460
|
-
颜色、背景色、字号、字体、行高、表格和代码语言都通过 `PayloadPanelSchema` 描述候选项。
|
|
461
|
-
DOM 默认 UI 和业务自绘 request 使用同一份 schema。
|
|
462
|
-
|
|
463
|
-
```ts
|
|
464
|
-
createRichTextEditor(container, {
|
|
465
|
-
payloadPanelConfig: {
|
|
466
|
-
'font-size': {
|
|
467
|
-
fields: {
|
|
468
|
-
value: {
|
|
469
|
-
includeValues: ['16px', '20px', '24px'],
|
|
470
|
-
optionLabels: {
|
|
471
|
-
'16px': '正文',
|
|
472
|
-
'24px': '标题'
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
},
|
|
477
|
-
color: {
|
|
478
|
-
fields: {
|
|
479
|
-
value: {
|
|
480
|
-
options: [
|
|
481
|
-
{ label: '品牌蓝', value: '#1677ff' },
|
|
482
|
-
{ label: '危险红', value: '#ff4d4f' }
|
|
483
|
-
]
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
},
|
|
487
|
-
table: {
|
|
488
|
-
fields: {
|
|
489
|
-
rows: { defaultValue: '2', max: 6 },
|
|
490
|
-
cols: { defaultValue: '3', max: 6 }
|
|
491
|
-
}
|
|
492
|
-
},
|
|
493
|
-
'code-block-language': {
|
|
494
|
-
fields: {
|
|
495
|
-
language: {
|
|
496
|
-
includeValues: ['plain', 'javascript', 'typescript', 'json']
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
},
|
|
501
|
-
onPayloadPanelRequest(request) {
|
|
502
|
-
if (request.panel.id === 'color') {
|
|
503
|
-
renderColorPanel(request);
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
自绘接管规则:
|
|
511
|
-
|
|
512
|
-
```ts
|
|
513
|
-
createRichTextEditor(container, {
|
|
514
|
-
onPayloadPanelRequest(request) {
|
|
515
|
-
renderPanel({
|
|
516
|
-
title: request.panel.title,
|
|
517
|
-
fields: request.panel.fields,
|
|
518
|
-
currentValues: request.currentValues,
|
|
519
|
-
readonly: request.readonly,
|
|
520
|
-
submit: request.submit,
|
|
521
|
-
cancel: request.cancel
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
`onPayloadPanelRequest` 返回 `true` 表示业务接管渲染,DOM 默认面板不会显示。业务自绘完成后:
|
|
530
|
-
|
|
531
|
-
```ts
|
|
532
|
-
request.submit({ value: '#1677ff' });
|
|
533
|
-
request.cancel();
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
readonly 下 request 会带 `readonly: true`,自绘层应展示只读态;DOM 默认面板不会打开。
|
|
537
|
-
|
|
538
|
-
代码块语言也可以直接传完整 schema:
|
|
410
|
+
独立 toolbar:
|
|
539
411
|
|
|
540
412
|
```ts
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
413
|
+
const toolbar = createRichTextToolbar(toolbarContainer, {
|
|
414
|
+
editor,
|
|
415
|
+
placement: 'top',
|
|
416
|
+
toolbarConfig: {
|
|
417
|
+
toolbarKeys: ['bold', 'italic', '|', 'undo', 'redo']
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
toolbar.update();
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## 自定义菜单、Icon 和文案
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
import { createRichTextEditor, createRichTextToolbar } from 'bridgerte/dom';
|
|
428
|
+
import { defaultMenuSchema, type MenuItem } from 'bridgerte/native-spec';
|
|
429
|
+
|
|
430
|
+
const customMenu: MenuItem = {
|
|
431
|
+
id: 'custom-clear',
|
|
432
|
+
command: { type: 'content.clear' },
|
|
433
|
+
label: '清空',
|
|
434
|
+
icon: 'custom-clear',
|
|
435
|
+
group: 'history'
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const menuSchema = [...defaultMenuSchema, customMenu];
|
|
439
|
+
const icons = {
|
|
440
|
+
'custom-clear': '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 6h16"/></svg>'
|
|
441
|
+
};
|
|
442
|
+
const menuLabels = {
|
|
443
|
+
'custom-clear': '清空文档',
|
|
444
|
+
bold: '加粗文本'
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const editor = createRichTextEditor(editorContainer, {
|
|
448
|
+
menuSchema,
|
|
449
|
+
icons,
|
|
450
|
+
menuLabels
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const toolbar = createRichTextToolbar(toolbarContainer, {
|
|
454
|
+
editor,
|
|
455
|
+
menuSchema,
|
|
456
|
+
toolbarConfig: {
|
|
457
|
+
insertKeys: {
|
|
458
|
+
index: 0,
|
|
459
|
+
keys: ['custom-clear', '|']
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
icons,
|
|
463
|
+
menuLabels
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
toolbar.update();
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
稳定约束:
|
|
470
|
+
|
|
471
|
+
- `MenuItem.id` 是菜单配置使用的稳定 key。
|
|
472
|
+
- `MenuItem.icon` 是稳定 icon key,不是 SVG 字符串。
|
|
473
|
+
- 业务覆盖 icon 只能通过 `icons` map。
|
|
474
|
+
- 业务覆盖文案只能通过 `menuLabels`。
|
|
475
|
+
- `menuLabels` 影响按钮文本、tooltip 和 `aria-label`,不改变命令语义。
|
|
476
|
+
- 缺失 icon 时,DOM 菜单使用 label 文本兜底。
|
|
477
|
+
|
|
478
|
+
## Hoverbar
|
|
479
|
+
|
|
480
|
+
选中文本 hoverbar 默认开启,复用 `menuSchema`、`icons` 和 `menuLabels`。它有独立的
|
|
481
|
+
`hoverbarConfig`,可以和 toolbar 展示不同菜单。
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
createRichTextEditor(container, {
|
|
485
|
+
hoverbarConfig: {
|
|
486
|
+
toolbarKeys: [
|
|
487
|
+
'bold',
|
|
488
|
+
'italic',
|
|
489
|
+
'|',
|
|
490
|
+
'color',
|
|
491
|
+
'background-color',
|
|
492
|
+
'|',
|
|
493
|
+
'font-size',
|
|
494
|
+
'line-height'
|
|
495
|
+
]
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
关闭内置 hoverbar:
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
createRichTextEditor(container, {
|
|
504
|
+
floatingMenus: {
|
|
505
|
+
hoverbar: false
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
关闭后只是不显示 DOM hoverbar,不影响底层命令 API。H5/WebView 或品牌化项目可以关闭内置
|
|
511
|
+
hoverbar 后自绘选区菜单。
|
|
512
|
+
|
|
513
|
+
## 参数面板
|
|
514
|
+
|
|
515
|
+
颜色、背景色、字号、字体、行高、表格和代码语言都通过 `PayloadPanelSchema` 描述候选项。
|
|
516
|
+
DOM 默认 UI 和业务自绘 request 使用同一份 schema。
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
createRichTextEditor(container, {
|
|
520
|
+
payloadPanelConfig: {
|
|
521
|
+
'font-size': {
|
|
522
|
+
fields: {
|
|
523
|
+
value: {
|
|
524
|
+
includeValues: ['16px', '20px', '24px'],
|
|
525
|
+
optionLabels: {
|
|
526
|
+
'16px': '正文',
|
|
527
|
+
'24px': '标题'
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
color: {
|
|
533
|
+
fields: {
|
|
534
|
+
value: {
|
|
535
|
+
options: [
|
|
536
|
+
{ label: '品牌蓝', value: '#1677ff' },
|
|
537
|
+
{ label: '危险红', value: '#ff4d4f' }
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
table: {
|
|
543
|
+
fields: {
|
|
544
|
+
rows: { defaultValue: '2', max: 6 },
|
|
545
|
+
cols: { defaultValue: '3', max: 6 }
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
'code-block-language': {
|
|
549
|
+
fields: {
|
|
550
|
+
language: {
|
|
551
|
+
includeValues: ['plain', 'javascript', 'typescript', 'json']
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
onPayloadPanelRequest(request) {
|
|
557
|
+
if (request.panel.id === 'color') {
|
|
558
|
+
renderColorPanel(request);
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
自绘接管规则:
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
createRichTextEditor(container, {
|
|
569
|
+
onPayloadPanelRequest(request) {
|
|
570
|
+
renderPanel({
|
|
571
|
+
title: request.panel.title,
|
|
572
|
+
fields: request.panel.fields,
|
|
573
|
+
currentValues: request.currentValues,
|
|
574
|
+
readonly: request.readonly,
|
|
575
|
+
submit: request.submit,
|
|
576
|
+
cancel: request.cancel
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
`onPayloadPanelRequest` 返回 `true` 表示业务接管渲染,DOM 默认面板不会显示。业务自绘完成后:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
request.submit({ value: '#1677ff' });
|
|
588
|
+
request.cancel();
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
readonly 下 request 会带 `readonly: true`,自绘层应展示只读态;DOM 默认面板不会打开。
|
|
592
|
+
|
|
593
|
+
代码块语言也可以直接传完整 schema:
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
createRichTextEditor(container, {
|
|
597
|
+
codeBlockLanguagePanel: {
|
|
598
|
+
id: 'code-block-language',
|
|
599
|
+
title: '代码语言',
|
|
600
|
+
fields: [
|
|
546
601
|
{
|
|
547
602
|
type: 'select',
|
|
548
603
|
name: 'language',
|
|
549
604
|
label: '语言',
|
|
550
605
|
options: [
|
|
551
|
-
{ label: '纯文本', value: '
|
|
606
|
+
{ label: '纯文本', value: '' },
|
|
607
|
+
{ label: 'Go', value: 'go' },
|
|
608
|
+
{ label: 'TSX', value: 'tsx' },
|
|
552
609
|
{ label: 'TypeScript', value: 'typescript' },
|
|
553
610
|
{ label: 'JSON', value: 'json' }
|
|
554
611
|
]
|
|
@@ -558,142 +615,153 @@ createRichTextEditor(container, {
|
|
|
558
615
|
});
|
|
559
616
|
```
|
|
560
617
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
```ts
|
|
566
|
-
import { createRichTextEditor } from 'bridgerte/dom';
|
|
567
|
-
import type { UploadAdapter } from 'bridgerte/core';
|
|
568
|
-
|
|
569
|
-
const uploadBlob = async (url: string, file: unknown, signal?: AbortSignal) => {
|
|
570
|
-
const formData = new FormData();
|
|
571
|
-
|
|
572
|
-
if (file instanceof Blob) formData.append('file', file);
|
|
573
|
-
|
|
574
|
-
const response = await fetch(url, {
|
|
575
|
-
method: 'POST',
|
|
576
|
-
body: formData,
|
|
577
|
-
signal
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
return await response.json() as {
|
|
581
|
-
url: string;
|
|
582
|
-
width?: number;
|
|
583
|
-
height?: number;
|
|
584
|
-
poster?: string;
|
|
585
|
-
};
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const uploadAdapter: UploadAdapter = {
|
|
589
|
-
async uploadImage(file, context) {
|
|
590
|
-
return await uploadBlob('/api/upload-image', file.data, context.signal as AbortSignal);
|
|
591
|
-
},
|
|
592
|
-
async uploadVideo(file, context) {
|
|
593
|
-
return await uploadBlob('/api/upload-video', file.data, context.signal as AbortSignal);
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
createRichTextEditor(container, {
|
|
598
|
-
uploadAdapter,
|
|
599
|
-
mediaDefaultWidthPercent: 50
|
|
600
|
-
});
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
媒体能力:
|
|
604
|
-
|
|
605
|
-
- 默认 toolbar 提供本地上传图片/视频入口。
|
|
606
|
-
- URL 图片/视频插入使用 `media.insertImage` / `media.insertVideo` 命令。
|
|
607
|
-
- 图片/视频加载前会显示占位和 loading 状态,资源加载完成后再显示真实媒体。
|
|
608
|
-
- 上传失败时可以重试或删除。
|
|
609
|
-
- 成功态 controls 支持左/中/右对齐、`20%`、`50%`、`100%` 显示比例和删除。
|
|
610
|
-
- `mediaDefaultWidthPercent` 可设为 `20 | 50 | 100`,默认 `50`。
|
|
611
|
-
|
|
612
|
-
配置媒体 controls:
|
|
613
|
-
|
|
614
|
-
```ts
|
|
615
|
-
createRichTextEditor(container, {
|
|
616
|
-
mediaControlsConfig: {
|
|
617
|
-
toolbarKeys: [
|
|
618
|
-
'media-align-left',
|
|
619
|
-
'media-align-center',
|
|
620
|
-
'media-align-right',
|
|
621
|
-
'|',
|
|
622
|
-
'media-remove'
|
|
623
|
-
]
|
|
624
|
-
},
|
|
625
|
-
menuLabels: {
|
|
626
|
-
'media-remove': '删除媒体'
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
`media-resize-20`、`media-resize-50`、`media-resize-100` 是基础尺寸能力,配置中误删时会自动补回。
|
|
632
|
-
如果业务完全自绘媒体 controls,也需要提供等价的 `20%`、`50%`、`100%` 尺寸能力。
|
|
633
|
-
|
|
634
|
-
## Mention
|
|
635
|
-
|
|
636
|
-
`@` mention 默认开启。`mentionProvider` 负责返回候选数据,展示字段由
|
|
637
|
-
`mentionMenuConfig` 控制。
|
|
638
|
-
|
|
639
|
-
```ts
|
|
640
|
-
import { createRichTextEditor } from 'bridgerte/dom';
|
|
641
|
-
import type { MentionItem } from 'bridgerte/core';
|
|
642
|
-
|
|
643
|
-
const mentionProvider = async (query: string): Promise<MentionItem[]> => {
|
|
644
|
-
const response = await fetch(`/api/members?q=${encodeURIComponent(query)}`);
|
|
645
|
-
return await response.json() as MentionItem[];
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
createRichTextEditor(container, {
|
|
649
|
-
mentionProvider,
|
|
650
|
-
mentionMenuConfig: {
|
|
651
|
-
labelField: 'data.displayName',
|
|
652
|
-
descriptionField: 'data.role',
|
|
653
|
-
avatarField: 'data.avatarUrl',
|
|
654
|
-
showAvatar: true,
|
|
655
|
-
showDescription: true,
|
|
656
|
-
loadingText: '搜索成员中',
|
|
657
|
-
emptyText: '没有匹配成员',
|
|
658
|
-
errorText: '成员加载失败'
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
```
|
|
618
|
+
默认代码块高亮使用 `@lexical/code-shiki`,语言 schema 只放技术文档高频候选;业务可以继续用
|
|
619
|
+
`codeBlockLanguagePanel` 完整替换。HTML 粘贴会按 Shiki 支持的语言归一化显式 `language-*` /
|
|
620
|
+
`data-language` 标记,纯文本 fenced code 只在带语言时接管。
|
|
662
621
|
|
|
663
|
-
|
|
622
|
+
当前内置语言候选包括:纯文本、Bash、C、C++、C#、CSS、Dart、Dockerfile、Go、HTML、Java、
|
|
623
|
+
JavaScript、JSON、Kotlin、Markdown、PHP、Python、Rust、SQL、Swift、TOML、TSX、TypeScript、
|
|
624
|
+
Vue、XML、YAML。粘贴 fenced code 时也会识别常见 alias,
|
|
625
|
+
例如 `bash`/`sh` 会归一到 `shellscript`,`js` 到 `javascript`,`ts` 到 `typescript`,
|
|
626
|
+
`yml` 到 `yaml`。
|
|
664
627
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
628
|
+
## 图片、视频和上传
|
|
629
|
+
|
|
630
|
+
BridgeRTE 不内置上传后端。图片/视频上传必须由业务实现 `uploadAdapter`。
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
import { createRichTextEditor } from 'bridgerte/dom';
|
|
634
|
+
import type { UploadAdapter } from 'bridgerte/core';
|
|
635
|
+
|
|
636
|
+
const uploadBlob = async (url: string, file: unknown, signal?: AbortSignal) => {
|
|
637
|
+
const formData = new FormData();
|
|
638
|
+
|
|
639
|
+
if (file instanceof Blob) formData.append('file', file);
|
|
640
|
+
|
|
641
|
+
const response = await fetch(url, {
|
|
642
|
+
method: 'POST',
|
|
643
|
+
body: formData,
|
|
644
|
+
signal
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
return await response.json() as {
|
|
648
|
+
url: string;
|
|
649
|
+
width?: number;
|
|
650
|
+
height?: number;
|
|
651
|
+
poster?: string;
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const uploadAdapter: UploadAdapter = {
|
|
656
|
+
async uploadImage(file, context) {
|
|
657
|
+
return await uploadBlob('/api/upload-image', file.data, context.signal as AbortSignal);
|
|
658
|
+
},
|
|
659
|
+
async uploadVideo(file, context) {
|
|
660
|
+
return await uploadBlob('/api/upload-video', file.data, context.signal as AbortSignal);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
createRichTextEditor(container, {
|
|
665
|
+
uploadAdapter,
|
|
666
|
+
mediaDefaultWidthPercent: 50
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
媒体能力:
|
|
671
|
+
|
|
672
|
+
- 默认 toolbar 提供本地上传图片/视频入口。
|
|
673
|
+
- URL 图片/视频插入使用 `media.insertImage` / `media.insertVideo` 命令。
|
|
674
|
+
- 图片/视频加载前会显示占位和 loading 状态,资源加载完成后再显示真实媒体。
|
|
675
|
+
- 上传失败时可以重试或删除。
|
|
676
|
+
- 成功态 controls 支持左/中/右对齐、`20%`、`50%`、`100%` 显示比例和删除。
|
|
677
|
+
- `mediaDefaultWidthPercent` 可设为 `20 | 50 | 100`,默认 `50`。
|
|
678
|
+
|
|
679
|
+
配置媒体 controls:
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
createRichTextEditor(container, {
|
|
683
|
+
mediaControlsConfig: {
|
|
684
|
+
toolbarKeys: [
|
|
685
|
+
'media-align-left',
|
|
686
|
+
'media-align-center',
|
|
687
|
+
'media-align-right',
|
|
688
|
+
'|',
|
|
689
|
+
'media-remove'
|
|
690
|
+
]
|
|
691
|
+
},
|
|
692
|
+
menuLabels: {
|
|
693
|
+
'media-remove': '删除媒体'
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
`media-resize-20`、`media-resize-50`、`media-resize-100` 是基础尺寸能力,配置中误删时会自动补回。
|
|
699
|
+
如果业务完全自绘媒体 controls,也需要提供等价的 `20%`、`50%`、`100%` 尺寸能力。
|
|
700
|
+
|
|
701
|
+
## Mention
|
|
702
|
+
|
|
703
|
+
`@` mention 默认开启。`mentionProvider` 负责返回候选数据,展示字段由
|
|
704
|
+
`mentionMenuConfig` 控制。
|
|
705
|
+
|
|
706
|
+
```ts
|
|
707
|
+
import { createRichTextEditor } from 'bridgerte/dom';
|
|
708
|
+
import type { MentionItem } from 'bridgerte/core';
|
|
709
|
+
|
|
710
|
+
const mentionProvider = async (query: string): Promise<MentionItem[]> => {
|
|
711
|
+
const response = await fetch(`/api/members?q=${encodeURIComponent(query)}`);
|
|
712
|
+
return await response.json() as MentionItem[];
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
createRichTextEditor(container, {
|
|
716
|
+
mentionProvider,
|
|
717
|
+
mentionMenuConfig: {
|
|
718
|
+
labelField: 'data.displayName',
|
|
719
|
+
descriptionField: 'data.role',
|
|
720
|
+
avatarField: 'data.avatarUrl',
|
|
721
|
+
showAvatar: true,
|
|
722
|
+
showDescription: true,
|
|
723
|
+
loadingText: '搜索成员中',
|
|
724
|
+
emptyText: '没有匹配成员',
|
|
725
|
+
errorText: '成员加载失败'
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
自绘 mention 菜单:
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
createRichTextEditor(container, {
|
|
734
|
+
mentionProvider,
|
|
735
|
+
onMentionMenuRequest(request) {
|
|
736
|
+
renderMentionPopover(request);
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
关闭 mention trigger:
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
createRichTextEditor(container, {
|
|
746
|
+
floatingMenus: {
|
|
747
|
+
mention: false
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
关闭后不会监听 `@query`,也不会请求 provider;业务仍可主动执行 `mention.insert` 命令。
|
|
753
|
+
|
|
754
|
+
## Slash Command
|
|
755
|
+
|
|
756
|
+
`/` slash command 默认从菜单 schema 里选择常用结构命令。
|
|
757
|
+
|
|
758
|
+
```ts
|
|
692
759
|
createRichTextEditor(container, {
|
|
693
760
|
slashCommandConfig: {
|
|
694
761
|
toolbarKeys: ['heading-1', 'heading-2', '|', 'quote', 'code-block', 'table']
|
|
695
762
|
},
|
|
696
763
|
slashCommandMenuConfig: {
|
|
764
|
+
showIcon: true,
|
|
697
765
|
loadingText: '加载命令中',
|
|
698
766
|
emptyText: '没有匹配命令',
|
|
699
767
|
errorText: '命令加载失败'
|
|
@@ -701,242 +769,255 @@ createRichTextEditor(container, {
|
|
|
701
769
|
});
|
|
702
770
|
```
|
|
703
771
|
|
|
704
|
-
|
|
772
|
+
`slashCommandMenuConfig.showIcon` 只控制默认 DOM `/` 菜单是否展示图标;schema 和 provider
|
|
773
|
+
候选里的 `icon` key 仍会保留,业务自绘菜单可以继续读取它。想做成纯文字候选:
|
|
705
774
|
|
|
706
775
|
```ts
|
|
707
|
-
import type { SlashCommandItem } from 'bridgerte/core';
|
|
708
|
-
|
|
709
|
-
const slashCommandProvider = async (query: string): Promise<SlashCommandItem[]> => [
|
|
710
|
-
{
|
|
711
|
-
id: 'insert-template',
|
|
712
|
-
label: '插入模板',
|
|
713
|
-
description: `按 ${query} 搜索模板`,
|
|
714
|
-
icon: 'template',
|
|
715
|
-
command: { type: 'block.quote' }
|
|
716
|
-
}
|
|
717
|
-
];
|
|
718
|
-
|
|
719
776
|
createRichTextEditor(container, {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
slashCommandProvider
|
|
777
|
+
slashCommandMenuConfig: {
|
|
778
|
+
showIcon: false
|
|
779
|
+
}
|
|
724
780
|
});
|
|
725
781
|
```
|
|
726
782
|
|
|
727
|
-
|
|
783
|
+
追加动态候选:
|
|
728
784
|
|
|
729
|
-
|
|
785
|
+
```ts
|
|
786
|
+
import type { SlashCommandItem } from 'bridgerte/core';
|
|
787
|
+
|
|
788
|
+
const slashCommandProvider = async (query: string): Promise<SlashCommandItem[]> => [
|
|
789
|
+
{
|
|
790
|
+
id: 'insert-template',
|
|
791
|
+
label: '插入模板',
|
|
792
|
+
description: `按 ${query} 搜索模板`,
|
|
793
|
+
icon: 'template',
|
|
794
|
+
command: { type: 'block.quote' }
|
|
795
|
+
}
|
|
796
|
+
];
|
|
797
|
+
|
|
798
|
+
createRichTextEditor(container, {
|
|
799
|
+
slashCommandConfig: {
|
|
800
|
+
toolbarKeys: ['quote', 'table']
|
|
801
|
+
},
|
|
802
|
+
slashCommandProvider
|
|
803
|
+
});
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
语义说明:
|
|
807
|
+
|
|
808
|
+
- 只传 `slashCommandProvider` 时,provider 候选替换默认 slash 列表。
|
|
730
809
|
- 传入 `slashCommandConfig` 或自定义 `menuSchema` 后,provider 作为动态候选追加。
|
|
731
810
|
- provider 失败时,如果静态候选可用,仍显示静态候选。
|
|
732
811
|
- 表格这类需要参数的命令会继续走参数面板。
|
|
812
|
+
- `slashCommandConfig` 复用 toolbar 的显示规则,`toolbarKeys`、`insertKeys`、`excludeKeys`
|
|
813
|
+
的语义一致;未知 key 会被忽略,首尾或连续分割线会在解析阶段清理。
|
|
733
814
|
|
|
734
815
|
自绘 slash command:
|
|
735
|
-
|
|
736
|
-
```ts
|
|
737
|
-
createRichTextEditor(container, {
|
|
738
|
-
onSlashCommandMenuRequest(request) {
|
|
739
|
-
renderSlashPopover(request);
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
关闭 slash trigger:
|
|
746
|
-
|
|
747
|
-
```ts
|
|
748
|
-
createRichTextEditor(container, {
|
|
749
|
-
floatingMenus: {
|
|
750
|
-
slash: false
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
## WebView
|
|
756
|
-
|
|
757
|
-
WebView 页面内使用 `createWebViewBridgeRuntime()` 接 RN/Flutter 外壳消息。
|
|
758
|
-
|
|
759
|
-
```ts
|
|
760
|
-
import { createWebViewBridgeRuntime } from 'bridgerte/webview';
|
|
761
|
-
import 'bridgerte/style.css';
|
|
762
|
-
|
|
763
|
-
const runtime = createWebViewBridgeRuntime({
|
|
764
|
-
container,
|
|
765
|
-
transport: {
|
|
766
|
-
postMessage(message) {
|
|
767
|
-
window.ReactNativeWebView?.postMessage(JSON.stringify(message));
|
|
768
|
-
},
|
|
769
|
-
addMessageListener(listener) {
|
|
770
|
-
const handleMessage = (event: MessageEvent) => {
|
|
771
|
-
listener(JSON.parse(String(event.data)));
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
window.addEventListener('message', handleMessage);
|
|
775
|
-
|
|
776
|
-
return () => {
|
|
777
|
-
window.removeEventListener('message', handleMessage);
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
window.addEventListener('beforeunload', () => {
|
|
784
|
-
runtime.destroy();
|
|
785
|
-
});
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
也可以不提供 `addMessageListener`,由业务手动转发消息:
|
|
789
|
-
|
|
790
|
-
```ts
|
|
791
|
-
runtime.receive(messageFromNative);
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
原生侧发给编辑器:
|
|
795
|
-
|
|
796
|
-
- `editor.init`
|
|
797
|
-
- `editor.executeCommand`
|
|
798
|
-
- `editor.setContent`
|
|
799
|
-
- `editor.setReadonly`
|
|
800
|
-
- `editor.requestContent`
|
|
801
|
-
- `editor.payloadPanelResolved`
|
|
802
|
-
- `editor.payloadPanelCanceled`
|
|
803
|
-
- `editor.uploadResolved`
|
|
804
|
-
- `editor.uploadRejected`
|
|
805
|
-
|
|
806
|
-
编辑器发给原生侧:
|
|
807
|
-
|
|
808
|
-
- `editor.ready`
|
|
809
|
-
- `editor.content`
|
|
810
|
-
- `editor.contentChange`
|
|
811
|
-
- `editor.commandStateChange`
|
|
812
|
-
- `editor.payloadPanelRequest`
|
|
813
|
-
- `editor.uploadRequest`
|
|
814
|
-
- `editor.heightChange`
|
|
815
|
-
- `editor.error`
|
|
816
|
-
|
|
817
|
-
内容消息边界:
|
|
818
|
-
|
|
819
|
-
- 高频自动 `editor.contentChange` 只传轻量摘要。
|
|
820
|
-
- 完整 `EditorContent` 通过原生侧发送 `editor.requestContent` 获取。
|
|
821
|
-
- `requestContent` 的响应消息是 `editor.content`。
|
|
822
|
-
- 兼容期也会同步发送旧 `editor.contentChange` 完整响应。
|
|
823
|
-
- bridge 不传 File、Blob、base64 或大体积二进制文件。
|
|
824
|
-
|
|
825
|
-
## RN / Flutter 原生菜单
|
|
826
|
-
|
|
827
|
-
RN/Flutter 原生侧可以读取 `bridgerte/native-spec` 来渲染自己的菜单:
|
|
828
|
-
|
|
829
|
-
```ts
|
|
830
|
-
import {
|
|
831
|
-
defaultMenuSchema,
|
|
832
|
-
defaultToolbarConfig,
|
|
833
|
-
resolveToolbarMenu,
|
|
834
|
-
isMenuItemCommandState
|
|
835
|
-
} from 'bridgerte/native-spec';
|
|
836
|
-
|
|
837
|
-
const toolbarItems = resolveToolbarMenu(defaultToolbarConfig, defaultMenuSchema);
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
渲染规则:
|
|
841
|
-
|
|
842
|
-
- `MenuItem.id` 是配置和状态匹配的稳定 key。
|
|
843
|
-
- `MenuItem.command` 是完整命令,点击后可以直接发给 WebView。
|
|
844
|
-
- `MenuItem.icon` 是稳定 icon key,RN/Flutter 映射到自己的原生图标。
|
|
845
|
-
- `MenuItem.icon` 不是 SVG 字符串。
|
|
846
|
-
- `payloadPanel` 描述需要原生侧补齐的参数。
|
|
847
|
-
- 命令状态可用 `isMenuItemCommandState(item, state)` 匹配。
|
|
848
|
-
|
|
849
|
-
原生菜单不复用 DOM CSS;WebView 内编辑器样式通过 `--bridgerte-*` CSS Variables 覆盖。
|
|
850
|
-
|
|
851
|
-
## 样式和主题
|
|
852
|
-
|
|
853
|
-
导入默认样式:
|
|
854
|
-
|
|
855
|
-
```ts
|
|
856
|
-
import 'bridgerte/style.css';
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
覆盖主题变量:
|
|
860
|
-
|
|
861
|
-
```css
|
|
862
|
-
.editor-shell {
|
|
863
|
-
--bridgerte-color-primary: #1677ff;
|
|
864
|
-
--bridgerte-color-text: #1f2329;
|
|
865
|
-
--bridgerte-color-text-muted: #86909c;
|
|
866
|
-
--bridgerte-color-bg: #ffffff;
|
|
867
|
-
--bridgerte-color-panel: #ffffff;
|
|
868
|
-
--bridgerte-color-border: #e5e6eb;
|
|
869
|
-
--bridgerte-color-active-bg: #e8f3ff;
|
|
870
|
-
--bridgerte-color-placeholder: #b7bcc5;
|
|
871
|
-
--bridgerte-shadow-panel: 0 12px 32px rgb(15 23 42 / 14%);
|
|
872
|
-
--bridgerte-font-size: 15px;
|
|
873
|
-
--bridgerte-line-height: 1.7;
|
|
874
|
-
--bridgerte-radius: 8px;
|
|
875
|
-
--bridgerte-toolbar-height: 42px;
|
|
876
|
-
--bridgerte-control-height: 32px;
|
|
877
|
-
--bridgerte-editor-padding: 12px;
|
|
878
|
-
}
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
常用变量:
|
|
882
|
-
|
|
883
|
-
- `--bridgerte-color-primary`
|
|
884
|
-
- `--bridgerte-color-text`
|
|
885
|
-
- `--bridgerte-color-text-muted`
|
|
886
|
-
- `--bridgerte-color-bg`
|
|
887
|
-
- `--bridgerte-color-panel`
|
|
888
|
-
- `--bridgerte-color-border`
|
|
889
|
-
- `--bridgerte-color-active-bg`
|
|
890
|
-
- `--bridgerte-color-placeholder`
|
|
891
|
-
- `--bridgerte-color-danger`
|
|
892
|
-
- `--bridgerte-shadow-panel`
|
|
893
|
-
- `--bridgerte-font-size`
|
|
894
|
-
- `--bridgerte-line-height`
|
|
895
|
-
- `--bridgerte-radius`
|
|
896
|
-
- `--bridgerte-toolbar-height`
|
|
897
|
-
- `--bridgerte-control-height`
|
|
898
|
-
- `--bridgerte-hoverbar-button-size`
|
|
899
|
-
- `--bridgerte-editor-padding`
|
|
900
|
-
|
|
901
|
-
PC/H5 边界:
|
|
902
|
-
|
|
903
|
-
- DOM 默认样式只服务 Web/PC/H5。
|
|
904
|
-
- RN/Flutter 原生菜单不复用 DOM CSS。
|
|
905
|
-
- H5 触屏端不依赖 hover tooltip。
|
|
906
|
-
- 业务可以在外层容器覆盖 CSS Variables 实现品牌主题。
|
|
907
|
-
|
|
908
|
-
## 性能建议
|
|
909
|
-
|
|
910
|
-
BridgeRTE 按 10w 字符级内容设计输入路径:
|
|
911
|
-
|
|
912
|
-
- 不要在每次输入时调用 `getContent()`。
|
|
913
|
-
- 不要对 10w 内容逐字依赖 `onChange` 完整保存。
|
|
914
|
-
- 高频 UI 状态使用 `onContentChange` 摘要。
|
|
915
|
-
- 保存、提交或离开页面确认时再调用 `getContent()`。
|
|
916
|
-
- WebView 高频 `editor.contentChange` 只依赖摘要。
|
|
917
|
-
- WebView 完整内容通过 `editor.requestContent` 主动获取。
|
|
918
|
-
- bridge 不传 base64、File、Blob 或二进制大文件。
|
|
919
|
-
|
|
920
|
-
推荐保存:
|
|
921
|
-
|
|
922
|
-
```ts
|
|
923
|
-
const save = async () => {
|
|
924
|
-
const content = editor.getContent();
|
|
925
|
-
|
|
926
|
-
await fetch('/api/document', {
|
|
927
|
-
method: 'POST',
|
|
928
|
-
headers: {
|
|
929
|
-
'content-type': 'application/json'
|
|
930
|
-
},
|
|
931
|
-
body: JSON.stringify(content)
|
|
932
|
-
});
|
|
933
|
-
};
|
|
934
|
-
```
|
|
935
|
-
|
|
936
|
-
## 当前边界
|
|
937
|
-
|
|
938
|
-
- 业务项目只安装 `bridgerte`,不需要安装 Lexical。
|
|
939
|
-
- 默认 toolbar 不提供图片/视频 URL 插入入口;业务用命令 API 自定义入口。
|
|
940
|
-
- 主动链接编辑不作为默认内置入口;业务用 `link.*` 命令自定义入口。
|
|
941
|
-
- BridgeRTE 不内置上传后端,上传必须由业务实现 `uploadAdapter`。
|
|
942
|
-
- WebView bridge 不传大体积二进制和 base64 文件。
|
|
816
|
+
|
|
817
|
+
```ts
|
|
818
|
+
createRichTextEditor(container, {
|
|
819
|
+
onSlashCommandMenuRequest(request) {
|
|
820
|
+
renderSlashPopover(request);
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
关闭 slash trigger:
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
createRichTextEditor(container, {
|
|
830
|
+
floatingMenus: {
|
|
831
|
+
slash: false
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## WebView
|
|
837
|
+
|
|
838
|
+
WebView 页面内使用 `createWebViewBridgeRuntime()` 接 RN/Flutter 外壳消息。
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
import { createWebViewBridgeRuntime } from 'bridgerte/webview';
|
|
842
|
+
import 'bridgerte/style.css';
|
|
843
|
+
|
|
844
|
+
const runtime = createWebViewBridgeRuntime({
|
|
845
|
+
container,
|
|
846
|
+
transport: {
|
|
847
|
+
postMessage(message) {
|
|
848
|
+
window.ReactNativeWebView?.postMessage(JSON.stringify(message));
|
|
849
|
+
},
|
|
850
|
+
addMessageListener(listener) {
|
|
851
|
+
const handleMessage = (event: MessageEvent) => {
|
|
852
|
+
listener(JSON.parse(String(event.data)));
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
window.addEventListener('message', handleMessage);
|
|
856
|
+
|
|
857
|
+
return () => {
|
|
858
|
+
window.removeEventListener('message', handleMessage);
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
window.addEventListener('beforeunload', () => {
|
|
865
|
+
runtime.destroy();
|
|
866
|
+
});
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
也可以不提供 `addMessageListener`,由业务手动转发消息:
|
|
870
|
+
|
|
871
|
+
```ts
|
|
872
|
+
runtime.receive(messageFromNative);
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
原生侧发给编辑器:
|
|
876
|
+
|
|
877
|
+
- `editor.init`
|
|
878
|
+
- `editor.executeCommand`
|
|
879
|
+
- `editor.setContent`
|
|
880
|
+
- `editor.setReadonly`
|
|
881
|
+
- `editor.requestContent`
|
|
882
|
+
- `editor.payloadPanelResolved`
|
|
883
|
+
- `editor.payloadPanelCanceled`
|
|
884
|
+
- `editor.uploadResolved`
|
|
885
|
+
- `editor.uploadRejected`
|
|
886
|
+
|
|
887
|
+
编辑器发给原生侧:
|
|
888
|
+
|
|
889
|
+
- `editor.ready`
|
|
890
|
+
- `editor.content`
|
|
891
|
+
- `editor.contentChange`
|
|
892
|
+
- `editor.commandStateChange`
|
|
893
|
+
- `editor.payloadPanelRequest`
|
|
894
|
+
- `editor.uploadRequest`
|
|
895
|
+
- `editor.heightChange`
|
|
896
|
+
- `editor.error`
|
|
897
|
+
|
|
898
|
+
内容消息边界:
|
|
899
|
+
|
|
900
|
+
- 高频自动 `editor.contentChange` 只传轻量摘要。
|
|
901
|
+
- 完整 `EditorContent` 通过原生侧发送 `editor.requestContent` 获取。
|
|
902
|
+
- `requestContent` 的响应消息是 `editor.content`。
|
|
903
|
+
- 兼容期也会同步发送旧 `editor.contentChange` 完整响应。
|
|
904
|
+
- bridge 不传 File、Blob、base64 或大体积二进制文件。
|
|
905
|
+
|
|
906
|
+
## RN / Flutter 原生菜单
|
|
907
|
+
|
|
908
|
+
RN/Flutter 原生侧可以读取 `bridgerte/native-spec` 来渲染自己的菜单:
|
|
909
|
+
|
|
910
|
+
```ts
|
|
911
|
+
import {
|
|
912
|
+
defaultMenuSchema,
|
|
913
|
+
defaultToolbarConfig,
|
|
914
|
+
resolveToolbarMenu,
|
|
915
|
+
isMenuItemCommandState
|
|
916
|
+
} from 'bridgerte/native-spec';
|
|
917
|
+
|
|
918
|
+
const toolbarItems = resolveToolbarMenu(defaultToolbarConfig, defaultMenuSchema);
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
渲染规则:
|
|
922
|
+
|
|
923
|
+
- `MenuItem.id` 是配置和状态匹配的稳定 key。
|
|
924
|
+
- `MenuItem.command` 是完整命令,点击后可以直接发给 WebView。
|
|
925
|
+
- `MenuItem.icon` 是稳定 icon key,RN/Flutter 映射到自己的原生图标。
|
|
926
|
+
- `MenuItem.icon` 不是 SVG 字符串。
|
|
927
|
+
- `payloadPanel` 描述需要原生侧补齐的参数。
|
|
928
|
+
- 命令状态可用 `isMenuItemCommandState(item, state)` 匹配。
|
|
929
|
+
|
|
930
|
+
原生菜单不复用 DOM CSS;WebView 内编辑器样式通过 `--bridgerte-*` CSS Variables 覆盖。
|
|
931
|
+
|
|
932
|
+
## 样式和主题
|
|
933
|
+
|
|
934
|
+
导入默认样式:
|
|
935
|
+
|
|
936
|
+
```ts
|
|
937
|
+
import 'bridgerte/style.css';
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
覆盖主题变量:
|
|
941
|
+
|
|
942
|
+
```css
|
|
943
|
+
.editor-shell {
|
|
944
|
+
--bridgerte-color-primary: #1677ff;
|
|
945
|
+
--bridgerte-color-text: #1f2329;
|
|
946
|
+
--bridgerte-color-text-muted: #86909c;
|
|
947
|
+
--bridgerte-color-bg: #ffffff;
|
|
948
|
+
--bridgerte-color-panel: #ffffff;
|
|
949
|
+
--bridgerte-color-border: #e5e6eb;
|
|
950
|
+
--bridgerte-color-active-bg: #e8f3ff;
|
|
951
|
+
--bridgerte-color-placeholder: #b7bcc5;
|
|
952
|
+
--bridgerte-shadow-panel: 0 12px 32px rgb(15 23 42 / 14%);
|
|
953
|
+
--bridgerte-font-size: 15px;
|
|
954
|
+
--bridgerte-line-height: 1.7;
|
|
955
|
+
--bridgerte-radius: 8px;
|
|
956
|
+
--bridgerte-toolbar-height: 42px;
|
|
957
|
+
--bridgerte-control-height: 32px;
|
|
958
|
+
--bridgerte-editor-padding: 12px;
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
常用变量:
|
|
963
|
+
|
|
964
|
+
- `--bridgerte-color-primary`
|
|
965
|
+
- `--bridgerte-color-text`
|
|
966
|
+
- `--bridgerte-color-text-muted`
|
|
967
|
+
- `--bridgerte-color-bg`
|
|
968
|
+
- `--bridgerte-color-panel`
|
|
969
|
+
- `--bridgerte-color-border`
|
|
970
|
+
- `--bridgerte-color-active-bg`
|
|
971
|
+
- `--bridgerte-color-placeholder`
|
|
972
|
+
- `--bridgerte-color-danger`
|
|
973
|
+
- `--bridgerte-shadow-panel`
|
|
974
|
+
- `--bridgerte-font-size`
|
|
975
|
+
- `--bridgerte-line-height`
|
|
976
|
+
- `--bridgerte-radius`
|
|
977
|
+
- `--bridgerte-toolbar-height`
|
|
978
|
+
- `--bridgerte-control-height`
|
|
979
|
+
- `--bridgerte-hoverbar-button-size`
|
|
980
|
+
- `--bridgerte-editor-padding`
|
|
981
|
+
|
|
982
|
+
PC/H5 边界:
|
|
983
|
+
|
|
984
|
+
- DOM 默认样式只服务 Web/PC/H5。
|
|
985
|
+
- RN/Flutter 原生菜单不复用 DOM CSS。
|
|
986
|
+
- H5 触屏端不依赖 hover tooltip。
|
|
987
|
+
- 业务可以在外层容器覆盖 CSS Variables 实现品牌主题。
|
|
988
|
+
|
|
989
|
+
## 性能建议
|
|
990
|
+
|
|
991
|
+
BridgeRTE 按 10w 字符级内容设计输入路径:
|
|
992
|
+
|
|
993
|
+
- 不要在每次输入时调用 `getContent()`。
|
|
994
|
+
- 不要对 10w 内容逐字依赖 `onChange` 完整保存。
|
|
995
|
+
- 高频 UI 状态使用 `onContentChange` 摘要。
|
|
996
|
+
- 保存、提交或离开页面确认时再调用 `getContent()`。
|
|
997
|
+
- WebView 高频 `editor.contentChange` 只依赖摘要。
|
|
998
|
+
- WebView 完整内容通过 `editor.requestContent` 主动获取。
|
|
999
|
+
- bridge 不传 base64、File、Blob 或二进制大文件。
|
|
1000
|
+
|
|
1001
|
+
推荐保存:
|
|
1002
|
+
|
|
1003
|
+
```ts
|
|
1004
|
+
const save = async () => {
|
|
1005
|
+
const content = editor.getContent();
|
|
1006
|
+
|
|
1007
|
+
await fetch('/api/document', {
|
|
1008
|
+
method: 'POST',
|
|
1009
|
+
headers: {
|
|
1010
|
+
'content-type': 'application/json'
|
|
1011
|
+
},
|
|
1012
|
+
body: JSON.stringify(content)
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
## 当前边界
|
|
1018
|
+
|
|
1019
|
+
- 业务项目只安装 `bridgerte`,不需要安装 Lexical。
|
|
1020
|
+
- 默认 toolbar 不提供图片/视频 URL 插入入口;业务用命令 API 自定义入口。
|
|
1021
|
+
- 主动链接编辑不作为默认内置入口;业务用 `link.*` 命令自定义入口。
|
|
1022
|
+
- BridgeRTE 不内置上传后端,上传必须由业务实现 `uploadAdapter`。
|
|
1023
|
+
- WebView bridge 不传大体积二进制和 base64 文件。
|