@mteditor/vue2 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,184 @@
1
- # Temporary Holding Version
1
+ # @mteditor/vue2
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 的 Vue 2.7 适配层 —— **薄壳,不含任何编辑逻辑**。
4
+
5
+ ## 状态
6
+
7
+ **M3-d 已落地**。组件、类型、单测与可跑通示例均在本包内;
8
+ 可跑通的最小示例见 [`../../examples/vue2/`](../../examples/vue2/)。
9
+
10
+ ## 用法
11
+
12
+ ```vue
13
+ <template>
14
+ <mt-editor v-model="doc" :options="options" @change="onChange" @error="onError" />
15
+ </template>
16
+
17
+ <script>
18
+ import { MtEditor } from '@mteditor/vue2'
19
+ import '@mteditor/theme-default/index.css'
20
+
21
+ export default {
22
+ components: { MtEditor },
23
+ data() {
24
+ return { doc: null, options: { placeholder: '请输入…', theme: 'light' } }
25
+ },
26
+ methods: {
27
+ onChange(payload) {
28
+ /* v-model 已经写了 doc,这里只是额外关心一次事件 */
29
+ },
30
+ onError(payload) {
31
+ console.error(payload.code, payload.message)
32
+ },
33
+ },
34
+ }
35
+ </script>
36
+ ```
37
+
38
+ 按 **Options API** 书写,用 `Vue.extend`(不是 `defineComponent`,理由见下),
39
+ 只写 Options API 的老项目里接入方式与手写组件没有任何区别。
40
+
41
+ ## `v-model` 双兼容
42
+
43
+ Vue 2 的 `v-model` 编译产物是 `props.value` + `on.input`;Vue 2.7 起也支持
44
+ `modelValue` + `update:modelValue`。本组件**两个 prop 都收、两个事件都发**:
45
+
46
+ | 宿主写法 | 走哪条 |
47
+ |---|---|
48
+ | `<mt-editor v-model="doc" />` | `value` 进 / `input` 出 |
49
+ | `<mt-editor :modelValue="doc" @update:modelValue="doc = $event" />` | `modelValue` 进 / `update:modelValue` 出 |
50
+
51
+ 两者同时给出时 `modelValue` 优先。**`input` 与 `update:modelValue` 总是同时派发** ——
52
+ 换一种写法不会「少一半事件」,宿主也不必猜自己在走哪条协议。
53
+
54
+ `null` 与 `undefined` 都算「没给」:`data() { return { doc: null } }` 是 Vue 2 里最常见的
55
+ 「还没加载」写法,此时会回落到 `options.content`,且**不会**把 `null` 写进编辑器。
56
+
57
+ ## Props
58
+
59
+ | Prop | 类型 | 说明 |
60
+ |---|---|---|
61
+ | `value` | `MtDocument \| null \| undefined` | 文档(真相源)。Vue 2 的 `v-model` 默认走这里 |
62
+ | `modelValue` | `MtDocument \| null \| undefined` | 文档(真相源)。Vue 2.7 的 `v-model` 新写法走这里 |
63
+ | `options` | `MtAdapterOptions` | 其余内核选项 |
64
+
65
+ `MtAdapterOptions = Omit<MtEditorOptions, 'element' | 'onChange' | 'onReady' | 'onError'>`。
66
+ 被摘掉的四个由组件自己接管;**`content` 保留**,语义收窄为「初始内容,挂载时读取一次」,
67
+ 受控值一旦给出就优先于它。
68
+
69
+ **运行时可变**的选项只有三个:`theme` / `locale` / `readonly`
70
+ (内核分别为它们提供了 `setTheme` / `setLocale` / `setReadonly`)。
71
+ 其余(`platform` / `ui` / `placeholder` / `plugins` / `historyDepth` /
72
+ `changeDebounce` / `selectionThrottle`)**挂载时读取一次**,要换档请用 `:key` 重挂载。
73
+
74
+ ## 事件
75
+
76
+ 内核事件名**原样**沿用(§7.5),不改名也不合并。
77
+
78
+ | 事件 | 载荷 | 来源 |
79
+ |---|---|---|
80
+ | `input` | `MtDocument` | Vue 2 的 `v-model` 协议 |
81
+ | `update:modelValue` | `MtDocument` | Vue 2.7 的 `v-model` 协议 |
82
+ | `change` | `MtChangePayload`(150ms 防抖) | 内核 |
83
+ | `ready` | `{ instance }` | 内核 |
84
+ | `focus` / `blur` | `{ instance }` | 内核 |
85
+ | `selectionChange` | `MtSelectionInfo`(100ms 节流,leading + trailing) | 内核 |
86
+ | `error` | `MtErrorPayload` | 内核 |
87
+ | `destroy` | `{ instance }` | 内核 |
88
+
89
+ `ready` 在内核的**构造函数里**就派发了,因此组件把它当**构造选项**(`onReady`)传入,
90
+ 而不是事后 `on('ready')` 订阅 —— 后者永远收不到。
91
+ 也正因如此,`onReady` 是**唯一**能直接拿到「刚建好的实例」的时机。
92
+
93
+ ## 通过模板 ref 拿到内核实例
94
+
95
+ ```vue
96
+ <mt-editor ref="ed" v-model="doc" />
97
+ ```
98
+
99
+ ```ts
100
+ const handle = this.$refs.ed as unknown as MtEditorVue2Handle
101
+ handle.editor?.command('focus')
102
+ ```
103
+
104
+ 适配层**只暴露 `editor` 一个字段**,不把内核方法再转发一遍 ——
105
+ 否则内核每加一个方法就要改五个适配包,且「适配层的方法」会与内核文档分叉。
106
+ 卸载后该字段为 `null`。
107
+
108
+ ## 边界(AGENTS.md §8.1)
109
+
110
+ 允许做:
111
+
112
+ 1. 挂载 / 卸载底层实例;
113
+ 2. `props` → `MtEditorOptions` 映射;
114
+ 3. `options` 变更时**增量更新**(不重建实例);
115
+ 4. 内核事件 → 宿主事件透传;
116
+ 5. `v-model` 双向绑定(两条协议)。
117
+
118
+ **不允许做**:任何编辑逻辑、任何依赖 Vue 2 生态辅助包(`vuelidate` / `vuex` 等)。
119
+
120
+ 两条具体的禁止(ADR-0028 决策 4):
121
+
122
+ - **不在这里做回声抑制**。「灌回来的是同一份内容就不该碰视图」这条判据写在**内核**
123
+ (`MtEngine.setDocument()` 的结构等价短路)。适配层再判一次等于两处判据,
124
+ 且六个适配端会各判各的。
125
+ - **不为了「同步内容」而重建实例**。重建会丢选区、撤销栈与 IME composition 状态。
126
+
127
+ ## 两处 Vue 2 特有的实现要点
128
+
129
+ ### 用 `Vue.extend`,不用 `defineComponent`
130
+
131
+ Vue 2.7 也导出了 `defineComponent`,但它在 Vue 2 里**运行时只是原样返回 options 对象**,
132
+ 类型上却假装自己是构造器。用 `Vue.extend` 得到的是真正的构造器:既能被 `new`,
133
+ 也能直接交给 `h()` 或 `components:` —— 后者正是宿主要做的事。
134
+ 它还换来了 `ThisType`,让 `mounted` / `watch` / `render` 里的 `this` 全部有类型。
135
+
136
+ ### 内核实例必须 `markRaw`
137
+
138
+ Vue 2 的 `observe()` 用 `Object.prototype.toString` 判断「是不是普通对象」,
139
+ **类实例会被判成普通对象**而遭深度遍历 —— 那会把整个编辑器(含 ProseMirror 视图)
140
+ 塞满 getter/setter。因此写进 `data` 前必须 `markRaw(editor)`。
141
+
142
+ 事件退订函数则**不放进 `data`**(数组的 `push` 被 Vue 2 改写后会触发重渲染),
143
+ 改用一个模块级 `WeakMap` 挂在实例上,卸载时随实例一起回收。
144
+
145
+ ## 依赖
146
+
147
+ | 类型 | 内容 |
148
+ |---|---|
149
+ | `dependencies` | `@mteditor/core` |
150
+ | `peerDependencies` | `vue: >=2.7.0 <3.0.0`(Vue 2.7 是末版,且内置 Composition API) |
151
+ | devDependencies | 含 `vue-template-compiler@^2.7.16` —— `@vue/test-utils@1` 的必需 peer,见下 |
152
+
153
+ ### 为什么需要 `vue-template-compiler`
154
+
155
+ `@vue/test-utils@1`(Vue 2 专用,Vue 3 用的是 `@2`)在入口就
156
+ `import { compileToFunctions } from 'vue-template-compiler'`,
157
+ 而 Vue 2.7 已把该包废弃(改用 `vue/compiler-sfc`)。
158
+ 缺了它,单测会在**收集阶段**就报 `Failed to resolve import "vue-template-compiler"`。
159
+
160
+ 它是**测试期依赖**,不进任何产物。这一处就地声明记在 ADR-0028 决策 7。
161
+
162
+ ## 测试
163
+
164
+ ```bash
165
+ pnpm --filter @mteditor/vue2 test # 单测(happy-dom + @vue/test-utils@1)
166
+ pnpm --filter @mteditor/vue2 typecheck
167
+ ```
168
+
169
+ 12 条单测覆盖四组:挂载与卸载、`v-model` 双兼容(两条协议各一条用例 + 同时派发 +
170
+ `null` 的「没给」语义)、回灌短路、选项映射。
171
+
172
+ 端到端在 [`../../tests/e2e/adapters.spec.ts`](../../tests/e2e/adapters.spec.ts):
173
+ 真实浏览器里挂载 → 输入 → 读回**宿主侧的绑定值**。
174
+ 适配包不设覆盖率阈值(ADR-0019 只覆盖 `document` / `core` / `renderer-web`),
175
+ 但示例必须纳入 E2E(§8.3,ADR-0028 决策 6)。
176
+
177
+ ## 交付要求
178
+
179
+ 每个适配包必须在 `examples/vue2/` 下提供**可跑通的最小示例** + 完整 README
180
+ (安装、引入、props 表、事件表、常见坑)。**没有示例视为未完成,不允许发版**(§8.3)。
181
+
182
+ ## 许可
183
+
184
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var Vue = require('vue');
6
+ var core = require('@mteditor/core');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var Vue__default = /*#__PURE__*/_interopDefault(Vue);
11
+
12
+ // src/component.ts
13
+ var applyRuntimeOptions = (instance, options) => {
14
+ if (options.theme !== void 0) instance.setTheme(options.theme);
15
+ if (options.locale !== void 0) instance.setLocale(options.locale);
16
+ if (options.readonly !== void 0) instance.setReadonly(options.readonly);
17
+ };
18
+ var teardown = /* @__PURE__ */ new WeakMap();
19
+ var hostFrom = (refs) => {
20
+ const ref = refs["host"];
21
+ return ref instanceof HTMLElement ? ref : null;
22
+ };
23
+ var MtEditor = Vue__default.default.extend({
24
+ name: "MtEditor",
25
+ props: {
26
+ /**
27
+ * 文档(真相源)。Vue 2 的 `v-model` 默认走这里。
28
+ *
29
+ * 类型里带上 `null`:Vue 2 的宿主习惯写 `data() { return { doc: null } }`,
30
+ * 且在 `null` 上做 `=== null` 判空是必需的分支(`undefined` 覆盖不到它)。
31
+ */
32
+ value: { type: Object, default: void 0 },
33
+ /** 文档(真相源)。Vue 2.7 的 `v-model` 新写法走这里 */
34
+ modelValue: { type: Object, default: void 0 },
35
+ /** 其余内核选项 */
36
+ options: { type: Object, default: () => ({}) }
37
+ },
38
+ data() {
39
+ return {
40
+ /**
41
+ * 内核实例。
42
+ *
43
+ * **必须 `markRaw`**:Vue 2 的 `observe()` 用 `Object.prototype.toString` 判
44
+ * 「是不是普通对象」,类实例会被判成普通对象而遭深度遍历 ——
45
+ * 那会把整个编辑器(含 ProseMirror 视图)塞满 getter/setter。
46
+ */
47
+ editor: null
48
+ };
49
+ },
50
+ computed: {
51
+ /**
52
+ * 实际生效的受控值。
53
+ *
54
+ * 两条 `v-model` 协议共用一个入口:`modelValue` 优先于 `value`,
55
+ * 而 `null` / `undefined` 都算「没给」(Vue 2 的宿主习惯用 `null` 表示「还没加载」)。
56
+ * 有了它,下面只需要**一个** watcher,「两个 prop 谁说了算」也就只有一处答案。
57
+ */
58
+ boundValue() {
59
+ return this.modelValue !== void 0 && this.modelValue !== null ? this.modelValue : this.value;
60
+ }
61
+ },
62
+ watch: {
63
+ // 「编辑器 → 宿主」由 onChange 完成;这里只做「宿主 → 编辑器」。
64
+ // 内核的结构等价短路保证回灌同一个文档时不会重置光标(ADR-0028 决策 2)。
65
+ boundValue(next) {
66
+ if (next === null || next === void 0 || this.editor === null) return;
67
+ this.editor.setJSON(next);
68
+ },
69
+ // 选项变化:只同步能在运行时就地改的那几个(结构性选项挂载时读取一次)
70
+ options: {
71
+ deep: true,
72
+ handler(next) {
73
+ if (this.editor === null) return;
74
+ applyRuntimeOptions(this.editor, next);
75
+ }
76
+ }
77
+ },
78
+ mounted() {
79
+ const element = hostFrom(this.$refs);
80
+ if (element === null) return;
81
+ const { content: initialContent, ...restOptions } = this.options;
82
+ const editor = new core.MtEditor({
83
+ ...restOptions,
84
+ element,
85
+ content: this.boundValue ?? initialContent,
86
+ onChange: (payload) => {
87
+ this.$emit("input", payload.json);
88
+ this.$emit("update:modelValue", payload.json);
89
+ this.$emit("change", payload);
90
+ },
91
+ // `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅
92
+ onReady: (payload) => this.$emit("ready", payload),
93
+ onError: (payload) => this.$emit("error", payload)
94
+ });
95
+ teardown.set(this, [
96
+ editor.on("focus", (payload) => this.$emit("focus", payload)),
97
+ editor.on("blur", (payload) => this.$emit("blur", payload)),
98
+ editor.on("selectionChange", (payload) => this.$emit("selectionChange", payload)),
99
+ editor.on("destroy", (payload) => this.$emit("destroy", payload))
100
+ ]);
101
+ this.editor = Vue.markRaw(editor);
102
+ },
103
+ // Vue 2 的生命周期名(Vue 3 里叫 `beforeUnmount`)
104
+ beforeDestroy() {
105
+ const editor = this.editor;
106
+ this.editor = null;
107
+ if (editor === null) return;
108
+ editor.destroy();
109
+ for (const off of teardown.get(this) ?? []) off();
110
+ teardown.delete(this);
111
+ },
112
+ render(h) {
113
+ return h("div", {
114
+ ref: "host",
115
+ staticClass: "mt-editor-host",
116
+ attrs: { "data-mt-adapter": "vue2" }
117
+ });
118
+ }
119
+ });
120
+ var component_default = MtEditor;
121
+
122
+ exports.MtEditor = MtEditor;
123
+ exports.default = component_default;
124
+ //# sourceMappingURL=index.cjs.map
125
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/component.ts"],"names":["Vue","MtCoreEditor","markRaw"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Vue 2.7 适配组件(AGENTS.md §8.2)。\n *\n * 它是**薄壳**(§8.1):只做挂载 / 卸载、props → 选项映射、增量更新、事件透传与双向绑定。\n * 任何编辑逻辑都写在 `@mteditor/core`,写在这里视为违规。\n *\n * ## 为什么用 `Vue.extend` 而不是 `defineComponent`\n *\n * Vue 2.7 也导出了 `defineComponent`,但它在 Vue 2 里**运行时只是原样返回 options 对象**,\n * 类型上却假装自己是构造器。用 `Vue.extend` 得到的是真正的构造器:既能被 `new`,\n * 也能直接丢给 `h()` / `components:` —— 后者正是宿主要做的事。\n *\n * ## `v-model` 双兼容\n *\n * Vue 2 的 `v-model` 编译产物是 `props.value` + `on.input`;Vue 2.7 起也支持\n * `modelValue` + `update:modelValue`。本组件**两个 prop 都收、两个事件都发**:\n *\n * | 宿主写法 | 走哪条 |\n * |---|---|\n * | `<mt-editor v-model=\"doc\" />` | `value` 进 / `input` 出 |\n * | `<mt-editor :modelValue=\"doc\" @update:modelValue=\"doc = $event\" />` | `modelValue` 进 / `update:modelValue` 出 |\n *\n * 两者同时给出时 `modelValue` 优先(见 `boundValue` 计算属性)。\n *\n * ## 两条关键设计(ADR-0028 决策 4)\n *\n * 1. **这里不做回声抑制**。宿主把编辑器刚给出的文档灌回来是 `v-model` 的必然结果,\n * 而「灌回来的是同一份内容就不该碰视图」这条判据写在**内核**\n * (`MtEngine.setDocument()` 的结构等价短路)。适配层再判一次等于两处判据,\n * 且 6 个适配端会各判各的。\n * 2. **结构性选项挂载时读取一次**。内核没有「运行时换档」的能力\n * (`platform` / `ui` / `placeholder` / `historyDepth` 等都没有 setter),\n * 因此本组件不假装支持它。要换档请用 Vue 重挂载机制(`:key`)。\n */\n\nimport Vue, { markRaw } from 'vue'\n\nimport { MtEditor as MtCoreEditor } from '@mteditor/core'\nimport type { MtAdapterOptions, MtChangePayload, MtDocument, MtErrorPayload } from '@mteditor/core'\n\n/**\n * 能就地同步的选项子集。类型取自 `MtAdapterOptions`,不另立一份定义\n * (三者的 setter 分别是 `setTheme` / `setLocale` / `setReadonly`)。\n */\ntype MtRuntimeOptions = Pick<MtAdapterOptions, 'theme' | 'locale' | 'readonly'>\n\n/**\n * 把可在**运行时**就地同步的选项写进已存在的内核实例。\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 * 每个组件实例的事件退订函数。\n *\n * 不放进 `data`:Vue 2 会把 `data` **深度 observe**,数组的 `push` 会被改写成\n * 触发重渲染的版本;而这些函数与渲染无关。用 `WeakMap` 挂在实例上,\n * 卸载后随实例一起被回收。\n */\nconst teardown = new WeakMap<object, (() => void)[]>()\n\n/**\n * 从 `$refs` 里取挂载容器。\n *\n * `$refs` 的值可能是组件实例、DOM 元素或它们的数组,因此必须走类型守卫。\n *\n * @param refs 组件实例的 `$refs`\n * @returns 命中且确为 HTMLElement 时返回它,否则 `null`\n */\nconst hostFrom = (refs: unknown): HTMLElement | null => {\n const ref = (refs as Record<string, unknown>)['host']\n return ref instanceof HTMLElement ? ref : null\n}\n\n/** `<MtEditor>` —— Vue 2.7 的 MtEditor 组件 */\nexport const MtEditor = Vue.extend({\n name: 'MtEditor',\n\n props: {\n /**\n * 文档(真相源)。Vue 2 的 `v-model` 默认走这里。\n *\n * 类型里带上 `null`:Vue 2 的宿主习惯写 `data() { return { doc: null } }`,\n * 且在 `null` 上做 `=== null` 判空是必需的分支(`undefined` 覆盖不到它)。\n */\n value: { type: Object as () => MtDocument | null | undefined, default: undefined },\n /** 文档(真相源)。Vue 2.7 的 `v-model` 新写法走这里 */\n modelValue: { type: Object as () => MtDocument | null | undefined, default: undefined },\n /** 其余内核选项 */\n options: { type: Object as () => MtAdapterOptions, default: (): MtAdapterOptions => ({}) },\n },\n\n data() {\n return {\n /**\n * 内核实例。\n *\n * **必须 `markRaw`**:Vue 2 的 `observe()` 用 `Object.prototype.toString` 判\n * 「是不是普通对象」,类实例会被判成普通对象而遭深度遍历 ——\n * 那会把整个编辑器(含 ProseMirror 视图)塞满 getter/setter。\n */\n editor: null as MtCoreEditor | null,\n }\n },\n\n computed: {\n /**\n * 实际生效的受控值。\n *\n * 两条 `v-model` 协议共用一个入口:`modelValue` 优先于 `value`,\n * 而 `null` / `undefined` 都算「没给」(Vue 2 的宿主习惯用 `null` 表示「还没加载」)。\n * 有了它,下面只需要**一个** watcher,「两个 prop 谁说了算」也就只有一处答案。\n */\n boundValue(): MtDocument | null | undefined {\n return this.modelValue !== undefined && this.modelValue !== null\n ? this.modelValue\n : this.value\n },\n },\n\n watch: {\n // 「编辑器 → 宿主」由 onChange 完成;这里只做「宿主 → 编辑器」。\n // 内核的结构等价短路保证回灌同一个文档时不会重置光标(ADR-0028 决策 2)。\n boundValue(next: MtDocument | null | undefined) {\n if (next === null || next === undefined || this.editor === null) return\n this.editor.setJSON(next)\n },\n\n // 选项变化:只同步能在运行时就地改的那几个(结构性选项挂载时读取一次)\n options: {\n deep: true,\n handler(next: MtAdapterOptions) {\n if (this.editor === null) return\n applyRuntimeOptions(this.editor, next)\n },\n },\n },\n\n mounted() {\n const element = hostFrom(this.$refs)\n if (element === null) return\n\n // `options.content` 语义是「初始内容」;受控值一旦给出就优先于它。\n // 必须先摘出来再展开,否则 `...options` 会把 `content` 覆盖回旧值。\n const { content: initialContent, ...restOptions } = this.options\n\n const editor = new MtCoreEditor({\n ...restOptions,\n element,\n content: this.boundValue ?? initialContent,\n onChange: (payload: MtChangePayload) => {\n // 两条 v-model 协议同时派发,宿主用哪种写法都能收到\n this.$emit('input', payload.json)\n this.$emit('update:modelValue', payload.json)\n this.$emit('change', payload)\n },\n // `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅\n onReady: (payload) => this.$emit('ready', payload),\n onError: (payload: MtErrorPayload) => this.$emit('error', payload),\n })\n\n teardown.set(this, [\n editor.on('focus', (payload) => this.$emit('focus', payload)),\n editor.on('blur', (payload) => this.$emit('blur', payload)),\n editor.on('selectionChange', (payload) => this.$emit('selectionChange', payload)),\n editor.on('destroy', (payload) => this.$emit('destroy', payload)),\n ])\n\n this.editor = markRaw(editor)\n },\n\n // Vue 2 的生命周期名(Vue 3 里叫 `beforeUnmount`)\n beforeDestroy() {\n const editor = this.editor\n this.editor = null\n if (editor === null) return\n // 先销毁再退订:`destroy` 事件本身要透给宿主\n editor.destroy()\n for (const off of teardown.get(this) ?? []) off()\n teardown.delete(this)\n },\n\n render(h) {\n // 无子节点:内核自己往这个 div 里建 DOM,Vue 因此不会去碰它。\n // 用 `ref: 'host'` 字符串引用(Vue 2 的写法),由 `hostFrom()` 取回。\n return h('div', {\n ref: 'host',\n staticClass: 'mt-editor-host',\n attrs: { 'data-mt-adapter': 'vue2' },\n })\n },\n})\n\n/**\n * `<MtEditor>` 的 props,**由组件定义推断而来**。\n *\n * Vue 2 的 props 与 data 都直接挂在实例上,因此这里从实例类型上摘出 props 对应的三个字段。\n * 刻意不在 `types.ts` 里手写一份 interface:手写的那份会与运行时的 props 定义漂移。\n */\nexport type MtEditorVue2Props = Pick<\n InstanceType<typeof MtEditor>,\n 'value' | 'modelValue' | 'options'\n>\n\nexport default MtEditor\n"]}
@@ -0,0 +1,96 @@
1
+ import * as vue_types_vue from 'vue/types/vue';
2
+ import * as Vue from 'vue';
3
+ import Vue__default from 'vue';
4
+ import { MtEditor as MtEditor$1, MtDocument, MtAdapterOptions, MtChangePayload, MtSelectionInfo, MtErrorPayload } from '@mteditor/core';
5
+ export { MtAdapterOptions, MtChangePayload, MtDocument, MtEditor as MtEditorInstance, MtErrorPayload, MtSelectionInfo, MtThemeName } from '@mteditor/core';
6
+
7
+ /** `<MtEditor>` —— Vue 2.7 的 MtEditor 组件 */
8
+ declare const MtEditor: vue_types_vue.ExtendedVue<Vue__default<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue__default>, {
9
+ /**
10
+ * 内核实例。
11
+ *
12
+ * **必须 `markRaw`**:Vue 2 的 `observe()` 用 `Object.prototype.toString` 判
13
+ * 「是不是普通对象」,类实例会被判成普通对象而遭深度遍历 ——
14
+ * 那会把整个编辑器(含 ProseMirror 视图)塞满 getter/setter。
15
+ */
16
+ editor: MtEditor$1 | null;
17
+ }, unknown, {
18
+ boundValue: MtDocument | null | undefined;
19
+ }, {
20
+ value: MtDocument | null | undefined;
21
+ modelValue: MtDocument | null | undefined;
22
+ options: MtAdapterOptions;
23
+ }, {}, Vue.ComponentOptionsMixin, Vue.ComponentOptionsMixin>;
24
+ /**
25
+ * `<MtEditor>` 的 props,**由组件定义推断而来**。
26
+ *
27
+ * Vue 2 的 props 与 data 都直接挂在实例上,因此这里从实例类型上摘出 props 对应的三个字段。
28
+ * 刻意不在 `types.ts` 里手写一份 interface:手写的那份会与运行时的 props 定义漂移。
29
+ */
30
+ type MtEditorVue2Props = Pick<InstanceType<typeof MtEditor>, 'value' | 'modelValue' | 'options'>;
31
+
32
+ /**
33
+ * `@mteditor/vue2` 的公开类型面。
34
+ *
35
+ * 这些类型是**给宿主写模板与处理函数用的**,不是内部实现类型。
36
+ * 它们只是 `@mteditor/core` 的再导出与组合 —— 适配层不得自己另立一套文档模型类型,
37
+ * 否则「适配层的类型」与「内核的类型」会漂移成两份(ADR-0028 决策 3)。
38
+ */
39
+
40
+ /**
41
+ * `<MtEditor>` 派发的事件与载荷。
42
+ *
43
+ * 事件名**原样**沿用内核的 §7.5 命名(`change` / `ready` / `focus` / `blur` /
44
+ * `selectionChange` / `error` / `destroy`),不改名也不合并 ——
45
+ * 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法(ADR-0028 决策 4)。
46
+ *
47
+ * 另外两条是 Vue 2 的 `v-model` 协议,**不是**内核事件:
48
+ *
49
+ * - `input`:Vue 2 的默认 `v-model` 事件(`<MtEditor v-model="doc" />`);
50
+ * - `update:modelValue`:Vue 2.7 起可用的新写法(`<MtEditor :modelValue="doc"
51
+ * @update:modelValue="doc = $event" />`)。
52
+ *
53
+ * 两者**总是同时派发**,因此两种写法都能用,且不会「换一种写法就少一半事件」。
54
+ */
55
+ interface MtEditorVue2Events {
56
+ /** `v-model` 协议:Vue 2 默认写法 */
57
+ input: MtDocument;
58
+ /** `v-model` 协议:Vue 2.7 新写法 */
59
+ 'update:modelValue': MtDocument;
60
+ change: MtChangePayload;
61
+ ready: {
62
+ instance: MtEditor$1;
63
+ };
64
+ focus: {
65
+ instance: MtEditor$1;
66
+ };
67
+ blur: {
68
+ instance: MtEditor$1;
69
+ };
70
+ selectionChange: MtSelectionInfo;
71
+ error: MtErrorPayload;
72
+ destroy: {
73
+ instance: MtEditor$1;
74
+ };
75
+ }
76
+ /**
77
+ * 通过模板 `ref` 拿到的组件实例上,本适配层承诺可用的**唯一**字段。
78
+ *
79
+ * 只暴露 `editor`:适配层是薄壳,不该把内核的方法再转发一遍 ——
80
+ * 那样每加一个内核方法就要改 5 个适配包,且「适配层的方法」会与内核文档分叉。
81
+ *
82
+ * @example
83
+ * ```vue
84
+ * <mt-editor ref="ed" v-model="doc" />
85
+ * ```
86
+ * ```ts
87
+ * const handle = this.$refs.ed as unknown as MtEditorVue2Handle
88
+ * handle.editor?.command('focus')
89
+ * ```
90
+ */
91
+ interface MtEditorVue2Handle {
92
+ /** 底层内核实例;未挂载(或已销毁)时为 `null` */
93
+ readonly editor: MtEditor$1 | null;
94
+ }
95
+
96
+ export { MtEditor, type MtEditorVue2Events, type MtEditorVue2Handle, type MtEditorVue2Props, MtEditor as default };
@@ -0,0 +1,96 @@
1
+ import * as vue_types_vue from 'vue/types/vue';
2
+ import * as Vue from 'vue';
3
+ import Vue__default from 'vue';
4
+ import { MtEditor as MtEditor$1, MtDocument, MtAdapterOptions, MtChangePayload, MtSelectionInfo, MtErrorPayload } from '@mteditor/core';
5
+ export { MtAdapterOptions, MtChangePayload, MtDocument, MtEditor as MtEditorInstance, MtErrorPayload, MtSelectionInfo, MtThemeName } from '@mteditor/core';
6
+
7
+ /** `<MtEditor>` —— Vue 2.7 的 MtEditor 组件 */
8
+ declare const MtEditor: vue_types_vue.ExtendedVue<Vue__default<Record<string, any>, Record<string, any>, never, never, (event: string, ...args: any[]) => Vue__default>, {
9
+ /**
10
+ * 内核实例。
11
+ *
12
+ * **必须 `markRaw`**:Vue 2 的 `observe()` 用 `Object.prototype.toString` 判
13
+ * 「是不是普通对象」,类实例会被判成普通对象而遭深度遍历 ——
14
+ * 那会把整个编辑器(含 ProseMirror 视图)塞满 getter/setter。
15
+ */
16
+ editor: MtEditor$1 | null;
17
+ }, unknown, {
18
+ boundValue: MtDocument | null | undefined;
19
+ }, {
20
+ value: MtDocument | null | undefined;
21
+ modelValue: MtDocument | null | undefined;
22
+ options: MtAdapterOptions;
23
+ }, {}, Vue.ComponentOptionsMixin, Vue.ComponentOptionsMixin>;
24
+ /**
25
+ * `<MtEditor>` 的 props,**由组件定义推断而来**。
26
+ *
27
+ * Vue 2 的 props 与 data 都直接挂在实例上,因此这里从实例类型上摘出 props 对应的三个字段。
28
+ * 刻意不在 `types.ts` 里手写一份 interface:手写的那份会与运行时的 props 定义漂移。
29
+ */
30
+ type MtEditorVue2Props = Pick<InstanceType<typeof MtEditor>, 'value' | 'modelValue' | 'options'>;
31
+
32
+ /**
33
+ * `@mteditor/vue2` 的公开类型面。
34
+ *
35
+ * 这些类型是**给宿主写模板与处理函数用的**,不是内部实现类型。
36
+ * 它们只是 `@mteditor/core` 的再导出与组合 —— 适配层不得自己另立一套文档模型类型,
37
+ * 否则「适配层的类型」与「内核的类型」会漂移成两份(ADR-0028 决策 3)。
38
+ */
39
+
40
+ /**
41
+ * `<MtEditor>` 派发的事件与载荷。
42
+ *
43
+ * 事件名**原样**沿用内核的 §7.5 命名(`change` / `ready` / `focus` / `blur` /
44
+ * `selectionChange` / `error` / `destroy`),不改名也不合并 ——
45
+ * 改名会让「内核文档里的事件名」与「框架里的用法」变成两套说法(ADR-0028 决策 4)。
46
+ *
47
+ * 另外两条是 Vue 2 的 `v-model` 协议,**不是**内核事件:
48
+ *
49
+ * - `input`:Vue 2 的默认 `v-model` 事件(`<MtEditor v-model="doc" />`);
50
+ * - `update:modelValue`:Vue 2.7 起可用的新写法(`<MtEditor :modelValue="doc"
51
+ * @update:modelValue="doc = $event" />`)。
52
+ *
53
+ * 两者**总是同时派发**,因此两种写法都能用,且不会「换一种写法就少一半事件」。
54
+ */
55
+ interface MtEditorVue2Events {
56
+ /** `v-model` 协议:Vue 2 默认写法 */
57
+ input: MtDocument;
58
+ /** `v-model` 协议:Vue 2.7 新写法 */
59
+ 'update:modelValue': MtDocument;
60
+ change: MtChangePayload;
61
+ ready: {
62
+ instance: MtEditor$1;
63
+ };
64
+ focus: {
65
+ instance: MtEditor$1;
66
+ };
67
+ blur: {
68
+ instance: MtEditor$1;
69
+ };
70
+ selectionChange: MtSelectionInfo;
71
+ error: MtErrorPayload;
72
+ destroy: {
73
+ instance: MtEditor$1;
74
+ };
75
+ }
76
+ /**
77
+ * 通过模板 `ref` 拿到的组件实例上,本适配层承诺可用的**唯一**字段。
78
+ *
79
+ * 只暴露 `editor`:适配层是薄壳,不该把内核的方法再转发一遍 ——
80
+ * 那样每加一个内核方法就要改 5 个适配包,且「适配层的方法」会与内核文档分叉。
81
+ *
82
+ * @example
83
+ * ```vue
84
+ * <mt-editor ref="ed" v-model="doc" />
85
+ * ```
86
+ * ```ts
87
+ * const handle = this.$refs.ed as unknown as MtEditorVue2Handle
88
+ * handle.editor?.command('focus')
89
+ * ```
90
+ */
91
+ interface MtEditorVue2Handle {
92
+ /** 底层内核实例;未挂载(或已销毁)时为 `null` */
93
+ readonly editor: MtEditor$1 | null;
94
+ }
95
+
96
+ export { MtEditor, type MtEditorVue2Events, type MtEditorVue2Handle, type MtEditorVue2Props, MtEditor as default };
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ import Vue, { markRaw } from 'vue';
2
+ import { MtEditor as MtEditor$1 } from '@mteditor/core';
3
+
4
+ // src/component.ts
5
+ var applyRuntimeOptions = (instance, options) => {
6
+ if (options.theme !== void 0) instance.setTheme(options.theme);
7
+ if (options.locale !== void 0) instance.setLocale(options.locale);
8
+ if (options.readonly !== void 0) instance.setReadonly(options.readonly);
9
+ };
10
+ var teardown = /* @__PURE__ */ new WeakMap();
11
+ var hostFrom = (refs) => {
12
+ const ref = refs["host"];
13
+ return ref instanceof HTMLElement ? ref : null;
14
+ };
15
+ var MtEditor = Vue.extend({
16
+ name: "MtEditor",
17
+ props: {
18
+ /**
19
+ * 文档(真相源)。Vue 2 的 `v-model` 默认走这里。
20
+ *
21
+ * 类型里带上 `null`:Vue 2 的宿主习惯写 `data() { return { doc: null } }`,
22
+ * 且在 `null` 上做 `=== null` 判空是必需的分支(`undefined` 覆盖不到它)。
23
+ */
24
+ value: { type: Object, default: void 0 },
25
+ /** 文档(真相源)。Vue 2.7 的 `v-model` 新写法走这里 */
26
+ modelValue: { type: Object, default: void 0 },
27
+ /** 其余内核选项 */
28
+ options: { type: Object, default: () => ({}) }
29
+ },
30
+ data() {
31
+ return {
32
+ /**
33
+ * 内核实例。
34
+ *
35
+ * **必须 `markRaw`**:Vue 2 的 `observe()` 用 `Object.prototype.toString` 判
36
+ * 「是不是普通对象」,类实例会被判成普通对象而遭深度遍历 ——
37
+ * 那会把整个编辑器(含 ProseMirror 视图)塞满 getter/setter。
38
+ */
39
+ editor: null
40
+ };
41
+ },
42
+ computed: {
43
+ /**
44
+ * 实际生效的受控值。
45
+ *
46
+ * 两条 `v-model` 协议共用一个入口:`modelValue` 优先于 `value`,
47
+ * 而 `null` / `undefined` 都算「没给」(Vue 2 的宿主习惯用 `null` 表示「还没加载」)。
48
+ * 有了它,下面只需要**一个** watcher,「两个 prop 谁说了算」也就只有一处答案。
49
+ */
50
+ boundValue() {
51
+ return this.modelValue !== void 0 && this.modelValue !== null ? this.modelValue : this.value;
52
+ }
53
+ },
54
+ watch: {
55
+ // 「编辑器 → 宿主」由 onChange 完成;这里只做「宿主 → 编辑器」。
56
+ // 内核的结构等价短路保证回灌同一个文档时不会重置光标(ADR-0028 决策 2)。
57
+ boundValue(next) {
58
+ if (next === null || next === void 0 || this.editor === null) return;
59
+ this.editor.setJSON(next);
60
+ },
61
+ // 选项变化:只同步能在运行时就地改的那几个(结构性选项挂载时读取一次)
62
+ options: {
63
+ deep: true,
64
+ handler(next) {
65
+ if (this.editor === null) return;
66
+ applyRuntimeOptions(this.editor, next);
67
+ }
68
+ }
69
+ },
70
+ mounted() {
71
+ const element = hostFrom(this.$refs);
72
+ if (element === null) return;
73
+ const { content: initialContent, ...restOptions } = this.options;
74
+ const editor = new MtEditor$1({
75
+ ...restOptions,
76
+ element,
77
+ content: this.boundValue ?? initialContent,
78
+ onChange: (payload) => {
79
+ this.$emit("input", payload.json);
80
+ this.$emit("update:modelValue", payload.json);
81
+ this.$emit("change", payload);
82
+ },
83
+ // `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅
84
+ onReady: (payload) => this.$emit("ready", payload),
85
+ onError: (payload) => this.$emit("error", payload)
86
+ });
87
+ teardown.set(this, [
88
+ editor.on("focus", (payload) => this.$emit("focus", payload)),
89
+ editor.on("blur", (payload) => this.$emit("blur", payload)),
90
+ editor.on("selectionChange", (payload) => this.$emit("selectionChange", payload)),
91
+ editor.on("destroy", (payload) => this.$emit("destroy", payload))
92
+ ]);
93
+ this.editor = markRaw(editor);
94
+ },
95
+ // Vue 2 的生命周期名(Vue 3 里叫 `beforeUnmount`)
96
+ beforeDestroy() {
97
+ const editor = this.editor;
98
+ this.editor = null;
99
+ if (editor === null) return;
100
+ editor.destroy();
101
+ for (const off of teardown.get(this) ?? []) off();
102
+ teardown.delete(this);
103
+ },
104
+ render(h) {
105
+ return h("div", {
106
+ ref: "host",
107
+ staticClass: "mt-editor-host",
108
+ attrs: { "data-mt-adapter": "vue2" }
109
+ });
110
+ }
111
+ });
112
+ var component_default = MtEditor;
113
+
114
+ export { MtEditor, component_default as default };
115
+ //# sourceMappingURL=index.js.map
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/component.ts"],"names":["MtCoreEditor"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Vue 2.7 适配组件(AGENTS.md §8.2)。\n *\n * 它是**薄壳**(§8.1):只做挂载 / 卸载、props → 选项映射、增量更新、事件透传与双向绑定。\n * 任何编辑逻辑都写在 `@mteditor/core`,写在这里视为违规。\n *\n * ## 为什么用 `Vue.extend` 而不是 `defineComponent`\n *\n * Vue 2.7 也导出了 `defineComponent`,但它在 Vue 2 里**运行时只是原样返回 options 对象**,\n * 类型上却假装自己是构造器。用 `Vue.extend` 得到的是真正的构造器:既能被 `new`,\n * 也能直接丢给 `h()` / `components:` —— 后者正是宿主要做的事。\n *\n * ## `v-model` 双兼容\n *\n * Vue 2 的 `v-model` 编译产物是 `props.value` + `on.input`;Vue 2.7 起也支持\n * `modelValue` + `update:modelValue`。本组件**两个 prop 都收、两个事件都发**:\n *\n * | 宿主写法 | 走哪条 |\n * |---|---|\n * | `<mt-editor v-model=\"doc\" />` | `value` 进 / `input` 出 |\n * | `<mt-editor :modelValue=\"doc\" @update:modelValue=\"doc = $event\" />` | `modelValue` 进 / `update:modelValue` 出 |\n *\n * 两者同时给出时 `modelValue` 优先(见 `boundValue` 计算属性)。\n *\n * ## 两条关键设计(ADR-0028 决策 4)\n *\n * 1. **这里不做回声抑制**。宿主把编辑器刚给出的文档灌回来是 `v-model` 的必然结果,\n * 而「灌回来的是同一份内容就不该碰视图」这条判据写在**内核**\n * (`MtEngine.setDocument()` 的结构等价短路)。适配层再判一次等于两处判据,\n * 且 6 个适配端会各判各的。\n * 2. **结构性选项挂载时读取一次**。内核没有「运行时换档」的能力\n * (`platform` / `ui` / `placeholder` / `historyDepth` 等都没有 setter),\n * 因此本组件不假装支持它。要换档请用 Vue 重挂载机制(`:key`)。\n */\n\nimport Vue, { markRaw } from 'vue'\n\nimport { MtEditor as MtCoreEditor } from '@mteditor/core'\nimport type { MtAdapterOptions, MtChangePayload, MtDocument, MtErrorPayload } from '@mteditor/core'\n\n/**\n * 能就地同步的选项子集。类型取自 `MtAdapterOptions`,不另立一份定义\n * (三者的 setter 分别是 `setTheme` / `setLocale` / `setReadonly`)。\n */\ntype MtRuntimeOptions = Pick<MtAdapterOptions, 'theme' | 'locale' | 'readonly'>\n\n/**\n * 把可在**运行时**就地同步的选项写进已存在的内核实例。\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 * 每个组件实例的事件退订函数。\n *\n * 不放进 `data`:Vue 2 会把 `data` **深度 observe**,数组的 `push` 会被改写成\n * 触发重渲染的版本;而这些函数与渲染无关。用 `WeakMap` 挂在实例上,\n * 卸载后随实例一起被回收。\n */\nconst teardown = new WeakMap<object, (() => void)[]>()\n\n/**\n * 从 `$refs` 里取挂载容器。\n *\n * `$refs` 的值可能是组件实例、DOM 元素或它们的数组,因此必须走类型守卫。\n *\n * @param refs 组件实例的 `$refs`\n * @returns 命中且确为 HTMLElement 时返回它,否则 `null`\n */\nconst hostFrom = (refs: unknown): HTMLElement | null => {\n const ref = (refs as Record<string, unknown>)['host']\n return ref instanceof HTMLElement ? ref : null\n}\n\n/** `<MtEditor>` —— Vue 2.7 的 MtEditor 组件 */\nexport const MtEditor = Vue.extend({\n name: 'MtEditor',\n\n props: {\n /**\n * 文档(真相源)。Vue 2 的 `v-model` 默认走这里。\n *\n * 类型里带上 `null`:Vue 2 的宿主习惯写 `data() { return { doc: null } }`,\n * 且在 `null` 上做 `=== null` 判空是必需的分支(`undefined` 覆盖不到它)。\n */\n value: { type: Object as () => MtDocument | null | undefined, default: undefined },\n /** 文档(真相源)。Vue 2.7 的 `v-model` 新写法走这里 */\n modelValue: { type: Object as () => MtDocument | null | undefined, default: undefined },\n /** 其余内核选项 */\n options: { type: Object as () => MtAdapterOptions, default: (): MtAdapterOptions => ({}) },\n },\n\n data() {\n return {\n /**\n * 内核实例。\n *\n * **必须 `markRaw`**:Vue 2 的 `observe()` 用 `Object.prototype.toString` 判\n * 「是不是普通对象」,类实例会被判成普通对象而遭深度遍历 ——\n * 那会把整个编辑器(含 ProseMirror 视图)塞满 getter/setter。\n */\n editor: null as MtCoreEditor | null,\n }\n },\n\n computed: {\n /**\n * 实际生效的受控值。\n *\n * 两条 `v-model` 协议共用一个入口:`modelValue` 优先于 `value`,\n * 而 `null` / `undefined` 都算「没给」(Vue 2 的宿主习惯用 `null` 表示「还没加载」)。\n * 有了它,下面只需要**一个** watcher,「两个 prop 谁说了算」也就只有一处答案。\n */\n boundValue(): MtDocument | null | undefined {\n return this.modelValue !== undefined && this.modelValue !== null\n ? this.modelValue\n : this.value\n },\n },\n\n watch: {\n // 「编辑器 → 宿主」由 onChange 完成;这里只做「宿主 → 编辑器」。\n // 内核的结构等价短路保证回灌同一个文档时不会重置光标(ADR-0028 决策 2)。\n boundValue(next: MtDocument | null | undefined) {\n if (next === null || next === undefined || this.editor === null) return\n this.editor.setJSON(next)\n },\n\n // 选项变化:只同步能在运行时就地改的那几个(结构性选项挂载时读取一次)\n options: {\n deep: true,\n handler(next: MtAdapterOptions) {\n if (this.editor === null) return\n applyRuntimeOptions(this.editor, next)\n },\n },\n },\n\n mounted() {\n const element = hostFrom(this.$refs)\n if (element === null) return\n\n // `options.content` 语义是「初始内容」;受控值一旦给出就优先于它。\n // 必须先摘出来再展开,否则 `...options` 会把 `content` 覆盖回旧值。\n const { content: initialContent, ...restOptions } = this.options\n\n const editor = new MtCoreEditor({\n ...restOptions,\n element,\n content: this.boundValue ?? initialContent,\n onChange: (payload: MtChangePayload) => {\n // 两条 v-model 协议同时派发,宿主用哪种写法都能收到\n this.$emit('input', payload.json)\n this.$emit('update:modelValue', payload.json)\n this.$emit('change', payload)\n },\n // `ready` 在构造期就已派发,因此它走构造选项;其余事件走总线订阅\n onReady: (payload) => this.$emit('ready', payload),\n onError: (payload: MtErrorPayload) => this.$emit('error', payload),\n })\n\n teardown.set(this, [\n editor.on('focus', (payload) => this.$emit('focus', payload)),\n editor.on('blur', (payload) => this.$emit('blur', payload)),\n editor.on('selectionChange', (payload) => this.$emit('selectionChange', payload)),\n editor.on('destroy', (payload) => this.$emit('destroy', payload)),\n ])\n\n this.editor = markRaw(editor)\n },\n\n // Vue 2 的生命周期名(Vue 3 里叫 `beforeUnmount`)\n beforeDestroy() {\n const editor = this.editor\n this.editor = null\n if (editor === null) return\n // 先销毁再退订:`destroy` 事件本身要透给宿主\n editor.destroy()\n for (const off of teardown.get(this) ?? []) off()\n teardown.delete(this)\n },\n\n render(h) {\n // 无子节点:内核自己往这个 div 里建 DOM,Vue 因此不会去碰它。\n // 用 `ref: 'host'` 字符串引用(Vue 2 的写法),由 `hostFrom()` 取回。\n return h('div', {\n ref: 'host',\n staticClass: 'mt-editor-host',\n attrs: { 'data-mt-adapter': 'vue2' },\n })\n },\n})\n\n/**\n * `<MtEditor>` 的 props,**由组件定义推断而来**。\n *\n * Vue 2 的 props 与 data 都直接挂在实例上,因此这里从实例类型上摘出 props 对应的三个字段。\n * 刻意不在 `types.ts` 里手写一份 interface:手写的那份会与运行时的 props 定义漂移。\n */\nexport type MtEditorVue2Props = Pick<\n InstanceType<typeof MtEditor>,\n 'value' | 'modelValue' | 'options'\n>\n\nexport default MtEditor\n"]}
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@mteditor/vue2",
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 的 Vue 2.7 适配层:Options API 薄壳,兼容 value/input 形式的 v-model",
5
+ "keywords": [
6
+ "mteditor",
7
+ "vue2",
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
+ "vue": ">=2.7.0 <3.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@vue/test-utils": "^1.3.6",
57
+ "happy-dom": "^15.11.7",
58
+ "rimraf": "^6.0.1",
59
+ "tsup": "^8.3.5",
60
+ "typescript": "^5.7.2",
61
+ "vitest": "^2.1.8",
62
+ "vue": "^2.7.16",
63
+ "vue-template-compiler": "^2.7.16"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup",
67
+ "typecheck": "tsc --noEmit",
68
+ "test": "vitest run --passWithNoTests",
69
+ "test:watch": "vitest",
70
+ "clean": "rimraf dist .turbo"
71
+ }
6
72
  }