ngx-rich-text-pro 1.0.4 → 1.0.6
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/fesm2022/ngx-rich-text-pro.mjs +964 -33
- package/fesm2022/ngx-rich-text-pro.mjs.map +1 -1
- package/ngx-rich-text-pro-1.0.6.tgz +0 -0
- package/package.json +1 -1
- package/types/ngx-rich-text-pro.d.ts +25 -3
- package/fesm2022/ngx-rich-text-editor.mjs +0 -1973
- package/fesm2022/ngx-rich-text-editor.mjs.map +0 -1
- package/types/ngx-rich-text-editor.d.ts +0 -378
|
@@ -955,6 +955,16 @@ class NgxRichTextEditorComponent {
|
|
|
955
955
|
this.mergeConfig();
|
|
956
956
|
this.updateStats();
|
|
957
957
|
}
|
|
958
|
+
ngAfterViewInit() {
|
|
959
|
+
try {
|
|
960
|
+
document.execCommand('defaultParagraphSeparator', false, 'p');
|
|
961
|
+
}
|
|
962
|
+
catch { }
|
|
963
|
+
const initialContent = this.content || this.value;
|
|
964
|
+
if (initialContent) {
|
|
965
|
+
this.setContent(initialContent, false);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
958
968
|
ngOnChanges(changes) {
|
|
959
969
|
if (changes['config'] ||
|
|
960
970
|
changes['tools'] ||
|
|
@@ -1207,6 +1217,14 @@ class NgxRichTextEditorComponent {
|
|
|
1207
1217
|
this.setCaretCharacterOffsetWithin(root, offset);
|
|
1208
1218
|
}
|
|
1209
1219
|
}
|
|
1220
|
+
if (this.exclusiveInlineFormats ?? this.computedConfig.exclusiveInlineFormats) {
|
|
1221
|
+
const hasNested = root.querySelector('u b, u strong, u i, u em, b u, b ins, b i, b em, i u, i ins, i b, i strong') !== null;
|
|
1222
|
+
if (hasNested) {
|
|
1223
|
+
const offset = this.getCaretCharacterOffsetWithin(root);
|
|
1224
|
+
this.sanitizeExclusiveFormatting(root);
|
|
1225
|
+
this.setCaretCharacterOffsetWithin(root, offset);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1210
1228
|
const isMarkdown = this.isMarkdownMode();
|
|
1211
1229
|
const html = root.innerHTML;
|
|
1212
1230
|
const markdown = this.htmlToMarkdown(root);
|
|
@@ -1232,6 +1250,10 @@ class NgxRichTextEditorComponent {
|
|
|
1232
1250
|
}
|
|
1233
1251
|
onEditorFocus(event) {
|
|
1234
1252
|
this.isFocused = true;
|
|
1253
|
+
try {
|
|
1254
|
+
document.execCommand('defaultParagraphSeparator', false, 'p');
|
|
1255
|
+
}
|
|
1256
|
+
catch { }
|
|
1235
1257
|
this.editorFocus.emit(event);
|
|
1236
1258
|
}
|
|
1237
1259
|
onEditorBlur(event) {
|
|
@@ -1618,22 +1640,850 @@ class NgxRichTextEditorComponent {
|
|
|
1618
1640
|
toggleExclusiveInlineFormat(targetCommand, otherCommands) {
|
|
1619
1641
|
if (this.disabled || this.readonly || this.isSourceView)
|
|
1620
1642
|
return;
|
|
1621
|
-
|
|
1622
|
-
const
|
|
1623
|
-
if (
|
|
1643
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
1644
|
+
const root = this.editorContentRef?.nativeElement;
|
|
1645
|
+
if (!sel || sel.rangeCount === 0 || !root) {
|
|
1646
|
+
this.focusEditor();
|
|
1624
1647
|
this.commandService.execute(targetCommand);
|
|
1648
|
+
this.onEditorInput();
|
|
1649
|
+
this.updateEditorState();
|
|
1650
|
+
return;
|
|
1625
1651
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1652
|
+
const range = sel.getRangeAt(0);
|
|
1653
|
+
// If selection is outside editor root, fall back to standard execution
|
|
1654
|
+
if (!root.contains(range.commonAncestorContainer)) {
|
|
1655
|
+
this.focusEditor();
|
|
1656
|
+
this.commandService.execute(targetCommand);
|
|
1657
|
+
this.onEditorInput();
|
|
1658
|
+
this.updateEditorState();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (range.collapsed) {
|
|
1662
|
+
this.focusEditor();
|
|
1663
|
+
const isTargetActive = this.commandService.queryState(targetCommand);
|
|
1664
|
+
if (isTargetActive) {
|
|
1665
|
+
this.commandService.execute(targetCommand);
|
|
1666
|
+
}
|
|
1667
|
+
else {
|
|
1668
|
+
for (const cmd of otherCommands) {
|
|
1669
|
+
if (this.commandService.queryState(cmd)) {
|
|
1670
|
+
this.commandService.execute(cmd);
|
|
1671
|
+
}
|
|
1630
1672
|
}
|
|
1673
|
+
this.commandService.execute(targetCommand);
|
|
1674
|
+
}
|
|
1675
|
+
this.onEditorInput();
|
|
1676
|
+
this.updateEditorState();
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
// Range has selected text:
|
|
1680
|
+
this.normalizeRangeBoundaries(range);
|
|
1681
|
+
const tagMap = {
|
|
1682
|
+
bold: 'b',
|
|
1683
|
+
italic: 'i',
|
|
1684
|
+
underline: 'u',
|
|
1685
|
+
};
|
|
1686
|
+
const formatAncestor = this.getFormattingAncestor(range, root);
|
|
1687
|
+
if (formatAncestor) {
|
|
1688
|
+
const ancestorFormat = this.getFormatType(formatAncestor);
|
|
1689
|
+
const isSameFormat = ancestorFormat === targetCommand;
|
|
1690
|
+
if (isSameFormat) {
|
|
1691
|
+
// Toggle off (deselect) target format from selection inside ancestor
|
|
1692
|
+
this.splitAndFormatInsideAncestor(formatAncestor, range, null);
|
|
1693
|
+
}
|
|
1694
|
+
else {
|
|
1695
|
+
// Apply target format exclusively to selection, replacing ancestor format
|
|
1696
|
+
const targetTag = tagMap[targetCommand] || 'b';
|
|
1697
|
+
this.splitAndFormatInsideAncestor(formatAncestor, range, targetTag);
|
|
1631
1698
|
}
|
|
1632
|
-
this.commandService.execute(targetCommand);
|
|
1633
1699
|
}
|
|
1700
|
+
else {
|
|
1701
|
+
const isTargetActive = this.isFormatActiveOnRange(range, targetCommand, root);
|
|
1702
|
+
if (isTargetActive) {
|
|
1703
|
+
// Toggle off target format from selection
|
|
1704
|
+
this.removeFormatFromRange(range, targetCommand);
|
|
1705
|
+
}
|
|
1706
|
+
else {
|
|
1707
|
+
// Apply target format exclusively to selection, stripping any conflicting formats
|
|
1708
|
+
this.applyExclusiveFormatToRange(range, targetCommand);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
this.sanitizeExclusiveFormatting(root);
|
|
1712
|
+
this.cleanupEmptyFormattingElements(root);
|
|
1713
|
+
this.cleanupEmptyBlockElements(root);
|
|
1714
|
+
this.mergeAdjacentFormattingElements(root);
|
|
1634
1715
|
this.onEditorInput();
|
|
1635
1716
|
this.updateEditorState();
|
|
1636
1717
|
}
|
|
1718
|
+
getFirstTextNode(node) {
|
|
1719
|
+
if (node.nodeType === Node.TEXT_NODE)
|
|
1720
|
+
return node;
|
|
1721
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
1722
|
+
const found = this.getFirstTextNode(node.childNodes[i]);
|
|
1723
|
+
if (found)
|
|
1724
|
+
return found;
|
|
1725
|
+
}
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
getLastTextNode(node) {
|
|
1729
|
+
if (node.nodeType === Node.TEXT_NODE)
|
|
1730
|
+
return node;
|
|
1731
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
1732
|
+
const found = this.getLastTextNode(node.childNodes[i]);
|
|
1733
|
+
if (found)
|
|
1734
|
+
return found;
|
|
1735
|
+
}
|
|
1736
|
+
return null;
|
|
1737
|
+
}
|
|
1738
|
+
normalizeRangeBoundaries(range, root) {
|
|
1739
|
+
if (range.collapsed)
|
|
1740
|
+
return;
|
|
1741
|
+
// 1. Normalize endContainer
|
|
1742
|
+
if (range.endContainer.nodeType === Node.ELEMENT_NODE) {
|
|
1743
|
+
const el = range.endContainer;
|
|
1744
|
+
if (range.endOffset === 0) {
|
|
1745
|
+
let prev = null;
|
|
1746
|
+
let curr = el;
|
|
1747
|
+
while (curr && curr !== root) {
|
|
1748
|
+
if (curr.previousSibling) {
|
|
1749
|
+
prev = curr.previousSibling;
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
curr = curr.parentNode;
|
|
1753
|
+
}
|
|
1754
|
+
if (prev) {
|
|
1755
|
+
const lastText = this.getLastTextNode(prev);
|
|
1756
|
+
if (lastText) {
|
|
1757
|
+
range.setEnd(lastText, lastText.textContent?.length || 0);
|
|
1758
|
+
}
|
|
1759
|
+
else {
|
|
1760
|
+
range.setEnd(prev, prev.childNodes.length);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
else if (el.childNodes.length > 0) {
|
|
1765
|
+
const child = el.childNodes[Math.min(range.endOffset - 1, el.childNodes.length - 1)];
|
|
1766
|
+
const lastText = this.getLastTextNode(child);
|
|
1767
|
+
if (lastText) {
|
|
1768
|
+
range.setEnd(lastText, lastText.textContent?.length || 0);
|
|
1769
|
+
}
|
|
1770
|
+
else {
|
|
1771
|
+
range.setEnd(child, child.childNodes.length);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
else if (range.endContainer.nodeType === Node.TEXT_NODE && range.endOffset === 0) {
|
|
1776
|
+
let prev = null;
|
|
1777
|
+
let curr = range.endContainer;
|
|
1778
|
+
while (curr && curr !== root) {
|
|
1779
|
+
if (curr.previousSibling) {
|
|
1780
|
+
prev = curr.previousSibling;
|
|
1781
|
+
break;
|
|
1782
|
+
}
|
|
1783
|
+
curr = curr.parentNode;
|
|
1784
|
+
}
|
|
1785
|
+
if (prev) {
|
|
1786
|
+
const lastText = this.getLastTextNode(prev);
|
|
1787
|
+
if (lastText) {
|
|
1788
|
+
range.setEnd(lastText, lastText.textContent?.length || 0);
|
|
1789
|
+
}
|
|
1790
|
+
else {
|
|
1791
|
+
range.setEnd(prev, prev.childNodes.length);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// 2. Normalize startContainer
|
|
1796
|
+
if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
|
|
1797
|
+
const el = range.startContainer;
|
|
1798
|
+
if (el.childNodes.length > 0 && range.startOffset < el.childNodes.length) {
|
|
1799
|
+
const child = el.childNodes[range.startOffset];
|
|
1800
|
+
const firstText = this.getFirstTextNode(child);
|
|
1801
|
+
if (firstText) {
|
|
1802
|
+
range.setStart(firstText, 0);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
else if (range.startOffset >= el.childNodes.length) {
|
|
1806
|
+
let next = null;
|
|
1807
|
+
let curr = el;
|
|
1808
|
+
while (curr && curr !== root) {
|
|
1809
|
+
if (curr.nextSibling) {
|
|
1810
|
+
next = curr.nextSibling;
|
|
1811
|
+
break;
|
|
1812
|
+
}
|
|
1813
|
+
curr = curr.parentNode;
|
|
1814
|
+
}
|
|
1815
|
+
if (next) {
|
|
1816
|
+
const firstText = this.getFirstTextNode(next);
|
|
1817
|
+
if (firstText) {
|
|
1818
|
+
range.setStart(firstText, 0);
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
range.setStart(next, 0);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
else if (range.startContainer.nodeType === Node.TEXT_NODE &&
|
|
1827
|
+
range.startOffset >= (range.startContainer.textContent?.length || 0)) {
|
|
1828
|
+
let next = null;
|
|
1829
|
+
let curr = range.startContainer;
|
|
1830
|
+
while (curr && curr !== root) {
|
|
1831
|
+
if (curr.nextSibling) {
|
|
1832
|
+
next = curr.nextSibling;
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1835
|
+
curr = curr.parentNode;
|
|
1836
|
+
}
|
|
1837
|
+
if (next) {
|
|
1838
|
+
const firstText = this.getFirstTextNode(next);
|
|
1839
|
+
if (firstText) {
|
|
1840
|
+
range.setStart(firstText, 0);
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
range.setStart(next, 0);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
getFormattingAncestor(range, root) {
|
|
1849
|
+
const isFormatTag = (el) => ['b', 'strong', 'i', 'em', 'u', 'ins'].includes(el.tagName.toLowerCase());
|
|
1850
|
+
let node = range.commonAncestorContainer;
|
|
1851
|
+
while (node && node !== root) {
|
|
1852
|
+
if (node.nodeType === Node.ELEMENT_NODE && isFormatTag(node)) {
|
|
1853
|
+
return node;
|
|
1854
|
+
}
|
|
1855
|
+
node = node.parentNode;
|
|
1856
|
+
}
|
|
1857
|
+
let startAnc = null;
|
|
1858
|
+
let sNode = range.startContainer;
|
|
1859
|
+
while (sNode && sNode !== root) {
|
|
1860
|
+
if (sNode.nodeType === Node.ELEMENT_NODE && isFormatTag(sNode)) {
|
|
1861
|
+
startAnc = sNode;
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1864
|
+
sNode = sNode.parentNode;
|
|
1865
|
+
}
|
|
1866
|
+
let endAnc = null;
|
|
1867
|
+
let eNode = range.endContainer;
|
|
1868
|
+
while (eNode && eNode !== root) {
|
|
1869
|
+
if (eNode.nodeType === Node.ELEMENT_NODE && isFormatTag(eNode)) {
|
|
1870
|
+
endAnc = eNode;
|
|
1871
|
+
break;
|
|
1872
|
+
}
|
|
1873
|
+
eNode = eNode.parentNode;
|
|
1874
|
+
}
|
|
1875
|
+
if (startAnc && endAnc && startAnc === endAnc) {
|
|
1876
|
+
return startAnc;
|
|
1877
|
+
}
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
getFormatType(el) {
|
|
1881
|
+
const tag = el.tagName.toLowerCase();
|
|
1882
|
+
if (tag === 'b' || tag === 'strong')
|
|
1883
|
+
return 'bold';
|
|
1884
|
+
if (tag === 'i' || tag === 'em')
|
|
1885
|
+
return 'italic';
|
|
1886
|
+
if (tag === 'u' || tag === 'ins')
|
|
1887
|
+
return 'underline';
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
splitAndFormatInsideAncestor(ancestor, range, newTag) {
|
|
1891
|
+
const parentNode = ancestor.parentNode;
|
|
1892
|
+
if (!parentNode)
|
|
1893
|
+
return;
|
|
1894
|
+
this.normalizeRangeBoundaries(range);
|
|
1895
|
+
if (range.startContainer === range.endContainer &&
|
|
1896
|
+
range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
1897
|
+
const textNode = range.startContainer;
|
|
1898
|
+
const startOffset = range.startOffset;
|
|
1899
|
+
const endOffset = range.endOffset;
|
|
1900
|
+
// 1. Split after selection
|
|
1901
|
+
const afterText = textNode.splitText(endOffset);
|
|
1902
|
+
// 2. Split before selection
|
|
1903
|
+
const selectedText = startOffset > 0 ? textNode.splitText(startOffset) : textNode;
|
|
1904
|
+
// Collect all siblings after selectedText inside ancestor
|
|
1905
|
+
const afterNodes = [];
|
|
1906
|
+
let sibling = selectedText.nextSibling;
|
|
1907
|
+
while (sibling) {
|
|
1908
|
+
afterNodes.push(sibling);
|
|
1909
|
+
sibling = sibling.nextSibling;
|
|
1910
|
+
}
|
|
1911
|
+
// Move afterNodes into a clone of ancestor
|
|
1912
|
+
const cloneAfter = ancestor.cloneNode(false);
|
|
1913
|
+
for (const node of afterNodes) {
|
|
1914
|
+
cloneAfter.appendChild(node);
|
|
1915
|
+
}
|
|
1916
|
+
let middleNode;
|
|
1917
|
+
if (newTag) {
|
|
1918
|
+
const wrapper = document.createElement(newTag);
|
|
1919
|
+
wrapper.appendChild(selectedText);
|
|
1920
|
+
middleNode = wrapper;
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
middleNode = selectedText;
|
|
1924
|
+
}
|
|
1925
|
+
parentNode.insertBefore(middleNode, ancestor.nextSibling);
|
|
1926
|
+
if (cloneAfter.hasChildNodes() &&
|
|
1927
|
+
(cloneAfter.textContent !== '' || cloneAfter.querySelector('img, br'))) {
|
|
1928
|
+
parentNode.insertBefore(cloneAfter, middleNode.nextSibling);
|
|
1929
|
+
}
|
|
1930
|
+
if (!ancestor.hasChildNodes() ||
|
|
1931
|
+
(ancestor.textContent === '' && !ancestor.querySelector('img, br'))) {
|
|
1932
|
+
ancestor.remove();
|
|
1933
|
+
}
|
|
1934
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
1935
|
+
if (sel) {
|
|
1936
|
+
sel.removeAllRanges();
|
|
1937
|
+
const newRange = document.createRange();
|
|
1938
|
+
if (middleNode.nodeType === Node.ELEMENT_NODE && middleNode.firstChild) {
|
|
1939
|
+
newRange.setStart(middleNode.firstChild, 0);
|
|
1940
|
+
newRange.setEnd(middleNode.lastChild, middleNode.lastChild.textContent?.length || 0);
|
|
1941
|
+
}
|
|
1942
|
+
else if (middleNode.nodeType === Node.TEXT_NODE) {
|
|
1943
|
+
newRange.setStart(middleNode, 0);
|
|
1944
|
+
newRange.setEnd(middleNode, middleNode.textContent?.length || 0);
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
newRange.selectNodeContents(middleNode);
|
|
1948
|
+
}
|
|
1949
|
+
sel.addRange(newRange);
|
|
1950
|
+
}
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
// General multi-node fallback within ancestor:
|
|
1954
|
+
const fragment = range.extractContents();
|
|
1955
|
+
const formatEls = Array.from(fragment.querySelectorAll('b, strong, i, em, u, ins'));
|
|
1956
|
+
for (const el of formatEls) {
|
|
1957
|
+
while (el.firstChild) {
|
|
1958
|
+
el.parentNode?.insertBefore(el.firstChild, el);
|
|
1959
|
+
}
|
|
1960
|
+
el.remove();
|
|
1961
|
+
}
|
|
1962
|
+
const styledEls = Array.from(fragment.querySelectorAll('*'));
|
|
1963
|
+
for (const el of styledEls) {
|
|
1964
|
+
if (el.style) {
|
|
1965
|
+
el.style.fontWeight = '';
|
|
1966
|
+
el.style.fontStyle = '';
|
|
1967
|
+
el.style.textDecoration = '';
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const cloneAfter = ancestor.cloneNode(false);
|
|
1971
|
+
const tailRange = document.createRange();
|
|
1972
|
+
tailRange.selectNodeContents(ancestor);
|
|
1973
|
+
tailRange.setStart(range.startContainer, range.startOffset);
|
|
1974
|
+
const tailFragment = tailRange.extractContents();
|
|
1975
|
+
cloneAfter.appendChild(tailFragment);
|
|
1976
|
+
const insertionRef = ancestor.nextSibling;
|
|
1977
|
+
let selectedNodes = [];
|
|
1978
|
+
if (newTag) {
|
|
1979
|
+
const wrapper = document.createElement(newTag);
|
|
1980
|
+
wrapper.appendChild(fragment);
|
|
1981
|
+
parentNode.insertBefore(wrapper, insertionRef);
|
|
1982
|
+
selectedNodes = [wrapper];
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
const nodes = Array.from(fragment.childNodes);
|
|
1986
|
+
for (const node of nodes) {
|
|
1987
|
+
parentNode.insertBefore(node, insertionRef);
|
|
1988
|
+
}
|
|
1989
|
+
selectedNodes = nodes;
|
|
1990
|
+
}
|
|
1991
|
+
if (cloneAfter.hasChildNodes() &&
|
|
1992
|
+
(cloneAfter.textContent !== '' || cloneAfter.querySelector('img, br'))) {
|
|
1993
|
+
parentNode.insertBefore(cloneAfter, insertionRef);
|
|
1994
|
+
}
|
|
1995
|
+
if (!ancestor.hasChildNodes() ||
|
|
1996
|
+
(ancestor.textContent === '' && !ancestor.querySelector('img, br'))) {
|
|
1997
|
+
ancestor.remove();
|
|
1998
|
+
}
|
|
1999
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2000
|
+
if (sel && selectedNodes.length > 0) {
|
|
2001
|
+
sel.removeAllRanges();
|
|
2002
|
+
const newRange = document.createRange();
|
|
2003
|
+
const first = selectedNodes[0];
|
|
2004
|
+
const last = selectedNodes[selectedNodes.length - 1];
|
|
2005
|
+
if (first.nodeType === Node.ELEMENT_NODE && first.firstChild) {
|
|
2006
|
+
newRange.setStart(first.firstChild, 0);
|
|
2007
|
+
}
|
|
2008
|
+
else if (first.nodeType === Node.TEXT_NODE) {
|
|
2009
|
+
newRange.setStart(first, 0);
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
newRange.setStartBefore(first);
|
|
2013
|
+
}
|
|
2014
|
+
if (last.nodeType === Node.ELEMENT_NODE && last.lastChild) {
|
|
2015
|
+
newRange.setEnd(last.lastChild, last.lastChild.textContent?.length || 0);
|
|
2016
|
+
}
|
|
2017
|
+
else if (last.nodeType === Node.TEXT_NODE) {
|
|
2018
|
+
newRange.setEnd(last, last.textContent?.length || 0);
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
newRange.setEndAfter(last);
|
|
2022
|
+
}
|
|
2023
|
+
sel.addRange(newRange);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
mergeAdjacentFormattingElements(container) {
|
|
2027
|
+
const formatTags = ['b', 'strong', 'i', 'em', 'u', 'ins'];
|
|
2028
|
+
for (const tag of formatTags) {
|
|
2029
|
+
const els = Array.from(container.querySelectorAll(tag));
|
|
2030
|
+
for (const el of els) {
|
|
2031
|
+
if (!el.parentNode)
|
|
2032
|
+
continue;
|
|
2033
|
+
let next = el.nextSibling;
|
|
2034
|
+
while (next &&
|
|
2035
|
+
next.nodeType === Node.ELEMENT_NODE &&
|
|
2036
|
+
next.tagName.toLowerCase() === tag) {
|
|
2037
|
+
const nextEl = next;
|
|
2038
|
+
while (nextEl.firstChild) {
|
|
2039
|
+
el.appendChild(nextEl.firstChild);
|
|
2040
|
+
}
|
|
2041
|
+
const toRemove = nextEl;
|
|
2042
|
+
next = nextEl.nextSibling;
|
|
2043
|
+
toRemove.remove();
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
getTextNodesInRange(range, root) {
|
|
2049
|
+
const textNodes = [];
|
|
2050
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
2051
|
+
let node = walker.nextNode();
|
|
2052
|
+
while (node) {
|
|
2053
|
+
const textNode = node;
|
|
2054
|
+
try {
|
|
2055
|
+
if (range.intersectsNode(textNode)) {
|
|
2056
|
+
let isIncluded = true;
|
|
2057
|
+
if (range.startContainer === textNode &&
|
|
2058
|
+
range.startOffset >= (textNode.textContent?.length || 0)) {
|
|
2059
|
+
isIncluded = false;
|
|
2060
|
+
}
|
|
2061
|
+
if (range.endContainer === textNode && range.endOffset === 0) {
|
|
2062
|
+
isIncluded = false;
|
|
2063
|
+
}
|
|
2064
|
+
if (isIncluded) {
|
|
2065
|
+
textNodes.push(textNode);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
catch {
|
|
2070
|
+
// ignore detached
|
|
2071
|
+
}
|
|
2072
|
+
node = walker.nextNode();
|
|
2073
|
+
}
|
|
2074
|
+
return textNodes;
|
|
2075
|
+
}
|
|
2076
|
+
getLeafBlocksInRange(range, root) {
|
|
2077
|
+
const blockSelector = 'p, div, li, h1, h2, h3, h4, h5, h6, blockquote, pre';
|
|
2078
|
+
const allBlocks = Array.from(root.querySelectorAll(blockSelector));
|
|
2079
|
+
const leafBlocks = allBlocks.filter((block) => {
|
|
2080
|
+
return !block.querySelector(blockSelector);
|
|
2081
|
+
});
|
|
2082
|
+
if (leafBlocks.length === 0) {
|
|
2083
|
+
return [];
|
|
2084
|
+
}
|
|
2085
|
+
return leafBlocks.filter((block) => {
|
|
2086
|
+
try {
|
|
2087
|
+
return range.intersectsNode(block);
|
|
2088
|
+
}
|
|
2089
|
+
catch {
|
|
2090
|
+
return false;
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
isFormatActiveOnRange(range, targetCommand, root) {
|
|
2095
|
+
const formatAncestor = this.getFormattingAncestor(range, root);
|
|
2096
|
+
if (formatAncestor) {
|
|
2097
|
+
return this.getFormatType(formatAncestor) === targetCommand;
|
|
2098
|
+
}
|
|
2099
|
+
const textNodes = this.getTextNodesInRange(range, root);
|
|
2100
|
+
if (textNodes.length > 0) {
|
|
2101
|
+
const isTargetTag = (el) => {
|
|
2102
|
+
const tag = el.tagName.toLowerCase();
|
|
2103
|
+
if (targetCommand === 'bold')
|
|
2104
|
+
return tag === 'b' || tag === 'strong';
|
|
2105
|
+
if (targetCommand === 'italic')
|
|
2106
|
+
return tag === 'i' || tag === 'em';
|
|
2107
|
+
if (targetCommand === 'underline')
|
|
2108
|
+
return tag === 'u' || tag === 'ins';
|
|
2109
|
+
return false;
|
|
2110
|
+
};
|
|
2111
|
+
const nonEmptyTextNodes = textNodes.filter((t) => (t.textContent || '').trim().length > 0);
|
|
2112
|
+
if (nonEmptyTextNodes.length > 0) {
|
|
2113
|
+
const allFormatted = nonEmptyTextNodes.every((node) => {
|
|
2114
|
+
let cur = node.parentNode;
|
|
2115
|
+
while (cur && cur !== root) {
|
|
2116
|
+
if (cur.nodeType === Node.ELEMENT_NODE && isTargetTag(cur)) {
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
cur = cur.parentNode;
|
|
2120
|
+
}
|
|
2121
|
+
return false;
|
|
2122
|
+
});
|
|
2123
|
+
if (allFormatted) {
|
|
2124
|
+
return true;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
return this.commandService.queryState(targetCommand);
|
|
2129
|
+
}
|
|
2130
|
+
applyExclusiveFormatToRange(range, targetCommand) {
|
|
2131
|
+
const root = this.editorContentRef?.nativeElement;
|
|
2132
|
+
if (!root)
|
|
2133
|
+
return;
|
|
2134
|
+
const leafBlocks = this.getLeafBlocksInRange(range, root);
|
|
2135
|
+
if (leafBlocks.length > 1) {
|
|
2136
|
+
let firstWrapper = null;
|
|
2137
|
+
let lastWrapper = null;
|
|
2138
|
+
for (const block of leafBlocks) {
|
|
2139
|
+
const text = block.textContent || '';
|
|
2140
|
+
if (text.trim() === '') {
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
const subRange = document.createRange();
|
|
2144
|
+
if (block.contains(range.startContainer)) {
|
|
2145
|
+
subRange.setStart(range.startContainer, range.startOffset);
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
const firstText = this.getFirstTextNode(block) || block;
|
|
2149
|
+
subRange.setStart(firstText, 0);
|
|
2150
|
+
}
|
|
2151
|
+
if (block.contains(range.endContainer)) {
|
|
2152
|
+
subRange.setEnd(range.endContainer, range.endOffset);
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
const lastText = this.getLastTextNode(block) || block;
|
|
2156
|
+
const len = lastText.nodeType === Node.TEXT_NODE
|
|
2157
|
+
? lastText.textContent?.length || 0
|
|
2158
|
+
: lastText.childNodes.length;
|
|
2159
|
+
subRange.setEnd(lastText, len);
|
|
2160
|
+
}
|
|
2161
|
+
if (!subRange.collapsed) {
|
|
2162
|
+
const wrapper = this.applyExclusiveFormatToSingleRange(subRange, targetCommand);
|
|
2163
|
+
if (wrapper) {
|
|
2164
|
+
if (!firstWrapper)
|
|
2165
|
+
firstWrapper = wrapper;
|
|
2166
|
+
lastWrapper = wrapper;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2171
|
+
if (sel && firstWrapper && lastWrapper) {
|
|
2172
|
+
sel.removeAllRanges();
|
|
2173
|
+
const newRange = document.createRange();
|
|
2174
|
+
newRange.setStartBefore(firstWrapper);
|
|
2175
|
+
newRange.setEndAfter(lastWrapper);
|
|
2176
|
+
sel.addRange(newRange);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
else {
|
|
2180
|
+
const wrapper = this.applyExclusiveFormatToSingleRange(range, targetCommand);
|
|
2181
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2182
|
+
if (sel && wrapper) {
|
|
2183
|
+
sel.removeAllRanges();
|
|
2184
|
+
const newRange = document.createRange();
|
|
2185
|
+
newRange.selectNodeContents(wrapper);
|
|
2186
|
+
sel.addRange(newRange);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
applyExclusiveFormatToSingleRange(range, targetCommand) {
|
|
2191
|
+
const root = this.editorContentRef?.nativeElement;
|
|
2192
|
+
if (root) {
|
|
2193
|
+
this.expandRangeToEnclosingFormat(range, root);
|
|
2194
|
+
}
|
|
2195
|
+
const tagMap = {
|
|
2196
|
+
bold: 'b',
|
|
2197
|
+
italic: 'i',
|
|
2198
|
+
underline: 'u',
|
|
2199
|
+
};
|
|
2200
|
+
const targetTag = tagMap[targetCommand] || 'b';
|
|
2201
|
+
const fragment = range.extractContents();
|
|
2202
|
+
const formatEls = Array.from(fragment.querySelectorAll('b, strong, i, em, u, ins'));
|
|
2203
|
+
for (const el of formatEls) {
|
|
2204
|
+
while (el.firstChild) {
|
|
2205
|
+
el.parentNode?.insertBefore(el.firstChild, el);
|
|
2206
|
+
}
|
|
2207
|
+
el.remove();
|
|
2208
|
+
}
|
|
2209
|
+
const styledEls = Array.from(fragment.querySelectorAll('*'));
|
|
2210
|
+
for (const el of styledEls) {
|
|
2211
|
+
if (el.style) {
|
|
2212
|
+
el.style.fontWeight = '';
|
|
2213
|
+
el.style.fontStyle = '';
|
|
2214
|
+
el.style.textDecoration = '';
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (!fragment.hasChildNodes() || fragment.textContent === '') {
|
|
2218
|
+
range.insertNode(fragment);
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2221
|
+
const wrapper = document.createElement(targetTag);
|
|
2222
|
+
wrapper.appendChild(fragment);
|
|
2223
|
+
range.insertNode(wrapper);
|
|
2224
|
+
return wrapper;
|
|
2225
|
+
}
|
|
2226
|
+
removeFormatFromRange(range, targetCommand) {
|
|
2227
|
+
const root = this.editorContentRef?.nativeElement;
|
|
2228
|
+
if (!root)
|
|
2229
|
+
return;
|
|
2230
|
+
const leafBlocks = this.getLeafBlocksInRange(range, root);
|
|
2231
|
+
if (leafBlocks.length > 1) {
|
|
2232
|
+
let firstBlock = null;
|
|
2233
|
+
let lastBlock = null;
|
|
2234
|
+
for (const block of leafBlocks) {
|
|
2235
|
+
const text = block.textContent || '';
|
|
2236
|
+
if (text.trim() === '') {
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const subRange = document.createRange();
|
|
2240
|
+
if (block.contains(range.startContainer)) {
|
|
2241
|
+
subRange.setStart(range.startContainer, range.startOffset);
|
|
2242
|
+
}
|
|
2243
|
+
else {
|
|
2244
|
+
const firstText = this.getFirstTextNode(block) || block;
|
|
2245
|
+
subRange.setStart(firstText, 0);
|
|
2246
|
+
}
|
|
2247
|
+
if (block.contains(range.endContainer)) {
|
|
2248
|
+
subRange.setEnd(range.endContainer, range.endOffset);
|
|
2249
|
+
}
|
|
2250
|
+
else {
|
|
2251
|
+
const lastText = this.getLastTextNode(block) || block;
|
|
2252
|
+
const len = lastText.nodeType === Node.TEXT_NODE
|
|
2253
|
+
? lastText.textContent?.length || 0
|
|
2254
|
+
: lastText.childNodes.length;
|
|
2255
|
+
subRange.setEnd(lastText, len);
|
|
2256
|
+
}
|
|
2257
|
+
if (!subRange.collapsed) {
|
|
2258
|
+
this.removeFormatFromSingleRange(subRange, targetCommand);
|
|
2259
|
+
if (!firstBlock)
|
|
2260
|
+
firstBlock = block;
|
|
2261
|
+
lastBlock = block;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2265
|
+
if (sel && firstBlock && lastBlock) {
|
|
2266
|
+
const firstText = this.getFirstTextNode(firstBlock) || firstBlock;
|
|
2267
|
+
const lastText = this.getLastTextNode(lastBlock) || lastBlock;
|
|
2268
|
+
sel.removeAllRanges();
|
|
2269
|
+
const newRange = document.createRange();
|
|
2270
|
+
newRange.setStart(firstText, 0);
|
|
2271
|
+
newRange.setEnd(lastText, lastText.nodeType === Node.TEXT_NODE
|
|
2272
|
+
? lastText.textContent?.length || 0
|
|
2273
|
+
: lastText.childNodes.length);
|
|
2274
|
+
sel.addRange(newRange);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2278
|
+
this.removeFormatFromSingleRange(range, targetCommand);
|
|
2279
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2280
|
+
if (sel) {
|
|
2281
|
+
sel.removeAllRanges();
|
|
2282
|
+
sel.addRange(range);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
removeFormatFromSingleRange(range, targetCommand) {
|
|
2287
|
+
const root = this.editorContentRef?.nativeElement;
|
|
2288
|
+
if (root) {
|
|
2289
|
+
this.expandRangeToEnclosingFormat(range, root);
|
|
2290
|
+
}
|
|
2291
|
+
const formatTags = {
|
|
2292
|
+
bold: ['b', 'strong'],
|
|
2293
|
+
italic: ['i', 'em'],
|
|
2294
|
+
underline: ['u', 'ins'],
|
|
2295
|
+
};
|
|
2296
|
+
const tagsToRemove = formatTags[targetCommand] || [];
|
|
2297
|
+
const fragment = range.extractContents();
|
|
2298
|
+
const selector = tagsToRemove.join(', ');
|
|
2299
|
+
if (selector) {
|
|
2300
|
+
const matchedEls = Array.from(fragment.querySelectorAll(selector));
|
|
2301
|
+
for (const el of matchedEls) {
|
|
2302
|
+
while (el.firstChild) {
|
|
2303
|
+
el.parentNode?.insertBefore(el.firstChild, el);
|
|
2304
|
+
}
|
|
2305
|
+
el.remove();
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const styledEls = Array.from(fragment.querySelectorAll('*'));
|
|
2309
|
+
for (const el of styledEls) {
|
|
2310
|
+
if (el.style) {
|
|
2311
|
+
if (targetCommand === 'bold')
|
|
2312
|
+
el.style.fontWeight = '';
|
|
2313
|
+
if (targetCommand === 'italic')
|
|
2314
|
+
el.style.fontStyle = '';
|
|
2315
|
+
if (targetCommand === 'underline')
|
|
2316
|
+
el.style.textDecoration = '';
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
const firstChild = fragment.firstChild;
|
|
2320
|
+
const lastChild = fragment.lastChild;
|
|
2321
|
+
range.insertNode(fragment);
|
|
2322
|
+
if (firstChild && lastChild) {
|
|
2323
|
+
range.setStartBefore(firstChild);
|
|
2324
|
+
range.setEndAfter(lastChild);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
expandRangeToEnclosingFormat(range, root) {
|
|
2328
|
+
const isFormattingTag = (el) => {
|
|
2329
|
+
const tag = el.tagName.toLowerCase();
|
|
2330
|
+
return ['b', 'strong', 'i', 'em', 'u', 'ins'].includes(tag);
|
|
2331
|
+
};
|
|
2332
|
+
let startNode = range.startContainer;
|
|
2333
|
+
while (startNode && startNode !== root) {
|
|
2334
|
+
if (startNode.nodeType === Node.ELEMENT_NODE && isFormattingTag(startNode)) {
|
|
2335
|
+
if (range.startOffset === 0) {
|
|
2336
|
+
range.setStartBefore(startNode);
|
|
2337
|
+
}
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2340
|
+
if (startNode.previousSibling)
|
|
2341
|
+
break;
|
|
2342
|
+
startNode = startNode.parentNode;
|
|
2343
|
+
}
|
|
2344
|
+
let endNode = range.endContainer;
|
|
2345
|
+
while (endNode && endNode !== root) {
|
|
2346
|
+
if (endNode.nodeType === Node.ELEMENT_NODE && isFormattingTag(endNode)) {
|
|
2347
|
+
const len = endNode.childNodes.length;
|
|
2348
|
+
if (range.endOffset >= len) {
|
|
2349
|
+
range.setEndAfter(endNode);
|
|
2350
|
+
}
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
2353
|
+
if (endNode.nextSibling)
|
|
2354
|
+
break;
|
|
2355
|
+
endNode = endNode.parentNode;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
sanitizeExclusiveFormatting(container) {
|
|
2359
|
+
// 1. Unwrap redundant same-tag nesting (e.g. <b><b>text</b></b> -> <b>text</b>)
|
|
2360
|
+
const boldEls = container.querySelectorAll('b, strong');
|
|
2361
|
+
boldEls.forEach((b) => {
|
|
2362
|
+
const innerBolds = b.querySelectorAll('b, strong');
|
|
2363
|
+
innerBolds.forEach((inner) => {
|
|
2364
|
+
while (inner.firstChild) {
|
|
2365
|
+
inner.parentNode?.insertBefore(inner.firstChild, inner);
|
|
2366
|
+
}
|
|
2367
|
+
inner.remove();
|
|
2368
|
+
});
|
|
2369
|
+
});
|
|
2370
|
+
const italicEls = container.querySelectorAll('i, em');
|
|
2371
|
+
italicEls.forEach((i) => {
|
|
2372
|
+
const innerItalics = i.querySelectorAll('i, em');
|
|
2373
|
+
innerItalics.forEach((inner) => {
|
|
2374
|
+
while (inner.firstChild) {
|
|
2375
|
+
inner.parentNode?.insertBefore(inner.firstChild, inner);
|
|
2376
|
+
}
|
|
2377
|
+
inner.remove();
|
|
2378
|
+
});
|
|
2379
|
+
});
|
|
2380
|
+
const underEls = container.querySelectorAll('u, ins');
|
|
2381
|
+
underEls.forEach((u) => {
|
|
2382
|
+
const innerUnders = u.querySelectorAll('u, ins');
|
|
2383
|
+
innerUnders.forEach((inner) => {
|
|
2384
|
+
while (inner.firstChild) {
|
|
2385
|
+
inner.parentNode?.insertBefore(inner.firstChild, inner);
|
|
2386
|
+
}
|
|
2387
|
+
inner.remove();
|
|
2388
|
+
});
|
|
2389
|
+
});
|
|
2390
|
+
// 2. Split any parent formatting tag around any different inner formatting tags
|
|
2391
|
+
let passes = 0;
|
|
2392
|
+
while (passes < 5) {
|
|
2393
|
+
passes++;
|
|
2394
|
+
let hasNested = false;
|
|
2395
|
+
// If <u> contains <b> or <i>: split <u> around <b>, <strong>, <i>, <em>
|
|
2396
|
+
const uWithOthers = Array.from(container.querySelectorAll('u, ins')).filter((el) => el.querySelector('b, strong, i, em') !== null);
|
|
2397
|
+
for (const parent of uWithOthers) {
|
|
2398
|
+
if (parent.parentNode) {
|
|
2399
|
+
hasNested = true;
|
|
2400
|
+
this.splitAroundInnerTags(parent, ['b', 'strong', 'i', 'em']);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
// If <b> contains <u> or <i>: split <b> around <u>, <ins>, <i>, <em>
|
|
2404
|
+
const bWithOthers = Array.from(container.querySelectorAll('b, strong')).filter((el) => el.querySelector('u, ins, i, em') !== null);
|
|
2405
|
+
for (const parent of bWithOthers) {
|
|
2406
|
+
if (parent.parentNode) {
|
|
2407
|
+
hasNested = true;
|
|
2408
|
+
this.splitAroundInnerTags(parent, ['u', 'ins', 'i', 'em']);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
// If <i> contains <u> or <b>: split <i> around <u>, <ins>, <b>, <strong>
|
|
2412
|
+
const iWithOthers = Array.from(container.querySelectorAll('i, em')).filter((el) => el.querySelector('u, ins, b, strong') !== null);
|
|
2413
|
+
for (const parent of iWithOthers) {
|
|
2414
|
+
if (parent.parentNode) {
|
|
2415
|
+
hasNested = true;
|
|
2416
|
+
this.splitAroundInnerTags(parent, ['u', 'ins', 'b', 'strong']);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
if (!hasNested)
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
this.cleanupEmptyFormattingElements(container);
|
|
2423
|
+
}
|
|
2424
|
+
splitAroundInnerTags(parent, innerTagNames) {
|
|
2425
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
2426
|
+
const children = Array.from(parent.childNodes);
|
|
2427
|
+
const parentNode = parent.parentNode;
|
|
2428
|
+
if (!parentNode)
|
|
2429
|
+
return;
|
|
2430
|
+
let currentGroup = [];
|
|
2431
|
+
const flushGroup = () => {
|
|
2432
|
+
if (currentGroup.length === 0)
|
|
2433
|
+
return;
|
|
2434
|
+
const clone = document.createElement(parentTag);
|
|
2435
|
+
if (parent.style.cssText)
|
|
2436
|
+
clone.style.cssText = parent.style.cssText;
|
|
2437
|
+
if (parent.className)
|
|
2438
|
+
clone.className = parent.className;
|
|
2439
|
+
for (const node of currentGroup) {
|
|
2440
|
+
clone.appendChild(node);
|
|
2441
|
+
}
|
|
2442
|
+
parentNode.insertBefore(clone, parent);
|
|
2443
|
+
currentGroup = [];
|
|
2444
|
+
};
|
|
2445
|
+
for (const child of children) {
|
|
2446
|
+
if (child.nodeType === Node.ELEMENT_NODE &&
|
|
2447
|
+
innerTagNames.includes(child.tagName.toLowerCase())) {
|
|
2448
|
+
flushGroup();
|
|
2449
|
+
parentNode.insertBefore(child, parent);
|
|
2450
|
+
}
|
|
2451
|
+
else {
|
|
2452
|
+
currentGroup.push(child);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
flushGroup();
|
|
2456
|
+
parent.remove();
|
|
2457
|
+
}
|
|
2458
|
+
cleanupEmptyFormattingElements(container) {
|
|
2459
|
+
const els = Array.from(container.querySelectorAll('b, strong, i, em, u, ins'));
|
|
2460
|
+
els.forEach((el) => {
|
|
2461
|
+
// If the formatting element only contains a <br>, unwrap the <br>
|
|
2462
|
+
if (el.textContent === '' && el.querySelector('br')) {
|
|
2463
|
+
while (el.firstChild) {
|
|
2464
|
+
el.parentNode?.insertBefore(el.firstChild, el);
|
|
2465
|
+
}
|
|
2466
|
+
el.remove();
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
if (!el.hasChildNodes() || (el.textContent === '' && !el.querySelector('img'))) {
|
|
2470
|
+
el.remove();
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
cleanupEmptyBlockElements(container) {
|
|
2475
|
+
const blockEls = Array.from(container.querySelectorAll('p, div'));
|
|
2476
|
+
blockEls.forEach((el) => {
|
|
2477
|
+
if (el.childNodes.length === 0) {
|
|
2478
|
+
el.remove();
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
if (el.textContent === '' &&
|
|
2482
|
+
!el.querySelector('br, img, hr, iframe, video, audio, table, svg, canvas')) {
|
|
2483
|
+
el.remove();
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
1637
2487
|
strike() {
|
|
1638
2488
|
this.exec('strikeThrough');
|
|
1639
2489
|
}
|
|
@@ -1740,8 +2590,18 @@ class NgxRichTextEditorComponent {
|
|
|
1740
2590
|
setFontFamily(font) {
|
|
1741
2591
|
if (this.disabled || this.readonly || this.isSourceView || !font)
|
|
1742
2592
|
return;
|
|
1743
|
-
|
|
1744
|
-
|
|
2593
|
+
const selection = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2594
|
+
const hasSelection = selection && !selection.isCollapsed && (selection.toString()?.length ?? 0) > 0;
|
|
2595
|
+
if (hasSelection) {
|
|
2596
|
+
this.focusEditor();
|
|
2597
|
+
this.commandService.setFontName(font);
|
|
2598
|
+
}
|
|
2599
|
+
else {
|
|
2600
|
+
this.fontFamily = font;
|
|
2601
|
+
if (this.editorContentRef?.nativeElement) {
|
|
2602
|
+
this.editorContentRef.nativeElement.style.fontFamily = font;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
1745
2605
|
this.onEditorInput();
|
|
1746
2606
|
this.updateEditorState();
|
|
1747
2607
|
}
|
|
@@ -1751,8 +2611,18 @@ class NgxRichTextEditorComponent {
|
|
|
1751
2611
|
setFontSize(size) {
|
|
1752
2612
|
if (this.disabled || this.readonly || this.isSourceView || !size)
|
|
1753
2613
|
return;
|
|
1754
|
-
|
|
1755
|
-
|
|
2614
|
+
const selection = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2615
|
+
const hasSelection = selection && !selection.isCollapsed && (selection.toString()?.length ?? 0) > 0;
|
|
2616
|
+
if (hasSelection) {
|
|
2617
|
+
this.focusEditor();
|
|
2618
|
+
this.commandService.setFontSize(String(size));
|
|
2619
|
+
}
|
|
2620
|
+
else {
|
|
2621
|
+
this.fontSize = String(size);
|
|
2622
|
+
if (this.editorContentRef?.nativeElement) {
|
|
2623
|
+
this.editorContentRef.nativeElement.style.fontSize = String(size);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
1756
2626
|
this.onEditorInput();
|
|
1757
2627
|
this.updateEditorState();
|
|
1758
2628
|
}
|
|
@@ -2040,7 +2910,7 @@ class NgxRichTextEditorComponent {
|
|
|
2040
2910
|
this.focusEditor();
|
|
2041
2911
|
}
|
|
2042
2912
|
getHtml() {
|
|
2043
|
-
return this.content;
|
|
2913
|
+
return this.editorContentRef?.nativeElement?.innerHTML || this.content;
|
|
2044
2914
|
}
|
|
2045
2915
|
getText() {
|
|
2046
2916
|
return this.editorContentRef?.nativeElement?.innerText || '';
|
|
@@ -2199,12 +3069,36 @@ class NgxRichTextEditorComponent {
|
|
|
2199
3069
|
let italic = this.commandService.queryState('italic');
|
|
2200
3070
|
let underline = this.commandService.queryState('underline');
|
|
2201
3071
|
if (this.exclusiveInlineFormats ?? this.computedConfig.exclusiveInlineFormats) {
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
3072
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
3073
|
+
const root = this.editorContentRef?.nativeElement;
|
|
3074
|
+
let formatAncestor = null;
|
|
3075
|
+
if (sel && sel.rangeCount > 0 && root) {
|
|
3076
|
+
const range = sel.getRangeAt(0);
|
|
3077
|
+
if (root.contains(range.commonAncestorContainer)) {
|
|
3078
|
+
formatAncestor = this.getFormattingAncestor(range, root);
|
|
3079
|
+
}
|
|
2205
3080
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
3081
|
+
if (formatAncestor) {
|
|
3082
|
+
const type = this.getFormatType(formatAncestor);
|
|
3083
|
+
bold = type === 'bold';
|
|
3084
|
+
italic = type === 'italic';
|
|
3085
|
+
underline = type === 'underline';
|
|
3086
|
+
}
|
|
3087
|
+
else {
|
|
3088
|
+
if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
|
|
3089
|
+
bold = false;
|
|
3090
|
+
italic = false;
|
|
3091
|
+
underline = false;
|
|
3092
|
+
}
|
|
3093
|
+
else {
|
|
3094
|
+
if (underline) {
|
|
3095
|
+
bold = false;
|
|
3096
|
+
italic = false;
|
|
3097
|
+
}
|
|
3098
|
+
else if (bold) {
|
|
3099
|
+
italic = false;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
2208
3102
|
}
|
|
2209
3103
|
}
|
|
2210
3104
|
this.activeStates = {
|
|
@@ -2239,14 +3133,26 @@ class NgxRichTextEditorComponent {
|
|
|
2239
3133
|
const el = element || this.editorContentRef?.nativeElement;
|
|
2240
3134
|
if (!el)
|
|
2241
3135
|
return '';
|
|
2242
|
-
|
|
3136
|
+
const md = this.serializeChildren(el, el).replace(/\n$/, '');
|
|
3137
|
+
return md;
|
|
2243
3138
|
}
|
|
2244
|
-
|
|
3139
|
+
isBlockElement(node, editorRoot) {
|
|
3140
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
3141
|
+
return false;
|
|
3142
|
+
const el = node;
|
|
3143
|
+
const tag = el.tagName.toLowerCase();
|
|
3144
|
+
const blockTags = [
|
|
3145
|
+
'p', 'div', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
3146
|
+
'blockquote', 'pre', 'section', 'article', 'header', 'footer', 'hr'
|
|
3147
|
+
];
|
|
3148
|
+
return blockTags.includes(tag);
|
|
3149
|
+
}
|
|
3150
|
+
serializeChildren(element, editorRoot) {
|
|
2245
3151
|
let output = '';
|
|
3152
|
+
const root = editorRoot || (element === this.editorContentRef?.nativeElement ? element : this.editorContentRef?.nativeElement);
|
|
2246
3153
|
for (const node of Array.from(element.childNodes)) {
|
|
2247
|
-
const serialized = this.serializeNode(node);
|
|
2248
|
-
const isBlock = node
|
|
2249
|
-
['p', 'div', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'blockquote', 'pre'].includes(node.tagName.toLowerCase());
|
|
3154
|
+
const serialized = this.serializeNode(node, root);
|
|
3155
|
+
const isBlock = this.isBlockElement(node, root);
|
|
2250
3156
|
if (isBlock && output.length > 0 && !output.endsWith('\n')) {
|
|
2251
3157
|
output += '\n';
|
|
2252
3158
|
}
|
|
@@ -2254,30 +3160,36 @@ class NgxRichTextEditorComponent {
|
|
|
2254
3160
|
}
|
|
2255
3161
|
return output;
|
|
2256
3162
|
}
|
|
2257
|
-
serializeNode(node) {
|
|
3163
|
+
serializeNode(node, editorRoot) {
|
|
2258
3164
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
2259
|
-
const text = node.textContent ?? '';
|
|
2260
|
-
|
|
3165
|
+
const text = (node.textContent ?? '').replace(/\r\n?/g, '\n');
|
|
3166
|
+
if (editorRoot && node.parentElement) {
|
|
3167
|
+
const hasBlockSiblings = Array.from(node.parentElement.childNodes).some((sibling) => sibling !== node && this.isBlockElement(sibling, editorRoot));
|
|
3168
|
+
if (hasBlockSiblings && /^[\n\r\t ]+$/.test(text)) {
|
|
3169
|
+
return '';
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
return text;
|
|
2261
3173
|
}
|
|
2262
3174
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
2263
3175
|
return '';
|
|
2264
3176
|
}
|
|
2265
3177
|
const element = node;
|
|
2266
3178
|
const tag = element.tagName.toLowerCase();
|
|
2267
|
-
const children = this.serializeChildren(element);
|
|
3179
|
+
const children = this.serializeChildren(element, editorRoot);
|
|
2268
3180
|
// Hashtag span: extract raw hashtag text (#tag)
|
|
2269
3181
|
if (element.classList.contains('ngx-editor-hashtag') || element.classList.contains('post-content-hashtag')) {
|
|
2270
3182
|
return element.textContent?.trim() || '';
|
|
2271
3183
|
}
|
|
2272
3184
|
// Exclusive single effect precedence: Underline > Bold > Italic
|
|
2273
3185
|
if (this.isUnderlineElement(element)) {
|
|
2274
|
-
return this.
|
|
3186
|
+
return this.wrapMarkdown(children, '__');
|
|
2275
3187
|
}
|
|
2276
3188
|
if (this.isBoldElement(element)) {
|
|
2277
|
-
return this.
|
|
3189
|
+
return this.wrapMarkdown(children, '**');
|
|
2278
3190
|
}
|
|
2279
3191
|
if (this.isItalicElement(element)) {
|
|
2280
|
-
return this.
|
|
3192
|
+
return this.wrapMarkdown(children, '*');
|
|
2281
3193
|
}
|
|
2282
3194
|
if (tag === 'br') {
|
|
2283
3195
|
return '\n';
|
|
@@ -2293,12 +3205,27 @@ class NgxRichTextEditorComponent {
|
|
|
2293
3205
|
if (tag === 'li') {
|
|
2294
3206
|
return children;
|
|
2295
3207
|
}
|
|
2296
|
-
if (tag === 'p' || tag === 'div') {
|
|
3208
|
+
if (tag === 'p' || tag === 'div' || this.isBlockElement(element, editorRoot)) {
|
|
2297
3209
|
return children.endsWith('\n') ? children : `${children}\n`;
|
|
2298
3210
|
}
|
|
2299
3211
|
return children;
|
|
2300
3212
|
}
|
|
2301
3213
|
wrapMarkdown(text, marker) {
|
|
3214
|
+
if (!text)
|
|
3215
|
+
return '';
|
|
3216
|
+
// If text contains newlines, wrap each line individually so markdown formatting
|
|
3217
|
+
// does not span across paragraph/line breaks (which breaks markdown line parsers)
|
|
3218
|
+
if (text.includes('\n')) {
|
|
3219
|
+
return text
|
|
3220
|
+
.split('\n')
|
|
3221
|
+
.map((line) => this.wrapMarkdownSingleLine(line, marker))
|
|
3222
|
+
.join('\n');
|
|
3223
|
+
}
|
|
3224
|
+
return this.wrapMarkdownSingleLine(text, marker);
|
|
3225
|
+
}
|
|
3226
|
+
wrapMarkdownSingleLine(text, marker) {
|
|
3227
|
+
if (!text)
|
|
3228
|
+
return '';
|
|
2302
3229
|
let clean = text;
|
|
2303
3230
|
// Mutually exclusive formatting: strip conflicting markers so only ONE effect is applied
|
|
2304
3231
|
if (marker === '__') {
|
|
@@ -2688,6 +3615,10 @@ class NgxRichTextEditorComponent {
|
|
|
2688
3615
|
focusEditor() {
|
|
2689
3616
|
if (this.editorContentRef && this.editorContentRef.nativeElement) {
|
|
2690
3617
|
this.editorContentRef.nativeElement.focus();
|
|
3618
|
+
try {
|
|
3619
|
+
document.execCommand('defaultParagraphSeparator', false, 'p');
|
|
3620
|
+
}
|
|
3621
|
+
catch { }
|
|
2691
3622
|
}
|
|
2692
3623
|
}
|
|
2693
3624
|
onDocumentClick(event) {
|
|
@@ -2702,7 +3633,7 @@ class NgxRichTextEditorComponent {
|
|
|
2702
3633
|
useExisting: forwardRef(() => NgxRichTextEditorComponent),
|
|
2703
3634
|
multi: true,
|
|
2704
3635
|
},
|
|
2705
|
-
], viewQueries: [{ propertyName: "editorContentRef", first: true, predicate: ["editorContent"], descendants: true }, { propertyName: "sourceTextareaRef", first: true, predicate: ["sourceTextarea"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"ngx-editor-container\"\n [ngClass]=\"customClass\"\n [class.is-focused]=\"isFocused\"\n [class.is-disabled]=\"disabled\"\n [style.min-height]=\"computedConfig.minHeight\"\n [style.max-height]=\"computedConfig.maxHeight\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedEditorStyle\"\n>\n <!-- Toolbar -->\n @if (computedConfig.showToolbar && !readonly) {\n <!-- Array-configured Toolbar (if tools input is provided) -->\n @if (computedConfig.tools && computedConfig.tools.length > 0) {\n <ngx-editor-toolbar\n [editor]=\"this\"\n [tools]=\"computedConfig.tools\"\n [fontFamilies]=\"resolvedFontFamilies\"\n ></ngx-editor-toolbar>\n } @else {\n <!-- Default Full Toolbar -->\n <div class=\"ngx-editor-toolbar\" role=\"toolbar\" aria-label=\"Editor Toolbar\">\n <!-- History -->\n @if (computedConfig.toolbar?.history) {\n <div class=\"ngx-editor-toolbar-group\">\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Undo (Ctrl+Z)\"\n aria-label=\"Undo\"\n (mousedown)=\"$event.preventDefault(); undo()\"\n >\n <ngx-editor-icon name=\"undo\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Redo (Ctrl+Y)\"\n aria-label=\"Redo\"\n (mousedown)=\"$event.preventDefault(); redo()\"\n >\n <ngx-editor-icon name=\"redo\"></ngx-editor-icon>\n </button>\n </div>\n }\n\n <!-- Format Block -->\n @if (computedConfig.toolbar?.format) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Paragraph Format\"\n aria-label=\"Paragraph Format\"\n [value]=\"activeBlockTag\"\n (change)=\"onFormatBlockChange($event)\"\n >\n <option value=\"p\">Normal text</option>\n <option value=\"h1\">Heading 1</option>\n <option value=\"h2\">Heading 2</option>\n <option value=\"h3\">Heading 3</option>\n <option value=\"h4\">Heading 4</option>\n <option value=\"blockquote\">Quote</option>\n <option value=\"pre\">Code Block</option>\n </select>\n </div>\n }\n\n <!-- Font Family -->\n @if (computedConfig.toolbar?.fontName) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Family\"\n aria-label=\"Font Family\"\n (change)=\"onFontNameChange($event)\"\n >\n <option value=\"\">Font</option>\n @for (font of resolvedFontFamilies; track font.value) {\n <option [value]=\"font.value\">{{ font.label }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Font Size -->\n @if (computedConfig.toolbar?.fontSize) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Size\"\n aria-label=\"Font Size\"\n (change)=\"onFontSizeChange($event)\"\n >\n <option value=\"\">Size</option>\n <option value=\"1\">Small (10pt)</option>\n <option value=\"3\">Normal (12pt)</option>\n <option value=\"5\">Large (18pt)</option>\n <option value=\"7\">Huge (36pt)</option>\n </select>\n </div>\n }\n\n <!-- Inline Styles: Bold, Italic, Underline, Strike, Sub, Sup, Code -->\n @if (computedConfig.toolbar?.inline) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.inline?.bold !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.bold\"\n title=\"Bold (Ctrl+B)\"\n aria-label=\"Bold\"\n (mousedown)=\"$event.preventDefault(); bold()\"\n >\n <ngx-editor-icon name=\"bold\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.italic !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.italic\"\n title=\"Italic (Ctrl+I)\"\n aria-label=\"Italic\"\n (mousedown)=\"$event.preventDefault(); italic()\"\n >\n <ngx-editor-icon name=\"italic\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.underline !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.underline\"\n title=\"Underline (Ctrl+U)\"\n aria-label=\"Underline\"\n (mousedown)=\"$event.preventDefault(); underline()\"\n >\n <ngx-editor-icon name=\"underline\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.strikeThrough !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.strikeThrough\"\n title=\"Strikethrough\"\n aria-label=\"Strikethrough\"\n (mousedown)=\"$event.preventDefault(); strike()\"\n >\n <ngx-editor-icon name=\"strike\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.subscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.subscript\"\n title=\"Subscript\"\n aria-label=\"Subscript\"\n (mousedown)=\"$event.preventDefault(); subscript()\"\n >\n <ngx-editor-icon name=\"subscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.superscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.superscript\"\n title=\"Superscript\"\n aria-label=\"Superscript\"\n (mousedown)=\"$event.preventDefault(); superscript()\"\n >\n <ngx-editor-icon name=\"superscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.code !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Inline Code\"\n aria-label=\"Inline Code\"\n (mousedown)=\"$event.preventDefault(); wrapSelectionWithCode()\"\n >\n <ngx-editor-icon name=\"code\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Colors -->\n @if (computedConfig.toolbar?.color) {\n <div class=\"ngx-editor-toolbar-group\" style=\"position: relative;\">\n @if (computedConfig.toolbar?.color?.textColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Text Color\"\n aria-label=\"Text Color\"\n (click)=\"toggleColorPicker('text')\"\n >\n <ngx-editor-icon name=\"textColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedTextColor\"\n ></div>\n }\n\n @if (computedConfig.toolbar?.color?.backgroundColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Highlight Color\"\n aria-label=\"Highlight Color\"\n (click)=\"toggleColorPicker('background')\"\n >\n <ngx-editor-icon name=\"bgColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedBgColor\"\n ></div>\n }\n\n <!-- Color Popover -->\n @if (activeColorPicker) {\n <div class=\"ngx-color-popover\" (mousedown)=\"$event.stopPropagation()\">\n <div class=\"color-grid\">\n @for (color of computedConfig.colorPalette; track color) {\n <button\n type=\"button\"\n class=\"color-swatch\"\n [style.background-color]=\"color\"\n (click)=\"applyColor(color)\"\n ></button>\n }\n </div>\n <div class=\"custom-color-picker\">\n <input\n type=\"color\"\n (change)=\"onCustomColorChange($event)\"\n />\n <span>Custom color</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Alignment -->\n @if (computedConfig.toolbar?.alignment) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.alignment?.left !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyLeft\"\n title=\"Align Left\"\n aria-label=\"Align Left\"\n (mousedown)=\"$event.preventDefault(); alignLeft()\"\n >\n <ngx-editor-icon name=\"alignLeft\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.center !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyCenter\"\n title=\"Align Center\"\n aria-label=\"Align Center\"\n (mousedown)=\"$event.preventDefault(); alignCenter()\"\n >\n <ngx-editor-icon name=\"alignCenter\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.right !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyRight\"\n title=\"Align Right\"\n aria-label=\"Align Right\"\n (mousedown)=\"$event.preventDefault(); alignRight()\"\n >\n <ngx-editor-icon name=\"alignRight\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.justify !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyFull\"\n title=\"Justify\"\n aria-label=\"Justify\"\n (mousedown)=\"$event.preventDefault(); alignJustify()\"\n >\n <ngx-editor-icon name=\"alignJustify\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Lists & Indentation -->\n @if (computedConfig.toolbar?.list) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.list?.unordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertUnorderedList\"\n title=\"Bullet List\"\n aria-label=\"Bullet List\"\n (mousedown)=\"$event.preventDefault(); insertUnorderedList()\"\n >\n <ngx-editor-icon name=\"unorderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.ordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertOrderedList\"\n title=\"Numbered List\"\n aria-label=\"Numbered List\"\n (mousedown)=\"$event.preventDefault(); insertOrderedList()\"\n >\n <ngx-editor-icon name=\"orderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.indent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Increase Indent\"\n aria-label=\"Increase Indent\"\n (mousedown)=\"$event.preventDefault(); indent()\"\n >\n <ngx-editor-icon name=\"indent\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.outdent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Decrease Indent\"\n aria-label=\"Decrease Indent\"\n (mousedown)=\"$event.preventDefault(); outdent()\"\n >\n <ngx-editor-icon name=\"outdent\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Insert Options -->\n @if (computedConfig.toolbar?.insert) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.insert?.link !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.link\"\n title=\"Insert Link (Ctrl+K)\"\n aria-label=\"Insert Link\"\n (click)=\"openLinkModal()\"\n >\n <ngx-editor-icon name=\"link\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Remove Link\"\n aria-label=\"Remove Link\"\n (mousedown)=\"$event.preventDefault(); unlink()\"\n >\n <ngx-editor-icon name=\"unlink\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.image !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Insert Image\"\n aria-label=\"Insert Image\"\n (click)=\"openImageModal()\"\n >\n <ngx-editor-icon name=\"image\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.horizontalRule !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Horizontal Line\"\n aria-label=\"Horizontal Line\"\n (mousedown)=\"$event.preventDefault(); insertHorizontalRule()\"\n >\n <ngx-editor-icon name=\"horizontalRule\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Extra Actions -->\n @if (computedConfig.toolbar?.extra) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.extra?.clearFormat !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Clear Formatting\"\n aria-label=\"Clear Formatting\"\n (mousedown)=\"$event.preventDefault(); clearFormatting()\"\n >\n <ngx-editor-icon name=\"clearFormat\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.extra?.sourceView !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"isSourceView\"\n title=\"View HTML Source\"\n aria-label=\"View HTML Source\"\n (click)=\"toggleSourceView()\"\n >\n <ngx-editor-icon name=\"sourceView\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n }\n\n <!-- Editor Content Area -->\n <div class=\"ngx-editor-content-wrapper\">\n <!-- Visual contenteditable -->\n <div\n #editorContent\n class=\"ngx-editor-content\"\n [style.display]=\"isSourceView ? 'none' : 'block'\"\n [style.height]=\"computedConfig.height\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedContentStyle\"\n [attr.contenteditable]=\"!disabled && !readonly\"\n [attr.spellcheck]=\"computedConfig.spellcheck\"\n [attr.data-placeholder]=\"computedConfig.placeholder\"\n [attr.data-empty]=\"isEmpty\"\n (input)=\"onEditorInput()\"\n (focus)=\"onEditorFocus($event)\"\n (blur)=\"onEditorBlur($event)\"\n (keydown)=\"onEditorKeydown($event)\"\n (keyup)=\"updateEditorState()\"\n (mouseup)=\"updateEditorState()\"\n (paste)=\"onEditorPaste($event)\"\n ></div>\n\n <!-- Raw HTML Source Code View -->\n @if (isSourceView) {\n <textarea\n #sourceTextarea\n class=\"ngx-editor-source\"\n [style.height]=\"computedConfig.height\"\n [disabled]=\"disabled || readonly\"\n [value]=\"content\"\n (input)=\"onSourceInput($event)\"\n ></textarea>\n }\n </div>\n\n <!-- Status Bar -->\n @if (computedConfig.showStatusBar) {\n <div class=\"ngx-editor-statusbar\">\n <div class=\"statusbar-left\">\n @if (activeBlockTag) {\n <span class=\"status-tag\"><{{ activeBlockTag }}></span>\n }\n @if (isSourceView) {\n <span class=\"status-badge\">HTML SOURCE MODE</span>\n }\n </div>\n <div class=\"statusbar-right\">\n @if (computedConfig.showWordCount) {\n <span>Words: <strong>{{ stats.words }}</strong></span>\n }\n @if (computedConfig.maxCharacters) {\n <span [class.limit-reached]=\"stats.chars > computedConfig.maxCharacters\">\n {{ stats.chars }}/{{ computedConfig.maxCharacters }} Characters\n </span>\n } @else if (computedConfig.showCharCount) {\n <span>Characters: <strong>{{ stats.chars }}</strong></span>\n }\n </div>\n </div>\n }\n\n <!-- Link Modal -->\n @if (showLinkModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeLinkModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Link</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeLinkModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Link Text</label>\n <input\n type=\"text\"\n placeholder=\"Display text\"\n [(ngModel)]=\"linkModel.text\"\n />\n </div>\n <div class=\"form-group\">\n <label>Target URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com\"\n [(ngModel)]=\"linkModel.url\"\n (keydown.enter)=\"insertLinkFromModal()\"\n />\n </div>\n <div class=\"form-group\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"linkModel.newTab\" />\n Open in a new tab\n </label>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeLinkModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!linkModel.url\"\n (click)=\"insertLinkFromModal()\"\n >\n Insert Link\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Image Modal -->\n @if (showImageModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeImageModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Image</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeImageModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Image URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com/image.png\"\n [(ngModel)]=\"imageModel.url\"\n />\n </div>\n <div class=\"form-group\">\n <label>Or Upload File</label>\n <input\n type=\"file\"\n accept=\"image/*\"\n (change)=\"onImageFileUpload($event)\"\n />\n </div>\n <div class=\"form-group\">\n <label>Alt Description</label>\n <input\n type=\"text\"\n placeholder=\"Image description\"\n [(ngModel)]=\"imageModel.alt\"\n />\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeImageModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!imageModel.url\"\n (click)=\"insertImageFromModal()\"\n >\n Insert Image\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;font-family:var(--ngx-editor-font-family, inherit);box-sizing:border-box;--ngx-editor-font-family: inherit;--ngx-editor-font-size: 15px;--ngx-editor-line-height: 1.65;--ngx-editor-border: #e2e8f0;--ngx-editor-border-focus: #3b82f6;--ngx-editor-bg: #ffffff;--ngx-editor-toolbar-bg: #f8fafc;--ngx-editor-toolbar-btn-hover: #e2e8f0;--ngx-editor-toolbar-btn-active: #dbeafe;--ngx-editor-toolbar-btn-active-color: #1d4ed8;--ngx-editor-text: #1e293b;--ngx-editor-muted: #64748b;--ngx-editor-radius: 8px;--ngx-editor-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);--ngx-editor-shadow-focus: 0 0 0 3px rgba(59, 130, 246, .25);--ngx-editor-hashtag-color: #0069FF}.ngx-editor-container{display:flex;flex-direction:column;background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:var(--ngx-editor-radius);box-shadow:var(--ngx-editor-shadow);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease;position:relative}.ngx-editor-container.is-focused{border-color:var(--ngx-editor-border-focus);box-shadow:var(--ngx-editor-shadow-focus)}.ngx-editor-container.is-disabled{background-color:#f1f5f9;opacity:.75;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px 8px;background-color:var(--ngx-editor-toolbar-bg);border-bottom:1px solid var(--ngx-editor-border);-webkit-user-select:none;user-select:none;z-index:10}.ngx-editor-toolbar-group{display:flex;align-items:center;gap:2px}.ngx-editor-toolbar-group:not(:last-child):after{content:\"\";display:inline-block;width:1px;height:18px;background-color:var(--ngx-editor-border);margin:0 4px}.ngx-editor-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;color:var(--ngx-editor-text);cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease;outline:none}.ngx-editor-btn:hover:not(:disabled){background-color:var(--ngx-editor-toolbar-btn-hover)}.ngx-editor-btn:active:not(:disabled),.ngx-editor-btn.is-active{background-color:var(--ngx-editor-toolbar-btn-active);color:var(--ngx-editor-toolbar-btn-active-color);border-color:#bfdbfe}.ngx-editor-btn:disabled{opacity:.35;cursor:not-allowed}.ngx-editor-btn svg{display:block;pointer-events:none}.ngx-editor-select{height:32px;padding:0 8px;font-size:13px;color:var(--ngx-editor-text);background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:4px;outline:none;cursor:pointer;transition:border-color .15s ease}.ngx-editor-select:hover{border-color:#cbd5e1}.ngx-editor-select:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-color-btn{position:relative}.ngx-editor-color-btn .color-bar{position:absolute;bottom:4px;left:6px;right:6px;height:3px;border-radius:1px}.ngx-color-popover{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid var(--ngx-editor-border);border-radius:6px;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;padding:8px;z-index:50;width:176px}.ngx-color-popover .color-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:4px;margin-bottom:8px}.ngx-color-popover .color-swatch{width:22px;height:22px;border-radius:3px;border:1px solid rgba(0,0,0,.15);cursor:pointer;padding:0;transition:transform .1s ease}.ngx-color-popover .color-swatch:hover{transform:scale(1.15);z-index:2}.ngx-color-popover .custom-color-picker{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ngx-editor-muted)}.ngx-color-popover .custom-color-picker input[type=color]{width:28px;height:24px;border:1px solid var(--ngx-editor-border);border-radius:3px;padding:0;cursor:pointer;background:none}.ngx-editor-content-wrapper{position:relative;flex:1;display:flex;flex-direction:column}.ngx-editor-content{flex:1;padding:16px;outline:none;font-family:var(--ngx-editor-font-family, inherit);font-size:var(--ngx-editor-font-size, 15px);line-height:var(--ngx-editor-line-height, 1.65);color:var(--ngx-editor-text);overflow-y:auto;min-height:200px;word-break:break-word}.ngx-editor-content p{margin:0 0 1em}.ngx-editor-content b,.ngx-editor-content strong{font-weight:700!important}.ngx-editor-content i,.ngx-editor-content em{font-style:italic!important}.ngx-editor-content u,.ngx-editor-content .publish-editor-underline{text-decoration:underline!important;text-decoration-thickness:1px;text-underline-offset:.12em}.ngx-editor-content h1{font-size:2em;font-weight:700;margin:.67em 0;line-height:1.25}.ngx-editor-content h2{font-size:1.6em;font-weight:600;margin:.75em 0;line-height:1.3}.ngx-editor-content h3{font-size:1.3em;font-weight:600;margin:.83em 0}.ngx-editor-content h4{font-size:1.1em;font-weight:600;margin:1em 0}.ngx-editor-content ul,.ngx-editor-content ol{margin:0 0 1em 1.5em;padding:0}.ngx-editor-content li{margin-bottom:.25em}.ngx-editor-content blockquote{margin:1em 0;padding:.5em 1em;border-left:4px solid var(--ngx-editor-border-focus);background-color:#f8fafc;color:var(--ngx-editor-muted);font-style:italic}.ngx-editor-content pre{background-color:#1e293b;color:#f8fafc;padding:12px;border-radius:6px;font-family:Consolas,Monaco,Courier New,monospace;font-size:13px;overflow-x:auto;margin:1em 0}.ngx-editor-content code{background-color:#f1f5f9;color:#e11d48;padding:2px 5px;border-radius:4px;font-family:Consolas,Monaco,Courier New,monospace;font-size:.9em}.ngx-editor-content a{color:#2563eb;text-decoration:underline;text-underline-offset:2px;cursor:pointer}.ngx-editor-content .ngx-editor-hashtag,.ngx-editor-content .post-content-hashtag{color:var(--ngx-editor-hashtag-color, #0069FF)!important;font-weight:500;display:inline;cursor:text}.ngx-editor-content hr{border:none;border-top:1px solid var(--ngx-editor-border);margin:1.5em 0}.ngx-editor-content img{max-width:100%;height:auto;border-radius:4px}.ngx-editor-content[data-empty=true]:before{content:attr(data-placeholder);position:absolute;color:#94a3b8;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-content:has(ol,ul,li,table,img,hr,blockquote,pre):before{display:none!important;content:none!important}.ngx-editor-source{width:100%;flex:1;min-height:200px;padding:16px;border:none;outline:none;font-family:Consolas,Monaco,Courier New,monospace;font-size:13.5px;line-height:1.5;background-color:#0f172a;color:#38bdf8;resize:vertical;box-sizing:border-box}.ngx-editor-statusbar{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background-color:var(--ngx-editor-toolbar-bg);border-top:1px solid var(--ngx-editor-border);font-size:12px;color:var(--ngx-editor-muted)}.ngx-editor-statusbar .statusbar-left,.ngx-editor-statusbar .statusbar-right{display:flex;align-items:center;gap:12px}.ngx-editor-statusbar .status-tag{background-color:#e2e8f0;padding:2px 6px;border-radius:4px;font-weight:600;text-transform:uppercase;font-size:10px}.ngx-editor-statusbar .status-badge{background-color:#fee2e2;color:#ef4444;padding:2px 6px;border-radius:4px;font-weight:500;font-size:11px}.ngx-editor-modal-backdrop{position:absolute;inset:0;background:#0f172a66;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:100;animation:fadeIn .15s ease-out}.ngx-editor-modal{background:#fff;border-radius:8px;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;width:90%;max-width:400px;padding:20px;box-sizing:border-box}.ngx-editor-modal .modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.ngx-editor-modal .modal-header h3{margin:0;font-size:16px;font-weight:600;color:var(--ngx-editor-text)}.ngx-editor-modal .modal-body{display:flex;flex-direction:column;gap:12px}.ngx-editor-modal .form-group{display:flex;flex-direction:column;gap:4px}.ngx-editor-modal .form-group label{font-size:13px;font-weight:500;color:var(--ngx-editor-text)}.ngx-editor-modal .form-group input[type=text],.ngx-editor-modal .form-group input[type=url]{padding:8px 12px;border:1px solid var(--ngx-editor-border);border-radius:6px;font-size:14px;outline:none;transition:border-color .15s ease}.ngx-editor-modal .form-group input[type=text]:focus,.ngx-editor-modal .form-group input[type=url]:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-modal .form-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--ngx-editor-text);-webkit-user-select:none;user-select:none}.ngx-editor-modal .modal-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.ngx-editor-modal .modal-footer button{padding:7px 14px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:all .15s ease;outline:none}.ngx-editor-modal .modal-footer .btn-secondary{background:#f1f5f9;border:1px solid var(--ngx-editor-border);color:var(--ngx-editor-text)}.ngx-editor-modal .modal-footer .btn-secondary:hover{background:#e2e8f0}.ngx-editor-modal .modal-footer .btn-primary{background:#2563eb;border:1px solid #2563eb;color:#fff}.ngx-editor-modal .modal-footer .btn-primary:hover{background:#1d4ed8}.ngx-editor-modal .modal-footer .btn-primary:disabled{opacity:.5;cursor:not-allowed}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: NgxEditorIconComponent, selector: "ngx-editor-icon", inputs: ["name"] }, { kind: "component", type: NgxEditorToolbarComponent, selector: "ngx-editor-toolbar", inputs: ["editor", "tools", "fontFamilies"], outputs: ["actionTriggered"] }] });
|
|
3636
|
+
], viewQueries: [{ propertyName: "editorContentRef", first: true, predicate: ["editorContent"], descendants: true }, { propertyName: "sourceTextareaRef", first: true, predicate: ["sourceTextarea"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"ngx-editor-container\"\n [ngClass]=\"customClass\"\n [class.is-focused]=\"isFocused\"\n [class.is-disabled]=\"disabled\"\n [style.min-height]=\"computedConfig.minHeight\"\n [style.max-height]=\"computedConfig.maxHeight\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedEditorStyle\"\n>\n <!-- Toolbar -->\n @if (computedConfig.showToolbar && !readonly) {\n <!-- Array-configured Toolbar (if tools input is provided) -->\n @if (computedConfig.tools && computedConfig.tools.length > 0) {\n <ngx-editor-toolbar\n [editor]=\"this\"\n [tools]=\"computedConfig.tools\"\n [fontFamilies]=\"resolvedFontFamilies\"\n ></ngx-editor-toolbar>\n } @else {\n <!-- Default Full Toolbar -->\n <div class=\"ngx-editor-toolbar\" role=\"toolbar\" aria-label=\"Editor Toolbar\">\n <!-- History -->\n @if (computedConfig.toolbar?.history) {\n <div class=\"ngx-editor-toolbar-group\">\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Undo (Ctrl+Z)\"\n aria-label=\"Undo\"\n (mousedown)=\"$event.preventDefault(); undo()\"\n >\n <ngx-editor-icon name=\"undo\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Redo (Ctrl+Y)\"\n aria-label=\"Redo\"\n (mousedown)=\"$event.preventDefault(); redo()\"\n >\n <ngx-editor-icon name=\"redo\"></ngx-editor-icon>\n </button>\n </div>\n }\n\n <!-- Format Block -->\n @if (computedConfig.toolbar?.format) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Paragraph Format\"\n aria-label=\"Paragraph Format\"\n [value]=\"activeBlockTag\"\n (change)=\"onFormatBlockChange($event)\"\n >\n <option value=\"p\">Normal text</option>\n <option value=\"h1\">Heading 1</option>\n <option value=\"h2\">Heading 2</option>\n <option value=\"h3\">Heading 3</option>\n <option value=\"h4\">Heading 4</option>\n <option value=\"blockquote\">Quote</option>\n <option value=\"pre\">Code Block</option>\n </select>\n </div>\n }\n\n <!-- Font Family -->\n @if (computedConfig.toolbar?.fontName) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Family\"\n aria-label=\"Font Family\"\n (change)=\"onFontNameChange($event)\"\n >\n <option value=\"\">Font</option>\n @for (font of resolvedFontFamilies; track font.value) {\n <option [value]=\"font.value\">{{ font.label }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Font Size -->\n @if (computedConfig.toolbar?.fontSize) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Size\"\n aria-label=\"Font Size\"\n (change)=\"onFontSizeChange($event)\"\n >\n <option value=\"\">Size</option>\n <option value=\"1\">Small (10pt)</option>\n <option value=\"3\">Normal (12pt)</option>\n <option value=\"5\">Large (18pt)</option>\n <option value=\"7\">Huge (36pt)</option>\n </select>\n </div>\n }\n\n <!-- Inline Styles: Bold, Italic, Underline, Strike, Sub, Sup, Code -->\n @if (computedConfig.toolbar?.inline) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.inline?.bold !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.bold\"\n title=\"Bold (Ctrl+B)\"\n aria-label=\"Bold\"\n (mousedown)=\"$event.preventDefault(); bold()\"\n >\n <ngx-editor-icon name=\"bold\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.italic !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.italic\"\n title=\"Italic (Ctrl+I)\"\n aria-label=\"Italic\"\n (mousedown)=\"$event.preventDefault(); italic()\"\n >\n <ngx-editor-icon name=\"italic\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.underline !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.underline\"\n title=\"Underline (Ctrl+U)\"\n aria-label=\"Underline\"\n (mousedown)=\"$event.preventDefault(); underline()\"\n >\n <ngx-editor-icon name=\"underline\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.strikeThrough !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.strikeThrough\"\n title=\"Strikethrough\"\n aria-label=\"Strikethrough\"\n (mousedown)=\"$event.preventDefault(); strike()\"\n >\n <ngx-editor-icon name=\"strike\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.subscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.subscript\"\n title=\"Subscript\"\n aria-label=\"Subscript\"\n (mousedown)=\"$event.preventDefault(); subscript()\"\n >\n <ngx-editor-icon name=\"subscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.superscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.superscript\"\n title=\"Superscript\"\n aria-label=\"Superscript\"\n (mousedown)=\"$event.preventDefault(); superscript()\"\n >\n <ngx-editor-icon name=\"superscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.code !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Inline Code\"\n aria-label=\"Inline Code\"\n (mousedown)=\"$event.preventDefault(); wrapSelectionWithCode()\"\n >\n <ngx-editor-icon name=\"code\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Colors -->\n @if (computedConfig.toolbar?.color) {\n <div class=\"ngx-editor-toolbar-group\" style=\"position: relative;\">\n @if (computedConfig.toolbar?.color?.textColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Text Color\"\n aria-label=\"Text Color\"\n (click)=\"toggleColorPicker('text')\"\n >\n <ngx-editor-icon name=\"textColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedTextColor\"\n ></div>\n }\n\n @if (computedConfig.toolbar?.color?.backgroundColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Highlight Color\"\n aria-label=\"Highlight Color\"\n (click)=\"toggleColorPicker('background')\"\n >\n <ngx-editor-icon name=\"bgColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedBgColor\"\n ></div>\n }\n\n <!-- Color Popover -->\n @if (activeColorPicker) {\n <div class=\"ngx-color-popover\" (mousedown)=\"$event.stopPropagation()\">\n <div class=\"color-grid\">\n @for (color of computedConfig.colorPalette; track color) {\n <button\n type=\"button\"\n class=\"color-swatch\"\n [style.background-color]=\"color\"\n (click)=\"applyColor(color)\"\n ></button>\n }\n </div>\n <div class=\"custom-color-picker\">\n <input\n type=\"color\"\n (change)=\"onCustomColorChange($event)\"\n />\n <span>Custom color</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Alignment -->\n @if (computedConfig.toolbar?.alignment) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.alignment?.left !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyLeft\"\n title=\"Align Left\"\n aria-label=\"Align Left\"\n (mousedown)=\"$event.preventDefault(); alignLeft()\"\n >\n <ngx-editor-icon name=\"alignLeft\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.center !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyCenter\"\n title=\"Align Center\"\n aria-label=\"Align Center\"\n (mousedown)=\"$event.preventDefault(); alignCenter()\"\n >\n <ngx-editor-icon name=\"alignCenter\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.right !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyRight\"\n title=\"Align Right\"\n aria-label=\"Align Right\"\n (mousedown)=\"$event.preventDefault(); alignRight()\"\n >\n <ngx-editor-icon name=\"alignRight\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.justify !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyFull\"\n title=\"Justify\"\n aria-label=\"Justify\"\n (mousedown)=\"$event.preventDefault(); alignJustify()\"\n >\n <ngx-editor-icon name=\"alignJustify\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Lists & Indentation -->\n @if (computedConfig.toolbar?.list) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.list?.unordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertUnorderedList\"\n title=\"Bullet List\"\n aria-label=\"Bullet List\"\n (mousedown)=\"$event.preventDefault(); insertUnorderedList()\"\n >\n <ngx-editor-icon name=\"unorderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.ordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertOrderedList\"\n title=\"Numbered List\"\n aria-label=\"Numbered List\"\n (mousedown)=\"$event.preventDefault(); insertOrderedList()\"\n >\n <ngx-editor-icon name=\"orderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.indent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Increase Indent\"\n aria-label=\"Increase Indent\"\n (mousedown)=\"$event.preventDefault(); indent()\"\n >\n <ngx-editor-icon name=\"indent\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.outdent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Decrease Indent\"\n aria-label=\"Decrease Indent\"\n (mousedown)=\"$event.preventDefault(); outdent()\"\n >\n <ngx-editor-icon name=\"outdent\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Insert Options -->\n @if (computedConfig.toolbar?.insert) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.insert?.link !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.link\"\n title=\"Insert Link (Ctrl+K)\"\n aria-label=\"Insert Link\"\n (click)=\"openLinkModal()\"\n >\n <ngx-editor-icon name=\"link\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Remove Link\"\n aria-label=\"Remove Link\"\n (mousedown)=\"$event.preventDefault(); unlink()\"\n >\n <ngx-editor-icon name=\"unlink\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.image !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Insert Image\"\n aria-label=\"Insert Image\"\n (click)=\"openImageModal()\"\n >\n <ngx-editor-icon name=\"image\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.horizontalRule !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Horizontal Line\"\n aria-label=\"Horizontal Line\"\n (mousedown)=\"$event.preventDefault(); insertHorizontalRule()\"\n >\n <ngx-editor-icon name=\"horizontalRule\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Extra Actions -->\n @if (computedConfig.toolbar?.extra) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.extra?.clearFormat !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Clear Formatting\"\n aria-label=\"Clear Formatting\"\n (mousedown)=\"$event.preventDefault(); clearFormatting()\"\n >\n <ngx-editor-icon name=\"clearFormat\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.extra?.sourceView !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"isSourceView\"\n title=\"View HTML Source\"\n aria-label=\"View HTML Source\"\n (click)=\"toggleSourceView()\"\n >\n <ngx-editor-icon name=\"sourceView\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n }\n\n <!-- Editor Content Area -->\n <div class=\"ngx-editor-content-wrapper\">\n <!-- Visual contenteditable -->\n <div\n #editorContent\n class=\"ngx-editor-content\"\n [style.display]=\"isSourceView ? 'none' : 'block'\"\n [style.height]=\"computedConfig.height\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedContentStyle\"\n [attr.contenteditable]=\"!disabled && !readonly\"\n [attr.spellcheck]=\"computedConfig.spellcheck\"\n [attr.data-placeholder]=\"computedConfig.placeholder\"\n [attr.data-empty]=\"isEmpty\"\n (input)=\"onEditorInput()\"\n (focus)=\"onEditorFocus($event)\"\n (blur)=\"onEditorBlur($event)\"\n (keydown)=\"onEditorKeydown($event)\"\n (keyup)=\"updateEditorState()\"\n (mouseup)=\"updateEditorState()\"\n (paste)=\"onEditorPaste($event)\"\n ></div>\n\n <!-- Raw HTML Source Code View -->\n @if (isSourceView) {\n <textarea\n #sourceTextarea\n class=\"ngx-editor-source\"\n [style.height]=\"computedConfig.height\"\n [disabled]=\"disabled || readonly\"\n [value]=\"content\"\n (input)=\"onSourceInput($event)\"\n ></textarea>\n }\n </div>\n\n <!-- Status Bar -->\n @if (computedConfig.showStatusBar) {\n <div class=\"ngx-editor-statusbar\">\n <div class=\"statusbar-left\">\n @if (activeBlockTag) {\n <span class=\"status-tag\"><{{ activeBlockTag }}></span>\n }\n @if (isSourceView) {\n <span class=\"status-badge\">HTML SOURCE MODE</span>\n }\n </div>\n <div class=\"statusbar-right\">\n @if (computedConfig.showWordCount) {\n <span>Words: <strong>{{ stats.words }}</strong></span>\n }\n @if (computedConfig.maxCharacters) {\n <span [class.limit-reached]=\"stats.chars > computedConfig.maxCharacters\">\n {{ stats.chars }}/{{ computedConfig.maxCharacters }} Characters\n </span>\n } @else if (computedConfig.showCharCount) {\n <span>Characters: <strong>{{ stats.chars }}</strong></span>\n }\n </div>\n </div>\n }\n\n <!-- Link Modal -->\n @if (showLinkModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeLinkModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Link</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeLinkModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Link Text</label>\n <input\n type=\"text\"\n placeholder=\"Display text\"\n [(ngModel)]=\"linkModel.text\"\n />\n </div>\n <div class=\"form-group\">\n <label>Target URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com\"\n [(ngModel)]=\"linkModel.url\"\n (keydown.enter)=\"insertLinkFromModal()\"\n />\n </div>\n <div class=\"form-group\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"linkModel.newTab\" />\n Open in a new tab\n </label>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeLinkModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!linkModel.url\"\n (click)=\"insertLinkFromModal()\"\n >\n Insert Link\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Image Modal -->\n @if (showImageModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeImageModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Image</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeImageModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Image URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com/image.png\"\n [(ngModel)]=\"imageModel.url\"\n />\n </div>\n <div class=\"form-group\">\n <label>Or Upload File</label>\n <input\n type=\"file\"\n accept=\"image/*\"\n (change)=\"onImageFileUpload($event)\"\n />\n </div>\n <div class=\"form-group\">\n <label>Alt Description</label>\n <input\n type=\"text\"\n placeholder=\"Image description\"\n [(ngModel)]=\"imageModel.alt\"\n />\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeImageModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!imageModel.url\"\n (click)=\"insertImageFromModal()\"\n >\n Insert Image\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;font-family:var(--ngx-editor-font-family, inherit);box-sizing:border-box;--ngx-editor-font-family: inherit;--ngx-editor-font-size: 15px;--ngx-editor-line-height: 1.65;--ngx-editor-border: #e2e8f0;--ngx-editor-border-focus: #3b82f6;--ngx-editor-bg: #ffffff;--ngx-editor-toolbar-bg: #f8fafc;--ngx-editor-toolbar-btn-hover: #e2e8f0;--ngx-editor-toolbar-btn-active: #dbeafe;--ngx-editor-toolbar-btn-active-color: #1d4ed8;--ngx-editor-text: #1e293b;--ngx-editor-muted: #64748b;--ngx-editor-radius: 8px;--ngx-editor-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);--ngx-editor-shadow-focus: 0 0 0 3px rgba(59, 130, 246, .25);--ngx-editor-hashtag-color: #0069FF}.ngx-editor-container{display:flex;flex-direction:column;background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:var(--ngx-editor-radius);box-shadow:var(--ngx-editor-shadow);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease;position:relative}.ngx-editor-container.is-focused{border-color:var(--ngx-editor-border-focus);box-shadow:var(--ngx-editor-shadow-focus)}.ngx-editor-container.is-disabled{background-color:#f1f5f9;opacity:.75;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px 8px;background-color:var(--ngx-editor-toolbar-bg);border-bottom:1px solid var(--ngx-editor-border);-webkit-user-select:none;user-select:none;z-index:10}.ngx-editor-toolbar-group{display:flex;align-items:center;gap:2px}.ngx-editor-toolbar-group:not(:last-child):after{content:\"\";display:inline-block;width:1px;height:18px;background-color:var(--ngx-editor-border);margin:0 4px}.ngx-editor-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;color:var(--ngx-editor-text);cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease;outline:none}.ngx-editor-btn:hover:not(:disabled){background-color:var(--ngx-editor-toolbar-btn-hover)}.ngx-editor-btn:active:not(:disabled),.ngx-editor-btn.is-active{background-color:var(--ngx-editor-toolbar-btn-active);color:var(--ngx-editor-toolbar-btn-active-color);border-color:#bfdbfe}.ngx-editor-btn:disabled{opacity:.35;cursor:not-allowed}.ngx-editor-btn svg{display:block;pointer-events:none}.ngx-editor-select{height:32px;padding:0 8px;font-size:13px;color:var(--ngx-editor-text);background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:4px;outline:none;cursor:pointer;transition:border-color .15s ease}.ngx-editor-select:hover{border-color:#cbd5e1}.ngx-editor-select:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-color-btn{position:relative}.ngx-editor-color-btn .color-bar{position:absolute;bottom:4px;left:6px;right:6px;height:3px;border-radius:1px}.ngx-color-popover{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid var(--ngx-editor-border);border-radius:6px;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;padding:8px;z-index:50;width:176px}.ngx-color-popover .color-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:4px;margin-bottom:8px}.ngx-color-popover .color-swatch{width:22px;height:22px;border-radius:3px;border:1px solid rgba(0,0,0,.15);cursor:pointer;padding:0;transition:transform .1s ease}.ngx-color-popover .color-swatch:hover{transform:scale(1.15);z-index:2}.ngx-color-popover .custom-color-picker{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ngx-editor-muted)}.ngx-color-popover .custom-color-picker input[type=color]{width:28px;height:24px;border:1px solid var(--ngx-editor-border);border-radius:3px;padding:0;cursor:pointer;background:none}.ngx-editor-content-wrapper{position:relative;flex:1;display:flex;flex-direction:column}.ngx-editor-content{flex:1;padding:16px;outline:none;font-family:var(--ngx-editor-font-family, inherit);font-size:var(--ngx-editor-font-size, 15px);line-height:var(--ngx-editor-line-height, 1.65);color:var(--ngx-editor-text);overflow-y:auto;min-height:200px;word-break:break-word}.ngx-editor-content p{margin:0;min-height:1.65em}.ngx-editor-content b,.ngx-editor-content strong{font-weight:700!important}.ngx-editor-content i,.ngx-editor-content em{font-style:italic!important}.ngx-editor-content u,.ngx-editor-content .publish-editor-underline{text-decoration:underline!important;text-decoration-thickness:1px;text-underline-offset:.12em}.ngx-editor-content h1{font-size:2em;font-weight:700;margin:.67em 0;line-height:1.25}.ngx-editor-content h2{font-size:1.6em;font-weight:600;margin:.75em 0;line-height:1.3}.ngx-editor-content h3{font-size:1.3em;font-weight:600;margin:.83em 0}.ngx-editor-content h4{font-size:1.1em;font-weight:600;margin:1em 0}.ngx-editor-content ul,.ngx-editor-content ol{margin:0 0 1em 1.5em;padding:0}.ngx-editor-content li{margin-bottom:.25em}.ngx-editor-content blockquote{margin:1em 0;padding:.5em 1em;border-left:4px solid var(--ngx-editor-border-focus);background-color:#f8fafc;color:var(--ngx-editor-muted);font-style:italic}.ngx-editor-content pre{background-color:#1e293b;color:#f8fafc;padding:12px;border-radius:6px;font-family:Consolas,Monaco,Courier New,monospace;font-size:13px;overflow-x:auto;margin:1em 0}.ngx-editor-content code{background-color:#f1f5f9;color:#e11d48;padding:2px 5px;border-radius:4px;font-family:Consolas,Monaco,Courier New,monospace;font-size:.9em}.ngx-editor-content a{color:#2563eb;text-decoration:underline;text-underline-offset:2px;cursor:pointer}.ngx-editor-content .ngx-editor-hashtag,.ngx-editor-content .post-content-hashtag{color:var(--ngx-editor-hashtag-color, #0069FF)!important;font-weight:500;display:inline;cursor:text}.ngx-editor-content hr{border:none;border-top:1px solid var(--ngx-editor-border);margin:1.5em 0}.ngx-editor-content img{max-width:100%;height:auto;border-radius:4px}.ngx-editor-content[data-empty=true]:before{content:attr(data-placeholder);position:absolute;color:#94a3b8;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-content:has(ol,ul,li,table,img,hr,blockquote,pre):before{display:none!important;content:none!important}.ngx-editor-source{width:100%;flex:1;min-height:200px;padding:16px;border:none;outline:none;font-family:Consolas,Monaco,Courier New,monospace;font-size:13.5px;line-height:1.5;background-color:#0f172a;color:#38bdf8;resize:vertical;box-sizing:border-box}.ngx-editor-statusbar{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background-color:var(--ngx-editor-toolbar-bg);border-top:1px solid var(--ngx-editor-border);font-size:12px;color:var(--ngx-editor-muted)}.ngx-editor-statusbar .statusbar-left,.ngx-editor-statusbar .statusbar-right{display:flex;align-items:center;gap:12px}.ngx-editor-statusbar .status-tag{background-color:#e2e8f0;padding:2px 6px;border-radius:4px;font-weight:600;text-transform:uppercase;font-size:10px}.ngx-editor-statusbar .status-badge{background-color:#fee2e2;color:#ef4444;padding:2px 6px;border-radius:4px;font-weight:500;font-size:11px}.ngx-editor-modal-backdrop{position:absolute;inset:0;background:#0f172a66;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:100;animation:fadeIn .15s ease-out}.ngx-editor-modal{background:#fff;border-radius:8px;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;width:90%;max-width:400px;padding:20px;box-sizing:border-box}.ngx-editor-modal .modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.ngx-editor-modal .modal-header h3{margin:0;font-size:16px;font-weight:600;color:var(--ngx-editor-text)}.ngx-editor-modal .modal-body{display:flex;flex-direction:column;gap:12px}.ngx-editor-modal .form-group{display:flex;flex-direction:column;gap:4px}.ngx-editor-modal .form-group label{font-size:13px;font-weight:500;color:var(--ngx-editor-text)}.ngx-editor-modal .form-group input[type=text],.ngx-editor-modal .form-group input[type=url]{padding:8px 12px;border:1px solid var(--ngx-editor-border);border-radius:6px;font-size:14px;outline:none;transition:border-color .15s ease}.ngx-editor-modal .form-group input[type=text]:focus,.ngx-editor-modal .form-group input[type=url]:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-modal .form-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--ngx-editor-text);-webkit-user-select:none;user-select:none}.ngx-editor-modal .modal-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.ngx-editor-modal .modal-footer button{padding:7px 14px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:all .15s ease;outline:none}.ngx-editor-modal .modal-footer .btn-secondary{background:#f1f5f9;border:1px solid var(--ngx-editor-border);color:var(--ngx-editor-text)}.ngx-editor-modal .modal-footer .btn-secondary:hover{background:#e2e8f0}.ngx-editor-modal .modal-footer .btn-primary{background:#2563eb;border:1px solid #2563eb;color:#fff}.ngx-editor-modal .modal-footer .btn-primary:hover{background:#1d4ed8}.ngx-editor-modal .modal-footer .btn-primary:disabled{opacity:.5;cursor:not-allowed}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: NgxEditorIconComponent, selector: "ngx-editor-icon", inputs: ["name"] }, { kind: "component", type: NgxEditorToolbarComponent, selector: "ngx-editor-toolbar", inputs: ["editor", "tools", "fontFamilies"], outputs: ["actionTriggered"] }] });
|
|
2706
3637
|
}
|
|
2707
3638
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NgxRichTextEditorComponent, decorators: [{
|
|
2708
3639
|
type: Component,
|
|
@@ -2712,7 +3643,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2712
3643
|
useExisting: forwardRef(() => NgxRichTextEditorComponent),
|
|
2713
3644
|
multi: true,
|
|
2714
3645
|
},
|
|
2715
|
-
], template: "<div\n class=\"ngx-editor-container\"\n [ngClass]=\"customClass\"\n [class.is-focused]=\"isFocused\"\n [class.is-disabled]=\"disabled\"\n [style.min-height]=\"computedConfig.minHeight\"\n [style.max-height]=\"computedConfig.maxHeight\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedEditorStyle\"\n>\n <!-- Toolbar -->\n @if (computedConfig.showToolbar && !readonly) {\n <!-- Array-configured Toolbar (if tools input is provided) -->\n @if (computedConfig.tools && computedConfig.tools.length > 0) {\n <ngx-editor-toolbar\n [editor]=\"this\"\n [tools]=\"computedConfig.tools\"\n [fontFamilies]=\"resolvedFontFamilies\"\n ></ngx-editor-toolbar>\n } @else {\n <!-- Default Full Toolbar -->\n <div class=\"ngx-editor-toolbar\" role=\"toolbar\" aria-label=\"Editor Toolbar\">\n <!-- History -->\n @if (computedConfig.toolbar?.history) {\n <div class=\"ngx-editor-toolbar-group\">\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Undo (Ctrl+Z)\"\n aria-label=\"Undo\"\n (mousedown)=\"$event.preventDefault(); undo()\"\n >\n <ngx-editor-icon name=\"undo\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Redo (Ctrl+Y)\"\n aria-label=\"Redo\"\n (mousedown)=\"$event.preventDefault(); redo()\"\n >\n <ngx-editor-icon name=\"redo\"></ngx-editor-icon>\n </button>\n </div>\n }\n\n <!-- Format Block -->\n @if (computedConfig.toolbar?.format) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Paragraph Format\"\n aria-label=\"Paragraph Format\"\n [value]=\"activeBlockTag\"\n (change)=\"onFormatBlockChange($event)\"\n >\n <option value=\"p\">Normal text</option>\n <option value=\"h1\">Heading 1</option>\n <option value=\"h2\">Heading 2</option>\n <option value=\"h3\">Heading 3</option>\n <option value=\"h4\">Heading 4</option>\n <option value=\"blockquote\">Quote</option>\n <option value=\"pre\">Code Block</option>\n </select>\n </div>\n }\n\n <!-- Font Family -->\n @if (computedConfig.toolbar?.fontName) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Family\"\n aria-label=\"Font Family\"\n (change)=\"onFontNameChange($event)\"\n >\n <option value=\"\">Font</option>\n @for (font of resolvedFontFamilies; track font.value) {\n <option [value]=\"font.value\">{{ font.label }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Font Size -->\n @if (computedConfig.toolbar?.fontSize) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Size\"\n aria-label=\"Font Size\"\n (change)=\"onFontSizeChange($event)\"\n >\n <option value=\"\">Size</option>\n <option value=\"1\">Small (10pt)</option>\n <option value=\"3\">Normal (12pt)</option>\n <option value=\"5\">Large (18pt)</option>\n <option value=\"7\">Huge (36pt)</option>\n </select>\n </div>\n }\n\n <!-- Inline Styles: Bold, Italic, Underline, Strike, Sub, Sup, Code -->\n @if (computedConfig.toolbar?.inline) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.inline?.bold !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.bold\"\n title=\"Bold (Ctrl+B)\"\n aria-label=\"Bold\"\n (mousedown)=\"$event.preventDefault(); bold()\"\n >\n <ngx-editor-icon name=\"bold\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.italic !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.italic\"\n title=\"Italic (Ctrl+I)\"\n aria-label=\"Italic\"\n (mousedown)=\"$event.preventDefault(); italic()\"\n >\n <ngx-editor-icon name=\"italic\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.underline !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.underline\"\n title=\"Underline (Ctrl+U)\"\n aria-label=\"Underline\"\n (mousedown)=\"$event.preventDefault(); underline()\"\n >\n <ngx-editor-icon name=\"underline\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.strikeThrough !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.strikeThrough\"\n title=\"Strikethrough\"\n aria-label=\"Strikethrough\"\n (mousedown)=\"$event.preventDefault(); strike()\"\n >\n <ngx-editor-icon name=\"strike\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.subscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.subscript\"\n title=\"Subscript\"\n aria-label=\"Subscript\"\n (mousedown)=\"$event.preventDefault(); subscript()\"\n >\n <ngx-editor-icon name=\"subscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.superscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.superscript\"\n title=\"Superscript\"\n aria-label=\"Superscript\"\n (mousedown)=\"$event.preventDefault(); superscript()\"\n >\n <ngx-editor-icon name=\"superscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.code !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Inline Code\"\n aria-label=\"Inline Code\"\n (mousedown)=\"$event.preventDefault(); wrapSelectionWithCode()\"\n >\n <ngx-editor-icon name=\"code\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Colors -->\n @if (computedConfig.toolbar?.color) {\n <div class=\"ngx-editor-toolbar-group\" style=\"position: relative;\">\n @if (computedConfig.toolbar?.color?.textColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Text Color\"\n aria-label=\"Text Color\"\n (click)=\"toggleColorPicker('text')\"\n >\n <ngx-editor-icon name=\"textColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedTextColor\"\n ></div>\n }\n\n @if (computedConfig.toolbar?.color?.backgroundColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Highlight Color\"\n aria-label=\"Highlight Color\"\n (click)=\"toggleColorPicker('background')\"\n >\n <ngx-editor-icon name=\"bgColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedBgColor\"\n ></div>\n }\n\n <!-- Color Popover -->\n @if (activeColorPicker) {\n <div class=\"ngx-color-popover\" (mousedown)=\"$event.stopPropagation()\">\n <div class=\"color-grid\">\n @for (color of computedConfig.colorPalette; track color) {\n <button\n type=\"button\"\n class=\"color-swatch\"\n [style.background-color]=\"color\"\n (click)=\"applyColor(color)\"\n ></button>\n }\n </div>\n <div class=\"custom-color-picker\">\n <input\n type=\"color\"\n (change)=\"onCustomColorChange($event)\"\n />\n <span>Custom color</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Alignment -->\n @if (computedConfig.toolbar?.alignment) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.alignment?.left !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyLeft\"\n title=\"Align Left\"\n aria-label=\"Align Left\"\n (mousedown)=\"$event.preventDefault(); alignLeft()\"\n >\n <ngx-editor-icon name=\"alignLeft\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.center !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyCenter\"\n title=\"Align Center\"\n aria-label=\"Align Center\"\n (mousedown)=\"$event.preventDefault(); alignCenter()\"\n >\n <ngx-editor-icon name=\"alignCenter\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.right !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyRight\"\n title=\"Align Right\"\n aria-label=\"Align Right\"\n (mousedown)=\"$event.preventDefault(); alignRight()\"\n >\n <ngx-editor-icon name=\"alignRight\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.justify !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyFull\"\n title=\"Justify\"\n aria-label=\"Justify\"\n (mousedown)=\"$event.preventDefault(); alignJustify()\"\n >\n <ngx-editor-icon name=\"alignJustify\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Lists & Indentation -->\n @if (computedConfig.toolbar?.list) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.list?.unordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertUnorderedList\"\n title=\"Bullet List\"\n aria-label=\"Bullet List\"\n (mousedown)=\"$event.preventDefault(); insertUnorderedList()\"\n >\n <ngx-editor-icon name=\"unorderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.ordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertOrderedList\"\n title=\"Numbered List\"\n aria-label=\"Numbered List\"\n (mousedown)=\"$event.preventDefault(); insertOrderedList()\"\n >\n <ngx-editor-icon name=\"orderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.indent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Increase Indent\"\n aria-label=\"Increase Indent\"\n (mousedown)=\"$event.preventDefault(); indent()\"\n >\n <ngx-editor-icon name=\"indent\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.outdent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Decrease Indent\"\n aria-label=\"Decrease Indent\"\n (mousedown)=\"$event.preventDefault(); outdent()\"\n >\n <ngx-editor-icon name=\"outdent\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Insert Options -->\n @if (computedConfig.toolbar?.insert) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.insert?.link !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.link\"\n title=\"Insert Link (Ctrl+K)\"\n aria-label=\"Insert Link\"\n (click)=\"openLinkModal()\"\n >\n <ngx-editor-icon name=\"link\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Remove Link\"\n aria-label=\"Remove Link\"\n (mousedown)=\"$event.preventDefault(); unlink()\"\n >\n <ngx-editor-icon name=\"unlink\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.image !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Insert Image\"\n aria-label=\"Insert Image\"\n (click)=\"openImageModal()\"\n >\n <ngx-editor-icon name=\"image\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.horizontalRule !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Horizontal Line\"\n aria-label=\"Horizontal Line\"\n (mousedown)=\"$event.preventDefault(); insertHorizontalRule()\"\n >\n <ngx-editor-icon name=\"horizontalRule\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Extra Actions -->\n @if (computedConfig.toolbar?.extra) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.extra?.clearFormat !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Clear Formatting\"\n aria-label=\"Clear Formatting\"\n (mousedown)=\"$event.preventDefault(); clearFormatting()\"\n >\n <ngx-editor-icon name=\"clearFormat\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.extra?.sourceView !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"isSourceView\"\n title=\"View HTML Source\"\n aria-label=\"View HTML Source\"\n (click)=\"toggleSourceView()\"\n >\n <ngx-editor-icon name=\"sourceView\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n }\n\n <!-- Editor Content Area -->\n <div class=\"ngx-editor-content-wrapper\">\n <!-- Visual contenteditable -->\n <div\n #editorContent\n class=\"ngx-editor-content\"\n [style.display]=\"isSourceView ? 'none' : 'block'\"\n [style.height]=\"computedConfig.height\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedContentStyle\"\n [attr.contenteditable]=\"!disabled && !readonly\"\n [attr.spellcheck]=\"computedConfig.spellcheck\"\n [attr.data-placeholder]=\"computedConfig.placeholder\"\n [attr.data-empty]=\"isEmpty\"\n (input)=\"onEditorInput()\"\n (focus)=\"onEditorFocus($event)\"\n (blur)=\"onEditorBlur($event)\"\n (keydown)=\"onEditorKeydown($event)\"\n (keyup)=\"updateEditorState()\"\n (mouseup)=\"updateEditorState()\"\n (paste)=\"onEditorPaste($event)\"\n ></div>\n\n <!-- Raw HTML Source Code View -->\n @if (isSourceView) {\n <textarea\n #sourceTextarea\n class=\"ngx-editor-source\"\n [style.height]=\"computedConfig.height\"\n [disabled]=\"disabled || readonly\"\n [value]=\"content\"\n (input)=\"onSourceInput($event)\"\n ></textarea>\n }\n </div>\n\n <!-- Status Bar -->\n @if (computedConfig.showStatusBar) {\n <div class=\"ngx-editor-statusbar\">\n <div class=\"statusbar-left\">\n @if (activeBlockTag) {\n <span class=\"status-tag\"><{{ activeBlockTag }}></span>\n }\n @if (isSourceView) {\n <span class=\"status-badge\">HTML SOURCE MODE</span>\n }\n </div>\n <div class=\"statusbar-right\">\n @if (computedConfig.showWordCount) {\n <span>Words: <strong>{{ stats.words }}</strong></span>\n }\n @if (computedConfig.maxCharacters) {\n <span [class.limit-reached]=\"stats.chars > computedConfig.maxCharacters\">\n {{ stats.chars }}/{{ computedConfig.maxCharacters }} Characters\n </span>\n } @else if (computedConfig.showCharCount) {\n <span>Characters: <strong>{{ stats.chars }}</strong></span>\n }\n </div>\n </div>\n }\n\n <!-- Link Modal -->\n @if (showLinkModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeLinkModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Link</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeLinkModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Link Text</label>\n <input\n type=\"text\"\n placeholder=\"Display text\"\n [(ngModel)]=\"linkModel.text\"\n />\n </div>\n <div class=\"form-group\">\n <label>Target URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com\"\n [(ngModel)]=\"linkModel.url\"\n (keydown.enter)=\"insertLinkFromModal()\"\n />\n </div>\n <div class=\"form-group\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"linkModel.newTab\" />\n Open in a new tab\n </label>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeLinkModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!linkModel.url\"\n (click)=\"insertLinkFromModal()\"\n >\n Insert Link\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Image Modal -->\n @if (showImageModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeImageModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Image</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeImageModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Image URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com/image.png\"\n [(ngModel)]=\"imageModel.url\"\n />\n </div>\n <div class=\"form-group\">\n <label>Or Upload File</label>\n <input\n type=\"file\"\n accept=\"image/*\"\n (change)=\"onImageFileUpload($event)\"\n />\n </div>\n <div class=\"form-group\">\n <label>Alt Description</label>\n <input\n type=\"text\"\n placeholder=\"Image description\"\n [(ngModel)]=\"imageModel.alt\"\n />\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeImageModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!imageModel.url\"\n (click)=\"insertImageFromModal()\"\n >\n Insert Image\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;font-family:var(--ngx-editor-font-family, inherit);box-sizing:border-box;--ngx-editor-font-family: inherit;--ngx-editor-font-size: 15px;--ngx-editor-line-height: 1.65;--ngx-editor-border: #e2e8f0;--ngx-editor-border-focus: #3b82f6;--ngx-editor-bg: #ffffff;--ngx-editor-toolbar-bg: #f8fafc;--ngx-editor-toolbar-btn-hover: #e2e8f0;--ngx-editor-toolbar-btn-active: #dbeafe;--ngx-editor-toolbar-btn-active-color: #1d4ed8;--ngx-editor-text: #1e293b;--ngx-editor-muted: #64748b;--ngx-editor-radius: 8px;--ngx-editor-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);--ngx-editor-shadow-focus: 0 0 0 3px rgba(59, 130, 246, .25);--ngx-editor-hashtag-color: #0069FF}.ngx-editor-container{display:flex;flex-direction:column;background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:var(--ngx-editor-radius);box-shadow:var(--ngx-editor-shadow);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease;position:relative}.ngx-editor-container.is-focused{border-color:var(--ngx-editor-border-focus);box-shadow:var(--ngx-editor-shadow-focus)}.ngx-editor-container.is-disabled{background-color:#f1f5f9;opacity:.75;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px 8px;background-color:var(--ngx-editor-toolbar-bg);border-bottom:1px solid var(--ngx-editor-border);-webkit-user-select:none;user-select:none;z-index:10}.ngx-editor-toolbar-group{display:flex;align-items:center;gap:2px}.ngx-editor-toolbar-group:not(:last-child):after{content:\"\";display:inline-block;width:1px;height:18px;background-color:var(--ngx-editor-border);margin:0 4px}.ngx-editor-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;color:var(--ngx-editor-text);cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease;outline:none}.ngx-editor-btn:hover:not(:disabled){background-color:var(--ngx-editor-toolbar-btn-hover)}.ngx-editor-btn:active:not(:disabled),.ngx-editor-btn.is-active{background-color:var(--ngx-editor-toolbar-btn-active);color:var(--ngx-editor-toolbar-btn-active-color);border-color:#bfdbfe}.ngx-editor-btn:disabled{opacity:.35;cursor:not-allowed}.ngx-editor-btn svg{display:block;pointer-events:none}.ngx-editor-select{height:32px;padding:0 8px;font-size:13px;color:var(--ngx-editor-text);background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:4px;outline:none;cursor:pointer;transition:border-color .15s ease}.ngx-editor-select:hover{border-color:#cbd5e1}.ngx-editor-select:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-color-btn{position:relative}.ngx-editor-color-btn .color-bar{position:absolute;bottom:4px;left:6px;right:6px;height:3px;border-radius:1px}.ngx-color-popover{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid var(--ngx-editor-border);border-radius:6px;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;padding:8px;z-index:50;width:176px}.ngx-color-popover .color-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:4px;margin-bottom:8px}.ngx-color-popover .color-swatch{width:22px;height:22px;border-radius:3px;border:1px solid rgba(0,0,0,.15);cursor:pointer;padding:0;transition:transform .1s ease}.ngx-color-popover .color-swatch:hover{transform:scale(1.15);z-index:2}.ngx-color-popover .custom-color-picker{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ngx-editor-muted)}.ngx-color-popover .custom-color-picker input[type=color]{width:28px;height:24px;border:1px solid var(--ngx-editor-border);border-radius:3px;padding:0;cursor:pointer;background:none}.ngx-editor-content-wrapper{position:relative;flex:1;display:flex;flex-direction:column}.ngx-editor-content{flex:1;padding:16px;outline:none;font-family:var(--ngx-editor-font-family, inherit);font-size:var(--ngx-editor-font-size, 15px);line-height:var(--ngx-editor-line-height, 1.65);color:var(--ngx-editor-text);overflow-y:auto;min-height:200px;word-break:break-word}.ngx-editor-content p{margin:0 0 1em}.ngx-editor-content b,.ngx-editor-content strong{font-weight:700!important}.ngx-editor-content i,.ngx-editor-content em{font-style:italic!important}.ngx-editor-content u,.ngx-editor-content .publish-editor-underline{text-decoration:underline!important;text-decoration-thickness:1px;text-underline-offset:.12em}.ngx-editor-content h1{font-size:2em;font-weight:700;margin:.67em 0;line-height:1.25}.ngx-editor-content h2{font-size:1.6em;font-weight:600;margin:.75em 0;line-height:1.3}.ngx-editor-content h3{font-size:1.3em;font-weight:600;margin:.83em 0}.ngx-editor-content h4{font-size:1.1em;font-weight:600;margin:1em 0}.ngx-editor-content ul,.ngx-editor-content ol{margin:0 0 1em 1.5em;padding:0}.ngx-editor-content li{margin-bottom:.25em}.ngx-editor-content blockquote{margin:1em 0;padding:.5em 1em;border-left:4px solid var(--ngx-editor-border-focus);background-color:#f8fafc;color:var(--ngx-editor-muted);font-style:italic}.ngx-editor-content pre{background-color:#1e293b;color:#f8fafc;padding:12px;border-radius:6px;font-family:Consolas,Monaco,Courier New,monospace;font-size:13px;overflow-x:auto;margin:1em 0}.ngx-editor-content code{background-color:#f1f5f9;color:#e11d48;padding:2px 5px;border-radius:4px;font-family:Consolas,Monaco,Courier New,monospace;font-size:.9em}.ngx-editor-content a{color:#2563eb;text-decoration:underline;text-underline-offset:2px;cursor:pointer}.ngx-editor-content .ngx-editor-hashtag,.ngx-editor-content .post-content-hashtag{color:var(--ngx-editor-hashtag-color, #0069FF)!important;font-weight:500;display:inline;cursor:text}.ngx-editor-content hr{border:none;border-top:1px solid var(--ngx-editor-border);margin:1.5em 0}.ngx-editor-content img{max-width:100%;height:auto;border-radius:4px}.ngx-editor-content[data-empty=true]:before{content:attr(data-placeholder);position:absolute;color:#94a3b8;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-content:has(ol,ul,li,table,img,hr,blockquote,pre):before{display:none!important;content:none!important}.ngx-editor-source{width:100%;flex:1;min-height:200px;padding:16px;border:none;outline:none;font-family:Consolas,Monaco,Courier New,monospace;font-size:13.5px;line-height:1.5;background-color:#0f172a;color:#38bdf8;resize:vertical;box-sizing:border-box}.ngx-editor-statusbar{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background-color:var(--ngx-editor-toolbar-bg);border-top:1px solid var(--ngx-editor-border);font-size:12px;color:var(--ngx-editor-muted)}.ngx-editor-statusbar .statusbar-left,.ngx-editor-statusbar .statusbar-right{display:flex;align-items:center;gap:12px}.ngx-editor-statusbar .status-tag{background-color:#e2e8f0;padding:2px 6px;border-radius:4px;font-weight:600;text-transform:uppercase;font-size:10px}.ngx-editor-statusbar .status-badge{background-color:#fee2e2;color:#ef4444;padding:2px 6px;border-radius:4px;font-weight:500;font-size:11px}.ngx-editor-modal-backdrop{position:absolute;inset:0;background:#0f172a66;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:100;animation:fadeIn .15s ease-out}.ngx-editor-modal{background:#fff;border-radius:8px;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;width:90%;max-width:400px;padding:20px;box-sizing:border-box}.ngx-editor-modal .modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.ngx-editor-modal .modal-header h3{margin:0;font-size:16px;font-weight:600;color:var(--ngx-editor-text)}.ngx-editor-modal .modal-body{display:flex;flex-direction:column;gap:12px}.ngx-editor-modal .form-group{display:flex;flex-direction:column;gap:4px}.ngx-editor-modal .form-group label{font-size:13px;font-weight:500;color:var(--ngx-editor-text)}.ngx-editor-modal .form-group input[type=text],.ngx-editor-modal .form-group input[type=url]{padding:8px 12px;border:1px solid var(--ngx-editor-border);border-radius:6px;font-size:14px;outline:none;transition:border-color .15s ease}.ngx-editor-modal .form-group input[type=text]:focus,.ngx-editor-modal .form-group input[type=url]:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-modal .form-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--ngx-editor-text);-webkit-user-select:none;user-select:none}.ngx-editor-modal .modal-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.ngx-editor-modal .modal-footer button{padding:7px 14px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:all .15s ease;outline:none}.ngx-editor-modal .modal-footer .btn-secondary{background:#f1f5f9;border:1px solid var(--ngx-editor-border);color:var(--ngx-editor-text)}.ngx-editor-modal .modal-footer .btn-secondary:hover{background:#e2e8f0}.ngx-editor-modal .modal-footer .btn-primary{background:#2563eb;border:1px solid #2563eb;color:#fff}.ngx-editor-modal .modal-footer .btn-primary:hover{background:#1d4ed8}.ngx-editor-modal .modal-footer .btn-primary:disabled{opacity:.5;cursor:not-allowed}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
|
|
3646
|
+
], template: "<div\n class=\"ngx-editor-container\"\n [ngClass]=\"customClass\"\n [class.is-focused]=\"isFocused\"\n [class.is-disabled]=\"disabled\"\n [style.min-height]=\"computedConfig.minHeight\"\n [style.max-height]=\"computedConfig.maxHeight\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedEditorStyle\"\n>\n <!-- Toolbar -->\n @if (computedConfig.showToolbar && !readonly) {\n <!-- Array-configured Toolbar (if tools input is provided) -->\n @if (computedConfig.tools && computedConfig.tools.length > 0) {\n <ngx-editor-toolbar\n [editor]=\"this\"\n [tools]=\"computedConfig.tools\"\n [fontFamilies]=\"resolvedFontFamilies\"\n ></ngx-editor-toolbar>\n } @else {\n <!-- Default Full Toolbar -->\n <div class=\"ngx-editor-toolbar\" role=\"toolbar\" aria-label=\"Editor Toolbar\">\n <!-- History -->\n @if (computedConfig.toolbar?.history) {\n <div class=\"ngx-editor-toolbar-group\">\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Undo (Ctrl+Z)\"\n aria-label=\"Undo\"\n (mousedown)=\"$event.preventDefault(); undo()\"\n >\n <ngx-editor-icon name=\"undo\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Redo (Ctrl+Y)\"\n aria-label=\"Redo\"\n (mousedown)=\"$event.preventDefault(); redo()\"\n >\n <ngx-editor-icon name=\"redo\"></ngx-editor-icon>\n </button>\n </div>\n }\n\n <!-- Format Block -->\n @if (computedConfig.toolbar?.format) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Paragraph Format\"\n aria-label=\"Paragraph Format\"\n [value]=\"activeBlockTag\"\n (change)=\"onFormatBlockChange($event)\"\n >\n <option value=\"p\">Normal text</option>\n <option value=\"h1\">Heading 1</option>\n <option value=\"h2\">Heading 2</option>\n <option value=\"h3\">Heading 3</option>\n <option value=\"h4\">Heading 4</option>\n <option value=\"blockquote\">Quote</option>\n <option value=\"pre\">Code Block</option>\n </select>\n </div>\n }\n\n <!-- Font Family -->\n @if (computedConfig.toolbar?.fontName) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Family\"\n aria-label=\"Font Family\"\n (change)=\"onFontNameChange($event)\"\n >\n <option value=\"\">Font</option>\n @for (font of resolvedFontFamilies; track font.value) {\n <option [value]=\"font.value\">{{ font.label }}</option>\n }\n </select>\n </div>\n }\n\n <!-- Font Size -->\n @if (computedConfig.toolbar?.fontSize) {\n <div class=\"ngx-editor-toolbar-group\">\n <select\n class=\"ngx-editor-select\"\n title=\"Font Size\"\n aria-label=\"Font Size\"\n (change)=\"onFontSizeChange($event)\"\n >\n <option value=\"\">Size</option>\n <option value=\"1\">Small (10pt)</option>\n <option value=\"3\">Normal (12pt)</option>\n <option value=\"5\">Large (18pt)</option>\n <option value=\"7\">Huge (36pt)</option>\n </select>\n </div>\n }\n\n <!-- Inline Styles: Bold, Italic, Underline, Strike, Sub, Sup, Code -->\n @if (computedConfig.toolbar?.inline) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.inline?.bold !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.bold\"\n title=\"Bold (Ctrl+B)\"\n aria-label=\"Bold\"\n (mousedown)=\"$event.preventDefault(); bold()\"\n >\n <ngx-editor-icon name=\"bold\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.italic !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.italic\"\n title=\"Italic (Ctrl+I)\"\n aria-label=\"Italic\"\n (mousedown)=\"$event.preventDefault(); italic()\"\n >\n <ngx-editor-icon name=\"italic\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.underline !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.underline\"\n title=\"Underline (Ctrl+U)\"\n aria-label=\"Underline\"\n (mousedown)=\"$event.preventDefault(); underline()\"\n >\n <ngx-editor-icon name=\"underline\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.strikeThrough !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.strikeThrough\"\n title=\"Strikethrough\"\n aria-label=\"Strikethrough\"\n (mousedown)=\"$event.preventDefault(); strike()\"\n >\n <ngx-editor-icon name=\"strike\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.subscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.subscript\"\n title=\"Subscript\"\n aria-label=\"Subscript\"\n (mousedown)=\"$event.preventDefault(); subscript()\"\n >\n <ngx-editor-icon name=\"subscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.superscript !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.superscript\"\n title=\"Superscript\"\n aria-label=\"Superscript\"\n (mousedown)=\"$event.preventDefault(); superscript()\"\n >\n <ngx-editor-icon name=\"superscript\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.inline?.code !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Inline Code\"\n aria-label=\"Inline Code\"\n (mousedown)=\"$event.preventDefault(); wrapSelectionWithCode()\"\n >\n <ngx-editor-icon name=\"code\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Colors -->\n @if (computedConfig.toolbar?.color) {\n <div class=\"ngx-editor-toolbar-group\" style=\"position: relative;\">\n @if (computedConfig.toolbar?.color?.textColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Text Color\"\n aria-label=\"Text Color\"\n (click)=\"toggleColorPicker('text')\"\n >\n <ngx-editor-icon name=\"textColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedTextColor\"\n ></div>\n }\n\n @if (computedConfig.toolbar?.color?.backgroundColor !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn ngx-editor-color-btn\"\n title=\"Highlight Color\"\n aria-label=\"Highlight Color\"\n (click)=\"toggleColorPicker('background')\"\n >\n <ngx-editor-icon name=\"bgColor\"></ngx-editor-icon>\n </button>\n <div\n class=\"color-bar\"\n [style.background-color]=\"selectedBgColor\"\n ></div>\n }\n\n <!-- Color Popover -->\n @if (activeColorPicker) {\n <div class=\"ngx-color-popover\" (mousedown)=\"$event.stopPropagation()\">\n <div class=\"color-grid\">\n @for (color of computedConfig.colorPalette; track color) {\n <button\n type=\"button\"\n class=\"color-swatch\"\n [style.background-color]=\"color\"\n (click)=\"applyColor(color)\"\n ></button>\n }\n </div>\n <div class=\"custom-color-picker\">\n <input\n type=\"color\"\n (change)=\"onCustomColorChange($event)\"\n />\n <span>Custom color</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Alignment -->\n @if (computedConfig.toolbar?.alignment) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.alignment?.left !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyLeft\"\n title=\"Align Left\"\n aria-label=\"Align Left\"\n (mousedown)=\"$event.preventDefault(); alignLeft()\"\n >\n <ngx-editor-icon name=\"alignLeft\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.center !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyCenter\"\n title=\"Align Center\"\n aria-label=\"Align Center\"\n (mousedown)=\"$event.preventDefault(); alignCenter()\"\n >\n <ngx-editor-icon name=\"alignCenter\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.right !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyRight\"\n title=\"Align Right\"\n aria-label=\"Align Right\"\n (mousedown)=\"$event.preventDefault(); alignRight()\"\n >\n <ngx-editor-icon name=\"alignRight\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.alignment?.justify !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.justifyFull\"\n title=\"Justify\"\n aria-label=\"Justify\"\n (mousedown)=\"$event.preventDefault(); alignJustify()\"\n >\n <ngx-editor-icon name=\"alignJustify\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Lists & Indentation -->\n @if (computedConfig.toolbar?.list) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.list?.unordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertUnorderedList\"\n title=\"Bullet List\"\n aria-label=\"Bullet List\"\n (mousedown)=\"$event.preventDefault(); insertUnorderedList()\"\n >\n <ngx-editor-icon name=\"unorderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.ordered !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.insertOrderedList\"\n title=\"Numbered List\"\n aria-label=\"Numbered List\"\n (mousedown)=\"$event.preventDefault(); insertOrderedList()\"\n >\n <ngx-editor-icon name=\"orderedList\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.indent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Increase Indent\"\n aria-label=\"Increase Indent\"\n (mousedown)=\"$event.preventDefault(); indent()\"\n >\n <ngx-editor-icon name=\"indent\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.list?.outdent !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Decrease Indent\"\n aria-label=\"Decrease Indent\"\n (mousedown)=\"$event.preventDefault(); outdent()\"\n >\n <ngx-editor-icon name=\"outdent\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Insert Options -->\n @if (computedConfig.toolbar?.insert) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.insert?.link !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"activeStates.link\"\n title=\"Insert Link (Ctrl+K)\"\n aria-label=\"Insert Link\"\n (click)=\"openLinkModal()\"\n >\n <ngx-editor-icon name=\"link\"></ngx-editor-icon>\n </button>\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Remove Link\"\n aria-label=\"Remove Link\"\n (mousedown)=\"$event.preventDefault(); unlink()\"\n >\n <ngx-editor-icon name=\"unlink\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.image !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Insert Image\"\n aria-label=\"Insert Image\"\n (click)=\"openImageModal()\"\n >\n <ngx-editor-icon name=\"image\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.insert?.horizontalRule !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Horizontal Line\"\n aria-label=\"Horizontal Line\"\n (mousedown)=\"$event.preventDefault(); insertHorizontalRule()\"\n >\n <ngx-editor-icon name=\"horizontalRule\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n\n <!-- Extra Actions -->\n @if (computedConfig.toolbar?.extra) {\n <div class=\"ngx-editor-toolbar-group\">\n @if (computedConfig.toolbar?.extra?.clearFormat !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n title=\"Clear Formatting\"\n aria-label=\"Clear Formatting\"\n (mousedown)=\"$event.preventDefault(); clearFormatting()\"\n >\n <ngx-editor-icon name=\"clearFormat\"></ngx-editor-icon>\n </button>\n }\n @if (computedConfig.toolbar?.extra?.sourceView !== false) {\n <button\n type=\"button\"\n class=\"ngx-editor-btn\"\n [class.is-active]=\"isSourceView\"\n title=\"View HTML Source\"\n aria-label=\"View HTML Source\"\n (click)=\"toggleSourceView()\"\n >\n <ngx-editor-icon name=\"sourceView\"></ngx-editor-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n }\n\n <!-- Editor Content Area -->\n <div class=\"ngx-editor-content-wrapper\">\n <!-- Visual contenteditable -->\n <div\n #editorContent\n class=\"ngx-editor-content\"\n [style.display]=\"isSourceView ? 'none' : 'block'\"\n [style.height]=\"computedConfig.height\"\n [style.font-family]=\"fontFamily || computedConfig.fontFamily || null\"\n [style.font-size]=\"fontSize || computedConfig.fontSize || null\"\n [ngStyle]=\"parsedContentStyle\"\n [attr.contenteditable]=\"!disabled && !readonly\"\n [attr.spellcheck]=\"computedConfig.spellcheck\"\n [attr.data-placeholder]=\"computedConfig.placeholder\"\n [attr.data-empty]=\"isEmpty\"\n (input)=\"onEditorInput()\"\n (focus)=\"onEditorFocus($event)\"\n (blur)=\"onEditorBlur($event)\"\n (keydown)=\"onEditorKeydown($event)\"\n (keyup)=\"updateEditorState()\"\n (mouseup)=\"updateEditorState()\"\n (paste)=\"onEditorPaste($event)\"\n ></div>\n\n <!-- Raw HTML Source Code View -->\n @if (isSourceView) {\n <textarea\n #sourceTextarea\n class=\"ngx-editor-source\"\n [style.height]=\"computedConfig.height\"\n [disabled]=\"disabled || readonly\"\n [value]=\"content\"\n (input)=\"onSourceInput($event)\"\n ></textarea>\n }\n </div>\n\n <!-- Status Bar -->\n @if (computedConfig.showStatusBar) {\n <div class=\"ngx-editor-statusbar\">\n <div class=\"statusbar-left\">\n @if (activeBlockTag) {\n <span class=\"status-tag\"><{{ activeBlockTag }}></span>\n }\n @if (isSourceView) {\n <span class=\"status-badge\">HTML SOURCE MODE</span>\n }\n </div>\n <div class=\"statusbar-right\">\n @if (computedConfig.showWordCount) {\n <span>Words: <strong>{{ stats.words }}</strong></span>\n }\n @if (computedConfig.maxCharacters) {\n <span [class.limit-reached]=\"stats.chars > computedConfig.maxCharacters\">\n {{ stats.chars }}/{{ computedConfig.maxCharacters }} Characters\n </span>\n } @else if (computedConfig.showCharCount) {\n <span>Characters: <strong>{{ stats.chars }}</strong></span>\n }\n </div>\n </div>\n }\n\n <!-- Link Modal -->\n @if (showLinkModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeLinkModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Link</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeLinkModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Link Text</label>\n <input\n type=\"text\"\n placeholder=\"Display text\"\n [(ngModel)]=\"linkModel.text\"\n />\n </div>\n <div class=\"form-group\">\n <label>Target URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com\"\n [(ngModel)]=\"linkModel.url\"\n (keydown.enter)=\"insertLinkFromModal()\"\n />\n </div>\n <div class=\"form-group\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"linkModel.newTab\" />\n Open in a new tab\n </label>\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeLinkModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!linkModel.url\"\n (click)=\"insertLinkFromModal()\"\n >\n Insert Link\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Image Modal -->\n @if (showImageModal) {\n <div class=\"ngx-editor-modal-backdrop\" (click)=\"closeImageModal()\">\n <div class=\"ngx-editor-modal\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\">\n <h3>Insert Image</h3>\n <button type=\"button\" class=\"ngx-editor-btn\" (click)=\"closeImageModal()\" aria-label=\"Close\">\n <ngx-editor-icon name=\"close\"></ngx-editor-icon>\n </button>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <label>Image URL</label>\n <input\n type=\"url\"\n placeholder=\"https://example.com/image.png\"\n [(ngModel)]=\"imageModel.url\"\n />\n </div>\n <div class=\"form-group\">\n <label>Or Upload File</label>\n <input\n type=\"file\"\n accept=\"image/*\"\n (change)=\"onImageFileUpload($event)\"\n />\n </div>\n <div class=\"form-group\">\n <label>Alt Description</label>\n <input\n type=\"text\"\n placeholder=\"Image description\"\n [(ngModel)]=\"imageModel.alt\"\n />\n </div>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn-secondary\" (click)=\"closeImageModal()\">Cancel</button>\n <button\n type=\"button\"\n class=\"btn-primary\"\n [disabled]=\"!imageModel.url\"\n (click)=\"insertImageFromModal()\"\n >\n Insert Image\n </button>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;font-family:var(--ngx-editor-font-family, inherit);box-sizing:border-box;--ngx-editor-font-family: inherit;--ngx-editor-font-size: 15px;--ngx-editor-line-height: 1.65;--ngx-editor-border: #e2e8f0;--ngx-editor-border-focus: #3b82f6;--ngx-editor-bg: #ffffff;--ngx-editor-toolbar-bg: #f8fafc;--ngx-editor-toolbar-btn-hover: #e2e8f0;--ngx-editor-toolbar-btn-active: #dbeafe;--ngx-editor-toolbar-btn-active-color: #1d4ed8;--ngx-editor-text: #1e293b;--ngx-editor-muted: #64748b;--ngx-editor-radius: 8px;--ngx-editor-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);--ngx-editor-shadow-focus: 0 0 0 3px rgba(59, 130, 246, .25);--ngx-editor-hashtag-color: #0069FF}.ngx-editor-container{display:flex;flex-direction:column;background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:var(--ngx-editor-radius);box-shadow:var(--ngx-editor-shadow);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease;position:relative}.ngx-editor-container.is-focused{border-color:var(--ngx-editor-border-focus);box-shadow:var(--ngx-editor-shadow-focus)}.ngx-editor-container.is-disabled{background-color:#f1f5f9;opacity:.75;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px 8px;background-color:var(--ngx-editor-toolbar-bg);border-bottom:1px solid var(--ngx-editor-border);-webkit-user-select:none;user-select:none;z-index:10}.ngx-editor-toolbar-group{display:flex;align-items:center;gap:2px}.ngx-editor-toolbar-group:not(:last-child):after{content:\"\";display:inline-block;width:1px;height:18px;background-color:var(--ngx-editor-border);margin:0 4px}.ngx-editor-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;color:var(--ngx-editor-text);cursor:pointer;transition:background-color .15s ease,color .15s ease,border-color .15s ease;outline:none}.ngx-editor-btn:hover:not(:disabled){background-color:var(--ngx-editor-toolbar-btn-hover)}.ngx-editor-btn:active:not(:disabled),.ngx-editor-btn.is-active{background-color:var(--ngx-editor-toolbar-btn-active);color:var(--ngx-editor-toolbar-btn-active-color);border-color:#bfdbfe}.ngx-editor-btn:disabled{opacity:.35;cursor:not-allowed}.ngx-editor-btn svg{display:block;pointer-events:none}.ngx-editor-select{height:32px;padding:0 8px;font-size:13px;color:var(--ngx-editor-text);background-color:var(--ngx-editor-bg);border:1px solid var(--ngx-editor-border);border-radius:4px;outline:none;cursor:pointer;transition:border-color .15s ease}.ngx-editor-select:hover{border-color:#cbd5e1}.ngx-editor-select:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-color-btn{position:relative}.ngx-editor-color-btn .color-bar{position:absolute;bottom:4px;left:6px;right:6px;height:3px;border-radius:1px}.ngx-color-popover{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid var(--ngx-editor-border);border-radius:6px;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;padding:8px;z-index:50;width:176px}.ngx-color-popover .color-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:4px;margin-bottom:8px}.ngx-color-popover .color-swatch{width:22px;height:22px;border-radius:3px;border:1px solid rgba(0,0,0,.15);cursor:pointer;padding:0;transition:transform .1s ease}.ngx-color-popover .color-swatch:hover{transform:scale(1.15);z-index:2}.ngx-color-popover .custom-color-picker{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ngx-editor-muted)}.ngx-color-popover .custom-color-picker input[type=color]{width:28px;height:24px;border:1px solid var(--ngx-editor-border);border-radius:3px;padding:0;cursor:pointer;background:none}.ngx-editor-content-wrapper{position:relative;flex:1;display:flex;flex-direction:column}.ngx-editor-content{flex:1;padding:16px;outline:none;font-family:var(--ngx-editor-font-family, inherit);font-size:var(--ngx-editor-font-size, 15px);line-height:var(--ngx-editor-line-height, 1.65);color:var(--ngx-editor-text);overflow-y:auto;min-height:200px;word-break:break-word}.ngx-editor-content p{margin:0;min-height:1.65em}.ngx-editor-content b,.ngx-editor-content strong{font-weight:700!important}.ngx-editor-content i,.ngx-editor-content em{font-style:italic!important}.ngx-editor-content u,.ngx-editor-content .publish-editor-underline{text-decoration:underline!important;text-decoration-thickness:1px;text-underline-offset:.12em}.ngx-editor-content h1{font-size:2em;font-weight:700;margin:.67em 0;line-height:1.25}.ngx-editor-content h2{font-size:1.6em;font-weight:600;margin:.75em 0;line-height:1.3}.ngx-editor-content h3{font-size:1.3em;font-weight:600;margin:.83em 0}.ngx-editor-content h4{font-size:1.1em;font-weight:600;margin:1em 0}.ngx-editor-content ul,.ngx-editor-content ol{margin:0 0 1em 1.5em;padding:0}.ngx-editor-content li{margin-bottom:.25em}.ngx-editor-content blockquote{margin:1em 0;padding:.5em 1em;border-left:4px solid var(--ngx-editor-border-focus);background-color:#f8fafc;color:var(--ngx-editor-muted);font-style:italic}.ngx-editor-content pre{background-color:#1e293b;color:#f8fafc;padding:12px;border-radius:6px;font-family:Consolas,Monaco,Courier New,monospace;font-size:13px;overflow-x:auto;margin:1em 0}.ngx-editor-content code{background-color:#f1f5f9;color:#e11d48;padding:2px 5px;border-radius:4px;font-family:Consolas,Monaco,Courier New,monospace;font-size:.9em}.ngx-editor-content a{color:#2563eb;text-decoration:underline;text-underline-offset:2px;cursor:pointer}.ngx-editor-content .ngx-editor-hashtag,.ngx-editor-content .post-content-hashtag{color:var(--ngx-editor-hashtag-color, #0069FF)!important;font-weight:500;display:inline;cursor:text}.ngx-editor-content hr{border:none;border-top:1px solid var(--ngx-editor-border);margin:1.5em 0}.ngx-editor-content img{max-width:100%;height:auto;border-radius:4px}.ngx-editor-content[data-empty=true]:before{content:attr(data-placeholder);position:absolute;color:#94a3b8;pointer-events:none;-webkit-user-select:none;user-select:none}.ngx-editor-content:has(ol,ul,li,table,img,hr,blockquote,pre):before{display:none!important;content:none!important}.ngx-editor-source{width:100%;flex:1;min-height:200px;padding:16px;border:none;outline:none;font-family:Consolas,Monaco,Courier New,monospace;font-size:13.5px;line-height:1.5;background-color:#0f172a;color:#38bdf8;resize:vertical;box-sizing:border-box}.ngx-editor-statusbar{display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background-color:var(--ngx-editor-toolbar-bg);border-top:1px solid var(--ngx-editor-border);font-size:12px;color:var(--ngx-editor-muted)}.ngx-editor-statusbar .statusbar-left,.ngx-editor-statusbar .statusbar-right{display:flex;align-items:center;gap:12px}.ngx-editor-statusbar .status-tag{background-color:#e2e8f0;padding:2px 6px;border-radius:4px;font-weight:600;text-transform:uppercase;font-size:10px}.ngx-editor-statusbar .status-badge{background-color:#fee2e2;color:#ef4444;padding:2px 6px;border-radius:4px;font-weight:500;font-size:11px}.ngx-editor-modal-backdrop{position:absolute;inset:0;background:#0f172a66;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;z-index:100;animation:fadeIn .15s ease-out}.ngx-editor-modal{background:#fff;border-radius:8px;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;width:90%;max-width:400px;padding:20px;box-sizing:border-box}.ngx-editor-modal .modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.ngx-editor-modal .modal-header h3{margin:0;font-size:16px;font-weight:600;color:var(--ngx-editor-text)}.ngx-editor-modal .modal-body{display:flex;flex-direction:column;gap:12px}.ngx-editor-modal .form-group{display:flex;flex-direction:column;gap:4px}.ngx-editor-modal .form-group label{font-size:13px;font-weight:500;color:var(--ngx-editor-text)}.ngx-editor-modal .form-group input[type=text],.ngx-editor-modal .form-group input[type=url]{padding:8px 12px;border:1px solid var(--ngx-editor-border);border-radius:6px;font-size:14px;outline:none;transition:border-color .15s ease}.ngx-editor-modal .form-group input[type=text]:focus,.ngx-editor-modal .form-group input[type=url]:focus{border-color:var(--ngx-editor-border-focus)}.ngx-editor-modal .form-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--ngx-editor-text);-webkit-user-select:none;user-select:none}.ngx-editor-modal .modal-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.ngx-editor-modal .modal-footer button{padding:7px 14px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:all .15s ease;outline:none}.ngx-editor-modal .modal-footer .btn-secondary{background:#f1f5f9;border:1px solid var(--ngx-editor-border);color:var(--ngx-editor-text)}.ngx-editor-modal .modal-footer .btn-secondary:hover{background:#e2e8f0}.ngx-editor-modal .modal-footer .btn-primary{background:#2563eb;border:1px solid #2563eb;color:#fff}.ngx-editor-modal .modal-footer .btn-primary:hover{background:#1d4ed8}.ngx-editor-modal .modal-footer .btn-primary:disabled{opacity:.5;cursor:not-allowed}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}\n"] }]
|
|
2716
3647
|
}], ctorParameters: () => [{ type: EditorCommandService }, { type: i2.DomSanitizer }, { type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { editorContentRef: [{
|
|
2717
3648
|
type: ViewChild,
|
|
2718
3649
|
args: ['editorContent', { static: false }]
|