remons-markdown-editor 1.0.0-beta.4 → 1.0.0-beta.6

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.
Files changed (32) hide show
  1. package/dist/App.d.ts +2 -0
  2. package/dist/MarkdownEditor.d.ts +23 -0
  3. package/dist/components/Dialog/EditDialog.d.ts +21 -0
  4. package/dist/components/Editor/codemirrorExtensions.d.ts +12 -0
  5. package/dist/components/Editor/containerWidget.d.ts +20 -0
  6. package/dist/components/Editor/index.d.ts +16 -0
  7. package/dist/components/Panel/SplitPanels.d.ts +7 -0
  8. package/dist/components/Preview/index.d.ts +10 -0
  9. package/dist/components/Toolbar/buttons.d.ts +34 -0
  10. package/dist/components/Toolbar/config.d.ts +6 -0
  11. package/dist/components/Toolbar/hooks/useToolbar.d.ts +14 -0
  12. package/dist/components/Toolbar/index.d.ts +13 -0
  13. package/dist/components/Toolbar/renderers/TablePicker/TableDropdown.d.ts +5 -0
  14. package/dist/components/Toolbar/renderers/TablePicker/index.d.ts +9 -0
  15. package/dist/components/Toolbar/renderers/ToolbarItemRenderer.d.ts +7 -0
  16. package/dist/hooks/useFullscreen.d.ts +4 -0
  17. package/dist/hooks/useRegisterToolbar.d.ts +51 -0
  18. package/dist/hooks/useScrollSync.d.ts +5 -0
  19. package/dist/index.cjs +1314 -1191
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.css +248 -8
  22. package/dist/index.d.ts +5 -157
  23. package/dist/index.js +1289 -1169
  24. package/dist/index.js.map +1 -1
  25. package/dist/main.d.ts +0 -0
  26. package/dist/registry/componentRegistry.d.ts +34 -0
  27. package/dist/types/index.d.ts +76 -0
  28. package/dist/utils/parseContainer.d.ts +18 -0
  29. package/dist/utils/writeContainer.d.ts +16 -0
  30. package/package.json +7 -7
  31. package/dist/index.css.map +0 -1
  32. package/dist/index.d.cts +0 -157
package/dist/index.js CHANGED
@@ -1,1240 +1,1360 @@
1
- // src/MarkdownEditor.tsx
2
- import { useState as useState4, useRef as useRef5, useCallback as useCallback6, useEffect as useEffect6, forwardRef as forwardRef2, useImperativeHandle } from "react";
3
- import { message } from "antd";
4
-
5
- // src/components/Editor/index.tsx
6
- import { useEffect, useRef } from "react";
7
- import { EditorView as EditorView2, basicSetup } from "codemirror";
8
- import { EditorState as EditorState2 } from "@codemirror/state";
9
-
10
- // src/components/Editor/codemirrorExtensions.ts
11
- import { EditorView, keymap } from "@codemirror/view";
1
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
+ import { Button, Dropdown, Modal, Splitter, Tooltip, message } from "antd";
3
+ import { EditorView, basicSetup } from "codemirror";
4
+ import { EditorState, Facet } from "@codemirror/state";
5
+ import { Decoration, EditorView as EditorView$1, ViewPlugin, WidgetType, keymap } from "@codemirror/view";
12
6
  import { defaultKeymap, indentWithTab } from "@codemirror/commands";
13
7
  import { markdown } from "@codemirror/lang-markdown";
14
8
  import { oneDark } from "@codemirror/theme-one-dark";
