remons-markdown-editor 1.0.0-beta.3 → 1.0.0-beta.5

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 +13 -0
  12. package/dist/components/Toolbar/index.d.ts +13 -0
  13. package/dist/components/Toolbar/renderers/TablePicker/TableDropdown.d.ts +5 -0
  14. package/dist/components/Toolbar/renderers/TablePicker/index.d.ts +9 -0
  15. package/dist/components/Toolbar/renderers/ToolbarItemRenderer.d.ts +7 -0
  16. package/dist/hooks/useFullscreen.d.ts +4 -0
  17. package/dist/hooks/useRegisterToolbar.d.ts +51 -0
  18. package/dist/hooks/useScrollSync.d.ts +5 -0
  19. package/dist/index.cjs +1325 -1207
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.css +108 -88
  22. package/dist/index.d.ts +5 -105
  23. package/dist/index.js +1301 -1187
  24. package/dist/index.js.map +1 -1
  25. package/dist/main.d.ts +0 -0
  26. package/dist/registry/componentRegistry.d.ts +34 -0
  27. package/dist/types/index.d.ts +76 -0
  28. package/dist/utils/parseContainer.d.ts +18 -0
  29. package/dist/utils/writeContainer.d.ts +16 -0
  30. package/package.json +7 -7
  31. package/readme.md +103 -4
  32. package/dist/index.css.map +0 -1
  33. package/dist/index.d.cts +0 -105
package/dist/index.cjs CHANGED
@@ -1,1259 +1,1377 @@
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;
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;
347
260
  }
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" });
487
- }
488
-
489
- // src/components/Preview/index.tsx
490
- var import_react2 = require("react");
491
- var import_remons_render_markdown = __toESM(require("remons-render-markdown"), 1);
492
- var import_dist = require("remons-render-markdown/dist/index.css");
493
- var import_jsx_runtime3 = require("react/jsx-runtime");
494
- var Preview = (0, import_react2.forwardRef)(({ content, previewOptions }, ref) => {
495
- (0, import_react2.useEffect)(() => {
496
- (0, import_remons_render_markdown.initHighlighter)({
497
- ...import_remons_render_markdown.languagesCommon,
498
- ...(previewOptions == null ? void 0 : previewOptions.languages) ? previewOptions == null ? void 0 : previewOptions.languages : {}
499
- });
500
- }, [previewOptions == null ? void 0 : previewOptions.languages]);
501
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref, className: "preview", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_remons_render_markdown.default, { content, ...previewOptions }) });
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
+ } }
502
294
  });
503
- Preview.displayName = "Preview";
504
- var Preview_default = Preview;
505
-
506
- // src/components/Panel/SplitPanels.tsx
507
- var import_antd2 = require("antd");
508
- var import_jsx_runtime4 = require("react/jsx-runtime");
509
- function SplitPanels({ leftPanel, rightPanel }) {
510
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_antd2.Splitter, { className: "split-panels", children: [
511
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_antd2.Splitter.Panel, { defaultSize: "50%", min: "20%", max: "80%", children: leftPanel }),
512
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_antd2.Splitter.Panel, { children: rightPanel })
513
- ] });
514
- }
515
-
516
- // src/components/Toolbar/index.tsx
517
- var import_react6 = require("react");
518
- var import_antd5 = require("antd");
519
- var import_icons4 = require("@ant-design/icons");
520
-
521
- // src/components/Toolbar/hooks/useToolbar.ts
522
- var import_react3 = require("react");
523
-
524
- // src/components/Toolbar/config.ts
525
- var import_icons = require("@ant-design/icons");
526
-
527
- // src/components/Toolbar/buttons.tsx
295
+ //#endregion
296
+ //#region src/components/Toolbar/buttons.tsx
297
+ /**
298
+ * 在光标位置插入文本
299
+ */
528
300
  function insertAtCursor(view, before, after = "", placeholder = "") {
529
- const { from, to } = view.state.selection.main;
530
- const selectedText = view.state.doc.sliceString(from, to);
531
- const insert = selectedText ? `${before}${selectedText}${after}` : `${before}${placeholder}${after}`;
532
- view.dispatch({ changes: { from, to, insert } });
533
- if (!selectedText && placeholder) {
534
- view.dispatch({ selection: { anchor: from + before.length } });
535
- }
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 } });
536
310
  }
311
+ /**
312
+ * 在当前行行首插入文本
313
+ */
537
314
  function insertAtLineStart(view, text) {
538
- const { from } = view.state.selection.main;
539
- const line = view.state.doc.lineAt(from);
540
- view.dispatch({ changes: { from: line.from, to: line.from, insert: text } });
541
- 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 } });
542
323
  }
543
- async function uploadFileToServer(file) {
544
- var _a;
545
- const formData = new FormData();
546
- formData.append("file", file);
547
- const response = await fetch("https://remons.cn:3008/content/uploadMarkdownImg", {
548
- method: "POST",
549
- body: formData
550
- });
551
- if (!response.ok) return null;
552
- const result = await response.json();
553
- if (result.code === 200 && result.success && ((_a = result.data) == null ? void 0 : _a.path)) {
554
- return `https://remons.cn:3008${result.data.path}`;
555
- }
556
- return null;
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;
557
344
  }
558
- function uploadImage(view) {
559
- const input = document.createElement("input");
560
- input.type = "file";
561
- input.accept = "image/*";
562
- input.style.display = "none";
563
- input.addEventListener("change", async () => {
564
- if (!input.files || input.files.length === 0) return;
565
- const file = input.files[0];
566
- const { from, to } = view.state.selection.main;
567
- const selectedText = view.state.doc.sliceString(from, to) || "\u63CF\u8FF0";
568
- const url = await uploadFileToServer(file);
569
- if (url) {
570
- const insert = `![${selectedText}](${url})`;
571
- view.dispatch({ changes: { from, to, insert } });
572
- }
573
- input.remove();
574
- });
575
- document.body.appendChild(input);
576
- input.click();
345
+ /**
346
+ * 判断是否配置了图片上传
347
+ */
348
+ function isUploadConfigured(uploadConfig) {
349
+ return !!(uploadConfig?.uploadUrl || uploadConfig?.onUploadImage);
577
350
  }
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();
379
+ }
380
+ /**
381
+ * 用前缀和后缀包裹选中文本
382
+ */
578
383
  function wrapSelection(view, prefix, suffix) {
579
- const { from, to, empty } = view.state.selection.main;
580
- if (empty) return;
581
- const selected = view.state.doc.sliceString(from, to);
582
- 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
+ } });
583
392
  }
393
+ /**
394
+ * 切换包裹:选中的文本已包含标签则移除,否则添加
395
+ */
584
396
  function toggleWrap(view, openTag, closeTag) {
585
- const { from, to, empty } = view.state.selection.main;
586
- const doc = view.state.doc;
587
- if (!empty) {
588
- const selected = doc.sliceString(from, to);
589
- if (selected.startsWith(openTag) && selected.endsWith(closeTag)) {
590
- const inner = selected.slice(openTag.length, -closeTag.length);
591
- view.dispatch({ changes: { from, to, insert: inner } });
592
- return;
593
- }
594
- }
595
- {
596
- const cursorPos = empty ? from : to;
597
- const line = doc.lineAt(cursorPos);
598
- const lineText = doc.sliceString(line.from, line.to);
599
- const openIdx = lineText.indexOf(openTag);
600
- const closeIdx = lineText.lastIndexOf(closeTag);
601
- if (openIdx >= 0 && closeIdx > openIdx) {
602
- const absOpen = line.from + openIdx;
603
- const absClose = line.from + closeIdx;
604
- if (cursorPos >= absOpen + openTag.length && cursorPos <= absClose) {
605
- const inner = doc.sliceString(absOpen + openTag.length, absClose);
606
- view.dispatch({
607
- changes: {
608
- from: absOpen,
609
- to: absClose + closeTag.length,
610
- insert: inner
611
- }
612
- });
613
- return;
614
- }
615
- }
616
- }
617
- 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);
618
432
  }
