@stackch/angular-material-richtext-editor 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, forwardRef } from '@angular/core';
3
3
  import { CommonModule } from '@angular/common';
4
4
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
5
  import * as i1 from '@angular/material/button';
@@ -12,6 +12,8 @@ import * as i4 from '@angular/material/tooltip';
12
12
  import { MatTooltipModule } from '@angular/material/tooltip';
13
13
  import { MatFormFieldModule } from '@angular/material/form-field';
14
14
  import { MatSelectModule } from '@angular/material/select';
15
+ import { StackchRichtextEditorBase } from '@stackch/angular-richtext-editor';
16
+ export { STACKCH_RTE_I18N_DE, STACKCH_RTE_I18N_FR, STACKCH_RTE_I18N_IT, StackchRichtextEditorBase, StackchRichtextEditorConfig, StackchRichtextEditorI18n } from '@stackch/angular-richtext-editor';
15
17
 
16
18
  class StackchRichtextEditorMaterialToolbar {
17
19
  // State/config from parent editor
@@ -144,2033 +146,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
144
146
  type: Output
145
147
  }] } });
146
148
 
147
- class StackchRichtextEditorConfig {
148
- // Sichtbarkeit einzelner Toolbar-Elemente (Default: an)
149
- showUndoRedo = true;
150
- showFontPanel = true;
151
- showHeading = true;
152
- showSpacing = true;
153
- showColor = true;
154
- showBold = true;
155
- showItalic = true;
156
- showUnderline = true;
157
- showLists = true;
158
- showAlign = true;
159
- showLink = true;
160
- showRemoveFormat = true;
161
- // i18n overrides (partial), default is English
162
- i18n;
163
- }
164
- class StackchRichtextEditorI18n {
165
- // Generic
166
- placeholder = 'Write…';
167
- // Toolbar titles
168
- undoTitle = 'Undo (Ctrl+Z)';
169
- redoTitle = 'Redo (Ctrl+Y)';
170
- fontPanelTitle = 'Font & Size';
171
- fontSectionTitle = 'Font';
172
- sizeSectionTitle = 'Size';
173
- headingTitle = 'Heading';
174
- spacingTitle = 'Spacing (Margin/Padding)';
175
- marginTitle = 'Margin';
176
- paddingTitle = 'Padding';
177
- colorsTitle = 'Colors';
178
- colorMenuTitle = 'Pick a color';
179
- textColorTitle = 'Text color';
180
- highlightTitle = 'Highlight';
181
- boldTitle = 'Bold';
182
- italicTitle = 'Italic';
183
- underlineTitle = 'Underline';
184
- listsTitle = 'Lists';
185
- bulletListTitle = 'Bullet list';
186
- numberedListTitle = 'Numbered list';
187
- alignTitle = 'Alignment';
188
- alignLeftTitle = 'Left';
189
- alignCenterTitle = 'Center';
190
- alignRightTitle = 'Right';
191
- alignJustifyTitle = 'Justify';
192
- linkTitle = 'Link';
193
- removeFormatTitle = 'Remove format';
194
- // Heading menu labels
195
- paragraphLabel = 'Paragraph (P)';
196
- codeLabel = 'Code (pre)';
197
- h1Label = 'H1';
198
- h2Label = 'H2';
199
- h3Label = 'H3';
200
- h4Label = 'H4';
201
- h5Label = 'H5';
202
- h6Label = 'H6';
203
- }
204
- // Predefined i18n bundles
205
- // Consumers can import these and pass via config.i18n to localize the toolbar/labels.
206
- const STACKCH_RTE_I18N_DE = {
207
- // Generic
208
- placeholder: 'Schreiben…',
209
- // Toolbar titles
210
- undoTitle: 'Rückgängig (Strg+Z)',
211
- redoTitle: 'Wiederholen (Strg+Y)',
212
- fontPanelTitle: 'Schrift & Größe',
213
- fontSectionTitle: 'Schrift',
214
- sizeSectionTitle: 'Größe',
215
- headingTitle: 'Überschrift',
216
- spacingTitle: 'Abstand (Außen/Innen)',
217
- marginTitle: 'Außenabstand',
218
- paddingTitle: 'Innenabstand',
219
- colorsTitle: 'Farben',
220
- colorMenuTitle: 'Farbe wählen',
221
- textColorTitle: 'Textfarbe',
222
- highlightTitle: 'Hervorheben',
223
- boldTitle: 'Fett',
224
- italicTitle: 'Kursiv',
225
- underlineTitle: 'Unterstreichen',
226
- listsTitle: 'Listen',
227
- bulletListTitle: 'Aufzählung',
228
- numberedListTitle: 'Nummeriert',
229
- alignTitle: 'Ausrichtung',
230
- alignLeftTitle: 'Links',
231
- alignCenterTitle: 'Zentriert',
232
- alignRightTitle: 'Rechts',
233
- alignJustifyTitle: 'Blocksatz',
234
- linkTitle: 'Link',
235
- removeFormatTitle: 'Formatierung entfernen',
236
- // Heading menu labels
237
- paragraphLabel: 'Absatz (P)',
238
- codeLabel: 'Code (pre)',
239
- h1Label: 'H1',
240
- h2Label: 'H2',
241
- h3Label: 'H3',
242
- h4Label: 'H4',
243
- h5Label: 'H5',
244
- h6Label: 'H6',
245
- };
246
- const STACKCH_RTE_I18N_FR = {
247
- // Generic
248
- placeholder: 'Écrire…',
249
- // Toolbar titles
250
- undoTitle: 'Annuler (Ctrl+Z)',
251
- redoTitle: 'Rétablir (Ctrl+Y)',
252
- fontPanelTitle: 'Police et taille',
253
- fontSectionTitle: 'Police',
254
- sizeSectionTitle: 'Taille',
255
- headingTitle: 'Titre',
256
- spacingTitle: 'Espacement (Marge/Remplissage)',
257
- marginTitle: 'Marge',
258
- paddingTitle: 'Remplissage',
259
- colorsTitle: 'Couleurs',
260
- colorMenuTitle: 'Choisir une couleur',
261
- textColorTitle: 'Couleur du texte',
262
- highlightTitle: 'Surligner',
263
- boldTitle: 'Gras',
264
- italicTitle: 'Italique',
265
- underlineTitle: 'Souligné',
266
- listsTitle: 'Listes',
267
- bulletListTitle: 'Liste à puces',
268
- numberedListTitle: 'Liste numérotée',
269
- alignTitle: 'Alignement',
270
- alignLeftTitle: 'Gauche',
271
- alignCenterTitle: 'Centré',
272
- alignRightTitle: 'Droite',
273
- alignJustifyTitle: 'Justifié',
274
- linkTitle: 'Lien',
275
- removeFormatTitle: 'Effacer la mise en forme',
276
- // Heading menu labels
277
- paragraphLabel: 'Paragraphe (P)',
278
- codeLabel: 'Code (pre)',
279
- h1Label: 'H1',
280
- h2Label: 'H2',
281
- h3Label: 'H3',
282
- h4Label: 'H4',
283
- h5Label: 'H5',
284
- h6Label: 'H6',
285
- };
286
- const STACKCH_RTE_I18N_IT = {
287
- // Generic
288
- placeholder: 'Scrivi…',
289
- // Toolbar titles
290
- undoTitle: 'Annulla (Ctrl+Z)',
291
- redoTitle: 'Ripristina (Ctrl+Y)',
292
- fontPanelTitle: 'Carattere e dimensione',
293
- fontSectionTitle: 'Carattere',
294
- sizeSectionTitle: 'Dimensione',
295
- headingTitle: 'Titolo',
296
- spacingTitle: 'Spaziatura (Margine/Riempimento)',
297
- marginTitle: 'Margine',
298
- paddingTitle: 'Riempimento',
299
- colorsTitle: 'Colori',
300
- colorMenuTitle: 'Scegli un colore',
301
- textColorTitle: 'Colore del testo',
302
- highlightTitle: 'Evidenzia',
303
- boldTitle: 'Grassetto',
304
- italicTitle: 'Corsivo',
305
- underlineTitle: 'Sottolineato',
306
- listsTitle: 'Elenchi',
307
- bulletListTitle: 'Elenco puntato',
308
- numberedListTitle: 'Elenco numerato',
309
- alignTitle: 'Allineamento',
310
- alignLeftTitle: 'Sinistra',
311
- alignCenterTitle: 'Centrato',
312
- alignRightTitle: 'Destra',
313
- alignJustifyTitle: 'Giustificato',
314
- linkTitle: 'Collegamento',
315
- removeFormatTitle: 'Rimuovi formattazione',
316
- // Heading menu labels
317
- paragraphLabel: 'Paragrafo (P)',
318
- codeLabel: 'Codice (pre)',
319
- h1Label: 'H1',
320
- h2Label: 'H2',
321
- h3Label: 'H3',
322
- h4Label: 'H4',
323
- h5Label: 'H5',
324
- h6Label: 'H6',
325
- };
326
- class StackchRichtextEditorMaterial {
327
- cdr;
328
- placeholder = '';
329
- showToolbar = true;
330
- fonts = [
331
- 'Arial, Helvetica, sans-serif',
332
- 'Georgia, serif',
333
- 'Times New Roman, Times, serif',
334
- 'Trebuchet MS, sans-serif',
335
- 'Verdana, Geneva, sans-serif',
336
- 'Courier New, Courier, monospace',
337
- 'Monaco, monospace'
338
- ];
339
- fontSizes = [12, 14, 16, 18, 20, 24, 28, 32];
340
- height;
341
- minHeight = 160;
342
- maxHeight;
343
- set disabled(value) { this._disabled = value; }
344
- get disabled() { return this._disabled; }
345
- _disabled = false;
346
- valueChange = new EventEmitter();
347
- // Structured metrics event (future-proof)
348
- metricsChange = new EventEmitter();
349
- editorRef;
149
+ class StackchRichtextEditorMaterial extends StackchRichtextEditorBase {
150
+ logPrefix = '[RTE-material]';
350
151
  constructor(cdr) {
351
- this.cdr = cdr;
352
- }
353
- // Toolbar-Konfiguration (default: alles an). Wir halten die übergebene Referenz
354
- // und mergen Defaults im Getter, damit auch Eigenschaftsänderungen via ngModel sofort wirken.
355
- config;
356
- get cfg() {
357
- return Object.assign(new StackchRichtextEditorConfig(), this.config || {});
358
- }
359
- get i18n() {
360
- return Object.assign(new StackchRichtextEditorI18n(), this.cfg.i18n || {});
361
- }
362
- onChange = () => { };
363
- onTouched = () => { };
364
- savedRange = null;
365
- // History (Undo/Redo)
366
- history = [];
367
- historyIndex = -1;
368
- isRestoringHistory = false;
369
- snapshotTimer = null;
370
- // UI state for compact dropdowns / panel
371
- showFontMenu = false;
372
- showSizeMenu = false;
373
- showFontPanel = false;
374
- showHeadingMenu = false;
375
- showSpacingMenu = false;
376
- showAlignMenu = false;
377
- showColorMenu = false;
378
- showListMenu = false;
379
- // Inline state flags for active buttons
380
- isBoldActive = false;
381
- isItalicActive = false;
382
- isUnderlineActive = false;
383
- // Selection helpers
384
- saveSelection() {
385
- const sel = window.getSelection();
386
- if (!sel || sel.rangeCount === 0)
387
- return;
388
- const range = sel.getRangeAt(0);
389
- if (!this.isRangeInEditor(range))
390
- return;
391
- this.savedRange = range.cloneRange();
392
- this.updateInlineStates();
393
- }
394
- // Prevent toolbar buttons from stealing focus from the editor while preserving selection
395
- onToolbarMouseDown(evt) {
396
- // Keep focus on the editor to avoid persistent button focus outlines
397
- evt.preventDefault();
398
- evt.stopPropagation();
399
- this.saveSelection();
400
- }
401
- // Beim Öffnen des Color-Pickers: Selektion sichern
402
- onColorPointerDown(_evt) {
403
- this.saveSelection();
404
- }
405
- restoreSelection() {
406
- const sel = window.getSelection();
407
- if (!sel || !this.savedRange)
408
- return;
409
- if (!this.isRangeInEditor(this.savedRange))
410
- return;
411
- sel.removeAllRanges();
412
- sel.addRange(this.savedRange);
413
- }
414
- isRangeInEditor(range) {
415
- const editor = this.editorRef?.nativeElement;
416
- if (!editor)
417
- return false;
418
- const container = range.commonAncestorContainer;
419
- const node = container.nodeType === Node.ELEMENT_NODE ? container : container.parentElement;
420
- return !!node && editor.contains(node);
421
- }
422
- // Update active-state on selection changes inside the editor
423
- onSelectionChange() {
424
- const sel = window.getSelection();
425
- if (!sel || sel.rangeCount === 0)
426
- return;
427
- const range = sel.getRangeAt(0);
428
- if (!this.isRangeInEditor(range))
429
- return;
430
- this.updateInlineStates();
431
- }
432
- // Close dropdowns when clicking anywhere in the document
433
- closeMenus() {
434
- this.showFontMenu = false;
435
- this.showSizeMenu = false;
436
- this.showFontPanel = false;
437
- this.showHeadingMenu = false;
438
- this.showSpacingMenu = false;
439
- this.showAlignMenu = false;
440
- this.showColorMenu = false;
441
- this.showListMenu = false;
442
- }
443
- // Keyboard: Undo/Redo
444
- onKeydown(evt) {
445
- const isMac = navigator.platform.toLowerCase().includes('mac');
446
- const mod = isMac ? evt.metaKey : evt.ctrlKey;
447
- if (mod && !evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')) {
448
- evt.preventDefault();
449
- this.undo();
450
- }
451
- else if (mod && (evt.key === 'y' || evt.key === 'Y' || (evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')))) {
452
- evt.preventDefault();
453
- this.redo();
454
- }
455
- }
456
- toggleFontMenu(evt) {
457
- this.saveSelection();
458
- this.showFontMenu = !this.showFontMenu;
459
- if (this.showFontMenu)
460
- this.showSizeMenu = false;
461
- evt.stopPropagation();
462
- }
463
- toggleSizeMenu(evt) {
464
- this.saveSelection();
465
- this.showSizeMenu = !this.showSizeMenu;
466
- if (this.showSizeMenu)
467
- this.showFontMenu = false;
468
- evt.stopPropagation();
469
- }
470
- toggleFontPanel(evt) {
471
- this.saveSelection();
472
- this.showFontPanel = !this.showFontPanel;
473
- // close others if open
474
- if (this.showFontPanel) {
475
- this.showFontMenu = false;
476
- this.showSizeMenu = false;
477
- this.showHeadingMenu = false;
478
- this.showSpacingMenu = false;
479
- this.showAlignMenu = false;
480
- this.showColorMenu = false;
481
- }
482
- evt.stopPropagation();
483
- }
484
- toggleHeadingMenu(evt) {
485
- this.saveSelection();
486
- this.showHeadingMenu = !this.showHeadingMenu;
487
- if (this.showHeadingMenu) {
488
- this.showFontMenu = false;
489
- this.showSizeMenu = false;
490
- this.showFontPanel = false;
491
- this.showSpacingMenu = false;
492
- this.showAlignMenu = false;
493
- this.showColorMenu = false;
494
- }
495
- evt.stopPropagation();
496
- }
497
- toggleSpacingMenu(evt) {
498
- this.saveSelection();
499
- this.showSpacingMenu = !this.showSpacingMenu;
500
- if (this.showSpacingMenu) {
501
- this.showFontPanel = false;
502
- this.showHeadingMenu = false;
503
- this.showAlignMenu = false;
504
- this.showColorMenu = false;
505
- }
506
- evt.stopPropagation();
507
- }
508
- toggleAlignMenu(evt) {
509
- this.saveSelection();
510
- this.showAlignMenu = !this.showAlignMenu;
511
- if (this.showAlignMenu) {
512
- this.showFontMenu = false;
513
- this.showSizeMenu = false;
514
- this.showFontPanel = false;
515
- this.showHeadingMenu = false;
516
- this.showSpacingMenu = false;
517
- this.showColorMenu = false;
518
- this.showListMenu = false;
519
- }
520
- evt.stopPropagation();
521
- }
522
- toggleColorMenu(evt) {
523
- this.saveSelection();
524
- this.showColorMenu = !this.showColorMenu;
525
- if (this.showColorMenu) {
526
- this.showFontMenu = false;
527
- this.showSizeMenu = false;
528
- this.showFontPanel = false;
529
- this.showHeadingMenu = false;
530
- this.showSpacingMenu = false;
531
- this.showAlignMenu = false;
532
- this.showListMenu = false;
533
- }
534
- evt.stopPropagation();
535
- }
536
- toggleListMenu(evt) {
537
- this.saveSelection();
538
- this.showListMenu = !this.showListMenu;
539
- if (this.showListMenu) {
540
- this.showFontMenu = false;
541
- this.showSizeMenu = false;
542
- this.showFontPanel = false;
543
- this.showHeadingMenu = false;
544
- this.showSpacingMenu = false;
545
- this.showAlignMenu = false;
546
- this.showColorMenu = false;
547
- }
548
- evt.stopPropagation();
549
- }
550
- onPickAlign(where) {
551
- this.focusEditor();
552
- this.restoreSelection();
553
- this.alignBlocks(where);
554
- this.showAlignMenu = false;
555
- this.emitValue();
556
- this.takeSnapshot('align');
557
- }
558
- onPickFont(font) {
559
- if (!font)
560
- return;
561
- this.focusEditor();
562
- this.restoreSelection();
563
- this.applyInlineStyle('fontFamily', font);
564
- this.showFontMenu = false;
565
- }
566
- onPickFontSize(size) {
567
- if (!Number.isFinite(size))
568
- return;
569
- this.focusEditor();
570
- this.restoreSelection();
571
- this.applyInlineStyle('fontSize', `${size}px`);
572
- this.showSizeMenu = false;
573
- }
574
- onPickList(kind) {
575
- this.focusEditor();
576
- this.restoreSelection();
577
- this.toggleList(kind);
578
- this.showListMenu = false;
579
- this.emitValue();
580
- this.takeSnapshot('list');
581
- }
582
- onPickHeading(tag) {
583
- this.focusEditor();
584
- this.restoreSelection();
585
- this.setHeading(tag);
586
- this.showHeadingMenu = false;
587
- this.emitValue();
588
- this.takeSnapshot('heading');
589
- }
590
- onPickSpacing(kind, target, value) {
591
- this.focusEditor();
592
- this.restoreSelection();
593
- // Bevorzugt: direkt auf die Selektion anwenden (inline Wrapper)
594
- if (!this.applySpacingToSelection(kind, target, value)) {
595
- // Fallback: Block-Spacing, wenn Auswahl blockübergreifend ist
596
- this.setBlockSpacing(kind, target, value);
597
- }
598
- this.showSpacingMenu = false;
599
- this.emitValue();
600
- this.takeSnapshot('spacing');
601
- }
602
- // ControlValueAccessor
603
- writeValue(value) {
604
- const el = this.editorRef?.nativeElement;
605
- if (!el)
606
- return;
607
- el.innerHTML = value || '';
608
- // Initial snapshot nur einmal anlegen
609
- if (this.history.length === 0) {
610
- this.takeSnapshot('init');
611
- }
612
- // Emit initial metrics when value is written programmatically
613
- this.emitMetrics();
614
- }
615
- registerOnChange(fn) { this.onChange = fn; }
616
- registerOnTouched(fn) { this.onTouched = fn; }
617
- setDisabledState(isDisabled) { this.disabled = isDisabled; }
618
- // Toolbar actions using document.execCommand (deprecated but broadly supported)
619
- cmd(command, value) {
620
- // Restore last selection from editor before applying an action triggered by toolbar
621
- this.focusEditor();
622
- this.restoreSelection();
623
- // Bevorzugt: eigene Range-basierte Implementierungen
624
- switch (command) {
625
- case 'bold':
626
- this.applyInlineStyleSmart('fontWeight', 'bold');
627
- return;
628
- case 'italic':
629
- this.applyInlineStyleSmart('fontStyle', 'italic');
630
- return;
631
- case 'underline':
632
- this.applyInlineStyleSmart('textDecoration', 'underline');
633
- return;
634
- case 'foreColor':
635
- this.applyInlineStyle('color', value || '');
636
- return;
637
- case 'hiliteColor':
638
- case 'backColor':
639
- this.applyInlineStyle('backgroundColor', value || '');
640
- return;
641
- case 'createLink':
642
- if (value) {
643
- this.wrapSelectionWith('a', { href: value });
644
- this.emitValue();
645
- this.takeSnapshot('link');
646
- }
647
- return;
648
- case 'insertText': {
649
- const text = value ?? '';
650
- this.insertTextAtSelection(text);
651
- this.emitValue();
652
- this.takeSnapshot('insertText');
653
- return;
654
- }
655
- case 'insertUnorderedList':
656
- this.toggleList('ul');
657
- this.emitValue();
658
- this.takeSnapshot('list');
659
- return;
660
- case 'insertOrderedList':
661
- this.toggleList('ol');
662
- this.emitValue();
663
- this.takeSnapshot('list');
664
- return;
665
- case 'justifyLeft':
666
- this.alignBlocks('left');
667
- this.emitValue();
668
- this.takeSnapshot('align');
669
- return;
670
- case 'justifyCenter':
671
- this.alignBlocks('center');
672
- this.emitValue();
673
- this.takeSnapshot('align');
674
- return;
675
- case 'justifyRight':
676
- this.alignBlocks('right');
677
- this.emitValue();
678
- this.takeSnapshot('align');
679
- return;
680
- case 'justifyFull':
681
- this.alignBlocks('justify');
682
- this.emitValue();
683
- this.takeSnapshot('align');
684
- return;
685
- }
686
- // Fallback (deprecated): nur für Format entfernen / Links lösen als Übergang
687
- try {
688
- // Hinweis im Dev-Mode ausgeben
689
- if (!('___rteWarned' in this)) {
690
- console.warn('[richtext-editor] document.execCommand ist deprecated; Fallback wird nur für removeFormat/unlink verwendet.');
691
- this.___rteWarned = true;
692
- }
693
- if (command === 'removeFormat' || command === 'unlink') {
694
- document.execCommand(command, false, value);
695
- }
696
- }
697
- catch {
698
- // Ignorieren, wenn nicht unterstützt
699
- }
700
- this.emitValue();
701
- this.takeSnapshot('fallback');
702
- }
703
- applyFont(font) {
704
- if (!font)
705
- return;
706
- this.applyInlineStyle('fontFamily', font);
707
- }
708
- applyFontSize(sizePx) {
709
- if (!sizePx)
710
- return;
711
- const size = Number(sizePx);
712
- if (!Number.isFinite(size))
713
- return;
714
- this.applyInlineStyle('fontSize', `${size}px`);
715
- }
716
- applyColor(color) {
717
- this.focusEditor();
718
- this.restoreSelection();
719
- const sel = window.getSelection();
720
- if (!sel || sel.rangeCount === 0)
721
- return;
722
- const r = sel.getRangeAt(0);
723
- if (r.collapsed) {
724
- // Firefox/Edge: wenn keine explizite Auswahl, erweitere auf nächstes Wort
725
- this.expandRangeToWord(r);
726
- }
727
- this.applySelectionStyles({ color });
728
- this.saveSelection();
729
- }
730
- applyHighlight(color) {
731
- this.focusEditor();
732
- this.restoreSelection();
733
- const sel = window.getSelection();
734
- if (!sel || sel.rangeCount === 0)
735
- return;
736
- const r = sel.getRangeAt(0);
737
- if (r.collapsed) {
738
- this.expandRangeToWord(r);
739
- }
740
- this.applySelectionStyles({ backgroundColor: color });
741
- this.saveSelection();
742
- }
743
- expandRangeToWord(range) {
744
- try {
745
- const editor = this.editorRef.nativeElement;
746
- let node = range.startContainer;
747
- if (node.nodeType !== Node.TEXT_NODE) {
748
- // Versuche, einen Textknoten in der Nähe zu finden
749
- const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
750
- let found = null;
751
- while (walker.nextNode()) {
752
- const n = walker.currentNode;
753
- if (n.parentElement && editor.contains(n.parentElement)) {
754
- found = n;
755
- break;
756
- }
757
- }
758
- if (found)
759
- node = found;
760
- }
761
- const text = node.nodeType === Node.TEXT_NODE ? node.data : '';
762
- let start = 0, end = text.length;
763
- // einfache Wortgrenzen-Heuristik
764
- const pos = range.startOffset;
765
- for (let i = pos; i > 0; i--) {
766
- if (/\s/.test(text[i - 1])) {
767
- start = i;
768
- break;
769
- }
770
- }
771
- for (let i = pos; i < text.length; i++) {
772
- if (/\s/.test(text[i])) {
773
- end = i;
774
- break;
775
- }
776
- }
777
- range.setStart(node, start);
778
- range.setEnd(node, end);
779
- const sel = window.getSelection();
780
- if (sel) {
781
- sel.removeAllRanges();
782
- sel.addRange(range);
783
- }
784
- }
785
- catch { /* noop */ }
786
- }
787
- insertLink() {
788
- const url = prompt('Link-URL eingeben:', 'https://');
789
- if (url) {
790
- this.cmd('createLink', url);
791
- }
792
- }
793
- removeFormat() {
794
- this.cmd('removeFormat');
795
- this.cmd('unlink');
796
- }
797
- onInput() {
798
- this.emitValue();
799
- this.scheduleSnapshot();
800
- }
801
- onKeyup(_evt) {
802
- this.saveSelection();
803
- this.emitValue();
804
- this.updateInlineStates();
805
- }
806
- onPaste(evt) {
807
- // Optional: Clean paste to plain text while keeping basic formatting minimal.
808
- if (!evt.clipboardData)
809
- return;
810
- evt.preventDefault();
811
- const text = evt.clipboardData.getData('text/plain');
812
- this.insertTextAtSelection(text);
813
- this.emitValue();
814
- this.takeSnapshot('paste');
815
- }
816
- emitValue() {
817
- // Clean up empty style attributes and redundant spans before emitting
818
- this.cleanupEmptyStylesAndSpans();
819
- const val = this.editorRef.nativeElement.innerHTML;
820
- this.onChange(val);
821
- this.valueChange.emit(val);
822
- this.emitMetrics();
823
- this.updateInlineStates();
824
- }
825
- emitMetrics() {
826
- const editor = this.editorRef?.nativeElement;
827
- if (!editor)
828
- return;
829
- const htmlLen = editor.innerHTML.length;
830
- const textLen = (editor.textContent || '').length;
831
- this.metricsChange.emit({ htmlLength: htmlLen, textLength: textLen });
832
- }
833
- // Remove empty style attributes (style="") and unwrap spans without any attributes
834
- cleanupEmptyStylesAndSpans(root) {
835
- console.log('[RTE-material] cleanupEmptyStylesAndSpans: start cleanup');
836
- const editor = root || this.editorRef.nativeElement;
837
- const toUnwrap = [];
838
- const toRemove = [];
839
- const walker = document.createTreeWalker(editor, NodeFilter.SHOW_ELEMENT);
840
- while (walker.nextNode()) {
841
- const el = walker.currentNode;
842
- if (el.hasAttribute('style')) {
843
- // If no style declarations remain, drop the attribute
844
- const cssText = el.getAttribute('style') || '';
845
- const byApiEmpty = (el.style ? el.style.length === 0 : false);
846
- const normalized = cssText.replace(/[\s;]/g, '');
847
- if (byApiEmpty || normalized.length === 0) {
848
- el.removeAttribute('style');
849
- }
850
- }
851
- if (el.tagName === 'SPAN') {
852
- if (!el.firstChild) {
853
- console.log('[RTE-material] cleanup: removing empty span', el);
854
- toRemove.push(el);
855
- continue;
856
- }
857
- if (el.attributes.length === 0) {
858
- console.log('[RTE-material] cleanup: unwrapping style-free span', el);
859
- toUnwrap.push(el);
860
- }
861
- }
862
- }
863
- for (const span of toUnwrap) {
864
- console.log('[RTE-material] cleanup: unwrap span', { span, childCount: span.childNodes.length });
865
- while (span.firstChild)
866
- span.parentNode?.insertBefore(span.firstChild, span);
867
- span.remove();
868
- }
869
- for (const span of toRemove) {
870
- console.log('[RTE-material] cleanup: remove span without children', span);
871
- span.remove();
872
- }
873
- }
874
- // History API
875
- get canUndo() { return this.historyIndex > 0; }
876
- get canRedo() { return this.historyIndex >= 0 && this.historyIndex < this.history.length - 1; }
877
- undo() {
878
- if (!this.canUndo)
879
- return;
880
- this.applySnapshot(this.historyIndex - 1);
881
- }
882
- redo() {
883
- if (!this.canRedo)
884
- return;
885
- this.applySnapshot(this.historyIndex + 1);
886
- }
887
- scheduleSnapshot() {
888
- if (this.isRestoringHistory)
889
- return;
890
- if (this.snapshotTimer)
891
- clearTimeout(this.snapshotTimer);
892
- this.snapshotTimer = setTimeout(() => this.takeSnapshot('input'), 250);
893
- }
894
- takeSnapshot(_reason) {
895
- if (this.isRestoringHistory)
896
- return;
897
- const editor = this.editorRef.nativeElement;
898
- const html = editor.innerHTML;
899
- const sel = window.getSelection();
900
- let rangeData = null;
901
- if (sel && sel.rangeCount > 0) {
902
- const r = sel.getRangeAt(0);
903
- if (this.isRangeInEditor(r)) {
904
- const ser = this.serializeRange(r);
905
- if (ser)
906
- rangeData = ser;
907
- }
908
- }
909
- // Verhindere Duplikate nacheinander
910
- const last = this.history[this.historyIndex];
911
- if (last && last.html === html)
912
- return;
913
- // Zukunft verwerfen bei neuem Snapshot
914
- if (this.historyIndex < this.history.length - 1) {
915
- this.history = this.history.slice(0, this.historyIndex + 1);
916
- }
917
- this.history.push({ html, range: rangeData });
918
- this.historyIndex = this.history.length - 1;
919
- // Begrenze Historie
920
- const MAX = 50;
921
- if (this.history.length > MAX) {
922
- const drop = this.history.length - MAX;
923
- this.history.splice(0, drop);
924
- this.historyIndex -= drop;
925
- if (this.historyIndex < 0)
926
- this.historyIndex = 0;
927
- }
928
- }
929
- applySnapshot(index) {
930
- if (index < 0 || index >= this.history.length)
931
- return;
932
- const snap = this.history[index];
933
- const editor = this.editorRef.nativeElement;
934
- this.isRestoringHistory = true;
935
- editor.innerHTML = snap.html;
936
- // Selektion wiederherstellen
937
- if (snap.range) {
938
- this.restoreSerializedRange(snap.range);
939
- }
940
- else {
941
- // Cursor ans Ende
942
- const sel = window.getSelection();
943
- if (sel) {
944
- sel.removeAllRanges();
945
- const r = document.createRange();
946
- r.selectNodeContents(editor);
947
- r.collapse(false);
948
- sel.addRange(r);
949
- }
950
- }
951
- this.historyIndex = index;
952
- this.isRestoringHistory = false;
953
- // Werte emittieren nach Restore
954
- this.emitValue();
955
- }
956
- serializeRange(range) {
957
- const s = this.nodePathFromNode(range.startContainer);
958
- const e = this.nodePathFromNode(range.endContainer);
959
- if (!s || !e)
960
- return null;
961
- return {
962
- startPath: s,
963
- startOffset: range.startOffset,
964
- endPath: e,
965
- endOffset: range.endOffset,
966
- };
967
- }
968
- restoreSerializedRange(data) {
969
- const editor = this.editorRef.nativeElement;
970
- const startNode = this.nodeFromPath(data.startPath);
971
- const endNode = this.nodeFromPath(data.endPath);
972
- if (!startNode || !endNode)
973
- return;
974
- const r = document.createRange();
975
- try {
976
- r.setStart(startNode, Math.min(data.startOffset, this.maxOffset(startNode)));
977
- r.setEnd(endNode, Math.min(data.endOffset, this.maxOffset(endNode)));
978
- }
979
- catch {
980
- r.selectNodeContents(editor);
981
- r.collapse(false);
982
- }
983
- const sel = window.getSelection();
984
- if (sel) {
985
- sel.removeAllRanges();
986
- sel.addRange(r);
987
- }
988
- }
989
- nodePathFromNode(node) {
990
- const editor = this.editorRef.nativeElement;
991
- const path = [];
992
- let n = node;
993
- while (n && n !== editor) {
994
- const pnode = n.parentNode;
995
- if (!pnode)
996
- return null;
997
- const idx = Array.prototype.indexOf.call(pnode.childNodes, n);
998
- path.push(idx);
999
- n = pnode;
1000
- }
1001
- if (n !== editor)
1002
- return null;
1003
- path.reverse();
1004
- return path;
1005
- }
1006
- nodeFromPath(path) {
1007
- const editor = this.editorRef.nativeElement;
1008
- let n = editor;
1009
- for (const idx of path) {
1010
- if (!n.childNodes || idx < 0 || idx >= n.childNodes.length)
1011
- return null;
1012
- n = n.childNodes[idx];
1013
- }
1014
- return n;
1015
- }
1016
- maxOffset(node) {
1017
- if (node.nodeType === Node.TEXT_NODE)
1018
- return node.data.length;
1019
- return node.childNodes.length;
1020
- }
1021
- focusEditor() {
1022
- const el = this.editorRef.nativeElement;
1023
- if (document.activeElement !== el) {
1024
- el.focus();
1025
- }
1026
- }
1027
- // Minimal inline style applier for inline styles
1028
- applyInlineStyle(cssProp, value) {
1029
- // Delegate to a generic, style-agnostic applier
1030
- this.applySelectionStyles({ [cssProp]: value });
1031
- }
1032
- applyInlineStyleSmart(cssProp, value) {
1033
- // Use generic, which handles single vs. multi-block selection
1034
- this.applySelectionStyles({ [cssProp]: value });
1035
- }
1036
- applySelectionStyles(styles) {
1037
- this.focusEditor();
1038
- this.restoreSelection();
1039
- const sel = window.getSelection();
1040
- if (!sel || sel.rangeCount === 0) {
1041
- console.warn('[RTE-material] applySelectionStyles: no selection available', { styles });
1042
- return;
1043
- }
1044
- let range = sel.getRangeAt(0);
1045
- if (range.collapsed) {
1046
- console.warn('[RTE-material] applySelectionStyles: range is collapsed', { styles });
1047
- return;
1048
- }
1049
- const entries = Object.entries(styles).filter(([, v]) => v != null && v !== '');
1050
- if (entries.length === 0) {
1051
- console.warn('[RTE-material] applySelectionStyles: no style entries to apply', { styles });
1052
- return;
1053
- }
1054
- const propsToClear = Array.from(new Set(entries.map(([prop]) => this.toCssProperty(prop))));
1055
- console.log('[RTE-material] applySelectionStyles: clearing props before apply', { propsToClear, entries });
1056
- for (const cssProp of propsToClear) {
1057
- const beforeClear = range.cloneRange();
1058
- const clearedRange = this.removeInlineStyleInRange(range, cssProp, { suppressEmit: true });
1059
- const refreshedSel = window.getSelection();
1060
- if (refreshedSel && refreshedSel.rangeCount > 0) {
1061
- range = refreshedSel.getRangeAt(0);
1062
- }
1063
- else if (clearedRange) {
1064
- range = clearedRange;
1065
- }
1066
- if (range.collapsed && !beforeClear.collapsed) {
1067
- console.warn('[RTE-material] applySelectionStyles: range collapsed after clearing, restoring previous range');
1068
- const sel = window.getSelection();
1069
- if (sel) {
1070
- sel.removeAllRanges();
1071
- sel.addRange(beforeClear);
1072
- }
1073
- range = beforeClear;
1074
- }
1075
- }
1076
- const segments = this.collectTextSegments(range);
1077
- if (segments.length === 0) {
1078
- console.warn('[RTE-material] applySelectionStyles: no text segments found', {
1079
- entries,
1080
- commonAncestor: range.commonAncestorContainer
1081
- });
1082
- return;
1083
- }
1084
- console.log('[RTE-material] applySelectionStyles: applying styles to segments', {
1085
- entries,
1086
- segmentCount: segments.length
1087
- });
1088
- const wrappedSegments = [];
1089
- for (let i = segments.length - 1; i >= 0; i--) {
1090
- const wrapped = this.applyStylesToSegment(segments[i], entries);
1091
- if (wrapped)
1092
- wrappedSegments.unshift(wrapped);
1093
- }
1094
- if (wrappedSegments.length) {
1095
- const first = wrappedSegments[0];
1096
- const last = wrappedSegments[wrappedSegments.length - 1];
1097
- const newRange = document.createRange();
1098
- newRange.setStartBefore(first);
1099
- newRange.setEndAfter(last);
1100
- const selAfter = window.getSelection();
1101
- if (selAfter) {
1102
- selAfter.removeAllRanges();
1103
- selAfter.addRange(newRange);
1104
- }
1105
- this.saveSelectionFromRange(newRange);
1106
- }
1107
- this.emitValue();
1108
- this.takeSnapshot('style');
1109
- }
1110
- collectTextSegments(range) {
1111
- const segments = [];
1112
- const root = range.commonAncestorContainer;
1113
- if (root.nodeType === Node.TEXT_NODE) {
1114
- const text = root;
1115
- const start = text === range.startContainer ? range.startOffset : 0;
1116
- const end = text === range.endContainer ? range.endOffset : text.data.length;
1117
- if (start < end)
1118
- segments.push({ node: text, start, end });
1119
- return segments;
1120
- }
1121
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1122
- while (walker.nextNode()) {
1123
- const text = walker.currentNode;
1124
- if (!text.data)
1125
- continue;
1126
- if (!this.intersectsRange(range, text))
1127
- continue;
1128
- const start = text === range.startContainer ? range.startOffset : 0;
1129
- const end = text === range.endContainer ? range.endOffset : text.data.length;
1130
- if (start >= end)
1131
- continue;
1132
- segments.push({ node: text, start, end });
1133
- }
1134
- return segments;
1135
- }
1136
- intersectsRange(range, node) {
1137
- try {
1138
- return range.intersectsNode ? range.intersectsNode(node) : true;
1139
- }
1140
- catch {
1141
- return false;
1142
- }
1143
- }
1144
- applyStylesToSegment(segment, entries) {
1145
- const segRange = document.createRange();
1146
- segRange.setStart(segment.node, segment.start);
1147
- segRange.setEnd(segment.node, segment.end);
1148
- const wrapper = document.createElement('span');
1149
- for (const [prop, value] of entries) {
1150
- wrapper.style.setProperty(this.toCssProperty(prop), value);
1151
- }
1152
- try {
1153
- segRange.surroundContents(wrapper);
1154
- return this.mergeAdjacentStyledSpans(wrapper);
1155
- }
1156
- catch (err) {
1157
- console.error('[RTE-material] applyStylesToSegment: failed to surround contents', {
1158
- error: err,
1159
- segment,
1160
- entries
1161
- });
1162
- }
1163
- return null;
1164
- }
1165
- toCssProperty(prop) {
1166
- return prop.replace(/([A-Z])/g, '-$1').toLowerCase();
1167
- }
1168
- mergeAdjacentStyledSpans(span) {
1169
- if (span.tagName !== 'SPAN')
1170
- return span;
1171
- let current = span;
1172
- const normalize = (el) => el && el.tagName === 'SPAN' ? el : null;
1173
- const styleText = (el) => (el ? el.getAttribute('style') || '' : '');
1174
- let parent = normalize(current.parentElement);
1175
- if (parent && styleText(parent) === styleText(current) && parent.attributes.length === current.attributes.length) {
1176
- while (current.firstChild)
1177
- parent.insertBefore(current.firstChild, current);
1178
- current.remove();
1179
- current = parent;
1180
- }
1181
- let prev = current.previousSibling;
1182
- while (prev && prev.nodeType === Node.TEXT_NODE && !prev.data.trim())
1183
- prev = prev.previousSibling;
1184
- if (prev instanceof HTMLElement && prev.tagName === 'SPAN' && styleText(prev) === styleText(current) && prev.attributes.length === current.attributes.length) {
1185
- while (current.firstChild)
1186
- prev.appendChild(current.firstChild);
1187
- current.remove();
1188
- current = prev;
1189
- }
1190
- let next = current.nextSibling;
1191
- while (next && next.nodeType === Node.TEXT_NODE && !next.data.trim())
1192
- next = next.nextSibling;
1193
- if (next instanceof HTMLElement && next.tagName === 'SPAN' && styleText(next) === styleText(current) && next.attributes.length === current.attributes.length) {
1194
- while (next.firstChild)
1195
- current.appendChild(next.firstChild);
1196
- next.remove();
1197
- }
1198
- return current;
1199
- }
1200
- isBoldCarrier(el) {
1201
- if (!el)
1202
- return false;
1203
- const tag = el.tagName;
1204
- if (tag === 'B' || tag === 'STRONG')
1205
- return true;
1206
- const fw = el.style?.fontWeight || '';
1207
- if (fw && fw !== 'normal' && fw !== '400')
1208
- return true;
1209
- return false;
1210
- }
1211
- boldAncestorContainingRange(range) {
1212
- let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1213
- ? range.commonAncestorContainer
1214
- : range.commonAncestorContainer.parentElement;
1215
- const editor = this.editorRef.nativeElement;
1216
- while (el && el !== editor) {
1217
- if (this.isBoldCarrier(el)) {
1218
- // ensure el contains both boundary containers fully
1219
- const sc = range.startContainer;
1220
- const ec = range.endContainer;
1221
- if (el.contains(sc) && el.contains(ec))
1222
- return el;
1223
- }
1224
- el = el.parentElement;
1225
- }
1226
- return null;
1227
- }
1228
- ancestorContainingRange(range, isCarrier) {
1229
- let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1230
- ? range.commonAncestorContainer
1231
- : range.commonAncestorContainer.parentElement;
1232
- const editor = this.editorRef.nativeElement;
1233
- while (el && el !== editor) {
1234
- if (isCarrier(el)) {
1235
- const sc = range.startContainer;
1236
- const ec = range.endContainer;
1237
- if (el.contains(sc) && el.contains(ec))
1238
- return el;
1239
- }
1240
- el = el.parentElement;
1241
- }
1242
- return null;
1243
- }
1244
- isItalicCarrier(el) {
1245
- if (!el)
1246
- return false;
1247
- if (el.tagName === 'I' || el.tagName === 'EM')
1248
- return true;
1249
- const fs = el.style?.fontStyle || '';
1250
- return !!fs && fs !== 'normal';
1251
- }
1252
- isUnderlineCarrier(el) {
1253
- if (!el)
1254
- return false;
1255
- if (el.tagName === 'U')
1256
- return true;
1257
- const td = el.style?.textDecoration || el.style?.textDecorationLine || '';
1258
- return typeof td === 'string' && td.includes('underline');
1259
- }
1260
- // Unwrap bold formatting only for the selected content inside a containing bold ancestor by splitting it into left/selection/right parts
1261
- deselectBoldBySplitting(range) {
1262
- const ancestor = this.boldAncestorContainingRange(range);
1263
- if (!ancestor)
1264
- return false;
1265
- this.splitCarrierAroundSelection(range, ancestor);
1266
- return true;
1267
- }
1268
- deselectItalicBySplitting(range) {
1269
- const anc = this.ancestorContainingRange(range, el => this.isItalicCarrier(el));
1270
- if (!anc)
1271
- return false;
1272
- this.splitCarrierAroundSelection(range, anc);
1273
- return true;
1274
- }
1275
- deselectUnderlineBySplitting(range) {
1276
- const anc = this.ancestorContainingRange(range, el => this.isUnderlineCarrier(el));
1277
- if (!anc)
1278
- return false;
1279
- this.splitCarrierAroundSelection(range, anc);
1280
- return true;
1281
- }
1282
- cleanupEmptyItalicSpans(root) {
1283
- const toRemove = [];
1284
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1285
- while (walker.nextNode()) {
1286
- const el = walker.currentNode;
1287
- const isItalic = (el.tagName === 'I' || el.tagName === 'EM' || (el.tagName === 'SPAN' && el.style.fontStyle && el.style.fontStyle !== 'normal'));
1288
- if (!isItalic)
1289
- continue;
1290
- const text = el.textContent ?? '';
1291
- const normalized = text.replace(/[\u00A0\u200B\s]/g, '');
1292
- const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
1293
- if (onlyWhitespace)
1294
- toRemove.push(el);
1295
- }
1296
- for (const el of toRemove)
1297
- el.remove();
1298
- }
1299
- cleanupEmptyUnderlineSpans(root) {
1300
- const toRemove = [];
1301
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1302
- while (walker.nextNode()) {
1303
- const el = walker.currentNode;
1304
- const isU = (el.tagName === 'U' || (el.tagName === 'SPAN' && typeof el.style.textDecoration === 'string' && el.style.textDecoration.includes('underline')));
1305
- if (!isU)
1306
- continue;
1307
- const text = el.textContent ?? '';
1308
- const normalized = text.replace(/[\u00A0\u200B\s]/g, '');
1309
- const onlyWhitespace = !el.firstChild || (normalized.length === 0 && el.querySelectorAll('*').length === 0);
1310
- if (onlyWhitespace)
1311
- toRemove.push(el);
1312
- }
1313
- for (const el of toRemove)
1314
- el.remove();
1315
- }
1316
- // Remove bold wrappers and inline font-weight style inside a container
1317
- stripBoldWithin(container) {
1318
- const toProcess = [];
1319
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
1320
- while (walker.nextNode()) {
1321
- toProcess.push(walker.currentNode);
1322
- }
1323
- for (const el of toProcess) {
1324
- if (el.tagName === 'B' || el.tagName === 'STRONG') {
1325
- while (el.firstChild)
1326
- el.parentNode?.insertBefore(el.firstChild, el);
1327
- el.remove();
1328
- continue;
1329
- }
1330
- if (el.tagName === 'SPAN') {
1331
- if (el.style.fontWeight)
1332
- el.style.removeProperty('font-weight');
1333
- // Clean empty style spans
1334
- if (!el.getAttribute('style')) {
1335
- while (el.firstChild)
1336
- el.parentNode?.insertBefore(el.firstChild, el);
1337
- el.remove();
1338
- }
1339
- }
1340
- }
1341
- }
1342
- // Pull the given node out of any bold carrier ancestors by splitting them around the node
1343
- liftOutOfBoldAncestors(node) {
1344
- const editor = this.editorRef.nativeElement;
1345
- let currentBold = this.findClosest(node, 'b,strong,span[style*="font-weight"]');
1346
- while (currentBold && currentBold !== editor) {
1347
- this.splitOutOfAncestor(currentBold, node);
1348
- currentBold = this.findClosest(node, 'b,strong,span[style*="font-weight"]');
1349
- }
1350
- }
1351
- // Split arbitrary ancestor element so that `node` becomes a sibling outside of it, preserving order even if `node` is nested deeply
1352
- splitOutOfAncestor(ancestor, node) {
1353
- // First, climb from node up to direct child of ancestor, splitting wrappers after `node` at each level
1354
- let child = node;
1355
- let parent = child.parentElement;
1356
- while (parent && parent !== ancestor) {
1357
- const right = parent.cloneNode(false);
1358
- // move siblings after `child` into right clone
1359
- while (child.nextSibling)
1360
- right.appendChild(child.nextSibling);
1361
- // insert right after parent (avoid Document-level issues)
1362
- const pParent = parent.parentNode;
1363
- if (pParent && pParent.nodeType !== Node.DOCUMENT_NODE) {
1364
- pParent.insertBefore(right, parent.nextSibling);
1365
- }
1366
- else {
1367
- while (right.firstChild)
1368
- parent.appendChild(right.firstChild);
1369
- }
1370
- // ascend
1371
- child = parent;
1372
- parent = child.parentElement;
1373
- }
1374
- if (!parent)
1375
- return;
1376
- // Now `child` is a direct child of ancestor
1377
- const before = ancestor.cloneNode(false);
1378
- while (ancestor.firstChild && ancestor.firstChild !== child) {
1379
- before.appendChild(ancestor.firstChild);
1380
- }
1381
- const after = ancestor.cloneNode(false);
1382
- while (child.nextSibling)
1383
- after.appendChild(child.nextSibling);
1384
- const gp = ancestor.parentNode;
1385
- if (!gp)
1386
- return;
1387
- if (gp.nodeType === Node.DOCUMENT_NODE) {
1388
- if (before.childNodes.length) {
1389
- while (before.firstChild)
1390
- ancestor.parentNode?.insertBefore(before.firstChild, ancestor);
1391
- }
1392
- ancestor.parentNode?.insertBefore(node, ancestor);
1393
- if (after.childNodes.length) {
1394
- while (after.firstChild)
1395
- ancestor.parentNode?.insertBefore(after.firstChild, ancestor);
1396
- }
1397
- ancestor.remove();
1398
- return;
1399
- }
1400
- if (before.childNodes.length)
1401
- gp.insertBefore(before, ancestor);
1402
- // move node out, place where ancestor was
1403
- gp.insertBefore(node, ancestor);
1404
- if (after.childNodes.length)
1405
- gp.insertBefore(after, ancestor);
1406
- ancestor.remove();
1407
- }
1408
- saveSelectionFromRange(range) {
1409
- this.savedRange = range.cloneRange();
1410
- }
1411
- wrapSelectionWith(tag, attrs) {
1412
- const sel = window.getSelection();
1413
- if (!sel || sel.rangeCount === 0)
1414
- return;
1415
- const range = sel.getRangeAt(0);
1416
- if (range.collapsed)
1417
- return;
1418
- const el = document.createElement(tag);
1419
- if (attrs) {
1420
- for (const [k, v] of Object.entries(attrs)) {
1421
- if (v != null)
1422
- el.setAttribute(k, v);
1423
- }
1424
- }
1425
- const frag = range.extractContents();
1426
- el.appendChild(frag);
1427
- range.insertNode(el);
1428
- // Auswahl hinter das Element setzen
1429
- sel.removeAllRanges();
1430
- const after = document.createRange();
1431
- after.setStartAfter(el);
1432
- after.collapse(true);
1433
- sel.addRange(after);
1434
- }
1435
- insertTextAtSelection(text) {
1436
- const sel = window.getSelection();
1437
- if (!sel || sel.rangeCount === 0)
1438
- return;
1439
- const range = sel.getRangeAt(0);
1440
- range.deleteContents();
1441
- const node = document.createTextNode(text);
1442
- range.insertNode(node);
1443
- // Cursor ans Ende des eingefügten Textes
1444
- sel.removeAllRanges();
1445
- const after = document.createRange();
1446
- after.setStartAfter(node);
1447
- after.collapse(true);
1448
- sel.addRange(after);
1449
- }
1450
- // ----- Inline toggle logic (Bold/Italic/Underline) -----
1451
- updateInlineStates() {
1452
- const sel = window.getSelection();
1453
- if (!sel || sel.rangeCount === 0) {
1454
- this.isBoldActive = this.isItalicActive = this.isUnderlineActive = false;
1455
- return;
1456
- }
1457
- const range = sel.getRangeAt(0);
1458
- if (!this.isRangeInEditor(range)) {
1459
- this.isBoldActive = this.isItalicActive = this.isUnderlineActive = false;
1460
- return;
1461
- }
1462
- if (range.collapsed) {
1463
- const node = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement;
1464
- this.isBoldActive = this.isNodeBold(node);
1465
- this.isItalicActive = this.isNodeItalic(node);
1466
- this.isUnderlineActive = this.isNodeUnderline(node);
1467
- return;
1468
- }
1469
- this.isBoldActive = this.computeBoldAnyForRange(range);
1470
- this.isItalicActive = this.computeItalicAnyForRange(range);
1471
- this.isUnderlineActive = this.computeUnderlineAnyForRange(range);
1472
- // ensure UI reflects changes immediately
1473
- try {
1474
- this.cdr.detectChanges();
1475
- }
1476
- catch { }
1477
- }
1478
- computeBoldAnyForRange(range) {
1479
- return this.selectionHasStyle(range, 'font-weight');
1480
- }
1481
- computeItalicAnyForRange(range) {
1482
- return this.selectionHasStyle(range, 'font-style');
1483
- }
1484
- computeUnderlineAnyForRange(range) {
1485
- return this.selectionHasStyle(range, 'text-decoration');
1486
- }
1487
- isNodeBold(node) {
1488
- let el = node;
1489
- while (el) {
1490
- if (el.tagName === 'B' || el.tagName === 'STRONG')
1491
- return true;
1492
- const cs = getComputedStyle(el);
1493
- if (cs.fontWeight && cs.fontWeight !== 'normal' && cs.fontWeight !== '400') {
1494
- const w = cs.fontWeight === 'bold' ? 700 : parseInt(cs.fontWeight, 10);
1495
- if (!Number.isNaN(w) && w >= 600)
1496
- return true;
1497
- }
1498
- el = el.parentElement;
1499
- }
1500
- return false;
1501
- }
1502
- isNodeItalic(node) {
1503
- let el = node;
1504
- while (el) {
1505
- if (el.tagName === 'I' || el.tagName === 'EM')
1506
- return true;
1507
- const cs = getComputedStyle(el);
1508
- if (cs.fontStyle && cs.fontStyle !== 'normal')
1509
- return true;
1510
- el = el.parentElement;
1511
- }
1512
- return false;
1513
- }
1514
- isNodeUnderline(node) {
1515
- let el = node;
1516
- while (el) {
1517
- if (el.tagName === 'U')
1518
- return true;
1519
- const cs = getComputedStyle(el);
1520
- if (cs.textDecorationLine && cs.textDecorationLine.includes('underline'))
1521
- return true;
1522
- el = el.parentElement;
1523
- }
1524
- return false;
1525
- }
1526
- removeInlineStyleInRange(range, cssPropKebab, options) {
1527
- const shouldRestoreSelection = options?.restoreSelection !== false;
1528
- this.isolateRangeFromStyleCarriers(range, cssPropKebab);
1529
- const editor = this.editorRef.nativeElement;
1530
- const selAfter = window.getSelection();
1531
- const effectiveRange = selAfter && selAfter.rangeCount > 0 ? selAfter.getRangeAt(0) : range;
1532
- const common = effectiveRange.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1533
- ? effectiveRange.commonAncestorContainer
1534
- : (effectiveRange.commonAncestorContainer.parentElement || editor);
1535
- const affected = [];
1536
- const intersects = (node) => {
1537
- try {
1538
- return effectiveRange.intersectsNode ? effectiveRange.intersectsNode(node) : true;
1539
- }
1540
- catch {
1541
- return false;
1542
- }
1543
- };
1544
- const matchesEl = (el) => this.isStyleCarrier(el, cssPropKebab);
1545
- console.log('[RTE-material] removeInlineStyleInRange:start', {
1546
- cssPropKebab,
1547
- rangeCollapsed: effectiveRange.collapsed,
1548
- commonTag: common.tagName,
1549
- commonHasStyle: common.style ? common.style.getPropertyValue?.(cssPropKebab) : undefined
1550
- });
1551
- if (common instanceof HTMLElement && matchesEl(common) && intersects(common)) {
1552
- affected.push(common);
1553
- }
1554
- const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
1555
- while (walker.nextNode()) {
1556
- const el = walker.currentNode;
1557
- if (!matchesEl(el))
1558
- continue;
1559
- if (intersects(el)) {
1560
- affected.push(el);
1561
- console.log('[RTE-material] removeInlineStyleInRange:match', el.tagName, el.getAttribute('style'));
1562
- }
1563
- }
1564
- let removedTags = 0;
1565
- let removedProps = 0;
1566
- for (const el of affected) {
1567
- if (el.tagName === 'B' || el.tagName === 'STRONG' || el.tagName === 'I' || el.tagName === 'EM' || el.tagName === 'U') {
1568
- console.log('[RTE-material] unwrap tag', el.tagName);
1569
- while (el.firstChild)
1570
- el.parentNode?.insertBefore(el.firstChild, el);
1571
- el.remove();
1572
- removedTags++;
1573
- continue;
1574
- }
1575
- console.log('[RTE-material] remove style', cssPropKebab, 'from', el.tagName, el.getAttribute('style'));
1576
- el.style.removeProperty(cssPropKebab);
1577
- if (!el.style.length) {
1578
- console.log('[RTE-material] removeInlineStyleInRange: style attribute now empty, removing attribute', el);
1579
- el.removeAttribute('style');
1580
- }
1581
- removedProps++;
1582
- if (!el.getAttribute('style')) {
1583
- console.log('[RTE-material] unwrap empty styled span', el.tagName);
1584
- while (el.firstChild)
1585
- el.parentNode?.insertBefore(el.firstChild, el);
1586
- el.remove();
1587
- }
1588
- }
1589
- this.pruneDanglingStyleCarriers(common, cssPropKebab);
1590
- console.log('[RTE-material] removeInlineStyleInRange:done', { affected: affected.length, removedTags, removedProps });
1591
- let resultRange = null;
1592
- if (!options?.suppressEmit) {
1593
- this.emitValue();
1594
- resultRange = null;
1595
- }
1596
- else {
1597
- const sel = window.getSelection();
1598
- if (sel && sel.rangeCount > 0) {
1599
- resultRange = sel.getRangeAt(0).cloneRange();
1600
- }
1601
- else if (shouldRestoreSelection) {
1602
- resultRange = effectiveRange.cloneRange();
1603
- }
1604
- if (shouldRestoreSelection && resultRange) {
1605
- const selRestore = window.getSelection();
1606
- if (selRestore) {
1607
- selRestore.removeAllRanges();
1608
- selRestore.addRange(resultRange);
1609
- this.saveSelectionFromRange(resultRange);
1610
- }
1611
- }
1612
- }
1613
- return resultRange;
1614
- }
1615
- pruneDanglingStyleCarriers(root, cssPropKebab) {
1616
- const toRemove = [];
1617
- if (root instanceof HTMLElement && this.isStyleCarrier(root, cssPropKebab) && !this.elementHasVisibleContent(root)) {
1618
- toRemove.push(root);
1619
- }
1620
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1621
- while (walker.nextNode()) {
1622
- const el = walker.currentNode;
1623
- if (!this.isStyleCarrier(el, cssPropKebab))
1624
- continue;
1625
- if (this.elementHasVisibleContent(el))
1626
- continue;
1627
- toRemove.push(el);
1628
- }
1629
- for (const el of toRemove) {
1630
- console.log('[RTE-material] pruneDanglingStyleCarriers: removing empty carrier', { tag: el.tagName, style: el.getAttribute('style') });
1631
- if (el.tagName === 'B' || el.tagName === 'STRONG' || el.tagName === 'I' || el.tagName === 'EM' || el.tagName === 'U') {
1632
- el.remove();
1633
- continue;
1634
- }
1635
- el.remove();
1636
- }
1637
- }
1638
- elementHasVisibleContent(node) {
1639
- if (node.nodeType === Node.TEXT_NODE) {
1640
- const text = node.data;
1641
- for (let i = 0; i < text.length; i++) {
1642
- const ch = text[i];
1643
- if (ch === '\u00a0')
1644
- return true;
1645
- if (!/\s/.test(ch) && ch !== '\u200b' && ch !== '\u200c' && ch !== '\u200d' && ch !== '\ufeff') {
1646
- return true;
1647
- }
1648
- }
1649
- return false;
1650
- }
1651
- if (node.nodeType === Node.ELEMENT_NODE) {
1652
- const el = node;
1653
- if (el.tagName === 'BR')
1654
- return true;
1655
- for (const child of Array.from(node.childNodes)) {
1656
- if (this.elementHasVisibleContent(child))
1657
- return true;
1658
- }
1659
- }
1660
- return false;
1661
- }
1662
- isolateRangeFromStyleCarriers(range, cssPropKebab) {
1663
- let carrier = this.findCarrierContainingRange(range, cssPropKebab);
1664
- while (carrier) {
1665
- this.splitCarrierAroundSelection(range, carrier);
1666
- carrier = this.findCarrierContainingRange(range, cssPropKebab);
1667
- }
1668
- }
1669
- selectionHasStyle(range, cssPropKebab) {
1670
- const editor = this.editorRef.nativeElement;
1671
- const common = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1672
- ? range.commonAncestorContainer
1673
- : (range.commonAncestorContainer.parentElement || editor);
1674
- const intersects = (node) => this.intersectsRange(range, node);
1675
- if (common instanceof HTMLElement && this.isStyleCarrier(common, cssPropKebab) && intersects(common)) {
1676
- return true;
1677
- }
1678
- const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
1679
- while (walker.nextNode()) {
1680
- const el = walker.currentNode;
1681
- if (!this.isStyleCarrier(el, cssPropKebab))
1682
- continue;
1683
- if (intersects(el))
1684
- return true;
1685
- }
1686
- return false;
1687
- }
1688
- findCarrierContainingRange(range, cssPropKebab) {
1689
- let el = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
1690
- ? range.commonAncestorContainer
1691
- : range.commonAncestorContainer.parentElement;
1692
- const editor = this.editorRef.nativeElement;
1693
- while (el && el !== editor) {
1694
- if (this.isStyleCarrier(el, cssPropKebab)) {
1695
- const sc = range.startContainer;
1696
- const ec = range.endContainer;
1697
- if (el.contains(sc) && el.contains(ec))
1698
- return el;
1699
- }
1700
- el = el.parentElement;
1701
- }
1702
- return null;
1703
- }
1704
- isStyleCarrier(el, cssPropKebab) {
1705
- const tag = el.tagName;
1706
- if (cssPropKebab === 'font-weight') {
1707
- if (tag === 'B' || tag === 'STRONG')
1708
- return true;
1709
- const fw = el.style?.fontWeight || '';
1710
- return !!fw && fw !== 'normal' && fw !== '400';
1711
- }
1712
- if (cssPropKebab === 'font-style') {
1713
- if (tag === 'I' || tag === 'EM')
1714
- return true;
1715
- const fs = el.style?.fontStyle || '';
1716
- return !!fs && fs !== 'normal';
1717
- }
1718
- if (cssPropKebab === 'text-decoration') {
1719
- if (tag === 'U')
1720
- return true;
1721
- const td = el.style?.textDecoration || el.style?.textDecorationLine || '';
1722
- return typeof td === 'string' && td.includes('underline');
1723
- }
1724
- const inline = el.style?.getPropertyValue(cssPropKebab) || '';
1725
- if (!inline)
1726
- return false;
1727
- if (cssPropKebab === 'color') {
1728
- return inline !== 'inherit' && inline !== 'initial';
1729
- }
1730
- if (cssPropKebab === 'background-color') {
1731
- return inline !== 'transparent' && inline !== 'initial';
1732
- }
1733
- return true;
1734
- }
1735
- splitCarrierAroundSelection(range, carrier) {
1736
- const parent = carrier.parentNode;
1737
- if (!parent)
1738
- return;
1739
- const rightRange = document.createRange();
1740
- rightRange.setStart(range.endContainer, range.endOffset);
1741
- try {
1742
- rightRange.setEnd(carrier, carrier.childNodes.length);
1743
- }
1744
- catch { }
1745
- const rightFrag = rightRange.extractContents();
1746
- const leftRange = document.createRange();
1747
- leftRange.setStart(carrier, 0);
1748
- try {
1749
- leftRange.setEnd(range.startContainer, range.startOffset);
1750
- }
1751
- catch { }
1752
- const leftFrag = leftRange.extractContents();
1753
- if (leftFrag.childNodes.length) {
1754
- const leftClone = carrier.cloneNode(false);
1755
- leftClone.appendChild(leftFrag);
1756
- parent.insertBefore(leftClone, carrier);
1757
- }
1758
- let insertAfter = carrier;
1759
- if (rightFrag.childNodes.length) {
1760
- const rightClone = carrier.cloneNode(false);
1761
- rightClone.appendChild(rightFrag);
1762
- parent.insertBefore(rightClone, carrier.nextSibling);
1763
- insertAfter = rightClone;
1764
- }
1765
- while (carrier.firstChild)
1766
- parent.insertBefore(carrier.firstChild, insertAfter);
1767
- carrier.remove();
1768
- }
1769
- toggleBold() {
1770
- this.focusEditor();
1771
- this.restoreSelection();
1772
- const sel = window.getSelection();
1773
- if (!sel || sel.rangeCount === 0)
1774
- return;
1775
- const range = sel.getRangeAt(0);
1776
- if (range.collapsed)
1777
- this.expandRangeToWord(range);
1778
- const anyActive = this.computeBoldAnyForRange(range);
1779
- if (anyActive) {
1780
- this.removeInlineStyleInRange(range, 'font-weight');
1781
- }
1782
- else {
1783
- this.applySelectionStyles({ fontWeight: 'bold' });
1784
- }
1785
- this.updateInlineStates();
1786
- this.takeSnapshot('toggle-bold');
1787
- }
1788
- toggleItalic() {
1789
- this.focusEditor();
1790
- this.restoreSelection();
1791
- const sel = window.getSelection();
1792
- if (!sel || sel.rangeCount === 0)
1793
- return;
1794
- const range = sel.getRangeAt(0);
1795
- if (range.collapsed)
1796
- this.expandRangeToWord(range);
1797
- const anyActive = this.computeItalicAnyForRange(range);
1798
- if (anyActive) {
1799
- this.removeInlineStyleInRange(range, 'font-style');
1800
- }
1801
- else {
1802
- this.applySelectionStyles({ fontStyle: 'italic' });
1803
- }
1804
- this.updateInlineStates();
1805
- this.takeSnapshot('toggle-italic');
1806
- }
1807
- toggleUnderline() {
1808
- this.focusEditor();
1809
- this.restoreSelection();
1810
- const sel = window.getSelection();
1811
- if (!sel || sel.rangeCount === 0)
1812
- return;
1813
- const range = sel.getRangeAt(0);
1814
- if (range.collapsed)
1815
- this.expandRangeToWord(range);
1816
- const anyActive = this.computeUnderlineAnyForRange(range);
1817
- if (anyActive) {
1818
- this.removeInlineStyleInRange(range, 'text-decoration');
1819
- }
1820
- else {
1821
- this.applySelectionStyles({ textDecoration: 'underline' });
1822
- }
1823
- this.updateInlineStates();
1824
- this.takeSnapshot('toggle-underline');
1825
- }
1826
- getCurrentRange() {
1827
- const sel = window.getSelection();
1828
- if (!sel || sel.rangeCount === 0)
1829
- return null;
1830
- const range = sel.getRangeAt(0);
1831
- return this.isRangeInEditor(range) ? range : null;
1832
- }
1833
- getEditorChildAncestor(node) {
1834
- const editor = this.editorRef.nativeElement;
1835
- let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1836
- while (el && el.parentElement && el.parentElement !== editor) {
1837
- el = el.parentElement;
1838
- }
1839
- // Wenn direktes Kind des Editors
1840
- if (el && el.parentElement === editor)
1841
- return el;
1842
- return editor;
1843
- }
1844
- findClosest(node, selector) {
1845
- let el = node && node.nodeType === 1 ? node : node?.parentElement ?? null;
1846
- while (el) {
1847
- if (el.matches(selector))
1848
- return el;
1849
- el = el.parentElement;
1850
- }
1851
- return null;
1852
- }
1853
- toggleList(kind) {
1854
- const range = this.getCurrentRange();
1855
- if (!range)
1856
- return;
1857
- const editor = this.editorRef.nativeElement;
1858
- // Wenn bereits in einer Liste, Liste aufheben
1859
- const listAncestor = this.findClosest(range.commonAncestorContainer, 'ul,ol');
1860
- if (listAncestor) {
1861
- // unwrap: li-Inhalte an die Stelle der Liste setzen
1862
- const frag = document.createDocumentFragment();
1863
- const items = Array.from(listAncestor.children); // li
1864
- for (const li of items) {
1865
- while (li.firstChild)
1866
- frag.appendChild(li.firstChild);
1867
- // Optional: Zeilenumbruch zwischen Items
1868
- frag.appendChild(document.createElement('br'));
1869
- }
1870
- listAncestor.replaceWith(frag);
1871
- return;
1872
- }
1873
- // Neue Liste um die aktuelle Auswahl legen
1874
- const list = document.createElement(kind);
1875
- const li = document.createElement('li');
1876
- const contents = range.extractContents();
1877
- if (!contents.hasChildNodes()) {
1878
- li.textContent = '\u200b'; // zero-width space um leere li zu vermeiden
1879
- }
1880
- else {
1881
- li.appendChild(contents);
1882
- }
1883
- list.appendChild(li);
1884
- range.insertNode(list);
1885
- // Cursor ins li setzen
1886
- const sel = window.getSelection();
1887
- if (sel) {
1888
- sel.removeAllRanges();
1889
- const after = document.createRange();
1890
- after.selectNodeContents(li);
1891
- after.collapse(false);
1892
- sel.addRange(after);
1893
- }
1894
- }
1895
- alignBlocks(align) {
1896
- const range = this.getCurrentRange();
1897
- if (!range)
1898
- return;
1899
- const startBlock = this.getEditorChildAncestor(range.startContainer);
1900
- const endBlock = this.getEditorChildAncestor(range.endContainer);
1901
- const editor = this.editorRef.nativeElement;
1902
- const applyAlign = (el) => {
1903
- if (!el)
1904
- return;
1905
- el.style.textAlign = align;
1906
- };
1907
- if (startBlock === endBlock) {
1908
- applyAlign(startBlock);
1909
- }
1910
- else {
1911
- // grob: alle direkten Kinder zwischen start und end ausrichten
1912
- const children = Array.from(editor.children);
1913
- const i1 = children.indexOf(startBlock);
1914
- const i2 = children.indexOf(endBlock);
1915
- if (i1 >= 0 && i2 >= 0) {
1916
- const [from, to] = i1 <= i2 ? [i1, i2] : [i2, i1];
1917
- for (let i = from; i <= to; i++)
1918
- applyAlign(children[i]);
1919
- }
1920
- else {
1921
- applyAlign(startBlock);
1922
- }
1923
- }
1924
- }
1925
- setBlockSpacing(kind, target, value) {
1926
- const range = this.getCurrentRange();
1927
- if (!range)
1928
- return;
1929
- const editor = this.editorRef.nativeElement;
1930
- const apply = (el) => {
1931
- if (!el)
1932
- return;
1933
- const v = `${value}px`;
1934
- if (target === 'all') {
1935
- el.style[kind] = v;
1936
- }
1937
- else if (target === 'vertical') {
1938
- el.style[`${kind}Top`] = v;
1939
- el.style[`${kind}Bottom`] = v;
1940
- }
1941
- else if (target === 'horizontal') {
1942
- el.style[`${kind}Left`] = v;
1943
- el.style[`${kind}Right`] = v;
1944
- }
1945
- };
1946
- const startBlock = this.getEditorChildAncestor(range.startContainer);
1947
- const endBlock = this.getEditorChildAncestor(range.endContainer);
1948
- if (startBlock === endBlock) {
1949
- apply(startBlock);
1950
- }
1951
- else {
1952
- const children = Array.from(editor.children);
1953
- const i1 = children.indexOf(startBlock);
1954
- const i2 = children.indexOf(endBlock);
1955
- if (i1 >= 0 && i2 >= 0) {
1956
- const [from, to] = i1 <= i2 ? [i1, i2] : [i2, i1];
1957
- for (let i = from; i <= to; i++)
1958
- apply(children[i]);
1959
- }
1960
- else {
1961
- apply(startBlock);
1962
- }
1963
- }
1964
- }
1965
- // Versucht, Margin/Padding auf die konkrete Text-Selektion anzuwenden, indem die Auswahl mit einem Span umschlossen wird.
1966
- // Liefert true, wenn inline angewendet werden konnte; sonst false (z. B. wenn Auswahl blockübergreifend ist).
1967
- applySpacingToSelection(kind, target, value) {
1968
- const sel = window.getSelection();
1969
- if (!sel || sel.rangeCount === 0)
1970
- return false;
1971
- const range = sel.getRangeAt(0);
1972
- if (range.collapsed)
1973
- return false;
1974
- const startBlock = this.getEditorChildAncestor(range.startContainer);
1975
- const endBlock = this.getEditorChildAncestor(range.endContainer);
1976
- // Nur inline, wenn innerhalb desselben Blocks
1977
- if (!startBlock || !endBlock || startBlock !== endBlock)
1978
- return false;
1979
- const wrapper = document.createElement('span');
1980
- const px = `${value}px`;
1981
- if (target === 'all') {
1982
- wrapper.style[kind] = px;
1983
- }
1984
- else if (target === 'vertical') {
1985
- wrapper.style[`${kind}Top`] = px;
1986
- wrapper.style[`${kind}Bottom`] = px;
1987
- }
1988
- else if (target === 'horizontal') {
1989
- wrapper.style[`${kind}Left`] = px;
1990
- wrapper.style[`${kind}Right`] = px;
1991
- }
1992
- // Für vertikale Margins auf Inline-Elementen sicherstellen, dass sie greifen
1993
- if (kind === 'margin' && (target === 'all' || target === 'vertical')) {
1994
- wrapper.style.display = 'inline-block';
1995
- }
1996
- // Auswahl extrahieren und in Wrapper einsetzen
1997
- const frag = range.extractContents();
1998
- wrapper.appendChild(frag);
1999
- range.insertNode(wrapper);
2000
- // Gleichartige Wrapper zusammenführen und Cursor korrekt setzen
2001
- const normalized = this.normalizeSpacingSpans(wrapper, kind, target, value) || wrapper;
2002
- sel.removeAllRanges();
2003
- const after = document.createRange();
2004
- if (normalized.isConnected) {
2005
- after.setStartAfter(normalized);
2006
- }
2007
- else {
2008
- // Fallback: an das Ende des Startblocks
2009
- const sb = this.getEditorChildAncestor(range.startContainer);
2010
- if (sb)
2011
- after.selectNodeContents(sb);
2012
- }
2013
- after.collapse(true);
2014
- sel.addRange(after);
2015
- return true;
2016
- }
2017
- normalizeSpacingSpans(span, kind, target, value) {
2018
- const props = [];
2019
- if (target === 'all')
2020
- props.push(kind);
2021
- if (target === 'vertical')
2022
- props.push(`${kind}Top`, `${kind}Bottom`);
2023
- if (target === 'horizontal')
2024
- props.push(`${kind}Left`, `${kind}Right`);
2025
- const displayNeeded = kind === 'margin' && (target === 'all' || target === 'vertical');
2026
- const v = `${value}px`;
2027
- const hasSameSpacing = (a, b) => {
2028
- for (const p of props) {
2029
- if (a.style[p] !== b.style[p])
2030
- return false;
2031
- }
2032
- if (displayNeeded) {
2033
- if (a.style.display !== b.style.display)
2034
- return false;
2035
- }
2036
- return true;
2037
- };
2038
- // Downward: verschachtelte identische Spans in span zusammenführen
2039
- if (span.children.length === 1) {
2040
- const only = span.children[0];
2041
- if (only && only.tagName === 'SPAN' && hasSameSpacing(span, only)) {
2042
- while (only.firstChild)
2043
- span.appendChild(only.firstChild);
2044
- only.remove();
2045
- }
2046
- }
2047
- let current = span;
2048
- // Upward: mit Elternelement verschmelzen, wenn identischer Span
2049
- const parent = current.parentElement;
2050
- if (parent && parent.tagName === 'SPAN' && hasSameSpacing(parent, current)) {
2051
- while (current.firstChild)
2052
- parent.insertBefore(current.firstChild, current);
2053
- current.remove();
2054
- current = parent;
2055
- }
2056
- if (!current)
2057
- return null;
2058
- // Left merge: vorherige Geschwister-Spans mit gleicher Formatierung in current ziehen
2059
- let prev = current.previousElementSibling;
2060
- if (prev && prev.tagName === 'SPAN' && hasSameSpacing(prev, current)) {
2061
- while (current.firstChild)
2062
- prev.appendChild(current.firstChild);
2063
- current.remove();
2064
- current = prev;
2065
- }
2066
- // Right merge: folgende Geschwister-Spans in current ziehen
2067
- let next = current.nextElementSibling;
2068
- while (next && next.tagName === 'SPAN' && hasSameSpacing(current, next)) {
2069
- while (next.firstChild)
2070
- current.appendChild(next.firstChild);
2071
- const toRemove = next;
2072
- next = next.nextElementSibling;
2073
- toRemove.remove();
2074
- }
2075
- return current;
2076
- }
2077
- setHeading(tag) {
2078
- const range = this.getCurrentRange();
2079
- if (!range)
2080
- return;
2081
- const editor = this.editorRef.nativeElement;
2082
- const replaceTag = (el, newTag) => {
2083
- if (!el || el === editor)
2084
- return null;
2085
- const neo = document.createElement(newTag);
2086
- while (el.firstChild)
2087
- neo.appendChild(el.firstChild);
2088
- el.replaceWith(neo);
2089
- return neo;
2090
- };
2091
- const startBlock = this.getEditorChildAncestor(range.startContainer);
2092
- const endBlock = this.getEditorChildAncestor(range.endContainer);
2093
- if (startBlock && startBlock !== editor && (startBlock === endBlock)) {
2094
- const currentTag = (startBlock.tagName || '').toLowerCase();
2095
- if (currentTag === tag)
2096
- return;
2097
- const convertible = /^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag);
2098
- if (convertible) {
2099
- const neo = replaceTag(startBlock, tag);
2100
- if (neo && tag === 'pre') {
2101
- neo.setAttribute('style', 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;');
2102
- }
2103
- if (neo) {
2104
- const sel = window.getSelection();
2105
- if (sel) {
2106
- sel.removeAllRanges();
2107
- const after = document.createRange();
2108
- after.selectNodeContents(neo);
2109
- after.collapse(false);
2110
- sel.addRange(after);
2111
- }
2112
- }
2113
- return;
2114
- }
2115
- }
2116
- if (startBlock && endBlock && startBlock !== editor && endBlock !== editor) {
2117
- const children = Array.from(editor.children);
2118
- const i1 = children.indexOf(startBlock);
2119
- const i2 = children.indexOf(endBlock);
2120
- if (i1 >= 0 && i2 >= 0) {
2121
- const [from, to] = i1 <= i2 ? [i1, i2] : [i2, i1];
2122
- let last = null;
2123
- for (let i = from; i <= to; i++) {
2124
- const el = children[i];
2125
- const currentTag = (el.tagName || '').toLowerCase();
2126
- if (/^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag)) {
2127
- const neo = replaceTag(el, tag);
2128
- if (neo && tag === 'pre') {
2129
- neo.setAttribute('style', 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;');
2130
- }
2131
- if (neo)
2132
- last = neo;
2133
- }
2134
- }
2135
- if (last) {
2136
- const sel = window.getSelection();
2137
- if (sel) {
2138
- sel.removeAllRanges();
2139
- const after = document.createRange();
2140
- after.selectNodeContents(last);
2141
- after.collapse(false);
2142
- sel.addRange(after);
2143
- }
2144
- }
2145
- return;
2146
- }
2147
- }
2148
- if (!range.collapsed) {
2149
- const el = document.createElement(tag);
2150
- if (tag === 'pre') {
2151
- el.setAttribute('style', 'white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;');
2152
- }
2153
- const frag = range.extractContents();
2154
- el.appendChild(frag);
2155
- range.insertNode(el);
2156
- const sel = window.getSelection();
2157
- if (sel) {
2158
- sel.removeAllRanges();
2159
- const after = document.createRange();
2160
- after.selectNodeContents(el);
2161
- after.collapse(false);
2162
- sel.addRange(after);
2163
- }
2164
- }
152
+ super(cdr);
2165
153
  }
2166
154
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorMaterial, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2167
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditorMaterial, isStandalone: true, selector: "stackch-richtext-editor-material", 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)" } }, providers: [
155
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditorMaterial, isStandalone: true, selector: "stackch-richtext-editor-material", providers: [
2168
156
  {
2169
157
  provide: NG_VALUE_ACCESSOR,
2170
158
  useExisting: forwardRef(() => StackchRichtextEditorMaterial),
2171
159
  multi: true,
2172
160
  },
2173
- ], 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-material-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: StackchRichtextEditorMaterialToolbar, selector: "stackch-richtext-editor-material-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"] }] });
161
+ ], usesInheritance: true, ngImport: i0, template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-material-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: StackchRichtextEditorMaterialToolbar, selector: "stackch-richtext-editor-material-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"] }] });
2174
162
  }
