@mteditor/react 0.1.0 → 0.1.1
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 +24 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
> MtEditor 的 React 适配层 —— **薄壳,不含任何编辑逻辑**。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 安装
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm i @mteditor/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// 样式是独立的包,且是纯 CSS(无 JS 入口),必须显式引入
|
|
13
|
+
import '@mteditor/theme-default/index.css'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@mteditor/core` 由本包作为普通依赖带进来。peer 依赖为 `react: ^18 || ^19`。
|
|
9
17
|
|
|
10
18
|
## 用法
|
|
11
19
|
|
|
@@ -29,11 +37,6 @@ export const App = () => {
|
|
|
29
37
|
}
|
|
30
38
|
```
|
|
31
39
|
|
|
32
|
-
```ts
|
|
33
|
-
// 样式是独立的包,且是纯 CSS(无 JS 入口),必须显式引入
|
|
34
|
-
import '@mteditor/theme-default/index.css'
|
|
35
|
-
```
|
|
36
|
-
|
|
37
40
|
React 没有 `v-model`,因此「双向绑定」在这里就是**受控组件**惯例:
|
|
38
41
|
`value` 进 / `onChange` 出,由宿主把两者接起来。
|
|
39
42
|
|
|
@@ -48,7 +51,7 @@ React 没有 `v-model`,因此「双向绑定」在这里就是**受控组件**
|
|
|
48
51
|
| `onChange` / `onReady` / `onFocus` / `onBlur` / `onSelectionChange` / `onError` / `onDestroy` | 见下表 | 全部可选 |
|
|
49
52
|
|
|
50
53
|
`className` / `style` 需要显式声明,是因为 **React 不像 Vue 那样自动透传 attrs**
|
|
51
|
-
|
|
54
|
+
到单根元素。
|
|
52
55
|
|
|
53
56
|
`MtAdapterOptions = Omit<MtEditorOptions, 'element' | 'onChange' | 'onReady' | 'onError'>`。
|
|
54
57
|
被摘掉的四个由组件自己接管;**`content` 保留**,语义收窄为「初始内容,挂载时读取一次」,
|
|
@@ -56,7 +59,6 @@ React 没有 `v-model`,因此「双向绑定」在这里就是**受控组件**
|
|
|
56
59
|
|
|
57
60
|
**这里没有 `html` prop**(与 Vue 适配层的一处刻意不对称):`html` 是有损派生物,
|
|
58
61
|
`onChange` 的载荷里已经带着它,再开一个输入通道只会多一个「谁覆盖谁」的问题。
|
|
59
|
-
Vue 侧之所以有 `v-model:html`,是因为它的绑定语法需要一个 prop 名。
|
|
60
62
|
|
|
61
63
|
**运行时可变**的选项只有三个:`theme` / `locale` / `readonly`
|
|
62
64
|
(内核分别为它们提供了 `setTheme` / `setLocale` / `setReadonly`)。
|
|
@@ -65,7 +67,7 @@ Vue 侧之所以有 `v-model:html`,是因为它的绑定语法需要一个 pro
|
|
|
65
67
|
|
|
66
68
|
## 事件
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
内核事件名**原样**沿用,只按 React 惯例加 `on` 前缀。
|
|
69
71
|
|
|
70
72
|
| Prop | 载荷 |
|
|
71
73
|
|---|---|
|
|
@@ -73,81 +75,34 @@ Vue 侧之所以有 `v-model:html`,是因为它的绑定语法需要一个 pro
|
|
|
73
75
|
| `onReady` | `{ instance }` |
|
|
74
76
|
| `onFocus` / `onBlur` | `{ instance }` |
|
|
75
77
|
| `onSelectionChange` | `MtSelectionInfo`(100ms 节流,leading + trailing) |
|
|
76
|
-
| `onError` | `MtErrorPayload
|
|
78
|
+
| `onError` | `MtErrorPayload`(**所有「闸口拒绝」都走这里**) |
|
|
77
79
|
| `onDestroy` | `{ instance }` |
|
|
78
80
|
|
|
79
81
|
`ready` 在内核的**构造函数里**就派发了,因此组件把它当**构造选项**(`onReady`)传入,
|
|
80
|
-
而不是事后 `on('ready')` 订阅 ——
|
|
81
|
-
|
|
82
|
+
而不是事后 `on('ready')` 订阅 —— 后者永远收不到。也正因如此,`onReady` 是**唯一**
|
|
83
|
+
能直接拿到「刚建好的实例」的时机。
|
|
82
84
|
|
|
83
85
|
## 暴露的句柄
|
|
84
86
|
|
|
85
87
|
```tsx
|
|
86
88
|
const ref = useRef<MtEditorReactHandle>(null)
|
|
87
|
-
// 「卸载后」为 null
|
|
88
|
-
ref.current?.editor?.command('focus')
|
|
89
|
+
ref.current?.editor?.command('focus') // 「卸载后」为 null
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
适配层**不转发内核方法**:取到 `editor` 后用法与原生完全一致。
|
|
92
93
|
组件卸载时 React 会把 `ref.current` **整体置空**,因此宿主不可能拿到已销毁的实例。
|
|
93
94
|
|
|
94
|
-
##
|
|
95
|
-
|
|
96
|
-
允许做:
|
|
97
|
-
|
|
98
|
-
1. 挂载 / 卸载底层实例;
|
|
99
|
-
2. `props` → `MtEditorOptions` 映射;
|
|
100
|
-
3. `options` 变更时**增量更新**(不重建实例);
|
|
101
|
-
4. 内核事件 → 宿主回调透传;
|
|
102
|
-
5. 受控值回灌(`value` → `setJSON()`)。
|
|
103
|
-
|
|
104
|
-
**不允许做**:任何编辑逻辑、任何依赖 React 生态辅助包。
|
|
105
|
-
|
|
106
|
-
三条具体的实现约束(ADR-0028 决策 4):
|
|
107
|
-
|
|
108
|
-
- **挂载 effect 的依赖数组必须是空的**。React 的内联回调每次渲染都是新引用,
|
|
109
|
-
放进依赖数组就等于「每敲一个字重建一次实例」,而内容是看不出问题的 ——
|
|
110
|
-
丢的是选区、撤销栈与 IME composition 状态。做法是把最新 props 存进 ref,
|
|
111
|
-
构造时传的闭包只从 ref 里读。
|
|
112
|
-
- **不在这里做回声抑制**。「灌回来的是同一份内容就不该碰视图」这条判据写在**内核**
|
|
113
|
-
(`MtEngine.setDocument()` 的结构等价短路)。适配层再判一次等于两处判据,
|
|
114
|
-
且六个适配端会各判各的。
|
|
115
|
-
- **不为了「同步内容」而重建实例**。
|
|
116
|
-
|
|
117
|
-
## 严格模式双挂载(§8.2 必测项)
|
|
95
|
+
## 严格模式
|
|
118
96
|
|
|
119
97
|
开发模式下 React 会把 effect 执行两遍(挂载 → 清理 → 再挂载)来暴露副作用泄漏。
|
|
120
|
-
本组件的清理函数会 `destroy()`
|
|
121
|
-
|
|
122
|
-
单测里用 `<StrictMode>` 实测守三条:页面上只剩**一个** `.mt-editor`、
|
|
123
|
-
内容不重复、**一次编辑只派发一次 `onChange`**(监听不叠加)。
|
|
124
|
-
|
|
125
|
-
## 依赖
|
|
126
|
-
|
|
127
|
-
| 类型 | 内容 |
|
|
128
|
-
|---|---|
|
|
129
|
-
| `dependencies` | `@mteditor/core` |
|
|
130
|
-
| `peerDependencies` | `react: ^18.0.0 \|\| ^19.0.0`(宽范围,不打包宿主运行时) |
|
|
131
|
-
|
|
132
|
-
## 测试
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
pnpm --filter @mteditor/react test # 单测(happy-dom + @testing-library/react)
|
|
136
|
-
pnpm --filter @mteditor/react typecheck
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
14 条单测覆盖五组:挂载与卸载、受控值与事件(含「回灌不产生第二次派发」与
|
|
140
|
-
「克隆后回灌」)、选项映射、`className` / `style` 透传、严格模式双挂载。
|
|
141
|
-
|
|
142
|
-
端到端在 [`../../tests/e2e/adapters.spec.ts`](../../tests/e2e/adapters.spec.ts):
|
|
143
|
-
真实浏览器里(示例页也包着 `<StrictMode>`)挂载 → 输入 → 读回**宿主侧的受控值**。
|
|
144
|
-
适配包不设覆盖率阈值(ADR-0019 只覆盖 `document` / `core` / `renderer-web`),
|
|
145
|
-
但示例必须纳入 E2E(§8.3,ADR-0028 决策 6)。
|
|
98
|
+
本组件的清理函数会 `destroy()` 实例并退订全部监听,因此第二轮拿到的是一张干净的白纸:
|
|
99
|
+
页面上只会有一个编辑器,且**一次编辑只触发一次 `onChange`**。
|
|
146
100
|
|
|
147
|
-
##
|
|
101
|
+
## 注意事项
|
|
148
102
|
|
|
149
|
-
|
|
150
|
-
|
|
103
|
+
- **不要用重建实例来「同步内容」**。「灌回来的是同一份内容就不该碰视图」这条判据在**内核**
|
|
104
|
+
(结构等价短路),适配层不重复判断。重建会丢选区、撤销栈与 IME composition 状态。
|
|
105
|
+
- 浮层(气泡 / 下拉 / 抽屉)挂在 `document.body` 上,不在组件容器内。
|
|
151
106
|
|
|
152
107
|
## 许可
|
|
153
108
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/component.tsx"],"names":["forwardRef","useRef","useEffect","MtCoreEditor","useImperativeHandle","jsx"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * React
|
|
1
|
+
{"version":3,"sources":["../src/component.tsx"],"names":["forwardRef","useRef","useEffect","MtCoreEditor","useImperativeHandle","jsx"],"mappings":";;;;;;;;;AAyDA,IAAM,mBAAA,GAAsB,CAAC,QAAA,EAAwB,OAAA,KAAoC;AACvF,EAAA,IAAI,QAAQ,KAAA,KAAU,MAAA,EAAW,QAAA,CAAS,QAAA,CAAS,QAAQ,KAAK,CAAA;AAChE,EAAA,IAAI,QAAQ,MAAA,KAAW,MAAA,EAAW,QAAA,CAAS,SAAA,CAAU,QAAQ,MAAM,CAAA;AACnE,EAAA,IAAI,QAAQ,QAAA,KAAa,MAAA,EAAW,QAAA,CAAS,WAAA,CAAY,QAAQ,QAAQ,CAAA;AAC3E,CAAA;AA2BO,IAAM,QAAA,GAAWA,gBAAA,CAAoD,CAAC,KAAA,EAAO,GAAA,KAAQ;AAE1F,EAAA,MAAM,OAAA,GAAUC,aAA8B,IAAI,CAAA;AAElD,EAAA,MAAM,WAAA,GAAcA,aAA4B,IAAI,CAAA;AAEpD,EAAA,MAAM,QAAA,GAAWA,aAAO,KAAK,CAAA;AAG7B,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,QAAA,CAAS,OAAA,GAAU,KAAA;AAAA,EACrB,CAAC,CAAA;AAGD,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,UAAU,OAAA,CAAQ,OAAA;AACxB,IAAA,IAAI,OAAA,KAAY,MAAM,OAAO,MAAA;AAI7B,IAAA,MAAM,EAAE,SAAS,cAAA,EAAgB,GAAG,aAAY,GAAI,QAAA,CAAS,OAAA,CAAQ,OAAA,IAAW,EAAC;AAEjF,IAAA,MAAM,MAAA,GAAS,IAAIC,aAAA,CAAa;AAAA,MAC9B,GAAG,WAAA;AAAA,MACH,OAAA;AAAA,MACA,OAAA,EAAS,QAAA,CAAS,OAAA,CAAQ,KAAA,IAAS,cAAA;AAAA,MACnC,UAAU,CAAC,OAAA,KAA6B,QAAA,CAAS,OAAA,CAAQ,WAAW,OAAO,CAAA;AAAA;AAAA,MAE3E,SAAS,CAAC,OAAA,KAAY,QAAA,CAAS,OAAA,CAAQ,UAAU,OAAO,CAAA;AAAA,MACxD,SAAS,CAAC,OAAA,KAA4B,QAAA,CAAS,OAAA,CAAQ,UAAU,OAAO;AAAA,KACzE,CAAA;AAED,IAAA,MAAM,IAAA,GAAO;AAAA,MACX,MAAA,CAAO,GAAG,OAAA,EAAS,CAAC,YAAY,QAAA,CAAS,OAAA,CAAQ,OAAA,GAAU,OAAO,CAAC,CAAA;AAAA,MACnE,MAAA,CAAO,GAAG,MAAA,EAAQ,CAAC,YAAY,QAAA,CAAS,OAAA,CAAQ,MAAA,GAAS,OAAO,CAAC,CAAA;AAAA,MACjE,MAAA,CAAO,EAAA;AAAA,QAAG,iBAAA;AAAA,QAAmB,CAAC,OAAA,KAC5B,QAAA,CAAS,OAAA,CAAQ,oBAAoB,OAAO;AAAA,OAC9C;AAAA,MACA,MAAA,CAAO,GAAG,SAAA,EAAW,CAAC,YAAY,QAAA,CAAS,OAAA,CAAQ,SAAA,GAAY,OAAO,CAAC;AAAA,KACzE;AAEA,IAAA,WAAA,CAAY,OAAA,GAAU,MAAA;AAEtB,IAAA,OAAO,MAAM;AACX,MAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AAEtB,MAAA,MAAA,CAAO,OAAA,EAAQ;AACf,MAAA,KAAA,MAAW,GAAA,IAAO,MAAM,GAAA,EAAI;AAAA,IAC9B,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAIL,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAW,KAAA,EAAM,GAAI,KAAA;AACpC,EAAAD,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,SAAS,WAAA,CAAY,OAAA;AAC3B,IAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AAC5C,IAAA,MAAA,CAAO,QAAQ,KAAK,CAAA;AAAA,EACtB,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAGV,EAAA,MAAM,KAAA,GAAQ,MAAM,OAAA,EAAS,KAAA;AAC7B,EAAA,MAAM,MAAA,GAAS,MAAM,OAAA,EAAS,MAAA;AAC9B,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,EAAS,QAAA;AAChC,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,SAAS,WAAA,CAAY,OAAA;AAC3B,IAAA,IAAI,MAAA,KAAW,MAAM,mBAAA,CAAoB,MAAA,EAAQ,EAAE,KAAA,EAAO,MAAA,EAAQ,UAAU,CAAA;AAAA,EAC9E,CAAA,EAAG,CAAC,KAAA,EAAO,MAAA,EAAQ,QAAQ,CAAC,CAAA;AAG5B,EAAAE,yBAAA;AAAA,IACE,GAAA;AAAA,IACA,OAAO;AAAA,MACL,IAAI,MAAA,GAAS;AACX,QAAA,OAAO,WAAA,CAAY,OAAA;AAAA,MACrB;AAAA,KACF,CAAA;AAAA,IACA;AAAC,GACH;AAIA,EAAA,uBACEC,cAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAA,EAAK,OAAA;AAAA,MACL,SAAA,EAAW,SAAA,KAAc,MAAA,GAAY,gBAAA,GAAmB,kBAAkB,SAAS,CAAA,CAAA;AAAA,MACnF,KAAA;AAAA,MACA,iBAAA,EAAgB;AAAA;AAAA,GAClB;AAEJ,CAAC;AAGD,QAAA,CAAS,WAAA,GAAc,UAAA;AAEvB,IAAO,iBAAA,GAAQ","file":"index.cjs","sourcesContent":["/**\n * React 适配组件。\n *\n * 它是**薄壳**:只做挂载 / 卸载、props → 选项映射、增量更新、事件透传与受控值回灌。\n * 任何编辑逻辑都写在 `@mteditor/core`,写在这里视为违规。\n *\n * ## 三条关键设计\n *\n * 1. **挂载 effect 只跑一次(`[]` 依赖)**。React 的内联箭头函数每次渲染都是新的引用,\n * 若把 `onChange` 之类的 props 放进依赖数组,用户每敲一个字就会「销毁 → 重建」实例,\n * 选区、撤销栈与 IME composition 状态全丢。做法是把最新 props 存进一个 ref,\n * 构造时传的闭包只从 ref 里读 —— 依赖数组因此可以是空的。\n * 2. **这里不做回声抑制**。宿主把编辑器刚给出的文档灌回来是受控组件的必然结果,\n * 而「灌回来的是同一份内容就不该碰视图」这条判据写在**内核**\n * (`MtEngine.setDocument()` 的结构等价短路)。适配层再判一次等于两处判据,\n * 且 6 个适配端会各判各的。\n * 3. **结构性选项挂载时读取一次**。内核没有「运行时换档」的能力\n * (`platform` / `ui` / `placeholder` / `historyDepth` 等都没有 setter),\n * 因此本组件不假装支持它。要换档请用 React 的重挂载机制(`key`)。\n *\n * ## 严格模式双挂载\n *\n * 开发模式下 React 会把 effect 执行两遍(挂载 → 清理 → 再挂载)来暴露副作用泄漏。\n * 本组件的清理函数会 `destroy()` 实例并退订全部监听,因此第二轮拿到的是一张干净的白纸;\n * 「页面上只剩一个 `.mt-editor`、内容不重复、监听不叠加」由单测与 E2E 守(必测项)。\n */\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'\n\nimport { MtEditor as MtCoreEditor } from '@mteditor/core'\nimport type {\n MtAdapterOptions,\n MtChangePayload,\n MtErrorPayload,\n MtSelectionInfo,\n} from '@mteditor/core'\n\nimport type { MtEditorReactHandle, MtEditorReactProps } from './types'\n\n/** 能就地同步的选项子集。类型取自 `MtAdapterOptions`,不另立一份定义 */\ntype MtRuntimeOptions = Pick<MtAdapterOptions, 'theme' | 'locale' | 'readonly'>\n\n/**\n * 把可在**运行时**就地同步的选项写进已存在的内核实例。\n *\n * 只覆盖 `theme` / `locale` / `readonly` 三项 —— 内核只为这三项提供了 setter\n * (`setTheme` / `setLocale` / `setReadonly`)。其余选项\n * (`platform` / `ui` / `placeholder` / `plugins` / `historyDepth` /\n * `changeDebounce` / `selectionThrottle`)**在挂载时读取一次**,\n * 要换档请用 React 的重挂载机制(`key`)。\n *\n * 白名单是刻意的:内核将来新增运行时 setter 时,需要**显式**加进这里才会生效 ——\n * 比「默认尝试同步」安全,因为后者会让 `ui` 之类的结构性选项被半途写入而非完整重建。\n *\n * @param instance 内核实例\n * @param options 只含「能就地改」的三项\n */\nconst applyRuntimeOptions = (instance: MtCoreEditor, options: MtRuntimeOptions): void => {\n if (options.theme !== undefined) instance.setTheme(options.theme)\n if (options.locale !== undefined) instance.setLocale(options.locale)\n if (options.readonly !== undefined) instance.setReadonly(options.readonly)\n}\n\n/**\n * `<MtEditor>` —— React 的 MtEditor 组件。\n *\n * @example\n * ```tsx\n * import { useRef, useState } from 'react'\n * import { MtEditor } from '@mteditor/react'\n * import type { MtDocument, MtEditorReactHandle } from '@mteditor/react'\n * import '@mteditor/theme-default/index.css'\n *\n * export const App = () => {\n * const [doc, setDoc] = useState<MtDocument>()\n * const ref = useRef<MtEditorReactHandle>(null)\n *\n * return (\n * <MtEditor\n * ref={ref}\n * value={doc}\n * onChange={(payload) => setDoc(payload.json)}\n * options={{ placeholder: '请输入…', theme: 'light' }}\n * />\n * )\n * }\n * ```\n */\nexport const MtEditor = forwardRef<MtEditorReactHandle, MtEditorReactProps>((props, ref) => {\n /** 挂载容器。由本组件创建,且**不给它任何子节点** —— React 因此不会碰内核建的 DOM */\n const hostRef = useRef<HTMLDivElement | null>(null)\n /** 内核实例。用 ref 而不是 state:实例变化不需要重绘(句柄走 getter 读实时值) */\n const instanceRef = useRef<MtCoreEditor | null>(null)\n /** 最新 props。让「只跑一次」的挂载 effect 也能读到当下的回调(见文件头设计 1) */\n const propsRef = useRef(props)\n\n // 不用依赖数组:每次提交后同步一次最新 props 即可\n useEffect(() => {\n propsRef.current = props\n })\n\n // 挂载 / 卸载。**依赖数组必须是空的** —— 见文件头设计 1\n useEffect(() => {\n const element = hostRef.current\n if (element === null) return undefined\n\n // `options.content` 语义是「初始内容」;受控值一旦给出就优先于它。\n // 必须先摘出来再展开,否则 `...options` 会把 `content` 覆盖回旧值。\n const { content: initialContent, ...restOptions } = propsRef.current.options ?? {}\n\n const editor = new MtCoreEditor({\n ...restOptions,\n element,\n content: propsRef.current.value ?? initialContent,\n onChange: (payload: MtChangePayload) => propsRef.current.onChange?.(payload),\n // `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅\n onReady: (payload) => propsRef.current.onReady?.(payload),\n onError: (payload: MtErrorPayload) => propsRef.current.onError?.(payload),\n })\n\n const offs = [\n editor.on('focus', (payload) => propsRef.current.onFocus?.(payload)),\n editor.on('blur', (payload) => propsRef.current.onBlur?.(payload)),\n editor.on('selectionChange', (payload: MtSelectionInfo) =>\n propsRef.current.onSelectionChange?.(payload),\n ),\n editor.on('destroy', (payload) => propsRef.current.onDestroy?.(payload)),\n ]\n\n instanceRef.current = editor\n\n return () => {\n instanceRef.current = null\n // 先销毁再退订:`destroy` 事件本身要透给宿主\n editor.destroy()\n for (const off of offs) off()\n }\n }, [])\n\n // 「编辑器 → 宿主」由 onChange 完成;这里只做「宿主 → 编辑器」。\n // 内核的结构等价短路保证回灌同一个文档时不会重置光标。\n const { value, className, style } = props\n useEffect(() => {\n const editor = instanceRef.current\n if (editor === null || value === undefined) return\n editor.setJSON(value)\n }, [value])\n\n // 选项变化:只同步能在运行时就地改的那几个(结构性选项挂载时读取一次)\n const theme = props.options?.theme\n const locale = props.options?.locale\n const readonly = props.options?.readonly\n useEffect(() => {\n const editor = instanceRef.current\n if (editor !== null) applyRuntimeOptions(editor, { theme, locale, readonly })\n }, [theme, locale, readonly])\n\n // 句柄用 getter 读实时值:实例在挂载 effect 里才产生,渲染期取值只能取到 `null`\n useImperativeHandle(\n ref,\n () => ({\n get editor() {\n return instanceRef.current\n },\n }),\n [],\n )\n\n // 内核根容器在这个 div 内部,宿主若要整体样式钩子,用 `data-mt-adapter`。\n // React 不会像 Vue 那样自动透传 class / style,故这两个 prop 需要显式声明。\n return (\n <div\n ref={hostRef}\n className={className === undefined ? 'mt-editor-host' : `mt-editor-host ${className}`}\n style={style}\n data-mt-adapter=\"react\"\n />\n )\n})\n\n/** 组件展示名。React DevTools 里显示为 `MtEditor` 而不是 `ForwardRef` */\nMtEditor.displayName = 'MtEditor'\n\nexport default MtEditor\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -8,15 +8,15 @@ export { MtAdapterOptions, MtChangePayload, MtDocument, MtEditor as MtEditorInst
|
|
|
8
8
|
*
|
|
9
9
|
* 这些类型是**给宿主写 JSX 与处理函数用的**,不是内部实现类型。
|
|
10
10
|
* 它们只是 `@mteditor/core` 的再导出与组合 —— 适配层不得自己另立一套文档模型类型,
|
|
11
|
-
*
|
|
11
|
+
* 否则「适配层的类型」与「内核的类型」会漂移成两份。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* `<MtEditor>` 的处理函数 props。
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* 名字**原样**沿用内核的事件命名(`change` / `ready` / `focus` / `blur`
|
|
18
18
|
* `selectionChange` / `error` / `destroy`),只按 React 惯例加上 `on` 前缀 ——
|
|
19
|
-
*
|
|
19
|
+
* 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法。
|
|
20
20
|
*
|
|
21
21
|
* 全部可选:不传就是「不关心该事件」,不会因为漏传而在控制台报错。
|
|
22
22
|
*/
|
|
@@ -53,7 +53,7 @@ interface MtEditorReactHandlers {
|
|
|
53
53
|
* 与 Vue 适配层的一点不对称:**这里没有 `html` prop**。
|
|
54
54
|
* `html` 是有损派生物,`v-model:html` 在 Vue 里之所以存在,是因为 Vue 的绑定语法
|
|
55
55
|
* 需要一个 prop 名;React 的 `onChange` 载荷里已经带着 `html`,
|
|
56
|
-
*
|
|
56
|
+
* 再开一个输入通道只会多一个「谁覆盖谁」的问题。
|
|
57
57
|
*/
|
|
58
58
|
interface MtEditorReactProps extends MtEditorReactHandlers {
|
|
59
59
|
/** 文档(真相源)。与 `onChange` 配合即为受控用法 */
|
package/dist/index.d.ts
CHANGED
|
@@ -8,15 +8,15 @@ export { MtAdapterOptions, MtChangePayload, MtDocument, MtEditor as MtEditorInst
|
|
|
8
8
|
*
|
|
9
9
|
* 这些类型是**给宿主写 JSX 与处理函数用的**,不是内部实现类型。
|
|
10
10
|
* 它们只是 `@mteditor/core` 的再导出与组合 —— 适配层不得自己另立一套文档模型类型,
|
|
11
|
-
*
|
|
11
|
+
* 否则「适配层的类型」与「内核的类型」会漂移成两份。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* `<MtEditor>` 的处理函数 props。
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* 名字**原样**沿用内核的事件命名(`change` / `ready` / `focus` / `blur`
|
|
18
18
|
* `selectionChange` / `error` / `destroy`),只按 React 惯例加上 `on` 前缀 ——
|
|
19
|
-
*
|
|
19
|
+
* 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法。
|
|
20
20
|
*
|
|
21
21
|
* 全部可选:不传就是「不关心该事件」,不会因为漏传而在控制台报错。
|
|
22
22
|
*/
|
|
@@ -53,7 +53,7 @@ interface MtEditorReactHandlers {
|
|
|
53
53
|
* 与 Vue 适配层的一点不对称:**这里没有 `html` prop**。
|
|
54
54
|
* `html` 是有损派生物,`v-model:html` 在 Vue 里之所以存在,是因为 Vue 的绑定语法
|
|
55
55
|
* 需要一个 prop 名;React 的 `onChange` 载荷里已经带着 `html`,
|
|
56
|
-
*
|
|
56
|
+
* 再开一个输入通道只会多一个「谁覆盖谁」的问题。
|
|
57
57
|
*/
|
|
58
58
|
interface MtEditorReactProps extends MtEditorReactHandlers {
|
|
59
59
|
/** 文档(真相源)。与 `onChange` 配合即为受控用法 */
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/component.tsx"],"names":["MtCoreEditor"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * React
|
|
1
|
+
{"version":3,"sources":["../src/component.tsx"],"names":["MtCoreEditor"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * React 适配组件。\n *\n * 它是**薄壳**:只做挂载 / 卸载、props → 选项映射、增量更新、事件透传与受控值回灌。\n * 任何编辑逻辑都写在 `@mteditor/core`,写在这里视为违规。\n *\n * ## 三条关键设计\n *\n * 1. **挂载 effect 只跑一次(`[]` 依赖)**。React 的内联箭头函数每次渲染都是新的引用,\n * 若把 `onChange` 之类的 props 放进依赖数组,用户每敲一个字就会「销毁 → 重建」实例,\n * 选区、撤销栈与 IME composition 状态全丢。做法是把最新 props 存进一个 ref,\n * 构造时传的闭包只从 ref 里读 —— 依赖数组因此可以是空的。\n * 2. **这里不做回声抑制**。宿主把编辑器刚给出的文档灌回来是受控组件的必然结果,\n * 而「灌回来的是同一份内容就不该碰视图」这条判据写在**内核**\n * (`MtEngine.setDocument()` 的结构等价短路)。适配层再判一次等于两处判据,\n * 且 6 个适配端会各判各的。\n * 3. **结构性选项挂载时读取一次**。内核没有「运行时换档」的能力\n * (`platform` / `ui` / `placeholder` / `historyDepth` 等都没有 setter),\n * 因此本组件不假装支持它。要换档请用 React 的重挂载机制(`key`)。\n *\n * ## 严格模式双挂载\n *\n * 开发模式下 React 会把 effect 执行两遍(挂载 → 清理 → 再挂载)来暴露副作用泄漏。\n * 本组件的清理函数会 `destroy()` 实例并退订全部监听,因此第二轮拿到的是一张干净的白纸;\n * 「页面上只剩一个 `.mt-editor`、内容不重复、监听不叠加」由单测与 E2E 守(必测项)。\n */\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'\n\nimport { MtEditor as MtCoreEditor } from '@mteditor/core'\nimport type {\n MtAdapterOptions,\n MtChangePayload,\n MtErrorPayload,\n MtSelectionInfo,\n} from '@mteditor/core'\n\nimport type { MtEditorReactHandle, MtEditorReactProps } from './types'\n\n/** 能就地同步的选项子集。类型取自 `MtAdapterOptions`,不另立一份定义 */\ntype MtRuntimeOptions = Pick<MtAdapterOptions, 'theme' | 'locale' | 'readonly'>\n\n/**\n * 把可在**运行时**就地同步的选项写进已存在的内核实例。\n *\n * 只覆盖 `theme` / `locale` / `readonly` 三项 —— 内核只为这三项提供了 setter\n * (`setTheme` / `setLocale` / `setReadonly`)。其余选项\n * (`platform` / `ui` / `placeholder` / `plugins` / `historyDepth` /\n * `changeDebounce` / `selectionThrottle`)**在挂载时读取一次**,\n * 要换档请用 React 的重挂载机制(`key`)。\n *\n * 白名单是刻意的:内核将来新增运行时 setter 时,需要**显式**加进这里才会生效 ——\n * 比「默认尝试同步」安全,因为后者会让 `ui` 之类的结构性选项被半途写入而非完整重建。\n *\n * @param instance 内核实例\n * @param options 只含「能就地改」的三项\n */\nconst applyRuntimeOptions = (instance: MtCoreEditor, options: MtRuntimeOptions): void => {\n if (options.theme !== undefined) instance.setTheme(options.theme)\n if (options.locale !== undefined) instance.setLocale(options.locale)\n if (options.readonly !== undefined) instance.setReadonly(options.readonly)\n}\n\n/**\n * `<MtEditor>` —— React 的 MtEditor 组件。\n *\n * @example\n * ```tsx\n * import { useRef, useState } from 'react'\n * import { MtEditor } from '@mteditor/react'\n * import type { MtDocument, MtEditorReactHandle } from '@mteditor/react'\n * import '@mteditor/theme-default/index.css'\n *\n * export const App = () => {\n * const [doc, setDoc] = useState<MtDocument>()\n * const ref = useRef<MtEditorReactHandle>(null)\n *\n * return (\n * <MtEditor\n * ref={ref}\n * value={doc}\n * onChange={(payload) => setDoc(payload.json)}\n * options={{ placeholder: '请输入…', theme: 'light' }}\n * />\n * )\n * }\n * ```\n */\nexport const MtEditor = forwardRef<MtEditorReactHandle, MtEditorReactProps>((props, ref) => {\n /** 挂载容器。由本组件创建,且**不给它任何子节点** —— React 因此不会碰内核建的 DOM */\n const hostRef = useRef<HTMLDivElement | null>(null)\n /** 内核实例。用 ref 而不是 state:实例变化不需要重绘(句柄走 getter 读实时值) */\n const instanceRef = useRef<MtCoreEditor | null>(null)\n /** 最新 props。让「只跑一次」的挂载 effect 也能读到当下的回调(见文件头设计 1) */\n const propsRef = useRef(props)\n\n // 不用依赖数组:每次提交后同步一次最新 props 即可\n useEffect(() => {\n propsRef.current = props\n })\n\n // 挂载 / 卸载。**依赖数组必须是空的** —— 见文件头设计 1\n useEffect(() => {\n const element = hostRef.current\n if (element === null) return undefined\n\n // `options.content` 语义是「初始内容」;受控值一旦给出就优先于它。\n // 必须先摘出来再展开,否则 `...options` 会把 `content` 覆盖回旧值。\n const { content: initialContent, ...restOptions } = propsRef.current.options ?? {}\n\n const editor = new MtCoreEditor({\n ...restOptions,\n element,\n content: propsRef.current.value ?? initialContent,\n onChange: (payload: MtChangePayload) => propsRef.current.onChange?.(payload),\n // `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅\n onReady: (payload) => propsRef.current.onReady?.(payload),\n onError: (payload: MtErrorPayload) => propsRef.current.onError?.(payload),\n })\n\n const offs = [\n editor.on('focus', (payload) => propsRef.current.onFocus?.(payload)),\n editor.on('blur', (payload) => propsRef.current.onBlur?.(payload)),\n editor.on('selectionChange', (payload: MtSelectionInfo) =>\n propsRef.current.onSelectionChange?.(payload),\n ),\n editor.on('destroy', (payload) => propsRef.current.onDestroy?.(payload)),\n ]\n\n instanceRef.current = editor\n\n return () => {\n instanceRef.current = null\n // 先销毁再退订:`destroy` 事件本身要透给宿主\n editor.destroy()\n for (const off of offs) off()\n }\n }, [])\n\n // 「编辑器 → 宿主」由 onChange 完成;这里只做「宿主 → 编辑器」。\n // 内核的结构等价短路保证回灌同一个文档时不会重置光标。\n const { value, className, style } = props\n useEffect(() => {\n const editor = instanceRef.current\n if (editor === null || value === undefined) return\n editor.setJSON(value)\n }, [value])\n\n // 选项变化:只同步能在运行时就地改的那几个(结构性选项挂载时读取一次)\n const theme = props.options?.theme\n const locale = props.options?.locale\n const readonly = props.options?.readonly\n useEffect(() => {\n const editor = instanceRef.current\n if (editor !== null) applyRuntimeOptions(editor, { theme, locale, readonly })\n }, [theme, locale, readonly])\n\n // 句柄用 getter 读实时值:实例在挂载 effect 里才产生,渲染期取值只能取到 `null`\n useImperativeHandle(\n ref,\n () => ({\n get editor() {\n return instanceRef.current\n },\n }),\n [],\n )\n\n // 内核根容器在这个 div 内部,宿主若要整体样式钩子,用 `data-mt-adapter`。\n // React 不会像 Vue 那样自动透传 class / style,故这两个 prop 需要显式声明。\n return (\n <div\n ref={hostRef}\n className={className === undefined ? 'mt-editor-host' : `mt-editor-host ${className}`}\n style={style}\n data-mt-adapter=\"react\"\n />\n )\n})\n\n/** 组件展示名。React DevTools 里显示为 `MtEditor` 而不是 `ForwardRef` */\nMtEditor.displayName = 'MtEditor'\n\nexport default MtEditor\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mteditor/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "MtEditor 的 React 适配层:forwardRef 薄壳,严格模式双挂载幂等",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mteditor",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@mteditor/core": "0.1.
|
|
50
|
+
"@mteditor/core": "0.1.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0 || ^19.0.0"
|