619
-
620
- // src/components/Toolbar/config.ts
621
- function createToolbarConfig() {
622
- return [
623
- {
624
- id: "heading",
625
- icon: import_icons.FontSizeOutlined,
626
- label: "\u6807\u9898",
627
- children: [
628
- { id: "heading1", label: "\u6807\u9898 1", icon: import_icons.FontSizeOutlined, handler: (v) => insertAtLineStart(v, "# ") },
629
- { id: "heading2", label: "\u6807\u9898 2", icon: import_icons.FontSizeOutlined, handler: (v) => insertAtLineStart(v, "## ") },
630
- { id: "heading3", label: "\u6807\u9898 3", icon: import_icons.FontSizeOutlined, handler: (v) => insertAtLineStart(v, "### ") },
631
- { id: "heading4", label: "\u6807\u9898 4", icon: import_icons.FontSizeOutlined, handler: (v) => insertAtLineStart(v, "#### ") },
632
- { id: "heading5", label: "\u6807\u9898 5", icon: import_icons.FontSizeOutlined, handler: (v) => insertAtLineStart(v, "##### ") },
633
- { id: "heading6", label: "\u6807\u9898 6", icon: import_icons.FontSizeOutlined, handler: (v) => insertAtLineStart(v, "###### ") }
634
- ]
635
- },
636
- { separatorBefore: true },
637
- { id: "bold", label: "\u52A0\u7C97", icon: import_icons.BoldOutlined, handler: (v) => wrapSelection(v, "**", "**") },
638
- { id: "italic", label: "\u659C\u4F53", icon: import_icons.ItalicOutlined, handler: (v) => wrapSelection(v, "*", "*") },
639
- { id: "strikethrough", label: "\u5220\u9664\u7EBF", icon: import_icons.StrikethroughOutlined, handler: (v) => wrapSelection(v, "~~", "~~") },
640
- { id: "underline", label: "\u4E0B\u5212\u7EBF", icon: import_icons.UnderlineOutlined, handler: (v) => wrapSelection(v, "<u>", "</u>") },
641
- { separatorBefore: true },
642
- { id: "ul", label: "\u65E0\u5E8F\u5217\u8868", icon: import_icons.UnorderedListOutlined, handler: (v) => insertAtLineStart(v, "- ") },
643
- { id: "ol", label: "\u6709\u5E8F\u5217\u8868", icon: import_icons.OrderedListOutlined, handler: (v) => insertAtLineStart(v, "1. ") },
644
- { separatorBefore: true },
645
- { id: "link", label: "\u94FE\u63A5", icon: import_icons.LinkOutlined, handler: (v) => insertAtCursor(v, "[", "](url)", "\u6587\u672C") },
646
- { id: "image", label: "\u56FE\u7247", icon: import_icons.PictureOutlined, handler: (v) => uploadImage(v) },
647
- { separatorBefore: true },
648
- { id: "code", label: "\u884C\u5185\u4EE3\u7801", icon: import_icons.FieldStringOutlined, handler: (v) => wrapSelection(v, "`", "`") },
649
- {
650
- id: "codeblock",
651
- label: "\u4EE3\u7801\u5757",
652
- icon: import_icons.CodeOutlined,
653
- handler: (v) => {
654
- const { from, to } = v.state.selection.main;
655
- const selected = v.state.doc.sliceString(from, to);
656
- const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
657
- v.dispatch({ changes: { from, to, insert } });
658
- }
659
- },
660
- { id: "quote", label: "\u5F15\u7528", icon: import_icons.BlockOutlined, handler: (v) => insertAtLineStart(v, "> ") },
661
- { id: "textContainer", label: "\u6587\u5B57\u5BB9\u5668", icon: import_icons.TagsOutlined, handler: (v) => toggleWrap(v, "<text-container>", "</text-container>") },
662
- {
663
- id: "hr",
664
- label: "\u5206\u5272\u7EBF",
665
- icon: import_icons.MinusOutlined,
666
- handler: (v) => {
667
- const { from } = v.state.selection.main;
668
- v.dispatch({ changes: { from, insert: "\n---\n" } });
669
- }
670
- }
671
- ];
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
+ });
672
558
  }
673
-
674
- // src/components/Toolbar/hooks/useToolbar.ts
675
- function useToolbar(editorView, extraItems) {
676
- const config = (0, import_react3.useMemo)(() => {
677
- const base = createToolbarConfig();
678
- return extraItems ? [...base, { separatorBefore: true }, ...extraItems] : base;
679
- }, [extraItems]);
680
- const handleClick = (0, import_react3.useCallback)((button) => {
681
- if (button.handler && editorView) {
682
- editorView.focus();
683
- button.handler(editorView, editorView.state);
684
- }
685
- }, [editorView]);
686
- 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
+ function SplitPanels({ leftPanel, rightPanel }) {
581
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(antd.Splitter, {
582
+ className: "split-panels",
583
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Splitter.Panel, {
584
+ defaultSize: "50%",
585
+ min: "20%",
586
+ max: "80%",
587
+ children: leftPanel
588
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Splitter.Panel, { children: rightPanel })]
589
+ });
687
590
  }
