ngx-rich-text-pro 1.0.6 → 1.0.8
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.
|
@@ -927,6 +927,7 @@ class NgxRichTextEditorComponent {
|
|
|
927
927
|
imageModel = { url: '', alt: '' };
|
|
928
928
|
savedSelectionRange = null;
|
|
929
929
|
icons = EDITOR_ICONS;
|
|
930
|
+
pendingCollapsedFormat = null;
|
|
930
931
|
activeStates = {
|
|
931
932
|
bold: false,
|
|
932
933
|
italic: false,
|
|
@@ -964,6 +965,9 @@ class NgxRichTextEditorComponent {
|
|
|
964
965
|
if (initialContent) {
|
|
965
966
|
this.setContent(initialContent, false);
|
|
966
967
|
}
|
|
968
|
+
else if (this.editorContentRef?.nativeElement) {
|
|
969
|
+
this.editorContentRef.nativeElement.innerHTML = '<p><br></p>';
|
|
970
|
+
}
|
|
967
971
|
}
|
|
968
972
|
ngOnChanges(changes) {
|
|
969
973
|
if (changes['config'] ||
|
|
@@ -1158,8 +1162,8 @@ class NgxRichTextEditorComponent {
|
|
|
1158
1162
|
return this.computedConfig.outputFormat === 'markdown' || this.outputFormat === 'markdown';
|
|
1159
1163
|
}
|
|
1160
1164
|
prepareContentForEditor(value) {
|
|
1161
|
-
if (!value)
|
|
1162
|
-
return '';
|
|
1165
|
+
if (!value || value.trim() === '')
|
|
1166
|
+
return '<p><br></p>';
|
|
1163
1167
|
if (this.isMarkdownMode()) {
|
|
1164
1168
|
const looksLikeHtml = /^\s*<[a-z][\s\S]*>/i.test(value);
|
|
1165
1169
|
const hasMarkdownSyntax = /(__|\*\*|(?<!\*)\*(?!\*)|^\s*-\s|^\s*\d+\.\s)/m.test(value);
|
|
@@ -1201,6 +1205,7 @@ class NgxRichTextEditorComponent {
|
|
|
1201
1205
|
onEditorInput() {
|
|
1202
1206
|
if (this.editorContentRef && this.editorContentRef.nativeElement) {
|
|
1203
1207
|
const root = this.editorContentRef.nativeElement;
|
|
1208
|
+
this.ensureBlockWrapping(root);
|
|
1204
1209
|
if (this.highlightHashtags) {
|
|
1205
1210
|
const spans = root.querySelectorAll('.ngx-editor-hashtag, .post-content-hashtag');
|
|
1206
1211
|
let needsCleanup = false;
|
|
@@ -1254,10 +1259,45 @@ class NgxRichTextEditorComponent {
|
|
|
1254
1259
|
document.execCommand('defaultParagraphSeparator', false, 'p');
|
|
1255
1260
|
}
|
|
1256
1261
|
catch { }
|
|
1262
|
+
if (this.editorContentRef?.nativeElement) {
|
|
1263
|
+
const root = this.editorContentRef.nativeElement;
|
|
1264
|
+
if (!root.hasChildNodes() || root.innerHTML.trim() === '' || root.innerHTML === '<br>') {
|
|
1265
|
+
root.innerHTML = '<p><br></p>';
|
|
1266
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
1267
|
+
if (sel) {
|
|
1268
|
+
const range = document.createRange();
|
|
1269
|
+
range.setStart(root.firstChild, 0);
|
|
1270
|
+
range.collapse(true);
|
|
1271
|
+
sel.removeAllRanges();
|
|
1272
|
+
sel.addRange(range);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
this.ensureBlockWrapping(root);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1257
1279
|
this.editorFocus.emit(event);
|
|
1258
1280
|
}
|
|
1281
|
+
onContentInput(event) {
|
|
1282
|
+
const pending = this.pendingCollapsedFormat;
|
|
1283
|
+
this.pendingCollapsedFormat = null;
|
|
1284
|
+
if (pending &&
|
|
1285
|
+
(this.exclusiveInlineFormats ?? this.computedConfig.exclusiveInlineFormats)) {
|
|
1286
|
+
if (!(event instanceof InputEvent) ||
|
|
1287
|
+
!event.inputType ||
|
|
1288
|
+
!event.inputType.startsWith('delete')) {
|
|
1289
|
+
this.enforcePendingCollapsedFormat(pending);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
this.onEditorInput();
|
|
1293
|
+
}
|
|
1294
|
+
onEditorMouseUp() {
|
|
1295
|
+
this.pendingCollapsedFormat = null;
|
|
1296
|
+
this.updateEditorState();
|
|
1297
|
+
}
|
|
1259
1298
|
onEditorBlur(event) {
|
|
1260
1299
|
this.isFocused = false;
|
|
1300
|
+
this.pendingCollapsedFormat = null;
|
|
1261
1301
|
this.onTouched();
|
|
1262
1302
|
if (this.highlightHashtags) {
|
|
1263
1303
|
this.highlightHashtagsInElement();
|
|
@@ -1552,6 +1592,22 @@ class NgxRichTextEditorComponent {
|
|
|
1552
1592
|
return temp.innerHTML;
|
|
1553
1593
|
}
|
|
1554
1594
|
onEditorKeydown(event) {
|
|
1595
|
+
const navKeys = [
|
|
1596
|
+
'ArrowLeft',
|
|
1597
|
+
'ArrowRight',
|
|
1598
|
+
'ArrowUp',
|
|
1599
|
+
'ArrowDown',
|
|
1600
|
+
'Home',
|
|
1601
|
+
'End',
|
|
1602
|
+
'PageUp',
|
|
1603
|
+
'PageDown',
|
|
1604
|
+
'Escape',
|
|
1605
|
+
'Backspace',
|
|
1606
|
+
'Delete',
|
|
1607
|
+
];
|
|
1608
|
+
if (navKeys.includes(event.key)) {
|
|
1609
|
+
this.pendingCollapsedFormat = null;
|
|
1610
|
+
}
|
|
1555
1611
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
1556
1612
|
const modifier = isMac ? event.metaKey : event.ctrlKey;
|
|
1557
1613
|
if (!modifier && (event.key === ' ' || event.key === 'Enter')) {
|
|
@@ -1649,30 +1705,141 @@ class NgxRichTextEditorComponent {
|
|
|
1649
1705
|
this.updateEditorState();
|
|
1650
1706
|
return;
|
|
1651
1707
|
}
|
|
1708
|
+
this.ensureBlockWrapping(root);
|
|
1652
1709
|
const range = sel.getRangeAt(0);
|
|
1653
|
-
|
|
1654
|
-
if (
|
|
1655
|
-
this.focusEditor();
|
|
1656
|
-
this.commandService.execute(targetCommand);
|
|
1657
|
-
this.onEditorInput();
|
|
1658
|
-
this.updateEditorState();
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
if (range.collapsed) {
|
|
1710
|
+
const currentRange = range;
|
|
1711
|
+
if (currentRange.collapsed) {
|
|
1662
1712
|
this.focusEditor();
|
|
1663
|
-
const
|
|
1713
|
+
const formatAncestor = this.getFormattingAncestor(currentRange, root);
|
|
1714
|
+
const isAncestorTarget = formatAncestor !== null && this.getFormatType(formatAncestor) === targetCommand;
|
|
1715
|
+
const isTargetActive = this.commandService.queryState(targetCommand) || isAncestorTarget;
|
|
1664
1716
|
if (isTargetActive) {
|
|
1665
|
-
|
|
1717
|
+
// Toggle OFF targetCommand
|
|
1718
|
+
if (this.commandService.queryState(targetCommand)) {
|
|
1719
|
+
this.commandService.execute(targetCommand);
|
|
1720
|
+
}
|
|
1721
|
+
if (formatAncestor) {
|
|
1722
|
+
if ((formatAncestor.textContent || '').trim() === '') {
|
|
1723
|
+
// Empty formatting tag (e.g. <p><b><br></b></p> after Enter): unwrap it
|
|
1724
|
+
const parent = formatAncestor.parentNode;
|
|
1725
|
+
if (parent) {
|
|
1726
|
+
const firstChild = formatAncestor.firstChild;
|
|
1727
|
+
const nextSib = formatAncestor.nextSibling;
|
|
1728
|
+
const prevSib = formatAncestor.previousSibling;
|
|
1729
|
+
while (formatAncestor.firstChild) {
|
|
1730
|
+
parent.insertBefore(formatAncestor.firstChild, formatAncestor);
|
|
1731
|
+
}
|
|
1732
|
+
formatAncestor.remove();
|
|
1733
|
+
if (firstChild) {
|
|
1734
|
+
currentRange.setStartBefore(firstChild);
|
|
1735
|
+
}
|
|
1736
|
+
else if (nextSib) {
|
|
1737
|
+
currentRange.setStartBefore(nextSib);
|
|
1738
|
+
}
|
|
1739
|
+
else if (prevSib) {
|
|
1740
|
+
currentRange.setStartAfter(prevSib);
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
const br = document.createElement('br');
|
|
1744
|
+
parent.appendChild(br);
|
|
1745
|
+
currentRange.setStartBefore(br);
|
|
1746
|
+
}
|
|
1747
|
+
currentRange.collapse(true);
|
|
1748
|
+
sel.removeAllRanges();
|
|
1749
|
+
sel.addRange(currentRange);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
else if (this.isCaretAtEndOfElement(currentRange, formatAncestor)) {
|
|
1753
|
+
// At the end of formatting tag: move caret outside after it
|
|
1754
|
+
currentRange.setStartAfter(formatAncestor);
|
|
1755
|
+
currentRange.collapse(true);
|
|
1756
|
+
sel.removeAllRanges();
|
|
1757
|
+
sel.addRange(currentRange);
|
|
1758
|
+
}
|
|
1759
|
+
else if (this.isCaretAtStartOfElement(currentRange, formatAncestor)) {
|
|
1760
|
+
// At the start of formatting tag: move caret outside before it
|
|
1761
|
+
currentRange.setStartBefore(formatAncestor);
|
|
1762
|
+
currentRange.collapse(true);
|
|
1763
|
+
sel.removeAllRanges();
|
|
1764
|
+
sel.addRange(currentRange);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (this.commandService.queryState(targetCommand)) {
|
|
1768
|
+
this.commandService.execute(targetCommand);
|
|
1769
|
+
}
|
|
1770
|
+
this.pendingCollapsedFormat = 'none';
|
|
1771
|
+
this.activeStates[targetCommand] = false;
|
|
1666
1772
|
}
|
|
1667
1773
|
else {
|
|
1774
|
+
// Toggle ON targetCommand
|
|
1775
|
+
// 1. Turn off any other commands before moving or unwrapping formatAncestor
|
|
1776
|
+
for (const cmd of otherCommands) {
|
|
1777
|
+
if (this.commandService.queryState(cmd)) {
|
|
1778
|
+
this.commandService.execute(cmd);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
if (formatAncestor) {
|
|
1782
|
+
if ((formatAncestor.textContent || '').trim() === '') {
|
|
1783
|
+
const parent = formatAncestor.parentNode;
|
|
1784
|
+
if (parent) {
|
|
1785
|
+
const firstChild = formatAncestor.firstChild;
|
|
1786
|
+
const nextSib = formatAncestor.nextSibling;
|
|
1787
|
+
const prevSib = formatAncestor.previousSibling;
|
|
1788
|
+
while (formatAncestor.firstChild) {
|
|
1789
|
+
parent.insertBefore(formatAncestor.firstChild, formatAncestor);
|
|
1790
|
+
}
|
|
1791
|
+
formatAncestor.remove();
|
|
1792
|
+
if (firstChild) {
|
|
1793
|
+
currentRange.setStartBefore(firstChild);
|
|
1794
|
+
}
|
|
1795
|
+
else if (nextSib) {
|
|
1796
|
+
currentRange.setStartBefore(nextSib);
|
|
1797
|
+
}
|
|
1798
|
+
else if (prevSib) {
|
|
1799
|
+
currentRange.setStartAfter(prevSib);
|
|
1800
|
+
}
|
|
1801
|
+
else {
|
|
1802
|
+
const br = document.createElement('br');
|
|
1803
|
+
parent.appendChild(br);
|
|
1804
|
+
currentRange.setStartBefore(br);
|
|
1805
|
+
}
|
|
1806
|
+
currentRange.collapse(true);
|
|
1807
|
+
sel.removeAllRanges();
|
|
1808
|
+
sel.addRange(currentRange);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
else if (this.isCaretAtEndOfElement(currentRange, formatAncestor)) {
|
|
1812
|
+
// At the end of existing formatting tag: move caret outside after it!
|
|
1813
|
+
currentRange.setStartAfter(formatAncestor);
|
|
1814
|
+
currentRange.collapse(true);
|
|
1815
|
+
sel.removeAllRanges();
|
|
1816
|
+
sel.addRange(currentRange);
|
|
1817
|
+
}
|
|
1818
|
+
else if (this.isCaretAtStartOfElement(currentRange, formatAncestor)) {
|
|
1819
|
+
// At the start of existing formatting tag: move caret outside before it!
|
|
1820
|
+
currentRange.setStartBefore(formatAncestor);
|
|
1821
|
+
currentRange.collapse(true);
|
|
1822
|
+
sel.removeAllRanges();
|
|
1823
|
+
sel.addRange(currentRange);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
// 2. Turn off any other commands that might be active at the new position
|
|
1668
1827
|
for (const cmd of otherCommands) {
|
|
1669
1828
|
if (this.commandService.queryState(cmd)) {
|
|
1670
1829
|
this.commandService.execute(cmd);
|
|
1671
1830
|
}
|
|
1672
1831
|
}
|
|
1673
|
-
|
|
1832
|
+
// 3. Turn on targetCommand
|
|
1833
|
+
if (!this.commandService.queryState(targetCommand)) {
|
|
1834
|
+
this.commandService.execute(targetCommand);
|
|
1835
|
+
}
|
|
1836
|
+
this.pendingCollapsedFormat = targetCommand;
|
|
1837
|
+
this.activeStates[targetCommand] = true;
|
|
1838
|
+
for (const cmd of otherCommands) {
|
|
1839
|
+
this.activeStates[cmd] = false;
|
|
1840
|
+
}
|
|
1674
1841
|
}
|
|
1675
|
-
this.
|
|
1842
|
+
this.cdr.markForCheck();
|
|
1676
1843
|
this.updateEditorState();
|
|
1677
1844
|
return;
|
|
1678
1845
|
}
|
|
@@ -1735,6 +1902,28 @@ class NgxRichTextEditorComponent {
|
|
|
1735
1902
|
}
|
|
1736
1903
|
return null;
|
|
1737
1904
|
}
|
|
1905
|
+
isCaretAtEndOfElement(range, el) {
|
|
1906
|
+
try {
|
|
1907
|
+
const endRange = document.createRange();
|
|
1908
|
+
endRange.selectNodeContents(el);
|
|
1909
|
+
endRange.collapse(false);
|
|
1910
|
+
return range.compareBoundaryPoints(Range.START_TO_START, endRange) === 0;
|
|
1911
|
+
}
|
|
1912
|
+
catch {
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
isCaretAtStartOfElement(range, el) {
|
|
1917
|
+
try {
|
|
1918
|
+
const startRange = document.createRange();
|
|
1919
|
+
startRange.selectNodeContents(el);
|
|
1920
|
+
startRange.collapse(true);
|
|
1921
|
+
return range.compareBoundaryPoints(Range.START_TO_START, startRange) === 0;
|
|
1922
|
+
}
|
|
1923
|
+
catch {
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1738
1927
|
normalizeRangeBoundaries(range, root) {
|
|
1739
1928
|
if (range.collapsed)
|
|
1740
1929
|
return;
|
|
@@ -1887,11 +2076,184 @@ class NgxRichTextEditorComponent {
|
|
|
1887
2076
|
return 'underline';
|
|
1888
2077
|
return null;
|
|
1889
2078
|
}
|
|
2079
|
+
getElementFormattingAncestor(node, root) {
|
|
2080
|
+
const isFormatTag = (el) => ['b', 'strong', 'i', 'em', 'u', 'ins'].includes(el.tagName.toLowerCase());
|
|
2081
|
+
let curr = node.nodeType === Node.ELEMENT_NODE ? node : node.parentNode;
|
|
2082
|
+
while (curr && curr !== root) {
|
|
2083
|
+
if (curr.nodeType === Node.ELEMENT_NODE && isFormatTag(curr)) {
|
|
2084
|
+
return curr;
|
|
2085
|
+
}
|
|
2086
|
+
curr = curr.parentNode;
|
|
2087
|
+
}
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
enforcePendingCollapsedFormat(pending) {
|
|
2091
|
+
const root = this.editorContentRef?.nativeElement;
|
|
2092
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2093
|
+
if (!root || !sel || sel.rangeCount === 0)
|
|
2094
|
+
return;
|
|
2095
|
+
const range = sel.getRangeAt(0);
|
|
2096
|
+
let textNode = null;
|
|
2097
|
+
let offset = 0;
|
|
2098
|
+
let formatAncestor = null;
|
|
2099
|
+
if (range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
2100
|
+
textNode = range.startContainer;
|
|
2101
|
+
offset = range.startOffset;
|
|
2102
|
+
formatAncestor = this.getElementFormattingAncestor(textNode, root);
|
|
2103
|
+
}
|
|
2104
|
+
else if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
|
|
2105
|
+
const el = range.startContainer;
|
|
2106
|
+
if (range.startOffset > 0 && el.childNodes.length >= range.startOffset) {
|
|
2107
|
+
const prevChild = el.childNodes[range.startOffset - 1];
|
|
2108
|
+
if (prevChild.nodeType === Node.TEXT_NODE) {
|
|
2109
|
+
textNode = prevChild;
|
|
2110
|
+
offset = textNode.length;
|
|
2111
|
+
formatAncestor = this.getElementFormattingAncestor(textNode, root);
|
|
2112
|
+
}
|
|
2113
|
+
else if (prevChild.nodeType === Node.ELEMENT_NODE) {
|
|
2114
|
+
formatAncestor = this.getElementFormattingAncestor(prevChild, root);
|
|
2115
|
+
textNode = this.getLastTextNode(prevChild);
|
|
2116
|
+
if (textNode) {
|
|
2117
|
+
offset = textNode.length;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (!textNode || offset === 0)
|
|
2123
|
+
return;
|
|
2124
|
+
const currentFormat = formatAncestor ? this.getFormatType(formatAncestor) : null;
|
|
2125
|
+
if (pending === 'none') {
|
|
2126
|
+
if (!formatAncestor)
|
|
2127
|
+
return;
|
|
2128
|
+
if ((formatAncestor.textContent || '').length <= 1 ||
|
|
2129
|
+
(formatAncestor.textContent === textNode.textContent && textNode.length === offset && textNode.length === 1)) {
|
|
2130
|
+
const parent = formatAncestor.parentNode;
|
|
2131
|
+
if (parent) {
|
|
2132
|
+
while (formatAncestor.firstChild) {
|
|
2133
|
+
parent.insertBefore(formatAncestor.firstChild, formatAncestor);
|
|
2134
|
+
}
|
|
2135
|
+
formatAncestor.remove();
|
|
2136
|
+
const newRange = document.createRange();
|
|
2137
|
+
newRange.setStart(textNode, offset);
|
|
2138
|
+
newRange.collapse(true);
|
|
2139
|
+
sel.removeAllRanges();
|
|
2140
|
+
sel.addRange(newRange);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
else if (textNode.parentNode === formatAncestor && offset > 0) {
|
|
2144
|
+
const typedNode = textNode.splitText(offset - 1);
|
|
2145
|
+
formatAncestor.parentNode?.insertBefore(typedNode, formatAncestor.nextSibling);
|
|
2146
|
+
const newRange = document.createRange();
|
|
2147
|
+
newRange.setStart(typedNode, typedNode.length);
|
|
2148
|
+
newRange.collapse(true);
|
|
2149
|
+
sel.removeAllRanges();
|
|
2150
|
+
sel.addRange(newRange);
|
|
2151
|
+
}
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
if (currentFormat === pending)
|
|
2155
|
+
return;
|
|
2156
|
+
const targetTag = pending === 'bold' ? 'b' : pending === 'italic' ? 'i' : 'u';
|
|
2157
|
+
if (formatAncestor) {
|
|
2158
|
+
if ((formatAncestor.textContent || '').length <= 1 ||
|
|
2159
|
+
(formatAncestor.textContent === textNode.textContent && textNode.length === offset && textNode.length === 1)) {
|
|
2160
|
+
const newEl = document.createElement(targetTag);
|
|
2161
|
+
while (formatAncestor.firstChild) {
|
|
2162
|
+
newEl.appendChild(formatAncestor.firstChild);
|
|
2163
|
+
}
|
|
2164
|
+
formatAncestor.parentNode?.replaceChild(newEl, formatAncestor);
|
|
2165
|
+
const newRange = document.createRange();
|
|
2166
|
+
newRange.setStart(textNode, offset);
|
|
2167
|
+
newRange.collapse(true);
|
|
2168
|
+
sel.removeAllRanges();
|
|
2169
|
+
sel.addRange(newRange);
|
|
2170
|
+
}
|
|
2171
|
+
else if (textNode.parentNode === formatAncestor && offset > 0) {
|
|
2172
|
+
const typedNode = textNode.splitText(offset - 1);
|
|
2173
|
+
const newEl = document.createElement(targetTag);
|
|
2174
|
+
newEl.appendChild(typedNode);
|
|
2175
|
+
formatAncestor.parentNode?.insertBefore(newEl, formatAncestor.nextSibling);
|
|
2176
|
+
const newRange = document.createRange();
|
|
2177
|
+
newRange.setStart(typedNode, typedNode.length);
|
|
2178
|
+
newRange.collapse(true);
|
|
2179
|
+
sel.removeAllRanges();
|
|
2180
|
+
sel.addRange(newRange);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
else {
|
|
2184
|
+
if (offset > 0 && textNode.parentNode) {
|
|
2185
|
+
const typedNode = textNode.splitText(offset - 1);
|
|
2186
|
+
const newEl = document.createElement(targetTag);
|
|
2187
|
+
typedNode.parentNode?.insertBefore(newEl, typedNode);
|
|
2188
|
+
newEl.appendChild(typedNode);
|
|
2189
|
+
const newRange = document.createRange();
|
|
2190
|
+
newRange.setStart(typedNode, typedNode.length);
|
|
2191
|
+
newRange.collapse(true);
|
|
2192
|
+
sel.removeAllRanges();
|
|
2193
|
+
sel.addRange(newRange);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
1890
2197
|
splitAndFormatInsideAncestor(ancestor, range, newTag) {
|
|
1891
2198
|
const parentNode = ancestor.parentNode;
|
|
1892
2199
|
if (!parentNode)
|
|
1893
2200
|
return;
|
|
1894
2201
|
this.normalizeRangeBoundaries(range);
|
|
2202
|
+
// If the entire ancestor is selected, simply unwrap or re-tag it!
|
|
2203
|
+
const startRange = document.createRange();
|
|
2204
|
+
startRange.setStart(range.startContainer, range.startOffset);
|
|
2205
|
+
startRange.collapse(true);
|
|
2206
|
+
const endRange = document.createRange();
|
|
2207
|
+
endRange.setEnd(range.endContainer, range.endOffset);
|
|
2208
|
+
endRange.collapse(false);
|
|
2209
|
+
const coversAll = this.isCaretAtStartOfElement(startRange, ancestor) &&
|
|
2210
|
+
this.isCaretAtEndOfElement(endRange, ancestor);
|
|
2211
|
+
if (coversAll) {
|
|
2212
|
+
if (newTag) {
|
|
2213
|
+
const wrapper = document.createElement(newTag);
|
|
2214
|
+
while (ancestor.firstChild) {
|
|
2215
|
+
wrapper.appendChild(ancestor.firstChild);
|
|
2216
|
+
}
|
|
2217
|
+
parentNode.replaceChild(wrapper, ancestor);
|
|
2218
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2219
|
+
if (sel) {
|
|
2220
|
+
sel.removeAllRanges();
|
|
2221
|
+
const newRange = document.createRange();
|
|
2222
|
+
newRange.selectNodeContents(wrapper);
|
|
2223
|
+
sel.addRange(newRange);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
const nodes = [];
|
|
2228
|
+
while (ancestor.firstChild) {
|
|
2229
|
+
const child = ancestor.firstChild;
|
|
2230
|
+
parentNode.insertBefore(child, ancestor);
|
|
2231
|
+
nodes.push(child);
|
|
2232
|
+
}
|
|
2233
|
+
ancestor.remove();
|
|
2234
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2235
|
+
if (sel && nodes.length > 0) {
|
|
2236
|
+
sel.removeAllRanges();
|
|
2237
|
+
const newRange = document.createRange();
|
|
2238
|
+
const first = nodes[0];
|
|
2239
|
+
const last = nodes[nodes.length - 1];
|
|
2240
|
+
if (first.nodeType === Node.TEXT_NODE) {
|
|
2241
|
+
newRange.setStart(first, 0);
|
|
2242
|
+
}
|
|
2243
|
+
else {
|
|
2244
|
+
newRange.setStartBefore(first);
|
|
2245
|
+
}
|
|
2246
|
+
if (last.nodeType === Node.TEXT_NODE) {
|
|
2247
|
+
newRange.setEnd(last, last.textContent?.length || 0);
|
|
2248
|
+
}
|
|
2249
|
+
else {
|
|
2250
|
+
newRange.setEndAfter(last);
|
|
2251
|
+
}
|
|
2252
|
+
sel.addRange(newRange);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
1895
2257
|
if (range.startContainer === range.endContainer &&
|
|
1896
2258
|
range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
1897
2259
|
const textNode = range.startContainer;
|
|
@@ -1951,6 +2313,14 @@ class NgxRichTextEditorComponent {
|
|
|
1951
2313
|
return;
|
|
1952
2314
|
}
|
|
1953
2315
|
// General multi-node fallback within ancestor:
|
|
2316
|
+
// 1. Extract tail from range.end to end of ancestor into cloneAfter FIRST
|
|
2317
|
+
const cloneAfter = ancestor.cloneNode(false);
|
|
2318
|
+
const tailRange = document.createRange();
|
|
2319
|
+
tailRange.selectNodeContents(ancestor);
|
|
2320
|
+
tailRange.setStart(range.endContainer, range.endOffset);
|
|
2321
|
+
const tailFragment = tailRange.extractContents();
|
|
2322
|
+
cloneAfter.appendChild(tailFragment);
|
|
2323
|
+
// 2. Now extract middle from range.start to range.end into fragment
|
|
1954
2324
|
const fragment = range.extractContents();
|
|
1955
2325
|
const formatEls = Array.from(fragment.querySelectorAll('b, strong, i, em, u, ins'));
|
|
1956
2326
|
for (const el of formatEls) {
|
|
@@ -1967,12 +2337,6 @@ class NgxRichTextEditorComponent {
|
|
|
1967
2337
|
el.style.textDecoration = '';
|
|
1968
2338
|
}
|
|
1969
2339
|
}
|
|
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
2340
|
const insertionRef = ancestor.nextSibling;
|
|
1977
2341
|
let selectedNodes = [];
|
|
1978
2342
|
if (newTag) {
|
|
@@ -2074,6 +2438,11 @@ class NgxRichTextEditorComponent {
|
|
|
2074
2438
|
return textNodes;
|
|
2075
2439
|
}
|
|
2076
2440
|
getLeafBlocksInRange(range, root) {
|
|
2441
|
+
this.ensureBlockWrapping(root);
|
|
2442
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
2443
|
+
const activeRange = sel && sel.rangeCount > 0 && root.contains(sel.getRangeAt(0).commonAncestorContainer)
|
|
2444
|
+
? sel.getRangeAt(0)
|
|
2445
|
+
: range;
|
|
2077
2446
|
const blockSelector = 'p, div, li, h1, h2, h3, h4, h5, h6, blockquote, pre';
|
|
2078
2447
|
const allBlocks = Array.from(root.querySelectorAll(blockSelector));
|
|
2079
2448
|
const leafBlocks = allBlocks.filter((block) => {
|
|
@@ -2084,7 +2453,7 @@ class NgxRichTextEditorComponent {
|
|
|
2084
2453
|
}
|
|
2085
2454
|
return leafBlocks.filter((block) => {
|
|
2086
2455
|
try {
|
|
2087
|
-
return
|
|
2456
|
+
return activeRange.intersectsNode(block);
|
|
2088
2457
|
}
|
|
2089
2458
|
catch {
|
|
2090
2459
|
return false;
|
|
@@ -2100,17 +2469,26 @@ class NgxRichTextEditorComponent {
|
|
|
2100
2469
|
if (textNodes.length > 0) {
|
|
2101
2470
|
const isTargetTag = (el) => {
|
|
2102
2471
|
const tag = el.tagName.toLowerCase();
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2472
|
+
const fontWeight = (el.style?.fontWeight || '').toLowerCase();
|
|
2473
|
+
const fontStyle = (el.style?.fontStyle || '').toLowerCase();
|
|
2474
|
+
const textDecoration = (el.style?.textDecoration || '').toLowerCase();
|
|
2475
|
+
if (targetCommand === 'bold') {
|
|
2476
|
+
return (tag === 'b' ||
|
|
2477
|
+
tag === 'strong' ||
|
|
2478
|
+
fontWeight === 'bold' ||
|
|
2479
|
+
parseInt(fontWeight, 10) >= 600);
|
|
2480
|
+
}
|
|
2481
|
+
if (targetCommand === 'italic') {
|
|
2482
|
+
return tag === 'i' || tag === 'em' || fontStyle === 'italic';
|
|
2483
|
+
}
|
|
2484
|
+
if (targetCommand === 'underline') {
|
|
2485
|
+
return tag === 'u' || tag === 'ins' || textDecoration.includes('underline');
|
|
2486
|
+
}
|
|
2109
2487
|
return false;
|
|
2110
2488
|
};
|
|
2111
2489
|
const nonEmptyTextNodes = textNodes.filter((t) => (t.textContent || '').trim().length > 0);
|
|
2112
2490
|
if (nonEmptyTextNodes.length > 0) {
|
|
2113
|
-
|
|
2491
|
+
return nonEmptyTextNodes.every((node) => {
|
|
2114
2492
|
let cur = node.parentNode;
|
|
2115
2493
|
while (cur && cur !== root) {
|
|
2116
2494
|
if (cur.nodeType === Node.ELEMENT_NODE && isTargetTag(cur)) {
|
|
@@ -2120,9 +2498,6 @@ class NgxRichTextEditorComponent {
|
|
|
2120
2498
|
}
|
|
2121
2499
|
return false;
|
|
2122
2500
|
});
|
|
2123
|
-
if (allFormatted) {
|
|
2124
|
-
return true;
|
|
2125
|
-
}
|
|
2126
2501
|
}
|
|
2127
2502
|
}
|
|
2128
2503
|
return this.commandService.queryState(targetCommand);
|
|
@@ -2135,28 +2510,26 @@ class NgxRichTextEditorComponent {
|
|
|
2135
2510
|
if (leafBlocks.length > 1) {
|
|
2136
2511
|
let firstWrapper = null;
|
|
2137
2512
|
let lastWrapper = null;
|
|
2138
|
-
for (
|
|
2513
|
+
for (let i = 0; i < leafBlocks.length; i++) {
|
|
2514
|
+
const block = leafBlocks[i];
|
|
2139
2515
|
const text = block.textContent || '';
|
|
2140
2516
|
if (text.trim() === '') {
|
|
2141
2517
|
continue;
|
|
2142
2518
|
}
|
|
2519
|
+
const isFirst = i === 0;
|
|
2520
|
+
const isLast = i === leafBlocks.length - 1;
|
|
2143
2521
|
const subRange = document.createRange();
|
|
2144
|
-
if (block.contains(range.startContainer)) {
|
|
2522
|
+
if (isFirst && block.contains(range.startContainer)) {
|
|
2145
2523
|
subRange.setStart(range.startContainer, range.startOffset);
|
|
2146
2524
|
}
|
|
2147
2525
|
else {
|
|
2148
|
-
|
|
2149
|
-
subRange.setStart(firstText, 0);
|
|
2526
|
+
subRange.setStart(block, 0);
|
|
2150
2527
|
}
|
|
2151
|
-
if (block.contains(range.endContainer)) {
|
|
2528
|
+
if (isLast && block.contains(range.endContainer)) {
|
|
2152
2529
|
subRange.setEnd(range.endContainer, range.endOffset);
|
|
2153
2530
|
}
|
|
2154
2531
|
else {
|
|
2155
|
-
|
|
2156
|
-
const len = lastText.nodeType === Node.TEXT_NODE
|
|
2157
|
-
? lastText.textContent?.length || 0
|
|
2158
|
-
: lastText.childNodes.length;
|
|
2159
|
-
subRange.setEnd(lastText, len);
|
|
2532
|
+
subRange.setEnd(block, block.childNodes.length);
|
|
2160
2533
|
}
|
|
2161
2534
|
if (!subRange.collapsed) {
|
|
2162
2535
|
const wrapper = this.applyExclusiveFormatToSingleRange(subRange, targetCommand);
|
|
@@ -2231,28 +2604,26 @@ class NgxRichTextEditorComponent {
|
|
|
2231
2604
|
if (leafBlocks.length > 1) {
|
|
2232
2605
|
let firstBlock = null;
|
|
2233
2606
|
let lastBlock = null;
|
|
2234
|
-
for (
|
|
2607
|
+
for (let i = 0; i < leafBlocks.length; i++) {
|
|
2608
|
+
const block = leafBlocks[i];
|
|
2235
2609
|
const text = block.textContent || '';
|
|
2236
2610
|
if (text.trim() === '') {
|
|
2237
2611
|
continue;
|
|
2238
2612
|
}
|
|
2613
|
+
const isFirst = i === 0;
|
|
2614
|
+
const isLast = i === leafBlocks.length - 1;
|
|
2239
2615
|
const subRange = document.createRange();
|
|
2240
|
-
if (block.contains(range.startContainer)) {
|
|
2616
|
+
if (isFirst && block.contains(range.startContainer)) {
|
|
2241
2617
|
subRange.setStart(range.startContainer, range.startOffset);
|
|
2242
2618
|
}
|
|
2243
2619
|
else {
|
|
2244
|
-
|
|
2245
|
-
subRange.setStart(firstText, 0);
|
|
2620
|
+
subRange.setStart(block, 0);
|
|
2246
2621
|
}
|
|
2247
|
-
if (block.contains(range.endContainer)) {
|
|
2622
|
+
if (isLast && block.contains(range.endContainer)) {
|
|
2248
2623
|
subRange.setEnd(range.endContainer, range.endOffset);
|
|
2249
2624
|
}
|
|
2250
2625
|
else {
|
|
2251
|
-
|
|
2252
|
-
const len = lastText.nodeType === Node.TEXT_NODE
|
|
2253
|
-
? lastText.textContent?.length || 0
|
|
2254
|
-
: lastText.childNodes.length;
|
|
2255
|
-
subRange.setEnd(lastText, len);
|
|
2626
|
+
subRange.setEnd(block, block.childNodes.length);
|
|
2256
2627
|
}
|
|
2257
2628
|
if (!subRange.collapsed) {
|
|
2258
2629
|
this.removeFormatFromSingleRange(subRange, targetCommand);
|
|
@@ -2329,30 +2700,31 @@ class NgxRichTextEditorComponent {
|
|
|
2329
2700
|
const tag = el.tagName.toLowerCase();
|
|
2330
2701
|
return ['b', 'strong', 'i', 'em', 'u', 'ins'].includes(tag);
|
|
2331
2702
|
};
|
|
2332
|
-
let
|
|
2333
|
-
while (
|
|
2334
|
-
if (
|
|
2335
|
-
|
|
2336
|
-
|
|
2703
|
+
let sNode = range.startContainer;
|
|
2704
|
+
while (sNode && sNode !== root) {
|
|
2705
|
+
if (sNode.nodeType === Node.ELEMENT_NODE && isFormattingTag(sNode)) {
|
|
2706
|
+
const sRange = document.createRange();
|
|
2707
|
+
sRange.setStart(range.startContainer, range.startOffset);
|
|
2708
|
+
sRange.collapse(true);
|
|
2709
|
+
if (this.isCaretAtStartOfElement(sRange, sNode)) {
|
|
2710
|
+
range.setStartBefore(sNode);
|
|
2337
2711
|
}
|
|
2338
2712
|
break;
|
|
2339
2713
|
}
|
|
2340
|
-
|
|
2341
|
-
break;
|
|
2342
|
-
startNode = startNode.parentNode;
|
|
2714
|
+
sNode = sNode.parentNode;
|
|
2343
2715
|
}
|
|
2344
|
-
let
|
|
2345
|
-
while (
|
|
2346
|
-
if (
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2349
|
-
|
|
2716
|
+
let eNode = range.endContainer;
|
|
2717
|
+
while (eNode && eNode !== root) {
|
|
2718
|
+
if (eNode.nodeType === Node.ELEMENT_NODE && isFormattingTag(eNode)) {
|
|
2719
|
+
const eRange = document.createRange();
|
|
2720
|
+
eRange.setEnd(range.endContainer, range.endOffset);
|
|
2721
|
+
eRange.collapse(false);
|
|
2722
|
+
if (this.isCaretAtEndOfElement(eRange, eNode)) {
|
|
2723
|
+
range.setEndAfter(eNode);
|
|
2350
2724
|
}
|
|
2351
2725
|
break;
|
|
2352
2726
|
}
|
|
2353
|
-
|
|
2354
|
-
break;
|
|
2355
|
-
endNode = endNode.parentNode;
|
|
2727
|
+
eNode = eNode.parentNode;
|
|
2356
2728
|
}
|
|
2357
2729
|
}
|
|
2358
2730
|
sanitizeExclusiveFormatting(container) {
|
|
@@ -2696,6 +3068,114 @@ class NgxRichTextEditorComponent {
|
|
|
2696
3068
|
sel.addRange(range);
|
|
2697
3069
|
}
|
|
2698
3070
|
}
|
|
3071
|
+
getSelectionCharacterOffsetsWithin(element) {
|
|
3072
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
3073
|
+
if (!sel || sel.rangeCount === 0)
|
|
3074
|
+
return null;
|
|
3075
|
+
const range = sel.getRangeAt(0);
|
|
3076
|
+
if (!element.contains(range.commonAncestorContainer))
|
|
3077
|
+
return null;
|
|
3078
|
+
const startRange = range.cloneRange();
|
|
3079
|
+
startRange.selectNodeContents(element);
|
|
3080
|
+
try {
|
|
3081
|
+
startRange.setEnd(range.startContainer, range.startOffset);
|
|
3082
|
+
const start = startRange.toString().length;
|
|
3083
|
+
const endRange = range.cloneRange();
|
|
3084
|
+
endRange.selectNodeContents(element);
|
|
3085
|
+
endRange.setEnd(range.endContainer, range.endOffset);
|
|
3086
|
+
const end = endRange.toString().length;
|
|
3087
|
+
return { start, end };
|
|
3088
|
+
}
|
|
3089
|
+
catch {
|
|
3090
|
+
return null;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
setSelectionCharacterOffsetsWithin(element, start, end) {
|
|
3094
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
3095
|
+
if (!sel)
|
|
3096
|
+
return;
|
|
3097
|
+
let startNode = null;
|
|
3098
|
+
let startOffset = 0;
|
|
3099
|
+
let endNode = null;
|
|
3100
|
+
let endOffset = 0;
|
|
3101
|
+
let currentOffset = 0;
|
|
3102
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
|
|
3103
|
+
let node;
|
|
3104
|
+
while ((node = walker.nextNode())) {
|
|
3105
|
+
const textLength = node.textContent?.length || 0;
|
|
3106
|
+
if (!startNode && currentOffset + textLength >= start) {
|
|
3107
|
+
startNode = node;
|
|
3108
|
+
startOffset = Math.max(0, Math.min(start - currentOffset, textLength));
|
|
3109
|
+
}
|
|
3110
|
+
if (!endNode && currentOffset + textLength >= end) {
|
|
3111
|
+
endNode = node;
|
|
3112
|
+
endOffset = Math.max(0, Math.min(end - currentOffset, textLength));
|
|
3113
|
+
break;
|
|
3114
|
+
}
|
|
3115
|
+
currentOffset += textLength;
|
|
3116
|
+
}
|
|
3117
|
+
if (startNode && endNode) {
|
|
3118
|
+
const range = document.createRange();
|
|
3119
|
+
range.setStart(startNode, startOffset);
|
|
3120
|
+
range.setEnd(endNode, endOffset);
|
|
3121
|
+
sel.removeAllRanges();
|
|
3122
|
+
sel.addRange(range);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
ensureBlockWrapping(root) {
|
|
3126
|
+
const blockTags = new Set([
|
|
3127
|
+
'p', 'div', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
3128
|
+
'blockquote', 'pre', 'hr', 'table', 'section', 'article', 'header', 'footer'
|
|
3129
|
+
]);
|
|
3130
|
+
const isBlock = (node) => {
|
|
3131
|
+
return (node.nodeType === Node.ELEMENT_NODE &&
|
|
3132
|
+
blockTags.has(node.tagName.toLowerCase()));
|
|
3133
|
+
};
|
|
3134
|
+
const children = Array.from(root.childNodes);
|
|
3135
|
+
if (children.length === 0 || (children.length === 1 && children[0].nodeName === 'BR')) {
|
|
3136
|
+
root.innerHTML = '<p><br></p>';
|
|
3137
|
+
return true;
|
|
3138
|
+
}
|
|
3139
|
+
const hasUnwrappedInline = children.some((child) => !isBlock(child));
|
|
3140
|
+
if (!hasUnwrappedInline) {
|
|
3141
|
+
return false;
|
|
3142
|
+
}
|
|
3143
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
3144
|
+
const hasSelection = sel &&
|
|
3145
|
+
sel.rangeCount > 0 &&
|
|
3146
|
+
root.contains(sel.getRangeAt(0).commonAncestorContainer);
|
|
3147
|
+
const selOffsets = hasSelection ? this.getSelectionCharacterOffsetsWithin(root) : null;
|
|
3148
|
+
let inlineRun = [];
|
|
3149
|
+
const wrapRun = () => {
|
|
3150
|
+
if (inlineRun.length === 0)
|
|
3151
|
+
return;
|
|
3152
|
+
const p = document.createElement('p');
|
|
3153
|
+
const first = inlineRun[0];
|
|
3154
|
+
root.insertBefore(p, first);
|
|
3155
|
+
for (const n of inlineRun) {
|
|
3156
|
+
p.appendChild(n);
|
|
3157
|
+
}
|
|
3158
|
+
if (!p.hasChildNodes() || (p.textContent || '').trim() === '') {
|
|
3159
|
+
if (!p.querySelector('br, img')) {
|
|
3160
|
+
p.appendChild(document.createElement('br'));
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
inlineRun = [];
|
|
3164
|
+
};
|
|
3165
|
+
for (const child of children) {
|
|
3166
|
+
if (isBlock(child)) {
|
|
3167
|
+
wrapRun();
|
|
3168
|
+
}
|
|
3169
|
+
else {
|
|
3170
|
+
inlineRun.push(child);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
wrapRun();
|
|
3174
|
+
if (selOffsets) {
|
|
3175
|
+
this.setSelectionCharacterOffsetsWithin(root, selOffsets.start, selOffsets.end);
|
|
3176
|
+
}
|
|
3177
|
+
return true;
|
|
3178
|
+
}
|
|
2699
3179
|
sanitizeHashtags(root) {
|
|
2700
3180
|
if (!this.highlightHashtags)
|
|
2701
3181
|
return false;
|
|
@@ -3071,32 +3551,57 @@ class NgxRichTextEditorComponent {
|
|
|
3071
3551
|
if (this.exclusiveInlineFormats ?? this.computedConfig.exclusiveInlineFormats) {
|
|
3072
3552
|
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
3073
3553
|
const root = this.editorContentRef?.nativeElement;
|
|
3074
|
-
let formatAncestor = null;
|
|
3075
3554
|
if (sel && sel.rangeCount > 0 && root) {
|
|
3076
3555
|
const range = sel.getRangeAt(0);
|
|
3077
3556
|
if (root.contains(range.commonAncestorContainer)) {
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
underline = false;
|
|
3092
|
-
}
|
|
3093
|
-
else {
|
|
3094
|
-
if (underline) {
|
|
3095
|
-
bold = false;
|
|
3096
|
-
italic = false;
|
|
3557
|
+
if (!range.collapsed) {
|
|
3558
|
+
const formatAncestor = this.getFormattingAncestor(range, root);
|
|
3559
|
+
if (formatAncestor) {
|
|
3560
|
+
const type = this.getFormatType(formatAncestor);
|
|
3561
|
+
bold = type === 'bold';
|
|
3562
|
+
italic = type === 'italic';
|
|
3563
|
+
underline = type === 'underline';
|
|
3564
|
+
}
|
|
3565
|
+
else {
|
|
3566
|
+
bold = this.isFormatActiveOnRange(range, 'bold', root);
|
|
3567
|
+
italic = !bold && this.isFormatActiveOnRange(range, 'italic', root);
|
|
3568
|
+
underline = !bold && !italic && this.isFormatActiveOnRange(range, 'underline', root);
|
|
3569
|
+
}
|
|
3097
3570
|
}
|
|
3098
|
-
else
|
|
3099
|
-
|
|
3571
|
+
else {
|
|
3572
|
+
// Collapsed caret:
|
|
3573
|
+
if (this.pendingCollapsedFormat === 'none') {
|
|
3574
|
+
bold = false;
|
|
3575
|
+
italic = false;
|
|
3576
|
+
underline = false;
|
|
3577
|
+
}
|
|
3578
|
+
else if (this.pendingCollapsedFormat) {
|
|
3579
|
+
bold = this.pendingCollapsedFormat === 'bold';
|
|
3580
|
+
italic = this.pendingCollapsedFormat === 'italic';
|
|
3581
|
+
underline = this.pendingCollapsedFormat === 'underline';
|
|
3582
|
+
}
|
|
3583
|
+
else if (underline) {
|
|
3584
|
+
bold = false;
|
|
3585
|
+
italic = false;
|
|
3586
|
+
}
|
|
3587
|
+
else if (italic) {
|
|
3588
|
+
bold = false;
|
|
3589
|
+
underline = false;
|
|
3590
|
+
}
|
|
3591
|
+
else if (bold) {
|
|
3592
|
+
italic = false;
|
|
3593
|
+
underline = false;
|
|
3594
|
+
}
|
|
3595
|
+
else {
|
|
3596
|
+
// 2. If browser queryState is clean/unspecified, fallback to enclosing formatting tag
|
|
3597
|
+
const formatAncestor = this.getFormattingAncestor(range, root);
|
|
3598
|
+
if (formatAncestor) {
|
|
3599
|
+
const type = this.getFormatType(formatAncestor);
|
|
3600
|
+
bold = type === 'bold';
|
|
3601
|
+
italic = type === 'italic';
|
|
3602
|
+
underline = type === 'underline';
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3100
3605
|
}
|
|
3101
3606
|
}
|
|
3102
3607
|
}
|
|
@@ -3287,6 +3792,19 @@ class NgxRichTextEditorComponent {
|
|
|
3287
3792
|
.replace(/>/g, '>');
|
|
3288
3793
|
// Convert any Mathematical / LinkedIn Unicode characters and combining underlines into <b>, <i>, <u> tags
|
|
3289
3794
|
escaped = this.convertUnicodeMathFormatting(escaped);
|
|
3795
|
+
// Normalize adjacent formatting delimiters when bold and italic meet without whitespace:
|
|
3796
|
+
// e.g. **Hello***How* (bold close ** + italic open *) -> **Hello**\u200B*How*
|
|
3797
|
+
// e.g. *Hello***How** (italic close * + bold open **) -> *Hello*\u200B**How**
|
|
3798
|
+
// e.g. *Hello***Hello***Hello* -> *Hello*\u200B**Hello**\u200B*Hello*
|
|
3799
|
+
let prevNorm = '';
|
|
3800
|
+
let pass = 0;
|
|
3801
|
+
while (prevNorm !== escaped && pass < 10) {
|
|
3802
|
+
pass++;
|
|
3803
|
+
prevNorm = escaped;
|
|
3804
|
+
escaped = escaped
|
|
3805
|
+
.replace(/(?<!\*)(\*\*[^\*\n]+?\*\*)\*(?!\*)/g, '$1\u200B*')
|
|
3806
|
+
.replace(/(?<!\*)(\*[^\*\n]+?\*)\*\*(?!\*)/g, '$1\u200B**');
|
|
3807
|
+
}
|
|
3290
3808
|
// Handle any legacy / malformed combined markers like __***text***__ -> clean to single effect (underline)
|
|
3291
3809
|
escaped = escaped.replace(/__\*\*\*(.*?)\*\*\*__/g, '<u>$1</u>');
|
|
3292
3810
|
escaped = escaped.replace(/__\*\*(.*?)\*\*__/g, '<u>$1</u>');
|
|
@@ -3295,6 +3813,7 @@ class NgxRichTextEditorComponent {
|
|
|
3295
3813
|
escaped = escaped.replace(/__([^_]+?)__/g, '<u>$1</u>');
|
|
3296
3814
|
escaped = escaped.replace(/\*\*([^\*]+?)\*\*/g, '<b>$1</b>');
|
|
3297
3815
|
escaped = escaped.replace(/(?<!\*)\*(?!\*)([^\*\n]+?)(?<!\*)\*(?!\*)/g, '<i>$1</i>');
|
|
3816
|
+
escaped = escaped.replace(/\u200B/g, '');
|
|
3298
3817
|
if (this.computedConfig.highlightHashtags ?? this.highlightHashtags) {
|
|
3299
3818
|
escaped = escaped.replace(/(^|[\s>])#([a-zA-Z0-9_\u0080-\uFFFF]+)/g, '$1<span class="ngx-editor-hashtag post-content-hashtag">#$2</span>');
|
|
3300
3819
|
}
|
|
@@ -3633,7 +4152,7 @@ class NgxRichTextEditorComponent {
|
|
|
3633
4152
|
useExisting: forwardRef(() => NgxRichTextEditorComponent),
|
|
3634
4153
|
multi: true,
|
|
3635
4154
|
},
|
|
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"] }] });
|
|
4155
|
+
], 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)=\"onContentInput($event)\"\n (focus)=\"onEditorFocus($event)\"\n (blur)=\"onEditorBlur($event)\"\n (keydown)=\"onEditorKeydown($event)\"\n (keyup)=\"updateEditorState()\"\n (mouseup)=\"onEditorMouseUp()\"\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: inherit;--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, inherit);overflow-y:auto;min-height:200px;word-break:break-word}.ngx-editor-content::selection,.ngx-editor-content *::selection{color:inherit;background:#70b5f959}.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"] }] });
|
|
3637
4156
|
}
|
|
3638
4157
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NgxRichTextEditorComponent, decorators: [{
|
|
3639
4158
|
type: Component,
|
|
@@ -3643,7 +4162,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3643
4162
|
useExisting: forwardRef(() => NgxRichTextEditorComponent),
|
|
3644
4163
|
multi: true,
|
|
3645
4164
|
},
|
|
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"] }]
|
|
4165
|
+
], 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)=\"onContentInput($event)\"\n (focus)=\"onEditorFocus($event)\"\n (blur)=\"onEditorBlur($event)\"\n (keydown)=\"onEditorKeydown($event)\"\n (keyup)=\"updateEditorState()\"\n (mouseup)=\"onEditorMouseUp()\"\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: inherit;--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, inherit);overflow-y:auto;min-height:200px;word-break:break-word}.ngx-editor-content::selection,.ngx-editor-content *::selection{color:inherit;background:#70b5f959}.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"] }]
|
|
3647
4166
|
}], ctorParameters: () => [{ type: EditorCommandService }, { type: i2.DomSanitizer }, { type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { editorContentRef: [{
|
|
3648
4167
|
type: ViewChild,
|
|
3649
4168
|
args: ['editorContent', { static: false }]
|