2175
163
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorMaterial, decorators: [{
2176
164
  type: Component,
@@ -2181,45 +169,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
2181
169
  multi: true,
2182
170
  },
2183
171
  ], template: "<div class=\"stackch_rte\" [class.stackch_rte--disabled]=\"disabled\">\r\n @if (showToolbar) {\r\n <stackch-richtext-editor-material-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"] }]
2184
- }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { placeholder: [{
2185
- type: Input
2186
- }], showToolbar: [{
2187
- type: Input
2188
- }], fonts: [{
2189
- type: Input
2190
- }], fontSizes: [{
2191
- type: Input
2192
- }], height: [{
2193
- type: Input
2194
- }], minHeight: [{
2195
- type: Input
2196
- }], maxHeight: [{
2197
- type: Input
2198
- }], disabled: [{
2199
- type: Input
2200
- }], valueChange: [{
2201
- type: Output
2202
- }], metricsChange: [{
2203
- type: Output
2204
- }], editorRef: [{
2205
- type: ViewChild,
2206
- args: ['editor', { static: true }]
2207
- }], config: [{
2208
- type: Input
2209
- }], onSelectionChange: [{
2210
- type: HostListener,
2211
- args: ['document:selectionchange']
2212
- }], closeMenus: [{
2213
- type: HostListener,
2214
- args: ['document:click']
2215
- }], onKeydown: [{
2216
- type: HostListener,
2217
- args: ['keydown', ['$event']]
2218
- }] } });
172
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }] });
2219
173
 
2220
174
  /**
2221
175
  * Generated bundle index. Do not edit.
2222
176
  */
2223
177
 
2224
- export { STACKCH_RTE_I18N_DE, STACKCH_RTE_I18N_FR, STACKCH_RTE_I18N_IT, StackchRichtextEditorConfig, StackchRichtextEditorI18n, StackchRichtextEditorMaterial, StackchRichtextEditorMaterialToolbar };
178
+ export { StackchRichtextEditorMaterial, StackchRichtextEditorMaterialToolbar };
2225
179
  //# sourceMappingURL=stackch-angular-material-richtext-editor.mjs.map