688
-
689
- // src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
690
- var import_antd3 = require("antd");
691
- var import_icons2 = require("@ant-design/icons");
692
- var import_jsx_runtime5 = require("react/jsx-runtime");
591
+ //#endregion
592
+ //#region src/components/Toolbar/config.ts
593
+ /**
594
+ * 创建工具栏配置
595
+ * @param options - 配置选项
596
+ */
597
+ function createToolbarConfig(options) {
598
+ const hasUploadConfig = isUploadConfigured(options);
599
+ return [
600
+ {
601
+ id: "heading",
602
+ icon: _ant_design_icons.FontSizeOutlined,
603
+ label: "标题",
604
+ children: [
605
+ {
606
+ id: "heading1",
607
+ label: "标题 1",
608
+ icon: _ant_design_icons.FontSizeOutlined,
609
+ handler: (v) => insertAtLineStart(v, "# ")
610
+ },
611
+ {
612
+ id: "heading2",
613
+ label: "标题 2",
614
+ icon: _ant_design_icons.FontSizeOutlined,
615
+ handler: (v) => insertAtLineStart(v, "## ")
616
+ },
617
+ {
618
+ id: "heading3",
619
+ label: "标题 3",
620
+ icon: _ant_design_icons.FontSizeOutlined,
621
+ handler: (v) => insertAtLineStart(v, "### ")
622
+ },
623
+ {
624
+ id: "heading4",
625
+ label: "标题 4",
626
+ icon: _ant_design_icons.FontSizeOutlined,
627
+ handler: (v) => insertAtLineStart(v, "#### ")
628
+ },
629
+ {
630
+ id: "heading5",
631
+ label: "标题 5",
632
+ icon: _ant_design_icons.FontSizeOutlined,
633
+ handler: (v) => insertAtLineStart(v, "##### ")
634
+ },
635
+ {
636
+ id: "heading6",
637
+ label: "标题 6",
638
+ icon: _ant_design_icons.FontSizeOutlined,
639
+ handler: (v) => insertAtLineStart(v, "###### ")
640
+ }
641
+ ]
642
+ },
643
+ { separatorBefore: true },
644
+ {
645
+ id: "bold",
646
+ label: "加粗",
647
+ icon: _ant_design_icons.BoldOutlined,
648
+ handler: (v) => wrapSelection(v, "**", "**")
649
+ },
650
+ {
651
+ id: "italic",
652
+ label: "斜体",
653
+ icon: _ant_design_icons.ItalicOutlined,
654
+ handler: (v) => wrapSelection(v, "*", "*")
655
+ },
656
+ {
657
+ id: "strikethrough",
658
+ label: "删除线",
659
+ icon: _ant_design_icons.StrikethroughOutlined,
660
+ handler: (v) => wrapSelection(v, "~~", "~~")
661
+ },
662
+ {
663
+ id: "underline",
664
+ label: "下划线",
665
+ icon: _ant_design_icons.UnderlineOutlined,
666
+ handler: (v) => wrapSelection(v, "<u>", "</u>")
667
+ },
668
+ { separatorBefore: true },
669
+ {
670
+ id: "ul",
671
+ label: "无序列表",
672
+ icon: _ant_design_icons.UnorderedListOutlined,
673
+ handler: (v) => insertAtLineStart(v, "- ")
674
+ },
675
+ {
676
+ id: "ol",
677
+ label: "有序列表",
678
+ icon: _ant_design_icons.OrderedListOutlined,
679
+ handler: (v) => insertAtLineStart(v, "1. ")
680
+ },
681
+ { separatorBefore: true },
682
+ {
683
+ id: "link",
684
+ label: "链接",
685
+ icon: _ant_design_icons.LinkOutlined,
686
+ handler: (v) => insertAtCursor(v, "[", "](url)", "文本")
687
+ },
688
+ {
689
+ id: "image",
690
+ label: hasUploadConfig ? "图片" : "请配置图片上传地址",
691
+ icon: _ant_design_icons.PictureOutlined,
692
+ disabled: !hasUploadConfig,
693
+ handler: hasUploadConfig ? (v) => uploadImage(v, {
694
+ uploadUrl: options?.uploadUrl,
695
+ onUploadImage: options?.onUploadImage
696
+ }) : void 0
697
+ },
698
+ { separatorBefore: true },
699
+ {
700
+ id: "code",
701
+ label: "行内代码",
702
+ icon: _ant_design_icons.FieldStringOutlined,
703
+ handler: (v) => wrapSelection(v, "`", "`")
704
+ },
705
+ {
706
+ id: "codeblock",
707
+ label: "代码块",
708
+ icon: _ant_design_icons.CodeOutlined,
709
+ handler: (v) => {
710
+ const { from, to } = v.state.selection.main;
711
+ const selected = v.state.doc.sliceString(from, to);
712
+ const insert = selected ? "```\n" + selected + "\n```" : "```\n\n```";
713
+ v.dispatch({ changes: {
714
+ from,
715
+ to,
716
+ insert
717
+ } });
718
+ }
719
+ },
720
+ {
721
+ id: "quote",
722
+ label: "引用",
723
+ icon: _ant_design_icons.BlockOutlined,
724
+ handler: (v) => insertAtLineStart(v, "> ")
725
+ },
726
+ {
727
+ id: "hr",
728
+ label: "分割线",
729
+ icon: _ant_design_icons.MinusOutlined,
730
+ handler: (v) => {
731
+ const { from } = v.state.selection.main;
732
+ v.dispatch({ changes: {
733
+ from,
734
+ insert: "\n---\n"
735
+ } });
736
+ }
737
+ }
738
+ ];
739
+ }
740
+ //#endregion
741
+ //#region src/components/Toolbar/hooks/useToolbar.ts
742
+ function useToolbar(editorView, options) {
743
+ const { extraItems, uploadConfig } = options || {};
744
+ return {
745
+ config: (0, react.useMemo)(() => {
746
+ const base = createToolbarConfig(uploadConfig);
747
+ return extraItems ? [...base, ...extraItems] : base;
748
+ }, [extraItems, uploadConfig]),
749
+ handleClick: (0, react.useCallback)((button) => {
750
+ if (!editorView) return;
751
+ if (button.wrapTags) {
752
+ const [openTag, closeTag] = button.wrapTags;
753
+ toggleWrap(editorView, openTag, closeTag);
754
+ return;
755
+ }
756
+ if (button.handler) {
757
+ editorView.focus();
758
+ button.handler(editorView, editorView.state);
759
+ }
760
+ }, [editorView])
761
+ };
762
+ }
763
+ //#endregion
764
+ //#region src/components/Toolbar/renderers/ToolbarItemRenderer.tsx
693
765
  function ToolbarItemRenderer({ item, onClick }) {
694
- const Icon = item.icon;
695
- if (item.children && item.children.length > 0) {
696
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
697
- item.separatorBefore && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "toolbar-separator" }),
698
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "toolbar-group", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
699
- import_antd3.Dropdown,
700
- {
701
- menu: {
702
- items: item.children.map((child) => {
703
- var _a;
704
- return {
705
- type: "item",
706
- key: (_a = child.id) != null ? _a : "",
707
- label: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { onClick: () => onClick(child), children: [
708
- child.icon && (() => {
709
- const ChildIcon = child.icon;
710
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ChildIcon, {});
711
- })(),
712
- " ",
713
- child.label
714
- ] })
715
- };
716
- })
717
- },
718
- placement: "bottomLeft",
719
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_antd3.Button, { className: "toolbar-button", type: "text", size: "small", title: item.label, children: [
720
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, {}),
721
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_icons2.DownOutlined, { style: { marginLeft: 2, fontSize: 10 } })
722
- ] })
723
- }
724
- ) })
725
- ] });
726
- }
727
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
728
- item.separatorBefore && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "toolbar-separator" }),
729
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "toolbar-group", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
730
- import_antd3.Button,
731
- {
732
- id: item.id,
733
- className: "toolbar-button",
734
- type: "text",
735
- size: "small",
736
- title: item.label,
737
- onClick: () => onClick(item),
738
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, {})
739
- }
740
- ) })
741
- ] });
766
+ const Icon = item.icon;
767
+ 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", {
768
+ className: "toolbar-group",
769
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Tooltip, {
770
+ title: item.label,
771
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Dropdown, {
772
+ menu: { items: item.children.map((child) => ({
773
+ type: "item",
774
+ key: child.id ?? "",
775
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
776
+ onClick: () => onClick(child),
777
+ children: [
778
+ child.icon && (() => {
779
+ const ChildIcon = child.icon;
780
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChildIcon, {});
781
+ })(),
782
+ " ",
783
+ child.label
784
+ ]
785
+ })
786
+ })) },
787
+ placement: "bottomLeft",
788
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(antd.Button, {
789
+ className: "toolbar-button",
790
+ type: "text",
791
+ size: "small",
792
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_ant_design_icons.DownOutlined, { style: {
793
+ marginLeft: 2,
794
+ fontSize: 10
795
+ } })]
796
+ })
797
+ })
798
+ })
799
+ })] });
800
+ const isDisabled = item.disabled;
801
+ 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", {
802
+ className: "toolbar-group",
803
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Tooltip, {
804
+ title: item.label,
805
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Button, {
806
+ id: item.id,
807
+ className: "toolbar-button",
808
+ type: "text",
809
+ size: "small",
810
+ disabled: isDisabled,
811
+ onClick: () => !isDisabled && onClick(item),
812
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {})
813
+ })
814
+ })
815
+ })] });
742
816
  }