15
- import { Facet as Facet2 } from "@codemirror/state";
16
-
17
- // src/components/Editor/containerWidget.ts
18
- import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
19
- import { Facet } from "@codemirror/state";
20
-
21
- // src/utils/parseContainer.ts
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ import RenderMarkdown, { initHighlighter, languagesCommon } from "remons-render-markdown";
11
+ import "remons-render-markdown/dist/index.css";
12
+ import { BlockOutlined, BoldOutlined, CodeOutlined, DownOutlined, FieldStringOutlined, FontSizeOutlined, FullscreenExitOutlined, FullscreenOutlined, ItalicOutlined, LinkOutlined, MinusOutlined, OrderedListOutlined, PictureOutlined, ReadOutlined, StrikethroughOutlined, TableOutlined, UnderlineOutlined, UnorderedListOutlined } from "@ant-design/icons";
13
+ //#region src/utils/parseContainer.ts
14
+ /**
15
+ * 解析容器属性 JSON 字符串。
16
+ * 支持标准 JSON 和宽松格式: { key: 'value', key2: 'value2' }
17
+ */
22
18
  function parseContainerProps(jsonStr) {
23
- var _a, _b, _c;
24
- try {
25
- const props = {};
26
- const trimmed = jsonStr.trim();
27
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
28
- const inner = trimmed.slice(1, -1).trim();
29
- if (!inner) return props;
30
- const matches = inner.matchAll(/(?:["']?)(\w+)(?:["']?)\s*:\s*(?:"([^"]*)"|'([^']*)'|(\d+))/g);
31
- for (const m of matches) {
32
- props[m[1]] = (_c = (_b = (_a = m[2]) != null ? _a : m[3]) != null ? _b : m[4]) != null ? _c : "";
33
- }
34
- }
35
- return props;
36
- } catch (e) {
37
- return {};
38
- }
19
+ try {
20
+ const props = {};
21
+ const trimmed = jsonStr.trim();
22
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
23
+ const inner = trimmed.slice(1, -1).trim();
24
+ if (!inner) return props;
25
+ const matches = inner.matchAll(/(?:["']?)(\w+)(?:["']?)\s*:\s*(?:"([^"]*)"|'([^']*)'|(\d+))/g);
26
+ for (const m of matches) props[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
27
+ }
28
+ return props;
29
+ } catch {
30
+ return {};
31
+ }
39
32
  }
40
33
  function findContainers(docText) {
41
- const containers = [];
42
- const lines = docText.split("\n");
43
- let i = 0;
44
- while (i < lines.length) {
45
- const line = lines[i];
46
- const match = line.match(/^:::(\w+)$/);
47
- if (match) {
48
- const typeName = match[1];
49
- let charPos = 0;
50
- for (let l = 0; l < i; l++) {
51
- charPos += lines[l].length + 1;
52
- }
53
- let props = {};
54
- let propsLineFrom = 0;
55
- let propsLineTo = 0;
56
- let propsLineIndex = i + 1;
57
- if (propsLineIndex < lines.length) {
58
- const propsLine = lines[propsLineIndex].trim();
59
- const propsMatch = propsLine.match(/^\{[\s\S]*\}$/);
60
- if (propsMatch) {
61
- props = parseContainerProps(propsLine);
62
- propsLineFrom = charPos + line.length + 1;
63
- propsLineTo = propsLineFrom + lines[propsLineIndex].length;
64
- }
65
- }
66
- let endPos = 0;
67
- for (let j = i + 1; j < lines.length; j++) {
68
- if (lines[j].trim() === ":::") {
69
- let endCharPos = 0;
70
- for (let l = 0; l < j; l++) {
71
- endCharPos += lines[l].length + 1;
72
- }
73
- endPos = endCharPos;
74
- i = j + 1;
75
- break;
76
- }
77
- }
78
- containers.push({
79
- startPos: charPos,
80
- typeName,
81
- propsLineFrom,
82
- propsLineTo,
83
- props,
84
- endPos
85
- });
86
- }
87
- i++;
88
- }
89
- return containers;
34
+ const containers = [];
35
+ const lines = docText.split("\n");
36
+ let i = 0;
37
+ while (i < lines.length) {
38
+ const line = lines[i];
39
+ const match = line.match(/^:::(\w+)$/);
40
+ if (match) {
41
+ const typeName = match[1];
42
+ let charPos = 0;
43
+ for (let l = 0; l < i; l++) charPos += lines[l].length + 1;
44
+ let props = {};
45
+ let propsLineFrom = 0;
46
+ let propsLineTo = 0;
47
+ let propsLineIndex = i + 1;
48
+ if (propsLineIndex < lines.length) {
49
+ const propsLine = lines[propsLineIndex].trim();
50
+ if (propsLine.match(/^\{[\s\S]*\}$/)) {
51
+ props = parseContainerProps(propsLine);
52
+ propsLineFrom = charPos + line.length + 1;
53
+ propsLineTo = propsLineFrom + lines[propsLineIndex].length;
54
+ }
55
+ }
56
+ let endPos = 0;
57
+ for (let j = i + 1; j < lines.length; j++) if (lines[j].trim() === ":::") {
58
+ let endCharPos = 0;
59
+ for (let l = 0; l < j; l++) endCharPos += lines[l].length + 1;
60
+ endPos = endCharPos;
61
+ i = j + 1;
62
+ break;
63
+ }
64
+ containers.push({
65
+ startPos: charPos,
66
+ typeName,
67
+ propsLineFrom,
68
+ propsLineTo,
69
+ props,
70
+ endPos
71
+ });
72
+ }
73
+ i++;
74
+ }
75
+ return containers;
90
76
  }
91
-
92
- // src/registry/componentRegistry.tsx
77
+ //#endregion
78
+ //#region src/registry/componentRegistry.tsx
79
+ /**
80
+ * 组件注册中心,用于管理自定义组件的 schema。
81
+ * 每个容器组件(如 :::linkCard)在此注册其可编辑字段。
82
+ */
93
83
  var ComponentRegistry = class {
94
- constructor() {
95
- this.schemas = /* @__PURE__ */ new Map();
96
- }
97
- /**
98
- * 注册组件 schema
99
- */
100
- register(name, schema) {
101
- this.schemas.set(name, schema);
102
- }
103
- /**
104
- * 根据组件名获取已注册的 schema
105
- */
106
- get(name) {
107
- return this.schemas.get(name);
108
- }
109
- /**
110
- * 检查组件类型是否已注册
111
- */
112
- has(name) {
113
- return this.schemas.has(name);
114
- }
115
- /**
116
- * 获取所有已注册的组件名
117
- */
118
- getAllNames() {
119
- return Array.from(this.schemas.keys());
120
- }
121
- /**
122
- * 获取所有已注册的 schema
123
- */
124
- getAll() {
125
- return Array.from(this.schemas.values());
126
- }
127
- /**
128
- * 移除组件注册
129
- */
130
- unregister(name) {
131
- this.schemas.delete(name);
132
- }
84
+ schemas = /* @__PURE__ */ new Map();
85
+ /**
86
+ * 注册组件 schema
87
+ */
88
+ register(name, schema) {
89
+ this.schemas.set(name, schema);
90
+ }
91
+ /**
92
+ * 根据组件名获取已注册的 schema
93
+ */
94
+ get(name) {
95
+ return this.schemas.get(name);
96
+ }
97
+ /**
98
+ * 检查组件类型是否已注册
99
+ */
100
+ has(name) {
101
+ return this.schemas.has(name);
102
+ }
103
+ /**
104
+ * 获取所有已注册的组件名
105
+ */
106
+ getAllNames() {
107
+ return Array.from(this.schemas.keys());
108
+ }
109
+ /**
110
+ * 获取所有已注册的 schema
111
+ */
112
+ getAll() {
113
+ return Array.from(this.schemas.values());
114
+ }
115
+ /**
116
+ * 移除组件注册
117
+ */
118
+ unregister(name) {
119
+ this.schemas.delete(name);
120
+ }
133
121
  };
134
122
  var componentRegistry = new ComponentRegistry();
135
- var componentRegistry_default = componentRegistry;
136
-
137
- // src/components/Editor/containerWidget.ts
138
- var containerEditCallback = Facet.define({
139
- combine: (values) => values[0]
140
- });
123
+ //#endregion
124
+ //#region src/components/Editor/containerWidget.ts
125
+ /**
126
+ * 自定义 facet 用于传递编辑回调
127
+ */
128
+ var containerEditCallback = Facet.define({ combine: (values) => values[0] });
129
+ /**
130
+ * 只读的容器头行 Widget(显示 :::typeName + 编辑按钮)
131
+ * 使用 Decoration.replace 替换原始文本,使其不可直接编辑
132
+ */
141
133
  var ContainerHeaderWidget = class extends WidgetType {
142
- constructor(typeName) {
143
- super();
144
- this.typeName = typeName;
145
- }
146
- toDOM() {
147
- var _a, _b;
148
- const container = document.createElement("span");
149
- container.className = "container-widget-header";
150
- const text = document.createElement("span");
151
- text.className = "container-widget-text";
152
- text.textContent = `:::${this.typeName}`;
153
- const button = document.createElement("button");
154
- button.className = "container-edit-btn";
155
- button.title = `\u7F16\u8F91 ${(_b = (_a = componentRegistry_default.get(this.typeName)) == null ? void 0 : _a.label) != null ? _b : this.typeName}`;
156
- button.textContent = "\u270E";
157
- button.__typeName = this.typeName;
158
- container.appendChild(text);
159
- container.appendChild(button);
160
- return container;
161
- }
162
- eq(other) {
163
- return this.typeName === other.typeName;
164
- }
165
- ignoreEvent() {
166
- return false;
167
- }
134
+ typeName;
135
+ constructor(typeName) {
136
+ super();
137
+ this.typeName = typeName;
138
+ }
139
+ toDOM() {
140
+ const container = document.createElement("span");
141
+ container.className = "container-widget-header";
142
+ const text = document.createElement("span");
143
+ text.className = "container-widget-text";
144
+ text.textContent = `:::${this.typeName}`;
145
+ const button = document.createElement("button");
146
+ button.className = "container-edit-btn";
147
+ button.title = `编辑 ${componentRegistry.get(this.typeName)?.label ?? this.typeName}`;
148
+ button.textContent = "✎";
149
+ button.__typeName = this.typeName;
150
+ container.appendChild(text);
151
+ container.appendChild(button);
152
+ return container;
153
+ }
154
+ eq(other) {
155
+ return this.typeName === other.typeName;
156
+ }
157
+ ignoreEvent() {
158
+ return false;
159
+ }
168
160
  };
161
+ /**
162
+ * 只读的容器属性行 Widget(显示 { key: value, ... })
163
+ * 使用 Decoration.replace 替换原始文本,使其不可直接编辑
164
+ */
169
165
  var ContainerPropsWidget = class extends WidgetType {
170
- constructor(propsText) {
171
- super();
172
- this.propsText = propsText;
173
- }
174
- toDOM() {
175
- const span = document.createElement("span");
176
- span.className = "container-widget-props";
177
- span.textContent = this.propsText;
178
- return span;
179
- }
180
- eq(other) {
181
- return this.propsText === other.propsText;
182
- }
183
- ignoreEvent() {
184
- return false;
185
- }
166
+ propsText;
167
+ constructor(propsText) {
168
+ super();
169
+ this.propsText = propsText;
170
+ }
171
+ toDOM() {
172
+ const span = document.createElement("span");
173
+ span.className = "container-widget-props";
174
+ span.textContent = this.propsText;
175
+ return span;
176
+ }
177
+ eq(other) {
178
+ return this.propsText === other.propsText;
179
+ }
180
+ ignoreEvent() {
181
+ return false;
182
+ }
186
183
  };
184
+ /**
185
+ * 只读的容器结束行 Widget(显示 :::)
186
+ * 使用 Decoration.replace 替换原始文本,使其不可直接编辑
187
+ */
187
188
  var ContainerCloseWidget = class extends WidgetType {
188
- toDOM() {
189
- const span = document.createElement("span");
190
- span.className = "container-widget-text";
191
- span.textContent = ":::";
192
- return span;
193
- }
194
- eq() {
195
- return true;
196
- }
197
- ignoreEvent() {
198
- return false;
199
- }
189
+ toDOM() {
190
+ const span = document.createElement("span");
191
+ span.className = "container-widget-text";
192
+ span.textContent = ":::";
193
+ return span;
194
+ }
195
+ eq() {
196
+ return true;
197
+ }
198
+ ignoreEvent() {
199
+ return false;
200
+ }
200
201
  };
202
+ /**
203
+ * 查找文档中的容器,创建 Decoration 装饰物
204
+ * 将 ::: 头行、{} 属性行和 ::: 结束行替换为只读 widget,使其无法在编辑器中直接编辑
205
+ */
201
206
  function findContainerDecorations(state) {
202
- const docText = state.doc.toString();
203
- const containers = findContainers(docText);
204
- const decorations = [];
205
- for (const container of containers) {
206
- const headerLine = state.doc.lineAt(container.startPos);
207
- decorations.push({
208
- from: headerLine.from,
209
- to: headerLine.to,
210
- value: Decoration.replace({
211
- widget: new ContainerHeaderWidget(container.typeName)
212
- })
213
- });
214
- if (container.propsLineFrom > 0 && container.propsLineTo > 0) {
215
- const propsLine = state.doc.lineAt(container.propsLineFrom);
216
- const propsText = propsLine.text;
217
- decorations.push({
218
- from: propsLine.from,
219
- to: propsLine.to,
220
- value: Decoration.replace({
221
- widget: new ContainerPropsWidget(propsText)
222
- })
223
- });
224
- }
225
- if (container.endPos > 0) {
226
- const endLine = state.doc.lineAt(container.endPos);
227
- decorations.push({
228
- from: endLine.from,
229
- to: endLine.to,
230
- value: Decoration.replace({
231
- widget: new ContainerCloseWidget()
232
- })
233
- });
234
- }
235
- }
236
- return decorations;
207
+ const containers = findContainers(state.doc.toString());
208
+ const decorations = [];
209
+ for (const container of containers) {
210
+ const headerLine = state.doc.lineAt(container.startPos);
211
+ decorations.push({
212
+ from: headerLine.from,
213
+ to: headerLine.to,
214
+ value: Decoration.replace({ widget: new ContainerHeaderWidget(container.typeName) })
215
+ });
216
+ if (container.propsLineFrom > 0 && container.propsLineTo > 0) {
217
+ const propsLine = state.doc.lineAt(container.propsLineFrom);
218
+ const propsText = propsLine.text;
219
+ decorations.push({
220
+ from: propsLine.from,
221
+ to: propsLine.to,
222
+ value: Decoration.replace({ widget: new ContainerPropsWidget(propsText) })
223
+ });
224
+ }
225
+ if (container.endPos > 0) {
226
+ const endLine = state.doc.lineAt(container.endPos);
227
+ decorations.push({
228
+ from: endLine.from,
229
+ to: endLine.to,
230
+ value: Decoration.replace({ widget: new ContainerCloseWidget() })
231
+ });
232
+ }
233
+ }
234
+ return decorations;
237
235
  }
238
- var containerWidgetPlugin = ViewPlugin.fromClass(
239
- class {
240
- constructor(view) {
241
- this.decorations = Decoration.set(findContainerDecorations(view.state), true);
242
- }
243
- update(update) {
244
- if (update.docChanged || update.selectionSet) {
245
- this.decorations = Decoration.set(findContainerDecorations(update.state), true);
246
- }
247
- }
248
- },
249
- {
250
- decorations: (value) => value.decorations,
251
- eventHandlers: {
252
- // 点击编辑按钮时触发回调
253
- click: (event, view) => {
254
- const target = event.target;
255
- if (target.classList.contains("container-edit-btn")) {
256
- const typeName = target.__typeName;
257
- if (typeName) {
258
- const docText = view.state.doc.toString();
259
- const containers = findContainers(docText);
260
- const buttonPos = view.posAtDOM(target);
261
- for (const container of containers) {
262
- const headerLine = view.state.doc.lineAt(container.startPos);
263
- if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
264
- const callback = view.state.facet(containerEditCallback);
265
- if (callback) {
266
- callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
267
- }
268
- return true;
269
- }
270
- }
271
- }
272
- return false;
273
- }
274
- return false;
275
- }
276
- }
277
- }
278
- );
279
-
280
- // src/components/Toolbar/buttons.tsx
236
+ /**
237
+ * 容器编辑 ViewPlugin
238
+ * 将容器的 ::: 头行和 {} 属性行替换为只读 widget,使其无法在编辑器中直接编辑
239
+ */
240
+ var containerWidgetPlugin = ViewPlugin.fromClass(class {
241
+ decorations;
242
+ constructor(view) {
243
+ this.decorations = Decoration.set(findContainerDecorations(view.state), true);
244
+ }
245
+ update(update) {
246
+ if (update.docChanged || update.selectionSet) this.decorations = Decoration.set(findContainerDecorations(update.state), true);
247
+ }
248
+ }, {
249
+ decorations: (value) => value.decorations,
250
+ eventHandlers: { click: (event, view) => {
251
+ const target = event.target;
252
+ if (target.classList.contains("container-edit-btn")) {
253
+ if (target.__typeName) {
254
+ const containers = findContainers(view.state.doc.toString());
255
+ const buttonPos = view.posAtDOM(target);
256
+ for (const container of containers) {
257
+ const headerLine = view.state.doc.lineAt(container.startPos);
258
+ if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
259
+ const callback = view.state.facet(containerEditCallback);
260
+ if (callback) callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
261
+ return true;
262
+ }
263
+ }
264
+ }
265
+ return false;
266
+ }
267
+ return false;
268
+ } }
269
+ });
270
+ //#endregion
271
+ //#region src/components/Toolbar/buttons.tsx
272
+ /**
273
+ * 在光标位置插入文本
274
+ */
281
275
  function insertAtCursor(view, before, after = "", placeholder = "") {
282
- const { from, to } = view.state.selection.main;
283
- const selectedText = view.state.doc.sliceString(from, to);
284
- const insert = selectedText ? `${before}${selectedText}${after}` : `${before}${placeholder}${after}`;
285
- view.dispatch({ changes: { from, to, insert } });
286
- if (!selectedText && placeholder) {
287
- view.dispatch({ selection: { anchor: from + before.length } });
288
- }
276
+ const { from, to } = view.state.selection.main;
277
+ const selectedText = view.state.doc.sliceString(from, to);
278
+ const insert = selectedText ? `${before}${selectedText}${after}` : `${before}${placeholder}${after}`;
279
+ view.dispatch({ changes: {
280
+ from,
281
+ to,
282
+ insert
283
+ } });
284
+ if (!selectedText && placeholder) view.dispatch({ selection: { anchor: from + before.length } });
289
285
  }
286
+ /**
287
+ * 在当前行行首插入文本
288
+ */
290
289
  function insertAtLineStart(view, text) {
291
- const { from } = view.state.selection.main;
292
- const line = view.state.doc.lineAt(from);
293
- view.dispatch({ changes: { from: line.from, to: line.from, insert: text } });
294
- view.dispatch({ selection: { anchor: line.from + text.length } });
290
+ const { from } = view.state.selection.main;
291
+ const line = view.state.doc.lineAt(from);
292
+ view.dispatch({ changes: {
293
+ from: line.from,
294
+ to: line.from,
295
+ insert: text
296
+ } });
297
+ view.dispatch({ selection: { anchor: line.from + text.length } });
295
298
  }
299
+ /**
300
+ * 上传图片
301
+ * @param file - 要上传的文件
302
+ * @param uploadConfig - 上传配置
303
+ */
296
304
  async function uploadFileToServer(file, uploadConfig) {
297
- var _a;
298
- if (uploadConfig.onUploadImage) {
299
- return uploadConfig.onUploadImage(file);
300
- }
301
- if (uploadConfig.uploadUrl) {
302
- const formData = new FormData();
303
- formData.append("file", file);
304
- const response = await fetch(uploadConfig.uploadUrl, {
305
- method: "POST",
306
- body: formData
307
- });
308
- if (!response.ok) return null;
309
- const result = await response.json();
310
- if (uploadConfig.onGetUploadUrl) {
311
- return uploadConfig.onGetUploadUrl(result);
312
- }
313
- return ((_a = result.data) == null ? void 0 : _a.path) || null;
314
- }
315
- return null;
305
+ if (uploadConfig.onUploadImage) return uploadConfig.onUploadImage(file);
306
+ if (uploadConfig.uploadUrl) {
307
+ const formData = new FormData();
308
+ formData.append("file", file);
309
+ const response = await fetch(uploadConfig.uploadUrl, {
310
+ method: "POST",
311
+ body: formData
312
+ });
313
+ if (!response.ok) return null;
314
+ const result = await response.json();
315
+ if (uploadConfig.onGetUploadUrl) return uploadConfig.onGetUploadUrl(result);
316
+ return result.data?.path || null;
317
+ }
318
+ return null;
316
319
  }
320
+ /**
321
+ * 判断是否配置了图片上传
322
+ */
317
323
  function isUploadConfigured(uploadConfig) {
318
- return !!((uploadConfig == null ? void 0 : uploadConfig.uploadUrl) || (uploadConfig == null ? void 0 : uploadConfig.onUploadImage));
324
+ return !!(uploadConfig?.uploadUrl || uploadConfig?.onUploadImage);
319
325
  }
326
+ /**
327
+ * 图片上传:创建隐藏的 file input 触发选择,上传后插入 markdown 图片语法
328
+ * @param view - CodeMirror 编辑器实例
329
+ * @param uploadConfig - 上传配置(包含 uploadUrl 或 onUploadImage)
330
+ */
320
331
  function uploadImage(view, uploadConfig) {
321
- const input = document.createElement("input");
322
- input.type = "file";
323
- input.accept = "image/*";
324
- input.style.display = "none";
325
- input.addEventListener("change", async () => {
326
- if (!input.files || input.files.length === 0) return;
327
- const file = input.files[0];
328
- const { from, to } = view.state.selection.main;
329
- const selectedText = view.state.doc.sliceString(from, to) || "\u63CF\u8FF0";
330
- const url = await uploadFileToServer(file, uploadConfig);
331
- if (url) {
332
- const insert = `![${selectedText}](${url})`;
333
- view.dispatch({ changes: { from, to, insert } });
334
- }
335
- input.remove();
336
- });
337
- document.body.appendChild(input);
338
- input.click();
332
+ const input = document.createElement("input");
333
+ input.type = "file";
334
+ input.accept = "image/*";
335
+ input.style.display = "none";
336
+ input.addEventListener("change", async () => {
337
+ if (!input.files || input.files.length === 0) return;
338
+ const file = input.files[0];
339
+ const { from, to } = view.state.selection.main;
340
+ const selectedText = view.state.doc.sliceString(from, to) || "描述";
341
+ const url = await uploadFileToServer(file, uploadConfig);
342
+ if (url) {
343
+ const insert = `![${selectedText}](${url})`;
344
+ view.dispatch({ changes: {
345
+ from,
346
+ to,
347
+ insert
348
+ } });
349
+ }
350
+ input.remove();
351
+ });
352
+ document.body.appendChild(input);
353
+ input.click();
339
354
  }
355
+ /**
356
+ * 用前缀和后缀包裹选中文本
357
+ */
340
358
  function wrapSelection(view, prefix, suffix) {
341
- const { from, to, empty } = view.state.selection.main;
342
- if (empty) return;
343
- const selected = view.state.doc.sliceString(from, to);
344
- view.dispatch({ changes: { from, to, insert: `${prefix}${selected}${suffix}` } });
359
+ const { from, to, empty } = view.state.selection.main;
360
+ if (empty) return;
361
+ const selected = view.state.doc.sliceString(from, to);
362
+ view.dispatch({ changes: {
363
+ from,
364
+ to,
365
+ insert: `${prefix}${selected}${suffix}`
366
+ } });
345
367
  }
368
+ /**
369
+ * 切换包裹:选中的文本已包含标签则移除,否则添加
370
+ */
346
371
  function toggleWrap(view, openTag, closeTag) {
347
- const { from, to, empty } = view.state.selection.main;
348
- const doc = view.state.doc;
349
- if (!empty) {
350
- const selected = doc.sliceString(from, to);
351
- if (selected.startsWith(openTag) && selected.endsWith(closeTag)) {
352
- const inner = selected.slice(openTag.length, -closeTag.length);
353
- view.dispatch({ changes: { from, to, insert: inner } });
354
- return;
355
- }
356
- }
357
- {
358
- const cursorPos = empty ? from : to;
359
- const line = doc.lineAt(cursorPos);
360
- const lineText = doc.sliceString(line.from, line.to);
361
- const openIdx = lineText.indexOf(openTag);
362
- const closeIdx = lineText.lastIndexOf(closeTag);
363
- if (openIdx >= 0 && closeIdx > openIdx) {
364
- const absOpen = line.from + openIdx;
365
- const absClose = line.from + closeIdx;
366
- if (cursorPos >= absOpen + openTag.length && cursorPos <= absClose) {
367
- const inner = doc.sliceString(absOpen + openTag.length, absClose);
368
- view.dispatch({
369
- changes: {
370
- from: absOpen,
371
- to: absClose + closeTag.length,
372
- insert: inner
373
- }
374
- });
375
- return;
376
- }
377
- }
378
- }
379
- wrapSelection(view, openTag, closeTag);
372
+ const { from, to, empty } = view.state.selection.main;
373
+ const doc = view.state.doc;
374
+ if (!empty) {
375
+ const selected = doc.sliceString(from, to);
376
+ if (selected.startsWith(openTag) && selected.endsWith(closeTag)) {
377
+ const inner = selected.slice(openTag.length, -closeTag.length);
378
+ view.dispatch({ changes: {
379
+ from,
380
+ to,
381
+ insert: inner
382
+ } });
383
+ return;
384
+ }
385
+ }
386
+ {
387
+ const cursorPos = empty ? from : to;
388
+ const line = doc.lineAt(cursorPos);
389
+ const lineText = doc.sliceString(line.from, line.to);
390
+ const openIdx = lineText.indexOf(openTag);
391
+ const closeIdx = lineText.lastIndexOf(closeTag);
392
+ if (openIdx >= 0 && closeIdx > openIdx) {
393
+ const absOpen = line.from + openIdx;
394
+ const absClose = line.from + closeIdx;
395
+ if (cursorPos >= absOpen + openTag.length && cursorPos <= absClose) {
396
+ const inner = doc.sliceString(absOpen + openTag.length, absClose);
397
+ view.dispatch({ changes: {
398
+ from: absOpen,
399
+ to: absClose + closeTag.length,
400
+ insert: inner
401
+ } });
402
+ return;
403
+ }
404
+ }
405
+ }
406
+ wrapSelection(view, openTag, closeTag);
380
407
  }
381
-
382
- // src/components/Editor/codemirrorExtensions.ts
383
- var uploadConfigFacet = Facet2.define({
384
- combine: (values) => values[values.length - 1]
385
- });
386
- var messageFacet = Facet2.define({
387
- combine: (values) => values[values.length - 1]
388
- });
389
- var pasteHandler = EditorView.domEventHandlers({
390
- paste: (event, view) => {
391
- var _a;
392
- const files = (_a = event.clipboardData) == null ? void 0 : _a.files;
393
- if (!files) return false;
394
- const imageFiles = Array.from(files).filter((f) => f.type.startsWith("image/"));
395
- if (imageFiles.length === 0) return false;
396
- event.preventDefault();
397
- const uploadConfig = view.state.facet(uploadConfigFacet);
398
- const showMessage = view.state.facet(messageFacet);
399
- if (!isUploadConfigured(uploadConfig)) {
400
- showMessage == null ? void 0 : showMessage();
401
- return true;
402
- }
403
- const { from, to } = view.state.selection.main;
404
- let offset = 0;
405
- for (const file of imageFiles) {
406
- uploadFileToServer(file, uploadConfig).then((url) => {
407
- if (url) {
408
- const name = file.name.replace(/\.[^.]+$/, "");
409
- const insert = `![${name}](${url})`;
410
- view.dispatch({ changes: { from: from + offset, to: to + offset, insert } });
411
- offset += insert.length;
412
- }
413
- });
414
- }
415
- return true;
416
- }
417
- });
408
+ //#endregion
409
+ //#region src/components/Editor/codemirrorExtensions.ts
410
+ /** 图片上传配置 facet */
411
+ var uploadConfigFacet = Facet.define({ combine: (values) => values[values.length - 1] });
412
+ /** 未配置上传时的提示回调 facet */
413
+ var messageFacet = Facet.define({ combine: (values) => values[values.length - 1] });
414
+ /** 粘贴事件处理:拦截图片并上传 */
415
+ var pasteHandler = EditorView$1.domEventHandlers({ paste: (event, view) => {
416
+ const files = event.clipboardData?.files;
417
+ if (!files) return false;
418
+ const imageFiles = Array.from(files).filter((f) => f.type.startsWith("image/"));
419
+ if (imageFiles.length === 0) return false;
420
+ event.preventDefault();
421
+ const uploadConfig = view.state.facet(uploadConfigFacet);
422
+ const showMessage = view.state.facet(messageFacet);
423
+ if (!isUploadConfigured(uploadConfig)) {
424
+ showMessage?.();
425
+ return true;
426
+ }
427
+ const { from, to } = view.state.selection.main;
428
+ let offset = 0;
429
+ for (const file of imageFiles) uploadFileToServer(file, uploadConfig).then((url) => {
430
+ if (url) {
431
+ const insert = `![${file.name.replace(/\.[^.]+$/, "")}](${url})`;
432
+ view.dispatch({ changes: {
433
+ from: from + offset,
434
+ to: to + offset,
435
+ insert
436
+ } });
437
+ offset += insert.length;
438
+ }
439
+ });
440
+ return true;
441
+ } });
418
442
  var getCodeMirrorExtensions = (opts) => {
419
- const extensions = [
420
- markdown(),
421
- oneDark,
422
- keymap.of([...defaultKeymap, indentWithTab]),
423
- EditorView.lineWrapping,
424
- pasteHandler,
425
- EditorView.baseTheme({
426
- ".cm-scroller": {
427
- height: "100%",
428
- overflow: "auto"
429
- },
430
- // 容器头行 Widget 样式
431
- ".container-widget-header": {
432
- display: "inline",
433
- fontSize: "inherit",
434
- fontFamily: "inherit"
435
- },
436
- ".container-widget-text": {
437
- fontWeight: "bold",
438
- color: "#79c0ff"
439
- },
440
- // 容器属性行 Widget 样式
441
- ".container-widget-props": {
442
- display: "inline",
443
- fontSize: "inherit",
444
- fontFamily: "inherit",
445
- color: "#a5d6ff"
446
- },
447
- // 容器编辑按钮样式
448
- ".container-edit-btn": {
449
- display: "inline-block",
450
- padding: "0 4px",
451
- fontSize: "12px",
452
- color: "#58a6ff",
453
- background: "transparent",
454
- border: "none",
455
- borderRadius: "4px",
456
- cursor: "pointer",
457
- marginRight: "4px",
458
- verticalAlign: "middle"
459
- },
460
- ".container-edit-btn:hover": {
461
- background: "rgba(88, 166, 255, 0.16)"
462
- }
463
- })
464
- ];
465
- extensions.push(containerWidgetPlugin);
466
- if (opts == null ? void 0 : opts.onEditContainer) {
467
- extensions.push(containerEditCallback.of(opts.onEditContainer));
468
- }
469
- if (opts == null ? void 0 : opts.uploadConfig) {
470
- extensions.push(uploadConfigFacet.of(opts.uploadConfig));
471
- }
472
- if (opts == null ? void 0 : opts.onMessageNoUploadConfig) {
473
- extensions.push(messageFacet.of(opts.onMessageNoUploadConfig));
474
- }
475
- return extensions;
443
+ const extensions = [
444
+ markdown(),
445
+ oneDark,
446
+ keymap.of([...defaultKeymap, indentWithTab]),
447
+ EditorView$1.lineWrapping,
448
+ pasteHandler,
449
+ EditorView$1.baseTheme({
450
+ ".cm-scroller": {
451
+ height: "100%",
452
+ overflow: "auto"
453
+ },
454
+ ".container-widget-header": {
455
+ display: "inline",
456
+ fontSize: "inherit",
457
+ fontFamily: "inherit"
458
+ },
459
+ ".container-widget-text": {
460
+ fontWeight: "bold",
461
+ color: "#79c0ff"
462
+ },
463
+ ".container-widget-props": {
464
+ display: "inline",
465
+ fontSize: "inherit",
466
+ fontFamily: "inherit",
467
+ color: "#a5d6ff"
468
+ },
469
+ ".container-edit-btn": {
470
+ display: "inline-block",
471
+ padding: "0 4px",
472
+ fontSize: "12px",
473
+ color: "#58a6ff",
474
+ background: "transparent",
475
+ border: "none",
476
+ borderRadius: "4px",
477
+ cursor: "pointer",
478
+ marginRight: "4px",
479
+ verticalAlign: "middle"
480
+ },
481
+ ".container-edit-btn:hover": { background: "rgba(88, 166, 255, 0.16)" }
482
+ })
483
+ ];
484
+ extensions.push(containerWidgetPlugin);
485
+ if (opts?.onEditContainer) extensions.push(containerEditCallback.of(opts.onEditContainer));
486
+ if (opts?.uploadConfig) extensions.push(uploadConfigFacet.of(opts.uploadConfig));
487
+ if (opts?.onMessageNoUploadConfig) extensions.push(messageFacet.of(opts.onMessageNoUploadConfig));
488
+ return extensions;
476
489
  };
477
-
478
- // src/components/Editor/index.tsx
479
- import { jsx } from "react/jsx-runtime";
490
+ //#endregion
491
+ //#region src/components/Editor/index.tsx
480
492
  function Editor({ value, onChange, onScrollReady, onViewReady, onEditContainer, uploadConfig, onMessageNoUploadConfig }) {
481
- const editorRef = useRef(null);
482
- const viewRef = useRef(null);
483
- useEffect(() => {
484
- if (!editorRef.current) return;
485
- const extensions = [
486
- basicSetup,
487
- ...getCodeMirrorExtensions({ onEditContainer, uploadConfig, onMessageNoUploadConfig }),
488
- EditorView2.updateListener.of((update) => {
489
- if (update.docChanged) {
490
- onChange(update.state.doc.toString());
491
- }
492
- })
493
- ];
494
- const state = EditorState2.create({
495
- doc: value,
496
- extensions
497
- });
498
- viewRef.current = new EditorView2({
499
- state,
500
- parent: editorRef.current
501
- });
502
- if (onScrollReady && viewRef.current) {
503
- onScrollReady(viewRef.current.scrollDOM);
504
- }
505
- if (onViewReady) {
506
- onViewReady(viewRef.current);
507
- }
508
- return () => {
509
- var _a;
510
- (_a = viewRef.current) == null ? void 0 : _a.destroy();
511
- };
512
- }, []);
513
- useEffect(() => {
514
- if (viewRef.current && viewRef.current.state.doc.toString() !== value) {
515
- viewRef.current.dispatch({
516
- changes: { from: 0, to: viewRef.current.state.doc.length, insert: value }
517
- });
518
- }
519
- }, [value]);
520
- return /* @__PURE__ */ jsx("div", { ref: editorRef, className: "editor" });
493
+ const editorRef = useRef(null);
494
+ const viewRef = useRef(null);
495
+ useEffect(() => {
496
+ if (!editorRef.current) return;
497
+ const extensions = [
498
+ basicSetup,
499
+ ...getCodeMirrorExtensions({
500
+ onEditContainer,
501
+ uploadConfig,
502
+ onMessageNoUploadConfig
503
+ }),
504
+ EditorView.updateListener.of((update) => {
505
+ if (update.docChanged) onChange(update.state.doc.toString());
506
+ })
507
+ ];
508
+ const state = EditorState.create({
509
+ doc: value,
510
+ extensions
511
+ });
512
+ viewRef.current = new EditorView({
513
+ state,
514
+ parent: editorRef.current
515
+ });
516
+ if (onScrollReady && viewRef.current) onScrollReady(viewRef.current.scrollDOM);
517
+ if (onViewReady) onViewReady(viewRef.current);
518
+ return () => {
519
+ viewRef.current?.destroy();
520
+ };
521
+ }, []);
522
+ useEffect(() => {
523
+ if (viewRef.current && viewRef.current.state.doc.toString() !== value) viewRef.current.dispatch({ changes: {
524
+ from: 0,
525
+ to: viewRef.current.state.doc.length,
526
+ insert: value
527
+ } });
528
+ }, [value]);
529
+ return /* @__PURE__ */ jsx("div", {
530
+ ref: editorRef,
531
+ className: "editor"
532
+ });
521
533
  }
522
-
523
- // src/components/Preview/index.tsx
524
- import { forwardRef, useEffect as useEffect2 } from "react";
525
- import RenderMarkdown, { languagesCommon, initHighlighter } from "remons-render-markdown";
526
- import "remons-render-markdown/dist/index.css";
527
- import { jsx as jsx2 } from "react/jsx-runtime";
534
+ //#endregion
535
+ //#region src/components/Preview/index.tsx
528
536
  var Preview = forwardRef(({ content, previewOptions }, ref) => {
529
- useEffect2(() => {
530
- initHighlighter({
531
- ...languagesCommon,
532
- ...(previewOptions == null ? void 0 : previewOptions.languages) ? previewOptions == null ? void 0 : previewOptions.languages : {}
533
- });
534
- }, [previewOptions == null ? void 0 : previewOptions.languages]);
535
- return /* @__PURE__ */ jsx2("div", { ref, className: "preview", children: /* @__PURE__ */ jsx2(RenderMarkdown, { content, ...previewOptions }) });
537
+ useEffect(() => {
538
+ initHighlighter({
539
+ ...languagesCommon,
540
+ ...previewOptions?.languages ? previewOptions?.languages : {}
541
+ });
542
+ }, [previewOptions?.languages]);
543
+ return /* @__PURE__ */ jsx("div", {
544
+ ref,
545
+ className: "preview",
546
+ children: /* @__PURE__ */ jsx(RenderMarkdown, {
547
+ content,
548
+ ...previewOptions
549
+ })
550
+ });
536
551
  });
537
552
  Preview.displayName = "Preview";
538
- var Preview_default = Preview;
539
-
540
- // src/components/Panel/SplitPanels.tsx
541
- import { Splitter } from "antd";
542
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
553
+ //#endregion
554
+ //#region src/components/Panel/SplitPanels.tsx
543
555
  function SplitPanels({ leftPanel, rightPanel }) {
544
- return /* @__PURE__ */ jsxs(Splitter, { className: "split-panels", children: [
545
- /* @__PURE__ */ jsx3(Splitter.Panel, { defaultSize: "50%", min: "20%", max: "80%", children: leftPanel }),
546
- /* @__PURE__ */ jsx3(Splitter.Panel, { children: rightPanel })
547
- ] });
556
+ return /* @__PURE__ */ jsxs(Splitter, {
557
+ className: "split-panels",
558
+ children: [/* @__PURE__ */ jsx(Splitter.Panel, {
559
+ defaultSize: "50%",
560
+ min: "20%",
561
+ max: "80%",
562
+ children: leftPanel
563
+ }), /* @__PURE__ */ jsx(Splitter.Panel, { children: rightPanel })]
564
+ });
548
565
  }
549
-
550
- // src/components/Toolbar/index.tsx
551
- import { useCallback as useCallback4 } from "react";
552
- import { Button as Button3 } from "antd";
553
- import { FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons";
554
-
555
- // src/components/Toolbar/hooks/useToolbar.ts
556
- import { useCallback, useMemo } from "react";
557
-
558
- // src/components/Toolbar/config.ts
559
- import {
560
- BoldOutlined,
561
- ItalicOutlined,
562
- StrikethroughOutlined,
563
- UnderlineOutlined,
564
- UnorderedListOutlined,
565
- OrderedListOutlined,
566
- LinkOutlined,
567
- PictureOutlined,
568
- CodeOutlined,
569
- BlockOutlined,
570
- MinusOutlined,
571
- FieldStringOutlined,
572
- FontSizeOutlined
573
- } from "@ant-design/icons";
566
+ //#endregion
567
+ //#region src/components/Toolbar/config.ts
568
+ /**
569
+ * 创建工具栏配置
570
+ * @param options - 配置选项
571
+ */
574
572
  function createToolbarConfig(options) {
575
- const hasUploadConfig = isUploadConfigured(options);
576
- return [
577
- {
578
- id: "heading",
579
- icon: FontSizeOutlined,
580
- label: "\u6807\u9898",
581
- children: [
582
- { id: "heading1", label: "\u6807\u9898 1", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "# ") },
583
- { id: "heading2", label: "\u6807\u9898 2", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "## ") },
584
- { id: "heading3", label: "\u6807\u9898 3", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "### ") },
585
- { id: "heading4", label: "\u6807\u9898 4", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "#### ") },
586
- { id: "heading5", label: "\u6807\u9898 5", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "##### ") },
587
- { id: "heading6", label: "\u6807\u9898 6", icon: FontSizeOutlined, handler: (v) => insertAtLineStart(v, "###### ") }
588
- ]
589
- },
590
- { separatorBefore: true },
591
- { id: "bold", label: "\u52A0\u7C97", icon: BoldOutlined, handler: (v) => wrapSelection(v, "**", "**") },
592
- { id: "italic", label: "\u659C\u4F53", icon: ItalicOutlined, handler: (v) => wrapSelection(v, "*", "*") },
593
- { id: "strikethrough", label: "\u5220\u9664\u7EBF", icon: StrikethroughOutlined, handler: (v) => wrapSelection(v, "~~", "~~") },
594
- { id: "underline", label: "\u4E0B\u5212\u7EBF", icon: UnderlineOutlined, handler: (v) => wrapSelection(v, "<u>", "</u>") },
595
- { separatorBefore: true },
596
- { id: "ul", label: "\u65E0\u5E8F\u5217\u8868", icon: UnorderedListOutlined, handler: (v) => insertAtLineStart(v, "- ") },
597
- { id: "ol", label: "\u6709\u5E8F\u5217\u8868", icon: OrderedListOutlined, handler: (v) => insertAtLineStart(v, "1. ") },
598
- { separatorBefore: true },
599
- { id: "link", label: "\u94FE\u63A5", icon: LinkOutlined, handler: (v) => insertAtCursor(v, "[", "](url)", "\u6587\u672C") },
600
- // 图片按钮:未配置上传时禁用并提示
601
- {
602
- id: "image",
603
- label: hasUploadConfig ? "\u56FE\u7247" : "\u8BF7\u914D\u7F6E\u56FE\u7247\u4E0A\u4F20\u5730\u5740",
604
- icon: PictureOutlined,
605
- disabled: !hasUploadConfig,
606
- handler: hasUploadConfig ? (v) => uploadImage(v, { uploadUrl: options == null ? void 0 : options.uploadUrl, onUploadImage: options == null ? void 0 : options.onUploadImage }) : void 0
607
- },
608
- { separatorBefore: true },
609
- { id: "code", label: "\u884C\u5185\u4EE3\u7801", icon: FieldStringOutlined, handler: (v) => wrapSelection(v, "`", "`") },
610
- {
611
- id: "codeblock",
612
- label: "\u4EE3\u7801\u5757",
613
- icon: CodeOutlined,
614
- handler: (v) => {
615
- const { from, to } = v.state.selection.main;
616
- const selected = v.state.doc.sliceString(from, to);
617
- const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
618
- v.dispatch({ changes: { from, to, insert } });
619
- }
620
- },
621
- { id: "quote", label: "\u5F15\u7528", icon: BlockOutlined, handler: (v) => insertAtLineStart(v, "> ") },
622
- {
623
- id: "hr",
624
- label: "\u5206\u5272\u7EBF",
625
- icon: MinusOutlined,
626
- handler: (v) => {
627
- const { from } = v.state.selection.main;
628
- v.dispatch({ changes: { from, insert: "\n---\n" } });
629
- }
630
- }
631
- ];
573
+ const hasUploadConfig = isUploadConfigured(options);
574
+ return [
575
+ {
576
+ id: "heading",
577
+ icon: FontSizeOutlined,
578
+ label: "标题",
579
+ children: [
580
+ {
581
+ id: "heading1",
582
+ label: "标题 1",
583
+ icon: FontSizeOutlined,
584
+ handler: (v) => insertAtLineStart(v, "# ")
585
+ },
586
+ {
587
+ id: "heading2",
588
+ label: "标题 2",
589
+ icon: FontSizeOutlined,
590
+ handler: (v) => insertAtLineStart(v, "## ")
591
+ },
592
+ {
593
+ id: "heading3",
594
+ label: "标题 3",
595
+ icon: FontSizeOutlined,
596
+ handler: (v) => insertAtLineStart(v, "### ")
597
+ },
598
+ {
599
+ id: "heading4",
600
+ label: "标题 4",
601
+ icon: FontSizeOutlined,
602
+ handler: (v) => insertAtLineStart(v, "#### ")
603
+ },
604
+ {
605
+ id: "heading5",
606
+ label: "标题 5",
607
+ icon: FontSizeOutlined,
608
+ handler: (v) => insertAtLineStart(v, "##### ")
609
+ },
610
+ {
611
+ id: "heading6",
612
+ label: "标题 6",
613
+ icon: FontSizeOutlined,
614
+ handler: (v) => insertAtLineStart(v, "###### ")
615
+ }
616
+ ]
617
+ },
618
+ { separatorBefore: true },
619
+ {
620
+ id: "bold",
621
+ label: "加粗",
622
+ icon: BoldOutlined,
623
+ handler: (v) => wrapSelection(v, "**", "**")
624
+ },
625
+ {
626
+ id: "italic",
627
+ label: "斜体",
628
+ icon: ItalicOutlined,
629
+ handler: (v) => wrapSelection(v, "*", "*")
630
+ },
631
+ {
632
+ id: "strikethrough",
633
+ label: "删除线",
634
+ icon: StrikethroughOutlined,
635
+ handler: (v) => wrapSelection(v, "~~", "~~")
636
+ },
637
+ {
638
+ id: "underline",
639
+ label: "下划线",
640
+ icon: UnderlineOutlined,
641
+ handler: (v) => wrapSelection(v, "<u>", "</u>")
642
+ },
643
+ { separatorBefore: true },
644
+ {
645
+ id: "ul",
646
+ label: "无序列表",
647
+ icon: UnorderedListOutlined,
648
+ handler: (v) => insertAtLineStart(v, "- ")
649
+ },
650
+ {
651
+ id: "ol",
652
+ label: "有序列表",
653
+ icon: OrderedListOutlined,
654
+ handler: (v) => insertAtLineStart(v, "1. ")
655
+ },
656
+ { separatorBefore: true },
657
+ {
658
+ id: "link",
659
+ label: "链接",
660
+ icon: LinkOutlined,
661
+ handler: (v) => insertAtCursor(v, "[", "](url)", "文本")
662
+ },
663
+ {
664
+ id: "image",
665
+ label: hasUploadConfig ? "图片" : "请配置图片上传地址",
666
+ icon: PictureOutlined,
667
+ disabled: !hasUploadConfig,
668
+ handler: hasUploadConfig ? (v) => uploadImage(v, {
669
+ uploadUrl: options?.uploadUrl,
670
+ onUploadImage: options?.onUploadImage
671
+ }) : void 0
672
+ },
673
+ { separatorBefore: true },
674
+ {
675
+ id: "code",
676
+ label: "行内代码",
677
+ icon: FieldStringOutlined,
678
+ handler: (v) => wrapSelection(v, "`", "`")
679
+ },
680
+ {
681
+ id: "codeblock",
682
+ label: "代码块",
683
+ icon: CodeOutlined,
684
+ handler: (v) => {
685
+ const { from, to } = v.state.selection.main;
686
+ const selected = v.state.doc.sliceString(from, to);
687
+ const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
688
+ v.dispatch({ changes: {
689
+ from,
690
+ to,
691
+ insert
692
+ } });
693
+ }
694
+ },
695
+ {
696
+ id: "quote",
697
+ label: "引用",
698
+ icon: BlockOutlined,
699
+ handler: (v) => insertAtLineStart(v, "> ")
700
+ },
701
+ {
702
+ id: "hr",
703
+ label: "分割线",
704
+ icon: MinusOutlined,
705
+ handler: (v) => {
706
+ const { from } = v.state.selection.main;
707
+ v.dispatch({ changes: {
708
+ from,
709
+ insert: "\n---\n"
710
+ } });
711
+ }
712
+ }
713
+ ];
632
714
  }
