@mteditor/react 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +108 -2
- package/dist/index.cjs +89 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MtEditor contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,109 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mteditor/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> MtEditor 的 React 适配层 —— **薄壳,不含任何编辑逻辑**。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
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`。
|
|
17
|
+
|
|
18
|
+
## 用法
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { useRef, useState } from 'react'
|
|
22
|
+
import { MtEditor } from '@mteditor/react'
|
|
23
|
+
import type { MtDocument, MtEditorReactHandle } from '@mteditor/react'
|
|
24
|
+
|
|
25
|
+
export const App = () => {
|
|
26
|
+
const [doc, setDoc] = useState<MtDocument>()
|
|
27
|
+
const ref = useRef<MtEditorReactHandle>(null)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<MtEditor
|
|
31
|
+
ref={ref}
|
|
32
|
+
value={doc}
|
|
33
|
+
onChange={(payload) => setDoc(payload.json)}
|
|
34
|
+
options={{ placeholder: '请输入…', theme: 'light' }}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
React 没有 `v-model`,因此「双向绑定」在这里就是**受控组件**惯例:
|
|
41
|
+
`value` 进 / `onChange` 出,由宿主把两者接起来。
|
|
42
|
+
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
| Prop | 类型 | 说明 |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `value` | `MtDocument \| undefined` | 文档(真相源) |
|
|
48
|
+
| `options` | `MtAdapterOptions` | 其余内核选项 |
|
|
49
|
+
| `className` | `string` | 宿主容器的类名(会与 `mt-editor-host` 合并) |
|
|
50
|
+
| `style` | `CSSProperties` | 宿主容器的行内样式 |
|
|
51
|
+
| `onChange` / `onReady` / `onFocus` / `onBlur` / `onSelectionChange` / `onError` / `onDestroy` | 见下表 | 全部可选 |
|
|
52
|
+
|
|
53
|
+
`className` / `style` 需要显式声明,是因为 **React 不像 Vue 那样自动透传 attrs**
|
|
54
|
+
到单根元素。
|
|
55
|
+
|
|
56
|
+
`MtAdapterOptions = Omit<MtEditorOptions, 'element' | 'onChange' | 'onReady' | 'onError'>`。
|
|
57
|
+
被摘掉的四个由组件自己接管;**`content` 保留**,语义收窄为「初始内容,挂载时读取一次」,
|
|
58
|
+
`value` 一旦给出就优先于它。
|
|
59
|
+
|
|
60
|
+
**这里没有 `html` prop**(与 Vue 适配层的一处刻意不对称):`html` 是有损派生物,
|
|
61
|
+
`onChange` 的载荷里已经带着它,再开一个输入通道只会多一个「谁覆盖谁」的问题。
|
|
62
|
+
|
|
63
|
+
**运行时可变**的选项只有三个:`theme` / `locale` / `readonly`
|
|
64
|
+
(内核分别为它们提供了 `setTheme` / `setLocale` / `setReadonly`)。
|
|
65
|
+
其余(`platform` / `ui` / `placeholder` / `plugins` / `historyDepth` /
|
|
66
|
+
`changeDebounce` / `selectionThrottle`)**挂载时读取一次**,要换档请用 `key` 重挂载。
|
|
67
|
+
|
|
68
|
+
## 事件
|
|
69
|
+
|
|
70
|
+
内核事件名**原样**沿用,只按 React 惯例加 `on` 前缀。
|
|
71
|
+
|
|
72
|
+
| Prop | 载荷 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `onChange` | `MtChangePayload`(**150ms 防抖**) |
|
|
75
|
+
| `onReady` | `{ instance }` |
|
|
76
|
+
| `onFocus` / `onBlur` | `{ instance }` |
|
|
77
|
+
| `onSelectionChange` | `MtSelectionInfo`(100ms 节流,leading + trailing) |
|
|
78
|
+
| `onError` | `MtErrorPayload`(**所有「闸口拒绝」都走这里**) |
|
|
79
|
+
| `onDestroy` | `{ instance }` |
|
|
80
|
+
|
|
81
|
+
`ready` 在内核的**构造函数里**就派发了,因此组件把它当**构造选项**(`onReady`)传入,
|
|
82
|
+
而不是事后 `on('ready')` 订阅 —— 后者永远收不到。也正因如此,`onReady` 是**唯一**
|
|
83
|
+
能直接拿到「刚建好的实例」的时机。
|
|
84
|
+
|
|
85
|
+
## 暴露的句柄
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const ref = useRef<MtEditorReactHandle>(null)
|
|
89
|
+
ref.current?.editor?.command('focus') // 「卸载后」为 null
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
适配层**不转发内核方法**:取到 `editor` 后用法与原生完全一致。
|
|
93
|
+
组件卸载时 React 会把 `ref.current` **整体置空**,因此宿主不可能拿到已销毁的实例。
|
|
94
|
+
|
|
95
|
+
## 严格模式
|
|
96
|
+
|
|
97
|
+
开发模式下 React 会把 effect 执行两遍(挂载 → 清理 → 再挂载)来暴露副作用泄漏。
|
|
98
|
+
本组件的清理函数会 `destroy()` 实例并退订全部监听,因此第二轮拿到的是一张干净的白纸:
|
|
99
|
+
页面上只会有一个编辑器,且**一次编辑只触发一次 `onChange`**。
|
|
100
|
+
|
|
101
|
+
## 注意事项
|
|
102
|
+
|
|
103
|
+
- **不要用重建实例来「同步内容」**。「灌回来的是同一份内容就不该碰视图」这条判据在**内核**
|
|
104
|
+
(结构等价短路),适配层不重复判断。重建会丢选区、撤销栈与 IME composition 状态。
|
|
105
|
+
- 浮层(气泡 / 下拉 / 抽屉)挂在 `document.body` 上,不在组件容器内。
|
|
106
|
+
|
|
107
|
+
## 许可
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var core = require('@mteditor/core');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
// src/component.tsx
|
|
10
|
+
var applyRuntimeOptions = (instance, options) => {
|
|
11
|
+
if (options.theme !== void 0) instance.setTheme(options.theme);
|
|
12
|
+
if (options.locale !== void 0) instance.setLocale(options.locale);
|
|
13
|
+
if (options.readonly !== void 0) instance.setReadonly(options.readonly);
|
|
14
|
+
};
|
|
15
|
+
var MtEditor = react.forwardRef((props, ref) => {
|
|
16
|
+
const hostRef = react.useRef(null);
|
|
17
|
+
const instanceRef = react.useRef(null);
|
|
18
|
+
const propsRef = react.useRef(props);
|
|
19
|
+
react.useEffect(() => {
|
|
20
|
+
propsRef.current = props;
|
|
21
|
+
});
|
|
22
|
+
react.useEffect(() => {
|
|
23
|
+
const element = hostRef.current;
|
|
24
|
+
if (element === null) return void 0;
|
|
25
|
+
const { content: initialContent, ...restOptions } = propsRef.current.options ?? {};
|
|
26
|
+
const editor = new core.MtEditor({
|
|
27
|
+
...restOptions,
|
|
28
|
+
element,
|
|
29
|
+
content: propsRef.current.value ?? initialContent,
|
|
30
|
+
onChange: (payload) => propsRef.current.onChange?.(payload),
|
|
31
|
+
// `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅
|
|
32
|
+
onReady: (payload) => propsRef.current.onReady?.(payload),
|
|
33
|
+
onError: (payload) => propsRef.current.onError?.(payload)
|
|
34
|
+
});
|
|
35
|
+
const offs = [
|
|
36
|
+
editor.on("focus", (payload) => propsRef.current.onFocus?.(payload)),
|
|
37
|
+
editor.on("blur", (payload) => propsRef.current.onBlur?.(payload)),
|
|
38
|
+
editor.on(
|
|
39
|
+
"selectionChange",
|
|
40
|
+
(payload) => propsRef.current.onSelectionChange?.(payload)
|
|
41
|
+
),
|
|
42
|
+
editor.on("destroy", (payload) => propsRef.current.onDestroy?.(payload))
|
|
43
|
+
];
|
|
44
|
+
instanceRef.current = editor;
|
|
45
|
+
return () => {
|
|
46
|
+
instanceRef.current = null;
|
|
47
|
+
editor.destroy();
|
|
48
|
+
for (const off of offs) off();
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
const { value, className, style } = props;
|
|
52
|
+
react.useEffect(() => {
|
|
53
|
+
const editor = instanceRef.current;
|
|
54
|
+
if (editor === null || value === void 0) return;
|
|
55
|
+
editor.setJSON(value);
|
|
56
|
+
}, [value]);
|
|
57
|
+
const theme = props.options?.theme;
|
|
58
|
+
const locale = props.options?.locale;
|
|
59
|
+
const readonly = props.options?.readonly;
|
|
60
|
+
react.useEffect(() => {
|
|
61
|
+
const editor = instanceRef.current;
|
|
62
|
+
if (editor !== null) applyRuntimeOptions(editor, { theme, locale, readonly });
|
|
63
|
+
}, [theme, locale, readonly]);
|
|
64
|
+
react.useImperativeHandle(
|
|
65
|
+
ref,
|
|
66
|
+
() => ({
|
|
67
|
+
get editor() {
|
|
68
|
+
return instanceRef.current;
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
[]
|
|
72
|
+
);
|
|
73
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
74
|
+
"div",
|
|
75
|
+
{
|
|
76
|
+
ref: hostRef,
|
|
77
|
+
className: className === void 0 ? "mt-editor-host" : `mt-editor-host ${className}`,
|
|
78
|
+
style,
|
|
79
|
+
"data-mt-adapter": "react"
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
MtEditor.displayName = "MtEditor";
|
|
84
|
+
var component_default = MtEditor;
|
|
85
|
+
|
|
86
|
+
exports.MtEditor = MtEditor;
|
|
87
|
+
exports.default = component_default;
|
|
88
|
+
//# sourceMappingURL=index.cjs.map
|
|
89
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
import { MtChangePayload, MtEditor as MtEditor$1, MtSelectionInfo, MtErrorPayload, MtDocument, MtAdapterOptions } from '@mteditor/core';
|
|
4
|
+
export { MtAdapterOptions, MtChangePayload, MtDocument, MtEditor as MtEditorInstance, MtErrorPayload, MtSelectionInfo, MtThemeName } from '@mteditor/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@mteditor/react` 的公开类型面。
|
|
8
|
+
*
|
|
9
|
+
* 这些类型是**给宿主写 JSX 与处理函数用的**,不是内部实现类型。
|
|
10
|
+
* 它们只是 `@mteditor/core` 的再导出与组合 —— 适配层不得自己另立一套文档模型类型,
|
|
11
|
+
* 否则「适配层的类型」与「内核的类型」会漂移成两份。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `<MtEditor>` 的处理函数 props。
|
|
16
|
+
*
|
|
17
|
+
* 名字**原样**沿用内核的事件命名(`change` / `ready` / `focus` / `blur`
|
|
18
|
+
* `selectionChange` / `error` / `destroy`),只按 React 惯例加上 `on` 前缀 ——
|
|
19
|
+
* 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法。
|
|
20
|
+
*
|
|
21
|
+
* 全部可选:不传就是「不关心该事件」,不会因为漏传而在控制台报错。
|
|
22
|
+
*/
|
|
23
|
+
interface MtEditorReactHandlers {
|
|
24
|
+
/** 内容变化(**150ms 防抖**)。载荷含 `json` / `html` / `text` 三份产物 */
|
|
25
|
+
onChange?: (payload: MtChangePayload) => void;
|
|
26
|
+
/** 实例就绪。**在构造期同步派发**,因此它是唯一能拿到「刚建好的实例」的时机 */
|
|
27
|
+
onReady?: (payload: {
|
|
28
|
+
instance: MtEditor$1;
|
|
29
|
+
}) => void;
|
|
30
|
+
/** 编辑器获得焦点 */
|
|
31
|
+
onFocus?: (payload: {
|
|
32
|
+
instance: MtEditor$1;
|
|
33
|
+
}) => void;
|
|
34
|
+
/** 编辑器失去焦点 */
|
|
35
|
+
onBlur?: (payload: {
|
|
36
|
+
instance: MtEditor$1;
|
|
37
|
+
}) => void;
|
|
38
|
+
/** 选区变化(**100ms 节流**,leading + trailing) */
|
|
39
|
+
onSelectionChange?: (payload: MtSelectionInfo) => void;
|
|
40
|
+
/** 内核错误。**所有「闸口拒绝」都走这里**,不允许只 `console.warn` */
|
|
41
|
+
onError?: (payload: MtErrorPayload) => void;
|
|
42
|
+
/** 实例销毁(组件卸载时也会走到) */
|
|
43
|
+
onDestroy?: (payload: {
|
|
44
|
+
instance: MtEditor$1;
|
|
45
|
+
}) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `<MtEditor>` 的 props。
|
|
49
|
+
*
|
|
50
|
+
* React 没有 `v-model`,因此「双向绑定」在 React 侧表现为 **`value` 进 / `onChange` 出**,
|
|
51
|
+
* 由宿主自己把两者接起来(受控组件惯例)。
|
|
52
|
+
*
|
|
53
|
+
* 与 Vue 适配层的一点不对称:**这里没有 `html` prop**。
|
|
54
|
+
* `html` 是有损派生物,`v-model:html` 在 Vue 里之所以存在,是因为 Vue 的绑定语法
|
|
55
|
+
* 需要一个 prop 名;React 的 `onChange` 载荷里已经带着 `html`,
|
|
56
|
+
* 再开一个输入通道只会多一个「谁覆盖谁」的问题。
|
|
57
|
+
*/
|
|
58
|
+
interface MtEditorReactProps extends MtEditorReactHandlers {
|
|
59
|
+
/** 文档(真相源)。与 `onChange` 配合即为受控用法 */
|
|
60
|
+
value?: MtDocument;
|
|
61
|
+
/** 其余内核选项。见 `MtAdapterOptions` */
|
|
62
|
+
options?: MtAdapterOptions;
|
|
63
|
+
/** 宿主容器的类名。Vue 侧由 attrs 透传自动完成,React 需要显式声明 */
|
|
64
|
+
className?: string;
|
|
65
|
+
/** 宿主容器的行内样式。同上 */
|
|
66
|
+
style?: CSSProperties;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 通过 `ref` 取到的命令式句柄。
|
|
70
|
+
*
|
|
71
|
+
* 只暴露 `editor`:适配层是薄壳,不该把内核的方法再转发一遍 ——
|
|
72
|
+
* 那样每加一个内核方法就要改 6 个适配端,且「适配层的方法」会与内核文档分叉。
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* const editorRef = useRef<MtEditorReactHandle>(null)
|
|
77
|
+
* // …
|
|
78
|
+
* editorRef.current?.editor?.command('focus')
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
interface MtEditorReactHandle {
|
|
82
|
+
/** 底层内核实例;未挂载(或已卸载、或正处于严格模式的重挂载空档)时为 `null` */
|
|
83
|
+
readonly editor: MtEditor$1 | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `<MtEditor>` —— React 的 MtEditor 组件。
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* import { useRef, useState } from 'react'
|
|
92
|
+
* import { MtEditor } from '@mteditor/react'
|
|
93
|
+
* import type { MtDocument, MtEditorReactHandle } from '@mteditor/react'
|
|
94
|
+
* import '@mteditor/theme-default/index.css'
|
|
95
|
+
*
|
|
96
|
+
* export const App = () => {
|
|
97
|
+
* const [doc, setDoc] = useState<MtDocument>()
|
|
98
|
+
* const ref = useRef<MtEditorReactHandle>(null)
|
|
99
|
+
*
|
|
100
|
+
* return (
|
|
101
|
+
* <MtEditor
|
|
102
|
+
* ref={ref}
|
|
103
|
+
* value={doc}
|
|
104
|
+
* onChange={(payload) => setDoc(payload.json)}
|
|
105
|
+
* options={{ placeholder: '请输入…', theme: 'light' }}
|
|
106
|
+
* />
|
|
107
|
+
* )
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare const MtEditor: react.ForwardRefExoticComponent<MtEditorReactProps & react.RefAttributes<MtEditorReactHandle>>;
|
|
112
|
+
|
|
113
|
+
export { MtEditor, type MtEditorReactHandle, type MtEditorReactHandlers, type MtEditorReactProps, MtEditor as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
import { MtChangePayload, MtEditor as MtEditor$1, MtSelectionInfo, MtErrorPayload, MtDocument, MtAdapterOptions } from '@mteditor/core';
|
|
4
|
+
export { MtAdapterOptions, MtChangePayload, MtDocument, MtEditor as MtEditorInstance, MtErrorPayload, MtSelectionInfo, MtThemeName } from '@mteditor/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@mteditor/react` 的公开类型面。
|
|
8
|
+
*
|
|
9
|
+
* 这些类型是**给宿主写 JSX 与处理函数用的**,不是内部实现类型。
|
|
10
|
+
* 它们只是 `@mteditor/core` 的再导出与组合 —— 适配层不得自己另立一套文档模型类型,
|
|
11
|
+
* 否则「适配层的类型」与「内核的类型」会漂移成两份。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `<MtEditor>` 的处理函数 props。
|
|
16
|
+
*
|
|
17
|
+
* 名字**原样**沿用内核的事件命名(`change` / `ready` / `focus` / `blur`
|
|
18
|
+
* `selectionChange` / `error` / `destroy`),只按 React 惯例加上 `on` 前缀 ——
|
|
19
|
+
* 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法。
|
|
20
|
+
*
|
|
21
|
+
* 全部可选:不传就是「不关心该事件」,不会因为漏传而在控制台报错。
|
|
22
|
+
*/
|
|
23
|
+
interface MtEditorReactHandlers {
|
|
24
|
+
/** 内容变化(**150ms 防抖**)。载荷含 `json` / `html` / `text` 三份产物 */
|
|
25
|
+
onChange?: (payload: MtChangePayload) => void;
|
|
26
|
+
/** 实例就绪。**在构造期同步派发**,因此它是唯一能拿到「刚建好的实例」的时机 */
|
|
27
|
+
onReady?: (payload: {
|
|
28
|
+
instance: MtEditor$1;
|
|
29
|
+
}) => void;
|
|
30
|
+
/** 编辑器获得焦点 */
|
|
31
|
+
onFocus?: (payload: {
|
|
32
|
+
instance: MtEditor$1;
|
|
33
|
+
}) => void;
|
|
34
|
+
/** 编辑器失去焦点 */
|
|
35
|
+
onBlur?: (payload: {
|
|
36
|
+
instance: MtEditor$1;
|
|
37
|
+
}) => void;
|
|
38
|
+
/** 选区变化(**100ms 节流**,leading + trailing) */
|
|
39
|
+
onSelectionChange?: (payload: MtSelectionInfo) => void;
|
|
40
|
+
/** 内核错误。**所有「闸口拒绝」都走这里**,不允许只 `console.warn` */
|
|
41
|
+
onError?: (payload: MtErrorPayload) => void;
|
|
42
|
+
/** 实例销毁(组件卸载时也会走到) */
|
|
43
|
+
onDestroy?: (payload: {
|
|
44
|
+
instance: MtEditor$1;
|
|
45
|
+
}) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `<MtEditor>` 的 props。
|
|
49
|
+
*
|
|
50
|
+
* React 没有 `v-model`,因此「双向绑定」在 React 侧表现为 **`value` 进 / `onChange` 出**,
|
|
51
|
+
* 由宿主自己把两者接起来(受控组件惯例)。
|
|
52
|
+
*
|
|
53
|
+
* 与 Vue 适配层的一点不对称:**这里没有 `html` prop**。
|
|
54
|
+
* `html` 是有损派生物,`v-model:html` 在 Vue 里之所以存在,是因为 Vue 的绑定语法
|
|
55
|
+
* 需要一个 prop 名;React 的 `onChange` 载荷里已经带着 `html`,
|
|
56
|
+
* 再开一个输入通道只会多一个「谁覆盖谁」的问题。
|
|
57
|
+
*/
|
|
58
|
+
interface MtEditorReactProps extends MtEditorReactHandlers {
|
|
59
|
+
/** 文档(真相源)。与 `onChange` 配合即为受控用法 */
|
|
60
|
+
value?: MtDocument;
|
|
61
|
+
/** 其余内核选项。见 `MtAdapterOptions` */
|
|
62
|
+
options?: MtAdapterOptions;
|
|
63
|
+
/** 宿主容器的类名。Vue 侧由 attrs 透传自动完成,React 需要显式声明 */
|
|
64
|
+
className?: string;
|
|
65
|
+
/** 宿主容器的行内样式。同上 */
|
|
66
|
+
style?: CSSProperties;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 通过 `ref` 取到的命令式句柄。
|
|
70
|
+
*
|
|
71
|
+
* 只暴露 `editor`:适配层是薄壳,不该把内核的方法再转发一遍 ——
|
|
72
|
+
* 那样每加一个内核方法就要改 6 个适配端,且「适配层的方法」会与内核文档分叉。
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* const editorRef = useRef<MtEditorReactHandle>(null)
|
|
77
|
+
* // …
|
|
78
|
+
* editorRef.current?.editor?.command('focus')
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
interface MtEditorReactHandle {
|
|
82
|
+
/** 底层内核实例;未挂载(或已卸载、或正处于严格模式的重挂载空档)时为 `null` */
|
|
83
|
+
readonly editor: MtEditor$1 | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `<MtEditor>` —— React 的 MtEditor 组件。
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* import { useRef, useState } from 'react'
|
|
92
|
+
* import { MtEditor } from '@mteditor/react'
|
|
93
|
+
* import type { MtDocument, MtEditorReactHandle } from '@mteditor/react'
|
|
94
|
+
* import '@mteditor/theme-default/index.css'
|
|
95
|
+
*
|
|
96
|
+
* export const App = () => {
|
|
97
|
+
* const [doc, setDoc] = useState<MtDocument>()
|
|
98
|
+
* const ref = useRef<MtEditorReactHandle>(null)
|
|
99
|
+
*
|
|
100
|
+
* return (
|
|
101
|
+
* <MtEditor
|
|
102
|
+
* ref={ref}
|
|
103
|
+
* value={doc}
|
|
104
|
+
* onChange={(payload) => setDoc(payload.json)}
|
|
105
|
+
* options={{ placeholder: '请输入…', theme: 'light' }}
|
|
106
|
+
* />
|
|
107
|
+
* )
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare const MtEditor: react.ForwardRefExoticComponent<MtEditorReactProps & react.RefAttributes<MtEditorReactHandle>>;
|
|
112
|
+
|
|
113
|
+
export { MtEditor, type MtEditorReactHandle, type MtEditorReactHandlers, type MtEditorReactProps, MtEditor as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { forwardRef, useRef, useEffect, useImperativeHandle } from 'react';
|
|
2
|
+
import { MtEditor as MtEditor$1 } from '@mteditor/core';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/component.tsx
|
|
6
|
+
var applyRuntimeOptions = (instance, options) => {
|
|
7
|
+
if (options.theme !== void 0) instance.setTheme(options.theme);
|
|
8
|
+
if (options.locale !== void 0) instance.setLocale(options.locale);
|
|
9
|
+
if (options.readonly !== void 0) instance.setReadonly(options.readonly);
|
|
10
|
+
};
|
|
11
|
+
var MtEditor = forwardRef((props, ref) => {
|
|
12
|
+
const hostRef = useRef(null);
|
|
13
|
+
const instanceRef = useRef(null);
|
|
14
|
+
const propsRef = useRef(props);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
propsRef.current = props;
|
|
17
|
+
});
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const element = hostRef.current;
|
|
20
|
+
if (element === null) return void 0;
|
|
21
|
+
const { content: initialContent, ...restOptions } = propsRef.current.options ?? {};
|
|
22
|
+
const editor = new MtEditor$1({
|
|
23
|
+
...restOptions,
|
|
24
|
+
element,
|
|
25
|
+
content: propsRef.current.value ?? initialContent,
|
|
26
|
+
onChange: (payload) => propsRef.current.onChange?.(payload),
|
|
27
|
+
// `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅
|
|
28
|
+
onReady: (payload) => propsRef.current.onReady?.(payload),
|
|
29
|
+
onError: (payload) => propsRef.current.onError?.(payload)
|
|
30
|
+
});
|
|
31
|
+
const offs = [
|
|
32
|
+
editor.on("focus", (payload) => propsRef.current.onFocus?.(payload)),
|
|
33
|
+
editor.on("blur", (payload) => propsRef.current.onBlur?.(payload)),
|
|
34
|
+
editor.on(
|
|
35
|
+
"selectionChange",
|
|
36
|
+
(payload) => propsRef.current.onSelectionChange?.(payload)
|
|
37
|
+
),
|
|
38
|
+
editor.on("destroy", (payload) => propsRef.current.onDestroy?.(payload))
|
|
39
|
+
];
|
|
40
|
+
instanceRef.current = editor;
|
|
41
|
+
return () => {
|
|
42
|
+
instanceRef.current = null;
|
|
43
|
+
editor.destroy();
|
|
44
|
+
for (const off of offs) off();
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
const { value, className, style } = props;
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const editor = instanceRef.current;
|
|
50
|
+
if (editor === null || value === void 0) return;
|
|
51
|
+
editor.setJSON(value);
|
|
52
|
+
}, [value]);
|
|
53
|
+
const theme = props.options?.theme;
|
|
54
|
+
const locale = props.options?.locale;
|
|
55
|
+
const readonly = props.options?.readonly;
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const editor = instanceRef.current;
|
|
58
|
+
if (editor !== null) applyRuntimeOptions(editor, { theme, locale, readonly });
|
|
59
|
+
}, [theme, locale, readonly]);
|
|
60
|
+
useImperativeHandle(
|
|
61
|
+
ref,
|
|
62
|
+
() => ({
|
|
63
|
+
get editor() {
|
|
64
|
+
return instanceRef.current;
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
[]
|
|
68
|
+
);
|
|
69
|
+
return /* @__PURE__ */ jsx(
|
|
70
|
+
"div",
|
|
71
|
+
{
|
|
72
|
+
ref: hostRef,
|
|
73
|
+
className: className === void 0 ? "mt-editor-host" : `mt-editor-host ${className}`,
|
|
74
|
+
style,
|
|
75
|
+
"data-mt-adapter": "react"
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
MtEditor.displayName = "MtEditor";
|
|
80
|
+
var component_default = MtEditor;
|
|
81
|
+
|
|
82
|
+
export { MtEditor, component_default as default };
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
84
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mteditor/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MtEditor 的 React 适配层:forwardRef 薄壳,严格模式双挂载幂等",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mteditor",
|
|
7
|
+
"react",
|
|
8
|
+
"editor",
|
|
9
|
+
"rich-text"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://gitee.com/meilitao/editor",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://gitee.com/meilitao/editor/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://gitee.com/meilitao/editor.git"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/index.d.cts",
|
|
36
|
+
"default": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@mteditor/core": "0.1.1"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/dom": "^10.4.2",
|
|
57
|
+
"@testing-library/react": "^16.1.0",
|
|
58
|
+
"@types/react": "^18.3.18",
|
|
59
|
+
"happy-dom": "^15.11.7",
|
|
60
|
+
"react": "^18.3.1",
|
|
61
|
+
"react-dom": "^18.3.1",
|
|
62
|
+
"rimraf": "^6.0.1",
|
|
63
|
+
"tsup": "^8.3.5",
|
|
64
|
+
"typescript": "^5.7.2",
|
|
65
|
+
"vitest": "^2.1.8"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsup",
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"test": "vitest run --passWithNoTests",
|
|
71
|
+
"test:watch": "vitest",
|
|
72
|
+
"clean": "rimraf dist .turbo"
|
|
73
|
+
}
|
|
6
74
|
}
|