743
-
744
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
745
- var import_react5 = __toESM(require("react"), 1);
746
- var import_antd4 = require("antd");
747
- var import_icons3 = require("@ant-design/icons");
748
-
749
- // src/components/Toolbar/renderers/TablePicker/index.tsx
750
- var import_react4 = require("react");
751
-
752
- // src/components/Toolbar/renderers/TablePicker/index.module.less
753
- var index_module_default = {};
754
-
755
- // src/components/Toolbar/renderers/TablePicker/index.tsx
756
- var import_jsx_runtime6 = require("react/jsx-runtime");
757
- var MAX_ROW = 12;
758
- var MAX_COL = 12;
759
- var REFRESH_TIME = 200;
817
+ var index_module_default = {
818
+ container: "index-module__container___l6BT-",
819
+ grid: "index-module__grid___tCZ-r",
820
+ cell: "index-module__cell___vTDQz",
821
+ cellActive: "index-module__cellActive___0EYvU",
822
+ hint: "index-module__hint___dTObk",
823
+ label: "index-module__label___2CmK1"
824
+ };
825
+ //#endregion
826
+ //#region src/components/Toolbar/renderers/TablePicker/index.tsx
827
+ var MAX_ROW = 8;
828
+ var MAX_COL = 8;
760
829
  function TablePicker({ onSelect }) {
761
- const [hoverSize, setHoverSize] = (0, import_react4.useState)({ row: 0, col: 0 });
762
- const [prevHoverSize, setPrevHoverSize] = (0, import_react4.useState)({ row: 0, col: 0 });
763
- const [confirmedSize, setConfirmedSize] = (0, import_react4.useState)({ row: 0, col: 0 });
764
- const maxColRef = (0, import_react4.useRef)(0);
765
- const timerIdRef = (0, import_react4.useRef)(null);
766
- const refreshTimerRef = (0, import_react4.useRef)(null);
767
- const commitRef = (0, import_react4.useRef)(null);
768
- const clearTimer = (0, import_react4.useCallback)((flushFn) => {
769
- if (timerIdRef.current) {
770
- timerIdRef.current = null;
771
- }
772
- if (flushFn && maxColRef.current > 0) {
773
- timerIdRef.current = setTimeout(flushFn, REFRESH_TIME);
774
- }
775
- }, []);
776
- (0, import_react4.useEffect)(
777
- () => () => {
778
- if (timerIdRef.current) clearTimeout(timerIdRef.current);
779
- if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
780
- },
781
- []
782
- );
783
- function extend(col, row) {
784
- if (col > maxColRef.current) {
785
- maxColRef.current = col;
786
- setHoverSize({ row, col });
787
- setConfirmedSize({ row, col });
788
- } else {
789
- setHoverSize({ row, col });
790
- if (row !== confirmedSize.row) {
791
- setConfirmedSize({ row, col: maxColRef.current });
792
- }
793
- if (row === 1) {
794
- commitRef.current = { cols: col, rows: 1 };
795
- setConfirmedSize({ row: 1, col });
796
- }
797
- }
798
- }
799
- function flash(col, dir, limit, finalCallback) {
800
- let current = dir === 1 ? 1 : limit;
801
- const step = dir === 1 ? 1 : -1;
802
- let timer = 0;
803
- refreshTimerRef.current = setInterval(() => {
804
- setHoverSize({ row: current, col });
805
- if (current > confirmedSize.row || dir === -1) {
806
- maxColRef.current = Math.min(MAX_COL, col);
807
- }
808
- current += step;
809
- if (dir === 1 && current > limit || dir === -1 && current < 1) {
810
- clearInterval(refreshTimerRef.current);
811
- setHoverSize({ row: current - step, col });
812
- finalCallback(col, current - step);
813
- }
814
- }, 50);
815
- clearTimeout(timer);
816
- return () => {
817
- clearInterval(refreshTimerRef.current);
818
- };
819
- }
820
- function onCellEnterCheck(index) {
821
- clearTimer();
822
- const row = Math.floor(index / MAX_COL) + 1;
823
- const col = index % MAX_COL + 1;
824
- const isDown = row > hoverSize.row;
825
- if (Math.abs(row - hoverSize.row) <= 1) {
826
- extend(col, row);
827
- } else {
828
- if (isDown && (col > maxColRef.current || maxColRef.current >= MAX_COL)) {
829
- const finalRow = Math.min(row, MAX_ROW);
830
- flash(col, 1, finalRow, (c, r) => {
831
- setConfirmedSize({ row: r, col: c });
832
- });
833
- } else if (!isDown) {
834
- flash(col, -1, row, (c, r) => {
835
- setConfirmedSize({ row: r, col: c });
836
- if (r === 1) {
837
- commitRef.current = { cols: Math.min(MAX_COL, col), rows: 1 };
838
- setConfirmedSize({ row: 1, col: Math.min(MAX_COL, col) });
839
- }
840
- });
841
- }
842
- }
843
- if (col >= MAX_COL && row >= prevHoverSize.row) {
844
- if (row !== prevHoverSize.row) {
845
- maxColRef.current = 0;
846
- setPrevHoverSize({ row: 0, col: 0 });
847
- }
848
- }
849
- setPrevHoverSize({ row, col });
850
- }
851
- function onMouseDown() {
852
- const size = commitRef.current || { cols: maxColRef.current, rows: confirmedSize.row };
853
- if (size.rows > 0 && size.cols > 0) {
854
- onSelect(size);
855
- }
856
- }
857
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
858
- "div",
859
- {
860
- className: index_module_default.container,
861
- onMouseDown,
862
- children: [
863
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
864
- "div",
865
- {
866
- className: index_module_default.grid,
867
- style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
868
- children: Array.from({ length: MAX_ROW * MAX_COL }).map((_, i) => {
869
- const row = Math.floor(i / MAX_COL) + 1;
870
- const col = i % MAX_COL + 1;
871
- const isSelected = row <= hoverSize.row && col <= maxColRef.current;
872
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
873
- "div",
874
- {
875
- className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
876
- onMouseEnter: () => onCellEnterCheck(i)
877
- },
878
- i
879
- );
880
- })
881
- }
882
- ),
883
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: index_module_default.label, children: [
884
- hoverSize.row,
885
- " x ",
886
- maxColRef.current || 1
887
- ] })
888
- ]
889
- }
890
- );
830
+ const [hoverSize, setHoverSize] = (0, react.useState)({
831
+ row: 0,
832
+ col: 0
833
+ });
834
+ const [confirmedSize, setConfirmedSize] = (0, react.useState)({
835
+ row: 0,
836
+ col: 0
837
+ });
838
+ const maxColRef = (0, react.useRef)(0);
839
+ const prevRowRef = (0, react.useRef)(0);
840
+ const timerIdRef = (0, react.useRef)(null);
841
+ const refreshTimerRef = (0, react.useRef)(null);
842
+ const commitRef = (0, react.useRef)(null);
843
+ const clearTimer = (0, react.useCallback)(() => {
844
+ if (timerIdRef.current) {
845
+ clearTimeout(timerIdRef.current);
846
+ timerIdRef.current = null;
847
+ }
848
+ }, []);
849
+ const clearFlash = (0, react.useCallback)(() => {
850
+ if (refreshTimerRef.current) {
851
+ clearInterval(refreshTimerRef.current);
852
+ refreshTimerRef.current = null;
853
+ }
854
+ }, []);
855
+ (0, react.useEffect)(() => () => {
856
+ if (timerIdRef.current) clearTimeout(timerIdRef.current);
857
+ if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
858
+ }, []);
859
+ /**
860
+ * 扩展选中区域至 (col, row)
861
+ */
862
+ function extend(col, row) {
863
+ if (row < prevRowRef.current || col < maxColRef.current) maxColRef.current = 0;
864
+ if (col > maxColRef.current) maxColRef.current = col;
865
+ setHoverSize({
866
+ row,
867
+ col
868
+ });
869
+ setConfirmedSize({
870
+ row,
871
+ col: maxColRef.current
872
+ });
873
+ if (row === 1) commitRef.current = {
874
+ cols: maxColRef.current,
875
+ rows: 1
876
+ };
877
+ }
878
+ /**
879
+ * 跳跃动画效果
880
+ */
881
+ function flash(col, dir, limit, finalCallback) {
882
+ let current = dir === 1 ? 1 : limit;
883
+ const step = dir === 1 ? 1 : -1;
884
+ refreshTimerRef.current = setInterval(() => {
885
+ setHoverSize({
886
+ row: current,
887
+ col
888
+ });
889
+ if (current > confirmedSize.row || dir === -1) maxColRef.current = Math.min(MAX_COL, col);
890
+ current += step;
891
+ if (dir === 1 && current > limit || dir === -1 && current < 1) {
892
+ clearInterval(refreshTimerRef.current);
893
+ refreshTimerRef.current = null;
894
+ const finalRow = current - step;
895
+ setHoverSize({
896
+ row: finalRow,
897
+ col
898
+ });
899
+ finalCallback(col, finalRow);
900
+ }
901
+ }, 50);
902
+ }
903
+ /**
904
+ * 核心逻辑:鼠标进入格子
905
+ */
906
+ function onCellEnterCheck(index) {
907
+ clearTimer();
908
+ clearFlash();
909
+ const row = Math.floor(index / MAX_COL) + 1;
910
+ const col = index % MAX_COL + 1;
911
+ const prevRow = prevRowRef.current;
912
+ const isJump = Math.abs(row - prevRow) > 1;
913
+ const isDown = row > prevRow;
914
+ prevRowRef.current = row;
915
+ if (!isJump) extend(col, row);
916
+ else if (isDown) {
917
+ if (col > maxColRef.current || maxColRef.current >= MAX_COL) flash(col, 1, Math.min(row, MAX_ROW), (c, r) => {
918
+ setConfirmedSize({
919
+ row: r,
920
+ col: c
921
+ });
922
+ });
923
+ else extend(col, row);
924
+ } else {
925
+ maxColRef.current = 0;
926
+ flash(col, -1, row, (c, r) => {
927
+ setConfirmedSize({
928
+ row: r,
929
+ col: c
930
+ });
931
+ if (r === 1) {
932
+ commitRef.current = {
933
+ cols: Math.min(MAX_COL, col),
934
+ rows: 1
935
+ };
936
+ setConfirmedSize({
937
+ row: 1,
938
+ col: Math.min(MAX_COL, col)
939
+ });
940
+ }
941
+ });
942
+ }
943
+ }
944
+ function onMouseDown() {
945
+ const size = commitRef.current || {
946
+ cols: maxColRef.current,
947
+ rows: confirmedSize.row
948
+ };
949
+ if (size.rows > 0 && size.cols > 0) onSelect(size);
950
+ }
951
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
952
+ className: index_module_default.container,
953
+ onMouseDown,
954
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
955
+ className: index_module_default.grid,
956
+ style: { gridTemplateColumns: `repeat(${MAX_COL}, 24px)` },
957
+ children: Array.from({ length: 64 }).map((_, i) => {
958
+ const row = Math.floor(i / MAX_COL) + 1;
959
+ const col = i % MAX_COL + 1;
960
+ const isSelected = row <= hoverSize.row && col <= maxColRef.current;
961
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
962
+ className: `${index_module_default.cell} ${isSelected ? index_module_default.cellActive : ""}`,
963
+ onMouseEnter: () => onCellEnterCheck(i)
964
+ }, i);
965
+ })
966
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
967
+ className: index_module_default.label,
968
+ children: [
969
+ hoverSize.row,
970
+ " x ",
971
+ maxColRef.current || 1
972
+ ]
973
+ })]
974
+ });
891
975
  }
