@stackch/angular-richtext-editor 1.1.0

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.
@@ -0,0 +1,2125 @@
1
+ import * as i0 from '@angular/core';
2
+ import { EventEmitter, Output, Input, Component, forwardRef, HostListener, ViewChild } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
+
6
+ class StackchRichtextEditorToolbar {
7
+ // State/config from parent editor
8
+ cfg;
9
+ i18n;
10
+ disabled = false;
11
+ fonts = [];
12
+ fontSizes = [];
13
+ isBoldActive = false;
14
+ isItalicActive = false;
15
+ isUnderlineActive = false;
16
+ canUndo = false;
17
+ canRedo = false;
18
+ // Consolidated UI state
19
+ uiState;
20
+ // Actions -> bubble to parent editor
21
+ undo = new EventEmitter();
22
+ redo = new EventEmitter();
23
+ toggleFontPanel = new EventEmitter();
24
+ toggleHeadingMenu = new EventEmitter();
25
+ toggleSpacingMenu = new EventEmitter();
26
+ toggleAlignMenu = new EventEmitter();
27
+ toggleColorMenu = new EventEmitter();
28
+ toggleListMenu = new EventEmitter();
29
+ pickFont = new EventEmitter();
30
+ pickFontSize = new EventEmitter();
31
+ pickAlign = new EventEmitter();
32
+ pickList = new EventEmitter();
33
+ pickHeading = new EventEmitter();
34
+ pickSpacing = new EventEmitter();
35
+ applyColor = new EventEmitter();
36
+ applyHighlight = new EventEmitter();
37
+ toggleBold = new EventEmitter();
38
+ toggleItalic = new EventEmitter();
39
+ toggleUnderline = new EventEmitter();
40
+ insertLink = new EventEmitter();
41
+ removeFormat = new EventEmitter();
42
+ // Ask parent to preserve selection before toolbar interactions (mousedown/pointerdown/focus)
43
+ saveSelectionRequest = new EventEmitter();
44
+ onToolbarMouseDown(evt) {
45
+ // Prevent focus stealing and keep selection in the editor
46
+ evt.preventDefault();
47
+ evt.stopPropagation();
48
+ this.saveSelectionRequest.emit();
49
+ }
50
+ onColorPointerDown(evt) {
51
+ // Do not preventDefault to allow the color picker to open, just preserve selection
52
+ evt.stopPropagation();
53
+ this.saveSelectionRequest.emit();
54
+ }
55
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorToolbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
56
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: StackchRichtextEditorToolbar, isStandalone: true, selector: "stackch-richtext-editor-toolbar", inputs: { cfg: "cfg", i18n: "i18n", disabled: "disabled", fonts: "fonts", fontSizes: "fontSizes", isBoldActive: "isBoldActive", isItalicActive: "isItalicActive", isUnderlineActive: "isUnderlineActive", canUndo: "canUndo", canRedo: "canRedo", uiState: "uiState" }, outputs: { undo: "undo", redo: "redo", toggleFontPanel: "toggleFontPanel", toggleHeadingMenu: "toggleHeadingMenu", toggleSpacingMenu: "toggleSpacingMenu", toggleAlignMenu: "toggleAlignMenu", toggleColorMenu: "toggleColorMenu", toggleListMenu: "toggleListMenu", pickFont: "pickFont", pickFontSize: "pickFontSize", pickAlign: "pickAlign", pickList: "pickList", pickHeading: "pickHeading", pickSpacing: "pickSpacing", applyColor: "applyColor", applyHighlight: "applyHighlight", toggleBold: "toggleBold", toggleItalic: "toggleItalic", toggleUnderline: "toggleUnderline", insertLink: "insertLink", removeFormat: "removeFormat", saveSelectionRequest: "saveSelectionRequest" }, ngImport: i0, template: "<div class=\"stackch_rte__toolbar\">\r\n <!-- Undo / Redo -->\r\n @if (cfg.showUndoRedo) {\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"undo.emit()\" [disabled]=\"disabled || !canUndo\" [title]=\"i18n.undoTitle\">\u21B6</button>\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"redo.emit()\" [disabled]=\"disabled || !canRedo\" [title]=\"i18n.redoTitle\">\u21B7</button>\r\n <span class=\"stackch_rte__sep\"></span>\r\n }\r\n\r\n <!-- Schrift & Gr\u00F6\u00DFe (kombinierter Layer) -->\r\n @if (cfg.showFontPanel) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleFontPanel.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.fontPanelTitle\">A\u1D43 \u25BE</button>\r\n @if (uiState?.showFontPanel) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--grid\" (mousedown)=\"$event.stopPropagation()\">\r\n <div class=\"stackch_rte__menu-section\">\r\n <div class=\"stackch_rte__menu-title\">{{ i18n.fontSectionTitle }}</div>\r\n @for (f of fonts; track f) {\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [style.fontFamily]=\"f\" (click)=\"pickFont.emit(f)\">{{ f }}</button>\r\n }\r\n </div>\r\n <div class=\"stackch_rte__menu-section\">\r\n <div class=\"stackch_rte__menu-title\">{{ i18n.sizeSectionTitle }}</div>\r\n @for (s of fontSizes; track s) {\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickFontSize.emit(s)\">{{ s }}px</button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- \u00DCberschriften -->\r\n @if (cfg.showHeading) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleHeadingMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.headingTitle\">H \u25BE</button>\r\n @if (uiState?.showHeadingMenu) {\r\n <div class=\"stackch_rte__menu\" (mousedown)=\"$event.stopPropagation()\">\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('p')\">{{ i18n.paragraphLabel }}</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('pre')\"><span style=\"font-family:monospace;\">{{ i18n.codeLabel }}</span></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h1')\"><h1 style=\"display:inline; margin:0;\">{{ i18n.h1Label }}</h1></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h2')\"><h2 style=\"display:inline; margin:0;\">{{ i18n.h2Label }}</h2></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h3')\"><h3 style=\"display:inline; margin:0;\">{{ i18n.h3Label }}</h3></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h4')\"><h4 style=\"display:inline; margin:0;\">{{ i18n.h4Label }}</h4></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h5')\"><h5 style=\"display:inline; margin:0;\">{{ i18n.h5Label }}</h5></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h6')\"><h6 style=\"display:inline; margin:0;\">{{ i18n.h6Label }}</h6></button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Spacing (Margin/Padding) -->\r\n @if (cfg.showSpacing) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleSpacingMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.spacingTitle\">M/P \u25BE</button>\r\n @if (uiState?.showSpacingMenu) {\r\n <div class=\"stackch_rte__menu\" (mousedown)=\"$event.stopPropagation()\">\r\n <div class=\"stackch_rte__menu-title\">{{ i18n.marginTitle }}</div>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:0})\">0</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:4})\">4px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:8})\">8px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:12})\">12px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'vertical',value:16})\">Vert 16px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'horizontal',value:16})\">Horiz 16px</button>\r\n <div class=\"stackch_rte__menu-title\" style=\"margin-top:.25rem;\">{{ i18n.paddingTitle }}</div>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:0})\">0</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:4})\">4px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:8})\">8px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:12})\">12px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'vertical',value:16})\">Vert 16px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'horizontal',value:16})\">Horiz 16px</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Farben (Dropdown horizontal) -->\r\n @if (cfg.showColor) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleColorMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.colorsTitle\">\uD83C\uDFA8 \u25BE</button>\r\n @if (uiState?.showColorMenu) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--row\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" [title]=\"i18n.colorMenuTitle\">\r\n <label class=\"stackch_rte__color\" (mousedown)=\"$event.stopPropagation(); onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\">\r\n <input type=\"color\" (mousedown)=\"onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\" (focus)=\"saveSelectionRequest.emit()\" (input)=\"applyColor.emit($any($event.target).value)\" (change)=\"applyColor.emit($any($event.target).value)\" [disabled]=\"disabled\" [title]=\"i18n.textColorTitle\" />\r\n A\r\n </label>\r\n <label class=\"stackch_rte__color\" [title]=\"i18n.highlightTitle\" (mousedown)=\"$event.stopPropagation(); onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\">\r\n <input type=\"color\" (mousedown)=\"onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\" (focus)=\"saveSelectionRequest.emit()\" (input)=\"applyHighlight.emit($any($event.target).value)\" (change)=\"applyHighlight.emit($any($event.target).value)\" [disabled]=\"disabled\" />\r\n \u29C9\r\n </label>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Stil-Buttons -->\r\n @if (cfg.showBold) {<button type=\"button\" class=\"stackch_rte__btn\" [class.is-active]=\"isBoldActive\" (mousedown)=\"onToolbarMouseDown($event)\" (click)=\"toggleBold.emit()\" [disabled]=\"disabled\" aria-pressed=\"{{isBoldActive}}\" [title]=\"i18n.boldTitle\"><b>B</b></button>}\r\n @if (cfg.showItalic) {<button type=\"button\" class=\"stackch_rte__btn\" [class.is-active]=\"isItalicActive\" (mousedown)=\"onToolbarMouseDown($event)\" (click)=\"toggleItalic.emit()\" [disabled]=\"disabled\" aria-pressed=\"{{isItalicActive}}\" [title]=\"i18n.italicTitle\"><i>I</i></button>}\r\n @if (cfg.showUnderline) {<button type=\"button\" class=\"stackch_rte__btn\" [class.is-active]=\"isUnderlineActive\" (mousedown)=\"onToolbarMouseDown($event)\" (click)=\"toggleUnderline.emit()\" [disabled]=\"disabled\" aria-pressed=\"{{isUnderlineActive}}\" [title]=\"i18n.underlineTitle\"><u>U</u></button>}\r\n <span class=\"stackch_rte__sep\"></span>\r\n\r\n @if (cfg.showLists) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleListMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.listsTitle\">\u2630 \u25BE</button>\r\n @if (uiState?.showListMenu) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--row\" (mousedown)=\"$event.stopPropagation()\">\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.bulletListTitle\" (mousedown)=\"saveSelectionRequest.emit()\" (click)=\"pickList.emit('ul')\">{{ i18n.bulletListTitle }}</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.numberedListTitle\" (mousedown)=\"saveSelectionRequest.emit()\" (click)=\"pickList.emit('ol')\">{{ i18n.numberedListTitle }}</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n <span class=\"stackch_rte__sep\"></span>\r\n\r\n @if (cfg.showAlign) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleAlignMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.alignTitle\">\u2261 \u25BE</button>\r\n @if (uiState?.showAlignMenu) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--row\" (mousedown)=\"$event.stopPropagation()\">\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignLeftTitle\" (click)=\"pickAlign.emit('left')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"2\" y1=\"4\" x2=\"14\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"9\" x2=\"12\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"14\" x2=\"16\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignCenterTitle\" (click)=\"pickAlign.emit('center')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"3\" y1=\"4\" x2=\"15\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"5\" y1=\"9\" x2=\"13\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"3\" y1=\"14\" x2=\"15\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignRightTitle\" (click)=\"pickAlign.emit('right')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"4\" y1=\"4\" x2=\"16\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"6\" y1=\"9\" x2=\"16\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"14\" x2=\"16\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignJustifyTitle\" (click)=\"pickAlign.emit('justify')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"2\" y1=\"4\" x2=\"16\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"9\" x2=\"16\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"14\" x2=\"16\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n <span class=\"stackch_rte__sep\"></span>\r\n\r\n @if (cfg.showLink) {<button type=\"button\" class=\"stackch_rte__btn\" (click)=\"insertLink.emit()\" [disabled]=\"disabled\" [title]=\"i18n.linkTitle\">\uD83D\uDD17</button>}\r\n @if (cfg.showRemoveFormat) {<button type=\"button\" class=\"stackch_rte__btn\" (click)=\"removeFormat.emit()\" [disabled]=\"disabled\" [title]=\"i18n.removeFormatTitle\">Tx</button>}\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 }] });
57
+ }
58
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorToolbar, decorators: [{
59
+ type: Component,
60
+ args: [{ selector: 'stackch-richtext-editor-toolbar', standalone: true, imports: [CommonModule], template: "<div class=\"stackch_rte__toolbar\">\r\n <!-- Undo / Redo -->\r\n @if (cfg.showUndoRedo) {\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"undo.emit()\" [disabled]=\"disabled || !canUndo\" [title]=\"i18n.undoTitle\">\u21B6</button>\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"redo.emit()\" [disabled]=\"disabled || !canRedo\" [title]=\"i18n.redoTitle\">\u21B7</button>\r\n <span class=\"stackch_rte__sep\"></span>\r\n }\r\n\r\n <!-- Schrift & Gr\u00F6\u00DFe (kombinierter Layer) -->\r\n @if (cfg.showFontPanel) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleFontPanel.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.fontPanelTitle\">A\u1D43 \u25BE</button>\r\n @if (uiState?.showFontPanel) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--grid\" (mousedown)=\"$event.stopPropagation()\">\r\n <div class=\"stackch_rte__menu-section\">\r\n <div class=\"stackch_rte__menu-title\">{{ i18n.fontSectionTitle }}</div>\r\n @for (f of fonts; track f) {\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [style.fontFamily]=\"f\" (click)=\"pickFont.emit(f)\">{{ f }}</button>\r\n }\r\n </div>\r\n <div class=\"stackch_rte__menu-section\">\r\n <div class=\"stackch_rte__menu-title\">{{ i18n.sizeSectionTitle }}</div>\r\n @for (s of fontSizes; track s) {\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickFontSize.emit(s)\">{{ s }}px</button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- \u00DCberschriften -->\r\n @if (cfg.showHeading) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleHeadingMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.headingTitle\">H \u25BE</button>\r\n @if (uiState?.showHeadingMenu) {\r\n <div class=\"stackch_rte__menu\" (mousedown)=\"$event.stopPropagation()\">\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('p')\">{{ i18n.paragraphLabel }}</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('pre')\"><span style=\"font-family:monospace;\">{{ i18n.codeLabel }}</span></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h1')\"><h1 style=\"display:inline; margin:0;\">{{ i18n.h1Label }}</h1></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h2')\"><h2 style=\"display:inline; margin:0;\">{{ i18n.h2Label }}</h2></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h3')\"><h3 style=\"display:inline; margin:0;\">{{ i18n.h3Label }}</h3></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h4')\"><h4 style=\"display:inline; margin:0;\">{{ i18n.h4Label }}</h4></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h5')\"><h5 style=\"display:inline; margin:0;\">{{ i18n.h5Label }}</h5></button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickHeading.emit('h6')\"><h6 style=\"display:inline; margin:0;\">{{ i18n.h6Label }}</h6></button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Spacing (Margin/Padding) -->\r\n @if (cfg.showSpacing) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleSpacingMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.spacingTitle\">M/P \u25BE</button>\r\n @if (uiState?.showSpacingMenu) {\r\n <div class=\"stackch_rte__menu\" (mousedown)=\"$event.stopPropagation()\">\r\n <div class=\"stackch_rte__menu-title\">{{ i18n.marginTitle }}</div>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:0})\">0</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:4})\">4px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:8})\">8px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'all',value:12})\">12px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'vertical',value:16})\">Vert 16px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'margin',target:'horizontal',value:16})\">Horiz 16px</button>\r\n <div class=\"stackch_rte__menu-title\" style=\"margin-top:.25rem;\">{{ i18n.paddingTitle }}</div>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:0})\">0</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:4})\">4px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:8})\">8px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'all',value:12})\">12px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'vertical',value:16})\">Vert 16px</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" (click)=\"pickSpacing.emit({kind:'padding',target:'horizontal',value:16})\">Horiz 16px</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Farben (Dropdown horizontal) -->\r\n @if (cfg.showColor) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleColorMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.colorsTitle\">\uD83C\uDFA8 \u25BE</button>\r\n @if (uiState?.showColorMenu) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--row\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" [title]=\"i18n.colorMenuTitle\">\r\n <label class=\"stackch_rte__color\" (mousedown)=\"$event.stopPropagation(); onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\">\r\n <input type=\"color\" (mousedown)=\"onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\" (focus)=\"saveSelectionRequest.emit()\" (input)=\"applyColor.emit($any($event.target).value)\" (change)=\"applyColor.emit($any($event.target).value)\" [disabled]=\"disabled\" [title]=\"i18n.textColorTitle\" />\r\n A\r\n </label>\r\n <label class=\"stackch_rte__color\" [title]=\"i18n.highlightTitle\" (mousedown)=\"$event.stopPropagation(); onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\">\r\n <input type=\"color\" (mousedown)=\"onColorPointerDown($event)\" (click)=\"$event.stopPropagation()\" (pointerdown)=\"onColorPointerDown($event)\" (touchstart)=\"onColorPointerDown($event)\" (focus)=\"saveSelectionRequest.emit()\" (input)=\"applyHighlight.emit($any($event.target).value)\" (change)=\"applyHighlight.emit($any($event.target).value)\" [disabled]=\"disabled\" />\r\n \u29C9\r\n </label>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Stil-Buttons -->\r\n @if (cfg.showBold) {<button type=\"button\" class=\"stackch_rte__btn\" [class.is-active]=\"isBoldActive\" (mousedown)=\"onToolbarMouseDown($event)\" (click)=\"toggleBold.emit()\" [disabled]=\"disabled\" aria-pressed=\"{{isBoldActive}}\" [title]=\"i18n.boldTitle\"><b>B</b></button>}\r\n @if (cfg.showItalic) {<button type=\"button\" class=\"stackch_rte__btn\" [class.is-active]=\"isItalicActive\" (mousedown)=\"onToolbarMouseDown($event)\" (click)=\"toggleItalic.emit()\" [disabled]=\"disabled\" aria-pressed=\"{{isItalicActive}}\" [title]=\"i18n.italicTitle\"><i>I</i></button>}\r\n @if (cfg.showUnderline) {<button type=\"button\" class=\"stackch_rte__btn\" [class.is-active]=\"isUnderlineActive\" (mousedown)=\"onToolbarMouseDown($event)\" (click)=\"toggleUnderline.emit()\" [disabled]=\"disabled\" aria-pressed=\"{{isUnderlineActive}}\" [title]=\"i18n.underlineTitle\"><u>U</u></button>}\r\n <span class=\"stackch_rte__sep\"></span>\r\n\r\n @if (cfg.showLists) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleListMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.listsTitle\">\u2630 \u25BE</button>\r\n @if (uiState?.showListMenu) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--row\" (mousedown)=\"$event.stopPropagation()\">\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.bulletListTitle\" (mousedown)=\"saveSelectionRequest.emit()\" (click)=\"pickList.emit('ul')\">{{ i18n.bulletListTitle }}</button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.numberedListTitle\" (mousedown)=\"saveSelectionRequest.emit()\" (click)=\"pickList.emit('ol')\">{{ i18n.numberedListTitle }}</button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n <span class=\"stackch_rte__sep\"></span>\r\n\r\n @if (cfg.showAlign) {\r\n <div class=\"stackch_rte__dropdown\">\r\n <button type=\"button\" class=\"stackch_rte__btn\" (click)=\"toggleAlignMenu.emit($event)\" [disabled]=\"disabled\" [title]=\"i18n.alignTitle\">\u2261 \u25BE</button>\r\n @if (uiState?.showAlignMenu) {\r\n <div class=\"stackch_rte__menu stackch_rte__menu--row\" (mousedown)=\"$event.stopPropagation()\">\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignLeftTitle\" (click)=\"pickAlign.emit('left')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"2\" y1=\"4\" x2=\"14\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"9\" x2=\"12\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"14\" x2=\"16\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignCenterTitle\" (click)=\"pickAlign.emit('center')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"3\" y1=\"4\" x2=\"15\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"5\" y1=\"9\" x2=\"13\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"3\" y1=\"14\" x2=\"15\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignRightTitle\" (click)=\"pickAlign.emit('right')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"4\" y1=\"4\" x2=\"16\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"6\" y1=\"9\" x2=\"16\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"14\" x2=\"16\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n <button type=\"button\" class=\"stackch_rte__menu-item\" [title]=\"i18n.alignJustifyTitle\" (click)=\"pickAlign.emit('justify')\">\r\n <svg viewBox=\"0 0 18 18\" width=\"18\" height=\"18\" aria-hidden=\"true\">\r\n <line x1=\"2\" y1=\"4\" x2=\"16\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"9\" x2=\"16\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n <line x1=\"2\" y1=\"14\" x2=\"16\" y2=\"14\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"/>\r\n </svg>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n <span class=\"stackch_rte__sep\"></span>\r\n\r\n @if (cfg.showLink) {<button type=\"button\" class=\"stackch_rte__btn\" (click)=\"insertLink.emit()\" [disabled]=\"disabled\" [title]=\"i18n.linkTitle\">\uD83D\uDD17</button>}\r\n @if (cfg.showRemoveFormat) {<button type=\"button\" class=\"stackch_rte__btn\" (click)=\"removeFormat.emit()\" [disabled]=\"disabled\" [title]=\"i18n.removeFormatTitle\">Tx</button>}\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"] }]
61
+ }], propDecorators: { cfg: [{
62
+ type: Input
63
+ }], i18n: [{
64
+ type: Input
65
+ }], disabled: [{
66
+ type: Input
67
+ }], fonts: [{
68
+ type: Input
69
+ }], fontSizes: [{
70
+ type: Input
71
+ }], isBoldActive: [{
72
+ type: Input
73
+ }], isItalicActive: [{
74
+ type: Input
75
+ }], isUnderlineActive: [{
76
+ type: Input
77
+ }], canUndo: [{
78
+ type: Input
79
+ }], canRedo: [{
80
+ type: Input
81
+ }], uiState: [{
82
+ type: Input
83
+ }], undo: [{
84
+ type: Output
85
+ }], redo: [{
86
+ type: Output
87
+ }], toggleFontPanel: [{
88
+ type: Output
89
+ }], toggleHeadingMenu: [{
90
+ type: Output
91
+ }], toggleSpacingMenu: [{
92
+ type: Output
93
+ }], toggleAlignMenu: [{
94
+ type: Output
95
+ }], toggleColorMenu: [{
96
+ type: Output
97
+ }], toggleListMenu: [{
98
+ type: Output
99
+ }], pickFont: [{
100
+ type: Output
101
+ }], pickFontSize: [{
102
+ type: Output
103
+ }], pickAlign: [{
104
+ type: Output
105
+ }], pickList: [{
106
+ type: Output
107
+ }], pickHeading: [{
108
+ type: Output
109
+ }], pickSpacing: [{
110
+ type: Output
111
+ }], applyColor: [{
112
+ type: Output
113
+ }], applyHighlight: [{
114
+ type: Output
115
+ }], toggleBold: [{
116
+ type: Output
117
+ }], toggleItalic: [{
118
+ type: Output
119
+ }], toggleUnderline: [{
120
+ type: Output
121
+ }], insertLink: [{
122
+ type: Output
123
+ }], removeFormat: [{
124
+ type: Output
125
+ }], saveSelectionRequest: [{
126
+ type: Output
127
+ }] } });
128
+
129
+ class StackchRichtextEditorConfig {
130
+ // Sichtbarkeit einzelner Toolbar-Elemente (Default: an)
131
+ showUndoRedo = true;
132
+ showFontPanel = true;
133
+ showHeading = true;
134
+ showSpacing = true;
135
+ showColor = true;
136
+ showBold = true;
137
+ showItalic = true;
138
+ showUnderline = true;
139
+ showLists = true;
140
+ showAlign = true;
141
+ showLink = true;
142
+ showRemoveFormat = true;
143
+ // i18n overrides (partial), default is English
144
+ i18n;
145
+ }
146
+ 1;
147
+ class StackchRichtextEditorI18n {
148
+ // Generic
149
+ placeholder = 'Write…';
150
+ // Toolbar titles
151
+ undoTitle = 'Undo (Ctrl+Z)';
152
+ redoTitle = 'Redo (Ctrl+Y)';
153
+ fontPanelTitle = 'Font & Size';
154
+ fontSectionTitle = 'Font';
155
+ sizeSectionTitle = 'Size';
156
+ headingTitle = 'Heading';
157
+ spacingTitle = 'Spacing (Margin/Padding)';
158
+ marginTitle = 'Margin';
159
+ paddingTitle = 'Padding';
160
+ colorsTitle = 'Colors';
161
+ colorMenuTitle = 'Pick a color';
162
+ textColorTitle = 'Text color';
163
+ highlightTitle = 'Highlight';
164
+ boldTitle = 'Bold';
165
+ italicTitle = 'Italic';
166
+ underlineTitle = 'Underline';
167
+ listsTitle = 'Lists';
168
+ bulletListTitle = 'Bullet list';
169
+ numberedListTitle = 'Numbered list';
170
+ alignTitle = 'Alignment';
171
+ alignLeftTitle = 'Left';
172
+ alignCenterTitle = 'Center';
173
+ alignRightTitle = 'Right';
174
+ alignJustifyTitle = 'Justify';
175
+ linkTitle = 'Link';
176
+ removeFormatTitle = 'Remove format';
177
+ // Heading menu labels
178
+ paragraphLabel = 'Paragraph (P)';
179
+ codeLabel = 'Code (pre)';
180
+ h1Label = 'H1';
181
+ h2Label = 'H2';
182
+ h3Label = 'H3';
183
+ h4Label = 'H4';
184
+ h5Label = 'H5';
185
+ h6Label = 'H6';
186
+ }
187
+ // Predefined i18n bundles
188
+ // Consumers can import these and pass via config.i18n to localize the toolbar/labels.
189
+ const STACKCH_RTE_I18N_DE = {
190
+ // Generic
191
+ placeholder: 'Schreiben…',
192
+ // Toolbar titles
193
+ undoTitle: 'Rückgängig (Strg+Z)',
194
+ redoTitle: 'Wiederholen (Strg+Y)',
195
+ fontPanelTitle: 'Schrift & Größe',
196
+ fontSectionTitle: 'Schrift',
197
+ sizeSectionTitle: 'Größe',
198
+ headingTitle: 'Überschrift',
199
+ spacingTitle: 'Abstand (Außen/Innen)',
200
+ marginTitle: 'Außenabstand',
201
+ paddingTitle: 'Innenabstand',
202
+ colorsTitle: 'Farben',
203
+ colorMenuTitle: 'Farbe wählen',
204
+ textColorTitle: 'Textfarbe',
205
+ highlightTitle: 'Hervorheben',
206
+ boldTitle: 'Fett',
207
+ italicTitle: 'Kursiv',
208
+ underlineTitle: 'Unterstreichen',
209
+ listsTitle: 'Listen',
210
+ bulletListTitle: 'Aufzählung',
211
+ numberedListTitle: 'Nummeriert',
212
+ alignTitle: 'Ausrichtung',
213
+ alignLeftTitle: 'Links',
214
+ alignCenterTitle: 'Zentriert',
215
+ alignRightTitle: 'Rechts',
216
+ alignJustifyTitle: 'Blocksatz',
217
+ linkTitle: 'Link',
218
+ removeFormatTitle: 'Formatierung entfernen',
219
+ // Heading menu labels
220
+ paragraphLabel: 'Absatz (P)',
221
+ codeLabel: 'Code (pre)',
222
+ h1Label: 'H1',
223
+ h2Label: 'H2',
224
+ h3Label: 'H3',
225
+ h4Label: 'H4',
226
+ h5Label: 'H5',
227
+ h6Label: 'H6',
228
+ };
229
+ const STACKCH_RTE_I18N_FR = {
230
+ // Generic
231
+ placeholder: 'Écrire…',
232
+ // Toolbar titles
233
+ undoTitle: 'Annuler (Ctrl+Z)',
234
+ redoTitle: 'Rétablir (Ctrl+Y)',
235
+ fontPanelTitle: 'Police et taille',
236
+ fontSectionTitle: 'Police',
237
+ sizeSectionTitle: 'Taille',
238
+ headingTitle: 'Titre',
239
+ spacingTitle: 'Espacement (Marge/Remplissage)',
240
+ marginTitle: 'Marge',
241
+ paddingTitle: 'Remplissage',
242
+ colorsTitle: 'Couleurs',
243
+ colorMenuTitle: 'Choisir une couleur',
244
+ textColorTitle: 'Couleur du texte',
245
+ highlightTitle: 'Surligner',
246
+ boldTitle: 'Gras',
247
+ italicTitle: 'Italique',
248
+ underlineTitle: 'Souligné',
249
+ listsTitle: 'Listes',
250
+ bulletListTitle: 'Liste à puces',
251
+ numberedListTitle: 'Liste numérotée',
252
+ alignTitle: 'Alignement',
253
+ alignLeftTitle: 'Gauche',
254
+ alignCenterTitle: 'Centré',
255
+ alignRightTitle: 'Droite',
256
+ alignJustifyTitle: 'Justifié',
257
+ linkTitle: 'Lien',
258
+ removeFormatTitle: 'Effacer la mise en forme',
259
+ // Heading menu labels
260
+ paragraphLabel: 'Paragraphe (P)',
261
+ codeLabel: 'Code (pre)',
262
+ h1Label: 'H1',
263
+ h2Label: 'H2',
264
+ h3Label: 'H3',
265
+ h4Label: 'H4',
266
+ h5Label: 'H5',
267
+ h6Label: 'H6',
268
+ };
269
+ const STACKCH_RTE_I18N_IT = {
270
+ // Generic
271
+ placeholder: 'Scrivi…',
272
+ // Toolbar titles
273
+ undoTitle: 'Annulla (Ctrl+Z)',
274
+ redoTitle: 'Ripristina (Ctrl+Y)',
275
+ fontPanelTitle: 'Carattere e dimensione',
276
+ fontSectionTitle: 'Carattere',
277
+ sizeSectionTitle: 'Dimensione',
278
+ headingTitle: 'Titolo',
279
+ spacingTitle: 'Spaziatura (Margine/Riempimento)',
280
+ marginTitle: 'Margine',
281
+ paddingTitle: 'Riempimento',
282
+ colorsTitle: 'Colori',
283
+ colorMenuTitle: 'Scegli un colore',
284
+ textColorTitle: 'Colore del testo',
285
+ highlightTitle: 'Evidenzia',
286
+ boldTitle: 'Grassetto',
287
+ italicTitle: 'Corsivo',
288
+ underlineTitle: 'Sottolineato',
289
+ listsTitle: 'Elenchi',
290
+ bulletListTitle: 'Elenco puntato',
291
+ numberedListTitle: 'Elenco numerato',
292
+ alignTitle: 'Allineamento',
293
+ alignLeftTitle: 'Sinistra',
294
+ alignCenterTitle: 'Centrato',
295
+ alignRightTitle: 'Destra',
296
+ alignJustifyTitle: 'Giustificato',
297
+ linkTitle: 'Collegamento',
298
+ removeFormatTitle: 'Rimuovi formattazione',
299
+ // Heading menu labels
300
+ paragraphLabel: 'Paragrafo (P)',
301
+ codeLabel: 'Codice (pre)',
302
+ h1Label: 'H1',
303
+ h2Label: 'H2',
304
+ h3Label: 'H3',
305
+ h4Label: 'H4',
306
+ h5Label: 'H5',
307
+ h6Label: 'H6',
308
+ };
309
+ class StackchRichtextEditor {
310
+ cdr;
311
+ placeholder = '';
312
+ showToolbar = true;
313
+ fonts = [
314
+ 'Arial, Helvetica, sans-serif',
315
+ 'Georgia, serif',
316
+ 'Times New Roman, Times, serif',
317
+ 'Trebuchet MS, sans-serif',
318
+ 'Verdana, Geneva, sans-serif',
319
+ 'Courier New, Courier, monospace',
320
+ 'Monaco, monospace'
321
+ ];
322
+ fontSizes = [12, 14, 16, 18, 20, 24, 28, 32];
323
+ height;
324
+ minHeight = 160;
325
+ maxHeight;
326
+ set disabled(value) { this._disabled = value; }
327
+ get disabled() { return this._disabled; }
328
+ _disabled = false;
329
+ valueChange = new EventEmitter();
330
+ editorRef;
331
+ constructor(cdr) {
332
+ this.cdr = cdr;
333
+ }
334
+ // Toolbar-Konfiguration (default: alles an). Wir halten die übergebene Referenz
335
+ // und mergen Defaults im Getter, damit auch Eigenschaftsänderungen via ngModel sofort wirken.
336
+ config;
337
+ get cfg() {
338
+ return Object.assign(new StackchRichtextEditorConfig(), this.config || {});
339
+ }
340
+ get i18n() {
341
+ return Object.assign(new StackchRichtextEditorI18n(), this.cfg.i18n || {});
342
+ }
343
+ onChange = () => { };
344
+ onTouched = () => { };
345
+ savedRange = null;
346
+ // History (Undo/Redo)
347
+ history = [];
348
+ historyIndex = -1;
349
+ isRestoringHistory = false;
350
+ snapshotTimer = null;
351
+ // UI state for compact dropdowns / panel
352
+ showFontMenu = false;
353
+ showSizeMenu = false;
354
+ showFontPanel = false;
355
+ showHeadingMenu = false;
356
+ showSpacingMenu = false;
357
+ showAlignMenu = false;
358
+ showColorMenu = false;
359
+ showListMenu = false;
360
+ // Inline state flags for active buttons
361
+ isBoldActive = false;
362
+ isItalicActive = false;
363
+ isUnderlineActive = false;
364
+ // Selection helpers
365
+ saveSelection() {
366
+ const sel = window.getSelection();
367
+ if (!sel || sel.rangeCount === 0)
368
+ return;
369
+ const range = sel.getRangeAt(0);
370
+ if (!this.isRangeInEditor(range))
371
+ return;
372
+ this.savedRange = range.cloneRange();
373
+ this.updateInlineStates();
374
+ }
375
+ // Prevent toolbar buttons from stealing focus from the editor while preserving selection
376
+ onToolbarMouseDown(evt) {
377
+ // Keep focus on the editor to avoid persistent button focus outlines
378
+ evt.preventDefault();
379
+ evt.stopPropagation();
380
+ this.saveSelection();
381
+ }
382
+ // Beim Öffnen des Color-Pickers: Selektion sichern
383
+ onColorPointerDown(_evt) {
384
+ this.saveSelection();
385
+ }
386
+ restoreSelection() {
387
+ const sel = window.getSelection();
388
+ if (!sel || !this.savedRange)
389
+ return;
390
+ if (!this.isRangeInEditor(this.savedRange))
391
+ return;
392
+ sel.removeAllRanges();
393
+ sel.addRange(this.savedRange);
394
+ }
395
+ isRangeInEditor(range) {
396
+ const editor = this.editorRef?.nativeElement;
397
+ if (!editor)
398
+ return false;
399
+ const container = range.commonAncestorContainer;
400
+ const node = container.nodeType === Node.ELEMENT_NODE ? container : container.parentElement;
401
+ return !!node && editor.contains(node);
402
+ }
403
+ // Update active-state on selection changes inside the editor
404
+ onSelectionChange() {
405
+ const sel = window.getSelection();
406
+ if (!sel || sel.rangeCount === 0)
407
+ return;
408
+ const range = sel.getRangeAt(0);
409
+ if (!this.isRangeInEditor(range))
410
+ return;
411
+ this.updateInlineStates();
412
+ }
413
+ // Close dropdowns when clicking anywhere in the document
414
+ closeMenus() {
415
+ this.showFontMenu = false;
416
+ this.showSizeMenu = false;
417
+ this.showFontPanel = false;
418
+ this.showHeadingMenu = false;
419
+ this.showSpacingMenu = false;
420
+ this.showAlignMenu = false;
421
+ this.showColorMenu = false;
422
+ this.showListMenu = false;
423
+ }
424
+ // Keyboard: Undo/Redo
425
+ onKeydown(evt) {
426
+ const isMac = navigator.platform.toLowerCase().includes('mac');
427
+ const mod = isMac ? evt.metaKey : evt.ctrlKey;
428
+ if (mod && !evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')) {
429
+ evt.preventDefault();
430
+ this.undo();
431
+ }
432
+ else if (mod && (evt.key === 'y' || evt.key === 'Y' || (evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')))) {
433
+ evt.preventDefault();
434
+ this.redo();
435
+ }
436
+ }
437
+ toggleFontMenu(evt) {
438
+ this.saveSelection();
439
+ this.showFontMenu = !this.showFontMenu;
440
+ if (this.showFontMenu)
441
+ this.showSizeMenu = false;
442
+ evt.stopPropagation();
443
+ }
444
+ toggleSizeMenu(evt) {
445
+ this.saveSelection();
446
+ this.showSizeMenu = !this.showSizeMenu;
447
+ if (this.showSizeMenu)
448
+ this.showFontMenu = false;
449
+ evt.stopPropagation();
450
+ }
451
+ toggleFontPanel(evt) {
452
+ this.saveSelection();
453
+ this.showFontPanel = !this.showFontPanel;
454
+ // close others if open
455
+ if (this.showFontPanel) {
456
+ this.showFontMenu = false;
457
+ this.showSizeMenu = false;
458
+ this.showHeadingMenu = false;
459
+ this.showSpacingMenu = false;
460
+ this.showAlignMenu = false;
461
+ this.showColorMenu = false;
462
+ }
463
+ evt.stopPropagation();
464
+ }
465
+ toggleHeadingMenu(evt) {
466
+ this.saveSelection();
467
+ this.showHeadingMenu = !this.showHeadingMenu;
468
+ if (this.showHeadingMenu) {
469
+ this.showFontMenu = false;
470
+ this.showSizeMenu = false;
471
+ this.showFontPanel = false;
472
+ this.showSpacingMenu = false;
473
+ this.showAlignMenu = false;
474
+ this.showColorMenu = false;
475
+ }
476
+ evt.stopPropagation();
477
+ }
478
+ toggleSpacingMenu(evt) {
479
+ this.saveSelection();
480
+ this.showSpacingMenu = !this.showSpacingMenu;
481
+ if (this.showSpacingMenu) {
482
+ this.showFontMenu = false;
483
+ this.showSizeMenu = false;
484
+ this.showFontPanel = false;
485
+ this.showHeadingMenu = false;
486
+ this.showAlignMenu = false;
487
+ this.showColorMenu = false;
488
+ }
489
+ evt.stopPropagation();
490
+ }
491
+ toggleAlignMenu(evt) {
492
+ this.saveSelection();
493
+ this.showAlignMenu = !this.showAlignMenu;
494
+ if (this.showAlignMenu) {
495
+ this.showFontMenu = false;
496
+ this.showSizeMenu = false;
497
+ this.showFontPanel = false;
498
+ this.showHeadingMenu = false;
499
+ this.showSpacingMenu = false;
500
+ this.showColorMenu = false;
501
+ this.showListMenu = false;
502
+ }
503
+ evt.stopPropagation();
504
+ }
505
+ toggleColorMenu(evt) {
506
+ this.saveSelection();
507
+ this.showColorMenu = !this.showColorMenu;
508
+ if (this.showColorMenu) {
509
+ this.showFontMenu = false;
510
+ this.showSizeMenu = false;
511
+ this.showFontPanel = false;
512
+ this.showHeadingMenu = false;
513
+ this.showSpacingMenu = false;
514
+ this.showAlignMenu = false;
515
+ this.showListMenu = false;
516
+ }
517
+ evt.stopPropagation();
518
+ }
519
+ toggleListMenu(evt) {
520
+ this.saveSelection();
521
+ this.showListMenu = !this.showListMenu;
522
+ if (this.showListMenu) {
523
+ this.showFontMenu = false;
524
+ this.showSizeMenu = false;
525
+ this.showFontPanel = false;
526
+ this.showHeadingMenu = false;
527
+ this.showSpacingMenu = false;
528
+ this.showAlignMenu = false;
529
+ this.showColorMenu = false;
530
+ }
531
+ evt.stopPropagation();
532
+ }
533
+ onPickAlign(where) {
534
+ this.focusEditor();
535
+ this.restoreSelection();
536
+ this.alignBlocks(where);
537
+ this.showAlignMenu = false;
538
+ this.emitValue();
539
+ this.takeSnapshot('align');
540
+ }
541
+ onPickFont(font) {
542
+ if (!font)
543
+ return;
544
+ this.focusEditor();
545
+ this.restoreSelection();
546
+ this.applyInlineStyle('fontFamily', font);
547
+ this.showFontMenu = false;
548
+ }
549
+ onPickFontSize(size) {
550
+ if (!Number.isFinite(size))
551
+ return;
552
+ this.focusEditor();
553
+ this.restoreSelection();
554
+ this.applyInlineStyle('fontSize', `${size}px`);
555
+ this.showSizeMenu = false;
556
+ }
557
+ onPickList(kind) {
558
+ this.focusEditor();
559
+ this.restoreSelection();
560
+ this.toggleList(kind);
561
+ this.showListMenu = false;
562
+ this.emitValue();
563
+ this.takeSnapshot('list');
564
+ }
565
+ onPickHeading(tag) {
566
+ this.focusEditor();
567
+ this.restoreSelection();
568
+ this.setHeading(tag);
569
+ this.showHeadingMenu = false;
570
+ this.emitValue();
571
+ this.takeSnapshot('heading');
572
+ }
573
+ onPickSpacing(kind, target, value) {
574
+ this.focusEditor();
575
+ this.restoreSelection();
576
+ // Bevorzugt: direkt auf die Selektion anwenden (inline Wrapper)
577
+ if (!this.applySpacingToSelection(kind, target, value)) {
578
+ // Fallback: Block-Spacing, wenn Auswahl blockübergreifend ist
579
+ this.setBlockSpacing(kind, target, value);
580
+ }
581
+ this.showSpacingMenu = false;
582
+ this.emitValue();
583
+ this.takeSnapshot('spacing');
584
+ }
585
+ // ControlValueAccessor
586
+ writeValue(value) {
587
+ const el = this.editorRef?.nativeElement;
588
+ if (!el)
589
+ return;
590
+ el.innerHTML = value || '';
591
+ // Initial snapshot nur einmal anlegen
592
+ if (this.history.length === 0) {
593
+ this.takeSnapshot('init');
594
+ }
595
+ }
596
+ registerOnChange(fn) { this.onChange = fn; }
597
+ registerOnTouched(fn) { this.onTouched = fn; }
598
+ setDisabledState(isDisabled) { this.disabled = isDisabled; }
599
+ // Toolbar actions using document.execCommand (deprecated but broadly supported)
600
+ cmd(command, value) {
601
+ // Restore last selection from editor before applying an action triggered by toolbar
602
+ this.focusEditor();
603
+ this.restoreSelection();
604
+ // Bevorzugt: eigene Range-basierte Implementierungen
605
+ switch (command) {
606
+ case 'bold':
607
+ this.applyInlineStyleSmart('fontWeight', 'bold');
608
+ return;
609
+ case 'italic':
610
+ this.applyInlineStyleSmart('fontStyle', 'italic');
611
+ return;
612
+ case 'underline':
613
+ this.applyInlineStyleSmart('textDecoration', 'underline');
614
+ return;
615
+ case 'foreColor':
616
+ this.applyInlineStyle('color', value || '');
617
+ return;
618
+ case 'hiliteColor':
619
+ case 'backColor':
620
+ this.applyInlineStyle('backgroundColor', value || '');
621
+ return;
622
+ case 'createLink':
623
+ if (value) {
624
+ this.wrapSelectionWith('a', { href: value });
625
+ this.emitValue();
626
+ this.takeSnapshot('link');
627
+ }
628
+ return;
629
+ case 'insertText': {
630
+ const text = value ?? '';
631
+ this.insertTextAtSelection(text);
632
+ this.emitValue();
633
+ this.takeSnapshot('insertText');
634
+ return;
635
+ }
636
+ case 'insertUnorderedList':
637
+ this.toggleList('ul');
638
+ this.emitValue();
639
+ this.takeSnapshot('list');
640
+ return;
641
+ case 'insertOrderedList':
642
+ this.toggleList('ol');
643
+ this.emitValue();
644
+ this.takeSnapshot('list');
645
+ return;
646
+ case 'justifyLeft':
647
+ this.alignBlocks('left');
648
+ this.emitValue();
649
+ this.takeSnapshot('align');
650
+ return;
651
+ case 'justifyCenter':
652
+ this.alignBlocks('center');
653
+ this.emitValue();
654
+ this.takeSnapshot('align');
655
+ return;
656
+ case 'justifyRight':
657
+ this.alignBlocks('right');
658
+ this.emitValue();
659
+ this.takeSnapshot('align');
660
+ return;
661
+ case 'justifyFull':
662
+ this.alignBlocks('justify');
663
+ this.emitValue();
664
+ this.takeSnapshot('align');
665
+ return;
666
+ }
667
+ // Fallback (deprecated): nur für Format entfernen / Links lösen als Übergang
668
+ try {
669
+ // Hinweis im Dev-Mode ausgeben
670
+ if (!('___rteWarned' in this)) {
671
+ console.warn('[richtext-editor] document.execCommand ist deprecated; Fallback wird nur für removeFormat/unlink verwendet.');
672
+ this.___rteWarned = true;
673
+ }
674
+ if (command === 'removeFormat' || command === 'unlink') {
675
+ document.execCommand(command, false, value);
676
+ }
677
+ }
678
+ catch {
679
+ // Ignorieren, wenn nicht unterstützt
680
+ }
681
+ this.emitValue();
682
+ this.takeSnapshot('fallback');
683
+ }
684
+ applyFont(font) {
685
+ if (!font)
686
+ return;
687
+ this.applyInlineStyle('fontFamily', font);
688
+ }
689
+ applyFontSize(sizePx) {
690
+ if (!sizePx)
691
+ return;
692
+ const size = Number(sizePx);
693
+ if (!Number.isFinite(size))
694
+ return;
695
+ this.applyInlineStyle('fontSize', `${size}px`);
696
+ }
697
+ applyColor(color) {
698
+ this.focusEditor();
699
+ this.restoreSelection();
700
+ const sel = window.getSelection();
701
+ if (!sel || sel.rangeCount === 0)
702
+ return;
703
+ const r = sel.getRangeAt(0);
704
+ if (r.collapsed) {
705
+ // Firefox/Edge: wenn keine explizite Auswahl, erweitere auf nächstes Wort
706
+ this.expandRangeToWord(r);
707
+ }
708
+ this.applyInlineStyle('color', color);
709
+ }
710
+ applyHighlight(color) {
711
+ this.focusEditor();
712
+ this.restoreSelection();
713
+ const sel = window.getSelection();
714
+ if (!sel || sel.rangeCount === 0)
715
+ return;
716
+ const r = sel.getRangeAt(0);
717
+ if (r.collapsed) {
718
+ this.expandRangeToWord(r);
719
+ }
720
+ // Some browsers use 'hiliteColor', others 'backColor'
721
+ this.applyInlineStyle('backgroundColor', color);
722
+ }
723
+ expandRangeToWord(range) {
724
+ try {
725
+ const editor = this.editorRef.nativeElement;
726
+ let node = range.startContainer;
727
+ if (node.nodeType !== Node.TEXT_NODE) {
728
+ // Versuche, einen Textknoten in der Nähe zu finden
729
+ const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
730
+ let found = null;
731
+ while (walker.nextNode()) {
732
+ const n = walker.currentNode;
733
+ if (n.parentElement && editor.contains(n.parentElement)) {
734
+ found = n;
735
+ break;
736
+ }
737
+ }
738
+ if (found)
739
+ node = found;
740
+ }
741
+ const text = node.nodeType === Node.TEXT_NODE ? node.data : '';
742
+ let start = 0, end = text.length;
743
+ // einfache Wortgrenzen-Heuristik
744
+ const pos = range.startOffset;
745
+ for (let i = pos; i > 0; i--) {
746
+ if (/\s/.test(text[i - 1])) {
747
+ start = i;
748
+ break;
749
+ }
750
+ }
751
+ for (let i = pos; i < text.length; i++) {
752
+ if (/\s/.test(text[i])) {
753
+ end = i;
754
+ break;
755
+ }
756
+ }
757
+ range.setStart(node, start);
758
+ range.setEnd(node, end);
759
+ const sel = window.getSelection();
760
+ if (sel) {
761
+ sel.removeAllRanges();
762
+ sel.addRange(range);
763
+ }
764
+ }
765
+ catch { /* noop */ }
766
+ }
767
+ insertLink() {
768
+ const url = prompt('Link-URL eingeben:', 'https://');
769
+ if (url) {
770
+ this.cmd('createLink', url);
771
+ }
772
+ }
773
+ removeFormat() {
774
+ this.cmd('removeFormat');
775
+ this.cmd('unlink');
776
+ }
777
+ onInput() {
778
+ this.emitValue();
779
+ this.scheduleSnapshot();
780
+ }
781
+ onKeyup(_evt) {
782
+ this.saveSelection();
783
+ this.emitValue();
784
+ this.updateInlineStates();
785
+ }
786
+ onPaste(evt) {
787
+ // Optional: Clean paste to plain text while keeping basic formatting minimal.
788
+ if (!evt.clipboardData)
789
+ return;
790
+ evt.preventDefault();
791
+ const text = evt.clipboardData.getData('text/plain');
792
+ this.insertTextAtSelection(text);
793
+ this.emitValue();
794
+ this.takeSnapshot('paste');
795
+ }
796
+ emitValue() {
797
+ // Clean up empty style attributes and redundant spans before emitting
798
+ this.cleanupEmptyStylesAndSpans();
799
+ const val = this.editorRef.nativeElement.innerHTML;
800
+ this.onChange(val);
801
+ this.valueChange.emit(val);
802
+ this.updateInlineStates();
803
+ }
804
+ // Remove empty style attributes (style="") and unwrap spans without any attributes
805
+ cleanupEmptyStylesAndSpans(root) {
806
+ const editor = root || this.editorRef.nativeElement;
807
+ const toUnwrap = [];
808
+ const walker = document.createTreeWalker(editor, NodeFilter.SHOW_ELEMENT);
809
+ while (walker.nextNode()) {
810
+ const el = walker.currentNode;
811
+ if (el.hasAttribute('style')) {
812
+ // If no style declarations remain, drop the attribute
813
+ const cssText = el.getAttribute('style') || '';
814
+ const byApiEmpty = (el.style ? el.style.length === 0 : false);
815
+ const normalized = cssText.replace(/[\s;]/g, '');
816
+ if (byApiEmpty || normalized.length === 0) {
817
+ el.removeAttribute('style');
818
+ }
819
+ }
820
+ // Unwrap spans that have no attributes left
821
+ if (el.tagName === 'SPAN' && el.attributes.length === 0) {
822
+ toUnwrap.push(el);
823
+ }
824
+ }
825
+ for (const span of toUnwrap) {
826
+ while (span.firstChild)
827
+ span.parentNode?.insertBefore(span.firstChild, span);
828
+ span.remove();
829
+ }
830
+ }
831
+ // History API
832
+ get canUndo() { return this.historyIndex > 0; }
833
+ get canRedo() { return this.historyIndex >= 0 && this.historyIndex < this.history.length - 1; }
834
+ undo() {
835
+ if (!this.canUndo)
836
+ return;
837
+ this.applySnapshot(this.historyIndex - 1);
838
+ }
839
+ redo() {
840
+ if (!this.canRedo)
841
+ return;
842
+ this.applySnapshot(this.historyIndex + 1);
843
+ }
844
+ scheduleSnapshot() {
845
+ if (this.isRestoringHistory)
846
+ return;
847
+ if (this.snapshotTimer)
848
+ clearTimeout(this.snapshotTimer);
849
+ this.snapshotTimer = setTimeout(() => this.takeSnapshot('input'), 250);
850
+ }
851
+ takeSnapshot(_reason) {
852
+ if (this.isRestoringHistory)
853
+ return;
854
+ const editor = this.editorRef.nativeElement;
855
+ const html = editor.innerHTML;
856
+ const sel = window.getSelection();
857
+ let rangeData = null;
858
+ if (sel && sel.rangeCount > 0) {
859
+ const r = sel.getRangeAt(0);
860
+ if (this.isRangeInEditor(r)) {
861
+ const ser = this.serializeRange(r);
862
+ if (ser)
863
+ rangeData = ser;
864
+ }
865
+ }
866
+ // Verhindere Duplikate nacheinander
867
+ const last = this.history[this.historyIndex];
868
+ if (last && last.html === html)
869
+ return;
870
+ // Zukunft verwerfen bei neuem Snapshot
871
+ if (this.historyIndex < this.history.length - 1) {
872
+ this.history = this.history.slice(0, this.historyIndex + 1);
873
+ }
874
+ this.history.push({ html, range: rangeData });
875
+ this.historyIndex = this.history.length - 1;
876
+ // Begrenze Historie
877
+ const MAX = 50;
878
+ if (this.history.length > MAX) {
879
+ const drop = this.history.length - MAX;
880
+ this.history.splice(0, drop);
881
+ this.historyIndex -= drop;
882
+ if (this.historyIndex < 0)
883
+ this.historyIndex = 0;
884
+ }
885
+ }
886
+ applySnapshot(index) {
887
+ if (index < 0 || index >= this.history.length)
888
+ return;
889
+ const snap = this.history[index];
890
+ const editor = this.editorRef.nativeElement;
891
+ this.isRestoringHistory = true;
892
+ editor.innerHTML = snap.html;
893
+ // Selektion wiederherstellen
894
+ if (snap.range) {
895
+ this.restoreSerializedRange(snap.range);
896
+ }
897
+ else {
898
+ // Cursor ans Ende
899
+ const sel = window.getSelection();
900
+ if (sel) {
901
+ sel.removeAllRanges();
902
+ const r = document.createRange();
903
+ r.selectNodeContents(editor);
904
+ r.collapse(false);
905
+ sel.addRange(r);
906
+ }
907
+ }
908
+ this.historyIndex = index;
909
+ this.isRestoringHistory = false;
910
+ // Werte emittieren nach Restore
911
+ this.emitValue();
912
+ }
913
+ serializeRange(range) {
914
+ const s = this.nodePathFromNode(range.startContainer);
915
+ const e = this.nodePathFromNode(range.endContainer);
916
+ if (!s || !e)
917
+ return null;
918
+ return {
919
+ startPath: s,
920
+ startOffset: range.startOffset,
921
+ endPath: e,
922
+ endOffset: range.endOffset,
923
+ };
924
+ }
925
+ restoreSerializedRange(data) {
926
+ const editor = this.editorRef.nativeElement;
927
+ const startNode = this.nodeFromPath(data.startPath);
928
+ const endNode = this.nodeFromPath(data.endPath);
929
+ if (!startNode || !endNode)
930
+ return;
931
+ const r = document.createRange();
932
+ try {
933
+ r.setStart(startNode, Math.min(data.startOffset, this.maxOffset(startNode)));
934
+ r.setEnd(endNode, Math.min(data.endOffset, this.maxOffset(endNode)));
935
+ }
936
+ catch {
937
+ r.selectNodeContents(editor);
938
+ r.collapse(false);
939
+ }
940
+ const sel = window.getSelection();
941
+ if (sel) {
942
+ sel.removeAllRanges();
943
+ sel.addRange(r);
944
+ }
945
+ }
946
+ nodePathFromNode(node) {
947
+ const editor = this.editorRef.nativeElement;
948
+ const path = [];
949
+ let n = node;
950
+ while (n && n !== editor) {
951
+ const pnode = n.parentNode;
952
+ if (!pnode)
953
+ return null;
954
+ const idx = Array.prototype.indexOf.call(pnode.childNodes, n);
955
+ path.push(idx);
956
+ n = pnode;
957
+ }
958
+ if (n !== editor)
959
+ return null;
960
+ path.reverse();
961
+ return path;
962
+ }
963
+ nodeFromPath(path) {
964
+ const editor = this.editorRef.nativeElement;
965
+ let n = editor;
966
+ for (const idx of path) {
967
+ if (!n.childNodes || idx < 0 || idx >= n.childNodes.length)
968
+ return null;
969
+ n = n.childNodes[idx];
970
+ }
971
+ return n;
972
+ }
973
+ maxOffset(node) {
974
+ if (node.nodeType === Node.TEXT_NODE)
975
+ return node.data.length;
976
+ return node.childNodes.length;
977
+ }
978
+ focusEditor() {
979
+ const el = this.editorRef.nativeElement;
980
+ if (document.activeElement !== el) {
981
+ el.focus();
982
+ }
983
+ }
984
+ // Minimal inline style applier for inline styles
985
+ 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');
1008
+ }
1009
+ applyInlineStyleSmart(cssProp, value) {
1010
+ this.focusEditor();
1011
+ this.restoreSelection();
1012
+ 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);
1032
+ return;
1033
+ }
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 { }
1049
+ }
1050
+ for (const el of styled) {
1051
+ el.style[cssProp] = value;
1052
+ }
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);
1070
+ }
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;
1121
+ }
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;
1137
+ }
1138
+ el = el.parentElement;
1139
+ }
1140
+ return null;
1141
+ }
1142
+ splitCarrierAroundSelection(range, carrier) {
1143
+ const parent = carrier.parentNode;
1144
+ if (!parent)
1145
+ 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
+ }
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);
1172
+ }
1173
+ catch { }
1174
+ }
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;
1203
+ }
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);
1217
+ }
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);
1224
+ 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)
1228
+ 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);
1234
+ }
1235
+ for (const el of toRemove)
1236
+ el.remove();
1237
+ }
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);
1244
+ }
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
+ }
1262
+ }
1263
+ }
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;
1288
+ }
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
+ }
1325
+ }
1326
+ for (const el of toRemove)
1327
+ el.remove();
1328
+ }
1329
+ maybeUnwrapNormalSpan(span) {
1330
+ 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
+ }
1342
+ }
1343
+ }
1344
+ wrapSelectionWith(tag, attrs) {
1345
+ const sel = window.getSelection();
1346
+ if (!sel || sel.rangeCount === 0)
1347
+ return;
1348
+ const range = sel.getRangeAt(0);
1349
+ if (range.collapsed)
1350
+ return;
1351
+ const el = document.createElement(tag);
1352
+ if (attrs) {
1353
+ for (const [k, v] of Object.entries(attrs)) {
1354
+ if (v != null)
1355
+ el.setAttribute(k, v);
1356
+ }
1357
+ }
1358
+ const frag = range.extractContents();
1359
+ el.appendChild(frag);
1360
+ range.insertNode(el);
1361
+ // Auswahl hinter das Element setzen
1362
+ sel.removeAllRanges();
1363
+ const after = document.createRange();
1364
+ after.setStartAfter(el);
1365
+ after.collapse(true);
1366
+ sel.addRange(after);
1367
+ }
1368
+ insertTextAtSelection(text) {
1369
+ const sel = window.getSelection();
1370
+ if (!sel || sel.rangeCount === 0)
1371
+ return;
1372
+ const range = sel.getRangeAt(0);
1373
+ range.deleteContents();
1374
+ const node = document.createTextNode(text);
1375
+ range.insertNode(node);
1376
+ // Cursor ans Ende des eingefügten Textes
1377
+ sel.removeAllRanges();
1378
+ const after = document.createRange();
1379
+ after.setStartAfter(node);
1380
+ after.collapse(true);
1381
+ sel.addRange(after);
1382
+ }
1383
+ // ----- Inline toggle logic (Bold/Italic/Underline) -----
1384
+ updateInlineStates() {
1385
+ const sel = window.getSelection();
1386
+ if (!sel || sel.rangeCount === 0) {
1387
+ this.isBoldActive = this.isItalicActive = this.isUnderlineActive = false;
1388
+ return;
1389
+ }
1390
+ const range = sel.getRangeAt(0);
1391
+ if (!this.isRangeInEditor(range)) {
1392
+ this.isBoldActive = this.isItalicActive = this.isUnderlineActive = false;
1393
+ return;
1394
+ }
1395
+ if (range.collapsed) {
1396
+ const node = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement;
1397
+ this.isBoldActive = this.isNodeBold(node);
1398
+ this.isItalicActive = this.isNodeItalic(node);
1399
+ this.isUnderlineActive = this.isNodeUnderline(node);
1400
+ return;
1401
+ }
1402
+ this.isBoldActive = this.computeBoldAnyForRange(range);
1403
+ this.isItalicActive = this.computeItalicAnyForRange(range);
1404
+ this.isUnderlineActive = this.computeUnderlineAnyForRange(range);
1405
+ // ensure UI reflects changes immediately
1406
+ try {
1407
+ this.cdr.detectChanges();
1408
+ }
1409
+ catch { }
1410
+ }
1411
+ 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;
1428
+ }
1429
+ 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;
1445
+ }
1446
+ 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;
1462
+ }
1463
+ isNodeBold(node) {
1464
+ let el = node;
1465
+ while (el) {
1466
+ if (el.tagName === 'B' || el.tagName === 'STRONG')
1467
+ return true;
1468
+ const cs = getComputedStyle(el);
1469
+ if (cs.fontWeight && cs.fontWeight !== 'normal' && cs.fontWeight !== '400') {
1470
+ const w = cs.fontWeight === 'bold' ? 700 : parseInt(cs.fontWeight, 10);
1471
+ if (!Number.isNaN(w) && w >= 600)
1472
+ return true;
1473
+ }
1474
+ el = el.parentElement;
1475
+ }
1476
+ return false;
1477
+ }
1478
+ isNodeItalic(node) {
1479
+ let el = node;
1480
+ while (el) {
1481
+ if (el.tagName === 'I' || el.tagName === 'EM')
1482
+ return true;
1483
+ const cs = getComputedStyle(el);
1484
+ if (cs.fontStyle && cs.fontStyle !== 'normal')
1485
+ return true;
1486
+ el = el.parentElement;
1487
+ }
1488
+ return false;
1489
+ }
1490
+ isNodeUnderline(node) {
1491
+ let el = node;
1492
+ while (el) {
1493
+ if (el.tagName === 'U')
1494
+ return true;
1495
+ const cs = getComputedStyle(el);
1496
+ if (cs.textDecorationLine && cs.textDecorationLine.includes('underline'))
1497
+ return true;
1498
+ el = el.parentElement;
1499
+ }
1500
+ return false;
1501
+ }
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) {
1521
+ 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;
1548
+ try {
1549
+ if (range.intersectsNode(n))
1550
+ nodes.push(n);
1551
+ }
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);
1570
+ }
1571
+ catch { }
1572
+ }
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
+ const walker = document.createTreeWalker(common, NodeFilter.SHOW_ELEMENT);
1582
+ while (walker.nextNode()) {
1583
+ 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)
1594
+ continue;
1595
+ try {
1596
+ if (range.intersectsNode ? range.intersectsNode(el) : true)
1597
+ affected.push(el);
1598
+ }
1599
+ catch { }
1600
+ }
1601
+ for (const el of affected) {
1602
+ if (el.tagName === 'B' || el.tagName === 'STRONG' || el.tagName === 'I' || el.tagName === 'EM' || el.tagName === 'U') {
1603
+ while (el.firstChild)
1604
+ el.parentNode?.insertBefore(el.firstChild, el);
1605
+ el.remove();
1606
+ continue;
1607
+ }
1608
+ el.style.removeProperty(cssPropKebab);
1609
+ if (!el.getAttribute('style')) {
1610
+ while (el.firstChild)
1611
+ el.parentNode?.insertBefore(el.firstChild, el);
1612
+ el.remove();
1613
+ }
1614
+ }
1615
+ this.emitValue();
1616
+ }
1617
+ toggleBold() {
1618
+ this.focusEditor();
1619
+ this.restoreSelection();
1620
+ const sel = window.getSelection();
1621
+ if (!sel || sel.rangeCount === 0)
1622
+ return;
1623
+ const range = sel.getRangeAt(0);
1624
+ if (range.collapsed)
1625
+ this.expandRangeToWord(range);
1626
+ const anyActive = this.computeBoldAnyForRange(range);
1627
+ const blocks = this.getIntersectingBlocks(range);
1628
+ const multiBlock = blocks.length > 1;
1629
+ 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
+ }
1655
+ }
1656
+ 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');
1666
+ }
1667
+ this.updateInlineStates();
1668
+ this.takeSnapshot('toggle-bold');
1669
+ }
1670
+ toggleItalic() {
1671
+ this.focusEditor();
1672
+ this.restoreSelection();
1673
+ const sel = window.getSelection();
1674
+ if (!sel || sel.rangeCount === 0)
1675
+ return;
1676
+ const range = sel.getRangeAt(0);
1677
+ if (range.collapsed)
1678
+ this.expandRangeToWord(range);
1679
+ const anyActive = this.computeItalicAnyForRange(range);
1680
+ 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
+ }
1690
+ }
1691
+ else {
1692
+ this.applyInlineStyleSmart('fontStyle', 'italic');
1693
+ }
1694
+ this.updateInlineStates();
1695
+ this.takeSnapshot('toggle-italic');
1696
+ }
1697
+ toggleUnderline() {
1698
+ this.focusEditor();
1699
+ this.restoreSelection();
1700
+ const sel = window.getSelection();
1701
+ if (!sel || sel.rangeCount === 0)
1702
+ return;
1703
+ const range = sel.getRangeAt(0);
1704
+ if (range.collapsed)
1705
+ this.expandRangeToWord(range);
1706
+ const anyActive = this.computeUnderlineAnyForRange(range);
1707
+ 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
+ }
1717
+ }
1718
+ else {
1719
+ this.applyInlineStyleSmart('textDecoration', 'underline');
1720
+ }
1721
+ this.updateInlineStates();
1722
+ this.takeSnapshot('toggle-underline');
1723
+ }
1724
+ getCurrentRange() {
1725
+ const sel = window.getSelection();
1726
+ if (!sel || sel.rangeCount === 0)
1727
+ return null;
1728
+ const range = sel.getRangeAt(0);
1729
+ return this.isRangeInEditor(range) ? range : null;
1730
+ }
1731
+ getEditorChildAncestor(node) {
1732
+ const editor = this.editorRef.nativeElement;
1733
+ let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
1734
+ while (el && el.parentElement && el.parentElement !== editor) {
1735
+ el = el.parentElement;
1736
+ }
1737
+ // Wenn direktes Kind des Editors
1738
+ if (el && el.parentElement === editor)
1739
+ return el;
1740
+ return editor;
1741
+ }
1742
+ findClosest(node, selector) {
1743
+ let el = node && node.nodeType === 1 ? node : node?.parentElement ?? null;
1744
+ while (el) {
1745
+ if (el.matches(selector))
1746
+ return el;
1747
+ el = el.parentElement;
1748
+ }
1749
+ return null;
1750
+ }
1751
+ toggleList(kind) {
1752
+ const range = this.getCurrentRange();
1753
+ if (!range)
1754
+ return;
1755
+ const editor = this.editorRef.nativeElement;
1756
+ // Wenn bereits in einer Liste, Liste aufheben
1757
+ const listAncestor = this.findClosest(range.commonAncestorContainer, 'ul,ol');
1758
+ if (listAncestor) {
1759
+ // unwrap: li-Inhalte an die Stelle der Liste setzen
1760
+ const frag = document.createDocumentFragment();
1761
+ const items = Array.from(listAncestor.children); // li
1762
+ for (const li of items) {
1763
+ while (li.firstChild)
1764
+ frag.appendChild(li.firstChild);
1765
+ // Optional: Zeilenumbruch zwischen Items
1766
+ frag.appendChild(document.createElement('br'));
1767
+ }
1768
+ listAncestor.replaceWith(frag);
1769
+ return;
1770
+ }
1771
+ // Neue Liste um die aktuelle Auswahl legen
1772
+ const list = document.createElement(kind);
1773
+ const li = document.createElement('li');
1774
+ const contents = range.extractContents();
1775
+ if (!contents.hasChildNodes()) {
1776
+ li.textContent = '\u200b'; // zero-width space um leere li zu vermeiden
1777
+ }
1778
+ else {
1779
+ li.appendChild(contents);
1780
+ }
1781
+ list.appendChild(li);
1782
+ range.insertNode(list);
1783
+ // Cursor ins li setzen
1784
+ const sel = window.getSelection();
1785
+ if (sel) {
1786
+ sel.removeAllRanges();
1787
+ const after = document.createRange();
1788
+ after.selectNodeContents(li);
1789
+ after.collapse(false);
1790
+ sel.addRange(after);
1791
+ }
1792
+ }
1793
+ alignBlocks(align) {
1794
+ const range = this.getCurrentRange();
1795
+ if (!range)
1796
+ return;
1797
+ const startBlock = this.getEditorChildAncestor(range.startContainer);
1798
+ const endBlock = this.getEditorChildAncestor(range.endContainer);
1799
+ const editor = this.editorRef.nativeElement;
1800
+ const applyAlign = (el) => {
1801
+ if (!el)
1802
+ return;
1803
+ el.style.textAlign = align;
1804
+ };
1805
+ if (startBlock === endBlock) {
1806
+ applyAlign(startBlock);
1807
+ }
1808
+ else {
1809
+ // grob: alle direkten Kinder zwischen start und end ausrichten
1810
+ const children = Array.from(editor.children);
1811
+ const i1 = children.indexOf(startBlock);
1812
+ const i2 = children.indexOf(endBlock);
1813
+ if (i1 >= 0 && i2 >= 0) {
1814
+ const [from, to] = i1 <= i2 ? [i1, i2] : [i2, i1];
1815
+ for (let i = from; i <= to; i++)
1816
+ applyAlign(children[i]);
1817
+ }
1818
+ else {
1819
+ applyAlign(startBlock);
1820
+ }
1821
+ }
1822
+ }
1823
+ setBlockSpacing(kind, target, value) {
1824
+ const range = this.getCurrentRange();
1825
+ if (!range)
1826
+ return;
1827
+ const editor = this.editorRef.nativeElement;
1828
+ const apply = (el) => {
1829
+ if (!el)
1830
+ return;
1831
+ const v = `${value}px`;
1832
+ if (target === 'all') {
1833
+ el.style[kind] = v;
1834
+ }
1835
+ else if (target === 'vertical') {
1836
+ el.style[`${kind}Top`] = v;
1837
+ el.style[`${kind}Bottom`] = v;
1838
+ }
1839
+ else if (target === 'horizontal') {
1840
+ el.style[`${kind}Left`] = v;
1841
+ el.style[`${kind}Right`] = v;
1842
+ }
1843
+ };
1844
+ const startBlock = this.getEditorChildAncestor(range.startContainer);
1845
+ const endBlock = this.getEditorChildAncestor(range.endContainer);
1846
+ if (startBlock === endBlock) {
1847
+ apply(startBlock);
1848
+ }
1849
+ else {
1850
+ const children = Array.from(editor.children);
1851
+ const i1 = children.indexOf(startBlock);
1852
+ const i2 = children.indexOf(endBlock);
1853
+ if (i1 >= 0 && i2 >= 0) {
1854
+ const [from, to] = i1 <= i2 ? [i1, i2] : [i2, i1];
1855
+ for (let i = from; i <= to; i++)
1856
+ apply(children[i]);
1857
+ }
1858
+ else {
1859
+ apply(startBlock);
1860
+ }
1861
+ }
1862
+ }
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).
1865
+ applySpacingToSelection(kind, target, value) {
1866
+ const sel = window.getSelection();
1867
+ if (!sel || sel.rangeCount === 0)
1868
+ return false;
1869
+ const range = sel.getRangeAt(0);
1870
+ if (range.collapsed)
1871
+ return false;
1872
+ const startBlock = this.getEditorChildAncestor(range.startContainer);
1873
+ const endBlock = this.getEditorChildAncestor(range.endContainer);
1874
+ // Nur inline, wenn innerhalb desselben Blocks
1875
+ if (!startBlock || !endBlock || startBlock !== endBlock)
1876
+ return false;
1877
+ const wrapper = document.createElement('span');
1878
+ const px = `${value}px`;
1879
+ if (target === 'all') {
1880
+ wrapper.style[kind] = px;
1881
+ }
1882
+ else if (target === 'vertical') {
1883
+ wrapper.style[`${kind}Top`] = px;
1884
+ wrapper.style[`${kind}Bottom`] = px;
1885
+ }
1886
+ else if (target === 'horizontal') {
1887
+ wrapper.style[`${kind}Left`] = px;
1888
+ wrapper.style[`${kind}Right`] = px;
1889
+ }
1890
+ // Für vertikale Margins auf Inline-Elementen sicherstellen, dass sie greifen
1891
+ if (kind === 'margin' && (target === 'all' || target === 'vertical')) {
1892
+ wrapper.style.display = 'inline-block';
1893
+ }
1894
+ // Auswahl extrahieren und in Wrapper einsetzen
1895
+ const frag = range.extractContents();
1896
+ wrapper.appendChild(frag);
1897
+ range.insertNode(wrapper);
1898
+ // Gleichartige Wrapper zusammenführen und Cursor korrekt setzen
1899
+ const normalized = this.normalizeSpacingSpans(wrapper, kind, target, value) || wrapper;
1900
+ sel.removeAllRanges();
1901
+ const after = document.createRange();
1902
+ if (normalized.isConnected) {
1903
+ after.setStartAfter(normalized);
1904
+ }
1905
+ else {
1906
+ // Fallback: an das Ende des Startblocks
1907
+ const sb = this.getEditorChildAncestor(range.startContainer);
1908
+ if (sb)
1909
+ after.selectNodeContents(sb);
1910
+ }
1911
+ after.collapse(true);
1912
+ sel.addRange(after);
1913
+ return true;
1914
+ }
1915
+ normalizeSpacingSpans(span, kind, target, value) {
1916
+ const props = [];
1917
+ if (target === 'all')
1918
+ props.push(kind);
1919
+ if (target === 'vertical')
1920
+ props.push(`${kind}Top`, `${kind}Bottom`);
1921
+ if (target === 'horizontal')
1922
+ props.push(`${kind}Left`, `${kind}Right`);
1923
+ const displayNeeded = kind === 'margin' && (target === 'all' || target === 'vertical');
1924
+ const v = `${value}px`;
1925
+ const hasSameSpacing = (a, b) => {
1926
+ for (const p of props) {
1927
+ if (a.style[p] !== b.style[p])
1928
+ return false;
1929
+ }
1930
+ if (displayNeeded) {
1931
+ if (a.style.display !== b.style.display)
1932
+ return false;
1933
+ }
1934
+ return true;
1935
+ };
1936
+ // Downward: verschachtelte identische Spans in span zusammenführen
1937
+ if (span.children.length === 1) {
1938
+ const only = span.children[0];
1939
+ if (only && only.tagName === 'SPAN' && hasSameSpacing(span, only)) {
1940
+ while (only.firstChild)
1941
+ span.appendChild(only.firstChild);
1942
+ only.remove();
1943
+ }
1944
+ }
1945
+ let current = span;
1946
+ // Upward: mit Elternelement verschmelzen, wenn identischer Span
1947
+ const parent = current.parentElement;
1948
+ if (parent && parent.tagName === 'SPAN' && hasSameSpacing(parent, current)) {
1949
+ while (current.firstChild)
1950
+ parent.insertBefore(current.firstChild, current);
1951
+ current.remove();
1952
+ current = parent;
1953
+ }
1954
+ if (!current)
1955
+ return null;
1956
+ // Left merge: vorherige Geschwister-Spans mit gleicher Formatierung in current ziehen
1957
+ let prev = current.previousElementSibling;
1958
+ if (prev && prev.tagName === 'SPAN' && hasSameSpacing(prev, current)) {
1959
+ while (current.firstChild)
1960
+ prev.appendChild(current.firstChild);
1961
+ current.remove();
1962
+ current = prev;
1963
+ }
1964
+ // Right merge: folgende Geschwister-Spans in current ziehen
1965
+ let next = current.nextElementSibling;
1966
+ while (next && next.tagName === 'SPAN' && hasSameSpacing(current, next)) {
1967
+ while (next.firstChild)
1968
+ current.appendChild(next.firstChild);
1969
+ const toRemove = next;
1970
+ next = next.nextElementSibling;
1971
+ toRemove.remove();
1972
+ }
1973
+ return current;
1974
+ }
1975
+ setHeading(tag) {
1976
+ const range = this.getCurrentRange();
1977
+ if (!range)
1978
+ return;
1979
+ const editor = this.editorRef.nativeElement;
1980
+ const replaceTag = (el, newTag) => {
1981
+ if (!el || el === editor)
1982
+ return null;
1983
+ const neo = document.createElement(newTag);
1984
+ while (el.firstChild)
1985
+ neo.appendChild(el.firstChild);
1986
+ el.replaceWith(neo);
1987
+ return neo;
1988
+ };
1989
+ const startBlock = this.getEditorChildAncestor(range.startContainer);
1990
+ const endBlock = this.getEditorChildAncestor(range.endContainer);
1991
+ if (startBlock && startBlock !== editor && (startBlock === endBlock)) {
1992
+ const currentTag = (startBlock.tagName || '').toLowerCase();
1993
+ if (currentTag === tag)
1994
+ return;
1995
+ const convertible = /^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag);
1996
+ if (convertible) {
1997
+ 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
+ }
2001
+ if (neo) {
2002
+ const sel = window.getSelection();
2003
+ if (sel) {
2004
+ sel.removeAllRanges();
2005
+ const after = document.createRange();
2006
+ after.selectNodeContents(neo);
2007
+ after.collapse(false);
2008
+ sel.addRange(after);
2009
+ }
2010
+ }
2011
+ return;
2012
+ }
2013
+ }
2014
+ if (startBlock && endBlock && startBlock !== editor && endBlock !== editor) {
2015
+ const children = Array.from(editor.children);
2016
+ const i1 = children.indexOf(startBlock);
2017
+ const i2 = children.indexOf(endBlock);
2018
+ if (i1 >= 0 && i2 >= 0) {
2019
+ const [from, to] = i1 <= i2 ? [i1, i2] : [i2, i1];
2020
+ let last = null;
2021
+ for (let i = from; i <= to; i++) {
2022
+ const el = children[i];
2023
+ const currentTag = (el.tagName || '').toLowerCase();
2024
+ if (/^(p|div|pre|h1|h2|h3|h4|h5|h6)$/i.test(currentTag)) {
2025
+ 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
+ }
2029
+ if (neo)
2030
+ last = neo;
2031
+ }
2032
+ }
2033
+ if (last) {
2034
+ const sel = window.getSelection();
2035
+ if (sel) {
2036
+ sel.removeAllRanges();
2037
+ const after = document.createRange();
2038
+ after.selectNodeContents(last);
2039
+ after.collapse(false);
2040
+ sel.addRange(after);
2041
+ }
2042
+ }
2043
+ return;
2044
+ }
2045
+ }
2046
+ if (!range.collapsed) {
2047
+ 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
+ }
2051
+ const frag = range.extractContents();
2052
+ el.appendChild(frag);
2053
+ range.insertNode(el);
2054
+ const sel = window.getSelection();
2055
+ if (sel) {
2056
+ sel.removeAllRanges();
2057
+ const after = document.createRange();
2058
+ after.selectNodeContents(el);
2059
+ after.collapse(false);
2060
+ sel.addRange(after);
2061
+ }
2062
+ }
2063
+ }
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"] }] });
2072
+ }
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"] }]
2082
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { placeholder: [{
2083
+ type: Input
2084
+ }], showToolbar: [{
2085
+ type: Input
2086
+ }], fonts: [{
2087
+ type: Input
2088
+ }], fontSizes: [{
2089
+ type: Input
2090
+ }], height: [{
2091
+ type: Input
2092
+ }], minHeight: [{
2093
+ type: Input
2094
+ }], maxHeight: [{
2095
+ type: Input
2096
+ }], disabled: [{
2097
+ type: Input
2098
+ }], valueChange: [{
2099
+ type: Output
2100
+ }], editorRef: [{
2101
+ type: ViewChild,
2102
+ args: ['editor', { static: true }]
2103
+ }], config: [{
2104
+ type: Input
2105
+ }], onSelectionChange: [{
2106
+ type: HostListener,
2107
+ args: ['document:selectionchange']
2108
+ }], closeMenus: [{
2109
+ type: HostListener,
2110
+ args: ['document:click']
2111
+ }], onKeydown: [{
2112
+ type: HostListener,
2113
+ args: ['keydown', ['$event']]
2114
+ }] } });
2115
+
2116
+ /*
2117
+ * Public API Surface of richtext-editor
2118
+ */
2119
+
2120
+ /**
2121
+ * Generated bundle index. Do not edit.
2122
+ */
2123
+
2124
+ export { STACKCH_RTE_I18N_DE, STACKCH_RTE_I18N_FR, STACKCH_RTE_I18N_IT, StackchRichtextEditor, StackchRichtextEditorConfig, StackchRichtextEditorI18n, StackchRichtextEditorToolbar };
2125
+ //# sourceMappingURL=stackch-angular-richtext-editor.mjs.map