@stackch/angular-richtext-editor 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Output, Input, Component, forwardRef, HostListener, ViewChild } from '@angular/core';
2
+ import { EventEmitter, Output, Input, Component, HostListener, ViewChild, Directive, forwardRef } from '@angular/core';
3
3
  import { CommonModule } from '@angular/common';
4
4
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
5
 
@@ -126,6 +126,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
126
126
  type: Output
127
127
  }] } });
128
128
 
129
+ // ─── Config & Types ───────────────────────────────────────────────────────────
129
130
  class StackchRichtextEditorConfig {
130
131
  // Sichtbarkeit einzelner Toolbar-Elemente (Default: an)
131
132
  showUndoRedo = true;
@@ -143,7 +144,6 @@ class StackchRichtextEditorConfig {
143
144
  // i18n overrides (partial), default is English
144
145
  i18n;
145
146
  }
146
- 1;
147
147
  class StackchRichtextEditorI18n {
148
148
  // Generic
149
149
  placeholder = 'Write…';
@@ -184,12 +184,10 @@ class StackchRichtextEditorI18n {
184
184
  h5Label = 'H5';
185
185
  h6Label = 'H6';
186
186
  }
187
- // Predefined i18n bundles
187
+ // ─── Predefined i18n bundles ──────────────────────────────────────────────────
188
188
  // Consumers can import these and pass via config.i18n to localize the toolbar/labels.
189
189
  const STACKCH_RTE_I18N_DE = {
190
- // Generic
191
190
  placeholder: 'Schreiben…',
192
- // Toolbar titles
193
191
  undoTitle: 'Rückgängig (Strg+Z)',
194
192
  redoTitle: 'Wiederholen (Strg+Y)',
195
193
  fontPanelTitle: 'Schrift & Größe',
@@ -216,20 +214,13 @@ const STACKCH_RTE_I18N_DE = {
216
214
  alignJustifyTitle: 'Blocksatz',
217
215
  linkTitle: 'Link',
218
216
  removeFormatTitle: 'Formatierung entfernen',
219
- // Heading menu labels
220
217
  paragraphLabel: 'Absatz (P)',
221
218
  codeLabel: 'Code (pre)',
222
- h1Label: 'H1',
223
- h2Label: 'H2',
224
- h3Label: 'H3',
225
- h4Label: 'H4',
226
- h5Label: 'H5',
227
- h6Label: 'H6',
219
+ h1Label: 'H1', h2Label: 'H2', h3Label: 'H3',
220
+ h4Label: 'H4', h5Label: 'H5', h6Label: 'H6',
228
221
  };
229
222
  const STACKCH_RTE_I18N_FR = {
230
- // Generic
231
223
  placeholder: 'Écrire…',
232
- // Toolbar titles
233
224
  undoTitle: 'Annuler (Ctrl+Z)',
234
225
  redoTitle: 'Rétablir (Ctrl+Y)',
235
226
  fontPanelTitle: 'Police et taille',
@@ -256,20 +247,13 @@ const STACKCH_RTE_I18N_FR = {
256
247
  alignJustifyTitle: 'Justifié',
257
248
  linkTitle: 'Lien',
258
249
  removeFormatTitle: 'Effacer la mise en forme',
259
- // Heading menu labels
260
250
  paragraphLabel: 'Paragraphe (P)',
261
251
  codeLabel: 'Code (pre)',
262
- h1Label: 'H1',
263
- h2Label: 'H2',
264
- h3Label: 'H3',
265
- h4Label: 'H4',
266
- h5Label: 'H5',
267
- h6Label: 'H6',
252
+ h1Label: 'H1', h2Label: 'H2', h3Label: 'H3',
253
+ h4Label: 'H4', h5Label: 'H5', h6Label: 'H6',
268
254
  };
269
255
  const STACKCH_RTE_I18N_IT = {
270
- // Generic
271
256
  placeholder: 'Scrivi…',
272
- // Toolbar titles
273
257
  undoTitle: 'Annulla (Ctrl+Z)',
274
258
  redoTitle: 'Ripristina (Ctrl+Y)',
275
259
  fontPanelTitle: 'Carattere e dimensione',
@@ -296,18 +280,22 @@ const STACKCH_RTE_I18N_IT = {
296
280
  alignJustifyTitle: 'Giustificato',
297
281
  linkTitle: 'Collegamento',
298
282
  removeFormatTitle: 'Rimuovi formattazione',
299
- // Heading menu labels
300
283
  paragraphLabel: 'Paragrafo (P)',
301
284
  codeLabel: 'Codice (pre)',
302
- h1Label: 'H1',
303
- h2Label: 'H2',
304
- h3Label: 'H3',
305
- h4Label: 'H4',
306
- h5Label: 'H5',
307
- h6Label: 'H6',
285
+ h1Label: 'H1', h2Label: 'H2', h3Label: 'H3',
286
+ h4Label: 'H4', h5Label: 'H5', h6Label: 'H6',
308
287
  };
309
- class StackchRichtextEditor {
288
+ // ─── Abstract Base ────────────────────────────────────────────────────────────
289
+ /**
290
+ * Abstract base class shared by StackchRichtextEditor (core) and
291
+ * StackchRichtextEditorMaterial (Material variant).
292
+ * Contains all editing logic; concrete subclasses add only the @Component
293
+ * decorator with their own template / styles / toolbar.
294
+ */
295
+ class StackchRichtextEditorBase {
310
296
  cdr;
297
+ // Override in concrete class to distinguish log output
298
+ logPrefix = '[RTE]';
311
299
  placeholder = '';
312
300
  showToolbar = true;
313
301
  fonts = [
@@ -327,6 +315,7 @@ class StackchRichtextEditor {
327
315
  get disabled() { return this._disabled; }
328
316
  _disabled = false;
329
317
  valueChange = new EventEmitter();
318
+ metricsChange = new EventEmitter();
330
319
  editorRef;
331
320
  constructor(cdr) {
332
321
  this.cdr = cdr;
@@ -361,7 +350,7 @@ class StackchRichtextEditor {
361
350
  isBoldActive = false;
362
351
  isItalicActive = false;
363
352
  isUnderlineActive = false;
364
- // Selection helpers
353
+ // ─── Selection helpers ───────────────────────────────────────────────────
365
354
  saveSelection() {
366
355
  const sel = window.getSelection();
367
356
  if (!sel || sel.rangeCount === 0)
@@ -374,7 +363,6 @@ class StackchRichtextEditor {
374
363
  }
375
364
  // Prevent toolbar buttons from stealing focus from the editor while preserving selection
376
365
  onToolbarMouseDown(evt) {
377
- // Keep focus on the editor to avoid persistent button focus outlines
378
366
  evt.preventDefault();
379
367
  evt.stopPropagation();
380
368
  this.saveSelection();
@@ -400,6 +388,7 @@ class StackchRichtextEditor {
400
388
  const node = container.nodeType === Node.ELEMENT_NODE ? container : container.parentElement;
401
389
  return !!node && editor.contains(node);
402
390
  }
391
+ // ─── Host listeners ──────────────────────────────────────────────────────
403
392
  // Update active-state on selection changes inside the editor
404
393
  onSelectionChange() {
405
394
  const sel = window.getSelection();
@@ -423,7 +412,7 @@ class StackchRichtextEditor {
423
412
  }
424
413
  // Keyboard: Undo/Redo
425
414
  onKeydown(evt) {
426
- const isMac = navigator.platform.toLowerCase().includes('mac');
415
+ const isMac = (navigator.userAgentData?.platform ?? navigator.platform).toLowerCase().includes('mac');
427
416
  const mod = isMac ? evt.metaKey : evt.ctrlKey;
428
417
  if (mod && !evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')) {
429
418
  evt.preventDefault();
@@ -434,6 +423,7 @@ class StackchRichtextEditor {
434
423
  this.redo();
435
424
  }
436
425
  }
426
+ // ─── Menu toggles ────────────────────────────────────────────────────────
437
427
  toggleFontMenu(evt) {
438
428
  this.saveSelection();
439
429
  this.showFontMenu = !this.showFontMenu;
@@ -451,7 +441,6 @@ class StackchRichtextEditor {
451
441
  toggleFontPanel(evt) {
452
442
  this.saveSelection();
453
443
  this.showFontPanel = !this.showFontPanel;
454
- // close others if open
455
444
  if (this.showFontPanel) {
456
445
  this.showFontMenu = false;
457
446
  this.showSizeMenu = false;
@@ -530,6 +519,7 @@ class StackchRichtextEditor {
530
519
  }
531
520
  evt.stopPropagation();
532
521
  }
522
+ // ─── Pick handlers (toolbar → editor) ───────────────────────────────────
533
523
  onPickAlign(where) {
534
524
  this.focusEditor();
535
525
  this.restoreSelection();
@@ -582,7 +572,7 @@ class StackchRichtextEditor {
582
572
  this.emitValue();
583
573
  this.takeSnapshot('spacing');
584
574
  }
585
- // ControlValueAccessor
575
+ // ─── ControlValueAccessor ────────────────────────────────────────────────
586
576
  writeValue(value) {
587
577
  const el = this.editorRef?.nativeElement;
588
578
  if (!el)
@@ -592,16 +582,17 @@ class StackchRichtextEditor {
592
582
  if (this.history.length === 0) {
593
583
  this.takeSnapshot('init');
594
584
  }
585
+ // Emit initial metrics when value is written programmatically
586
+ this.emitMetrics();
595
587
  }
596
588
  registerOnChange(fn) { this.onChange = fn; }
597
589
  registerOnTouched(fn) { this.onTouched = fn; }
598
590
  setDisabledState(isDisabled) { this.disabled = isDisabled; }
591
+ // ─── Toolbar actions ─────────────────────────────────────────────────────
599
592
  // Toolbar actions using document.execCommand (deprecated but broadly supported)
600
593
  cmd(command, value) {
601
- // Restore last selection from editor before applying an action triggered by toolbar
602
594
  this.focusEditor();
603
595
  this.restoreSelection();
604
- // Bevorzugt: eigene Range-basierte Implementierungen
605
596
  switch (command) {
606
597
  case 'bold':
607
598
  this.applyInlineStyleSmart('fontWeight', 'bold');
@@ -666,7 +657,6 @@ class StackchRichtextEditor {
666
657
  }
667
658
  // Fallback (deprecated): nur für Format entfernen / Links lösen als Übergang
668
659
  try {
669
- // Hinweis im Dev-Mode ausgeben
670
660
  if (!('___rteWarned' in this)) {
671
661
  console.warn('[richtext-editor] document.execCommand ist deprecated; Fallback wird nur für removeFormat/unlink verwendet.');
672
662
  this.___rteWarned = true;
@@ -702,10 +692,10 @@ class StackchRichtextEditor {
702
692
  return;
703
693
  const r = sel.getRangeAt(0);
704
694
  if (r.collapsed) {
705
- // Firefox/Edge: wenn keine explizite Auswahl, erweitere auf nächstes Wort
706
695
  this.expandRangeToWord(r);
707
696
  }
708
- this.applyInlineStyle('color', color);
697
+ this.applySelectionStyles({ color });
698
+ this.saveSelection();
709
699
  }
710
700
  applyHighlight(color) {
711
701
  this.focusEditor();
@@ -717,15 +707,14 @@ class StackchRichtextEditor {
717
707
  if (r.collapsed) {
718
708
  this.expandRangeToWord(r);
719
709
  }
720
- // Some browsers use 'hiliteColor', others 'backColor'
721
- this.applyInlineStyle('backgroundColor', color);
710
+ this.applySelectionStyles({ backgroundColor: color });
711
+ this.saveSelection();
722
712
  }
723
713
  expandRangeToWord(range) {
724
714
  try {
725
715
  const editor = this.editorRef.nativeElement;
726
716
  let node = range.startContainer;
727
717
  if (node.nodeType !== Node.TEXT_NODE) {
728
- // Versuche, einen Textknoten in der Nähe zu finden
729
718
  const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
730
719
  let found = null;
731
720
  while (walker.nextNode()) {
@@ -740,7 +729,6 @@ class StackchRichtextEditor {
740
729
  }
741
730
  const text = node.nodeType === Node.TEXT_NODE ? node.data : '';
742
731
  let start = 0, end = text.length;
743
- // einfache Wortgrenzen-Heuristik
744
732
  const pos = range.startOffset;
745
733
  for (let i = pos; i > 0; i--) {
746
734
  if (/\s/.test(text[i - 1])) {
@@ -784,7 +772,6 @@ class StackchRichtextEditor {
784
772
  this.updateInlineStates();
785
773
  }
786
774
  onPaste(evt) {
787
- // Optional: Clean paste to plain text while keeping basic formatting minimal.
788
775
  if (!evt.clipboardData)
789
776
  return;
790
777
  evt.preventDefault();
@@ -793,23 +780,33 @@ class StackchRichtextEditor {
793
780
  this.emitValue();
794
781
  this.takeSnapshot('paste');
795
782
  }
783
+ // ─── Internal emit helpers ───────────────────────────────────────────────
796
784
  emitValue() {
797
- // Clean up empty style attributes and redundant spans before emitting
798
785
  this.cleanupEmptyStylesAndSpans();
799
786
  const val = this.editorRef.nativeElement.innerHTML;
800
787
  this.onChange(val);
801
788
  this.valueChange.emit(val);
789
+ this.emitMetrics();
802
790
  this.updateInlineStates();
803
791
  }
792
+ emitMetrics() {
793
+ const editor = this.editorRef?.nativeElement;
794
+ if (!editor)
795
+ return;
796
+ const htmlLen = editor.innerHTML.length;
797
+ const textLen = (editor.textContent || '').length;
798
+ this.metricsChange.emit({ htmlLength: htmlLen, textLength: textLen });
799
+ }
804
800
  // Remove empty style attributes (style="") and unwrap spans without any attributes
805
801
  cleanupEmptyStylesAndSpans(root) {
802
+ console.log(`${this.logPrefix} cleanupEmptyStylesAndSpans: start cleanup`);
806
803
  const editor = root || this.editorRef.nativeElement;
807
804
  const toUnwrap = [];
805
+ const toRemove = [];
808
806
  const walker = document.createTreeWalker(editor, NodeFilter.SHOW_ELEMENT);
809
807
  while (walker.nextNode()) {
810
808
  const el = walker.currentNode;
811
809
  if (el.hasAttribute('style')) {
812
- // If no style declarations remain, drop the attribute
813
810
  const cssText = el.getAttribute('style') || '';
814
811
  const byApiEmpty = (el.style ? el.style.length === 0 : false);
815
812
  const normalized = cssText.replace(/[\s;]/g, '');
@@ -817,18 +814,30 @@ class StackchRichtextEditor {
817
814
  el.removeAttribute('style');
818
815
  }
819
816
  }
820
- // Unwrap spans that have no attributes left
821
- if (el.tagName === 'SPAN' && el.attributes.length === 0) {
822
- toUnwrap.push(el);
817
+ if (el.tagName === 'SPAN') {
818
+ if (!el.firstChild) {
819
+ console.log(`${this.logPrefix} cleanup: removing empty span`, el);
820
+ toRemove.push(el);
821
+ continue;
822
+ }
823
+ if (el.attributes.length === 0) {
824
+ console.log(`${this.logPrefix} cleanup: unwrapping style-free span`, el);
825
+ toUnwrap.push(el);
826
+ }
823
827
  }
824
828
  }
825
829
  for (const span of toUnwrap) {
830
+ console.log(`${this.logPrefix} cleanup: unwrap span`, { span, childCount: span.childNodes.length });
826
831
  while (span.firstChild)
827
832
  span.parentNode?.insertBefore(span.firstChild, span);
828
833
  span.remove();
829
834
  }
835
+ for (const span of toRemove) {
836
+ console.log(`${this.logPrefix} cleanup: remove span without children`, span);
837
+ span.remove();
838
+ }
830
839
  }
831
- // History API
840
+ // ─── History API ─────────────────────────────────────────────────────────
832
841
  get canUndo() { return this.historyIndex > 0; }
833
842
  get canRedo() { return this.historyIndex >= 0 && this.historyIndex < this.history.length - 1; }
834
843
  undo() {
@@ -890,12 +899,10 @@ class StackchRichtextEditor {
890
899
  const editor = this.editorRef.nativeElement;
891
900
  this.isRestoringHistory = true;
892
901
  editor.innerHTML = snap.html;
893
- // Selektion wiederherstellen
894
902
  if (snap.range) {
895
903
  this.restoreSerializedRange(snap.range);
896
904
  }
897
905
  else {
898
- // Cursor ans Ende
899
906
  const sel = window.getSelection();
900
907
  if (sel) {
901
908
  sel.removeAllRanges();
@@ -907,7 +914,6 @@ class StackchRichtextEditor {
907
914
  }
908
915
  this.historyIndex = index;
909
916
  this.isRestoringHistory = false;
910
- // Werte emittieren nach Restore
911
917
  this.emitValue();
912
918
  }
913
919
  serializeRange(range) {
@@ -981,365 +987,182 @@ class StackchRichtextEditor {
981
987
  el.focus();
982
988
  }
983
989
  }
984
- // Minimal inline style applier for inline styles
990
+ // ─── Inline style application ────────────────────────────────────────────
985
991
  applyInlineStyle(cssProp, value) {
986
- this.focusEditor();
987
- this.restoreSelection();
988
- const sel = window.getSelection();
989
- if (!sel || sel.rangeCount === 0)
990
- return;
991
- const range = sel.getRangeAt(0);
992
- if (range.collapsed)
993
- return;
994
- const span = document.createElement('span');
995
- span.style[cssProp] = value;
996
- // Robuster als surroundContents: extract + wrap + insert
997
- const frag = range.extractContents();
998
- span.appendChild(frag);
999
- range.insertNode(span);
1000
- // Cursor hinter das eingefügte Element setzen
1001
- sel.removeAllRanges();
1002
- const after = document.createRange();
1003
- after.setStartAfter(span);
1004
- after.collapse(true);
1005
- sel.addRange(after);
1006
- this.emitValue();
1007
- this.takeSnapshot('style');
992
+ this.applySelectionStyles({ [cssProp]: value });
1008
993
  }
1009
994
  applyInlineStyleSmart(cssProp, value) {
995
+ this.applySelectionStyles({ [cssProp]: value });
996
+ }
997
+ applySelectionStyles(styles) {
1010
998
  this.focusEditor();
1011
999
  this.restoreSelection();
1012
1000
  const sel = window.getSelection();
1013
- if (!sel || sel.rangeCount === 0)
1014
- return;
1015
- const range = sel.getRangeAt(0);
1016
- if (range.collapsed)
1017
- return;
1018
- const isBlockTag = (tag) => /^(P|DIV|PRE|H1|H2|H3|H4|H5|H6|LI|TD|TH)$/i.test(tag);
1019
- const getBlockAncestor = (node) => {
1020
- const editor = this.editorRef.nativeElement;
1021
- let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1022
- while (el && el !== editor && !isBlockTag(el.tagName))
1023
- el = el.parentElement;
1024
- return (el && el !== editor) ? el : null;
1025
- };
1026
- const startBlock = getBlockAncestor(range.startContainer);
1027
- const endBlock = getBlockAncestor(range.endContainer);
1028
- const crossesBlocks = !!startBlock && !!endBlock && startBlock !== endBlock;
1029
- if (!crossesBlocks) {
1030
- // Single block selection: keep using inline wrapper for precise range
1031
- this.applyInlineStyle(cssProp, value);
1001
+ if (!sel || sel.rangeCount === 0) {
1002
+ console.warn(`${this.logPrefix} applySelectionStyles: no selection available`, { styles });
1032
1003
  return;
1033
1004
  }
1034
- // Multi-block selection: apply style to each intersecting block element (avoid invalid span structure)
1035
- const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1036
- ? range.commonAncestorContainer
1037
- : (range.commonAncestorContainer.parentElement || this.editorRef.nativeElement);
1038
- const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
1039
- const styled = new Set();
1040
- while (walker.nextNode()) {
1041
- const el = walker.currentNode;
1042
- if (!isBlockTag(el.tagName))
1043
- continue;
1044
- try {
1045
- if (range.intersectsNode && range.intersectsNode(el))
1046
- styled.add(el);
1047
- }
1048
- catch { }
1005
+ let range = sel.getRangeAt(0);
1006
+ if (range.collapsed) {
1007
+ console.warn(`${this.logPrefix} applySelectionStyles: range is collapsed`, { styles });
1008
+ return;
1049
1009
  }
1050
- for (const el of styled) {
1051
- el.style[cssProp] = value;
1010
+ const entries = Object.entries(styles).filter(([, v]) => v != null && v !== '');
1011
+ if (entries.length === 0) {
1012
+ console.warn(`${this.logPrefix} applySelectionStyles: no style entries to apply`, { styles });
1013
+ return;
1052
1014
  }
1053
- this.emitValue();
1054
- this.takeSnapshot('style-blocks');
1055
- }
1056
- getIntersectingBlocks(range) {
1057
- const isBlockTag = (tag) => /^(P|DIV|PRE|H1|H2|H3|H4|H5|H6|LI|TD|TH)$/i.test(tag);
1058
- const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1059
- ? range.commonAncestorContainer
1060
- : (range.commonAncestorContainer.parentElement || this.editorRef.nativeElement);
1061
- const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
1062
- const blocks = [];
1063
- while (walker.nextNode()) {
1064
- const el = walker.currentNode;
1065
- if (!isBlockTag(el.tagName))
1066
- continue;
1067
- try {
1068
- if (range.intersectsNode && range.intersectsNode(el))
1069
- blocks.push(el);
1015
+ const propsToClear = Array.from(new Set(entries.map(([prop]) => this.toCssProperty(prop))));
1016
+ console.log(`${this.logPrefix} applySelectionStyles: clearing props before apply`, { propsToClear, entries });
1017
+ for (const cssProp of propsToClear) {
1018
+ const beforeClear = range.cloneRange();
1019
+ const clearedRange = this.removeInlineStyleInRange(range, cssProp, { suppressEmit: true });
1020
+ const refreshedSel = window.getSelection();
1021
+ if (refreshedSel && refreshedSel.rangeCount > 0) {
1022
+ range = refreshedSel.getRangeAt(0);
1070
1023
  }
1071
- catch { }
1072
- }
1073
- return blocks;
1074
- }
1075
- // Variant of applyInlineStyle that returns the created span and does not emit/snapshot by itself
1076
- wrapSelectionInline(cssProp, value) {
1077
- this.focusEditor();
1078
- this.restoreSelection();
1079
- const sel = window.getSelection();
1080
- if (!sel || sel.rangeCount === 0)
1081
- return null;
1082
- const range = sel.getRangeAt(0);
1083
- if (range.collapsed)
1084
- return null;
1085
- const span = document.createElement('span');
1086
- span.style[cssProp] = value;
1087
- const frag = range.extractContents();
1088
- span.appendChild(frag);
1089
- range.insertNode(span);
1090
- // Restore selection just after for continuity
1091
- sel.removeAllRanges();
1092
- const after = document.createRange();
1093
- after.setStartAfter(span);
1094
- after.collapse(true);
1095
- sel.addRange(after);
1096
- return span;
1097
- }
1098
- isBoldCarrier(el) {
1099
- if (!el)
1100
- return false;
1101
- const tag = el.tagName;
1102
- if (tag === 'B' || tag === 'STRONG')
1103
- return true;
1104
- const fw = el.style?.fontWeight || '';
1105
- if (fw && fw !== 'normal' && fw !== '400')
1106
- return true;
1107
- return false;
1108
- }
1109
- boldAncestorContainingRange(range) {
1110
- let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1111
- ? range.commonAncestorContainer
1112
- : range.commonAncestorContainer.parentElement;
1113
- const editor = this.editorRef.nativeElement;
1114
- while (el && el !== editor) {
1115
- if (this.isBoldCarrier(el)) {
1116
- // ensure el contains both boundary containers fully
1117
- const sc = range.startContainer;
1118
- const ec = range.endContainer;
1119
- if (el.contains(sc) && el.contains(ec))
1120
- return el;
1024
+ else if (clearedRange) {
1025
+ range = clearedRange;
1121
1026
  }
1122
- el = el.parentElement;
1123
- }
1124
- return null;
1125
- }
1126
- ancestorContainingRange(range, isCarrier) {
1127
- let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1128
- ? range.commonAncestorContainer
1129
- : range.commonAncestorContainer.parentElement;
1130
- const editor = this.editorRef.nativeElement;
1131
- while (el && el !== editor) {
1132
- if (isCarrier(el)) {
1133
- const sc = range.startContainer;
1134
- const ec = range.endContainer;
1135
- if (el.contains(sc) && el.contains(ec))
1136
- return el;
1027
+ if (range.collapsed && !beforeClear.collapsed) {
1028
+ console.warn(`${this.logPrefix} applySelectionStyles: range collapsed after clearing, restoring previous range`);
1029
+ const sel2 = window.getSelection();
1030
+ if (sel2) {
1031
+ sel2.removeAllRanges();
1032
+ sel2.addRange(beforeClear);
1033
+ }
1034
+ range = beforeClear;
1137
1035
  }
1138
- el = el.parentElement;
1139
1036
  }
1140
- return null;
1141
- }
1142
- splitCarrierAroundSelection(range, carrier) {
1143
- const parent = carrier.parentNode;
1144
- if (!parent)
1037
+ const segments = this.collectTextSegments(range);
1038
+ if (segments.length === 0) {
1039
+ console.warn(`${this.logPrefix} applySelectionStyles: no text segments found`, {
1040
+ entries,
1041
+ commonAncestor: range.commonAncestorContainer
1042
+ });
1145
1043
  return;
1146
- const rRight = document.createRange();
1147
- rRight.setStart(range.endContainer, range.endOffset);
1148
- rightGuard(carrier, rRight);
1149
- const rightFrag = rRight.extractContents();
1150
- const rLeft = document.createRange();
1151
- rLeft.setStart(carrier, 0);
1152
- leftGuard(range, rLeft);
1153
- const leftFrag = rLeft.extractContents();
1154
- if (leftFrag.childNodes.length) {
1155
- const leftClone = carrier.cloneNode(false);
1156
- leftClone.appendChild(leftFrag);
1157
- parent.insertBefore(leftClone, carrier);
1158
1044
  }
1159
- let insertAfterRef = carrier;
1160
- if (rightFrag.childNodes.length) {
1161
- const rightClone = carrier.cloneNode(false);
1162
- rightClone.appendChild(rightFrag);
1163
- parent.insertBefore(rightClone, carrier.nextSibling);
1164
- insertAfterRef = rightClone;
1165
- }
1166
- while (carrier.firstChild)
1167
- parent.insertBefore(carrier.firstChild, insertAfterRef);
1168
- carrier.remove();
1169
- function rightGuard(node, r) {
1170
- try {
1171
- r.setEnd(node, node.childNodes.length);
1045
+ console.log(`${this.logPrefix} applySelectionStyles: applying styles to segments`, {
1046
+ entries,
1047
+ segmentCount: segments.length
1048
+ });
1049
+ const wrappedSegments = [];
1050
+ for (let i = segments.length - 1; i >= 0; i--) {
1051
+ const wrapped = this.applyStylesToSegment(segments[i], entries);
1052
+ if (wrapped)
1053
+ wrappedSegments.unshift(wrapped);
1054
+ }
1055
+ if (wrappedSegments.length) {
1056
+ const first = wrappedSegments[0];
1057
+ const last = wrappedSegments[wrappedSegments.length - 1];
1058
+ const newRange = document.createRange();
1059
+ newRange.setStartBefore(first);
1060
+ newRange.setEndAfter(last);
1061
+ const selAfter = window.getSelection();
1062
+ if (selAfter) {
1063
+ selAfter.removeAllRanges();
1064
+ selAfter.addRange(newRange);
1172
1065
  }
1173
- catch { }
1066
+ this.saveSelectionFromRange(newRange);
1174
1067
  }
1175
- function leftGuard(rng, r) {
1176
- try {
1177
- r.setEnd(rng.startContainer, rng.startOffset);
1178
- }
1179
- catch { }
1180
- }
1181
- }
1182
- // Unwrap bold formatting only for the selected content inside a containing bold ancestor by splitting it into left/selection/right parts
1183
- deselectBoldBySplitting(range) {
1184
- const ancestor = this.boldAncestorContainingRange(range);
1185
- if (!ancestor)
1186
- return false;
1187
- this.splitCarrierAroundSelection(range, ancestor);
1188
- return true;
1189
- }
1190
- deselectItalicBySplitting(range) {
1191
- const anc = this.ancestorContainingRange(range, el => this.isItalicCarrier(el));
1192
- if (!anc)
1193
- return false;
1194
- this.splitCarrierAroundSelection(range, anc);
1195
- return true;
1196
- }
1197
- deselectUnderlineBySplitting(range) {
1198
- const anc = this.ancestorContainingRange(range, el => this.isUnderlineCarrier(el));
1199
- if (!anc)
1200
- return false;
1201
- this.splitCarrierAroundSelection(range, anc);
1202
- return true;
1068
+ this.emitValue();
1069
+ this.takeSnapshot('style');
1203
1070
  }
1204
- cleanupEmptyItalicSpans(root) {
1205
- const toRemove = [];
1206
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1207
- while (walker.nextNode()) {
1208
- const el = walker.currentNode;
1209
- const isItalic = (el.tagName === 'I' || el.tagName === 'EM' || (el.tagName === 'SPAN' && el.style.fontStyle && el.style.fontStyle !== 'normal'));
1210
- if (!isItalic)
1211
- continue;
1212
- const text = el.textContent ?? '';
1213
- const normalized = text.replace(/[\u00A0\u200B\s]/g, '');
1214
- const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
1215
- if (onlyWhitespace)
1216
- toRemove.push(el);
1071
+ collectTextSegments(range) {
1072
+ const segments = [];
1073
+ const root = range.commonAncestorContainer;
1074
+ if (root.nodeType === Node.TEXT_NODE) {
1075
+ const text = root;
1076
+ const start = text === range.startContainer ? range.startOffset : 0;
1077
+ const end = text === range.endContainer ? range.endOffset : text.data.length;
1078
+ if (start < end)
1079
+ segments.push({ node: text, start, end });
1080
+ return segments;
1217
1081
  }
1218
- for (const el of toRemove)
1219
- el.remove();
1220
- }
1221
- cleanupEmptyUnderlineSpans(root) {
1222
- const toRemove = [];
1223
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1082
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1224
1083
  while (walker.nextNode()) {
1225
- const el = walker.currentNode;
1226
- const isU = (el.tagName === 'U' || (el.tagName === 'SPAN' && typeof el.style.textDecoration === 'string' && el.style.textDecoration.includes('underline')));
1227
- if (!isU)
1084
+ const text = walker.currentNode;
1085
+ if (!text.data)
1228
1086
  continue;
1229
- const text = el.textContent ?? '';
1230
- const normalized = text.replace(/[\u00A0\u200B\s]/g, '');
1231
- const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
1232
- if (onlyWhitespace)
1233
- toRemove.push(el);
1087
+ if (!this.intersectsRange(range, text))
1088
+ continue;
1089
+ const start = text === range.startContainer ? range.startOffset : 0;
1090
+ const end = text === range.endContainer ? range.endOffset : text.data.length;
1091
+ if (start >= end)
1092
+ continue;
1093
+ segments.push({ node: text, start, end });
1234
1094
  }
1235
- for (const el of toRemove)
1236
- el.remove();
1095
+ return segments;
1237
1096
  }
1238
- // Remove bold wrappers and inline font-weight style inside a container
1239
- stripBoldWithin(container) {
1240
- const toProcess = [];
1241
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
1242
- while (walker.nextNode()) {
1243
- toProcess.push(walker.currentNode);
1097
+ intersectsRange(range, node) {
1098
+ try {
1099
+ return range.intersectsNode ? range.intersectsNode(node) : true;
1244
1100
  }
1245
- for (const el of toProcess) {
1246
- if (el.tagName === 'B' || el.tagName === 'STRONG') {
1247
- while (el.firstChild)
1248
- el.parentNode?.insertBefore(el.firstChild, el);
1249
- el.remove();
1250
- continue;
1251
- }
1252
- if (el.tagName === 'SPAN') {
1253
- if (el.style.fontWeight)
1254
- el.style.removeProperty('font-weight');
1255
- // Clean empty style spans
1256
- if (!el.getAttribute('style')) {
1257
- while (el.firstChild)
1258
- el.parentNode?.insertBefore(el.firstChild, el);
1259
- el.remove();
1260
- }
1261
- }
1101
+ catch {
1102
+ return false;
1262
1103
  }
1263
1104
  }
1264
- // Pull the given node out of any bold carrier ancestors by splitting them around the node
1265
- liftOutOfBoldAncestors(node) {
1266
- const editor = this.editorRef.nativeElement;
1267
- let currentBold = this.findClosest(node, 'b,strong,span[style*="font-weight"]');
1268
- while (currentBold && currentBold !== editor) {
1269
- this.splitOutOfAncestor(currentBold, node);
1270
- currentBold = this.findClosest(node, 'b,strong,span[style*="font-weight"]');
1271
- }
1272
- }
1273
- // Split arbitrary ancestor element so that `node` becomes a sibling outside of it, preserving order even if `node` is nested deeply
1274
- splitOutOfAncestor(ancestor, node) {
1275
- // First, climb from node up to direct child of ancestor, splitting wrappers after `node` at each level
1276
- let child = node;
1277
- let parent = child.parentElement;
1278
- while (parent && parent !== ancestor) {
1279
- const right = parent.cloneNode(false);
1280
- // move siblings after `child` into right clone
1281
- while (child.nextSibling)
1282
- right.appendChild(child.nextSibling);
1283
- // insert right after parent
1284
- parent.after(right);
1285
- // ascend
1286
- child = parent;
1287
- parent = child.parentElement;
1105
+ applyStylesToSegment(segment, entries) {
1106
+ const segRange = document.createRange();
1107
+ segRange.setStart(segment.node, segment.start);
1108
+ segRange.setEnd(segment.node, segment.end);
1109
+ const wrapper = document.createElement('span');
1110
+ for (const [prop, value] of entries) {
1111
+ wrapper.style.setProperty(this.toCssProperty(prop), value);
1288
1112
  }
1289
- if (!parent)
1290
- return;
1291
- // Now `child` is a direct child of ancestor
1292
- const before = ancestor.cloneNode(false);
1293
- while (ancestor.firstChild && ancestor.firstChild !== child) {
1294
- before.appendChild(ancestor.firstChild);
1295
- }
1296
- const after = ancestor.cloneNode(false);
1297
- while (child.nextSibling)
1298
- after.appendChild(child.nextSibling);
1299
- const gp = ancestor.parentNode;
1300
- if (!gp)
1301
- return;
1302
- if (before.childNodes.length)
1303
- gp.insertBefore(before, ancestor);
1304
- // move node out, place where ancestor was
1305
- gp.insertBefore(node, ancestor);
1306
- if (after.childNodes.length)
1307
- gp.insertBefore(after, ancestor);
1308
- ancestor.remove();
1309
- }
1310
- cleanupEmptyBoldSpans(root) {
1311
- const toRemove = [];
1312
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1313
- while (walker.nextNode()) {
1314
- const el = walker.currentNode;
1315
- const isBoldEl = (el.tagName === 'B' || el.tagName === 'STRONG' || (el.tagName === 'SPAN' && el.style.fontWeight && el.style.fontWeight !== 'normal' && el.style.fontWeight !== '400'));
1316
- if (!isBoldEl)
1317
- continue;
1318
- // remove if no children or only whitespace (including NBSP \u00A0 and zero-width space \u200B)
1319
- const text = el.textContent ?? '';
1320
- const normalized = text.replace(/[\u00A0\u200B\s]/g, '');
1321
- const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
1322
- if (onlyWhitespace) {
1323
- toRemove.push(el);
1324
- }
1113
+ try {
1114
+ segRange.surroundContents(wrapper);
1115
+ return this.mergeAdjacentStyledSpans(wrapper);
1325
1116
  }
1326
- for (const el of toRemove)
1327
- el.remove();
1117
+ catch (err) {
1118
+ console.error(`${this.logPrefix} applyStylesToSegment: failed to surround contents`, {
1119
+ error: err,
1120
+ segment,
1121
+ entries
1122
+ });
1123
+ }
1124
+ return null;
1328
1125
  }
1329
- maybeUnwrapNormalSpan(span) {
1126
+ toCssProperty(prop) {
1127
+ return prop.replace(/([A-Z])/g, '-$1').toLowerCase();
1128
+ }
1129
+ mergeAdjacentStyledSpans(span) {
1330
1130
  if (span.tagName !== 'SPAN')
1331
- return;
1332
- // If no bold applies from ancestors, drop the 400 style and unwrap if style empty
1333
- const hasBoldAbove = this.isNodeBold(span.parentElement);
1334
- if (!hasBoldAbove) {
1335
- if (span.style.fontWeight === '400')
1336
- span.style.removeProperty('font-weight');
1337
- if (!span.getAttribute('style')) {
1338
- while (span.firstChild)
1339
- span.parentNode?.insertBefore(span.firstChild, span);
1340
- span.remove();
1341
- }
1131
+ return span;
1132
+ let current = span;
1133
+ const normalize = (el) => el && el.tagName === 'SPAN' ? el : null;
1134
+ const styleText = (el) => (el ? el.getAttribute('style') || '' : '');
1135
+ // Merge with parent if identical
1136
+ let parent = normalize(current.parentElement);
1137
+ if (parent && styleText(parent) === styleText(current) && parent.attributes.length === current.attributes.length) {
1138
+ while (current.firstChild)
1139
+ parent.insertBefore(current.firstChild, current);
1140
+ current.remove();
1141
+ current = parent;
1142
+ }
1143
+ // Merge with previous sibling ignoring empty text nodes
1144
+ let prev = current.previousSibling;
1145
+ while (prev && prev.nodeType === Node.TEXT_NODE && !prev.data.trim())
1146
+ prev = prev.previousSibling;
1147
+ if (prev instanceof HTMLElement && prev.tagName === 'SPAN' && styleText(prev) === styleText(current) && prev.attributes.length === current.attributes.length) {
1148
+ while (current.firstChild)
1149
+ prev.appendChild(current.firstChild);
1150
+ current.remove();
1151
+ current = prev;
1342
1152
  }
1153
+ // Merge with next sibling ignoring empty text nodes
1154
+ let next = current.nextSibling;
1155
+ while (next && next.nodeType === Node.TEXT_NODE && !next.data.trim())
1156
+ next = next.nextSibling;
1157
+ if (next instanceof HTMLElement && next.tagName === 'SPAN' && styleText(next) === styleText(current) && next.attributes.length === current.attributes.length) {
1158
+ while (next.firstChild)
1159
+ current.appendChild(next.firstChild);
1160
+ next.remove();
1161
+ }
1162
+ return current;
1163
+ }
1164
+ saveSelectionFromRange(range) {
1165
+ this.savedRange = range.cloneRange();
1343
1166
  }
1344
1167
  wrapSelectionWith(tag, attrs) {
1345
1168
  const sel = window.getSelection();
@@ -1358,7 +1181,6 @@ class StackchRichtextEditor {
1358
1181
  const frag = range.extractContents();
1359
1182
  el.appendChild(frag);
1360
1183
  range.insertNode(el);
1361
- // Auswahl hinter das Element setzen
1362
1184
  sel.removeAllRanges();
1363
1185
  const after = document.createRange();
1364
1186
  after.setStartAfter(el);
@@ -1373,14 +1195,13 @@ class StackchRichtextEditor {
1373
1195
  range.deleteContents();
1374
1196
  const node = document.createTextNode(text);
1375
1197
  range.insertNode(node);
1376
- // Cursor ans Ende des eingefügten Textes
1377
1198
  sel.removeAllRanges();
1378
1199
  const after = document.createRange();
1379
1200
  after.setStartAfter(node);
1380
1201
  after.collapse(true);
1381
1202
  sel.addRange(after);
1382
1203
  }
1383
- // ----- Inline toggle logic (Bold/Italic/Underline) -----
1204
+ // ─── Inline toggle logic (Bold / Italic / Underline) ─────────────────────
1384
1205
  updateInlineStates() {
1385
1206
  const sel = window.getSelection();
1386
1207
  if (!sel || sel.rangeCount === 0) {
@@ -1402,63 +1223,19 @@ class StackchRichtextEditor {
1402
1223
  this.isBoldActive = this.computeBoldAnyForRange(range);
1403
1224
  this.isItalicActive = this.computeItalicAnyForRange(range);
1404
1225
  this.isUnderlineActive = this.computeUnderlineAnyForRange(range);
1405
- // ensure UI reflects changes immediately
1406
1226
  try {
1407
1227
  this.cdr.detectChanges();
1408
1228
  }
1409
1229
  catch { }
1410
1230
  }
1411
1231
  computeBoldAnyForRange(range) {
1412
- const editor = this.editorRef.nativeElement;
1413
- const root = this.editorRef.nativeElement;
1414
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1415
- while (walker.nextNode()) {
1416
- const n = walker.currentNode;
1417
- if (!n.data.trim())
1418
- continue;
1419
- try {
1420
- if (range.intersectsNode(n)) {
1421
- if (this.isNodeBold(n.parentElement))
1422
- return true;
1423
- }
1424
- }
1425
- catch { }
1426
- }
1427
- return false;
1232
+ return this.selectionHasStyle(range, 'font-weight');
1428
1233
  }
1429
1234
  computeItalicAnyForRange(range) {
1430
- const root = this.editorRef.nativeElement;
1431
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1432
- while (walker.nextNode()) {
1433
- const n = walker.currentNode;
1434
- if (!n.data.trim())
1435
- continue;
1436
- try {
1437
- if (range.intersectsNode(n)) {
1438
- if (this.isNodeItalic(n.parentElement))
1439
- return true;
1440
- }
1441
- }
1442
- catch { }
1443
- }
1444
- return false;
1235
+ return this.selectionHasStyle(range, 'font-style');
1445
1236
  }
1446
1237
  computeUnderlineAnyForRange(range) {
1447
- const root = this.editorRef.nativeElement;
1448
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1449
- while (walker.nextNode()) {
1450
- const n = walker.currentNode;
1451
- if (!n.data.trim())
1452
- continue;
1453
- try {
1454
- if (range.intersectsNode(n)) {
1455
- if (this.isNodeUnderline(n.parentElement))
1456
- return true;
1457
- }
1458
- }
1459
- catch { }
1460
- }
1461
- return false;
1238
+ return this.selectionHasStyle(range, 'text-decoration');
1462
1239
  }
1463
1240
  isNodeBold(node) {
1464
1241
  let el = node;
@@ -1499,120 +1276,244 @@ class StackchRichtextEditor {
1499
1276
  }
1500
1277
  return false;
1501
1278
  }
1502
- isItalicCarrier(el) {
1503
- if (!el)
1504
- return false;
1505
- const tag = el.tagName;
1506
- if (tag === 'I' || tag === 'EM')
1507
- return true;
1508
- const fs = el.style?.fontStyle || '';
1509
- return !!fs && fs !== 'normal';
1510
- }
1511
- isUnderlineCarrier(el) {
1512
- if (!el)
1513
- return false;
1514
- const tag = el.tagName;
1515
- if (tag === 'U')
1516
- return true;
1517
- const td = el.style?.textDecoration || '';
1518
- return typeof td === 'string' && td.includes('underline');
1519
- }
1520
- computeBoldActiveForRange(range) {
1279
+ removeInlineStyleInRange(range, cssPropKebab, options) {
1280
+ const shouldRestoreSelection = options?.restoreSelection !== false;
1281
+ this.isolateRangeFromStyleCarriers(range, cssPropKebab);
1521
1282
  const editor = this.editorRef.nativeElement;
1522
- const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
1523
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1524
- const nodes = [];
1525
- while (walker.nextNode()) {
1526
- const n = walker.currentNode;
1527
- if (!n.data.trim())
1528
- continue;
1529
- try {
1530
- if (range.intersectsNode(n))
1531
- nodes.push(n);
1532
- }
1533
- catch { /* Safari may throw if node not in same tree */ }
1534
- }
1535
- if (nodes.length === 0)
1536
- return false;
1537
- return nodes.every(t => this.isNodeBold(t.parentElement));
1538
- }
1539
- computeItalicActiveForRange(range) {
1540
- const editor = this.editorRef.nativeElement;
1541
- const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
1542
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1543
- const nodes = [];
1544
- while (walker.nextNode()) {
1545
- const n = walker.currentNode;
1546
- if (!n.data.trim())
1547
- continue;
1283
+ const selAfter = window.getSelection();
1284
+ const effectiveRange = selAfter && selAfter.rangeCount > 0 ? selAfter.getRangeAt(0) : range;
1285
+ const common = effectiveRange.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1286
+ ? effectiveRange.commonAncestorContainer
1287
+ : (effectiveRange.commonAncestorContainer.parentElement || editor);
1288
+ const affected = [];
1289
+ const intersects = (node) => {
1548
1290
  try {
1549
- if (range.intersectsNode(n))
1550
- nodes.push(n);
1291
+ return effectiveRange.intersectsNode ? effectiveRange.intersectsNode(node) : true;
1551
1292
  }
1552
- catch { }
1553
- }
1554
- if (nodes.length === 0)
1555
- return false;
1556
- return nodes.every(t => this.isNodeItalic(t.parentElement));
1557
- }
1558
- computeUnderlineActiveForRange(range) {
1559
- const editor = this.editorRef.nativeElement;
1560
- const root = this.isRangeInEditor(range) ? this.editorRef.nativeElement : range.commonAncestorContainer;
1561
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1562
- const nodes = [];
1563
- while (walker.nextNode()) {
1564
- const n = walker.currentNode;
1565
- if (!n.data.trim())
1566
- continue;
1567
- try {
1568
- if (range.intersectsNode(n))
1569
- nodes.push(n);
1293
+ catch {
1294
+ return false;
1570
1295
  }
1571
- catch { }
1296
+ };
1297
+ const matchesEl = (el) => this.isStyleCarrier(el, cssPropKebab);
1298
+ console.log(`${this.logPrefix} removeInlineStyleInRange:start`, {
1299
+ cssPropKebab,
1300
+ rangeCollapsed: effectiveRange.collapsed,
1301
+ commonTag: common.tagName,
1302
+ commonHasStyle: common.style ? common.style.getPropertyValue?.(cssPropKebab) : undefined
1303
+ });
1304
+ if (common instanceof HTMLElement && matchesEl(common) && intersects(common)) {
1305
+ affected.push(common);
1572
1306
  }
1573
- if (nodes.length === 0)
1574
- return false;
1575
- return nodes.every(t => this.isNodeUnderline(t.parentElement));
1576
- }
1577
- removeInlineStyleInRange(range, cssPropKebab) {
1578
- const editor = this.editorRef.nativeElement;
1579
- const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement || editor;
1580
- const affected = [];
1581
1307
  const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
1582
1308
  while (walker.nextNode()) {
1583
1309
  const el = walker.currentNode;
1584
- let matches = false;
1585
- if (el.tagName === 'SPAN' && el.style && el.style.getPropertyValue(cssPropKebab))
1586
- matches = true;
1587
- if (cssPropKebab === 'font-weight' && (el.tagName === 'B' || el.tagName === 'STRONG'))
1588
- matches = true;
1589
- if (cssPropKebab === 'font-style' && (el.tagName === 'I' || el.tagName === 'EM'))
1590
- matches = true;
1591
- if (cssPropKebab === 'text-decoration' && el.tagName === 'U')
1592
- matches = true;
1593
- if (!matches)
1310
+ if (!matchesEl(el))
1594
1311
  continue;
1595
- try {
1596
- if (range.intersectsNode ? range.intersectsNode(el) : true)
1597
- affected.push(el);
1312
+ if (intersects(el)) {
1313
+ affected.push(el);
1314
+ console.log(`${this.logPrefix} removeInlineStyleInRange:match`, el.tagName, el.getAttribute('style'));
1598
1315
  }
1599
- catch { }
1600
1316
  }
1317
+ let removedTags = 0;
1318
+ let removedProps = 0;
1601
1319
  for (const el of affected) {
1602
1320
  if (el.tagName === 'B' || el.tagName === 'STRONG' || el.tagName === 'I' || el.tagName === 'EM' || el.tagName === 'U') {
1321
+ console.log(`${this.logPrefix} unwrap tag`, el.tagName);
1603
1322
  while (el.firstChild)
1604
1323
  el.parentNode?.insertBefore(el.firstChild, el);
1605
1324
  el.remove();
1325
+ removedTags++;
1606
1326
  continue;
1607
1327
  }
1328
+ console.log(`${this.logPrefix} remove style`, cssPropKebab, 'from', el.tagName, el.getAttribute('style'));
1608
1329
  el.style.removeProperty(cssPropKebab);
1330
+ if (!el.style.length) {
1331
+ console.log(`${this.logPrefix} removeInlineStyleInRange: style attribute now empty, removing attribute`, el);
1332
+ el.removeAttribute('style');
1333
+ }
1334
+ removedProps++;
1609
1335
  if (!el.getAttribute('style')) {
1336
+ console.log(`${this.logPrefix} unwrap empty styled span`, el.tagName);
1610
1337
  while (el.firstChild)
1611
1338
  el.parentNode?.insertBefore(el.firstChild, el);
1612
1339
  el.remove();
1613
1340
  }
1614
1341
  }
1615
- this.emitValue();
1342
+ this.pruneDanglingStyleCarriers(common, cssPropKebab);
1343
+ console.log(`${this.logPrefix} removeInlineStyleInRange:done`, { affected: affected.length, removedTags, removedProps });
1344
+ let resultRange = null;
1345
+ if (!options?.suppressEmit) {
1346
+ this.emitValue();
1347
+ resultRange = null;
1348
+ }
1349
+ else {
1350
+ const sel2 = window.getSelection();
1351
+ if (sel2 && sel2.rangeCount > 0) {
1352
+ resultRange = sel2.getRangeAt(0).cloneRange();
1353
+ }
1354
+ else if (shouldRestoreSelection) {
1355
+ resultRange = effectiveRange.cloneRange();
1356
+ }
1357
+ if (shouldRestoreSelection && resultRange) {
1358
+ const selRestore = window.getSelection();
1359
+ if (selRestore) {
1360
+ selRestore.removeAllRanges();
1361
+ selRestore.addRange(resultRange);
1362
+ this.saveSelectionFromRange(resultRange);
1363
+ }
1364
+ }
1365
+ }
1366
+ return resultRange;
1367
+ }
1368
+ pruneDanglingStyleCarriers(root, cssPropKebab) {
1369
+ const toRemove = [];
1370
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1371
+ if (root instanceof HTMLElement && this.isStyleCarrier(root, cssPropKebab) && !this.elementHasVisibleContent(root)) {
1372
+ toRemove.push(root);
1373
+ }
1374
+ while (walker.nextNode()) {
1375
+ const el = walker.currentNode;
1376
+ if (!this.isStyleCarrier(el, cssPropKebab))
1377
+ continue;
1378
+ if (this.elementHasVisibleContent(el))
1379
+ continue;
1380
+ toRemove.push(el);
1381
+ }
1382
+ for (const el of toRemove) {
1383
+ console.log(`${this.logPrefix} pruneDanglingStyleCarriers: removing empty carrier`, { tag: el.tagName, style: el.getAttribute('style') });
1384
+ el.remove();
1385
+ }
1386
+ }
1387
+ elementHasVisibleContent(node) {
1388
+ if (node.nodeType === Node.TEXT_NODE) {
1389
+ const text = node.data;
1390
+ for (let i = 0; i < text.length; i++) {
1391
+ const ch = text[i];
1392
+ if (ch === '\u00a0')
1393
+ return true;
1394
+ if (!/\s/.test(ch) && ch !== '\u200b' && ch !== '\u200c' && ch !== '\u200d' && ch !== '\ufeff') {
1395
+ return true;
1396
+ }
1397
+ }
1398
+ return false;
1399
+ }
1400
+ if (node.nodeType === Node.ELEMENT_NODE) {
1401
+ const el = node;
1402
+ if (el.tagName === 'BR')
1403
+ return true;
1404
+ for (const child of Array.from(node.childNodes)) {
1405
+ if (this.elementHasVisibleContent(child))
1406
+ return true;
1407
+ }
1408
+ }
1409
+ return false;
1410
+ }
1411
+ isolateRangeFromStyleCarriers(range, cssPropKebab) {
1412
+ let carrier = this.findCarrierContainingRange(range, cssPropKebab);
1413
+ while (carrier) {
1414
+ this.splitCarrierAroundSelection(range, carrier);
1415
+ carrier = this.findCarrierContainingRange(range, cssPropKebab);
1416
+ }
1417
+ }
1418
+ selectionHasStyle(range, cssPropKebab) {
1419
+ const editor = this.editorRef.nativeElement;
1420
+ const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1421
+ ? range.commonAncestorContainer
1422
+ : (range.commonAncestorContainer.parentElement || editor);
1423
+ const intersects = (node) => this.intersectsRange(range, node);
1424
+ if (common instanceof HTMLElement && this.isStyleCarrier(common, cssPropKebab) && intersects(common)) {
1425
+ return true;
1426
+ }
1427
+ const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
1428
+ while (walker.nextNode()) {
1429
+ const el = walker.currentNode;
1430
+ if (!this.isStyleCarrier(el, cssPropKebab))
1431
+ continue;
1432
+ if (intersects(el))
1433
+ return true;
1434
+ }
1435
+ return false;
1436
+ }
1437
+ findCarrierContainingRange(range, cssPropKebab) {
1438
+ let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1439
+ ? range.commonAncestorContainer
1440
+ : range.commonAncestorContainer.parentElement;
1441
+ const editor = this.editorRef.nativeElement;
1442
+ while (el && el !== editor) {
1443
+ if (this.isStyleCarrier(el, cssPropKebab)) {
1444
+ const sc = range.startContainer;
1445
+ const ec = range.endContainer;
1446
+ if (el.contains(sc) && el.contains(ec))
1447
+ return el;
1448
+ }
1449
+ el = el.parentElement;
1450
+ }
1451
+ return null;
1452
+ }
1453
+ isStyleCarrier(el, cssPropKebab) {
1454
+ const tag = el.tagName;
1455
+ if (cssPropKebab === 'font-weight') {
1456
+ if (tag === 'B' || tag === 'STRONG')
1457
+ return true;
1458
+ const fw = el.style?.fontWeight || '';
1459
+ return !!fw && fw !== 'normal' && fw !== '400';
1460
+ }
1461
+ if (cssPropKebab === 'font-style') {
1462
+ if (tag === 'I' || tag === 'EM')
1463
+ return true;
1464
+ const fs = el.style?.fontStyle || '';
1465
+ return !!fs && fs !== 'normal';
1466
+ }
1467
+ if (cssPropKebab === 'text-decoration') {
1468
+ if (tag === 'U')
1469
+ return true;
1470
+ const td = el.style?.textDecoration || el.style?.textDecorationLine || '';
1471
+ return typeof td === 'string' && td.includes('underline');
1472
+ }
1473
+ const inline = el.style?.getPropertyValue(cssPropKebab) || '';
1474
+ if (!inline)
1475
+ return false;
1476
+ if (cssPropKebab === 'color') {
1477
+ return inline !== 'inherit' && inline !== 'initial';
1478
+ }
1479
+ if (cssPropKebab === 'background-color') {
1480
+ return inline !== 'transparent' && inline !== 'initial';
1481
+ }
1482
+ return true;
1483
+ }
1484
+ splitCarrierAroundSelection(range, carrier) {
1485
+ const parent = carrier.parentNode;
1486
+ if (!parent)
1487
+ return;
1488
+ const rightRange = document.createRange();
1489
+ rightRange.setStart(range.endContainer, range.endOffset);
1490
+ try {
1491
+ rightRange.setEnd(carrier, carrier.childNodes.length);
1492
+ }
1493
+ catch { }
1494
+ const rightFrag = rightRange.extractContents();
1495
+ const leftRange = document.createRange();
1496
+ leftRange.setStart(carrier, 0);
1497
+ try {
1498
+ leftRange.setEnd(range.startContainer, range.startOffset);
1499
+ }
1500
+ catch { }
1501
+ const leftFrag = leftRange.extractContents();
1502
+ if (leftFrag.childNodes.length) {
1503
+ const leftClone = carrier.cloneNode(false);
1504
+ leftClone.appendChild(leftFrag);
1505
+ parent.insertBefore(leftClone, carrier);
1506
+ }
1507
+ let insertAfter = carrier;
1508
+ if (rightFrag.childNodes.length) {
1509
+ const rightClone = carrier.cloneNode(false);
1510
+ rightClone.appendChild(rightFrag);
1511
+ parent.insertBefore(rightClone, carrier.nextSibling);
1512
+ insertAfter = rightClone;
1513
+ }
1514
+ while (carrier.firstChild)
1515
+ parent.insertBefore(carrier.firstChild, insertAfter);
1516
+ carrier.remove();
1616
1517
  }
1617
1518
  toggleBold() {
1618
1519
  this.focusEditor();
@@ -1624,45 +1525,12 @@ class StackchRichtextEditor {
1624
1525
  if (range.collapsed)
1625
1526
  this.expandRangeToWord(range);
1626
1527
  const anyActive = this.computeBoldAnyForRange(range);
1627
- const blocks = this.getIntersectingBlocks(range);
1628
- const multiBlock = blocks.length > 1;
1528
+ console.log(`${this.logPrefix} toggleBold`, { anyActive, rangeCollapsed: range.collapsed });
1629
1529
  if (anyActive) {
1630
- if (multiBlock) {
1631
- for (const b of blocks)
1632
- b.style.removeProperty('font-weight');
1633
- this.emitValue();
1634
- this.updateInlineStates();
1635
- this.takeSnapshot('toggle-bold-blocks');
1636
- return;
1637
- }
1638
- // Preferred: split bold ancestor around selection so we keep surrounding text intact
1639
- const changed = this.deselectBoldBySplitting(range);
1640
- if (changed) {
1641
- this.cleanupEmptyBoldSpans(this.editorRef.nativeElement);
1642
- this.emitValue();
1643
- }
1644
- else {
1645
- // Fallback: previous neutralization approach for complex cases
1646
- const normal = this.wrapSelectionInline('fontWeight', '400');
1647
- if (normal) {
1648
- this.stripBoldWithin(normal);
1649
- this.liftOutOfBoldAncestors(normal);
1650
- this.cleanupEmptyBoldSpans(this.editorRef.nativeElement);
1651
- this.maybeUnwrapNormalSpan(normal);
1652
- this.emitValue();
1653
- }
1654
- }
1530
+ this.removeInlineStyleInRange(range, 'font-weight');
1655
1531
  }
1656
1532
  else {
1657
- if (multiBlock) {
1658
- for (const b of blocks)
1659
- b.style.fontWeight = 'bold';
1660
- this.emitValue();
1661
- this.updateInlineStates();
1662
- this.takeSnapshot('toggle-bold-blocks');
1663
- return;
1664
- }
1665
- this.applyInlineStyleSmart('fontWeight', 'bold');
1533
+ this.applySelectionStyles({ fontWeight: 'bold' });
1666
1534
  }
1667
1535
  this.updateInlineStates();
1668
1536
  this.takeSnapshot('toggle-bold');
@@ -1678,18 +1546,10 @@ class StackchRichtextEditor {
1678
1546
  this.expandRangeToWord(range);
1679
1547
  const anyActive = this.computeItalicAnyForRange(range);
1680
1548
  if (anyActive) {
1681
- const changed = this.deselectItalicBySplitting(range);
1682
- if (changed) {
1683
- this.cleanupEmptyItalicSpans(this.editorRef.nativeElement);
1684
- this.emitValue();
1685
- }
1686
- else {
1687
- this.removeInlineStyleInRange(range, 'font-style');
1688
- this.applyInlineStyle('fontStyle', 'normal');
1689
- }
1549
+ this.removeInlineStyleInRange(range, 'font-style');
1690
1550
  }
1691
1551
  else {
1692
- this.applyInlineStyleSmart('fontStyle', 'italic');
1552
+ this.applySelectionStyles({ fontStyle: 'italic' });
1693
1553
  }
1694
1554
  this.updateInlineStates();
1695
1555
  this.takeSnapshot('toggle-italic');
@@ -1705,22 +1565,15 @@ class StackchRichtextEditor {
1705
1565
  this.expandRangeToWord(range);
1706
1566
  const anyActive = this.computeUnderlineAnyForRange(range);
1707
1567
  if (anyActive) {
1708
- const changed = this.deselectUnderlineBySplitting(range);
1709
- if (changed) {
1710
- this.cleanupEmptyUnderlineSpans(this.editorRef.nativeElement);
1711
- this.emitValue();
1712
- }
1713
- else {
1714
- this.removeInlineStyleInRange(range, 'text-decoration');
1715
- this.applyInlineStyle('textDecoration', 'none');
1716
- }
1568
+ this.removeInlineStyleInRange(range, 'text-decoration');
1717
1569
  }
1718
1570
  else {
1719
- this.applyInlineStyleSmart('textDecoration', 'underline');
1571
+ this.applySelectionStyles({ textDecoration: 'underline' });
1720
1572
  }
1721
1573
  this.updateInlineStates();
1722
1574
  this.takeSnapshot('toggle-underline');
1723
1575
  }
1576
+ // ─── Block operations ─────────────────────────────────────────────────────
1724
1577
  getCurrentRange() {
1725
1578
  const sel = window.getSelection();
1726
1579
  if (!sel || sel.rangeCount === 0)
@@ -1734,7 +1587,6 @@ class StackchRichtextEditor {
1734
1587
  while (el && el.parentElement && el.parentElement !== editor) {
1735
1588
  el = el.parentElement;
1736
1589
  }
1737
- // Wenn direktes Kind des Editors
1738
1590
  if (el && el.parentElement === editor)
1739
1591
  return el;
1740
1592
  return editor;
@@ -1753,34 +1605,29 @@ class StackchRichtextEditor {
1753
1605
  if (!range)
1754
1606
  return;
1755
1607
  const editor = this.editorRef.nativeElement;
1756
- // Wenn bereits in einer Liste, Liste aufheben
1757
1608
  const listAncestor = this.findClosest(range.commonAncestorContainer, 'ul,ol');
1758
1609
  if (listAncestor) {
1759
- // unwrap: li-Inhalte an die Stelle der Liste setzen
1760
1610
  const frag = document.createDocumentFragment();
1761
- const items = Array.from(listAncestor.children); // li
1611
+ const items = Array.from(listAncestor.children);
1762
1612
  for (const li of items) {
1763
1613
  while (li.firstChild)
1764
1614
  frag.appendChild(li.firstChild);
1765
- // Optional: Zeilenumbruch zwischen Items
1766
1615
  frag.appendChild(document.createElement('br'));
1767
1616
  }
1768
1617
  listAncestor.replaceWith(frag);
1769
1618
  return;
1770
1619
  }
1771
- // Neue Liste um die aktuelle Auswahl legen
1772
1620
  const list = document.createElement(kind);
1773
1621
  const li = document.createElement('li');
1774
1622
  const contents = range.extractContents();
1775
1623
  if (!contents.hasChildNodes()) {
1776
- li.textContent = '\u200b'; // zero-width space um leere li zu vermeiden
1624
+ li.textContent = '\u200b';
1777
1625
  }
1778
1626
  else {
1779
1627
  li.appendChild(contents);
1780
1628
  }
1781
1629
  list.appendChild(li);
1782
1630
  range.insertNode(list);
1783
- // Cursor ins li setzen
1784
1631
  const sel = window.getSelection();
1785
1632
  if (sel) {
1786
1633
  sel.removeAllRanges();
@@ -1806,7 +1653,6 @@ class StackchRichtextEditor {
1806
1653
  applyAlign(startBlock);
1807
1654
  }
1808
1655
  else {
1809
- // grob: alle direkten Kinder zwischen start und end ausrichten
1810
1656
  const children = Array.from(editor.children);
1811
1657
  const i1 = children.indexOf(startBlock);
1812
1658
  const i2 = children.indexOf(endBlock);
@@ -1860,8 +1706,8 @@ class StackchRichtextEditor {
1860
1706
  }
1861
1707
  }
1862
1708
  }
1863
- // Versucht, Margin/Padding auf die konkrete Text-Selektion anzuwenden, indem die Auswahl mit einem Span umschlossen wird.
1864
- // Liefert true, wenn inline angewendet werden konnte; sonst false (z. B. wenn Auswahl blockübergreifend ist).
1709
+ // Versucht, Margin/Padding auf die konkrete Text-Selektion anzuwenden (inline Wrapper).
1710
+ // Liefert true, wenn inline angewendet werden konnte; sonst false.
1865
1711
  applySpacingToSelection(kind, target, value) {
1866
1712
  const sel = window.getSelection();
1867
1713
  if (!sel || sel.rangeCount === 0)
@@ -1871,7 +1717,6 @@ class StackchRichtextEditor {
1871
1717
  return false;
1872
1718
  const startBlock = this.getEditorChildAncestor(range.startContainer);
1873
1719
  const endBlock = this.getEditorChildAncestor(range.endContainer);
1874
- // Nur inline, wenn innerhalb desselben Blocks
1875
1720
  if (!startBlock || !endBlock || startBlock !== endBlock)
1876
1721
  return false;
1877
1722
  const wrapper = document.createElement('span');
@@ -1887,15 +1732,12 @@ class StackchRichtextEditor {
1887
1732
  wrapper.style[`${kind}Left`] = px;
1888
1733
  wrapper.style[`${kind}Right`] = px;
1889
1734
  }
1890
- // Für vertikale Margins auf Inline-Elementen sicherstellen, dass sie greifen
1891
1735
  if (kind === 'margin' && (target === 'all' || target === 'vertical')) {
1892
1736
  wrapper.style.display = 'inline-block';
1893
1737
  }
1894
- // Auswahl extrahieren und in Wrapper einsetzen
1895
1738
  const frag = range.extractContents();
1896
1739
  wrapper.appendChild(frag);
1897
1740
  range.insertNode(wrapper);
1898
- // Gleichartige Wrapper zusammenführen und Cursor korrekt setzen
1899
1741
  const normalized = this.normalizeSpacingSpans(wrapper, kind, target, value) || wrapper;
1900
1742
  sel.removeAllRanges();
1901
1743
  const after = document.createRange();
@@ -1903,7 +1745,6 @@ class StackchRichtextEditor {
1903
1745
  after.setStartAfter(normalized);
1904
1746
  }
1905
1747
  else {
1906
- // Fallback: an das Ende des Startblocks
1907
1748
  const sb = this.getEditorChildAncestor(range.startContainer);
1908
1749
  if (sb)
1909
1750
  after.selectNodeContents(sb);
@@ -1912,7 +1753,7 @@ class StackchRichtextEditor {
1912
1753
  sel.addRange(after);
1913
1754
  return true;
1914
1755
  }
1915
- normalizeSpacingSpans(span, kind, target, value) {
1756
+ normalizeSpacingSpans(span, kind, target, _value) {
1916
1757
  const props = [];
1917
1758
  if (target === 'all')
1918
1759
  props.push(kind);
@@ -1921,7 +1762,6 @@ class StackchRichtextEditor {
1921
1762
  if (target === 'horizontal')
1922
1763
  props.push(`${kind}Left`, `${kind}Right`);
1923
1764
  const displayNeeded = kind === 'margin' && (target === 'all' || target === 'vertical');
1924
- const v = `${value}px`;
1925
1765
  const hasSameSpacing = (a, b) => {
1926
1766
  for (const p of props) {
1927
1767
  if (a.style[p] !== b.style[p])
@@ -1933,7 +1773,6 @@ class StackchRichtextEditor {
1933
1773
  }
1934
1774
  return true;
1935
1775
  };
1936
- // Downward: verschachtelte identische Spans in span zusammenführen
1937
1776
  if (span.children.length === 1) {
1938
1777
  const only = span.children[0];
1939
1778
  if (only && only.tagName === 'SPAN' && hasSameSpacing(span, only)) {
@@ -1943,7 +1782,6 @@ class StackchRichtextEditor {
1943
1782
  }
1944
1783
  }
1945
1784
  let current = span;
1946
- // Upward: mit Elternelement verschmelzen, wenn identischer Span
1947
1785
  const parent = current.parentElement;
1948
1786
  if (parent && parent.tagName === 'SPAN' && hasSameSpacing(parent, current)) {
1949
1787
  while (current.firstChild)
@@ -1953,7 +1791,6 @@ class StackchRichtextEditor {
1953
1791
  }
1954
1792
  if (!current)
1955
1793
  return null;
1956
- // Left merge: vorherige Geschwister-Spans mit gleicher Formatierung in current ziehen
1957
1794
  let prev = current.previousElementSibling;
1958
1795
  if (prev && prev.tagName === 'SPAN' && hasSameSpacing(prev, current)) {
1959
1796
  while (current.firstChild)
@@ -1961,7 +1798,6 @@ class StackchRichtextEditor {
1961
1798
  current.remove();
1962
1799
  current = prev;
1963
1800
  }
1964
- // Right merge: folgende Geschwister-Spans in current ziehen
1965
1801
  let next = current.nextElementSibling;
1966
1802
  while (next && next.tagName === 'SPAN' && hasSameSpacing(current, next)) {
1967
1803
  while (next.firstChild)
@@ -1977,6 +1813,7 @@ class StackchRichtextEditor {
1977
1813
  if (!range)
1978
1814
  return;
1979
1815
  const editor = this.editorRef.nativeElement;
1816
+ const PRE_STYLE = 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;';
1980
1817
  const replaceTag = (el, newTag) => {
1981
1818
  if (!el || el === editor)
1982
1819
  return null;
@@ -1988,24 +1825,23 @@ class StackchRichtextEditor {
1988
1825
  };
1989
1826
  const startBlock = this.getEditorChildAncestor(range.startContainer);
1990
1827
  const endBlock = this.getEditorChildAncestor(range.endContainer);
1991
- if (startBlock && startBlock !== editor && (startBlock === endBlock)) {
1828
+ if (startBlock && startBlock !== editor && startBlock === endBlock) {
1992
1829
  const currentTag = (startBlock.tagName || '').toLowerCase();
1993
1830
  if (currentTag === tag)
1994
1831
  return;
1995
1832
  const convertible = /^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag);
1996
1833
  if (convertible) {
1997
1834
  const neo = replaceTag(startBlock, tag);
1998
- if (neo && tag === 'pre') {
1999
- neo.setAttribute('style', 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;');
2000
- }
1835
+ if (neo && tag === 'pre')
1836
+ neo.setAttribute('style', PRE_STYLE);
2001
1837
  if (neo) {
2002
1838
  const sel = window.getSelection();
2003
1839
  if (sel) {
2004
1840
  sel.removeAllRanges();
2005
- const after = document.createRange();
2006
- after.selectNodeContents(neo);
2007
- after.collapse(false);
2008
- sel.addRange(after);
1841
+ const r = document.createRange();
1842
+ r.selectNodeContents(neo);
1843
+ r.collapse(false);
1844
+ sel.addRange(r);
2009
1845
  }
2010
1846
  }
2011
1847
  return;
@@ -2020,12 +1856,10 @@ class StackchRichtextEditor {
2020
1856
  let last = null;
2021
1857
  for (let i = from; i <= to; i++) {
2022
1858
  const el = children[i];
2023
- const currentTag = (el.tagName || '').toLowerCase();
2024
- if (/^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag)) {
1859
+ if (/^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(el.tagName)) {
2025
1860
  const neo = replaceTag(el, tag);
2026
- if (neo && tag === 'pre') {
2027
- neo.setAttribute('style', 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;');
2028
- }
1861
+ if (neo && tag === 'pre')
1862
+ neo.setAttribute('style', PRE_STYLE);
2029
1863
  if (neo)
2030
1864
  last = neo;
2031
1865
  }
@@ -2034,10 +1868,10 @@ class StackchRichtextEditor {
2034
1868
  const sel = window.getSelection();
2035
1869
  if (sel) {
2036
1870
  sel.removeAllRanges();
2037
- const after = document.createRange();
2038
- after.selectNodeContents(last);
2039
- after.collapse(false);
2040
- sel.addRange(after);
1871
+ const r = document.createRange();
1872
+ r.selectNodeContents(last);
1873
+ r.collapse(false);
1874
+ sel.addRange(r);
2041
1875
  }
2042
1876
  }
2043
1877
  return;
@@ -2045,40 +1879,26 @@ class StackchRichtextEditor {
2045
1879
  }
2046
1880
  if (!range.collapsed) {
2047
1881
  const el = document.createElement(tag);
2048
- if (tag === 'pre') {
2049
- el.setAttribute('style', 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;');
2050
- }
1882
+ if (tag === 'pre')
1883
+ el.setAttribute('style', PRE_STYLE);
2051
1884
  const frag = range.extractContents();
2052
1885
  el.appendChild(frag);
2053
1886
  range.insertNode(el);
2054
1887
  const sel = window.getSelection();
2055
1888
  if (sel) {
2056
1889
  sel.removeAllRanges();
2057
- const after = document.createRange();
2058
- after.selectNodeContents(el);
2059
- after.collapse(false);
2060
- sel.addRange(after);
1890
+ const r = document.createRange();
1891
+ r.selectNodeContents(el);
1892
+ r.collapse(false);
1893
+ sel.addRange(r);
2061
1894
  }
2062
1895
  }
2063
1896
  }
2064
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditor, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2065
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditor, isStandalone: true, selector: "stackch-richtext-editor", inputs: { placeholder: "placeholder", showToolbar: "showToolbar", fonts: "fonts", fontSizes: "fontSizes", height: "height", minHeight: "minHeight", maxHeight: "maxHeight", disabled: "disabled", config: "config" }, outputs: { valueChange: "valueChange" }, host: { listeners: { "document:selectionchange": "onSelectionChange()", "document:click": "closeMenus()", "keydown": "onKeydown($event)" } }, providers: [
2066
- {
2067
- provide: NG_VALUE_ACCESSOR,
2068
- useExisting: forwardRef(() => StackchRichtextEditor),
2069
- multi: true,
2070
- },
2071
- ], viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: StackchRichtextEditorToolbar, selector: "stackch-richtext-editor-toolbar", inputs: ["cfg", "i18n", "disabled", "fonts", "fontSizes", "isBoldActive", "isItalicActive", "isUnderlineActive", "canUndo", "canRedo", "uiState"], outputs: ["undo", "redo", "toggleFontPanel", "toggleHeadingMenu", "toggleSpacingMenu", "toggleAlignMenu", "toggleColorMenu", "toggleListMenu", "pickFont", "pickFontSize", "pickAlign", "pickList", "pickHeading", "pickSpacing", "applyColor", "applyHighlight", "toggleBold", "toggleItalic", "toggleUnderline", "insertLink", "removeFormat", "saveSelectionRequest"] }] });
1897
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorBase, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive });
1898
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.4", type: StackchRichtextEditorBase, isStandalone: true, inputs: { placeholder: "placeholder", showToolbar: "showToolbar", fonts: "fonts", fontSizes: "fontSizes", height: "height", minHeight: "minHeight", maxHeight: "maxHeight", disabled: "disabled", config: "config" }, outputs: { valueChange: "valueChange", metricsChange: "metricsChange" }, host: { listeners: { "document:selectionchange": "onSelectionChange()", "document:click": "closeMenus()", "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true, static: true }], ngImport: i0 });
2072
1899
  }
2073
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditor, decorators: [{
2074
- type: Component,
2075
- args: [{ selector: 'stackch-richtext-editor', standalone: true, imports: [CommonModule, StackchRichtextEditorToolbar], providers: [
2076
- {
2077
- provide: NG_VALUE_ACCESSOR,
2078
- useExisting: forwardRef(() => StackchRichtextEditor),
2079
- multi: true,
2080
- },
2081
- ], template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"] }]
1900
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorBase, decorators: [{
1901
+ type: Directive
2082
1902
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { placeholder: [{
2083
1903
  type: Input
2084
1904
  }], showToolbar: [{
@@ -2097,6 +1917,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
2097
1917
  type: Input
2098
1918
  }], valueChange: [{
2099
1919
  type: Output
1920
+ }], metricsChange: [{
1921
+ type: Output
2100
1922
  }], editorRef: [{
2101
1923
  type: ViewChild,
2102
1924
  args: ['editor', { static: true }]
@@ -2113,6 +1935,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
2113
1935
  args: ['keydown', ['$event']]
2114
1936
  }] } });
2115
1937
 
1938
+ class StackchRichtextEditor extends StackchRichtextEditorBase {
1939
+ logPrefix = '[RTE]';
1940
+ constructor(cdr) {
1941
+ super(cdr);
1942
+ }
1943
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditor, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
1944
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditor, isStandalone: true, selector: "stackch-richtext-editor", providers: [
1945
+ {
1946
+ provide: NG_VALUE_ACCESSOR,
1947
+ useExisting: forwardRef(() => StackchRichtextEditor),
1948
+ multi: true,
1949
+ },
1950
+ ], usesInheritance: true, ngImport: i0, template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: StackchRichtextEditorToolbar, selector: "stackch-richtext-editor-toolbar", inputs: ["cfg", "i18n", "disabled", "fonts", "fontSizes", "isBoldActive", "isItalicActive", "isUnderlineActive", "canUndo", "canRedo", "uiState"], outputs: ["undo", "redo", "toggleFontPanel", "toggleHeadingMenu", "toggleSpacingMenu", "toggleAlignMenu", "toggleColorMenu", "toggleListMenu", "pickFont", "pickFontSize", "pickAlign", "pickList", "pickHeading", "pickSpacing", "applyColor", "applyHighlight", "toggleBold", "toggleItalic", "toggleUnderline", "insertLink", "removeFormat", "saveSelectionRequest"] }] });
1951
+ }
1952
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditor, decorators: [{
1953
+ type: Component,
1954
+ args: [{ selector: 'stackch-richtext-editor', standalone: true, imports: [CommonModule, StackchRichtextEditorToolbar], providers: [
1955
+ {
1956
+ provide: NG_VALUE_ACCESSOR,
1957
+ useExisting: forwardRef(() => StackchRichtextEditor),
1958
+ multi: true,
1959
+ },
1960
+ ], template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-toolbar\r\n [cfg]=\"cfg\"\r\n [i18n]=\"i18n\"\r\n [disabled]=\"disabled\"\r\n [fonts]=\"fonts\"\r\n [fontSizes]=\"fontSizes\"\r\n [isBoldActive]=\"isBoldActive\"\r\n [isItalicActive]=\"isItalicActive\"\r\n [isUnderlineActive]=\"isUnderlineActive\"\r\n [canUndo]=\"canUndo\"\r\n [canRedo]=\"canRedo\"\r\n [uiState]=\"{ showFontPanel: showFontPanel, showHeadingMenu: showHeadingMenu, showSpacingMenu: showSpacingMenu, showAlignMenu: showAlignMenu, showColorMenu: showColorMenu, showListMenu: showListMenu }\"\r\n\r\n (undo)=\"undo()\"\r\n (redo)=\"redo()\"\r\n (toggleFontPanel)=\"toggleFontPanel($event)\"\r\n (toggleHeadingMenu)=\"toggleHeadingMenu($event)\"\r\n (toggleSpacingMenu)=\"toggleSpacingMenu($event)\"\r\n (toggleAlignMenu)=\"toggleAlignMenu($event)\"\r\n (toggleColorMenu)=\"toggleColorMenu($event)\"\r\n (toggleListMenu)=\"toggleListMenu($event)\"\r\n (pickFont)=\"onPickFont($any($event))\"\r\n (pickFontSize)=\"onPickFontSize($any($event))\"\r\n (pickAlign)=\"onPickAlign($any($event))\"\r\n (pickList)=\"onPickList($any($event))\"\r\n (pickHeading)=\"onPickHeading($any($event))\"\r\n (pickSpacing)=\"onPickSpacing($any($event).kind, $any($event).target, $any($event).value)\"\r\n (applyColor)=\"applyColor($any($event))\"\r\n (applyHighlight)=\"applyHighlight($any($event))\"\r\n (toggleBold)=\"toggleBold()\"\r\n (toggleItalic)=\"toggleItalic()\"\r\n (toggleUnderline)=\"toggleUnderline()\"\r\n (insertLink)=\"insertLink()\"\r\n (removeFormat)=\"removeFormat()\"\r\n (saveSelectionRequest)=\"saveSelection()\"\r\n />\r\n }\r\n\r\n <div #editor\r\n class=\"stackch_rte__editor\"\r\n [attr.contenteditable]=\"!disabled\"\r\n [attr.data-placeholder]=\"placeholder || i18n.placeholder\"\r\n [style.minHeight.px]=\"minHeight\"\r\n [style.maxHeight.px]=\"maxHeight\"\r\n [style.height.px]=\"height\"\r\n (mouseup)=\"saveSelection()\"\r\n (keyup)=\"onKeyup($event)\"\r\n (input)=\"onInput()\"\r\n (blur)=\"onTouched()\"\r\n (paste)=\"onPaste($event)\"></div>\r\n</div>\r\n", styles: [".stackch_rte{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}.stackch_rte--disabled{opacity:.7;pointer-events:none}.stackch_rte__toolbar{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid #d0d7de;border-bottom:none;background:#f6f8fa;padding:.25rem;border-radius:6px 6px 0 0}.stackch_rte__btn{appearance:none;border:1px solid #d0d7de;background:#fff;padding:.25rem .5rem;border-radius:4px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease;outline:none}.stackch_rte__btn:hover{background:#f3f4f6}.stackch_rte__btn:focus{outline:none;box-shadow:none}.stackch_rte__btn:focus-visible{outline:2px solid rgba(9,105,218,.2)}.stackch_rte__btn.is-active{background:#e7f3ff;border-color:#0969da;color:#084298}.stackch_rte__select{border:1px solid #d0d7de;border-radius:4px;padding:.2rem .4rem;background:#fff}.stackch_rte__color{display:inline-flex;align-items:center;gap:.25rem;border:1px solid #d0d7de;padding:0 .4rem;border-radius:4px;background:#fff}.stackch_rte__color>input{inline-size:1.75rem;block-size:1.5rem;padding:0;border:none;background:none}.stackch_rte__sep{width:1px;height:1.25rem;background:#d0d7de;margin:0 .125rem}.stackch_rte__editor{border:1px solid #d0d7de;border-radius:0 0 6px 6px;padding:.5rem;background:#fff;overflow:auto}.stackch_rte__editor:empty:before{content:attr(data-placeholder);color:#97a1ad;pointer-events:none}.stackch_rte__editor:focus{outline:none;box-shadow:inset 0 0 0 1px #0969da;border-color:#0969da}.stackch_rte__dropdown{position:relative;display:inline-block}.stackch_rte__menu{position:absolute;top:100%;left:0;z-index:2;background:#fff;border:1px solid #d0d7de;border-radius:6px;box-shadow:0 4px 12px #00000014;padding:.25rem;min-width:220px;max-height:220px;overflow:auto}.stackch_rte__menu-item{display:block;width:100%;text-align:left;background:#fff;border:none;padding:.375rem .5rem;border-radius:4px;cursor:pointer}.stackch_rte__menu-item:hover{background:#f3f4f6}.stackch_rte__menu--row{display:flex;flex-direction:row;gap:.25rem;align-items:center;min-width:auto;max-height:none;flex-wrap:nowrap}.stackch_rte__menu--row .stackch_rte__menu-item{display:inline-flex;width:auto;text-align:center;justify-content:center;align-items:center;padding:.3rem .45rem;white-space:nowrap;flex:0 0 auto;word-break:keep-all;overflow-wrap:normal}.stackch_rte__menu--row .stackch_rte__menu-item[style*=\"width:24px\"]{padding:0;width:24px;height:24px}.stackch_rte__menu--grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1rem;min-width:420px}.stackch_rte__menu-section{display:flex;flex-direction:column;gap:.25rem}.stackch_rte__menu-title{font-size:12px;color:#57606a;padding:.25rem;text-transform:uppercase;letter-spacing:.04em}\n"] }]
1961
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }] });
1962
+
2116
1963
  /*
2117
1964
  * Public API Surface of richtext-editor
2118
1965
  */
@@ -2121,5 +1968,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
2121
1968
  * Generated bundle index. Do not edit.
2122
1969
  */
2123
1970
 
2124
- export { STACKCH_RTE_I18N_DE, STACKCH_RTE_I18N_FR, STACKCH_RTE_I18N_IT, StackchRichtextEditor, StackchRichtextEditorConfig, StackchRichtextEditorI18n, StackchRichtextEditorToolbar };
1971
+ export { STACKCH_RTE_I18N_DE, STACKCH_RTE_I18N_FR, STACKCH_RTE_I18N_IT, StackchRichtextEditor, StackchRichtextEditorBase, StackchRichtextEditorConfig, StackchRichtextEditorI18n, StackchRichtextEditorToolbar };
2125
1972
  //# sourceMappingURL=stackch-angular-richtext-editor.mjs.map