892
-
893
- // src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
894
- var import_jsx_runtime7 = require("react/jsx-runtime");
976
+ //#endregion
977
+ //#region src/components/Toolbar/renderers/TablePicker/TableDropdown.tsx
895
978
  function TableDropdown({ editorView }) {
896
- const [open, setOpen] = import_react5.default.useState(false);
897
- const openCountRef = (0, import_react5.useRef)(0);
898
- const handleTableSelect = (0, import_react5.useCallback)((rows, cols) => {
899
- if (!editorView || rows <= 0 || cols <= 0) return;
900
- editorView.focus();
901
- const headerCells = Array(cols).fill(null).map((_, i) => ` \u5217${i + 1} `).join("|");
902
- const separator = Array(cols).fill(" --- ").join("|");
903
- const dataCell = Array(cols).fill(" \u5185\u5BB9 ").join("|");
904
- const dataRows = Array(rows).fill(`|${dataCell}|`).join("\n");
905
- const table = `
906
- |${headerCells}|
907
- |${separator}|
908
- ${dataRows}
909
- `;
910
- const { from } = editorView.state.selection.main;
911
- editorView.dispatch({ changes: { from, insert: table } });
912
- }, [editorView]);
913
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
914
- import_antd4.Dropdown,
915
- {
916
- trigger: ["click"],
917
- open,
918
- onOpenChange: setOpen,
919
- popupRender: () => {
920
- openCountRef.current++;
921
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { padding: 4 }, onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
922
- TablePicker,
923
- {
924
- onSelect: (size) => {
925
- handleTableSelect(size.rows, size.cols);
926
- setOpen(false);
927
- }
928
- },
929
- openCountRef.current
930
- ) });
931
- },
932
- placement: "bottomLeft",
933
- 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, {}) })
934
- }
935
- );
979
+ const [open, setOpen] = react.default.useState(false);
980
+ const openCountRef = (0, react.useRef)(0);
981
+ const handleTableSelect = (0, react.useCallback)((rows, cols) => {
982
+ if (!editorView || rows <= 0 || cols <= 0) return;
983
+ editorView.focus();
984
+ const headerCells = Array(cols).fill(null).map((_, i) => ` 列${i + 1} `).join("|");
985
+ const separator = Array(cols).fill(" --- ").join("|");
986
+ const dataCell = Array(cols).fill(" 内容 ").join("|");
987
+ const table = `\n|${headerCells}|\n|${separator}|\n${Array(rows).fill(`|${dataCell}|`).join("\n")}\n`;
988
+ const { from } = editorView.state.selection.main;
989
+ editorView.dispatch({ changes: {
990
+ from,
991
+ insert: table
992
+ } });
993
+ }, [editorView]);
994
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Dropdown, {
995
+ trigger: ["click"],
996
+ open,
997
+ onOpenChange: setOpen,
998
+ popupRender: () => {
999
+ openCountRef.current++;
1000
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1001
+ style: { padding: 4 },
1002
+ onClick: (e) => e.stopPropagation(),
1003
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TablePicker, { onSelect: (size) => {
1004
+ handleTableSelect(size.rows, size.cols);
1005
+ setOpen(false);
1006
+ } }, openCountRef.current)
1007
+ });
1008
+ },
1009
+ placement: "bottomLeft",
1010
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Button, {
1011
+ className: "toolbar-button",
1012
+ type: "text",
1013
+ size: "small",
1014
+ title: "表格",
1015
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_ant_design_icons.TableOutlined, {})
1016
+ })
1017
+ });
936
1018
  }
937
-
938
- // src/components/Toolbar/index.tsx
939
- var import_jsx_runtime8 = require("react/jsx-runtime");
940
- function Toolbar({ editorView, fullscreen, onToggleFullscreen, extraItems }) {
941
- const { config, handleClick } = useToolbar(editorView, extraItems);
942
- const renderFullscreen = (0, import_react6.useCallback)(() => {
943
- if (!onToggleFullscreen) return null;
944
- const Icon = fullscreen ? import_icons4.FullscreenExitOutlined : import_icons4.FullscreenOutlined;
945
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
946
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "toolbar-separator" }),
947
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
948
- import_antd5.Button,
949
- {
950
- className: "toolbar-button",
951
- type: "text",
952
- size: "small",
953
- title: fullscreen ? "\u9000\u51FA\u5168\u5C4F" : "\u5168\u5C4F",
954
- onClick: () => {
955
- onToggleFullscreen();
956
- },
957
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, {})
958
- }
959
- )
960
- ] });
961
- }, [fullscreen, onToggleFullscreen]);
962
- function isSeparator(item) {
963
- return !!(!item.id && item.separatorBefore);
964
- }
965
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "toolbar", children: [
966
- config.map((item, idx) => {
967
- if (isSeparator(item)) {
968
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "toolbar-separator" }, `sep-${idx}`);
969
- }
970
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
971
- ToolbarItemRenderer,
972
- {
973
- item,
974
- onClick: handleClick
975
- },
976
- item.id
977
- );
978
- }),
979
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "toolbar-separator" }),
980
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TableDropdown, { editorView }),
981
- renderFullscreen()
982
- ] });
1019
+ //#endregion
1020
+ //#region src/components/Toolbar/index.tsx
1021
+ function Toolbar({ editorView, fullscreen = true, onToggleFullscreen, extraItems, uploadConfig }) {
1022
+ const { config, handleClick } = useToolbar(editorView, {
1023
+ extraItems,
1024
+ uploadConfig
1025
+ });
1026
+ const renderFullscreen = (0, react.useCallback)(() => {
1027
+ if (!onToggleFullscreen) return null;
1028
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Button, {
1029
+ className: "toolbar-button",
1030
+ type: "text",
1031
+ size: "small",
1032
+ title: fullscreen ? "退出全屏" : "全屏",
1033
+ onClick: () => {
1034
+ onToggleFullscreen();
1035
+ },
1036
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(fullscreen ? _ant_design_icons.FullscreenExitOutlined : _ant_design_icons.FullscreenOutlined, {})
1037
+ });
1038
+ }, [fullscreen, onToggleFullscreen]);
1039
+ function isSeparator(item) {
1040
+ return !!(!item.id && item.separatorBefore);
1041
+ }
1042
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1043
+ className: "toolbar",
1044
+ children: [
1045
+ config.map((item, idx) => {
1046
+ if (isSeparator(item)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "toolbar-separator" }, `sep-${idx}`);
1047
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarItemRenderer, {
1048
+ item,
1049
+ onClick: handleClick
1050
+ }, item.id);
1051
+ }),
1052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TableDropdown, { editorView }),
1053
+ renderFullscreen()
1054
+ ]
1055
+ });
983
1056
  }