633
-
634
- // src/components/Toolbar/hooks/useToolbar.ts
715
+ //#endregion
716
+ //#region src/components/Toolbar/hooks/useToolbar.ts
635
717
  function useToolbar(editorView, options) {
636
- const { extraItems, uploadConfig } = options || {};
637
- const config = useMemo(() => {
638
- const base = createToolbarConfig(uploadConfig);
639
- return extraItems ? [...base, ...extraItems] : base;
640
- }, [extraItems, uploadConfig]);
641
- const handleClick = useCallback((button) => {
642
- if (!editorView) return;
643
- if (button.wrapTags) {
644
- const [openTag, closeTag] = button.wrapTags;
645
- toggleWrap(editorView, openTag, closeTag);
646
- return;
647
- }
648
- if (button.handler) {
649
- editorView.focus();
650
- button.handler(editorView, editorView.state);
651
- }
652
- }, [editorView]);
653
- return { config, handleClick };
718
+ const { extraItems, uploadConfig } = options || {};
719
+ return {
720
+ config: useMemo(() => {
721
+ return createToolbarConfig(uploadConfig);
722
+ }, [uploadConfig]),
723
+ extraItemsConfig: useMemo(() => {
724
+ return extraItems || [];
725
+ }, [extraItems]),
726
+ handleClick: useCallback((button) => {
727
+ if (!editorView) return;
728
+ if (button.wrapTags) {
729
+ const [openTag, closeTag] = button.wrapTags;
730
+ toggleWrap(editorView, openTag, closeTag);
731
+ return;
732
+ }
733
+ if (button.handler) {
734
+ editorView.focus();
735
+ button.handler(editorView, editorView.state);
736
+ }
737
+ }, [editorView])
738
+ };
654
739
  }
