leksy-editor 2.2.0 → 2.2.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/utilities.js +231 -155
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leksy-editor",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Leksy Editor is an alternative to traditional WYSIWYG editors, designed primarily for creating mail templates, blogs, and documents without any content manipulation.",
5
5
  "main": "index.js",
6
6
  "directories": {
package/utilities.js CHANGED
@@ -3648,6 +3648,7 @@ const findAndReplace = (core, options, event) => {
3648
3648
  const matchCase = matchCaseCheckbox.checked;
3649
3649
  const useRegex = useRegexCheckbox.checked;
3650
3650
  const wholeWord = wholeWordCheckbox.checked;
3651
+
3651
3652
  span.innerText = "";
3652
3653
  countSpan.innerText = "";
3653
3654
 
@@ -3656,129 +3657,172 @@ const findAndReplace = (core, options, event) => {
3656
3657
  return;
3657
3658
  }
3658
3659
 
3659
- let textContent = editor.textContent;
3660
- let matchIndex = -1;
3661
- let matchLength = 0;
3662
-
3663
3660
  let searchPattern = searchText;
3664
3661
 
3665
3662
  if (!useRegex) {
3666
- const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3667
- if (wholeWord) {
3668
- searchPattern = `\\b${escaped}\\b`;
3669
- } else {
3670
- searchPattern = escaped;
3671
- }
3663
+ const cleaned = wholeWord ? searchText.trim() : searchText;
3664
+ const escaped = cleaned.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3665
+
3666
+ searchPattern = wholeWord
3667
+ ? `(?<!\\w)${escaped}(?!\\w)`
3668
+ : escaped;
3672
3669
  }
3673
3670
 
3674
3671
  const selection = iframeWindow.getSelection();
3675
3672
 
3673
+ let regex;
3676
3674
  try {
3677
3675
  const flags = matchCase ? 'g' : 'gi';
3678
- const regex = new RegExp(searchPattern, flags);
3679
- const matches = [...textContent.matchAll(regex)];
3680
-
3681
- if (matches.length > 0) {
3682
- let startIndex = 0;
3683
- if (selection.rangeCount > 0) {
3684
- const range = selection.getRangeAt(0);
3685
- if (direction === 'next') {
3686
- startIndex = getCharIndex(editor, range.endContainer, range.endOffset);
3687
- } else {
3688
- startIndex = getCharIndex(editor, range.startContainer, range.startOffset);
3689
- }
3690
- }
3676
+ regex = new RegExp(searchPattern, flags);
3677
+ } catch (error) {
3678
+ span.innerText = "Invalid regular expression";
3679
+ return;
3680
+ }
3691
3681
 
3692
- let match;
3693
- if (direction === 'next') {
3694
- match = matches.find(m => m.index >= startIndex);
3695
- if (!match) match = matches[0];
3696
- } else {
3697
- for (let i = matches.length - 1; i >= 0; i--) {
3698
- if (matches[i].index < startIndex) {
3699
- match = matches[i];
3700
- break;
3701
- }
3702
- }
3703
- if (!match) match = matches[matches.length - 1];
3704
- }
3682
+ // ✅ Collect matches (node-based + global index)
3683
+ const walker = core.elements.iframeWindow.createTreeWalker(
3684
+ editor,
3685
+ NodeFilter.SHOW_TEXT,
3686
+ null
3687
+ );
3705
3688
 
3706
- if (match) {
3707
- matchIndex = match.index;
3708
- matchLength = match[0].length;
3709
- countSpan.innerText = `${matches.indexOf(match) + 1} of ${matches.length}`;
3710
- }
3689
+ const matches = [];
3690
+ let node;
3691
+ let globalIndex = 0;
3692
+
3693
+ while ((node = walker.nextNode())) {
3694
+ const text = node.nodeValue;
3695
+ regex.lastIndex = 0;
3696
+
3697
+ let match;
3698
+ while ((match = regex.exec(text)) !== null) {
3699
+ matches.push({
3700
+ node,
3701
+ start: match.index,
3702
+ end: match.index + match[0].length,
3703
+ globalStart: globalIndex + match.index
3704
+ });
3711
3705
  }
3712
- } catch (error) {
3713
- span.innerText = "Invalid regular expression";
3706
+
3707
+ globalIndex += text.length;
3708
+ }
3709
+
3710
+ if (!matches.length) {
3711
+ countSpan.innerText = "0 of 0";
3714
3712
  return;
3715
3713
  }
3716
3714
 
3717
- if (matchIndex !== -1) {
3718
- const startNodeInfo = getNodeAndOffsetFromIndex(editor, matchIndex);
3719
- const endNodeInfo = getNodeAndOffsetFromIndex(editor, matchIndex + matchLength);
3715
+ // Get current cursor global position
3716
+ let currentGlobal = 0;
3720
3717
 
3721
- if (startNodeInfo && endNodeInfo) {
3722
- const newRange = iframeWindow.createRange();
3723
- newRange.setStart(startNodeInfo.node, startNodeInfo.offset);
3724
- newRange.setEnd(endNodeInfo.node, endNodeInfo.offset);
3725
- selection.removeAllRanges();
3726
- selection.addRange(newRange);
3718
+ if (selection.rangeCount > 0) {
3719
+ const range = selection.getRangeAt(0);
3720
+
3721
+ const walker2 = core.elements.iframeWindow.createTreeWalker(
3722
+ editor,
3723
+ NodeFilter.SHOW_TEXT,
3724
+ null
3725
+ );
3726
+
3727
+ let n, count = 0;
3727
3728
 
3728
- startNodeInfo.node.parentElement.scrollIntoView({ block: "center", behavior: "smooth" });
3729
+ while ((n = walker2.nextNode())) {
3730
+ if (n === range.startContainer) {
3731
+ currentGlobal = count + range.startOffset;
3732
+ break;
3733
+ }
3734
+ count += n.nodeValue.length;
3729
3735
  }
3736
+ }
3737
+
3738
+ // ✅ Find next / prev
3739
+ let matchIndex = -1;
3740
+
3741
+ if (direction === 'next') {
3742
+ matchIndex = matches.findIndex(m => m.globalStart > currentGlobal);
3743
+ if (matchIndex === -1) matchIndex = 0;
3730
3744
  } else {
3731
- countSpan.innerText = "0 of 0";
3745
+ for (let i = matches.length - 1; i >= 0; i--) {
3746
+ if (matches[i].globalStart < currentGlobal) {
3747
+ matchIndex = i;
3748
+ break;
3749
+ }
3750
+ }
3751
+ if (matchIndex === -1) matchIndex = matches.length - 1;
3732
3752
  }
3753
+
3754
+ const match = matches[matchIndex];
3755
+
3756
+ // ✅ Select
3757
+ const newRange = core.elements.iframeWindow.createRange();
3758
+ newRange.setStart(match.node, match.start);
3759
+ newRange.setEnd(match.node, match.end);
3760
+
3761
+ selection.removeAllRanges();
3762
+ selection.addRange(newRange);
3763
+
3764
+ match.node.parentElement?.scrollIntoView({
3765
+ block: "center",
3766
+ behavior: "smooth"
3767
+ });
3768
+
3769
+ countSpan.innerText = `${matchIndex + 1} of ${matches.length}`;
3733
3770
  };
3734
3771
 
3735
3772
  const replace = () => {
3736
3773
  const searchText = findInput.value;
3737
3774
  const replaceText = replaceInput.value;
3738
- const useRegex = useRegexCheckbox.checked;
3739
3775
  const matchCase = matchCaseCheckbox.checked;
3776
+ const useRegex = useRegexCheckbox.checked;
3740
3777
  const wholeWord = wholeWordCheckbox.checked;
3741
3778
 
3742
3779
  if (!searchText) return;
3743
3780
 
3781
+ // 🔹 Build regex (same as find)
3782
+ let pattern = searchText;
3783
+
3784
+ if (!useRegex) {
3785
+ const cleaned = wholeWord ? searchText.trim() : searchText;
3786
+ const escaped = cleaned.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3787
+
3788
+ pattern = wholeWord
3789
+ ? `(?<!\\w)${escaped}(?!\\w)`
3790
+ : escaped;
3791
+ }
3792
+
3793
+ let regex;
3794
+ try {
3795
+ regex = new RegExp(pattern, matchCase ? '' : 'i');
3796
+ } catch {
3797
+ return;
3798
+ }
3799
+
3744
3800
  const selection = iframeWindow.getSelection();
3745
- if (selection.rangeCount > 0) {
3746
- const range = selection.getRangeAt(0);
3747
- const selectedText = range.toString();
3748
- let shouldReplace = false;
3749
- let newText = replaceText;
3750
-
3751
- let searchPattern = searchText;
3752
- let isRegexSearch = useRegex;
3753
-
3754
- if (!useRegex && wholeWord) {
3755
- const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3756
- searchPattern = `\\b${escaped}\\b`;
3757
- isRegexSearch = true;
3758
- }
3759
-
3760
- if (isRegexSearch) {
3761
- try {
3762
- const regex = new RegExp(searchPattern, matchCase ? '' : 'i');
3763
- const match = selectedText.match(regex);
3764
- if (match && match[0] === selectedText) {
3765
- shouldReplace = true;
3766
- newText = selectedText.replace(regex, replaceText);
3767
- }
3768
- } catch (e) { }
3769
- } else if (selectedText === searchText || (!matchCase && selectedText.toLowerCase() === searchText.toLowerCase())) {
3770
- shouldReplace = true;
3771
- }
3772
3801
 
3773
- if (shouldReplace) {
3774
- iframeWindow.execCommand('insertText', false, newText);
3775
- find('next');
3776
- } else {
3777
- find('next');
3778
- }
3779
- } else {
3802
+ if (!selection.rangeCount) {
3780
3803
  find('next');
3804
+ return;
3781
3805
  }
3806
+
3807
+ const range = selection.getRangeAt(0);
3808
+ const selectedText = range.toString();
3809
+
3810
+ const match = selectedText.match(regex);
3811
+
3812
+ if (match && match[0] === selectedText) {
3813
+ const newText = selectedText.replace(regex, replaceText);
3814
+
3815
+ // ✅ safe replace
3816
+ range.deleteContents();
3817
+ range.insertNode(core.elements.iframeWindow.createTextNode(newText));
3818
+
3819
+ // move cursor after replaced text
3820
+ range.setStart(range.endContainer, range.endOffset);
3821
+ selection.removeAllRanges();
3822
+ selection.addRange(range);
3823
+ }
3824
+
3825
+ find('next');
3782
3826
  };
3783
3827
 
3784
3828
  const replaceAll = () => {
@@ -3790,76 +3834,72 @@ const findAndReplace = (core, options, event) => {
3790
3834
 
3791
3835
  if (!searchText) return;
3792
3836
 
3793
- let count = 0;
3794
- const selection = iframeWindow.getSelection();
3837
+ let pattern = searchText;
3795
3838
 
3796
- const range = iframeWindow.createRange();
3797
- const startNode = getNodeAndOffsetFromIndex(editor, 0);
3798
- if (startNode) {
3799
- range.setStart(startNode.node, 0);
3800
- range.collapse(true);
3801
- selection.removeAllRanges();
3802
- selection.addRange(range);
3839
+ if (!useRegex) {
3840
+ const cleaned = wholeWord ? searchText.trim() : searchText;
3841
+ const escaped = cleaned.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3842
+
3843
+ pattern = wholeWord
3844
+ ? `(?<!\\w)${escaped}(?!\\w)`
3845
+ : escaped;
3803
3846
  }
3804
3847
 
3805
- while (true) {
3806
- const currentRange = selection.getRangeAt(0);
3807
- const searchStart = getCharIndex(editor, currentRange.endContainer, currentRange.endOffset);
3848
+ let regex;
3849
+ try {
3850
+ regex = new RegExp(pattern, matchCase ? 'g' : 'gi');
3851
+ } catch {
3852
+ return;
3853
+ }
3808
3854
 
3809
- let currentContent = editor.textContent;
3810
- let matchIndex = -1;
3811
- let matchLength = 0;
3812
- let newText = replaceText;
3855
+ // 🔹 Collect matches (same as find)
3856
+ const walker = core.elements.iframeWindow.createTreeWalker(
3857
+ editor,
3858
+ NodeFilter.SHOW_TEXT,
3859
+ null
3860
+ );
3813
3861
 
3814
- let searchPattern = searchText;
3815
- let isRegexSearch = useRegex;
3862
+ const matches = [];
3863
+ let node;
3816
3864
 
3817
- if (!useRegex && wholeWord) {
3818
- const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3819
- searchPattern = `\\b${escaped}\\b`;
3820
- isRegexSearch = true;
3865
+ while ((node = walker.nextNode())) {
3866
+ const text = node.nodeValue;
3867
+ regex.lastIndex = 0;
3868
+
3869
+ let match;
3870
+ while ((match = regex.exec(text)) !== null) {
3871
+ matches.push({
3872
+ node,
3873
+ start: match.index,
3874
+ end: match.index + match[0].length,
3875
+ original: match[0]
3876
+ });
3821
3877
  }
3878
+ }
3822
3879
 
3823
- if (isRegexSearch) {
3824
- try {
3825
- const regex = new RegExp(searchPattern, matchCase ? 'g' : 'gi');
3826
- regex.lastIndex = searchStart;
3827
- const match = regex.exec(currentContent);
3828
- if (match) {
3829
- matchIndex = match.index;
3830
- matchLength = match[0].length;
3831
- newText = match[0].replace(new RegExp(searchPattern, matchCase ? '' : 'i'), replaceText);
3832
- }
3833
- } catch (e) { break; }
3834
- } else {
3835
- let search = searchText;
3836
- if (!matchCase) {
3837
- currentContent = currentContent.toLowerCase();
3838
- search = search.toLowerCase();
3839
- }
3840
- matchIndex = currentContent.indexOf(search, searchStart);
3841
- matchLength = search.length;
3842
- }
3880
+ // 🔹 Replace in reverse (important)
3881
+ let count = 0;
3843
3882
 
3844
- if (matchIndex === -1) break;
3883
+ for (let i = matches.length - 1; i >= 0; i--) {
3884
+ const m = matches[i];
3885
+ const text = m.node.nodeValue;
3845
3886
 
3846
- const startNodeInfo = getNodeAndOffsetFromIndex(editor, matchIndex);
3847
- const endNodeInfo = getNodeAndOffsetFromIndex(editor, matchIndex + matchLength);
3887
+ const newText = m.original.replace(
3888
+ new RegExp(pattern, matchCase ? '' : 'i'),
3889
+ replaceText
3890
+ );
3848
3891
 
3849
- if (startNodeInfo && endNodeInfo) {
3850
- const newRange = iframeWindow.createRange();
3851
- newRange.setStart(startNodeInfo.node, startNodeInfo.offset);
3852
- newRange.setEnd(endNodeInfo.node, endNodeInfo.offset);
3853
- selection.removeAllRanges();
3854
- selection.addRange(newRange);
3892
+ m.node.nodeValue =
3893
+ text.slice(0, m.start) +
3894
+ newText +
3895
+ text.slice(m.end);
3855
3896
 
3856
- iframeWindow.execCommand('insertText', false, newText);
3857
- count++;
3858
- } else {
3859
- break;
3860
- }
3897
+ count++;
3861
3898
  }
3899
+
3862
3900
  span.innerText = `Replaced ${count} occurrences.`;
3901
+
3902
+ find('next');
3863
3903
  };
3864
3904
 
3865
3905
  prevBtn.onclick = () => find('prev');
@@ -3903,32 +3943,68 @@ const handleBackspaceInList = (event, core) => {
3903
3943
  const nestedList = li.querySelector('ul, ol');
3904
3944
  if (!nestedList) return;
3905
3945
 
3946
+
3947
+ if (!isCaretAtStart(range, li)) return;
3948
+
3949
+ event.preventDefault();
3950
+
3951
+ const parentList = li.parentElement;
3952
+ const prevLi = li.previousElementSibling;
3953
+
3906
3954
  const clone = li.cloneNode(true);
3907
3955
  const nestedInClone = clone.querySelector('ul, ol');
3908
3956
  if (nestedInClone) nestedInClone.remove();
3909
- const textContent = clone.textContent.replace(/\u200B/g, '').trim();
3910
3957
 
3911
- if (textContent.length > 0 && !isCaretAtStart(range, li)) return;
3912
-
3913
- event.preventDefault();
3958
+ const parentText = clone.textContent.replace(/\u200B/g, '').trim();
3914
3959
 
3915
- const parentList = li.parentElement;
3916
3960
  const fragment = document.createDocumentFragment();
3917
3961
  const children = Array.from(nestedList.children);
3918
3962
 
3919
3963
  children.forEach(child => fragment.appendChild(child));
3920
3964
 
3921
- if (children.length > 0) {
3922
- parentList.insertBefore(fragment, li);
3923
- const firstChild = children[0];
3965
+ // Case 1: previous list item exists
3966
+ if (prevLi) {
3967
+
3968
+ if (parentText.length > 0) {
3969
+ prevLi.appendChild(document.createTextNode(parentText));
3970
+ }
3971
+
3972
+ if (children.length > 0) {
3973
+ parentList.insertBefore(fragment, li);
3974
+ }
3975
+
3976
+ li.remove();
3977
+
3978
+ const newRange = document.createRange();
3979
+ newRange.selectNodeContents(prevLi);
3980
+ newRange.collapse(false);
3981
+
3982
+ selection.removeAllRanges();
3983
+ selection.addRange(newRange);
3984
+
3985
+ }
3986
+ else {newRange.collapse(false);
3987
+
3988
+ // Case 2: first list item
3989
+ const p = document.createElement('p');
3990
+ p.textContent = parentText;
3991
+
3992
+ parentList.parentElement.insertBefore(p, parentList);
3993
+
3994
+ if (children.length > 0) {
3995
+ parentList.insertBefore(fragment, li);
3996
+ }
3997
+
3998
+ li.remove();
3999
+
3924
4000
  const newRange = document.createRange();
3925
- newRange.setStart(firstChild, 0);
4001
+ newRange.selectNodeContents(p);
3926
4002
  newRange.collapse(true);
4003
+
3927
4004
  selection.removeAllRanges();
3928
4005
  selection.addRange(newRange);
3929
4006
  }
3930
4007
 
3931
- li.remove();
3932
4008
  core.updateCaretPosition();
3933
4009
  };
3934
4010