984
-
985
- // src/components/Dialog/EditDialog.tsx
986
- var import_react7 = require("react");
987
- var import_antd6 = require("antd");
988
- var import_jsx_runtime9 = require("react/jsx-runtime");
1057
+ //#endregion
1058
+ //#region src/components/Dialog/EditDialog.tsx
1059
+ /**
1060
+ * 属性编辑弹窗组件
1061
+ * 根据组件 schema 的 renderDialog 函数渲染弹窗内容
1062
+ */
989
1063
  function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
990
- const schema = componentRegistry_default.get(typeName);
991
- const [formProps, setFormProps] = (0, import_react7.useState)({});
992
- const mergedProps = (0, import_react7.useMemo)(() => {
993
- var _a;
994
- if (!schema) return currentProps;
995
- const defaults = {};
996
- for (const field of schema.fields) {
997
- defaults[field.key] = (_a = field.defaultValue) != null ? _a : "";
998
- }
999
- return { ...defaults, ...currentProps };
1000
- }, [schema, currentProps]);
1001
- const handleDialogChange = (key, value) => {
1002
- setFormProps((prev) => ({ ...prev, [key]: value }));
1003
- };
1004
- const handleOk = () => {
1005
- const finalProps = visible ? { ...mergedProps, ...formProps } : mergedProps;
1006
- onSave(finalProps);
1007
- setFormProps({});
1008
- };
1009
- const handleClose = () => {
1010
- setFormProps({});
1011
- onClose();
1012
- };
1013
- if (!schema) {
1014
- return null;
1015
- }
1016
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1017
- import_antd6.Modal,
1018
- {
1019
- title: `${schema.label} \u5C5E\u6027\u7F16\u8F91`,
1020
- open: visible,
1021
- onOk: handleOk,
1022
- onCancel: handleClose,
1023
- okText: "\u4FDD\u5B58",
1024
- cancelText: "\u53D6\u6D88",
1025
- children: schema.renderDialog(visible ? { ...mergedProps, ...formProps } : mergedProps, handleDialogChange)
1026
- }
1027
- );
1064
+ const schema = componentRegistry.get(typeName);
1065
+ const [formProps, setFormProps] = (0, react.useState)({});
1066
+ const mergedProps = (0, react.useMemo)(() => {
1067
+ if (!schema) return currentProps;
1068
+ const defaults = {};
1069
+ for (const field of schema.fields || []) defaults[field.key] = field.defaultValue ?? "";
1070
+ return {
1071
+ ...defaults,
1072
+ ...currentProps
1073
+ };
1074
+ }, [schema, currentProps]);
1075
+ const handleDialogChange = (key, value) => {
1076
+ setFormProps((prev) => ({
1077
+ ...prev,
1078
+ [key]: value
1079
+ }));
1080
+ };
1081
+ const handleOk = () => {
1082
+ onSave(visible ? {
1083
+ ...mergedProps,
1084
+ ...formProps
1085
+ } : mergedProps);
1086
+ setFormProps({});
1087
+ };
1088
+ const handleClose = () => {
1089
+ setFormProps({});
1090
+ onClose();
1091
+ };
1092
+ if (!schema || !schema.renderDialog) return null;
1093
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(antd.Modal, {
1094
+ title: `${schema.label} 属性编辑`,
1095
+ open: visible,
1096
+ onOk: handleOk,
1097
+ onCancel: handleClose,
1098
+ okText: "保存",
1099
+ cancelText: "取消",
1100
+ children: schema.renderDialog(visible ? {
1101
+ ...mergedProps,
1102
+ ...formProps
1103
+ } : mergedProps, handleDialogChange)
1104
+ });
1028
1105
  }
1029
-
1030
- // src/utils/writeContainer.ts
1106
+ //#endregion
1107
+ //#region src/utils/writeContainer.ts
1108
+ /**
1109
+ * 从属性对象生成 JSON 属性行。
1110
+ * 格式: { key: 'value', key2: 'value2' }
1111
+ */
1031
1112
  function writeContainerProps(props) {
1032
- const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1033
- return entries ? `{ ${entries} }` : "{}";
1113
+ const entries = Object.entries(props).map(([k, v]) => `${k}: '${v}'`).join(", ");
1114
+ return entries ? `{ ${entries} }` : "{}";
1034
1115
  }
1116
+ /**
1117
+ * 更新编辑器文档中的容器属性。
1118
+ * 替换 JSON 属性行(::: 打开行之后的一行)为新属性。
1119
+ */
1035
1120
  function updateContainerProps(view, propsLineFrom, propsLineTo, newProps) {
1036
- const newPropsLine = writeContainerProps(newProps);
1037
- view.dispatch({
1038
- changes: {
1039
- from: propsLineFrom,
1040
- to: propsLineTo,
1041
- insert: newPropsLine
1042
- }
1043
- });
1121
+ const newPropsLine = writeContainerProps(newProps);
1122
+ view.dispatch({ changes: {
1123
+ from: propsLineFrom,
1124
+ to: propsLineTo,
1125
+ insert: newPropsLine
1126
+ } });
1044
1127
  }
1128
+ /**
1129
+ * 在当前光标位置插入新容器。
1130
+ * 创建 :::type 打开行 + JSON 属性 + ::: 关闭行的结构。
1131
+ */
1045
1132
  function insertContainer(view, typeName, defaultProps) {
1046
- const { from } = view.state.selection.main;
1047
- const propsLine = writeContainerProps(defaultProps);
1048
- const container = `:::${typeName}
1049
- ${propsLine}
1050
- :::
1051
- `;
1052
- view.dispatch({ changes: { from, insert: container } });
1133
+ const { from } = view.state.selection.main;
1134
+ const container = `:::${typeName}\n${writeContainerProps(defaultProps)}\n:::\n`;
1135
+ view.dispatch({ changes: {
1136
+ from,
1137
+ insert: container
1138
+ } });
1053
1139
  }