655
-
656
- // src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
657
- import { Button, Dropdown, Tooltip } from "antd";
658
- import { DownOutlined } from "@ant-design/icons";
659
- import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
740
+ //#endregion
741
+ //#region src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
660
742
  function ToolbarItemRenderer({ item, onClick }) {
661
- const Icon = item.icon;
662
- if (item.children && item.children.length > 0) {
663
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
664
- item.separatorBefore && /* @__PURE__ */ jsx4("div", { className: "toolbar-separator" }),
665
- /* @__PURE__ */ jsx4("div", { className: "toolbar-group", children: /* @__PURE__ */ jsx4(Tooltip, { title: item.label, children: /* @__PURE__ */ jsx4(
666
- Dropdown,
667
- {
668
- menu: {
669
- items: item.children.map((child) => {
670
- var _a;
671
- return {
672
- type: "item",
673
- key: (_a = child.id) != null ? _a : "",
674
- label: /* @__PURE__ */ jsxs2("span", { onClick: () => onClick(child), children: [
675
- child.icon && (() => {
676
- const ChildIcon = child.icon;
677
- return /* @__PURE__ */ jsx4(ChildIcon, {});
678
- })(),
679
- " ",
680
- child.label
681
- ] })
682
- };
683
- })
684
- },
685
- placement: "bottomLeft",
686
- children: /* @__PURE__ */ jsxs2(Button, { className: "toolbar-button", type: "text", size: "small", children: [
687
- /* @__PURE__ */ jsx4(Icon, {}),
688
- /* @__PURE__ */ jsx4(DownOutlined, { style: { marginLeft: 2, fontSize: 10 } })
689
- ] })
690
- }
691
- ) }) })
692
- ] });
693
- }
694
- const isDisabled = item.disabled;
695
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
696
- item.separatorBefore && /* @__PURE__ */ jsx4("div", { className: "toolbar-separator" }),
697
- /* @__PURE__ */ jsx4("div", { className: "toolbar-group", children: /* @__PURE__ */ jsx4(Tooltip, { title: item.label, children: /* @__PURE__ */ jsx4(
698
- Button,
699
- {
700
- id: item.id,
701
- className: "toolbar-button",
702
- type: "text",
703
- size: "small",
704
- disabled: isDisabled,
705
- onClick: () => !isDisabled && onClick(item),
706
- children: /* @__PURE__ */ jsx4(Icon, {})
707
- }
708
- ) }) })
709
- ] });
743
+ const Icon = item.icon;
744
+ if (item.children && item.children.length > 0) return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
745
+ className: "toolbar-group",
746
+ children: /* @__PURE__ */ jsx(Tooltip, {
747
+ title: item.label,
748
+ children: /* @__PURE__ */ jsx(Dropdown, {
749
+ menu: { items: item.children.map((child) => ({
750
+ type: "item",
751
+ key: child.id ?? "",
752
+ label: /* @__PURE__ */ jsxs("span", {
753
+ onClick: () => onClick(child),
754
+ children: [
755
+ child.icon && (() => {
756
+ const ChildIcon = child.icon;
757
+ return /* @__PURE__ */ jsx(ChildIcon, {});
758
+ })(),
759
+ " ",
760
+ child.label
761
+ ]
762
+ })
763
+ })) },
764
+ placement: "bottomLeft",
765
+ children: /* @__PURE__ */ jsxs(Button, {
766
+ className: "toolbar-button",
767
+ type: "text",
768
+ size: "small",
769
+ children: [/* @__PURE__ */ jsx(Icon, {}), /* @__PURE__ */ jsx(DownOutlined, { style: {
770
+ marginLeft: 2,
771
+ fontSize: 10
772
+ } })]
773
+ })
774
+ })
775
+ })
776
+ })] });
777
+ const isDisabled = item.disabled;
778
+ return /* @__PURE__ */ jsxs(Fragment, { children: [item.separatorBefore && /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }), /* @__PURE__ */ jsx("div", {
779
+ className: "toolbar-group",
780
+ children: /* @__PURE__ */ jsx(Tooltip, {
781
+ title: item.label,
782
+ children: /* @__PURE__ */ jsx(Button, {
783
+ id: item.id,
784
+ className: "toolbar-button",
785
+ type: "text",
786
+ size: "small",
787
+ disabled: isDisabled,
788
+ onClick: () => !isDisabled && onClick(item),
789
+ children: /* @__PURE__ */ jsx(Icon, {})
790
+ })
791
+ })
792
+ })] });
710
793
  }
