remons-markdown-editor 1.0.0-beta.1 → 1.0.0-beta.11

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