@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
JavaScript
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) {