711
-
712
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
713
- import React, { useCallback as useCallback3, useRef as useRef3 } from "react";
714
- import { Button as Button2, Dropdown as Dropdown2 } from "antd";
715
- import { TableOutlined } from "@ant-design/icons";
716
-
717
- // src/components/Toolbar/renderers/TablePicker/index.tsx
718
- import { useState, useRef as useRef2, useEffect as useEffect3, useCallback as useCallback2 } from "react";
719
-
720
- // src/components/Toolbar/renderers/TablePicker/index.module.less
721
- var index_module_default = {};
722
-
723
- // src/components/Toolbar/renderers/TablePicker/index.tsx
724
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
794
+ var index_module_default = {
795
+ container: "index-module__container___l6BT-",
796
+ grid: "index-module__grid___tCZ-r",
797
+ cell: "index-module__cell___vTDQz",
798
+ cellActive: "index-module__cellActive___0EYvU",
799
+ hint: "index-module__hint___dTObk",
800
+ label: "index-module__label___2CmK1"
801
+ };
802
+ //#endregion
803
+ //#region src/components/Toolbar/renderers/TablePicker/index.tsx
725
804
  var MAX_ROW = 8;
726
805
  var MAX_COL = 8;
727
806
  function TablePicker({ onSelect }) {
728
- const [hoverSize, setHoverSize] = useState({ row: 0, col: 0 });
729
- const [confirmedSize, setConfirmedSize] = useState({ row: 0, col: 0 });
730
- const maxColRef = useRef2(0);
731
- const prevRowRef = useRef2(0);
732
- const timerIdRef = useRef2(null);
733
- const refreshTimerRef = useRef2(null);
734
- const commitRef = useRef2(null);
735
- const clearTimer = useCallback2(() => {
736
- if (timerIdRef.current) {
737
- clearTimeout(timerIdRef.current);
738
- timerIdRef.current = null;
739
- }
740
- }, []);
741
- const clearFlash = useCallback2(() => {
742
- if (refreshTimerRef.current) {
743
- clearInterval(refreshTimerRef.current);
744
- refreshTimerRef.current = null;
745
- }
746
- }, []);
747
- useEffect3(
748
- () => () => {
749
- if (timerIdRef.current) clearTimeout(timerIdRef.current);
750
- if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
751
- },
752
- []
753
- );
754
- function extend(col, row) {
755
- if (row < prevRowRef.current || col < maxColRef.current) {
756
- maxColRef.current = 0;
757
- }
758
- if (col > maxColRef.current) {
759
- maxColRef.current = col;
760
- }
761
- setHoverSize({ row, col });
762
- setConfirmedSize({ row, col: maxColRef.current });
763
- if (row === 1) {
764
- commitRef.current = { cols: maxColRef.current, rows: 1 };
765
- }
766
- }
767
- function flash(col, dir, limit, finalCallback) {
768
- let current = dir === 1 ? 1 : limit;
769
- const step = dir === 1 ? 1 : -1;
770
- refreshTimerRef.current = setInterval(() => {
771
- setHoverSize({ row: current, col });
772
- if (current > confirmedSize.row || dir === -1) {
773
- maxColRef.current = Math.min(MAX_COL, col);
774
- }
775
- current += step;
776
- if (dir === 1 && current > limit || dir === -1 && current < 1) {
777
- clearInterval(refreshTimerRef.current);
778
- refreshTimerRef.current = null;
779
- const finalRow = current - step;
780
- setHoverSize({ row: finalRow, col });
781
- finalCallback(col, finalRow);
782
- }
783
- }, 50);
784
- }
785
- function onCellEnterCheck(index) {
786
- clearTimer();
787
- clearFlash();
788
- const row = Math.floor(index / MAX_COL) + 1;
789
- const col = index % MAX_COL + 1;
790
- const prevRow = prevRowRef.current;
791
- const isJump = Math.abs(row - prevRow) > 1;
792
- const isDown = row > prevRow;
793
- prevRowRef.current = row;
794
- if (!isJump) {
795
- extend(col, row);
796
- } else if (isDown) {
797
- if (col > maxColRef.current || maxColRef.current >= MAX_COL) {
798
- const finalRow = Math.min(row, MAX_ROW);
799
- flash(col, 1, finalRow, (c, r) => {
800
- setConfirmedSize({ row: r, col: c });
801
- });
802
- } else {
803
- extend(col, row);
804
- }
805
- } else {
806
- maxColRef.current = 0;
807
- flash(col, -1, row, (c, r) => {
808
- setConfirmedSize({ row: r, col: c });
809
- if (r === 1) {
810
- commitRef.current = { cols: Math.min(MAX_COL, col), rows: 1 };
811
- setConfirmedSize({ row: 1, col: Math.min(MAX_COL, col) });
812
- }
813
- });
814
- }
815
- }
816
- function onMouseDown() {
817
- const size = commitRef.current || { cols: maxColRef.current, rows: confirmedSize.row };
818
- if (size.rows > 0 && size.cols > 0) {
819
- onSelect(size);
820
- }
821
- }
822
- return /* @__PURE__ */ jsxs3(
823
- "div",
824
- {
825
- className: index_module_default.container,
826
- onMouseDown,
827
- children: [
828
- /* @__PURE__ */ jsx5(
829
- "div",
830
- {
831
- className: index_module_default.grid,
832
- style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
833
- children: Array.from({ length: MAX_ROW * MAX_COL }).map((_, i) => {
834
- const row = Math.floor(i / MAX_COL) + 1;
835
- const col = i % MAX_COL + 1;
836
- const isSelected = row <= hoverSize.row && col <= maxColRef.current;
837
- return /* @__PURE__ */ jsx5(
838
- "div",
839
- {
840
- className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
841
- onMouseEnter: () => onCellEnterCheck(i)
842
- },
843
- i
844
- );
845
- })
846
- }
847
- ),
848
- /* @__PURE__ */ jsxs3("div", { className: index_module_default.label, children: [
849
- hoverSize.row,
850
- " x ",
851
- maxColRef.current || 1
852
- ] })
853
- ]
854
- }
855
- );
807
+ const [hoverSize, setHoverSize] = useState({
808
+ row: 0,
809
+ col: 0
810
+ });
811
+ const [confirmedSize, setConfirmedSize] = useState({
812
+ row: 0,
813
+ col: 0
814
+ });
815
+ const maxColRef = useRef(0);
816
+ const prevRowRef = useRef(0);
817
+ const timerIdRef = useRef(null);
818
+ const refreshTimerRef = useRef(null);
819
+ const commitRef = useRef(null);
820
+ const clearTimer = useCallback(() => {
821
+ if (timerIdRef.current) {
822
+ clearTimeout(timerIdRef.current);
823
+ timerIdRef.current = null;
824
+ }
825
+ }, []);
826
+ const clearFlash = useCallback(() => {
827
+ if (refreshTimerRef.current) {
828
+ clearInterval(refreshTimerRef.current);
829
+ refreshTimerRef.current = null;
830
+ }
831
+ }, []);
832
+ useEffect(() => () => {
833
+ if (timerIdRef.current) clearTimeout(timerIdRef.current);
834
+ if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
835
+ }, []);
836
+ /**
837
+ * 扩展选中区域至 (col, row)
838
+ */
839
+ function extend(col, row) {
840
+ if (row < prevRowRef.current || col < maxColRef.current) maxColRef.current = 0;
841
+ if (col > maxColRef.current) maxColRef.current = col;
842
+ setHoverSize({
843
+ row,
844
+ col
845
+ });
846
+ setConfirmedSize({
847
+ row,
848
+ col: maxColRef.current
849
+ });
850
+ if (row === 1) commitRef.current = {
851
+ cols: maxColRef.current,
852
+ rows: 1
853
+ };
854
+ }
855
+ /**
856
+ * 跳跃动画效果
857
+ */
858
+ function flash(col, dir, limit, finalCallback) {
859
+ let current = dir === 1 ? 1 : limit;
860
+ const step = dir === 1 ? 1 : -1;
861
+ refreshTimerRef.current = setInterval(() => {
862
+ setHoverSize({
863
+ row: current,
864
+ col
865
+ });
866
+ if (current > confirmedSize.row || dir === -1) maxColRef.current = Math.min(MAX_COL, col);
867
+ current += step;
868
+ if (dir === 1 && current > limit || dir === -1 && current < 1) {
869
+ clearInterval(refreshTimerRef.current);
870
+ refreshTimerRef.current = null;
871
+ const finalRow = current - step;
872
+ setHoverSize({
873
+ row: finalRow,
874
+ col
875
+ });
876
+ finalCallback(col, finalRow);
877
+ }
878
+ }, 50);
879
+ }
880
+ /**
881
+ * 核心逻辑:鼠标进入格子
882
+ */
883
+ function onCellEnterCheck(index) {
884
+ clearTimer();
885
+ clearFlash();
886
+ const row = Math.floor(index / MAX_COL) + 1;
887
+ const col = index % MAX_COL + 1;
888
+ const prevRow = prevRowRef.current;
889
+ const isJump = Math.abs(row - prevRow) > 1;
890
+ const isDown = row > prevRow;
891
+ prevRowRef.current = row;
892
+ if (!isJump) extend(col, row);
893
+ else if (isDown) {
894
+ if (col > maxColRef.current || maxColRef.current >= MAX_COL) flash(col, 1, Math.min(row, MAX_ROW), (c, r) => {
895
+ setConfirmedSize({
896
+ row: r,
897
+ col: c
898
+ });
899
+ });
900
+ else extend(col, row);
901
+ } else {
902
+ maxColRef.current = 0;
903
+ flash(col, -1, row, (c, r) => {
904
+ setConfirmedSize({
905
+ row: r,
906
+ col: c
907
+ });
908
+ if (r === 1) {
909
+ commitRef.current = {
910
+ cols: Math.min(MAX_COL, col),
911
+ rows: 1
912
+ };
913
+ setConfirmedSize({
914
+ row: 1,
915
+ col: Math.min(MAX_COL, col)
916
+ });
917
+ }
918
+ });
919
+ }
920
+ }
921
+ function onMouseDown() {
922
+ const size = commitRef.current || {
923
+ cols: maxColRef.current,
924
+ rows: confirmedSize.row
925
+ };
926
+ if (size.rows > 0 && size.cols > 0) onSelect(size);
927
+ }
928
+ return /* @__PURE__ */ jsxs("div", {
929
+ className: index_module_default.container,
930
+ onMouseDown,
931
+ children: [/* @__PURE__ */ jsx("div", {
932
+ className: index_module_default.grid,
933
+ style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
934
+ children: Array.from({ length: 64 }).map((_, i) => {
935
+ const row = Math.floor(i / MAX_COL) + 1;
936
+ const col = i % MAX_COL + 1;
937
+ const isSelected = row <= hoverSize.row && col <= maxColRef.current;
938
+ return /* @__PURE__ */ jsx("div", {
939
+ className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
940
+ onMouseEnter: () => onCellEnterCheck(i)
941
+ }, i);
942
+ })
943
+ }), /* @__PURE__ */ jsxs("div", {
944
+ className: index_module_default.label,
945
+ children: [
946
+ hoverSize.row,
947
+ " x ",
948
+ maxColRef.current || 1
949
+ ]
950
+ })]
951
+ });
856
952
  }