1054
-
1055
- // src/MarkdownEditor.tsx
1056
- var import_icons5 = require("@ant-design/icons");
1057
- var import_jsx_runtime10 = require("react/jsx-runtime");
1058
- var MarkdownEditor_default = (0, import_react8.forwardRef)(function MarkdownEditor({
1059
- defaultValue = "",
1060
- value,
1061
- onChange,
1062
- showPreview = true,
1063
- fullscreen: fullscreenProp = false,
1064
- previewOptions
1065
- }, ref) {
1066
- const [content, setContent] = (0, import_react8.useState)(value != null ? value : defaultValue);
1067
- const [editorView, setEditorView] = (0, import_react8.useState)(null);
1068
- const scrollDOMRef = (0, import_react8.useRef)(null);
1069
- const previewElRef = (0, import_react8.useRef)(null);
1070
- const [fullscreen, setFullscreen] = (0, import_react8.useState)(false);
1071
- const [dialogVisible, setDialogVisible] = (0, import_react8.useState)(false);
1072
- const [dialogTypeName, setDialogTypeName] = (0, import_react8.useState)("");
1073
- const [dialogProps, setDialogProps] = (0, import_react8.useState)({});
1074
- const [dialogPropsRange, setDialogPropsRange] = (0, import_react8.useState)([0, 0]);
1075
- (0, import_react8.useEffect)(() => {
1076
- if (value !== void 0) {
1077
- setContent(value);
1078
- }
1079
- }, [value]);
1080
- const internalSetContent = (0, import_react8.useCallback)((newContent) => {
1081
- setContent(newContent);
1082
- onChange == null ? void 0 : onChange(newContent);
1083
- }, [onChange]);
1084
- (0, import_react8.useImperativeHandle)(ref, () => ({
1085
- getContent: () => content
1086
- }), [content]);
1087
- const toggleFullscreen = (0, import_react8.useCallback)(() => {
1088
- if (document.fullscreenElement) {
1089
- document.exitFullscreen().catch((e) => {
1090
- console.error("\u9000\u51FA\u5168\u5C4F\u5931\u8D25:", e);
1091
- });
1092
- } else {
1093
- const el = document.documentElement;
1094
- const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1095
- if (requestMethod) {
1096
- requestMethod.call(el).catch((e) => {
1097
- console.error("\u8FDB\u5165\u5168\u5C4F\u5931\u8D25:", e);
1098
- });
1099
- }
1100
- }
1101
- }, []);
1102
- (0, import_react8.useEffect)(() => {
1103
- const handleFullscreenChange = () => {
1104
- setFullscreen(!!document.fullscreenElement);
1105
- };
1106
- document.addEventListener("fullscreenchange", handleFullscreenChange);
1107
- document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1108
- return () => {
1109
- document.removeEventListener("fullscreenchange", handleFullscreenChange);
1110
- document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1111
- };
1112
- }, []);
1113
- const extraToolbarItems = componentRegistry_default.getAll().map((schema) => {
1114
- var _a, _b;
1115
- const defaultProps = {};
1116
- for (const field of schema.fields) {
1117
- defaultProps[field.key] = (_a = field.defaultValue) != null ? _a : "";
1118
- }
1119
- const iconNode = (_b = schema.icon) != null ? _b : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_icons5.ReadOutlined, {});
1120
- return {
1121
- id: `container-${schema.name}`,
1122
- label: schema.label,
1123
- icon: () => iconNode,
1124
- handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1125
- };
1126
- });
1127
- const handleScrollReady = (0, import_react8.useCallback)((scrollDOM) => {
1128
- scrollDOMRef.current = scrollDOM;
1129
- }, []);
1130
- (0, import_react8.useEffect)(() => {
1131
- const source = scrollDOMRef.current;
1132
- const target = previewElRef.current;
1133
- if (!source || !target) return;
1134
- let rafId = null;
1135
- const listener = () => {
1136
- if (rafId) return;
1137
- rafId = requestAnimationFrame(() => {
1138
- const s = scrollDOMRef.current;
1139
- const t = previewElRef.current;
1140
- if (!s || !t) {
1141
- rafId = null;
1142
- return;
1143
- }
1144
- const sourceScrollHeight = s.scrollHeight - s.clientHeight;
1145
- if (sourceScrollHeight <= 0) {
1146
- rafId = null;
1147
- return;
1148
- }
1149
- const ratio = s.scrollTop / sourceScrollHeight;
1150
- const targetScrollHeight = t.scrollHeight - t.clientHeight;
1151
- if (targetScrollHeight > 0) {
1152
- t.scrollTop = ratio * targetScrollHeight;
1153
- }
1154
- rafId = null;
1155
- });
1156
- };
1157
- source.addEventListener("scroll", listener, { passive: true });
1158
- return () => {
1159
- source.removeEventListener("scroll", listener);
1160
- if (rafId) cancelAnimationFrame(rafId);
1161
- };
1162
- }, [previewElRef.current]);
1163
- const handlePreviewRef = (0, import_react8.useCallback)((node) => {
1164
- previewElRef.current = node;
1165
- }, []);
1166
- const handleViewReady = (0, import_react8.useCallback)((view) => {
1167
- setEditorView(view);
1168
- }, []);
1169
- const handleEditContainer = (0, import_react8.useCallback)((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1170
- setDialogTypeName(typeName);
1171
- setDialogProps({ ...currentProps });
1172
- setDialogPropsRange([propsLineFrom, propsLineTo]);
1173
- setDialogVisible(true);
1174
- }, []);
1175
- const handleDialogSave = (0, import_react8.useCallback)((newProps) => {
1176
- setDialogVisible(false);
1177
- if (editorView) {
1178
- updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1179
- }
1180
- }, [editorView, dialogPropsRange]);
1181
- const handleDialogClose = (0, import_react8.useCallback)(() => {
1182
- setDialogVisible(false);
1183
- }, []);
1184
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: fullscreen ? "app fullscreen" : "app", children: [
1185
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1186
- Toolbar,
1187
- {
1188
- editorView,
1189
- fullscreen,
1190
- onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1191
- extraItems: extraToolbarItems
1192
- }
1193
- ),
1194
- showPreview ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1195
- SplitPanels,
1196
- {
1197
- leftPanel: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1198
- Editor,
1199
- {
1200
- value: content,
1201
- onChange: internalSetContent,
1202
- onScrollReady: handleScrollReady,
1203
- onViewReady: handleViewReady,
1204
- onEditContainer: handleEditContainer
1205
- }
1206
- ),
1207
- rightPanel: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1208
- Preview_default,
1209
- {
1210
- content,
1211
- ref: handlePreviewRef,
1212
- previewOptions
1213
- }
1214
- )
1215
- }
1216
- ) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1217
- Editor,
1218
- {
1219
- value: content,
1220
- onChange: internalSetContent,
1221
- onScrollReady: handleScrollReady,
1222
- onViewReady: handleViewReady,
1223
- onEditContainer: handleEditContainer
1224
- }
1225
- ),
1226
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1227
- EditDialog,
1228
- {
1229
- visible: dialogVisible,
1230
- typeName: dialogTypeName,
1231
- currentProps: dialogProps,
1232
- onSave: handleDialogSave,
1233
- onClose: handleDialogClose
1234
- }
1235
- )
1236
- ] });
1140
+ //#endregion
1141
+ //#region src/hooks/useFullscreen.ts
1142
+ function useFullscreen() {
1143
+ const [fullscreen, setFullscreen] = (0, react.useState)(false);
1144
+ const toggleFullscreen = (0, react.useCallback)(() => {
1145
+ if (document.fullscreenElement) document.exitFullscreen().catch((e) => {
1146
+ console.error("退出全屏失败:", e);
1147
+ });
1148
+ else {
1149
+ const el = document.documentElement;
1150
+ const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
1151
+ if (requestMethod) requestMethod.call(el).catch((e) => {
1152
+ console.error("进入全屏失败:", e);
1153
+ });
1154
+ }
1155
+ }, []);
1156
+ (0, react.useEffect)(() => {
1157
+ const handleFullscreenChange = () => {
1158
+ setFullscreen(!!document.fullscreenElement);
1159
+ };
1160
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
1161
+ document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
1162
+ return () => {
1163
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
1164
+ document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
1165
+ };
1166
+ }, []);
1167
+ return {
1168
+ fullscreen,
1169
+ toggleFullscreen
1170
+ };
1171
+ }
1172
+ //#endregion
1173
+ //#region src/hooks/useScrollSync.ts
1174
+ /**
1175
+ * 滚动同步 Hook
1176
+ * 将源滚动容器的滚动位置同步到目标预览容器
1177
+ */
1178
+ function useScrollSync(sourceRef, targetRef) {
1179
+ const rafIdRef = (0, react.useRef)(null);
1180
+ (0, react.useEffect)(() => {
1181
+ const source = sourceRef.current;
1182
+ const target = targetRef.current;
1183
+ if (!source || !target) return;
1184
+ const listener = () => {
1185
+ if (rafIdRef.current) return;
1186
+ rafIdRef.current = requestAnimationFrame(() => {
1187
+ const s = sourceRef.current;
1188
+ const t = targetRef.current;
1189
+ if (!s || !t) {
1190
+ rafIdRef.current = null;
1191
+ return;
1192
+ }
1193
+ const sourceScrollHeight = s.scrollHeight - s.clientHeight;
1194
+ if (sourceScrollHeight <= 0) {
1195
+ rafIdRef.current = null;
1196
+ return;
1197
+ }
1198
+ const ratio = s.scrollTop / sourceScrollHeight;
1199
+ const targetScrollHeight = t.scrollHeight - t.clientHeight;
1200
+ if (targetScrollHeight > 0) t.scrollTop = ratio * targetScrollHeight;
1201
+ rafIdRef.current = null;
1202
+ });
1203
+ };
1204
+ source.addEventListener("scroll", listener, { passive: true });
1205
+ return () => {
1206
+ source.removeEventListener("scroll", listener);
1207
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
1208
+ };
1209
+ }, [sourceRef, targetRef]);
1210
+ }
1211
+ //#endregion
1212
+ //#region src/MarkdownEditor.tsx
1213
+ var MarkdownEditor_default = (0, react.forwardRef)(function MarkdownEditor({ defaultValue = "", value, onChange, showPreview = true, fullscreen: fullscreenProp = false, previewOptions, uploadConfig }, ref) {
1214
+ const [content, setContent] = (0, react.useState)(value ?? defaultValue);
1215
+ const [editorView, setEditorView] = (0, react.useState)(null);
1216
+ const scrollDOMRef = (0, react.useRef)(null);
1217
+ const previewElRef = (0, react.useRef)(null);
1218
+ const { fullscreen, toggleFullscreen } = useFullscreen();
1219
+ useScrollSync(scrollDOMRef, previewElRef);
1220
+ const [dialogVisible, setDialogVisible] = (0, react.useState)(false);
1221
+ const [dialogTypeName, setDialogTypeName] = (0, react.useState)("");
1222
+ const [dialogProps, setDialogProps] = (0, react.useState)({});
1223
+ const [dialogPropsRange, setDialogPropsRange] = (0, react.useState)([0, 0]);
1224
+ (0, react.useEffect)(() => {
1225
+ if (value !== void 0) setContent(value);
1226
+ }, [value]);
1227
+ const internalSetContent = (0, react.useCallback)((newContent) => {
1228
+ setContent(newContent);
1229
+ onChange?.(newContent);
1230
+ }, [onChange]);
1231
+ (0, react.useImperativeHandle)(ref, () => ({ getContent: () => content }), [content]);
1232
+ const extraToolbarItems = componentRegistry.getAll().map((schema) => {
1233
+ const defaultProps = {};
1234
+ for (const field of schema?.fields ?? []) defaultProps[field.key] = field.defaultValue ?? "";
1235
+ const iconNode = schema.icon ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_ant_design_icons.ReadOutlined, {});
1236
+ return {
1237
+ id: `container-${schema.name}`,
1238
+ label: schema.label,
1239
+ icon: () => iconNode,
1240
+ handler: (view, _state) => insertContainer(view, schema.name, defaultProps)
1241
+ };
1242
+ });
1243
+ const handleScrollReady = (0, react.useCallback)((scrollDOM) => {
1244
+ scrollDOMRef.current = scrollDOM;
1245
+ }, []);
1246
+ const handlePreviewRef = (0, react.useCallback)((node) => {
1247
+ previewElRef.current = node;
1248
+ }, []);
1249
+ const handleViewReady = (0, react.useCallback)((view) => {
1250
+ setEditorView(view);
1251
+ }, []);
1252
+ const handleEditContainer = (0, react.useCallback)((_view, typeName, propsLineFrom, propsLineTo, currentProps) => {
1253
+ setDialogTypeName(typeName);
1254
+ setDialogProps({ ...currentProps });
1255
+ setDialogPropsRange([propsLineFrom, propsLineTo]);
1256
+ setDialogVisible(true);
1257
+ }, []);
1258
+ const handleDialogSave = (0, react.useCallback)((newProps) => {
1259
+ setDialogVisible(false);
1260
+ if (editorView) updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
1261
+ }, [editorView, dialogPropsRange]);
1262
+ const handleDialogClose = (0, react.useCallback)(() => {
1263
+ setDialogVisible(false);
1264
+ }, []);
1265
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1266
+ className: fullscreen ? "app fullscreen" : "app",
1267
+ children: [
1268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toolbar, {
1269
+ editorView,
1270
+ fullscreen,
1271
+ onToggleFullscreen: fullscreenProp ? toggleFullscreen : void 0,
1272
+ extraItems: extraToolbarItems,
1273
+ uploadConfig
1274
+ }),
1275
+ showPreview ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SplitPanels, {
1276
+ leftPanel: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Editor, {
1277
+ value: content,
1278
+ onChange: internalSetContent,
1279
+ onScrollReady: handleScrollReady,
1280
+ onViewReady: handleViewReady,
1281
+ onEditContainer: handleEditContainer,
1282
+ uploadConfig,
1283
+ onMessageNoUploadConfig: () => antd.message.warning("请先配置图片上传功能")
1284
+ }),
1285
+ rightPanel: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Preview, {
1286
+ content,
1287
+ ref: handlePreviewRef,
1288
+ previewOptions
1289
+ })
1290
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Editor, {
1291
+ value: content,
1292
+ onChange: internalSetContent,
1293
+ onScrollReady: handleScrollReady,
1294
+ onViewReady: handleViewReady,
1295
+ onEditContainer: handleEditContainer,
1296
+ uploadConfig,
1297
+ onMessageNoUploadConfig: () => antd.message.warning("请先配置图片上传功能")
1298
+ }),
1299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditDialog, {
1300
+ visible: dialogVisible,
1301
+ typeName: dialogTypeName,
1302
+ currentProps: dialogProps,
1303
+ onSave: handleDialogSave,
1304
+ onClose: handleDialogClose
1305
+ })
1306
+ ]
1307
+ });
1237
1308
  });
