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/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: { 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;
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
- return false;
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([..._codemirror_commands.defaultKeymap, _codemirror_commands.indentWithTab]),
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: "标题 1",
609
- icon: _ant_design_icons.FontSizeOutlined,
610
- handler: (v) => insertAtLineStart(v, "# ")
856
+ label: "H1",
857
+ handler: (v) => toggleHeading(v, 1)
611
858
  },
612
859
  {
613
860
  id: "heading2",
614
- label: "标题 2",
615
- icon: _ant_design_icons.FontSizeOutlined,
616
- handler: (v) => insertAtLineStart(v, "## ")
861
+ label: "H2",
862
+ handler: (v) => toggleHeading(v, 2)
617
863
  },
618
864
  {
619
865
  id: "heading3",
620
- label: "标题 3",
621
- icon: _ant_design_icons.FontSizeOutlined,
622
- handler: (v) => insertAtLineStart(v, "### ")
866
+ label: "H3",
867
+ handler: (v) => toggleHeading(v, 3)
623
868
  },
624
869
  {
625
870
  id: "heading4",
626
- label: "标题 4",
627
- icon: _ant_design_icons.FontSizeOutlined,
628
- handler: (v) => insertAtLineStart(v, "#### ")
871
+ label: "H4",
872
+ handler: (v) => toggleHeading(v, 4)
629
873
  },
630
874
  {
631
875
  id: "heading5",
632
- label: "标题 5",
633
- icon: _ant_design_icons.FontSizeOutlined,
634
- handler: (v) => insertAtLineStart(v, "##### ")
876
+ label: "H5",
877
+ handler: (v) => toggleHeading(v, 5)
635
878
  },
636
879
  {
637
880
  id: "heading6",
638
- label: "标题 6",
639
- icon: _ant_design_icons.FontSizeOutlined,
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) => wrapSelection(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) => wrapSelection(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) => wrapSelection(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) => wrapSelection(v, "<u>", "</u>")
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) => insertAtLineStart(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) => insertAtLineStart(v, "1. ")
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: hasUploadConfig ? "图片" : "请配置图片上传地址",
939
+ label: "图片",
692
940
  icon: _ant_design_icons.PictureOutlined,
693
- disabled: !hasUploadConfig,
694
- handler: hasUploadConfig ? (v) => uploadImage(v, {
695
- uploadUrl: options?.uploadUrl,
696
- onUploadImage: options?.onUploadImage
697
- }) : void 0
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: _ant_design_icons.FieldStringOutlined,
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: _ant_design_icons.BlockOutlined,
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
- onSave(finalProps);
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
- * 滚动同步 Hook
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 listener = () => {
1267
- if (rafIdRef.current) return;
1268
- rafIdRef.current = requestAnimationFrame(() => {
1269
- const s = sourceRef.current;
1270
- const t = targetRef.current;
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
- source.addEventListener("scroll", listener, { passive: true });
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", listener);
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)(() => {