857
-
858
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
859
- import { jsx as jsx6 } from "react/jsx-runtime";
953
+ //#endregion
954
+ //#region src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
860
955
  function TableDropdown({ editorView }) {
861
- const [open, setOpen] = React.useState(false);
862
- const openCountRef = useRef3(0);
863
- const handleTableSelect = useCallback3((rows, cols) => {
864
- if (!editorView || rows <= 0 || cols <= 0) return;
865
- editorView.focus();
866
- const headerCells = Array(cols).fill(null).map((_, i) => ` \u5217${i + 1} `).join("|");
867
- const separator = Array(cols).fill(" --- ").join("|");
868
- const dataCell = Array(cols).fill(" \u5185\u5BB9 ").join("|");
869
- const dataRows = Array(rows).fill(`|${dataCell}|`).join("\n");
870
- const table = `
871
- |${headerCells}|
872
- |${separator}|
873
- ${dataRows}
874
- `;
875
- const { from } = editorView.state.selection.main;
876
- editorView.dispatch({ changes: { from, insert: table } });
877
- }, [editorView]);
878
- return /* @__PURE__ */ jsx6(
879
- Dropdown2,
880
- {
881
- trigger: ["click"],
882
- open,
883
- onOpenChange: setOpen,
884
- popupRender: () => {
885
- openCountRef.current++;
886
- return /* @__PURE__ */ jsx6("div", { style: { padding: 4 }, onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx6(
887
- TablePicker,
888
- {
889
- onSelect: (size) => {
890
- handleTableSelect(size.rows, size.cols);
891
- setOpen(false);
892
- }
893
- },
894
- openCountRef.current
895
- ) });
896
- },
897
- placement: "bottomLeft",
898
- children: /* @__PURE__ */ jsx6(Button2, { className: "toolbar-button", type: "text", size: "small", title: "\u8868\u683C", children: /* @__PURE__ */ jsx6(TableOutlined, {}) })
899
- }
900
- );
956
+ const [open, setOpen] = React.useState(false);
957
+ const openCountRef = useRef(0);
958
+ const handleTableSelect = useCallback((rows, cols) => {
959
+ if (!editorView || rows <= 0 || cols <= 0) return;
960
+ editorView.focus();
961
+ const headerCells = Array(cols).fill(null).map((_, i) => ` 列${i + 1} `).join("|");
962
+ const separator = Array(cols).fill(" --- ").join("|");
963
+ const dataCell = Array(cols).fill(" 内容 ").join("|");
964
+ const table = `\n|${headerCells}|\n|${separator}|\n${Array(rows).fill(`|${dataCell}|`).join("\n")}\n`;
965
+ const { from } = editorView.state.selection.main;
966
+ editorView.dispatch({ changes: {
967
+ from,
968
+ insert: table
969
+ } });
970
+ }, [editorView]);
971
+ return /* @__PURE__ */ jsx(Dropdown, {
972
+ trigger: ["click"],
973
+ open,
974
+ onOpenChange: setOpen,
975
+ popupRender: () => {
976
+ openCountRef.current++;
977
+ return /* @__PURE__ */ jsx("div", {
978
+ style: { padding: 4 },
979
+ onClick: (e) => e.stopPropagation(),
980
+ children: /* @__PURE__ */ jsx(TablePicker, { onSelect: (size) => {
981
+ handleTableSelect(size.rows, size.cols);
982
+ setOpen(false);
983
+ } }, openCountRef.current)
984
+ });
985
+ },
986
+ placement: "bottomLeft",
987
+ children: /* @__PURE__ */ jsx(Button, {
988
+ className: "toolbar-button",
989
+ type: "text",
990
+ size: "small",
991
+ title: "表格",
992
+ children: /* @__PURE__ */ jsx(TableOutlined, {})
993
+ })
994
+ });
901
995
  }
902
-
903
- // src/components/Toolbar/index.tsx
904
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
996
+ //#endregion
997
+ //#region src/components/Toolbar/index.tsx
905
998
  function Toolbar({ editorView, fullscreen = true, onToggleFullscreen, extraItems, uploadConfig }) {
906
- const { config, handleClick } = useToolbar(editorView, { extraItems, uploadConfig });
907
- const renderFullscreen = useCallback4(() => {
908
- if (!onToggleFullscreen) return null;
909
- const Icon = fullscreen ? FullscreenExitOutlined : FullscreenOutlined;
910
- return /* @__PURE__ */ jsx7(
911
- Button3,
912
- {
913
- className: "toolbar-button",
914
- type: "text",
915
- size: "small",
916
- title: fullscreen ? "\u9000\u51FA\u5168\u5C4F" : "\u5168\u5C4F",
917
- onClick: () => {
918
- onToggleFullscreen();
919
- },
920
- children: /* @__PURE__ */ jsx7(Icon, {})
921
- }
922
- );
923
- }, [fullscreen, onToggleFullscreen]);
924
- function isSeparator(item) {
925
- return !!(!item.id && item.separatorBefore);
926
- }
927
- return /* @__PURE__ */ jsxs4("div", { className: "toolbar", children: [
928
- config.map((item, idx) => {
929
- if (isSeparator(item)) {
930
- return /* @__PURE__ */ jsx7("div", { className: "toolbar-separator" }, `sep-${idx}`);
931
- }
932
- return /* @__PURE__ */ jsx7(
933
- ToolbarItemRenderer,
934
- {
935
- item,
936
- onClick: handleClick
937
- },
938
- item.id
939
- );
940
- }),
941
- /* @__PURE__ */ jsx7(TableDropdown, { editorView }),
942
- renderFullscreen()
943
- ] });
999
+ const { config, extraItemsConfig, handleClick } = useToolbar(editorView, {
1000
+ extraItems,
1001
+ uploadConfig
1002
+ });
1003
+ const renderFullscreen = useCallback(() => {
1004
+ if (!onToggleFullscreen) return null;
1005
+ return /* @__PURE__ */ jsx(Button, {
1006
+ className: "toolbar-button",
1007
+ type: "text",
1008
+ size: "small",
1009
+ title: fullscreen ? "退出全屏" : "全屏",
1010
+ onClick: () => {
1011
+ onToggleFullscreen();
1012
+ },
1013
+ children: /* @__PURE__ */ jsx(fullscreen ? FullscreenExitOutlined : FullscreenOutlined, {})
1014
+ });
1015
+ }, [fullscreen, onToggleFullscreen]);
1016
+ function isSeparator(item) {
1017
+ return !!(!item.id && item.separatorBefore);
1018
+ }
1019
+ return /* @__PURE__ */ jsxs("div", {
1020
+ className: "toolbar",
1021
+ children: [
1022
+ config.map((item, idx) => {
1023
+ if (isSeparator(item)) return /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }, `sep-${idx}`);
1024
+ return /* @__PURE__ */ jsx(ToolbarItemRenderer, {
1025
+ item,
1026
+ onClick: handleClick
1027
+ }, item.id);
1028
+ }),
1029
+ /* @__PURE__ */ jsx(TableDropdown, { editorView }),
1030
+ extraItemsConfig?.map((item, idx) => {
1031
+ if (isSeparator(item)) return /* @__PURE__ */ jsx("div", { className: "toolbar-separator" }, `extra-sep-${idx}`);
1032
+ return /* @__PURE__ */ jsx(ToolbarItemRenderer, {
1033
+ item,
1034
+ onClick: handleClick
1035
+ }, item.id);
1036
+ }),
1037
+ renderFullscreen()
1038
+ ]
1039
+ });
944
1040
  }
