@mteditor/react 0.0.0-stage → 0.1.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/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,154 @@
1
- # Temporary Holding Version
1
+ # @mteditor/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > MtEditor 的 React 适配层 —— **薄壳,不含任何编辑逻辑**。
4
+
5
+ ## 状态
6
+
7
+ **M3-c 已落地**。组件、类型、单测与可跑通示例均在本包内;
8
+ 可跑通的最小示例见 [`../../examples/react/`](../../examples/react/)。
9
+
10
+ ## 用法
11
+
12
+ ```tsx
13
+ import { useRef, useState } from 'react'
14
+ import { MtEditor } from '@mteditor/react'
15
+ import type { MtDocument, MtEditorReactHandle } from '@mteditor/react'
16
+
17
+ export const App = () => {
18
+ const [doc, setDoc] = useState<MtDocument>()
19
+ const ref = useRef<MtEditorReactHandle>(null)
20
+
21
+ return (
22
+ <MtEditor
23
+ ref={ref}
24
+ value={doc}
25
+ onChange={(payload) => setDoc(payload.json)}
26
+ options={{ placeholder: '请输入…', theme: 'light' }}
27
+ />
28
+ )
29
+ }
30
+ ```
31
+
32
+ ```ts
33
+ // 样式是独立的包,且是纯 CSS(无 JS 入口),必须显式引入
34
+ import '@mteditor/theme-default/index.css'
35
+ ```
36
+
37
+ React 没有 `v-model`,因此「双向绑定」在这里就是**受控组件**惯例:
38
+ `value` 进 / `onChange` 出,由宿主把两者接起来。
39
+
40
+ ## Props
41
+
42
+ | Prop | 类型 | 说明 |
43
+ |---|---|---|
44
+ | `value` | `MtDocument \| undefined` | 文档(真相源) |
45
+ | `options` | `MtAdapterOptions` | 其余内核选项 |
46
+ | `className` | `string` | 宿主容器的类名(会与 `mt-editor-host` 合并) |
47
+ | `style` | `CSSProperties` | 宿主容器的行内样式 |
48
+ | `onChange` / `onReady` / `onFocus` / `onBlur` / `onSelectionChange` / `onError` / `onDestroy` | 见下表 | 全部可选 |
49
+
50
+ `className` / `style` 需要显式声明,是因为 **React 不像 Vue 那样自动透传 attrs**
51
+ 到单根元素 —— Vue 侧这两个能力是白送的,React 侧必须写出来。
52
+
53
+ `MtAdapterOptions = Omit<MtEditorOptions, 'element' | 'onChange' | 'onReady' | 'onError'>`。
54
+ 被摘掉的四个由组件自己接管;**`content` 保留**,语义收窄为「初始内容,挂载时读取一次」,
55
+ `value` 一旦给出就优先于它。
56
+
57
+ **这里没有 `html` prop**(与 Vue 适配层的一处刻意不对称):`html` 是有损派生物,
58
+ `onChange` 的载荷里已经带着它,再开一个输入通道只会多一个「谁覆盖谁」的问题。
59
+ Vue 侧之所以有 `v-model:html`,是因为它的绑定语法需要一个 prop 名。
60
+
61
+ **运行时可变**的选项只有三个:`theme` / `locale` / `readonly`
62
+ (内核分别为它们提供了 `setTheme` / `setLocale` / `setReadonly`)。
63
+ 其余(`platform` / `ui` / `placeholder` / `plugins` / `historyDepth` /
64
+ `changeDebounce` / `selectionThrottle`)**挂载时读取一次**,要换档请用 `key` 重挂载。
65
+
66
+ ## 事件
67
+
68
+ 内核事件名**原样**沿用(§7.5),只按 React 惯例加 `on` 前缀。
69
+
70
+ | Prop | 载荷 |
71
+ |---|---|
72
+ | `onChange` | `MtChangePayload`(**150ms 防抖**) |
73
+ | `onReady` | `{ instance }` |
74
+ | `onFocus` / `onBlur` | `{ instance }` |
75
+ | `onSelectionChange` | `MtSelectionInfo`(100ms 节流,leading + trailing) |
76
+ | `onError` | `MtErrorPayload` |
77
+ | `onDestroy` | `{ instance }` |
78
+
79
+ `ready` 在内核的**构造函数里**就派发了,因此组件把它当**构造选项**(`onReady`)传入,
80
+ 而不是事后 `on('ready')` 订阅 —— 后者永远收不到。
81
+ 也正因如此,`onReady` 是**唯一**能直接拿到「刚建好的实例」的时机。
82
+
83
+ ## 暴露的句柄
84
+
85
+ ```tsx
86
+ const ref = useRef<MtEditorReactHandle>(null)
87
+ // 「卸载后」为 null
88
+ ref.current?.editor?.command('focus')
89
+ ```
90
+
91
+ 适配层**不转发内核方法**:取到 `editor` 后用法与原生完全一致。
92
+ 组件卸载时 React 会把 `ref.current` **整体置空**,因此宿主不可能拿到已销毁的实例。
93
+
94
+ ## 边界(AGENTS.md §8.1)
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 必测项)
118
+
119
+ 开发模式下 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)。
146
+
147
+ ## 交付要求
148
+
149
+ 每个适配包必须在 `examples/react/` 下提供**可跑通的最小示例** + 完整 README
150
+ (安装、引入、props 表、事件表、常见坑)。**没有示例视为未完成,不允许发版**(§8.3)。
151
+
152
+ ## 许可
153
+
154
+ 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":"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","file":"index.cjs","sourcesContent":["/**\n * React 适配组件(AGENTS.md §8.2)。\n *\n * 它是**薄壳**(§8.1):只做挂载 / 卸载、props → 选项映射、增量更新、事件透传与受控值回灌。\n * 任何编辑逻辑都写在 `@mteditor/core`,写在这里视为违规。\n *\n * ## 三条关键设计(ADR-0028 决策 4)\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 守(§8.2 必测项)。\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 // 内核的结构等价短路保证回灌同一个文档时不会重置光标(ADR-0028 决策 2)。\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"]}
@@ -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
+ * 否则「适配层的类型」与「内核的类型」会漂移成两份(ADR-0028 决策 3)。
12
+ */
13
+
14
+ /**
15
+ * `<MtEditor>` 的处理函数 props。
16
+ *
17
+ * 名字**原样**沿用内核的 §7.5 事件命名(`change` / `ready` / `focus` / `blur` /
18
+ * `selectionChange` / `error` / `destroy`),只按 React 惯例加上 `on` 前缀 ——
19
+ * 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法(ADR-0028 决策 4)。
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
+ * 再开一个输入通道只会多一个「谁覆盖谁」的问题(ADR-0028 决策 4)。
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 };
@@ -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
+ * 否则「适配层的类型」与「内核的类型」会漂移成两份(ADR-0028 决策 3)。
12
+ */
13
+
14
+ /**
15
+ * `<MtEditor>` 的处理函数 props。
16
+ *
17
+ * 名字**原样**沿用内核的 §7.5 事件命名(`change` / `ready` / `focus` / `blur` /
18
+ * `selectionChange` / `error` / `destroy`),只按 React 惯例加上 `on` 前缀 ——
19
+ * 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法(ADR-0028 决策 4)。
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
+ * 再开一个输入通道只会多一个「谁覆盖谁」的问题(ADR-0028 决策 4)。
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 适配组件(AGENTS.md §8.2)。\n *\n * 它是**薄壳**(§8.1):只做挂载 / 卸载、props → 选项映射、增量更新、事件透传与受控值回灌。\n * 任何编辑逻辑都写在 `@mteditor/core`,写在这里视为违规。\n *\n * ## 三条关键设计(ADR-0028 决策 4)\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 守(§8.2 必测项)。\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 // 内核的结构等价短路保证回灌同一个文档时不会重置光标(ADR-0028 决策 2)。\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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
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.0"
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
  }