ngx-rich-text-pro 1.0.7 → 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,87 +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
- // If selection is outside editor root, fall back to standard execution
1654
- if (!root.contains(range.commonAncestorContainer)) {
1710
+ const currentRange = range;
1711
+ if (currentRange.collapsed) {
1655
1712
  this.focusEditor();
1656
- this.commandService.execute(targetCommand);
1657
- this.onEditorInput();
1658
- this.updateEditorState();
1659
- return;
1660
- }
1661
- if (range.collapsed) {
1662
- this.focusEditor();
1663
- const formatAncestor = this.getFormattingAncestor(range, root);
1713
+ const formatAncestor = this.getFormattingAncestor(currentRange, root);
1664
1714
  const isAncestorTarget = formatAncestor !== null && this.getFormatType(formatAncestor) === targetCommand;
1665
1715
  const isTargetActive = this.commandService.queryState(targetCommand) || isAncestorTarget;
1666
1716
  if (isTargetActive) {
1667
1717
  // Toggle OFF targetCommand
1718
+ if (this.commandService.queryState(targetCommand)) {
1719
+ this.commandService.execute(targetCommand);
1720
+ }
1668
1721
  if (formatAncestor) {
1669
1722
  if ((formatAncestor.textContent || '').trim() === '') {
1670
1723
  // Empty formatting tag (e.g. <p><b><br></b></p> after Enter): unwrap it
1671
1724
  const parent = formatAncestor.parentNode;
1672
1725
  if (parent) {
1673
1726
  const firstChild = formatAncestor.firstChild;
1727
+ const nextSib = formatAncestor.nextSibling;
1728
+ const prevSib = formatAncestor.previousSibling;
1674
1729
  while (formatAncestor.firstChild) {
1675
1730
  parent.insertBefore(formatAncestor.firstChild, formatAncestor);
1676
1731
  }
1677
1732
  formatAncestor.remove();
1678
1733
  if (firstChild) {
1679
- range.setStartBefore(firstChild);
1680
- range.collapse(true);
1681
- sel.removeAllRanges();
1682
- sel.addRange(range);
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);
1683
1746
  }
1747
+ currentRange.collapse(true);
1748
+ sel.removeAllRanges();
1749
+ sel.addRange(currentRange);
1684
1750
  }
1685
1751
  }
1686
- else if (this.isCaretAtEndOfElement(range, formatAncestor)) {
1752
+ else if (this.isCaretAtEndOfElement(currentRange, formatAncestor)) {
1687
1753
  // At the end of formatting tag: move caret outside after it
1688
- range.setStartAfter(formatAncestor);
1689
- range.collapse(true);
1754
+ currentRange.setStartAfter(formatAncestor);
1755
+ currentRange.collapse(true);
1690
1756
  sel.removeAllRanges();
1691
- sel.addRange(range);
1757
+ sel.addRange(currentRange);
1692
1758
  }
1693
- else if (this.isCaretAtStartOfElement(range, formatAncestor)) {
1759
+ else if (this.isCaretAtStartOfElement(currentRange, formatAncestor)) {
1694
1760
  // At the start of formatting tag: move caret outside before it
1695
- range.setStartBefore(formatAncestor);
1696
- range.collapse(true);
1761
+ currentRange.setStartBefore(formatAncestor);
1762
+ currentRange.collapse(true);
1697
1763
  sel.removeAllRanges();
1698
- sel.addRange(range);
1764
+ sel.addRange(currentRange);
1699
1765
  }
1700
1766
  }
1701
1767
  if (this.commandService.queryState(targetCommand)) {
1702
1768
  this.commandService.execute(targetCommand);
1703
1769
  }
1770
+ this.pendingCollapsedFormat = 'none';
1771
+ this.activeStates[targetCommand] = false;
1704
1772
  }
1705
1773
  else {
1706
1774
  // Toggle ON targetCommand
1707
- if (formatAncestor && (formatAncestor.textContent || '').trim() === '') {
1708
- const parent = formatAncestor.parentNode;
1709
- if (parent) {
1710
- const firstChild = formatAncestor.firstChild;
1711
- while (formatAncestor.firstChild) {
1712
- parent.insertBefore(formatAncestor.firstChild, formatAncestor);
1713
- }
1714
- formatAncestor.remove();
1715
- if (firstChild) {
1716
- range.setStartBefore(firstChild);
1717
- range.collapse(true);
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);
1718
1807
  sel.removeAllRanges();
1719
- sel.addRange(range);
1808
+ sel.addRange(currentRange);
1720
1809
  }
1721
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
+ }
1722
1825
  }
1826
+ // 2. Turn off any other commands that might be active at the new position
1723
1827
  for (const cmd of otherCommands) {
1724
1828
  if (this.commandService.queryState(cmd)) {
1725
1829
  this.commandService.execute(cmd);
1726
1830
  }
1727
1831
  }
1832
+ // 3. Turn on targetCommand
1728
1833
  if (!this.commandService.queryState(targetCommand)) {
1729
1834
  this.commandService.execute(targetCommand);
1730
1835
  }
1836
+ this.pendingCollapsedFormat = targetCommand;
1837
+ this.activeStates[targetCommand] = true;
1838
+ for (const cmd of otherCommands) {
1839
+ this.activeStates[cmd] = false;
1840
+ }
1731
1841
  }
1732
- this.onEditorInput();
1842
+ this.cdr.markForCheck();
1733
1843
  this.updateEditorState();
1734
1844
  return;
1735
1845
  }
@@ -1966,6 +2076,124 @@ class NgxRichTextEditorComponent {
1966
2076
  return 'underline';
1967
2077
  return null;
1968
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
+ }
1969
2197
  splitAndFormatInsideAncestor(ancestor, range, newTag) {
1970
2198
  const parentNode = ancestor.parentNode;
1971
2199
  if (!parentNode)
@@ -2210,6 +2438,11 @@ class NgxRichTextEditorComponent {
2210
2438
  return textNodes;
2211
2439
  }
2212
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;
2213
2446
  const blockSelector = 'p, div, li, h1, h2, h3, h4, h5, h6, blockquote, pre';
2214
2447
  const allBlocks = Array.from(root.querySelectorAll(blockSelector));
2215
2448
  const leafBlocks = allBlocks.filter((block) => {
@@ -2220,7 +2453,7 @@ class NgxRichTextEditorComponent {
2220
2453
  }
2221
2454
  return leafBlocks.filter((block) => {
2222
2455
  try {
2223
- return range.intersectsNode(block);
2456
+ return activeRange.intersectsNode(block);
2224
2457
  }
2225
2458
  catch {
2226
2459
  return false;
@@ -2835,6 +3068,114 @@ class NgxRichTextEditorComponent {
2835
3068
  sel.addRange(range);
2836
3069
  }
2837
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
+ }
2838
3179
  sanitizeHashtags(root) {
2839
3180
  if (!this.highlightHashtags)
2840
3181
  return false;
@@ -3228,19 +3569,38 @@ class NgxRichTextEditorComponent {
3228
3569
  }
3229
3570
  }
3230
3571
  else {
3231
- const formatAncestor = this.getFormattingAncestor(range, root);
3232
- if (formatAncestor) {
3233
- const type = this.getFormatType(formatAncestor);
3234
- bold = type === 'bold';
3235
- italic = type === 'italic';
3236
- underline = type === 'underline';
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';
3237
3582
  }
3238
- if (underline) {
3583
+ else if (underline) {
3239
3584
  bold = false;
3240
3585
  italic = false;
3241
3586
  }
3242
3587
  else if (italic) {
3243
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
+ }
3244
3604
  }
3245
3605
  }
3246
3606
  }
@@ -3432,6 +3792,19 @@ class NgxRichTextEditorComponent {
3432
3792
  .replace(/>/g, '&gt;');
3433
3793
  // Convert any Mathematical / LinkedIn Unicode characters and combining underlines into <b>, <i>, <u> tags
3434
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
+ }
3435
3808
  // Handle any legacy / malformed combined markers like __***text***__ -> clean to single effect (underline)
3436
3809
  escaped = escaped.replace(/__\*\*\*(.*?)\*\*\*__/g, '<u>$1</u>');
3437
3810
  escaped = escaped.replace(/__\*\*(.*?)\*\*__/g, '<u>$1</u>');
@@ -3440,6 +3813,7 @@ class NgxRichTextEditorComponent {
3440
3813
  escaped = escaped.replace(/__([^_]+?)__/g, '<u>$1</u>');
3441
3814
  escaped = escaped.replace(/\*\*([^\*]+?)\*\*/g, '<b>$1</b>');
3442
3815
  escaped = escaped.replace(/(?<!\*)\*(?!\*)([^\*\n]+?)(?<!\*)\*(?!\*)/g, '<i>$1</i>');
3816
+ escaped = escaped.replace(/\u200B/g, '');
3443
3817
  if (this.computedConfig.highlightHashtags ?? this.highlightHashtags) {
3444
3818
  escaped = escaped.replace(/(^|[\s>])#([a-zA-Z0-9_\u0080-\uFFFF]+)/g, '$1<span class="ngx-editor-hashtag post-content-hashtag">#$2</span>');
3445
3819
  }
@@ -3778,7 +4152,7 @@ class NgxRichTextEditorComponent {
3778
4152
  useExisting: forwardRef(() => NgxRichTextEditorComponent),
3779
4153
  multi: true,
3780
4154
  },
3781
- ], 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\">&lt;{{ activeBlockTag }}&gt;</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"] }] });
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\">&lt;{{ activeBlockTag }}&gt;</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"] }] });
3782
4156
  }
3783
4157
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NgxRichTextEditorComponent, decorators: [{
3784
4158
  type: Component,
@@ -3788,7 +4162,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3788
4162
  useExisting: forwardRef(() => NgxRichTextEditorComponent),
3789
4163
  multi: true,
3790
4164
  },
3791
- ], 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\">&lt;{{ activeBlockTag }}&gt;</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"] }]
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\">&lt;{{ activeBlockTag }}&gt;</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"] }]
3792
4166
  }], ctorParameters: () => [{ type: EditorCommandService }, { type: i2.DomSanitizer }, { type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { editorContentRef: [{
3793
4167
  type: ViewChild,
3794
4168
  args: ['editorContent', { static: false }]