leksy-editor 2.2.0 → 2.2.1

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 +189 -149
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leksy-editor",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
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);
3727
3720
 
3728
- startNodeInfo.node.parentElement.scrollIntoView({ block: "center", behavior: "smooth" });
3721
+ const walker2 = core.elements.iframeWindow.createTreeWalker(
3722
+ editor,
3723
+ NodeFilter.SHOW_TEXT,
3724
+ null
3725
+ );
3726
+
3727
+ let n, count = 0;
3728
+
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);
3808
-
3809
- let currentContent = editor.textContent;
3810
- let matchIndex = -1;
3811
- let matchLength = 0;
3812
- let newText = replaceText;
3813
-
3814
- let searchPattern = searchText;
3815
- let isRegexSearch = useRegex;
3816
-
3817
- if (!useRegex && wholeWord) {
3818
- const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3819
- searchPattern = `\\b${escaped}\\b`;
3820
- isRegexSearch = true;
3821
- }
3822
-
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;
3848
+ let regex;
3849
+ try {
3850
+ regex = new RegExp(pattern, matchCase ? 'g' : 'gi');
3851
+ } catch {
3852
+ return;
3853
+ }
3854
+
3855
+ // 🔹 Collect matches (same as find)
3856
+ const walker = core.elements.iframeWindow.createTreeWalker(
3857
+ editor,
3858
+ NodeFilter.SHOW_TEXT,
3859
+ null
3860
+ );
3861
+
3862
+ const matches = [];
3863
+ let node;
3864
+
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
+ });
3842
3877
  }
3878
+ }
3843
3879
 
3844
- if (matchIndex === -1) break;
3880
+ // 🔹 Replace in reverse (important)
3881
+ let count = 0;
3845
3882
 
3846
- const startNodeInfo = getNodeAndOffsetFromIndex(editor, matchIndex);
3847
- const endNodeInfo = getNodeAndOffsetFromIndex(editor, matchIndex + matchLength);
3883
+ for (let i = matches.length - 1; i >= 0; i--) {
3884
+ const m = matches[i];
3885
+ const text = m.node.nodeValue;
3848
3886
 
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);
3887
+ const newText = m.original.replace(
3888
+ new RegExp(pattern, matchCase ? '' : 'i'),
3889
+ replaceText
3890
+ );
3855
3891
 
3856
- iframeWindow.execCommand('insertText', false, newText);
3857
- count++;
3858
- } else {
3859
- break;
3860
- }
3892
+ m.node.nodeValue =
3893
+ text.slice(0, m.start) +
3894
+ newText +
3895
+ text.slice(m.end);
3896
+
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');