945
-
946
- // src/components/Dialog/EditDialog.tsx
947
- import { useState as useState2, useMemo as useMemo2 } from "react";
948
- import { Modal } from "antd";
949
- import { jsx as jsx8 } from "react/jsx-runtime";
1041
+ //#endregion
1042
+ //#region src/components/Dialog/EditDialog.tsx
1043
+ /**
1044
+ * 属性编辑弹窗组件
1045
+ * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
1046
+ */
950
1047
  function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
951
- const schema = componentRegistry_default.get(typeName);
952
- const [formProps, setFormProps] = useState2({});
953
- const mergedProps = useMemo2(() => {
954
- var _a;
955
- if (!schema) return currentProps;
956
- const defaults = {};
957
- for (const field of schema.fields) {
958
- defaults[field.key] = (_a = field.defaultValue) != null ? _a : "";
959
- }
960
- return { ...defaults, ...currentProps };
961
- }, [schema, currentProps]);
962
- const handleDialogChange = (key, value) => {
963
- setFormProps((prev) => ({ ...prev, [key]: value }));
964
- };
965
- const handleOk = () => {
966
- const finalProps = visible ? { ...mergedProps, ...formProps } : mergedProps;
967
- onSave(finalProps);
968
- setFormProps({});
969
- };
970
- const handleClose = () => {
971
- setFormProps({});
972
- onClose();
973
- };
974
- if (!schema) {
975
- return null;
976
- }
977
- return /* @__PURE__ */ jsx8(
978
- Modal,
979
- {
980
- title: `${schema.label} \u5C5E\u6027\u7F16\u8F91`,
981
- open: visible,
982
- onOk: handleOk,
983
- onCancel: handleClose,
984
- okText: "\u4FDD\u5B58",
985
- cancelText: "\u53D6\u6D88",
986
- children: schema.renderDialog(visible ? { ...mergedProps, ...formProps } : mergedProps, handleDialogChange)
987
- }
988
- );
1048
+ const schema = componentRegistry.get(typeName);
1049
+ const [formProps, setFormProps] = useState({});
1050
+ const mergedProps = useMemo(() => {
1051
+ if (!schema) return currentProps;
1052
+ const defaults = {};
1053
+ for (const field of schema.fields || []) defaults[field.key] = field.defaultValue ?? "";
1054
+ return {
1055
+ ...defaults,
1056
+ ...currentProps
1057
+ };
1058
+ }, [schema, currentProps]);
1059
+ const handleDialogChange = (key, value) => {
1060
+ setFormProps((prev) => ({
1061
+ ...prev,
1062
+ [key]: value
1063
+ }));
1064
+ };
1065
+ const handleOk = () => {
1066
+ onSave(visible ? {
1067
+ ...mergedProps,
1068
+ ...formProps
1069
+ } : mergedProps);
1070
+ setFormProps({});
1071
+ };
1072
+ const handleClose = () => {
1073
+ setFormProps({});
1074
+ onClose();
1075
+ };
1076
+ if (!schema || !schema.renderDialog) return null;
1077
+ return /* @__PURE__ */ jsx(Modal, {
1078
+ title: `${schema.label} 属性编辑`,
1079
+ open: visible,
1080
+ onOk: handleOk,
1081
+ onCancel: handleClose,
1082
+ okText: "保存",
1083
+ cancelText: "取消",
1084
+ children: schema.renderDialog(visible ? {
1085
+ ...mergedProps,
1086
+ ...formProps
1087
+ } : mergedProps, handleDialogChange)
1088
+ });
989
1089
  }
990
-
991
- // src/utils/writeContainer.ts
1090
+ //#endregion
1091
+ //#region src/utils/writeContainer.ts
1092
+ /**
1093
+ * 从属性对象生成 JSON 属性行。
1094
+ * 格式: { key: 'value', key2: 'value2' }
1095
+ */
992
1096
  function writeContainerProps(props) {
993
- const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
994
- return entries ? `{ ${entries} }` : "{}";
1097
+ const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1098
+ return entries ? `{ ${entries} }` : "{}";
995
1099
  }
1100
+ /**
1101
+ * 更新编辑器文档中的容器属性。
1102
+ * 替换 JSON 属性行(::: 打开行之后的一行)为新属性。
1103
+ */
996
1104
  function updateContainerProps(view, propsLineFrom, propsLineTo, newProps) {
997
- const newPropsLine = writeContainerProps(newProps);
998
- view.dispatch({
999
- changes: {
1000
- from: propsLineFrom,
1001
- to: propsLineTo,
1002
- insert: newPropsLine
1003
- }
1004
- });
1105
+ const newPropsLine = writeContainerProps(newProps);
1106
+ view.dispatch({ changes: {
1107
+ from: propsLineFrom,
1108
+ to: propsLineTo,
1109
+ insert: newPropsLine
1110
+ } });
1005
1111
  }
1112
+ /**
1113
+ * 在当前光标位置插入新容器。
1114
+ * 创建 :::type 打开行 + JSON 属性 + ::: 关闭行的结构。
1115
+ */
1006
1116
  function insertContainer(view, typeName, defaultProps) {
1007
- const { from } = view.state.selection.main;
1008
- const propsLine = writeContainerProps(defaultProps);
1009
- const container = `:::${typeName}
1010
- ${propsLine}
1011
- :::
1012
- `;
1013
- view.dispatch({ changes: { from, insert: container } });
1117
+ const { from } = view.state.selection.main;
1118
+ const container = `:::${typeName}\n${writeContainerProps(defaultProps)}\n:::\n`;
1119
+ view.dispatch({ changes: {
1120
+ from,
1121
+ insert: container
1122
+ } });
1014
1123
  }
1015
-
1016
- // src/MarkdownEditor.tsx
1017
- import { ReadOutlined } from "@ant-design/icons";
1018
-
1019
- // src/hooks/useFullscreen.ts
1020
- import { useState as useState3, useEffect as useEffect4, useCallback as useCallback5 } from "react";
1124
+ //#endregion
1125
+ //#region src/hooks/useFullscreen.ts
1021
1126
  function useFullscreen() {
1022
- const [fullscreen, setFullscreen] = useState3(false);
1023
- const toggleFullscreen = useCallback5(() => {
1024
- if (document.fullscreenElement) {
1025
- document.exitFullscreen().catch((e) => {
1026
- console.error("\u9000\u51FA\u5168\u5C4F\u5931\u8D25:", e);
1027
- });
1028
- } else {
1029
- const el = document.documentElement;
1030
- const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1031
- if (requestMethod) {
1032
- requestMethod.call(el).catch((e) => {
1033
- console.error("\u8FDB\u5165\u5168\u5C4F\u5931\u8D25:", e);
1034
- });
1035
- }
1036
- }
1037
- }, []);
1038
- useEffect4(() => {
1039
- const handleFullscreenChange = () => {
1040
- setFullscreen(!!document.fullscreenElement);
1041
- };
1042
- document.addEventListener("fullscreenchange", handleFullscreenChange);
1043
- document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1044
- return () => {
1045
- document.removeEventListener("fullscreenchange", handleFullscreenChange);
1046
- document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1047
- };
1048
- }, []);
1049
- return { fullscreen, toggleFullscreen };
1127
+ const [fullscreen, setFullscreen] = useState(false);
1128
+ const toggleFullscreen = useCallback(() => {
1129
+ if (document.fullscreenElement) document.exitFullscreen().catch((e) => {
1130
+ console.error("退出全屏失败:", e);
1131
+ });
1132
+ else {
1133
+ const el = document.documentElement;
1134
+ const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1135
+ if (requestMethod) requestMethod.call(el).catch((e) => {
1136
+ console.error("进入全屏失败:", e);
1137
+ });
1138
+ }
1139
+ }, []);
1140
+ useEffect(() => {
1141
+ const handleFullscreenChange = () => {
1142
+ setFullscreen(!!document.fullscreenElement);
1143
+ };
1144
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
1145
+ document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1146
+ return () => {
1147
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
1148
+ document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1149
+ };
1150
+ }, []);
1151
+ return {
1152
+ fullscreen,
1153
+ toggleFullscreen
1154
+ };
1050
1155
  }
1051
-
1052
- // src/hooks/useScrollSync.ts
1053
- import { useEffect as useEffect5, useRef as useRef4 } from "react";
1156
+ //#endregion
1157
+ //#region src/hooks/useScrollSync.ts
1158
+ /**
1159
+ * 滚动同步 Hook
1160
+ * 将源滚动容器的滚动位置同步到目标预览容器
1161
+ */
1054
1162
  function useScrollSync(sourceRef, targetRef) {
1055
- const rafIdRef = useRef4(null);
1056
- useEffect5(() => {
1057
- const source = sourceRef.current;
1058
- const target = targetRef.current;
1059
- if (!source || !target) return;
1060
- const listener = () => {
1061
- if (rafIdRef.current) return;
1062
- rafIdRef.current = requestAnimationFrame(() => {
1063
- const s = sourceRef.current;
1064
- const t = targetRef.current;
1065
- if (!s || !t) {
1066
- rafIdRef.current = null;
1067
- return;
1068
- }
1069
- const sourceScrollHeight = s.scrollHeight - s.clientHeight;
1070
- if (sourceScrollHeight <= 0) {
1071
- rafIdRef.current = null;
1072
- return;
1073
- }
1074
- const ratio = s.scrollTop / sourceScrollHeight;
1075
- const targetScrollHeight = t.scrollHeight - t.clientHeight;
1076
- if (targetScrollHeight > 0) {
1077
- t.scrollTop = ratio * targetScrollHeight;
1078
- }
1079
- rafIdRef.current = null;
1080
- });
1081
- };
1082
- source.addEventListener("scroll", listener, { passive: true });
1083
- return () => {
1084
- source.removeEventListener("scroll", listener);
1085
- if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
1086
- };
1087
- }, [sourceRef, targetRef]);
1163
+ const rafIdRef = useRef(null);
1164
+ useEffect(() => {
1165
+ const source = sourceRef.current;
1166
+ const target = targetRef.current;
1167
+ if (!source || !target) return;
1168
+ const listener = () => {
1169
+ if (rafIdRef.current) return;
1170
+ rafIdRef.current = requestAnimationFrame(() => {
1171
+ const s = sourceRef.current;
1172
+ const t = targetRef.current;
1173
+ if (!s || !t) {
1174
+ rafIdRef.current = null;
1175
+ return;
1176
+ }
1177
+ const sourceScrollHeight = s.scrollHeight - s.clientHeight;
1178
+ if (sourceScrollHeight <= 0) {
1179
+ rafIdRef.current = null;
1180
+ return;
1181
+ }
1182
+ const ratio = s.scrollTop / sourceScrollHeight;
1183
+ const targetScrollHeight = t.scrollHeight - t.clientHeight;
1184
+ if (targetScrollHeight > 0) t.scrollTop = ratio * targetScrollHeight;
1185
+ rafIdRef.current = null;
1186
+ });
1187
+ };
1188
+ source.addEventListener("scroll", listener, { passive: true });
1189
+ return () => {
1190
+ source.removeEventListener("scroll", listener);
1191
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
1192
+ };
1193
+ }, [sourceRef, targetRef]);
1088
1194
  }
