@overlap/rte 1.0.15 → 1.0.17

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
@@ -232,32 +232,6 @@ function findClosestListItem(node) {
232
232
  : node;
233
233
  return element?.closest("li") || null;
234
234
  }
235
- /**
236
- * Sets cursor position in a text node after an async DOM update.
237
- */
238
- function setCursorInTextNode(textNode, position, editor) {
239
- requestAnimationFrame(() => {
240
- requestAnimationFrame(() => {
241
- try {
242
- const range = document.createRange();
243
- const maxPos = textNode.textContent?.length || 0;
244
- const safePos = Math.min(Math.max(0, position), maxPos);
245
- range.setStart(textNode, safePos);
246
- range.collapse(true);
247
- const selection = window.getSelection();
248
- if (selection) {
249
- selection.removeAllRanges();
250
- selection.addRange(range);
251
- if (editor)
252
- editor.focus();
253
- }
254
- }
255
- catch (_) {
256
- // Silently fail - cursor positioning is best-effort
257
- }
258
- });
259
- });
260
- }
261
235
 
262
236
  /**
263
237
  * Pure checkbox utility functions for checkbox lists.
@@ -592,21 +566,39 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
592
566
  }
593
567
  return true;
594
568
  }
595
- // Normal case: create a new checkbox item
569
+ // Normal case: split content at cursor and create a new checkbox item
570
+ const afterRange = document.createRange();
571
+ afterRange.setStart(range.startContainer, range.startOffset);
572
+ if (listItem.lastChild) {
573
+ afterRange.setEndAfter(listItem.lastChild);
574
+ }
575
+ else {
576
+ afterRange.setEnd(listItem, listItem.childNodes.length);
577
+ }
578
+ const afterFragment = afterRange.extractContents();
596
579
  const newLi = document.createElement("li");
597
580
  updateListItemChecked(newLi, false);
598
- const textNode = document.createTextNode(" ");
599
- newLi.appendChild(textNode);
581
+ const hasContent = afterFragment.textContent?.trim();
582
+ if (hasContent) {
583
+ newLi.appendChild(afterFragment);
584
+ }
585
+ else {
586
+ newLi.appendChild(document.createTextNode(" "));
587
+ }
600
588
  if (listItem.nextSibling) {
601
589
  checkboxList.insertBefore(newLi, listItem.nextSibling);
602
590
  }
603
591
  else {
604
592
  checkboxList.appendChild(newLi);
605
593
  }
594
+ if (!listItem.firstChild) {
595
+ listItem.appendChild(document.createTextNode(" "));
596
+ }
606
597
  if (editor)
607
598
  ensureAllCheckboxes(editor);
599
+ const cursorNode = newLi.firstChild || newLi;
608
600
  const newRange = document.createRange();
609
- newRange.setStart(textNode, 0);
601
+ newRange.setStart(cursorNode, 0);
610
602
  newRange.collapse(true);
611
603
  selection.removeAllRanges();
612
604
  selection.addRange(newRange);
@@ -638,8 +630,16 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
638
630
  isUpdatingRef.current = false;
639
631
  return false;
640
632
  }
641
- // Already in a checkbox list? Remove it.
642
- const existingList = findClosestCheckboxList(element);
633
+ // Resolve the element at the start of the selection as well,
634
+ // because when selecting across multiple list items the
635
+ // commonAncestorContainer is the editor root (above the list).
636
+ const startNode = range.startContainer;
637
+ const startElement = startNode.nodeType === Node.TEXT_NODE
638
+ ? startNode.parentElement
639
+ : startNode;
640
+ // Already in a checkbox list? Remove it (convert back to bullet list).
641
+ const existingList = findClosestCheckboxList(element) ||
642
+ (startElement ? findClosestCheckboxList(startElement) : null);
643
643
  if (existingList) {
644
644
  existingList.classList.remove("rte-checkbox-list");
645
645
  existingList
@@ -652,48 +652,65 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
652
652
  isUpdatingRef.current = false;
653
653
  return true;
654
654
  }
655
- // Create new checkbox list
656
- const ul = document.createElement("ul");
657
- ul.classList.add("rte-checkbox-list");
658
- const li = document.createElement("li");
659
- updateListItemChecked(li, false);
660
- const textNode = document.createTextNode(" ");
661
- li.appendChild(textNode);
662
- ul.appendChild(li);
663
- // Find block element to replace
664
- const blockElement = element.closest("p, div, h1, h2, h3, h4, h5, h6, blockquote");
665
- const isValidBlockElement = blockElement &&
666
- blockElement !== editor &&
667
- editor.contains(blockElement) &&
668
- blockElement.parentElement;
669
- if (isValidBlockElement) {
670
- const textContent = blockElement.textContent || "";
671
- blockElement.parentElement.replaceChild(ul, blockElement);
672
- const finalTextNode = li.firstChild;
673
- if (finalTextNode) {
674
- finalTextNode.textContent = textContent || " ";
675
- const cursorPos = textContent ? textContent.length : 0;
676
- setCursorInTextNode(finalTextNode, cursorPos, editor);
677
- }
655
+ // Already in a <ul> (bullet list)? Convert in-place to checkbox.
656
+ const existingUl = element.closest("ul") ||
657
+ startElement?.closest("ul");
658
+ if (existingUl && editor.contains(existingUl)) {
659
+ existingUl.classList.add("rte-checkbox-list");
660
+ existingUl.querySelectorAll(":scope > li").forEach((li) => {
661
+ updateListItemChecked(li, false);
662
+ });
663
+ ensureAllCheckboxes(editor);
664
+ isUpdatingRef.current = false;
665
+ const content = getDomContent();
666
+ pushToHistory(content);
667
+ notifyChange(content);
668
+ return true;
678
669
  }
679
- else {
680
- try {
681
- range.deleteContents();
682
- range.insertNode(ul);
683
- const finalTextNode = li.firstChild;
684
- if (finalTextNode) {
685
- setCursorInTextNode(finalTextNode, 0, editor);
686
- }
670
+ // Already in an <ol> (numbered list)? Convert to <ul> first,
671
+ // then make it a checkbox list.
672
+ const existingOl = element.closest("ol") ||
673
+ startElement?.closest("ol");
674
+ if (existingOl && editor.contains(existingOl)) {
675
+ const ul = document.createElement("ul");
676
+ ul.classList.add("rte-checkbox-list");
677
+ while (existingOl.firstChild) {
678
+ ul.appendChild(existingOl.firstChild);
687
679
  }
688
- catch (_) {
689
- editor.appendChild(ul);
690
- const finalTextNode = li.firstChild;
691
- if (finalTextNode) {
692
- setCursorInTextNode(finalTextNode, 0, editor);
693
- }
680
+ existingOl.parentNode?.replaceChild(ul, existingOl);
681
+ ul.querySelectorAll(":scope > li").forEach((li) => {
682
+ updateListItemChecked(li, false);
683
+ });
684
+ ensureAllCheckboxes(editor);
685
+ isUpdatingRef.current = false;
686
+ const content = getDomContent();
687
+ pushToHistory(content);
688
+ notifyChange(content);
689
+ return true;
690
+ }
691
+ // Not in any list: use the browser's insertUnorderedList command
692
+ // to properly handle single paragraphs and multi-paragraph selections,
693
+ // then convert the resulting <ul> to a checkbox list.
694
+ if (document.activeElement !== editor) {
695
+ editor.focus();
696
+ }
697
+ document.execCommand("insertUnorderedList", false);
698
+ // Find the <ul> the cursor is now inside
699
+ const sel = window.getSelection();
700
+ if (sel && sel.rangeCount > 0) {
701
+ const r = sel.getRangeAt(0);
702
+ const node = r.commonAncestorContainer.nodeType === Node.TEXT_NODE
703
+ ? r.commonAncestorContainer.parentElement
704
+ : r.commonAncestorContainer;
705
+ const newUl = node?.closest("ul");
706
+ if (newUl && editor.contains(newUl)) {
707
+ newUl.classList.add("rte-checkbox-list");
708
+ newUl.querySelectorAll(":scope > li").forEach((li) => {
709
+ updateListItemChecked(li, false);
710
+ });
694
711
  }
695
712
  }
696
- // After insertion: ensure attributes and save to history
713
+ // Finalize: ensure attributes and save to history
697
714
  setTimeout(() => {
698
715
  if (!editor)
699
716
  return;
@@ -2656,18 +2673,25 @@ function createBlockFormatPlugin(headings = defaultHeadings$2, blockOptions = {}
2656
2673
  : container;
2657
2674
  if (!element)
2658
2675
  return undefined;
2676
+ // When the selection spans multiple blocks, commonAncestorContainer
2677
+ // may be the editor root. Fall back to startContainer to detect
2678
+ // formats from an element that is actually inside the content.
2679
+ const startNode = range.startContainer;
2680
+ const startEl = startNode.nodeType === Node.TEXT_NODE
2681
+ ? startNode.parentElement
2682
+ : startNode;
2659
2683
  const tagName = element.tagName.toLowerCase();
2660
2684
  if (headings.includes(tagName))
2661
2685
  return tagName;
2662
- if (element.closest("pre"))
2686
+ if (element.closest("pre") || startEl?.closest("pre"))
2663
2687
  return "code";
2664
- if (element.closest("blockquote"))
2688
+ if (element.closest("blockquote") || startEl?.closest("blockquote"))
2665
2689
  return "blockquote";
2666
- if (findClosestCheckboxList(element))
2690
+ if (findClosestCheckboxList(element) || (startEl && findClosestCheckboxList(startEl)))
2667
2691
  return "checkbox-list";
2668
- if (element.closest("ul"))
2692
+ if (element.closest("ul") || startEl?.closest("ul"))
2669
2693
  return "ul";
2670
- if (element.closest("ol"))
2694
+ if (element.closest("ol") || startEl?.closest("ol"))
2671
2695
  return "ol";
2672
2696
  if (tagName === "p")
2673
2697
  return "p";
@@ -2703,6 +2727,32 @@ function createBlockFormatPlugin(headings = defaultHeadings$2, blockOptions = {}
2703
2727
  ? container.parentElement
2704
2728
  : container;
2705
2729
  };
2730
+ // When the selection spans multiple blocks, commonAncestorContainer
2731
+ // is the editor root. Use startContainer to reach elements inside
2732
+ // the actual selected content.
2733
+ const getStartElement = () => {
2734
+ const sel = editor.getSelection();
2735
+ if (!sel || sel.rangeCount === 0)
2736
+ return null;
2737
+ const start = sel.getRangeAt(0).startContainer;
2738
+ return start.nodeType === Node.TEXT_NODE
2739
+ ? start.parentElement
2740
+ : start;
2741
+ };
2742
+ const stripCheckboxAttributes = (list) => {
2743
+ list.classList.remove("rte-checkbox-list");
2744
+ list.querySelectorAll("li[role='checkbox']").forEach((li) => {
2745
+ li.removeAttribute("role");
2746
+ li.removeAttribute("tabIndex");
2747
+ li.removeAttribute("aria-checked");
2748
+ });
2749
+ };
2750
+ const findCheckboxInSelection = () => {
2751
+ const el = getCursorElement();
2752
+ const startEl = getStartElement();
2753
+ return ((el ? findClosestCheckboxList(el) : null) ||
2754
+ (startEl ? findClosestCheckboxList(startEl) : null));
2755
+ };
2706
2756
  // Helper: merge all adjacent <pre> elements in the editor into one
2707
2757
  const mergeAdjacentPre = () => {
2708
2758
  const root = document.activeElement;
@@ -2726,20 +2776,12 @@ function createBlockFormatPlugin(headings = defaultHeadings$2, blockOptions = {}
2726
2776
  // Helper: if cursor is inside a list, remove the list first
2727
2777
  const escapeListIfNeeded = () => {
2728
2778
  const el = getCursorElement();
2729
- if (!el)
2730
- return;
2731
- const inCheckbox = findClosestCheckboxList(el);
2732
- const inUl = el.closest("ul");
2733
- const inOl = el.closest("ol");
2779
+ const startEl = getStartElement();
2780
+ const inCheckbox = findCheckboxInSelection();
2781
+ const inUl = el?.closest("ul") || startEl?.closest("ul");
2782
+ const inOl = el?.closest("ol") || startEl?.closest("ol");
2734
2783
  if (inCheckbox) {
2735
- inCheckbox.classList.remove("rte-checkbox-list");
2736
- inCheckbox
2737
- .querySelectorAll("li[role='checkbox']")
2738
- .forEach((li) => {
2739
- li.removeAttribute("role");
2740
- li.removeAttribute("tabIndex");
2741
- li.removeAttribute("aria-checked");
2742
- });
2784
+ stripCheckboxAttributes(inCheckbox);
2743
2785
  editor.executeCommand("insertUnorderedList");
2744
2786
  }
2745
2787
  else if (inUl) {
@@ -2750,29 +2792,36 @@ function createBlockFormatPlugin(headings = defaultHeadings$2, blockOptions = {}
2750
2792
  }
2751
2793
  };
2752
2794
  if (value === "checkbox-list") {
2753
- const el = getCursorElement();
2754
- if (!el)
2755
- return;
2756
- const checkboxList = findClosestCheckboxList(el);
2795
+ const checkboxList = findCheckboxInSelection();
2757
2796
  if (checkboxList) {
2758
- checkboxList.classList.remove("rte-checkbox-list");
2759
- checkboxList
2760
- .querySelectorAll("li[role='checkbox']")
2761
- .forEach((li) => {
2762
- li.removeAttribute("role");
2763
- li.removeAttribute("tabIndex");
2764
- li.removeAttribute("aria-checked");
2765
- });
2797
+ stripCheckboxAttributes(checkboxList);
2766
2798
  }
2767
2799
  else {
2768
2800
  editor.executeCommand("insertCheckboxList");
2769
2801
  }
2770
2802
  }
2771
2803
  else if (value === "ul") {
2772
- editor.executeCommand("insertUnorderedList");
2804
+ const checkboxList = findCheckboxInSelection();
2805
+ if (checkboxList) {
2806
+ stripCheckboxAttributes(checkboxList);
2807
+ }
2808
+ else {
2809
+ editor.executeCommand("insertUnorderedList");
2810
+ }
2773
2811
  }
2774
2812
  else if (value === "ol") {
2775
- editor.executeCommand("insertOrderedList");
2813
+ const checkboxList = findCheckboxInSelection();
2814
+ if (checkboxList) {
2815
+ stripCheckboxAttributes(checkboxList);
2816
+ const ol = document.createElement("ol");
2817
+ while (checkboxList.firstChild) {
2818
+ ol.appendChild(checkboxList.firstChild);
2819
+ }
2820
+ checkboxList.parentNode?.replaceChild(ol, checkboxList);
2821
+ }
2822
+ else {
2823
+ editor.executeCommand("insertOrderedList");
2824
+ }
2776
2825
  }
2777
2826
  else if (value === "blockquote") {
2778
2827
  const el = getCursorElement();
@@ -2812,13 +2861,17 @@ function createBlockFormatPlugin(headings = defaultHeadings$2, blockOptions = {}
2812
2861
  : container;
2813
2862
  if (!element)
2814
2863
  return false;
2864
+ const startNode = range.startContainer;
2865
+ const startEl = startNode.nodeType === Node.TEXT_NODE
2866
+ ? startNode.parentElement
2867
+ : startNode;
2815
2868
  const tagName = element.tagName.toLowerCase();
2816
2869
  return (headings.includes(tagName) ||
2817
- element.closest("pre") !== null ||
2818
- element.closest("blockquote") !== null ||
2819
- findClosestCheckboxList(element) !== null ||
2820
- element.closest("ul") !== null ||
2821
- element.closest("ol") !== null);
2870
+ element.closest("pre") !== null || startEl?.closest("pre") !== null ||
2871
+ element.closest("blockquote") !== null || startEl?.closest("blockquote") !== null ||
2872
+ findClosestCheckboxList(element) !== null || (startEl != null && findClosestCheckboxList(startEl) !== null) ||
2873
+ element.closest("ul") !== null || startEl?.closest("ul") !== null ||
2874
+ element.closest("ol") !== null || startEl?.closest("ol") !== null);
2822
2875
  },
2823
2876
  canExecute: () => true,
2824
2877
  };
@@ -3938,9 +3991,11 @@ function applyLinkData(link, data, customFields) {
3938
3991
  // Target
3939
3992
  if (data.target && data.target !== "_self") {
3940
3993
  link.setAttribute("target", data.target);
3994
+ link.setAttribute("rel", "noopener noreferrer");
3941
3995
  }
3942
3996
  else {
3943
3997
  link.removeAttribute("target");
3998
+ link.removeAttribute("rel");
3944
3999
  }
3945
4000
  // Custom field data-attributes
3946
4001
  for (const field of customFields) {
@@ -4010,9 +4065,10 @@ const LinkToolbarButton = (props) => {
4010
4065
  e.stopPropagation();
4011
4066
  const href = link.getAttribute("href");
4012
4067
  if (href && isUrlSafe(href))
4013
- window.open(href, "_blank");
4068
+ window.open(href, "_blank", "noopener,noreferrer");
4014
4069
  return;
4015
4070
  }
4071
+ e.preventDefault();
4016
4072
  if (isOpenRef.current)
4017
4073
  return;
4018
4074
  setTimeout(() => {