bridgerte 0.9.0

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