remons-markdown-editor 1.0.0-beta.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +1 -1
- package/dist/components/Dialog/EditDialog.d.ts +1 -1
- package/dist/components/Editor/index.d.ts +1 -1
- package/dist/components/Icon/index.d.ts +2 -0
- package/dist/components/Toolbar/buttons.d.ts +37 -0
- package/dist/components/Toolbar/index.d.ts +1 -1
- package/dist/components/Toolbar/renderers/TablePicker/index.d.ts +1 -1
- package/dist/components/Toolbar/renderers/ToolbarItemRenderer.d.ts +1 -1
- package/dist/hooks/useScrollSync.d.ts +3 -2
- package/dist/index.cjs +347 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +83 -31
- package/dist/index.js +348 -78
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +3 -5
- package/readme.md +3 -4
package/dist/index.js
CHANGED
|
@@ -6,10 +6,11 @@ import { Decoration, EditorView as EditorView$1, ViewPlugin, WidgetType, keymap
|
|
|
6
6
|
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
|
|
7
7
|
import { markdown } from "@codemirror/lang-markdown";
|
|
8
8
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
9
|
+
import { syntaxTree } from "@codemirror/language";
|
|
9
10
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
11
|
import RenderMarkdown, { initHighlighter, languagesCommon } from "remons-render-markdown";
|
|
11
12
|
import "remons-render-markdown/dist/index.css";
|
|
12
|
-
import {
|
|
13
|
+
import { BoldOutlined, CheckSquareOutlined, CodeOutlined, DownOutlined, FontSizeOutlined, FullscreenExitOutlined, FullscreenOutlined, ItalicOutlined, LinkOutlined, MinusOutlined, OrderedListOutlined, PictureOutlined, ReadOutlined, StrikethroughOutlined, TableOutlined, UnderlineOutlined, UnorderedListOutlined, createFromIconfontCN } from "@ant-design/icons";
|
|
13
14
|
//#region src/utils/parseContainer.ts
|
|
14
15
|
/**
|
|
15
16
|
* 解析容器属性 JSON 字符串。
|
|
@@ -247,25 +248,36 @@ var containerWidgetPlugin = ViewPlugin.fromClass(class {
|
|
|
247
248
|
}
|
|
248
249
|
}, {
|
|
249
250
|
decorations: (value) => value.decorations,
|
|
250
|
-
eventHandlers: {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
251
|
+
eventHandlers: {
|
|
252
|
+
mousedown: (event, view) => {
|
|
253
|
+
if (event.target.classList.contains("container-edit-btn")) {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
},
|
|
259
|
+
click: (event, view) => {
|
|
260
|
+
const target = event.target;
|
|
261
|
+
if (target.classList.contains("container-edit-btn")) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
event.stopPropagation();
|
|
264
|
+
if (target.__typeName) {
|
|
265
|
+
const containers = findContainers(view.state.doc.toString());
|
|
266
|
+
const buttonPos = view.posAtDOM(target);
|
|
267
|
+
for (const container of containers) {
|
|
268
|
+
const headerLine = view.state.doc.lineAt(container.startPos);
|
|
269
|
+
if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
|
|
270
|
+
const callback = view.state.facet(containerEditCallback);
|
|
271
|
+
if (callback) callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
262
274
|
}
|
|
263
275
|
}
|
|
276
|
+
return false;
|
|
264
277
|
}
|
|
265
278
|
return false;
|
|
266
279
|
}
|
|
267
|
-
|
|
268
|
-
} }
|
|
280
|
+
}
|
|
269
281
|
});
|
|
270
282
|
//#endregion
|
|
271
283
|
//#region src/components/Toolbar/buttons.tsx
|
|
@@ -297,6 +309,214 @@ function insertAtLineStart(view, text) {
|
|
|
297
309
|
view.dispatch({ selection: { anchor: line.from + text.length } });
|
|
298
310
|
}
|
|
299
311
|
/**
|
|
312
|
+
* 切换标题:
|
|
313
|
+
* - 当前行已是目标级别标题 → 移除行首 # 前缀(变回普通文本)
|
|
314
|
+
* - 当前行是其他级别标题 → 替换为目标级别
|
|
315
|
+
* - 当前行是普通文本 → 插入目标级别前缀
|
|
316
|
+
* @param view - CodeMirror 编辑器实例
|
|
317
|
+
* @param level - 标题级别(1-6)
|
|
318
|
+
*/
|
|
319
|
+
function toggleHeading(view, level) {
|
|
320
|
+
const line = view.state.doc.lineAt(view.state.selection.main.from);
|
|
321
|
+
const m = line.text.match(/^(\s*)(#{1,6})(\s+|\s*$)/);
|
|
322
|
+
const indent = m ? m[1] : "";
|
|
323
|
+
const text = "#".repeat(level) + " ";
|
|
324
|
+
if (m && m[2].length === level) view.dispatch({ changes: {
|
|
325
|
+
from: line.from,
|
|
326
|
+
to: line.from + m[0].length,
|
|
327
|
+
insert: indent
|
|
328
|
+
} });
|
|
329
|
+
else {
|
|
330
|
+
view.dispatch({ changes: {
|
|
331
|
+
from: line.from,
|
|
332
|
+
to: m ? line.from + m[0].length : line.from,
|
|
333
|
+
insert: indent + text
|
|
334
|
+
} });
|
|
335
|
+
view.dispatch({ selection: { anchor: line.from + indent.length + text.length } });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 切换任务列表(checkbox 待办)
|
|
340
|
+
* - 支持多行:对选区内每一行应用 `- [ ] `
|
|
341
|
+
* - 普通列表项(如 `- 内容`)直接转换为 `- [ ] 内容`
|
|
342
|
+
* - 切换:若所有目标行都已是任务项,则移除行首前缀
|
|
343
|
+
*/
|
|
344
|
+
function toggleTaskList(view) {
|
|
345
|
+
const doc = view.state.doc;
|
|
346
|
+
const { from, to } = view.state.selection.main;
|
|
347
|
+
const first = doc.lineAt(from);
|
|
348
|
+
const last = doc.lineAt(to);
|
|
349
|
+
const lines = [];
|
|
350
|
+
for (let n = first.number; n <= last.number; n++) lines.push(doc.line(n));
|
|
351
|
+
const taskRe = /^(\s*)([-*+])\s\[( |x)\]\s?/;
|
|
352
|
+
const allTask = lines.every((line) => taskRe.test(line.text));
|
|
353
|
+
const changes = [];
|
|
354
|
+
for (const line of lines) {
|
|
355
|
+
const taskMatch = line.text.match(taskRe);
|
|
356
|
+
if (taskMatch) changes.push({
|
|
357
|
+
from: line.from,
|
|
358
|
+
to: line.from + taskMatch[0].length,
|
|
359
|
+
insert: allTask ? "" : `${taskMatch[1]}${taskMatch[2]} [ ] `
|
|
360
|
+
});
|
|
361
|
+
else {
|
|
362
|
+
const bulletMatch = line.text.match(/^(\s*)([-*+])\s/);
|
|
363
|
+
if (bulletMatch) {
|
|
364
|
+
const idx = line.from + bulletMatch[1].length + bulletMatch[2].length;
|
|
365
|
+
changes.push({
|
|
366
|
+
from: idx,
|
|
367
|
+
to: idx + 1,
|
|
368
|
+
insert: " [ ] "
|
|
369
|
+
});
|
|
370
|
+
} else changes.push({
|
|
371
|
+
from: line.from,
|
|
372
|
+
to: line.from,
|
|
373
|
+
insert: "- [ ] "
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
view.dispatch({ changes });
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* 切换行首前缀(支持多行选区)
|
|
381
|
+
* - 已是对应前缀的行 → 移除前缀(变回普通文本)
|
|
382
|
+
* - 未加前缀的行 → 在行首插入前缀
|
|
383
|
+
* @param view - CodeMirror 编辑器实例
|
|
384
|
+
* @param pattern - 匹配行首前缀的正则
|
|
385
|
+
* @param insertText - 未匹配时插入的前缀文本
|
|
386
|
+
*/
|
|
387
|
+
function toggleLinePrefix(view, pattern, insertText) {
|
|
388
|
+
const doc = view.state.doc;
|
|
389
|
+
const { from, to } = view.state.selection.main;
|
|
390
|
+
const first = doc.lineAt(from);
|
|
391
|
+
const last = doc.lineAt(Math.max(to, from));
|
|
392
|
+
const changes = [];
|
|
393
|
+
for (let n = first.number; n <= last.number; n++) {
|
|
394
|
+
const line = doc.line(n);
|
|
395
|
+
const m = line.text.match(pattern);
|
|
396
|
+
if (m) changes.push({
|
|
397
|
+
from: line.from,
|
|
398
|
+
to: line.from + m[0].length,
|
|
399
|
+
insert: ""
|
|
400
|
+
});
|
|
401
|
+
else changes.push({
|
|
402
|
+
from: line.from,
|
|
403
|
+
to: line.from,
|
|
404
|
+
insert: insertText
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
view.dispatch({ changes });
|
|
408
|
+
}
|
|
409
|
+
/** Markdown 列表行:缩进 + 列表标记(无序/有序)+ 空白 */
|
|
410
|
+
var LIST_LINE_RE = /^(\s*)([-*+]|\d+\.)([ \t]+)/;
|
|
411
|
+
/** 列表缩进台阶:4 个空格,确保缩进后能被解析为子列表 */
|
|
412
|
+
var LIST_INDENT_WIDTH = 4;
|
|
413
|
+
/** 判断位置是否位于代码块内(代码块内不参与列表缩进逻辑) */
|
|
414
|
+
function isInCodeBlock(state, pos) {
|
|
415
|
+
let node = syntaxTree(state).resolveInner(pos, 1);
|
|
416
|
+
while (node) {
|
|
417
|
+
if (node.name === "FencedCode" || node.name === "CodeBlock") return true;
|
|
418
|
+
node = node.parent;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* 向上查找同缩进的前一个列表项,返回其序号
|
|
424
|
+
* - 跳过更深的子列表行和空行
|
|
425
|
+
* - 遇到非列表内容或更浅层列表时结束
|
|
426
|
+
*/
|
|
427
|
+
function findPrevSiblingOrder(state, plans, lineNo, indent) {
|
|
428
|
+
const doc = state.doc;
|
|
429
|
+
for (let n = lineNo - 1; n >= 1; n--) {
|
|
430
|
+
const plan = plans.get(n);
|
|
431
|
+
if (plan) {
|
|
432
|
+
if (plan.nextIndent === indent) return plan.nextOrder;
|
|
433
|
+
if (plan.nextIndent < indent) return null;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const text = doc.line(n).text;
|
|
437
|
+
if (!text.trim()) continue;
|
|
438
|
+
const m = text.match(LIST_LINE_RE);
|
|
439
|
+
if (!m) return null;
|
|
440
|
+
const lineIndent = m[1].length;
|
|
441
|
+
if (lineIndent === indent) return /^\d+\.$/.test(m[2]) ? parseInt(m[2], 10) : null;
|
|
442
|
+
if (lineIndent < indent) return null;
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 计算列表行缩进/反缩进的变更
|
|
448
|
+
* - 仅当所有选区行都是列表行时生效,否则返回 null 交给默认缩进行为
|
|
449
|
+
* - 有序列表缩进后重新编号,保证新一级子列表从 1 开始
|
|
450
|
+
*/
|
|
451
|
+
function planListIndent(view, direction) {
|
|
452
|
+
const { state } = view;
|
|
453
|
+
const doc = state.doc;
|
|
454
|
+
const plans = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const range of state.selection.ranges) {
|
|
456
|
+
const first = doc.lineAt(range.from).number;
|
|
457
|
+
const last = doc.lineAt(range.to).number;
|
|
458
|
+
for (let n = first; n <= last; n++) {
|
|
459
|
+
if (plans.has(n)) continue;
|
|
460
|
+
const line = doc.line(n);
|
|
461
|
+
if (isInCodeBlock(state, line.from)) return null;
|
|
462
|
+
const m = line.text.match(LIST_LINE_RE);
|
|
463
|
+
if (!m) return null;
|
|
464
|
+
const indent = m[1].length;
|
|
465
|
+
plans.set(n, {
|
|
466
|
+
indent,
|
|
467
|
+
nextIndent: direction === 1 ? indent + LIST_INDENT_WIDTH : Math.max(0, indent - LIST_INDENT_WIDTH),
|
|
468
|
+
order: /^\d+\.$/.test(m[2]) ? parseInt(m[2], 10) : null,
|
|
469
|
+
nextOrder: null
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (plans.size === 0) return null;
|
|
474
|
+
const ordered = [...plans.entries()].sort((a, b) => a[0] - b[0]);
|
|
475
|
+
for (const [lineNo, info] of ordered) {
|
|
476
|
+
if (info.order === null) continue;
|
|
477
|
+
const prevOrder = findPrevSiblingOrder(state, plans, lineNo, info.nextIndent);
|
|
478
|
+
info.nextOrder = prevOrder === null ? 1 : prevOrder + 1;
|
|
479
|
+
}
|
|
480
|
+
return ordered.map(([lineNo, info]) => {
|
|
481
|
+
const line = doc.line(lineNo);
|
|
482
|
+
const m = line.text.match(LIST_LINE_RE);
|
|
483
|
+
const useNextOrder = (direction === 1 || info.indent > 0) && info.order !== null && info.nextOrder !== null && info.order !== info.nextOrder;
|
|
484
|
+
return {
|
|
485
|
+
from: line.from,
|
|
486
|
+
to: line.from + m[0].length,
|
|
487
|
+
insert: " ".repeat(info.nextIndent) + (useNextOrder ? `${info.nextOrder}.` : m[2]) + m[3]
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Tab:列表行缩进一级(4 空格)
|
|
493
|
+
* - 普通行返回 false,继续走默认缩进
|
|
494
|
+
* - 有序列表缩进后重排序号,子列表从 1 开始
|
|
495
|
+
*/
|
|
496
|
+
function indentMarkdownList(view) {
|
|
497
|
+
const changes = planListIndent(view, 1);
|
|
498
|
+
if (!changes) return false;
|
|
499
|
+
view.dispatch({
|
|
500
|
+
changes,
|
|
501
|
+
userEvent: "input.indent"
|
|
502
|
+
});
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Shift-Tab:列表行反缩进一级
|
|
507
|
+
* - 普通行返回 false,继续走默认缩进
|
|
508
|
+
* - 有序列表反缩进后并入上一层并重排序号
|
|
509
|
+
*/
|
|
510
|
+
function dedentMarkdownList(view) {
|
|
511
|
+
const changes = planListIndent(view, -1);
|
|
512
|
+
if (!changes) return false;
|
|
513
|
+
view.dispatch({
|
|
514
|
+
changes,
|
|
515
|
+
userEvent: "delete.dedent"
|
|
516
|
+
});
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
300
520
|
* 上传图片
|
|
301
521
|
* @param file - 要上传的文件
|
|
302
522
|
* @param uploadConfig - 上传配置
|
|
@@ -443,7 +663,18 @@ var getCodeMirrorExtensions = (opts) => {
|
|
|
443
663
|
const extensions = [
|
|
444
664
|
markdown(),
|
|
445
665
|
oneDark,
|
|
446
|
-
keymap.of([
|
|
666
|
+
keymap.of([
|
|
667
|
+
{
|
|
668
|
+
key: "Tab",
|
|
669
|
+
run: indentMarkdownList
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
key: "Shift-Tab",
|
|
673
|
+
run: dedentMarkdownList
|
|
674
|
+
},
|
|
675
|
+
...defaultKeymap,
|
|
676
|
+
indentWithTab
|
|
677
|
+
]),
|
|
447
678
|
EditorView$1.lineWrapping,
|
|
448
679
|
pasteHandler,
|
|
449
680
|
EditorView$1.baseTheme({
|
|
@@ -565,7 +796,24 @@ function SplitPanels({ leftPanel, rightPanel }) {
|
|
|
565
796
|
});
|
|
566
797
|
}
|
|
567
798
|
//#endregion
|
|
799
|
+
//#region src/components/Icon/index.tsx
|
|
800
|
+
var Icon = createFromIconfontCN({ scriptUrl: "https://at.alicdn.com/t/c/font_5241307_xoojyr52dqn.js" });
|
|
801
|
+
function IconComponent(type, className) {
|
|
802
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
803
|
+
type: `icon-${type}`,
|
|
804
|
+
className
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
//#endregion
|
|
568
808
|
//#region src/components/Toolbar/config.ts
|
|
809
|
+
function Quote() {
|
|
810
|
+
return IconComponent("quote");
|
|
811
|
+
}
|
|
812
|
+
function InlineCode() {
|
|
813
|
+
return IconComponent("inlinecode");
|
|
814
|
+
}
|
|
815
|
+
/** 未配置图片上传时的提示文案 */
|
|
816
|
+
var UPLOAD_CONFIG_MISSING_MESSAGE = "请先配置图片上传功能";
|
|
569
817
|
/**
|
|
570
818
|
* 创建工具栏配置
|
|
571
819
|
* @param options - 配置选项
|
|
@@ -580,39 +828,33 @@ function createToolbarConfig(options) {
|
|
|
580
828
|
children: [
|
|
581
829
|
{
|
|
582
830
|
id: "heading1",
|
|
583
|
-
label: "
|
|
584
|
-
|
|
585
|
-
handler: (v) => insertAtLineStart(v, "# ")
|
|
831
|
+
label: "H1",
|
|
832
|
+
handler: (v) => toggleHeading(v, 1)
|
|
586
833
|
},
|
|
587
834
|
{
|
|
588
835
|
id: "heading2",
|
|
589
|
-
label: "
|
|
590
|
-
|
|
591
|
-
handler: (v) => insertAtLineStart(v, "## ")
|
|
836
|
+
label: "H2",
|
|
837
|
+
handler: (v) => toggleHeading(v, 2)
|
|
592
838
|
},
|
|
593
839
|
{
|
|
594
840
|
id: "heading3",
|
|
595
|
-
label: "
|
|
596
|
-
|
|
597
|
-
handler: (v) => insertAtLineStart(v, "### ")
|
|
841
|
+
label: "H3",
|
|
842
|
+
handler: (v) => toggleHeading(v, 3)
|
|
598
843
|
},
|
|
599
844
|
{
|
|
600
845
|
id: "heading4",
|
|
601
|
-
label: "
|
|
602
|
-
|
|
603
|
-
handler: (v) => insertAtLineStart(v, "#### ")
|
|
846
|
+
label: "H4",
|
|
847
|
+
handler: (v) => toggleHeading(v, 4)
|
|
604
848
|
},
|
|
605
849
|
{
|
|
606
850
|
id: "heading5",
|
|
607
|
-
label: "
|
|
608
|
-
|
|
609
|
-
handler: (v) => insertAtLineStart(v, "##### ")
|
|
851
|
+
label: "H5",
|
|
852
|
+
handler: (v) => toggleHeading(v, 5)
|
|
610
853
|
},
|
|
611
854
|
{
|
|
612
855
|
id: "heading6",
|
|
613
|
-
label: "
|
|
614
|
-
|
|
615
|
-
handler: (v) => insertAtLineStart(v, "###### ")
|
|
856
|
+
label: "H6",
|
|
857
|
+
handler: (v) => toggleHeading(v, 6)
|
|
616
858
|
}
|
|
617
859
|
]
|
|
618
860
|
},
|
|
@@ -621,38 +863,44 @@ function createToolbarConfig(options) {
|
|
|
621
863
|
id: "bold",
|
|
622
864
|
label: "加粗",
|
|
623
865
|
icon: BoldOutlined,
|
|
624
|
-
handler: (v) =>
|
|
866
|
+
handler: (v) => toggleWrap(v, "**", "**")
|
|
625
867
|
},
|
|
626
868
|
{
|
|
627
869
|
id: "italic",
|
|
628
870
|
label: "斜体",
|
|
629
871
|
icon: ItalicOutlined,
|
|
630
|
-
handler: (v) =>
|
|
872
|
+
handler: (v) => toggleWrap(v, "*", "*")
|
|
631
873
|
},
|
|
632
874
|
{
|
|
633
875
|
id: "strikethrough",
|
|
634
876
|
label: "删除线",
|
|
635
877
|
icon: StrikethroughOutlined,
|
|
636
|
-
handler: (v) =>
|
|
878
|
+
handler: (v) => toggleWrap(v, "~~", "~~")
|
|
637
879
|
},
|
|
638
880
|
{
|
|
639
881
|
id: "underline",
|
|
640
882
|
label: "下划线",
|
|
641
883
|
icon: UnderlineOutlined,
|
|
642
|
-
handler: (v) =>
|
|
884
|
+
handler: (v) => toggleWrap(v, "<u>", "</u>")
|
|
643
885
|
},
|
|
644
886
|
{ separatorBefore: true },
|
|
645
887
|
{
|
|
646
888
|
id: "ul",
|
|
647
889
|
label: "无序列表",
|
|
648
890
|
icon: UnorderedListOutlined,
|
|
649
|
-
handler: (v) =>
|
|
891
|
+
handler: (v) => toggleLinePrefix(v, /^(\s*)-(?!\s*\[)\s/, "- ")
|
|
650
892
|
},
|
|
651
893
|
{
|
|
652
894
|
id: "ol",
|
|
653
895
|
label: "有序列表",
|
|
654
896
|
icon: OrderedListOutlined,
|
|
655
|
-
handler: (v) =>
|
|
897
|
+
handler: (v) => toggleLinePrefix(v, /^(\s*)\d+\.\s/, "1. ")
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
id: "checkbox",
|
|
901
|
+
label: "待办",
|
|
902
|
+
icon: CheckSquareOutlined,
|
|
903
|
+
handler: (v) => toggleTaskList(v)
|
|
656
904
|
},
|
|
657
905
|
{ separatorBefore: true },
|
|
658
906
|
{
|
|
@@ -663,19 +911,15 @@ function createToolbarConfig(options) {
|
|
|
663
911
|
},
|
|
664
912
|
{
|
|
665
913
|
id: "image",
|
|
666
|
-
label:
|
|
914
|
+
label: "图片",
|
|
667
915
|
icon: PictureOutlined,
|
|
668
|
-
|
|
669
|
-
handler: hasUploadConfig ? (v) => uploadImage(v, {
|
|
670
|
-
uploadUrl: options?.uploadUrl,
|
|
671
|
-
onUploadImage: options?.onUploadImage
|
|
672
|
-
}) : void 0
|
|
916
|
+
handler: hasUploadConfig && options ? (v) => uploadImage(v, options) : () => message.warning(UPLOAD_CONFIG_MISSING_MESSAGE)
|
|
673
917
|
},
|
|
674
918
|
{ separatorBefore: true },
|
|
675
919
|
{
|
|
676
920
|
id: "code",
|
|
677
921
|
label: "行内代码",
|
|
678
|
-
icon:
|
|
922
|
+
icon: InlineCode,
|
|
679
923
|
handler: (v) => wrapSelection(v, "`", "`")
|
|
680
924
|
},
|
|
681
925
|
{
|
|
@@ -696,7 +940,7 @@ function createToolbarConfig(options) {
|
|
|
696
940
|
{
|
|
697
941
|
id: "quote",
|
|
698
942
|
label: "引用",
|
|
699
|
-
icon:
|
|
943
|
+
icon: Quote,
|
|
700
944
|
handler: (v) => insertAtLineStart(v, "> ")
|
|
701
945
|
},
|
|
702
946
|
{
|
|
@@ -767,10 +1011,7 @@ function ToolbarItemRenderer({ item, onClick }) {
|
|
|
767
1011
|
className: "toolbar-button",
|
|
768
1012
|
type: "text",
|
|
769
1013
|
size: "small",
|
|
770
|
-
children: [/* @__PURE__ */ jsx(Icon, {}), /* @__PURE__ */ jsx(DownOutlined, { style: {
|
|
771
|
-
marginLeft: 2,
|
|
772
|
-
fontSize: 10
|
|
773
|
-
} })]
|
|
1014
|
+
children: [/* @__PURE__ */ jsx(Icon, {}), /* @__PURE__ */ jsx(DownOutlined, { style: { fontSize: 10 } })]
|
|
774
1015
|
})
|
|
775
1016
|
})
|
|
776
1017
|
})
|
|
@@ -1095,6 +1336,9 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
|
|
|
1095
1336
|
const schema = componentRegistry.get(typeName);
|
|
1096
1337
|
const [formProps, setFormProps] = useState({});
|
|
1097
1338
|
const [form] = Form.useForm();
|
|
1339
|
+
const onSaveRef = useRef(onSave);
|
|
1340
|
+
onSaveRef.current = onSave;
|
|
1341
|
+
const dirtyRef = useRef(false);
|
|
1098
1342
|
const mergedProps = useMemo(() => {
|
|
1099
1343
|
if (!schema) return currentProps;
|
|
1100
1344
|
const defaults = {};
|
|
@@ -1132,18 +1376,28 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
|
|
|
1132
1376
|
...formValues
|
|
1133
1377
|
};
|
|
1134
1378
|
} catch {}
|
|
1135
|
-
|
|
1379
|
+
onSaveRef.current(finalProps);
|
|
1380
|
+
dirtyRef.current = false;
|
|
1381
|
+
onClose();
|
|
1136
1382
|
};
|
|
1137
1383
|
const handleClose = () => {
|
|
1138
1384
|
setFormProps({});
|
|
1385
|
+
dirtyRef.current = false;
|
|
1139
1386
|
onClose();
|
|
1140
1387
|
};
|
|
1141
1388
|
if (!schema) return null;
|
|
1389
|
+
const contextSave = (props) => {
|
|
1390
|
+
setFormProps((prev) => ({
|
|
1391
|
+
...prev,
|
|
1392
|
+
...props
|
|
1393
|
+
}));
|
|
1394
|
+
dirtyRef.current = true;
|
|
1395
|
+
};
|
|
1142
1396
|
const context = {
|
|
1143
1397
|
props: mergedProps,
|
|
1144
1398
|
form,
|
|
1145
1399
|
onChange: handleDialogChange,
|
|
1146
|
-
onSave
|
|
1400
|
+
onSave: contextSave
|
|
1147
1401
|
};
|
|
1148
1402
|
const renderDialogContent = () => {
|
|
1149
1403
|
if (schema.renderDialog) return schema.renderDialog(context);
|
|
@@ -1229,38 +1483,55 @@ function useFullscreen() {
|
|
|
1229
1483
|
//#endregion
|
|
1230
1484
|
//#region src/hooks/useScrollSync.ts
|
|
1231
1485
|
/**
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1486
|
+
* 双向滚动同步 Hook
|
|
1487
|
+
* 编辑器与预览区互相同步滚动位置(基于比例)
|
|
1488
|
+
* 使用 síncing 锁防止双向循环触发
|
|
1234
1489
|
*/
|
|
1235
1490
|
function useScrollSync(sourceRef, targetRef) {
|
|
1236
1491
|
const rafIdRef = useRef(null);
|
|
1492
|
+
const syncingRef = useRef(null);
|
|
1493
|
+
const syncScroll = (from, fromEl, toEl) => {
|
|
1494
|
+
if (rafIdRef.current) return;
|
|
1495
|
+
rafIdRef.current = requestAnimationFrame(() => {
|
|
1496
|
+
const fromScrollHeight = fromEl.scrollHeight - fromEl.clientHeight;
|
|
1497
|
+
if (fromScrollHeight <= 0) {
|
|
1498
|
+
rafIdRef.current = null;
|
|
1499
|
+
syncingRef.current = null;
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const ratio = fromEl.scrollTop / fromScrollHeight;
|
|
1503
|
+
const toScrollHeight = toEl.scrollHeight - toEl.clientHeight;
|
|
1504
|
+
if (toScrollHeight > 0) {
|
|
1505
|
+
syncingRef.current = from;
|
|
1506
|
+
toEl.scrollTop = ratio * toScrollHeight;
|
|
1507
|
+
requestAnimationFrame(() => {
|
|
1508
|
+
syncingRef.current = null;
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
rafIdRef.current = null;
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1237
1514
|
useEffect(() => {
|
|
1238
1515
|
const source = sourceRef.current;
|
|
1239
1516
|
const target = targetRef.current;
|
|
1240
1517
|
if (!source || !target) return;
|
|
1241
|
-
const
|
|
1242
|
-
if (
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (!s || !t) {
|
|
1247
|
-
rafIdRef.current = null;
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
const sourceScrollHeight = s.scrollHeight - s.clientHeight;
|
|
1251
|
-
if (sourceScrollHeight <= 0) {
|
|
1252
|
-
rafIdRef.current = null;
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
const ratio = s.scrollTop / sourceScrollHeight;
|
|
1256
|
-
const targetScrollHeight = t.scrollHeight - t.clientHeight;
|
|
1257
|
-
if (targetScrollHeight > 0) t.scrollTop = ratio * targetScrollHeight;
|
|
1258
|
-
rafIdRef.current = null;
|
|
1259
|
-
});
|
|
1518
|
+
const onSourceScroll = () => {
|
|
1519
|
+
if (syncingRef.current !== null) return;
|
|
1520
|
+
const s = sourceRef.current;
|
|
1521
|
+
const t = targetRef.current;
|
|
1522
|
+
syncScroll("source", s, t);
|
|
1260
1523
|
};
|
|
1261
|
-
|
|
1524
|
+
const onTargetScroll = () => {
|
|
1525
|
+
if (syncingRef.current !== null) return;
|
|
1526
|
+
const s = sourceRef.current;
|
|
1527
|
+
const t = targetRef.current;
|
|
1528
|
+
syncScroll("target", t, s);
|
|
1529
|
+
};
|
|
1530
|
+
source.addEventListener("scroll", onSourceScroll, { passive: true });
|
|
1531
|
+
target.addEventListener("scroll", onTargetScroll, { passive: true });
|
|
1262
1532
|
return () => {
|
|
1263
|
-
source.removeEventListener("scroll",
|
|
1533
|
+
source.removeEventListener("scroll", onSourceScroll);
|
|
1534
|
+
target.removeEventListener("scroll", onTargetScroll);
|
|
1264
1535
|
if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
|
|
1265
1536
|
};
|
|
1266
1537
|
}, [sourceRef, targetRef]);
|
|
@@ -1318,7 +1589,6 @@ var MarkdownEditor_default = forwardRef(function MarkdownEditor({ defaultValue =
|
|
|
1318
1589
|
setDialogVisible(true);
|
|
1319
1590
|
}, []);
|
|
1320
1591
|
const handleDialogSave = useCallback((newProps) => {
|
|
1321
|
-
setDialogVisible(false);
|
|
1322
1592
|
if (editorView) updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
|
|
1323
1593
|
}, [editorView, dialogPropsRange]);
|
|
1324
1594
|
const handleDialogClose = useCallback(() => {
|