1238
-
1239
- // src/hooks/useRegisterToolbar.ts
1240
- var import_react9 = require("react");
1309
+ //#endregion
1310
+ //#region src/hooks/useRegisterToolbar.ts
1311
+ /**
1312
+ * 注册自定义容器组件到工具栏。支持单个或批量注册。
1313
+ * 支持两种模式:
1314
+ *
1315
+ * 1. 容器模式(linkCard):name + fields + renderDialog
1316
+ * 插入 :::name 格式的容器,支持弹窗编辑属性
1317
+ *
1318
+ * 2. 标签包裹模式:name + wrapTags
1319
+ * 插入简单的标签包裹文本,如 <plugin-container>...</plugin-container>
1320
+ *
1321
+ * @example 容器模式
1322
+ * ```tsx
1323
+ * const schema: ComponentSchema = {
1324
+ * name: 'alert',
1325
+ * label: '警告框',
1326
+ * icon: <AlertOutlined />,
1327
+ * fields: [{ key: 'level', defaultValue: 'info' }],
1328
+ * renderDialog: (props, onChange) => (
1329
+ * <Input value={props.level} onChange={e => onChange('level', e.target.value)} />
1330
+ * ),
1331
+ * }
1332
+ *
1333
+ * function MyPage() {
1334
+ * useRegisterToolbar(schema)
1335
+ * return <MarkdownEditor />
1336
+ * }
1337
+ * ```
1338
+ *
1339
+ * @example 标签包裹模式
1340
+ * ```tsx
1341
+ * function MyPage() {
1342
+ * useRegisterToolbar({
1343
+ * name: 'plugin-container',
1344
+ * label: '自定义容器',
1345
+ * icon: <AlertOutlined />,
1346
+ * wrapTags: ['<plugin-container>', '</plugin-container>'],
1347
+ * })
1348
+ * return <MarkdownEditor />
1349
+ * }
1350
+ * ```
1351
+ *
1352
+ * @example 批量注册
1353
+ * ```tsx
1354
+ * function MyPage() {
1355
+ * useRegisterToolbar([schema1, schema2, schema3])
1356
+ * return <MarkdownEditor />
1357
+ * }
1358
+ * ```
1359
+ */
1241
1360
  function useRegisterToolbar(schemas) {
1242
- const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1243
- (0, import_react9.useEffect)(() => {
1244
- schemaList.forEach((schema) => {
1245
- componentRegistry_default.register(schema.name, schema);
1246
- });
1247
- return () => {
1248
- schemaList.forEach((schema) => {
1249
- componentRegistry_default.unregister(schema.name);
1250
- });
1251
- };
1252
- }, [schemaList]);
1361
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
1362
+ (0, react.useEffect)(() => {
1363
+ schemaList.forEach((schema) => {
1364
+ componentRegistry.register(schema.name, schema);
1365
+ });
1366
+ return () => {
1367
+ schemaList.forEach((schema) => {
1368
+ componentRegistry.unregister(schema.name);
1369
+ });
1370
+ };
1371
+ }, [schemaList]);
1253
1372
  }
1254
- // Annotate the CommonJS export names for ESM import in node:
1255
- 0 && (module.exports = {
1256
- MarkdownEditor,
1257
- useRegisterToolbar
1258
- });
1373
+ //#endregion
1374
+ exports.MarkdownEditor = MarkdownEditor_default;
1375
+ exports.useRegisterToolbar = useRegisterToolbar;
1376
+
1259
1377
  //# sourceMappingURL=index.cjs.map