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.
- package/package.json +1 -1
- package/utilities.js +231 -155
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leksy-editor",
|
|
3
|
-
"version": "2.2.
|
|
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
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
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
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
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
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
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
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
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
|
-
|
|
3713
|
-
|
|
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
|
-
|
|
3718
|
-
|
|
3719
|
-
const endNodeInfo = getNodeAndOffsetFromIndex(editor, matchIndex + matchLength);
|
|
3715
|
+
// ✅ Get current cursor global position
|
|
3716
|
+
let currentGlobal = 0;
|
|
3720
3717
|
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3794
|
-
const selection = iframeWindow.getSelection();
|
|
3837
|
+
let pattern = searchText;
|
|
3795
3838
|
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
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
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3848
|
+
let regex;
|
|
3849
|
+
try {
|
|
3850
|
+
regex = new RegExp(pattern, matchCase ? 'g' : 'gi');
|
|
3851
|
+
} catch {
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3808
3854
|
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
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
|
-
|
|
3815
|
-
|
|
3862
|
+
const matches = [];
|
|
3863
|
+
let node;
|
|
3816
3864
|
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
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
|
-
|
|
3824
|
-
|
|
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
|
-
|
|
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
|
|
3847
|
-
|
|
3887
|
+
const newText = m.original.replace(
|
|
3888
|
+
new RegExp(pattern, matchCase ? '' : 'i'),
|
|
3889
|
+
replaceText
|
|
3890
|
+
);
|
|
3848
3891
|
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
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.
|
|
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
|
|