UNPKG

@stackch/angular-richtext-editor

Version:

Lightweight Angular rich text editor (standalone) with a reusable toolbar: fonts, sizes, colors, lists, alignment, links.

496 lines (493 loc) 120 kB
import * as i0 from '@angular/core'; import { EventEmitter, Output, Input, Component, forwardRef, HostListener, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; class StackchRichtextEditorToolbar { // State/config from parent editor cfg; i18n; disabled = false; fonts = []; fontSizes = []; isBoldActive = false; isItalicActive = false; isUnderlineActive = false; canUndo = false; canRedo = false; // Consolidated UI state uiState; // Actions -> bubble to parent editor undo = new EventEmitter(); redo = new EventEmitter(); toggleFontPanel = new EventEmitter(); toggleHeadingMenu = new EventEmitter(); toggleSpacingMenu = new EventEmitter(); toggleAlignMenu = new EventEmitter(); toggleColorMenu = new EventEmitter(); toggleListMenu = new EventEmitter(); pickFont = new EventEmitter(); pickFontSize = new EventEmitter(); pickAlign = new EventEmitter(); pickList = new EventEmitter(); pickHeading = new EventEmitter(); pickSpacing = new EventEmitter(); applyColor = new EventEmitter(); applyHighlight = new EventEmitter(); toggleBold = new EventEmitter(); toggleItalic = new EventEmitter(); toggleUnderline = new EventEmitter(); insertLink = new EventEmitter(); removeFormat = new EventEmitter(); // Ask parent to preserve selection before toolbar interactions (mousedown/pointerdown/focus) saveSelectionRequest = new EventEmitter(); onToolbarMouseDown(evt) { // Prevent focus stealing and keep selection in the editor evt.preventDefault(); evt.stopPropagation(); this.saveSelectionRequest.emit(); } onColorPointerDown(evt) { // Do not preventDefault to allow the color picker to open, just preserve selection evt.stopPropagation(); this.saveSelectionRequest.emit(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorToolbar, deps: [], target: i0.ɵɵFactoryTarget.Component }); 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 }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: StackchRichtextEditorToolbar, decorators: [{ type: Component, 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"] }] }], propDecorators: { cfg: [{ type: Input }], i18n: [{ type: Input }], disabled: [{ type: Input }], fonts: [{ type: Input }], fontSizes: [{ type: Input }], isBoldActive: [{ type: Input }], isItalicActive: [{ type: Input }], isUnderlineActive: [{ type: Input }], canUndo: [{ type: Input }], canRedo: [{ type: Input }], uiState: [{ type: Input }], undo: [{ type: Output }], redo: [{ type: Output }], toggleFontPanel: [{ type: Output }], toggleHeadingMenu: [{ type: Output }], toggleSpacingMenu: [{ type: Output }], toggleAlignMenu: [{ type: Output }], toggleColorMenu: [{ type: Output }], toggleListMenu: [{ type: Output }], pickFont: [{ type: Output }], pickFontSize: [{ type: Output }], pickAlign: [{ type: Output }], pickList: [{ type: Output }], pickHeading: [{ type: Output }], pickSpacing: [{ type: Output }], applyColor: [{ type: Output }], applyHighlight: [{ type: Output }], toggleBold: [{ type: Output }], toggleItalic: [{ type: Output }], toggleUnderline: [{ type: Output }], insertLink: [{ type: Output }], removeFormat: [{ type: Output }], saveSelectionRequest: [{ type: Output }] } }); class StackchRichtextEditorConfig { // Sichtbarkeit einzelner Toolbar-Elemente (Default: an) showUndoRedo = true; showFontPanel = true; showHeading = true; showSpacing = true; showColor = true; showBold = true; showItalic = true; showUnderline = true; showLists = true; showAlign = true; showLink = true; showRemoveFormat = true; // i18n overrides (partial), default is English i18n; } class StackchRichtextEditorI18n { // Generic placeholder = 'Write…'; // Toolbar titles undoTitle = 'Undo (Ctrl+Z)'; redoTitle = 'Redo (Ctrl+Y)'; fontPanelTitle = 'Font & Size'; fontSectionTitle = 'Font'; sizeSectionTitle = 'Size'; headingTitle = 'Heading'; spacingTitle = 'Spacing (Margin/Padding)'; marginTitle = 'Margin'; paddingTitle = 'Padding'; colorsTitle = 'Colors'; colorMenuTitle = 'Pick a color'; textColorTitle = 'Text color'; highlightTitle = 'Highlight'; boldTitle = 'Bold'; italicTitle = 'Italic'; underlineTitle = 'Underline'; listsTitle = 'Lists'; bulletListTitle = 'Bullet list'; numberedListTitle = 'Numbered list'; alignTitle = 'Alignment'; alignLeftTitle = 'Left'; alignCenterTitle = 'Center'; alignRightTitle = 'Right'; alignJustifyTitle = 'Justify'; linkTitle = 'Link'; removeFormatTitle = 'Remove format'; // Heading menu labels paragraphLabel = 'Paragraph (P)'; codeLabel = 'Code (pre)'; h1Label = 'H1'; h2Label = 'H2'; h3Label = 'H3'; h4Label = 'H4'; h5Label = 'H5'; h6Label = 'H6'; } // Predefined i18n bundles // Consumers can import these and pass via config.i18n to localize the toolbar/labels. const STACKCH_RTE_I18N_DE = { // Generic placeholder: 'Schreiben…', // Toolbar titles undoTitle: 'Rückgängig (Strg+Z)', redoTitle: 'Wiederholen (Strg+Y)', fontPanelTitle: 'Schrift & Größe', fontSectionTitle: 'Schrift', sizeSectionTitle: 'Größe', headingTitle: 'Überschrift', spacingTitle: 'Abstand (Außen/Innen)', marginTitle: 'Außenabstand', paddingTitle: 'Innenabstand', colorsTitle: 'Farben', colorMenuTitle: 'Farbe wählen', textColorTitle: 'Textfarbe', highlightTitle: 'Hervorheben', boldTitle: 'Fett', italicTitle: 'Kursiv', underlineTitle: 'Unterstreichen', listsTitle: 'Listen', bulletListTitle: 'Aufzählung', numberedListTitle: 'Nummeriert', alignTitle: 'Ausrichtung', alignLeftTitle: 'Links', alignCenterTitle: 'Zentriert', alignRightTitle: 'Rechts', alignJustifyTitle: 'Blocksatz', linkTitle: 'Link', removeFormatTitle: 'Formatierung entfernen', // Heading menu labels paragraphLabel: 'Absatz (P)', codeLabel: 'Code (pre)', h1Label: 'H1', h2Label: 'H2', h3Label: 'H3', h4Label: 'H4', h5Label: 'H5', h6Label: 'H6', }; const STACKCH_RTE_I18N_FR = { // Generic placeholder: 'Écrire…', // Toolbar titles undoTitle: 'Annuler (Ctrl+Z)', redoTitle: 'Rétablir (Ctrl+Y)', fontPanelTitle: 'Police et taille', fontSectionTitle: 'Police', sizeSectionTitle: 'Taille', headingTitle: 'Titre', spacingTitle: 'Espacement (Marge/Remplissage)', marginTitle: 'Marge', paddingTitle: 'Remplissage', colorsTitle: 'Couleurs', colorMenuTitle: 'Choisir une couleur', textColorTitle: 'Couleur du texte', highlightTitle: 'Surligner', boldTitle: 'Gras', italicTitle: 'Italique', underlineTitle: 'Souligné', listsTitle: 'Listes', bulletListTitle: 'Liste à puces', numberedListTitle: 'Liste numérotée', alignTitle: 'Alignement', alignLeftTitle: 'Gauche', alignCenterTitle: 'Centré', alignRightTitle: 'Droite', alignJustifyTitle: 'Justifié', linkTitle: 'Lien', removeFormatTitle: 'Effacer la mise en forme', // Heading menu labels paragraphLabel: 'Paragraphe (P)', codeLabel: 'Code (pre)', h1Label: 'H1', h2Label: 'H2', h3Label: 'H3', h4Label: 'H4', h5Label: 'H5', h6Label: 'H6', }; const STACKCH_RTE_I18N_IT = { // Generic placeholder: 'Scrivi…', // Toolbar titles undoTitle: 'Annulla (Ctrl+Z)', redoTitle: 'Ripristina (Ctrl+Y)', fontPanelTitle: 'Carattere e dimensione', fontSectionTitle: 'Carattere', sizeSectionTitle: 'Dimensione', headingTitle: 'Titolo', spacingTitle: 'Spaziatura (Margine/Riempimento)', marginTitle: 'Margine', paddingTitle: 'Riempimento', colorsTitle: 'Colori', colorMenuTitle: 'Scegli un colore', textColorTitle: 'Colore del testo', highlightTitle: 'Evidenzia', boldTitle: 'Grassetto', italicTitle: 'Corsivo', underlineTitle: 'Sottolineato', listsTitle: 'Elenchi', bulletListTitle: 'Elenco puntato', numberedListTitle: 'Elenco numerato', alignTitle: 'Allineamento', alignLeftTitle: 'Sinistra', alignCenterTitle: 'Centrato', alignRightTitle: 'Destra', alignJustifyTitle: 'Giustificato', linkTitle: 'Collegamento', removeFormatTitle: 'Rimuovi formattazione', // Heading menu labels paragraphLabel: 'Paragrafo (P)', codeLabel: 'Codice (pre)', h1Label: 'H1', h2Label: 'H2', h3Label: 'H3', h4Label: 'H4', h5Label: 'H5', h6Label: 'H6', }; class StackchRichtextEditor { cdr; placeholder = ''; showToolbar = true; fonts = [ 'Arial, Helvetica, sans-serif', 'Georgia, serif', 'Times New Roman, Times, serif', 'Trebuchet MS, sans-serif', 'Verdana, Geneva, sans-serif', 'Courier New, Courier, monospace', 'Monaco, monospace' ]; fontSizes = [12, 14, 16, 18, 20, 24, 28, 32]; height; minHeight = 160; maxHeight; set disabled(value) { this._disabled = value; } get disabled() { return this._disabled; } _disabled = false; valueChange = new EventEmitter(); // Structured metrics event (future-proof) metricsChange = new EventEmitter(); editorRef; constructor(cdr) { this.cdr = cdr; } // Toolbar-Konfiguration (default: alles an). Wir halten die übergebene Referenz // und mergen Defaults im Getter, damit auch Eigenschaftsänderungen via ngModel sofort wirken. config; get cfg() { return Object.assign(new StackchRichtextEditorConfig(), this.config || {}); } get i18n() { return Object.assign(new StackchRichtextEditorI18n(), this.cfg.i18n || {}); } onChange = () => { }; onTouched = () => { }; savedRange = null; // History (Undo/Redo) history = []; historyIndex = -1; isRestoringHistory = false; snapshotTimer = null; // UI state for compact dropdowns / panel showFontMenu = false; showSizeMenu = false; showFontPanel = false; showHeadingMenu = false; showSpacingMenu = false; showAlignMenu = false; showColorMenu = false; showListMenu = false; // Inline state flags for active buttons isBoldActive = false; isItalicActive = false; isUnderlineActive = false; // Selection helpers saveSelection() { const sel = window.getSelection(); if (!sel || sel.rangeCount === 0) return; const range = sel.getRangeAt(0); if (!this.isRangeInEditor(range)) return; this.savedRange = range.cloneRange(); this.updateInlineStates(); } // Prevent toolbar buttons from stealing focus from the editor while preserving selection onToolbarMouseDown(evt) { // Keep focus on the editor to avoid persistent button focus outlines evt.preventDefault(); evt.stopPropagation(); this.saveSelection(); } // Beim Öffnen des Color-Pickers: Selektion sichern onColorPointerDown(_evt) { this.saveSelection(); } restoreSelection() { const sel = window.getSelection(); if (!sel || !this.savedRange) return; if (!this.isRangeInEditor(this.savedRange)) return; sel.removeAllRanges(); sel.addRange(this.savedRange); } isRangeInEditor(range) { const editor = this.editorRef?.nativeElement; if (!editor) return false; const container = range.commonAncestorContainer; const node = container.nodeType === Node.ELEMENT_NODE ? container : container.parentElement; return !!node && editor.contains(node); } // Update active-state on selection changes inside the editor onSelectionChange() { const sel = window.getSelection(); if (!sel || sel.rangeCount === 0) return; const range = sel.getRangeAt(0); if (!this.isRangeInEditor(range)) return; this.updateInlineStates(); } // Close dropdowns when clicking anywhere in the document closeMenus() { this.showFontMenu = false; this.showSizeMenu = false; this.showFontPanel = false; this.showHeadingMenu = false; this.showSpacingMenu = false; this.showAlignMenu = false; this.showColorMenu = false; this.showListMenu = false; } // Keyboard: Undo/Redo onKeydown(evt) { const isMac = navigator.platform.toLowerCase().includes('mac'); const mod = isMac ? evt.metaKey : evt.ctrlKey; if (mod && !evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')) { evt.preventDefault(); this.undo(); } else if (mod && (evt.key === 'y' || evt.key === 'Y' || (evt.shiftKey && (evt.key === 'z' || evt.key === 'Z')))) { evt.preventDefault(); this.redo(); } } toggleFontMenu(evt) { this.saveSelection(); this.showFontMenu = !this.showFontMenu; if (this.showFontMenu) this.showSizeMenu = false; evt.stopPropagation(); } toggleSizeMenu(evt) { this.saveSelection(); this.showSizeMenu = !this.showSizeMenu; if (this.showSizeMenu) this.showFontMenu = false; evt.stopPropagation(); } toggleFontPanel(evt) { this.saveSelection(); this.showFontPanel = !this.showFontPanel; // close others if open if (this.showFontPanel) { this.showFontMenu = false; this.showSizeMenu = false; this.showHeadingMenu = false; this.showSpacingMenu = false; this.showAlignMenu = false; this.showColorMenu = false; } evt.stopPropagation(); } toggleHeadingMenu(evt) { this.saveSelection(); this.showHeadingMenu = !this.showHeadingMenu; if (this.showHeadingMenu) { this.showFontMenu = false; this.showSizeMenu = false; this.showFontPanel = false; this.showSpacingMenu = false; this.showAlignMenu = false; this.showColorMenu = false; } evt.stopPropagation(); } toggleSpacingMenu(evt) { this.saveSelection(); this.showSpacingMenu = !this.showSpacingMenu; if (this.showSpacingMenu) { this.showFontMenu = false; this.showSizeMenu = false; this.showFontPanel = false; this.showHeadingMenu = false; this.showAlignMenu = false; this.showColorMenu = false; } evt.stopPropagation(); } toggleAlignMenu(evt) { this.saveSelection(); this.showAlignMenu = !this.showAlignMenu; if (this.showAlignMenu) {