remons-markdown-editor 1.0.0-beta.8 → 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 +7 -9
- package/readme.md +4 -5
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ let _codemirror_view = require("@codemirror/view");
|
|
|
30
30
|
let _codemirror_commands = require("@codemirror/commands");
|
|
31
31
|
let _codemirror_lang_markdown = require("@codemirror/lang-markdown");
|
|
32
32
|
let _codemirror_theme_one_dark = require("@codemirror/theme-one-dark");
|
|
33
|
+
let _codemirror_language = require("@codemirror/language");
|
|
33
34
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
34
35
|
let remons_render_markdown = require("remons-render-markdown");
|
|
35
36
|
remons_render_markdown = __toESM(remons_render_markdown, 1);
|
|
@@ -272,25 +273,36 @@ var containerWidgetPlugin = _codemirror_view.ViewPlugin.fromClass(class {
|
|
|
272
273
|
}
|
|
273
274
|
}, {
|
|
274
275
|
decorations: (value) => value.decorations,
|
|
275
|
-
eventHandlers: {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
eventHandlers: {
|
|
277
|
+
mousedown: (event, view) => {
|
|
278
|
+
if (event.target.classList.contains("container-edit-btn")) {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
},
|
|
284
|
+
click: (event, view) => {
|
|
285
|
+
const target = event.target;
|
|
286
|
+
if (target.classList.contains("container-edit-btn")) {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
event.stopPropagation();
|
|
289
|
+
if (target.__typeName) {
|
|
290
|
+
const containers = findContainers(view.state.doc.toString());
|
|
291
|
+
const buttonPos = view.posAtDOM(target);
|
|
292
|
+
for (const container of containers) {
|
|
293
|
+
const headerLine = view.state.doc.lineAt(container.startPos);
|
|
294
|
+
if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
|
|
295
|
+
const callback = view.state.facet(containerEditCallback);
|
|
296
|
+
if (callback) callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
287
299
|
}
|
|
288
300
|
}
|
|
301
|
+
return false;
|
|
289
302
|
}
|
|
290
303
|
return false;
|
|
291
304
|
}
|
|
292
|
-
|
|
293
|
-
} }
|
|
305
|
+
}
|
|
294
306
|
});
|
|
295
307
|
//#endregion
|
|
296
308
|
//#region src/components/Toolbar/buttons.tsx
|
|
@@ -322,6 +334,214 @@ function insertAtLineStart(view, text) {
|
|
|
322
334
|
view.dispatch({ selection: { anchor: line.from + text.length } });
|
|
323
335
|
}
|
|
324
336
|
/**
|
|
337
|
+
* 切换标题:
|
|
338
|
+
* - 当前行已是目标级别标题 → 移除行首 # 前缀(变回普通文本)
|
|
339
|
+
* - 当前行是其他级别标题 → 替换为目标级别
|
|
340
|
+
* - 当前行是普通文本 → 插入目标级别前缀
|
|
341
|
+
* @param view - CodeMirror 编辑器实例
|
|
342
|
+
* @param level - 标题级别(1-6)
|
|
343
|
+
*/
|
|
344
|
+
function toggleHeading(view, level) {
|
|
345
|
+
const line = view.state.doc.lineAt(view.state.selection.main.from);
|
|
346
|
+
const m = line.text.match(/^(\s*)(#{1,6})(\s+|\s*$)/);
|
|
347
|
+
const indent = m ? m[1] : "";
|
|
348
|
+
const text = "#".repeat(level) + " ";
|
|
349
|
+
if (m && m[2].length === level) view.dispatch({ changes: {
|
|
350
|
+
from: line.from,
|
|
351
|
+
to: line.from + m[0].length,
|
|
352
|
+
insert: indent
|
|
353
|
+
} });
|
|
354
|
+
else {
|
|
355
|
+
view.dispatch({ changes: {
|
|
356
|
+
from: line.from,
|
|
357
|
+
to: m ? line.from + m[0].length : line.from,
|
|
358
|
+
insert: indent + text
|
|
359
|
+
} });
|
|
360
|
+
view.dispatch({ selection: { anchor: line.from + indent.length + text.length } });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 切换任务列表(checkbox 待办)
|
|
365
|
+
* - 支持多行:对选区内每一行应用 `- [ ] `
|
|
366
|
+
* - 普通列表项(如 `- 内容`)直接转换为 `- [ ] 内容`
|
|
367
|
+
* - 切换:若所有目标行都已是任务项,则移除行首前缀
|
|
368
|
+
*/
|
|
369
|
+
function toggleTaskList(view) {
|
|
370
|
+
const doc = view.state.doc;
|
|
371
|
+
const { from, to } = view.state.selection.main;
|
|
372
|
+
const first = doc.lineAt(from);
|
|
373
|
+
const last = doc.lineAt(to);
|
|
374
|
+
const lines = [];
|
|
375
|
+
for (let n = first.number; n <= last.number; n++) lines.push(doc.line(n));
|
|
376
|
+
const taskRe = /^(\s*)([-*+])\s\[( |x)\]\s?/;
|
|
377
|
+
const allTask = lines.every((line) => taskRe.test(line.text));
|
|
378
|
+
const changes = [];
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
const taskMatch = line.text.match(taskRe);
|
|
381
|
+
if (taskMatch) changes.push({
|
|
382
|
+
from: line.from,
|
|
383
|
+
to: line.from + taskMatch[0].length,
|
|
384
|
+
insert: allTask ? "" : `${taskMatch[1]}${taskMatch[2]} [ ] `
|
|
385
|
+
});
|
|
386
|
+
else {
|
|
387
|
+
const bulletMatch = line.text.match(/^(\s*)([-*+])\s/);
|
|
388
|
+
if (bulletMatch) {
|
|
389
|
+
const idx = line.from + bulletMatch[1].length + bulletMatch[2].length;
|
|
390
|
+
changes.push({
|
|
391
|
+
from: idx,
|
|
392
|
+
to: idx + 1,
|
|
393
|
+
insert: " [ ] "
|
|
394
|
+
});
|
|
395
|
+
} else changes.push({
|
|
396
|
+
from: line.from,
|
|
397
|
+
to: line.from,
|
|
398
|
+
insert: "- [ ] "
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
view.dispatch({ changes });
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* 切换行首前缀(支持多行选区)
|
|
406
|
+
* - 已是对应前缀的行 → 移除前缀(变回普通文本)
|
|
407
|
+
* - 未加前缀的行 → 在行首插入前缀
|
|
408
|
+
* @param view - CodeMirror 编辑器实例
|
|
409
|
+
* @param pattern - 匹配行首前缀的正则
|
|
410
|
+
* @param insertText - 未匹配时插入的前缀文本
|
|
411
|
+
*/
|
|
412
|
+
function toggleLinePrefix(view, pattern, insertText) {
|
|
413
|
+
const doc = view.state.doc;
|
|
414
|
+
const { from, to } = view.state.selection.main;
|
|
415
|
+
const first = doc.lineAt(from);
|
|
416
|
+
const last = doc.lineAt(Math.max(to, from));
|
|
417
|
+
const changes = [];
|
|
418
|
+
for (let n = first.number; n <= last.number; n++) {
|
|
419
|
+
const line = doc.line(n);
|
|
420
|
+
const m = line.text.match(pattern);
|
|
421
|
+
if (m) changes.push({
|
|
422
|
+
from: line.from,
|
|
423
|
+
to: line.from + m[0].length,
|
|
424
|
+
insert: ""
|
|
425
|
+
});
|
|
426
|
+
else changes.push({
|
|
427
|
+
from: line.from,
|
|
428
|
+
to: line.from,
|
|
429
|
+
insert: insertText
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
view.dispatch({ changes });
|
|
433
|
+
}
|
|
434
|
+
/** Markdown 列表行:缩进 + 列表标记(无序/有序)+ 空白 */
|
|
435
|
+
var LIST_LINE_RE = /^(\s*)([-*+]|\d+\.)([ \t]+)/;
|
|
436
|
+
/** 列表缩进台阶:4 个空格,确保缩进后能被解析为子列表 */
|
|
437
|
+
var LIST_INDENT_WIDTH = 4;
|
|
438
|
+
/** 判断位置是否位于代码块内(代码块内不参与列表缩进逻辑) */
|
|
439
|
+
function isInCodeBlock(state, pos) {
|
|
440
|
+
let node = (0, _codemirror_language.syntaxTree)(state).resolveInner(pos, 1);
|
|
441
|
+
while (node) {
|
|
442
|
+
if (node.name === "FencedCode" || node.name === "CodeBlock") return true;
|
|
443
|
+
node = node.parent;
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 向上查找同缩进的前一个列表项,返回其序号
|
|
449
|
+
* - 跳过更深的子列表行和空行
|
|
450
|
+
* - 遇到非列表内容或更浅层列表时结束
|
|
451
|
+
*/
|
|
452
|
+
function findPrevSiblingOrder(state, plans, lineNo, indent) {
|
|
453
|
+
const doc = state.doc;
|
|
454
|
+
for (let n = lineNo - 1; n >= 1; n--) {
|
|
455
|
+
const plan = plans.get(n);
|
|
456
|
+
if (plan) {
|
|
457
|
+
if (plan.nextIndent === indent) return plan.nextOrder;
|
|
458
|
+
if (plan.nextIndent < indent) return null;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const text = doc.line(n).text;
|
|
462
|
+
if (!text.trim()) continue;
|
|
463
|
+
const m = text.match(LIST_LINE_RE);
|
|
464
|
+
if (!m) return null;
|
|
465
|
+
const lineIndent = m[1].length;
|
|
466
|
+
if (lineIndent === indent) return /^\d+\.$/.test(m[2]) ? parseInt(m[2], 10) : null;
|
|
467
|
+
if (lineIndent < indent) return null;
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* 计算列表行缩进/反缩进的变更
|
|
473
|
+
* - 仅当所有选区行都是列表行时生效,否则返回 null 交给默认缩进行为
|
|
474
|
+
* - 有序列表缩进后重新编号,保证新一级子列表从 1 开始
|
|
475
|
+
*/
|
|
476
|
+
function planListIndent(view, direction) {
|
|
477
|
+
const { state } = view;
|
|
478
|
+
const doc = state.doc;
|
|
479
|
+
const plans = /* @__PURE__ */ new Map();
|
|
480
|
+
for (const range of state.selection.ranges) {
|
|
481
|
+
const first = doc.lineAt(range.from).number;
|
|
482
|
+
const last = doc.lineAt(range.to).number;
|
|
483
|
+
for (let n = first; n <= last; n++) {
|
|
484
|
+
if (plans.has(n)) continue;
|
|
485
|
+
const line = doc.line(n);
|
|
486
|
+
if (isInCodeBlock(state, line.from)) return null;
|
|
487
|
+
const m = line.text.match(LIST_LINE_RE);
|
|
488
|
+
if (!m) return null;
|
|
489
|
+
const indent = m[1].length;
|
|
490
|
+
plans.set(n, {
|
|
491
|
+
indent,
|
|
492
|
+
nextIndent: direction === 1 ? indent + LIST_INDENT_WIDTH : Math.max(0, indent - LIST_INDENT_WIDTH),
|
|
493
|
+
order: /^\d+\.$/.test(m[2]) ? parseInt(m[2], 10) : null,
|
|
494
|
+
nextOrder: null
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (plans.size === 0) return null;
|
|
499
|
+
const ordered = [...plans.entries()].sort((a, b) => a[0] - b[0]);
|
|
500
|
+
for (const [lineNo, info] of ordered) {
|
|
501
|
+
if (info.order === null) continue;
|
|
502
|
+
const prevOrder = findPrevSiblingOrder(state, plans, lineNo, info.nextIndent);
|
|
503
|
+
info.nextOrder = prevOrder === null ? 1 : prevOrder + 1;
|
|
504
|
+
}
|
|
505
|
+
return ordered.map(([lineNo, info]) => {
|
|
506
|
+
const line = doc.line(lineNo);
|
|
507
|
+
const m = line.text.match(LIST_LINE_RE);
|
|
508
|
+
const useNextOrder = (direction === 1 || info.indent > 0) && info.order !== null && info.nextOrder !== null && info.order !== info.nextOrder;
|
|
509
|
+
return {
|
|
510
|
+
from: line.from,
|
|
511
|
+
to: line.from + m[0].length,
|
|
512
|
+
insert: " ".repeat(info.nextIndent) + (useNextOrder ? `${info.nextOrder}.` : m[2]) + m[3]
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Tab:列表行缩进一级(4 空格)
|
|
518
|
+
* - 普通行返回 false,继续走默认缩进
|
|
519
|
+
* - 有序列表缩进后重排序号,子列表从 1 开始
|
|
520
|
+
*/
|
|
521
|
+
function indentMarkdownList(view) {
|
|
522
|
+
const changes = planListIndent(view, 1);
|
|
523
|
+
if (!changes) return false;
|
|
524
|
+
view.dispatch({
|
|
525
|
+
changes,
|
|
526
|
+
userEvent: "input.indent"
|
|
527
|
+
});
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Shift-Tab:列表行反缩进一级
|
|
532
|
+
* - 普通行返回 false,继续走默认缩进
|
|
533
|
+
* - 有序列表反缩进后并入上一层并重排序号
|
|
534
|
+
*/
|
|
535
|
+
function dedentMarkdownList(view) {
|
|
536
|
+
const changes = planListIndent(view, -1);
|
|
537
|
+
if (!changes) return false;
|
|
538
|
+
view.dispatch({
|
|
539
|
+
changes,
|
|
540
|
+
userEvent: "delete.dedent"
|
|
541
|
+
});
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
325
545
|
* 上传图片
|
|
326
546
|
* @param file - 要上传的文件
|
|
327
547
|
* @param uploadConfig - 上传配置
|
|
@@ -468,7 +688,18 @@ var getCodeMirrorExtensions = (opts) => {
|
|
|
468
688
|
const extensions = [
|
|
469
689
|
(0, _codemirror_lang_markdown.markdown)(),
|
|
470
690
|
_codemirror_theme_one_dark.oneDark,
|
|
471
|
-
_codemirror_view.keymap.of([
|
|
691
|
+
_codemirror_view.keymap.of([
|
|
692
|
+
{
|
|
693
|
+
key: "Tab",
|
|
694
|
+
run: indentMarkdownList
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
key: "Shift-Tab",
|
|
698
|
+
run: dedentMarkdownList
|
|
699
|
+
},
|
|
700
|
+
..._codemirror_commands.defaultKeymap,
|
|
701
|
+
_codemirror_commands.indentWithTab
|
|
702
|
+
]),
|
|
472
703
|
_codemirror_view.EditorView.lineWrapping,
|
|
473
704
|
pasteHandler,
|
|
474
705
|
_codemirror_view.EditorView.baseTheme({
|
|
@@ -590,7 +821,24 @@ function SplitPanels({ leftPanel, rightPanel }) {
|
|
|
590
821
|
});
|
|
591
822
|
}
|
|
592
823
|
//#endregion
|
|
824
|
+
//#region src/components/Icon/index.tsx
|
|
825
|
+
var Icon = (0, _ant_design_icons.createFromIconfontCN)({ scriptUrl: "https://at.alicdn.com/t/c/font_5241307_xoojyr52dqn.js" });
|
|
826
|
+
function IconComponent(type, className) {
|
|
827
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
828
|
+
type: `icon-${type}`,
|
|
829
|
+
className
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
//#endregion
|
|
593
833
|
//#region src/components/Toolbar/config.ts
|
|
834
|
+
function Quote() {
|
|
835
|
+
return IconComponent("quote");
|
|
836
|
+
}
|
|
837
|
+
function InlineCode() {
|
|
838
|
+
return IconComponent("inlinecode");
|
|
839
|
+
}
|
|
840
|
+
/** 未配置图片上传时的提示文案 */
|
|
841
|
+
var UPLOAD_CONFIG_MISSING_MESSAGE = "请先配置图片上传功能";
|
|
594
842
|
/**
|
|
595
843
|
* 创建工具栏配置
|
|
596
844
|
* @param options - 配置选项
|
|
@@ -605,39 +853,33 @@ function createToolbarConfig(options) {
|
|
|
605
853
|
children: [
|
|
606
854
|
{
|
|
607
855
|
id: "heading1",
|
|
608
|
-
label: "
|
|
609
|
-
|
|
610
|
-
handler: (v) => insertAtLineStart(v, "# ")
|
|
856
|
+
label: "H1",
|
|
857
|
+
handler: (v) => toggleHeading(v, 1)
|
|
611
858
|
},
|
|
612
859
|
{
|
|
613
860
|
id: "heading2",
|
|
614
|
-
label: "
|
|
615
|
-
|
|
616
|
-
handler: (v) => insertAtLineStart(v, "## ")
|
|
861
|
+
label: "H2",
|
|
862
|
+
handler: (v) => toggleHeading(v, 2)
|
|
617
863
|
},
|
|
618
864
|
{
|
|
619
865
|
id: "heading3",
|
|
620
|
-
label: "
|
|
621
|
-
|
|
622
|
-
handler: (v) => insertAtLineStart(v, "### ")
|
|
866
|
+
label: "H3",
|
|
867
|
+
handler: (v) => toggleHeading(v, 3)
|
|
623
868
|
},
|
|
624
869
|
{
|
|
625
870
|
id: "heading4",
|
|
626
|
-
label: "
|
|
627
|
-
|
|
628
|
-
handler: (v) => insertAtLineStart(v, "#### ")
|
|
871
|
+
label: "H4",
|
|
872
|
+
handler: (v) => toggleHeading(v, 4)
|
|
629
873
|
},
|
|
630
874
|
{
|
|
631
875
|
id: "heading5",
|
|
632
|
-
label: "
|
|
633
|
-
|
|
634
|
-
handler: (v) => insertAtLineStart(v, "##### ")
|
|
876
|
+
label: "H5",
|
|
877
|
+
handler: (v) => toggleHeading(v, 5)
|
|
635
878
|
},
|
|
636
879
|
{
|
|
637
880
|
id: "heading6",
|
|
638
|
-
label: "
|
|
639
|
-
|
|
640
|
-
handler: (v) => insertAtLineStart(v, "###### ")
|
|
881
|
+
label: "H6",
|
|
882
|
+
handler: (v) => toggleHeading(v, 6)
|
|
641
883
|
}
|
|
642
884
|
]
|
|
643
885
|
},
|
|
@@ -646,38 +888,44 @@ function createToolbarConfig(options) {
|
|
|
646
888
|
id: "bold",
|
|
647
889
|
label: "加粗",
|
|
648
890
|
icon: _ant_design_icons.BoldOutlined,
|
|
649
|
-
handler: (v) =>
|
|
891
|
+
handler: (v) => toggleWrap(v, "**", "**")
|
|
650
892
|
},
|
|
651
893
|
{
|
|
652
894
|
id: "italic",
|
|
653
895
|
label: "斜体",
|
|
654
896
|
icon: _ant_design_icons.ItalicOutlined,
|
|
655
|
-
handler: (v) =>
|
|
897
|
+
handler: (v) => toggleWrap(v, "*", "*")
|
|
656
898
|
},
|
|
657
899
|
{
|
|
658
900
|
id: "strikethrough",
|
|
659
901
|
label: "删除线",
|
|
660
902
|
icon: _ant_design_icons.StrikethroughOutlined,
|
|
661
|
-
handler: (v) =>
|
|
903
|
+
handler: (v) => toggleWrap(v, "~~", "~~")
|
|
662
904
|
},
|
|
663
905
|
{
|
|
664
906
|
id: "underline",
|
|
665
907
|
label: "下划线",
|
|
666
908
|
icon: _ant_design_icons.UnderlineOutlined,
|
|
667
|
-
handler: (v) =>
|
|
909
|
+
handler: (v) => toggleWrap(v, "<u>", "</u>")
|
|
668
910
|
},
|
|
669
911
|
{ separatorBefore: true },
|
|
670
912
|
{
|
|
671
913
|
id: "ul",
|
|
672
914
|
label: "无序列表",
|
|
673
915
|
icon: _ant_design_icons.UnorderedListOutlined,
|
|
674
|
-
handler: (v) =>
|
|
916
|
+
handler: (v) => toggleLinePrefix(v, /^(\s*)-(?!\s*\[)\s/, "- ")
|
|
675
917
|
},
|
|
676
918
|
{
|
|
677
919
|
id: "ol",
|
|
678
920
|
label: "有序列表",
|
|
679
921
|
icon: _ant_design_icons.OrderedListOutlined,
|
|
680
|
-
handler: (v) =>
|
|
922
|
+
handler: (v) => toggleLinePrefix(v, /^(\s*)\d+\.\s/, "1. ")
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
id: "checkbox",
|
|
926
|
+
label: "待办",
|
|
927
|
+
icon: _ant_design_icons.CheckSquareOutlined,
|
|
928
|
+
handler: (v) => toggleTaskList(v)
|
|
681
929
|
},
|
|
682
930
|
{ separatorBefore: true },
|
|
683
931
|
{
|
|
@@ -688,19 +936,15 @@ function createToolbarConfig(options) {
|
|
|
688
936
|
},
|
|
689
937
|
{
|
|
690
938
|
id: "image",
|
|
691
|
-
label:
|
|
939
|
+
label: "图片",
|
|
692
940
|
icon: _ant_design_icons.PictureOutlined,
|
|
693
|
-
|
|
694
|
-
handler: hasUploadConfig ? (v) => uploadImage(v, {
|
|
695
|
-
uploadUrl: options?.uploadUrl,
|
|
696
|
-
onUploadImage: options?.onUploadImage
|
|
697
|
-
}) : void 0
|
|
941
|
+
handler: hasUploadConfig && options ? (v) => uploadImage(v, options) : () => antd.message.warning(UPLOAD_CONFIG_MISSING_MESSAGE)
|
|
698
942
|
},
|
|
699
943
|
{ separatorBefore: true },
|
|
700
944
|
{
|
|
701
945
|
id: "code",
|
|
702
946
|
label: "行内代码",
|
|
703
|
-
icon:
|
|
947
|
+
icon: InlineCode,
|
|
704
948
|
handler: (v) => wrapSelection(v, "`", "`")
|
|
705
949
|
},
|
|
706
950
|
{
|
|
@@ -721,7 +965,7 @@ function createToolbarConfig(options) {
|
|
|
721
965
|
{
|
|
722
966
|
id: "quote",
|
|
723
967
|
label: "引用",
|
|
724
|
-
icon:
|
|
968
|
+
icon: Quote,
|
|
725
969
|
handler: (v) => insertAtLineStart(v, "> ")
|
|
726
970
|
},
|
|
727
971
|
{
|
|
@@ -792,10 +1036,7 @@ function ToolbarItemRenderer({ item, onClick }) {
|
|
|
792
1036
|
className: "toolbar-button",
|
|
793
1037
|
type: "text",
|
|
794
1038
|
size: "small",
|
|
795
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_ant_design_icons.DownOutlined, { style: {
|
|
796
|
-
marginLeft: 2,
|
|
797
|
-
fontSize: 10
|
|
798
|
-
} })]
|
|
1039
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_ant_design_icons.DownOutlined, { style: { fontSize: 10 } })]
|
|
799
1040
|
})
|
|
800
1041
|
})
|
|
801
1042
|
})
|
|
@@ -1120,6 +1361,9 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
|
|
|
1120
1361
|
const schema = componentRegistry.get(typeName);
|
|
1121
1362
|
const [formProps, setFormProps] = (0, react.useState)({});
|
|
1122
1363
|
const [form] = antd.Form.useForm();
|
|
1364
|
+
const onSaveRef = (0, react.useRef)(onSave);
|
|
1365
|
+
onSaveRef.current = onSave;
|
|
1366
|
+
const dirtyRef = (0, react.useRef)(false);
|
|
1123
1367
|
const mergedProps = (0, react.useMemo)(() => {
|
|
1124
1368
|
if (!schema) return currentProps;
|
|
1125
1369
|
const defaults = {};
|
|
@@ -1157,18 +1401,28 @@ function EditDialog({ visible, typeName, currentProps, onSave, onClose }) {
|
|
|
1157
1401
|
...formValues
|
|
1158
1402
|
};
|
|
1159
1403
|
} catch {}
|
|
1160
|
-
|
|
1404
|
+
onSaveRef.current(finalProps);
|
|
1405
|
+
dirtyRef.current = false;
|
|
1406
|
+
onClose();
|
|
1161
1407
|
};
|
|
1162
1408
|
const handleClose = () => {
|
|
1163
1409
|
setFormProps({});
|
|
1410
|
+
dirtyRef.current = false;
|
|
1164
1411
|
onClose();
|
|
1165
1412
|
};
|
|
1166
1413
|
if (!schema) return null;
|
|
1414
|
+
const contextSave = (props) => {
|
|
1415
|
+
setFormProps((prev) => ({
|
|
1416
|
+
...prev,
|
|
1417
|
+
...props
|
|
1418
|
+
}));
|
|
1419
|
+
dirtyRef.current = true;
|
|
1420
|
+
};
|
|
1167
1421
|
const context = {
|
|
1168
1422
|
props: mergedProps,
|
|
1169
1423
|
form,
|
|
1170
1424
|
onChange: handleDialogChange,
|
|
1171
|
-
onSave
|
|
1425
|
+
onSave: contextSave
|
|
1172
1426
|
};
|
|
1173
1427
|
const renderDialogContent = () => {
|
|
1174
1428
|
if (schema.renderDialog) return schema.renderDialog(context);
|
|
@@ -1254,38 +1508,55 @@ function useFullscreen() {
|
|
|
1254
1508
|
//#endregion
|
|
1255
1509
|
//#region src/hooks/useScrollSync.ts
|
|
1256
1510
|
/**
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1511
|
+
* 双向滚动同步 Hook
|
|
1512
|
+
* 编辑器与预览区互相同步滚动位置(基于比例)
|
|
1513
|
+
* 使用 síncing 锁防止双向循环触发
|
|
1259
1514
|
*/
|
|
1260
1515
|
function useScrollSync(sourceRef, targetRef) {
|
|
1261
1516
|
const rafIdRef = (0, react.useRef)(null);
|
|
1517
|
+
const syncingRef = (0, react.useRef)(null);
|
|
1518
|
+
const syncScroll = (from, fromEl, toEl) => {
|
|
1519
|
+
if (rafIdRef.current) return;
|
|
1520
|
+
rafIdRef.current = requestAnimationFrame(() => {
|
|
1521
|
+
const fromScrollHeight = fromEl.scrollHeight - fromEl.clientHeight;
|
|
1522
|
+
if (fromScrollHeight <= 0) {
|
|
1523
|
+
rafIdRef.current = null;
|
|
1524
|
+
syncingRef.current = null;
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const ratio = fromEl.scrollTop / fromScrollHeight;
|
|
1528
|
+
const toScrollHeight = toEl.scrollHeight - toEl.clientHeight;
|
|
1529
|
+
if (toScrollHeight > 0) {
|
|
1530
|
+
syncingRef.current = from;
|
|
1531
|
+
toEl.scrollTop = ratio * toScrollHeight;
|
|
1532
|
+
requestAnimationFrame(() => {
|
|
1533
|
+
syncingRef.current = null;
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
rafIdRef.current = null;
|
|
1537
|
+
});
|
|
1538
|
+
};
|
|
1262
1539
|
(0, react.useEffect)(() => {
|
|
1263
1540
|
const source = sourceRef.current;
|
|
1264
1541
|
const target = targetRef.current;
|
|
1265
1542
|
if (!source || !target) return;
|
|
1266
|
-
const
|
|
1267
|
-
if (
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (!s || !t) {
|
|
1272
|
-
rafIdRef.current = null;
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
const sourceScrollHeight = s.scrollHeight - s.clientHeight;
|
|
1276
|
-
if (sourceScrollHeight <= 0) {
|
|
1277
|
-
rafIdRef.current = null;
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
const ratio = s.scrollTop / sourceScrollHeight;
|
|
1281
|
-
const targetScrollHeight = t.scrollHeight - t.clientHeight;
|
|
1282
|
-
if (targetScrollHeight > 0) t.scrollTop = ratio * targetScrollHeight;
|
|
1283
|
-
rafIdRef.current = null;
|
|
1284
|
-
});
|
|
1543
|
+
const onSourceScroll = () => {
|
|
1544
|
+
if (syncingRef.current !== null) return;
|
|
1545
|
+
const s = sourceRef.current;
|
|
1546
|
+
const t = targetRef.current;
|
|
1547
|
+
syncScroll("source", s, t);
|
|
1285
1548
|
};
|
|
1286
|
-
|
|
1549
|
+
const onTargetScroll = () => {
|
|
1550
|
+
if (syncingRef.current !== null) return;
|
|
1551
|
+
const s = sourceRef.current;
|
|
1552
|
+
const t = targetRef.current;
|
|
1553
|
+
syncScroll("target", t, s);
|
|
1554
|
+
};
|
|
1555
|
+
source.addEventListener("scroll", onSourceScroll, { passive: true });
|
|
1556
|
+
target.addEventListener("scroll", onTargetScroll, { passive: true });
|
|
1287
1557
|
return () => {
|
|
1288
|
-
source.removeEventListener("scroll",
|
|
1558
|
+
source.removeEventListener("scroll", onSourceScroll);
|
|
1559
|
+
target.removeEventListener("scroll", onTargetScroll);
|
|
1289
1560
|
if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
|
|
1290
1561
|
};
|
|
1291
1562
|
}, [sourceRef, targetRef]);
|
|
@@ -1343,7 +1614,6 @@ var MarkdownEditor_default = (0, react.forwardRef)(function MarkdownEditor({ def
|
|
|
1343
1614
|
setDialogVisible(true);
|
|
1344
1615
|
}, []);
|
|
1345
1616
|
const handleDialogSave = (0, react.useCallback)((newProps) => {
|
|
1346
|
-
setDialogVisible(false);
|
|
1347
1617
|
if (editorView) updateContainerProps(editorView, dialogPropsRange[0], dialogPropsRange[1], newProps);
|
|
1348
1618
|
}, [editorView, dialogPropsRange]);
|
|
1349
1619
|
const handleDialogClose = (0, react.useCallback)(() => {
|