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.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 { BlockOutlined, BoldOutlined, CodeOutlined, DownOutlined, FieldStringOutlined, FontSizeOutlined, FullscreenExitOutlined, FullscreenOutlined, ItalicOutlined, LinkOutlined, MinusOutlined, OrderedListOutlined, PictureOutlined, ReadOutlined, StrikethroughOutlined, TableOutlined, UnderlineOutlined, UnorderedListOutlined } from "@ant-design/icons";
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: { click: (event, view) => {
251
- const target = event.target;
252
- if (target.classList.contains("container-edit-btn")) {
253
- if (target.__typeName) {
254
- const containers = findContainers(view.state.doc.toString());
255
- const buttonPos = view.posAtDOM(target);
256
- for (const container of containers) {
257
- const headerLine = view.state.doc.lineAt(container.startPos);
258
- if (buttonPos >= headerLine.from - 5 && buttonPos <= headerLine.to + 5) {
259
- const callback = view.state.facet(containerEditCallback);
260
- if (callback) callback(view, container.typeName, container.propsLineFrom, container.propsLineTo, { ...container.props });
261
- return true;
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
- return false;
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([...defaultKeymap, indentWithTab]),
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: "标题 1",
584
- icon: FontSizeOutlined,
585
- handler: (v) => insertAtLineStart(v, "# ")
831
+ label: "H1",
832
+ handler: (v) => toggleHeading(v, 1)
586
833
  },
587
834
  {
588
835
  id: "heading2",
589
- label: "标题 2",
590
- icon: FontSizeOutlined,
591
- handler: (v) => insertAtLineStart(v, "## ")
836
+ label: "H2",
837
+ handler: (v) => toggleHeading(v, 2)
592
838
  },
593
839
  {
594
840
  id: "heading3",
595
- label: "标题 3",
596
- icon: FontSizeOutlined,
597
- handler: (v) => insertAtLineStart(v, "### ")
841
+ label: "H3",
842
+ handler: (v) => toggleHeading(v, 3)
598
843
  },
599
844
  {
600
845
  id: "heading4",
601
- label: "标题 4",
602
- icon: FontSizeOutlined,
603
- handler: (v) => insertAtLineStart(v, "#### ")
846
+ label: "H4",
847
+ handler: (v) => toggleHeading(v, 4)
604
848
  },
605
849
  {
606
850
  id: "heading5",
607
- label: "标题 5",
608
- icon: FontSizeOutlined,
609
- handler: (v) => insertAtLineStart(v, "##### ")
851
+ label: "H5",
852
+ handler: (v) => toggleHeading(v, 5)
610
853
  },
611
854
  {
612
855
  id: "heading6",
613
- label: "标题 6",
614
- icon: FontSizeOutlined,
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) => wrapSelection(v, "**", "**")
866
+ handler: (v) => toggleWrap(v, "**", "**")
625
867
  },
626
868
  {
627
869
  id: "italic",
628
870
  label: "斜体",
629
871
  icon: ItalicOutlined,
630
- handler: (v) => wrapSelection(v, "*", "*")
872
+ handler: (v) => toggleWrap(v, "*", "*")
631
873
  },
632
874
  {
633
875
  id: "strikethrough",
634
876
  label: "删除线",
635
877
  icon: StrikethroughOutlined,
636
- handler: (v) => wrapSelection(v, "~~", "~~")
878
+ handler: (v) => toggleWrap(v, "~~", "~~")
637
879
  },
638
880
  {
639
881
  id: "underline",
640
882
  label: "下划线",
641
883
  icon: UnderlineOutlined,
642
- handler: (v) => wrapSelection(v, "<u>", "</u>")
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) => insertAtLineStart(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) => insertAtLineStart(v, "1. ")
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: hasUploadConfig ? "图片" : "请配置图片上传地址",
914
+ label: "图片",
667
915
  icon: PictureOutlined,
668
- disabled: !hasUploadConfig,
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: FieldStringOutlined,
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: BlockOutlined,
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
- onSave(finalProps);
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
- * 滚动同步 Hook
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 listener = () => {
1242
- if (rafIdRef.current) return;
1243
- rafIdRef.current = requestAnimationFrame(() => {
1244
- const s = sourceRef.current;
1245
- const t = targetRef.current;
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
- source.addEventListener("scroll", listener, { passive: true });
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", listener);
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(() => {