1089
-
1090
- // src/MarkdownEditor.tsx
1091
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1092
- var MarkdownEditor_default = forwardRef2(function MarkdownEditor({
1093
- defaultValue = "",
1094
- value,
1095
- onChange,
1096
- showPreview = true,
1097
- fullscreen: fullscreenProp = false,
1098
- previewOptions,
1099
- uploadConfig
1100
- }, ref) {
1101
- const [content, setContent] = useState4(value != null ? value : defaultValue);
1102
- const [editorView, setEditorView] = useState4(null);
1103
- const scrollDOMRef = useRef5(null);
1104
- const previewElRef = useRef5(null);
1105
- const { fullscreen, toggleFullscreen } = useFullscreen();
1106
- useScrollSync(scrollDOMRef, previewElRef);
1107
- const [dialogVisible, setDialogVisible] = useState4(false);
1108
- const [dialogTypeName, setDialogTypeName] = useState4("");
1109
- const [dialogProps, setDialogProps] = useState4({});
1110
- const [dialogPropsRange, setDialogPropsRange] = useState4([0, 0]);
1111
- useEffect6(() => {
1112
- if (value !== void 0) {
1113
- setContent(value);
1114
- }
1115
- }, [value]);
1116
- const internalSetContent = useCallback6((newContent) => {
1117
- setContent(newContent);
1118
- onChange == null ? void 0 : onChange(newContent);
1119
- }, [onChange]);
1120
- useImperativeHandle(ref, () => ({
1121
- getContent: () => content
1122
- }), [content]);
1123
- const extraToolbarItems = componentRegistry_default.getAll().map((schema) => {
1124
- var _a, _b, _c;
1125
- const defaultProps = {};
1126
- for (const field of (_a = schema == null ? void 0 : schema.fields) != null ? _a : []) {
1127
- defaultProps[field.key] = (_b = field.defaultValue) != null ? _b : "";
1128
- }
1129
- const iconNode = (_c = schema.icon) != null ? _c : /* @__PURE__ */ jsx9(ReadOutlined, {});
1130
- return {
1131
- id: `container-${schema.name}`,
1132
- label: schema.label,
1133
- icon: () => iconNode,
1134
- handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1135
- };
1136
- });
1137
- const handleScrollReady = useCallback6((scrollDOM) => {
1138
- scrollDOMRef.current = scrollDOM;
1139
- }, []);
1140
- const handlePreviewRef = useCallback6((node) => {
1141
- previewElRef.current = node;
1142
- }, []);
1143
- const handleViewReady = useCallback6((view) => {
1144
- setEditorView(view);
1145
- }, []);
1146
- const handleEditContainer = useCallback6((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1147
- setDialogTypeName(typeName);
1148
- setDialogProps({ ...currentProps });
1149
- setDialogPropsRange([propsLineFrom, propsLineTo]);
1150
- setDialogVisible(true);
1151
- }, []);
1152
- const handleDialogSave = useCallback6((newProps) => {
1153
- setDialogVisible(false);
1154
- if (editorView) {
1155
- updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1156
- }
1157
- }, [editorView, dialogPropsRange]);
1158
- const handleDialogClose = useCallback6(() => {
1159
- setDialogVisible(false);
1160
- }, []);
1161
- return /* @__PURE__ */ jsxs5("div", { className: fullscreen ? "app fullscreen" : "app", children: [
1162
- /* @__PURE__ */ jsx9(
1163
- Toolbar,
1164
- {
1165
- editorView,
1166
- fullscreen,
1167
- onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1168
- extraItems: extraToolbarItems,
1169
- uploadConfig
1170
- }
1171
- ),
1172
- showPreview ? /* @__PURE__ */ jsx9(
1173
- SplitPanels,
1174
- {
1175
- leftPanel: /* @__PURE__ */ jsx9(
1176
- Editor,
1177
- {
1178
- value: content,
1179
- onChange: internalSetContent,
1180
- onScrollReady: handleScrollReady,
1181
- onViewReady: handleViewReady,
1182
- onEditContainer: handleEditContainer,
1183
- uploadConfig,
1184
- onMessageNoUploadConfig: () => message.warning("\u8BF7\u5148\u914D\u7F6E\u56FE\u7247\u4E0A\u4F20\u529F\u80FD")
1185
- }
1186
- ),
1187
- rightPanel: /* @__PURE__ */ jsx9(
1188
- Preview_default,
1189
- {
1190
- content,
1191
- ref: handlePreviewRef,
1192
- previewOptions
1193
- }
1194
- )
1195
- }
1196
- ) : /* @__PURE__ */ jsx9(
1197
- Editor,
1198
- {
1199
- value: content,
1200
- onChange: internalSetContent,
1201
- onScrollReady: handleScrollReady,
1202
- onViewReady: handleViewReady,
1203
- onEditContainer: handleEditContainer,
1204
- uploadConfig,
1205
- onMessageNoUploadConfig: () => message.warning("\u8BF7\u5148\u914D\u7F6E\u56FE\u7247\u4E0A\u4F20\u529F\u80FD")
1206
- }
1207
- ),
1208
- /* @__PURE__ */ jsx9(
1209
- EditDialog,
1210
- {
1211
- visible: dialogVisible,
1212
- typeName: dialogTypeName,
1213
- currentProps: dialogProps,
1214
- onSave: handleDialogSave,
1215
- onClose: handleDialogClose
1216
- }
1217
- )
1218
- ] });
1195
+ //#endregion
1196
+ //#region src/MarkdownEditor.tsx
1197
+ var MarkdownEditor_default = forwardRef(function MarkdownEditor({ defaultValue = "", value, onChange, showPreview = true, fullscreen: fullscreenProp = false, previewOptions, uploadConfig }, ref) {
1198
+ const [content, setContent] = useState(value ?? defaultValue);
1199
+ const [editorView, setEditorView] = useState(null);
1200
+ const scrollDOMRef = useRef(null);
1201
+ const previewElRef = useRef(null);
1202
+ const { fullscreen, toggleFullscreen } = useFullscreen();
1203
+ useScrollSync(scrollDOMRef, previewElRef);
1204
+ const [dialogVisible, setDialogVisible] = useState(false);
1205
+ const [dialogTypeName, setDialogTypeName] = useState("");
1206
+ const [dialogProps, setDialogProps] = useState({});
1207
+ const [dialogPropsRange, setDialogPropsRange] = useState([0, 0]);
1208
+ useEffect(() => {
1209
+ if (value !== void 0) setContent(value);
1210
+ }, [value]);
1211
+ const internalSetContent = useCallback((newContent) => {
1212
+ setContent(newContent);
1213
+ onChange?.(newContent);
1214
+ }, [onChange]);
1215
+ useImperativeHandle(ref, () => ({ getContent: () => content }), [content]);
1216
+ const extraToolbarItems = componentRegistry.getAll().map((schema) => {
1217
+ const defaultProps = {};
1218
+ for (const field of schema?.fields ?? []) defaultProps[field.key] = field.defaultValue ?? "";
1219
+ const iconNode = schema.icon ?? /* @__PURE__ */ jsx(ReadOutlined, {});
1220
+ return {
1221
+ id: `container-${schema.name}`,
1222
+ label: schema.label,
1223
+ icon: () => iconNode,
1224
+ handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1225
+ };
1226
+ });
1227
+ const handleScrollReady = useCallback((scrollDOM) => {
1228
+ scrollDOMRef.current = scrollDOM;
1229
+ }, []);
1230
+ const handlePreviewRef = useCallback((node) => {
1231
+ previewElRef.current = node;
1232
+ }, []);
1233
+ const handleViewReady = useCallback((view) => {
1234
+ setEditorView(view);
1235
+ }, []);
1236
+ const handleEditContainer = useCallback((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1237
+ setDialogTypeName(typeName);
1238
+ setDialogProps({ ...currentProps });
1239
+ setDialogPropsRange([propsLineFrom, propsLineTo]);
1240
+ setDialogVisible(true);
1241
+ }, []);
1242
+ const handleDialogSave = useCallback((newProps) => {
1243
+ setDialogVisible(false);
1244
+ if (editorView) updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1245
+ }, [editorView, dialogPropsRange]);
1246
+ const handleDialogClose = useCallback(() => {
1247
+ setDialogVisible(false);
1248
+ }, []);
1249
+ return /* @__PURE__ */ jsxs("div", {
1250
+ className: fullscreen ? "app fullscreen" : "app",
1251
+ children: [
1252
+ /* @__PURE__ */ jsx(Toolbar, {
1253
+ editorView,
1254
+ fullscreen,
1255
+ onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1256
+ extraItems: extraToolbarItems,
1257
+ uploadConfig
1258
+ }),
1259
+ showPreview ? /* @__PURE__ */ jsx(SplitPanels, {
1260
+ leftPanel: /* @__PURE__ */ jsx(Editor, {
1261
+ value: content,
1262
+ onChange: internalSetContent,
1263
+ onScrollReady: handleScrollReady,
1264
+ onViewReady: handleViewReady,
1265
+ onEditContainer: handleEditContainer,
1266
+ uploadConfig,
1267
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1268
+ }),
1269
+ rightPanel: /* @__PURE__ */ jsx(Preview, {
1270
+ content,
1271
+ ref: handlePreviewRef,
1272
+ previewOptions
1273
+ })
1274
+ }) : /* @__PURE__ */ jsx(Editor, {
1275
+ value: content,
1276
+ onChange: internalSetContent,
1277
+ onScrollReady: handleScrollReady,
1278
+ onViewReady: handleViewReady,
1279
+ onEditContainer: handleEditContainer,
1280
+ uploadConfig,
1281
+ onMessageNoUploadConfig: () => message.warning("请先配置图片上传功能")
1282
+ }),
1283
+ /* @__PURE__ */ jsx(EditDialog, {
1284
+ visible: dialogVisible,
1285
+ typeName: dialogTypeName,
1286
+ currentProps: dialogProps,
1287
+ onSave: handleDialogSave,
1288
+ onClose: handleDialogClose
1289
+ })
1290
+ ]
1291
+ });
1219
1292
  });
1220
-
1221
- // src/hooks/useRegisterToolbar.ts
1222
- import { useEffect as useEffect7 } from "react";
1293
+ //#endregion
1294
+ //#region src/hooks/useRegisterToolbar.ts
1295
+ /**
1296
+ * 注册自定义容器组件到工具栏。支持单个或批量注册。
1297
+ * 支持两种模式:
1298
+ *
1299
+ * 1. 容器模式(linkCard):name + fields + renderDialog
1300
+ * 插入 :::name 格式的容器,支持弹窗编辑属性
1301
+ *
1302
+ * 2. 标签包裹模式:name + wrapTags
1303
+ * 插入简单的标签包裹文本,如 <plugin-container>...</plugin-container>
1304
+ *
1305
+ * @example 容器模式
1306
+ * ```tsx
1307
+ * const schema: ComponentSchema = {
1308
+ * name: 'alert',
1309
+ * label: '警告框',
1310
+ * icon: <AlertOutlined />,
1311
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1312
+ * renderDialog: (props, onChange) => (
1313
+ * <Input value={props.level} onChange={e => onChange('level', e.target.value)} />
1314
+ * ),
1315
+ * }
1316
+ *
1317
+ * function MyPage() {
1318
+ * useRegisterToolbar(schema)
1319
+ * return <MarkdownEditor />
1320
+ * }
1321
+ * ```
1322
+ *
1323
+ * @example 标签包裹模式
1324
+ * ```tsx
1325
+ * function MyPage() {
1326
+ * useRegisterToolbar({
1327
+ * name: 'plugin-container',
1328
+ * label: '自定义容器',
1329
+ * icon: <AlertOutlined />,
1330
+ * wrapTags: ['<plugin-container>', '</plugin-container>'],
1331
+ * })
1332
+ * return <MarkdownEditor />
1333
+ * }
1334
+ * ```
1335
+ *
1336
+ * @example 批量注册
1337
+ * ```tsx
1338
+ * function MyPage() {
1339
+ * useRegisterToolbar([schema1, schema2, schema3])
1340
+ * return <MarkdownEditor />
1341
+ * }
1342
+ * ```
1343
+ */
1223
1344
  function useRegisterToolbar(schemas) {
1224
- const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1225
- useEffect7(() => {
1226
- schemaList.forEach((schema) => {
1227
- componentRegistry_default.register(schema.name, schema);
1228
- });
1229
- return () => {
1230
- schemaList.forEach((schema) => {
1231
- componentRegistry_default.unregister(schema.name);
1232
- });
1233
- };
1234
- }, [schemaList]);
1345
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1346
+ useEffect(() => {
1347
+ schemaList.forEach((schema) => {
1348
+ componentRegistry.register(schema.name, schema);
1349
+ });
1350
+ return () => {
1351
+ schemaList.forEach((schema) => {
1352
+ componentRegistry.unregister(schema.name);
1353
+ });
1354
+ };
1355
+ }, [schemaList]);
1235
1356
  }
1236
- export {
1237
- MarkdownEditor_default as MarkdownEditor,
1238
- useRegisterToolbar
1239
- };
1357
+ //#endregion
1358
+ export { MarkdownEditor_default as MarkdownEditor, useRegisterToolbar };
1359
+
1240
1360
  //# sourceMappingURL=index.js.map