diff --git a/public/assets/js/bridge/blocks-api.js b/public/assets/js/bridge/blocks-api.js
index 6c66495..1dd1afc 100644
--- a/public/assets/js/bridge/blocks-api.js
+++ b/public/assets/js/bridge/blocks-api.js
@@ -469,7 +469,13 @@
? B.RTE_FONT_FACE_CSS.trim()
: '';
const cssPayload = (fontCss ? fontCss + '\n' : '') + editor.getCss();
- const htmlContent = editor.getHtml() + '';
+ const serializeHtml = () => {
+ if (B && B.BridgeRTE && typeof B.BridgeRTE.serializeHtml === 'function') {
+ return B.BridgeRTE.serializeHtml(editor);
+ }
+ return editor.getHtml();
+ };
+ const htmlContent = serializeHtml() + '';
// 2. KRITISCH: Holt die JSON-Repräsentation des Editors
let jsonProjectDataRaw = '';
try {
diff --git a/public/assets/js/bridge/rte-editor.js b/public/assets/js/bridge/rte-editor.js
index d79488f..f34a20f 100644
--- a/public/assets/js/bridge/rte-editor.js
+++ b/public/assets/js/bridge/rte-editor.js
@@ -1,779 +1,832 @@
/* /assets/js/bridge/rte-editor.js */
(function () {
const PluginName = 'bridge-rte-editor';
- const B = window.BridgeParts || (window.BridgeParts = {});
+ const editorInstances = new WeakMap();
- const log = (type, message, color = '#94a3b8', logType = 'info', force = false) => {
- if (typeof B.log === 'function') {
- B.log(PluginName, `[${type}] ${message}`, color, logType, force);
+ class BridgeRTE {
+ constructor(bridgeParts) {
+ this.B = bridgeParts || (window.BridgeParts || (window.BridgeParts = {}));
+ this.editor = null;
+ this.modalOpen = false;
+ this.allowClose = false;
+ this.lastContent = new WeakMap();
+ this.restoring = new WeakSet();
}
- };
- const escapeHtml = (text) => String(text || '')
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-
- const sanitizeInlineHtml = (rawHtml, fallbackText) => {
- const wrapper = document.createElement('div');
- wrapper.innerHTML = String(rawHtml || '');
-
- wrapper.querySelectorAll('script,style').forEach((node) => node.remove());
-
- const inlineTags = new Set(['A', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'BR', 'SUB', 'SUP', 'SPAN']);
- const blockTags = new Set([
- 'DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
- 'UL', 'OL', 'LI', 'TABLE', 'TBODY', 'THEAD', 'TFOOT', 'TR', 'TD', 'TH',
- 'SECTION', 'ARTICLE', 'ASIDE', 'HEADER', 'FOOTER',
- ]);
-
- const unwrap = (el, addBreak) => {
- const frag = document.createDocumentFragment();
- if (addBreak) {
- const prev = el.previousSibling;
- const hasPrevContent = !!(prev && (prev.nodeType === 3 ? prev.textContent.trim() : (prev.tagName && prev.tagName !== 'BR')));
- if (hasPrevContent) {
- frag.appendChild(document.createElement('br'));
+ static serializeHtml(editor) {
+ if (!editor || typeof editor.getHtml !== 'function') return '';
+ let html = editor.getHtml();
+ try {
+ const wrapper = editor.getWrapper && editor.getWrapper();
+ if (wrapper && wrapper.find) {
+ const candidates = wrapper.find('[data-gjs-type=\"text\"], [data-gjs-type=\"link\"], [data-gjs-type=\"button\"]');
+ candidates.forEach((model) => {
+ const content = model && model.get ? String(model.get('content') || '') : '';
+ const id = model && (model.getId ? model.getId() : model.get && model.get('id'));
+ const tag = (model && model.get && (model.get('tagName') || model.get('tag'))) || 'div';
+ if (!content || !id) return;
+ const tagName = String(tag).toLowerCase();
+ const rx = new RegExp(`<${tagName}([^>]*\\\\bid=[\"']${id}[\"'][^>]*)>([\\\\s\\\\S]*?)<\\\\/${tagName}>`, 'i');
+ html = html.replace(rx, `<${tagName}$1>${content}${tagName}>`);
+ });
}
- }
- while (el.firstChild) {
- frag.appendChild(el.firstChild);
- }
- el.replaceWith(frag);
- };
+ } catch {}
+ return html;
+ }
- Array.from(wrapper.querySelectorAll('*')).forEach((el) => {
- const tag = el.tagName;
- if (inlineTags.has(tag)) {
- Array.from(el.attributes).forEach((attr) => {
- const name = attr.name.toLowerCase();
- if (tag === 'a') {
- if (!['href', 'target', 'rel', 'style', 'class'].includes(name)) {
+ log(type, message, color = '#94a3b8', logType = 'info', force = false) {
+ if (typeof this.B.log === 'function') {
+ this.B.log(PluginName, `[${type}] ${message}`, color, logType, force);
+ }
+ }
+
+ escapeHtml(text) {
+ return String(text || '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ sanitizeInlineHtml(rawHtml, fallbackText) {
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = String(rawHtml || '');
+
+ wrapper.querySelectorAll('script,style').forEach((node) => node.remove());
+
+ const inlineTags = new Set(['A', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'BR', 'SUB', 'SUP', 'SPAN']);
+ const blockTags = new Set([
+ 'DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
+ 'UL', 'OL', 'LI', 'TABLE', 'TBODY', 'THEAD', 'TFOOT', 'TR', 'TD', 'TH',
+ 'SECTION', 'ARTICLE', 'ASIDE', 'HEADER', 'FOOTER',
+ ]);
+
+ const unwrap = (el, addBreak) => {
+ const frag = document.createDocumentFragment();
+ if (addBreak) {
+ const prev = el.previousSibling;
+ const hasPrevContent = !!(prev && (prev.nodeType === 3 ? prev.textContent.trim() : (prev.tagName && prev.tagName !== 'BR')));
+ if (hasPrevContent) {
+ frag.appendChild(document.createElement('br'));
+ }
+ }
+ while (el.firstChild) {
+ frag.appendChild(el.firstChild);
+ }
+ el.replaceWith(frag);
+ };
+
+ Array.from(wrapper.querySelectorAll('*')).forEach((el) => {
+ const tag = el.tagName;
+ if (inlineTags.has(tag)) {
+ Array.from(el.attributes).forEach((attr) => {
+ const name = attr.name.toLowerCase();
+ if (tag === 'a') {
+ if (!['href', 'target', 'rel', 'style', 'class'].includes(name)) {
+ el.removeAttribute(attr.name);
+ }
+ } else if (!['style', 'class'].includes(name)) {
el.removeAttribute(attr.name);
}
- } else if (!['style', 'class'].includes(name)) {
- el.removeAttribute(attr.name);
- }
- });
- return;
- }
- unwrap(el, blockTags.has(tag));
- });
-
- let html = wrapper.innerHTML
- .replace(/
/gi, '
')
- .trim();
- const brCount = (html.match(/
/g) || []).length;
- if (brCount <= 1) {
- html = html.replace(/(
)+$/g, '').trim();
- }
- if (!html) {
- const text = String(fallbackText || wrapper.textContent || '').trim();
- if (text) html = escapeHtml(text);
- }
- return html;
- };
-
- const isTextLike = (component) => !!(component && component.is && (component.is('text') || component.is('button') || component.is('link')));
-
- const logConsoleSnapshot = (editor, component, label) => {
- try {
- const viewEl = component && component.view ? component.view.el : null;
- const modelContent = component && component.get ? String(component.get('content') || '') : '';
- const editorHtml = editor && typeof editor.getHtml === 'function' ? String(editor.getHtml() || '') : '';
- const maxLen = 1000;
- console.log(`[RTE DEBUG] ${label}`, {
- modelId: component && (component.getId ? component.getId() : component.get && component.get('id')),
- modelType: component && component.get ? component.get('type') : undefined,
- modelContentLen: modelContent.length,
- modelContent: modelContent.slice(0, maxLen),
- viewHtmlLen: viewEl ? String(viewEl.innerHTML || '').length : 0,
- viewHtml: viewEl ? String(viewEl.innerHTML || '').slice(0, maxLen) : '',
- viewOuterLen: viewEl ? String(viewEl.outerHTML || '').length : 0,
- viewOuter: viewEl ? String(viewEl.outerHTML || '').slice(0, maxLen) : '',
- editorHtmlLen: editorHtml.length,
- editorHtml: editorHtml.slice(0, maxLen),
- });
- } catch {}
- };
-
- const applyContentToComponent = (editor, component, html) => {
- if (!component) return;
- const content = String(html || '');
- try {
- const isText = (component.is && component.is('text'))
- || (component.get && component.get('type') === 'text');
- if (isText && component.components) {
- const comps = component.components();
- if (comps && typeof comps.reset === 'function') {
- comps.reset();
- }
- }
- if (component.set) component.set('content', content);
- } catch {}
- if (component.view && component.view.el) {
- component.view.el.innerHTML = content;
- }
- if (component.view && typeof component.view.render === 'function') {
- component.view.render();
- }
- if (component.trigger) {
- component.trigger('change:content');
- component.trigger('change:components');
- }
- if (editor && typeof editor.trigger === 'function') {
- editor.trigger('component:update', component);
- }
- };
-
- const collectFrameCss = (editor) => {
- const cssParts = [];
- try {
- const frameDoc = editor && editor.Canvas && editor.Canvas.getDocument ? editor.Canvas.getDocument() : null;
- if (frameDoc) {
- frameDoc.querySelectorAll('style').forEach((styleEl) => {
- const css = String(styleEl.textContent || '').trim();
- if (css) cssParts.push(css);
- });
- frameDoc.querySelectorAll('link[rel="stylesheet"][href]').forEach((linkEl) => {
- const href = linkEl.getAttribute('href');
- if (!href) return;
- try {
- const abs = new URL(href, frameDoc.baseURI).href;
- cssParts.push(`@import url("${abs}");`);
- } catch {}
- });
- }
- } catch {}
- return cssParts.join('\n');
- };
-
- const getEditableTag = (component) => {
- const viewEl = component && component.view ? component.view.el : null;
- const raw = (component && component.get && (component.get('tagName') || component.get('tag')))
- || (viewEl && viewEl.tagName)
- || 'div';
- const tag = String(raw || 'div').toLowerCase();
- const allowed = new Set([
- 'div', 'p', 'span', 'a', 'button',
- 'strong', 'b', 'em', 'i', 'u', 's',
- 'sub', 'sup', 'ul', 'ol', 'li',
- ]);
- return allowed.has(tag) ? tag : 'div';
- };
-
- const applyComponentPreviewStyles = (component, content) => {
- if (!component || !content) return;
- const mergeStyles = () => {
- const out = {};
- const fromGetStyle = (component.getStyle && component.getStyle()) || {};
- const fromGetAttr = (component.get && component.get('style')) || {};
- const add = (obj) => {
- if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return;
- Object.entries(obj).forEach(([key, val]) => {
- if (val === undefined || val === null || val === '') return;
- out[key] = val;
- });
- };
- add(fromGetStyle);
- add(fromGetAttr);
- return out;
- };
- const styleObj = mergeStyles();
- const applyStyle = (prop, value) => {
- if (!value) return;
- try { content.style[prop] = value; } catch {}
- };
- const pick = (keys) => {
- for (const key of keys) {
- if (styleObj[key]) return styleObj[key];
- }
- return '';
- };
- applyStyle('fontFamily', pick(['font-family', 'fontFamily']));
- applyStyle('fontSize', pick(['font-size', 'fontSize']));
- applyStyle('fontWeight', pick(['font-weight', 'fontWeight']));
- applyStyle('fontStyle', pick(['font-style', 'fontStyle']));
- applyStyle('textDecoration', pick(['text-decoration', 'textDecoration', 'text-decoration-line']));
- applyStyle('color', pick(['color']));
- applyStyle('lineHeight', pick(['line-height', 'lineHeight']));
- applyStyle('letterSpacing', pick(['letter-spacing', 'letterSpacing']));
- applyStyle('textAlign', pick(['text-align', 'textAlign']));
- applyStyle('backgroundColor', pick(['background-color', 'backgroundColor']));
- };
-
- const applyComputedPreviewStyles = (component, content) => {
- try {
- const viewEl = component && component.view ? component.view.el : null;
- if (!viewEl || !viewEl.ownerDocument) return;
- const computed = viewEl.ownerDocument.defaultView
- ? viewEl.ownerDocument.defaultView.getComputedStyle(viewEl)
- : null;
- if (!computed) return;
- const map = {
- fontFamily: 'font-family',
- fontSize: 'font-size',
- fontWeight: 'font-weight',
- fontStyle: 'font-style',
- textDecoration: 'text-decoration-line',
- color: 'color',
- lineHeight: 'line-height',
- letterSpacing: 'letter-spacing',
- textAlign: 'text-align',
- backgroundColor: 'background-color',
- };
- Object.entries(map).forEach(([prop, cssProp]) => {
- const val = computed.getPropertyValue(cssProp);
- if (val && val.trim()) {
- try { content.style[prop] = val.trim(); } catch {}
- }
- });
- } catch {}
- };
-
- const openRichTextModal = (editor, component) => {
- if (!isTextLike(component)) {
- log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
- return;
- }
-
- const modal = editor && editor.Modal;
- if (!modal || editor.__bridgeRteModalOpen) return;
- editor.__bridgeRteModalOpen = true;
- editor.__bridgeRteAllowClose = false;
-
- if (!modal.__bridgeCloseGuarded) {
- modal.__bridgeCloseGuarded = true;
- modal.__bridgeOriginalClose = modal.close ? modal.close.bind(modal) : null;
- if (modal.close) {
- modal.close = function (...args) {
- if (editor.__bridgeRteAllowClose && modal.__bridgeOriginalClose) {
- return modal.__bridgeOriginalClose(...args);
- }
- return undefined;
- };
- }
- }
-
- try {
- const editing = editor.getEditing && editor.getEditing();
- if (editing && editing.model === component && editor.setEditing) {
- editor.setEditing(null);
- }
- if (editor.RichTextEditor && editor.RichTextEditor.disable && component.view && component.view.el) {
- editor.RichTextEditor.disable(component.view.el);
- }
- } catch {}
-
- const closeModal = () => {
- editor.__bridgeRteAllowClose = true;
- editor.__bridgeRteModalOpen = false;
- if (B.allowModalCloseOnce) B.allowModalCloseOnce();
- if (typeof modal.__bridgeOriginalClose === 'function') {
- modal.__bridgeOriginalClose();
- } else if (typeof modal.close === 'function') {
- modal.close();
- } else if (modal.getModel && modal.getModel().set) {
- modal.getModel().set('open', false);
- }
- editor.__bridgeRteAllowClose = false;
- };
-
- const doc = document;
- const container = doc.createElement('div');
- container.style.display = 'flex';
- container.style.flexDirection = 'column';
- container.style.gap = '10px';
- container.style.height = '100%';
- container.style.minHeight = '360px';
-
- const toolbar = doc.createElement('div');
- toolbar.style.display = 'flex';
- toolbar.style.flexWrap = 'wrap';
- toolbar.style.gap = '6px';
- toolbar.style.alignItems = 'center';
-
- const content = doc.createElement(getEditableTag(component));
- content.contentEditable = 'true';
- content.style.flex = '1';
- content.style.minHeight = '280px';
- content.style.border = '1px solid #cbd5f5';
- content.style.borderRadius = '6px';
- content.style.padding = '12px';
- content.style.background = '#ffffff';
- content.style.overflow = 'auto';
- content.style.fontFamily = 'Arial, sans-serif';
- content.style.fontSize = '14px';
- if (component && component.view && component.view.el) {
- const viewEl = component.view.el;
- if (viewEl.className) content.className = viewEl.className;
- if (viewEl.id) content.id = viewEl.id;
- }
-
- const modelContent = (component.get && component.get('content')) || '';
- const viewHtml = (component.view && component.view.el && component.view.el.innerHTML) || '';
- let htmlSource = viewHtml || modelContent || '';
- if (!htmlSource && component && typeof component.toHTML === 'function') {
- try {
- const fullHtml = String(component.toHTML() || '');
- if (fullHtml) {
- const wrapper = document.createElement('div');
- wrapper.innerHTML = fullHtml;
- const first = wrapper.firstElementChild;
- if (first && first.innerHTML !== undefined) {
- htmlSource = first.innerHTML;
- } else {
- htmlSource = wrapper.innerHTML;
- }
- }
- } catch {}
- }
- const initialHtml = sanitizeInlineHtml(htmlSource, modelContent || '');
- content.innerHTML = initialHtml;
- const existingStyle = component && component.get && component.get('style') ? component.get('style') : null;
- if (existingStyle && typeof existingStyle === 'object') {
- if (existingStyle.fontFamily) {
- content.style.fontFamily = existingStyle.fontFamily;
- }
- if (existingStyle.fontSize) {
- content.style.fontSize = existingStyle.fontSize;
- }
- }
- applyComponentPreviewStyles(component, content);
- applyComputedPreviewStyles(component, content);
-
- let savedRange = null;
- const saveSelection = () => {
- try {
- const sel = (content.ownerDocument || document).getSelection();
- if (!sel || sel.rangeCount === 0) return;
- const range = sel.getRangeAt(0);
- if (content.contains(range.commonAncestorContainer)) {
- savedRange = range.cloneRange();
- }
- } catch {}
- };
- const restoreSelection = () => {
- try {
- const sel = (content.ownerDocument || document).getSelection();
- if (!sel || !savedRange) return;
- sel.removeAllRanges();
- sel.addRange(savedRange);
- } catch {}
- };
- const exec = (cmd, value) => {
- try {
- content.focus();
- restoreSelection();
- const docRef = content.ownerDocument || document;
- docRef.execCommand(cmd, false, value);
- saveSelection();
- } catch {}
- };
- const getSelectionRange = () => {
- try {
- const docRef = content.ownerDocument || document;
- const sel = docRef.getSelection();
- if (!sel || sel.rangeCount === 0) return null;
- const range = sel.getRangeAt(0);
- if (!content.contains(range.commonAncestorContainer)) return null;
- return range;
- } catch {
- return null;
- }
- };
- const applyInlineStyle = (styleProp, value) => {
- try {
- content.focus();
- restoreSelection();
- const docRef = content.ownerDocument || document;
- const range = getSelectionRange();
- if (!range || range.collapsed) return;
- const wrapper = docRef.createElement('span');
- wrapper.style[styleProp] = value;
- const fragment = range.extractContents();
- wrapper.appendChild(fragment);
- range.insertNode(wrapper);
- const sel = docRef.getSelection();
- if (sel) {
- sel.removeAllRanges();
- const newRange = docRef.createRange();
- newRange.selectNodeContents(wrapper);
- sel.addRange(newRange);
- }
- saveSelection();
- } catch {}
- };
-
- const addButton = (labelHtml, title, cmd, valueGetter, handler) => {
- const btn = doc.createElement('button');
- btn.type = 'button';
- btn.innerHTML = labelHtml;
- btn.title = title;
- btn.setAttribute('aria-label', title);
- btn.style.padding = '4px 8px';
- btn.style.border = '1px solid #cbd5f5';
- btn.style.borderRadius = '4px';
- btn.style.background = '#f8fafc';
- btn.style.cursor = 'pointer';
- btn.addEventListener('mousedown', (evt) => {
- evt.preventDefault();
- saveSelection();
- });
- btn.addEventListener('click', () => {
- if (typeof handler === 'function') {
- handler();
- return;
- }
- const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
- if (value === null || value === undefined) return;
- if (cmd === 'createLink' && !value) return;
- exec(cmd, value);
- });
- toolbar.appendChild(btn);
- };
-
- const addSelect = (options, title, onChange) => {
- const select = doc.createElement('select');
- select.title = title;
- select.setAttribute('aria-label', title);
- select.style.padding = '4px 8px';
- select.style.border = '1px solid #cbd5f5';
- select.style.borderRadius = '4px';
- select.style.background = '#ffffff';
- options.forEach((opt) => {
- const optEl = doc.createElement('option');
- optEl.value = opt.value;
- optEl.textContent = opt.label;
- select.appendChild(optEl);
- });
- select.addEventListener('mousedown', () => {
- saveSelection();
- });
- select.addEventListener('change', () => {
- const value = select.value;
- if (value) onChange(value);
- });
- toolbar.appendChild(select);
- return select;
- };
-
- const icon = (path) => ``;
- addButton(
- icon('M6 4h5a3 3 0 0 1 0 6H6V4zm0 8h6a3 3 0 0 1 0 6H6v-6z'),
- 'Fett',
- 'bold',
- null,
- () => applyInlineStyle('fontWeight', '700')
- );
- addButton(
- icon('M10 4h8v2h-3l-4 12h3v2H6v-2h3l4-12h-3V4z'),
- 'Kursiv',
- 'italic',
- null,
- () => applyInlineStyle('fontStyle', 'italic')
- );
- addButton(
- icon('M5 4h14v2h-6v3h4a4 4 0 0 1 0 8H7v-2h10a2 2 0 0 0 0-4h-4V6H5V4z'),
- 'Unterstrichen',
- 'underline',
- null,
- () => applyInlineStyle('textDecoration', 'underline')
- );
- addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
- addButton(icon('M4 6h4v2H4V6zm0 4h4v2H4v-2zm0 4h4v2H4v-2zm6-8h10v2H10V6zm0 4h10v2H10v-2zm0 4h10v2H10v-2z'), 'Liste (geordnet)', 'insertOrderedList');
- addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
- addButton(icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
- addButton(icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbuendig', 'justifyRight');
- addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
- addButton(icon('M7 7h10l-1.5 1.5-3-3-5.5 5.5v5h5l5.5-5.5-3-3L17 7z'), 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://'));
- addButton(icon('M7 7h4v2H7v4H5V9a2 2 0 0 1 2-2zm10 0a2 2 0 0 1 2 2v4h-2V9h-4V7h4zm0 10h-4v-2h4v-4h2v4a2 2 0 0 1-2 2zm-10 0a2 2 0 0 1-2-2v-4h2v4h4v2H7z'), 'Link entfernen', 'unlink');
- addButton(icon('M7 6h10v2H7zM9 10h6v2H9zM10 14h4v2h-4z'), 'Tiefgestellt', 'subscript');
- addButton(icon('M7 6h10v2H7zM9 10h6v2H9zM8 14h8v2H8z'), 'Hochgestellt', 'superscript');
- addButton(icon('M7 4h4v4H7V4zm6 12h4v4h-4v-4zM7 10h10v2H7v-2z'), 'Einzug', 'indent');
- addButton(icon('M13 4h4v4h-4V4zM7 16h4v4H7v-4zM7 10h10v2H7v-2z'), 'Ausruecken', 'outdent');
- addButton(icon('M5 5h14v2H5zM5 9h10v2H5zM5 13h14v2H5zM5 17h10v2H5z'), 'Formatierung entfernen', 'removeFormat');
-
- const fontOptions = (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length)
- ? B.RTE_FONTS
- : [
- { label: 'Arial', value: 'Arial, sans-serif' },
- { label: 'Calibri', value: 'Calibri, sans-serif' },
- { label: 'Cambria', value: 'Cambria, serif' },
- { label: 'Georgia', value: 'Georgia, serif' },
- { label: 'Tahoma', value: 'Tahoma, sans-serif' },
- { label: 'Times New Roman', value: 'Times New Roman, serif' },
- { label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
- { label: 'Verdana', value: 'Verdana, sans-serif' },
- ];
- const applyComponentStyle = (styleObj, opts = {}) => {
- try {
- if (opts.preview && content) {
- Object.entries(styleObj).forEach(([key, val]) => {
- content.style[key] = val;
});
return;
}
- const normalizeKeys = (obj) => {
- const out = {};
- Object.entries(obj || {}).forEach(([key, val]) => {
- if (key === 'fontFamily') out['font-family'] = val;
- else if (key === 'fontSize') out['font-size'] = val;
- else out[key] = val;
- });
- return out;
- };
- if (component && component.setStyle && component.getStyle) {
- const current = component.getStyle() || {};
- const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
- component.setStyle({ ...safeCurrent, ...normalizeKeys(styleObj) });
- } else if (component && component.set) {
- const current = component.get && component.get('style') ? component.get('style') : {};
- const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
- component.set('style', { ...safeCurrent, ...normalizeKeys(styleObj) });
+ unwrap(el, blockTags.has(tag));
+ });
+
+ let html = wrapper.innerHTML
+ .replace(/
/gi, '
')
+ .trim();
+ const brCount = (html.match(/
/g) || []).length;
+ if (brCount <= 1) {
+ html = html.replace(/(
)+$/g, '').trim();
+ }
+ if (!html) {
+ const text = String(fallbackText || wrapper.textContent || '').trim();
+ if (text) html = this.escapeHtml(text);
+ }
+ return html;
+ }
+
+ isTextLike(component) {
+ return !!(component && component.is && (component.is('text') || component.is('button') || component.is('link')));
+ }
+
+ logConsoleSnapshot(editor, component, label) {
+ try {
+ const viewEl = component && component.view ? component.view.el : null;
+ const modelContent = component && component.get ? String(component.get('content') || '') : '';
+ const editorHtml = editor && typeof editor.getHtml === 'function' ? String(editor.getHtml() || '') : '';
+ const maxLen = 1000;
+ console.log(`[RTE DEBUG] ${label}`, {
+ modelId: component && (component.getId ? component.getId() : component.get && component.get('id')),
+ modelType: component && component.get ? component.get('type') : undefined,
+ modelContentLen: modelContent.length,
+ modelContent: modelContent.slice(0, maxLen),
+ viewHtmlLen: viewEl ? String(viewEl.innerHTML || '').length : 0,
+ viewHtml: viewEl ? String(viewEl.innerHTML || '').slice(0, maxLen) : '',
+ viewOuterLen: viewEl ? String(viewEl.outerHTML || '').length : 0,
+ viewOuter: viewEl ? String(viewEl.outerHTML || '').slice(0, maxLen) : '',
+ editorHtmlLen: editorHtml.length,
+ editorHtml: editorHtml.slice(0, maxLen),
+ });
+ } catch {}
+ }
+
+ applyContentToComponent(editor, component, html) {
+ if (!component) return;
+ const content = String(html || '');
+ try {
+ const isText = (component.is && component.is('text'))
+ || (component.get && component.get('type') === 'text');
+ if (isText && component.components) {
+ const comps = component.components();
+ if (comps && typeof comps.reset === 'function') {
+ comps.reset();
+ }
}
- if (component && component.view && component.view.el) {
- Object.entries(styleObj).forEach(([key, val]) => {
- component.view.el.style[key] = val;
+ if (component.set) component.set('content', content);
+ } catch {}
+ if (component.view && component.view.el) {
+ component.view.el.innerHTML = content;
+ }
+ if (component.view && typeof component.view.render === 'function') {
+ component.view.render();
+ }
+ if (component.trigger) {
+ component.trigger('change:content');
+ component.trigger('change:components');
+ }
+ if (editor && typeof editor.trigger === 'function') {
+ editor.trigger('component:update', component);
+ }
+ }
+
+ collectFrameCss(editor) {
+ const cssParts = [];
+ try {
+ const frameDoc = editor && editor.Canvas && editor.Canvas.getDocument ? editor.Canvas.getDocument() : null;
+ if (frameDoc) {
+ frameDoc.querySelectorAll('style').forEach((styleEl) => {
+ const css = String(styleEl.textContent || '').trim();
+ if (css) cssParts.push(css);
+ });
+ frameDoc.querySelectorAll('link[rel="stylesheet"][href]').forEach((linkEl) => {
+ const href = linkEl.getAttribute('href');
+ if (!href) return;
+ try {
+ const abs = new URL(href, frameDoc.baseURI).href;
+ cssParts.push(`@import url("${abs}");`);
+ } catch {}
});
}
} catch {}
- };
-
- const pendingComponentStyle = {};
- const fontSelect = addSelect([{ label: 'Schriftart', value: '' }, ...fontOptions], 'Schriftart', (value) => {
- if (!value) return;
- pendingComponentStyle.fontFamily = value;
- applyComponentStyle({ fontFamily: value }, { preview: true });
- });
- if (existingStyle && existingStyle.fontFamily && fontSelect) {
- fontSelect.value = existingStyle.fontFamily;
- }
- addSelect([
- { label: 'Groesse', value: '' },
- { label: '10px', value: '10' },
- { label: '12px', value: '12' },
- { label: '14px', value: '14' },
- { label: '16px', value: '16' },
- { label: '18px', value: '18' },
- { label: '24px', value: '24' },
- { label: '32px', value: '32' },
- { label: 'Custom…', value: '__custom__' },
- ], 'Schriftgroesse', (value) => {
- if (value === '__custom__') {
- const raw = prompt('Schriftgroesse in px', '14');
- const num = Number(raw || 0);
- if (!Number.isFinite(num) || num <= 0) return;
- const range = getSelectionRange();
- if (range && !range.collapsed) {
- applyInlineStyle('fontSize', `${num}px`);
- } else {
- pendingComponentStyle.fontSize = `${num}px`;
- applyComponentStyle({ fontSize: `${num}px` }, { preview: true });
- }
- return;
- }
- const range = getSelectionRange();
- if (range && !range.collapsed) {
- applyInlineStyle('fontSize', `${value}px`);
- return;
- }
- pendingComponentStyle.fontSize = `${value}px`;
- applyComponentStyle({ fontSize: `${value}px` }, { preview: true });
- });
-
- // Emoji-Picker entfernt (auf Wunsch) – kann spaeter als echter Picker wiederkommen.
-
- const injectedStyle = doc.createElement('style');
- injectedStyle.setAttribute('data-bridge-rte-style', '1');
- const fontCss = (B && typeof B.RTE_FONT_FACE_CSS === 'string' && B.RTE_FONT_FACE_CSS.trim())
- ? B.RTE_FONT_FACE_CSS.trim()
- : '';
- let editorCss = '';
- try {
- editorCss = editor && typeof editor.getCss === 'function' ? String(editor.getCss() || '') : '';
- } catch {}
- const frameCss = collectFrameCss(editor);
- injectedStyle.textContent = `${fontCss}\n${editorCss}\n${frameCss}`.trim();
- if (injectedStyle.textContent) {
- container.appendChild(injectedStyle);
+ return cssParts.join('\n');
}
- container.appendChild(toolbar);
- container.appendChild(content);
- content.addEventListener('keyup', saveSelection);
- content.addEventListener('mouseup', saveSelection);
- content.addEventListener('focus', saveSelection);
+ getEditableTag(component) {
+ const viewEl = component && component.view ? component.view.el : null;
+ const raw = (component && component.get && (component.get('tagName') || component.get('tag')))
+ || (viewEl && viewEl.tagName)
+ || 'div';
+ const tag = String(raw || 'div').toLowerCase();
+ const allowed = new Set([
+ 'div', 'p', 'span', 'a', 'button',
+ 'strong', 'b', 'em', 'i', 'u', 's',
+ 'sub', 'sup', 'ul', 'ol', 'li',
+ ]);
+ return allowed.has(tag) ? tag : 'div';
+ }
- const actions = doc.createElement('div');
- actions.style.display = 'flex';
- actions.style.justifyContent = 'flex-end';
- actions.style.gap = '8px';
-
- const cancelBtn = doc.createElement('button');
- cancelBtn.type = 'button';
- cancelBtn.textContent = 'Abbrechen';
- cancelBtn.style.padding = '6px 12px';
- cancelBtn.style.border = '1px solid #cbd5f5';
- cancelBtn.style.borderRadius = '4px';
- cancelBtn.style.background = '#f8fafc';
- cancelBtn.style.cursor = 'pointer';
- cancelBtn.addEventListener('click', closeModal);
-
- const saveBtn = doc.createElement('button');
- saveBtn.type = 'button';
- saveBtn.textContent = 'Speichern';
- saveBtn.style.padding = '6px 12px';
- saveBtn.style.border = '1px solid #0ea5e9';
- saveBtn.style.borderRadius = '4px';
- saveBtn.style.background = '#0ea5e9';
- saveBtn.style.color = '#ffffff';
- saveBtn.style.cursor = 'pointer';
- saveBtn.addEventListener('click', () => {
- const rawHtml = content.innerHTML || '';
- const html = sanitizeInlineHtml(rawHtml, content.textContent || '');
- component.__bridgeRteLastContent = html;
- logConsoleSnapshot(editor, component, 'before-save');
- const forceApply = () => {
- if (Object.keys(pendingComponentStyle).length) {
- applyComponentStyle(pendingComponentStyle);
- }
- applyContentToComponent(editor, component, html);
- logConsoleSnapshot(editor, component, 'after-save');
+ applyComponentPreviewStyles(component, content) {
+ if (!component || !content) return;
+ const mergeStyles = () => {
+ const out = {};
+ const fromGetStyle = (component.getStyle && component.getStyle()) || {};
+ const fromGetAttr = (component.get && component.get('style')) || {};
+ const add = (obj) => {
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return;
+ Object.entries(obj).forEach(([key, val]) => {
+ if (val === undefined || val === null || val === '') return;
+ out[key] = val;
+ });
+ };
+ add(fromGetStyle);
+ add(fromGetAttr);
+ return out;
};
- forceApply();
- setTimeout(forceApply, 0);
- setTimeout(forceApply, 50);
- setTimeout(() => {
+ const styleObj = mergeStyles();
+ const applyStyle = (prop, value) => {
+ if (!value) return;
+ try { content.style[prop] = value; } catch {}
+ };
+ const pick = (keys) => {
+ for (const key of keys) {
+ if (styleObj[key]) return styleObj[key];
+ }
+ return '';
+ };
+ applyStyle('fontFamily', pick(['font-family', 'fontFamily']));
+ applyStyle('fontSize', pick(['font-size', 'fontSize']));
+ applyStyle('fontWeight', pick(['font-weight', 'fontWeight']));
+ applyStyle('fontStyle', pick(['font-style', 'fontStyle']));
+ applyStyle('textDecoration', pick(['text-decoration', 'textDecoration', 'text-decoration-line']));
+ applyStyle('color', pick(['color']));
+ applyStyle('lineHeight', pick(['line-height', 'lineHeight']));
+ applyStyle('letterSpacing', pick(['letter-spacing', 'letterSpacing']));
+ applyStyle('textAlign', pick(['text-align', 'textAlign']));
+ applyStyle('backgroundColor', pick(['background-color', 'backgroundColor']));
+ }
+
+ applyComputedPreviewStyles(component, content) {
+ try {
+ const viewEl = component && component.view ? component.view.el : null;
+ if (!viewEl || !viewEl.ownerDocument) return;
+ const computed = viewEl.ownerDocument.defaultView
+ ? viewEl.ownerDocument.defaultView.getComputedStyle(viewEl)
+ : null;
+ if (!computed) return;
+ const map = {
+ fontFamily: 'font-family',
+ fontSize: 'font-size',
+ fontWeight: 'font-weight',
+ fontStyle: 'font-style',
+ textDecoration: 'text-decoration-line',
+ color: 'color',
+ lineHeight: 'line-height',
+ letterSpacing: 'letter-spacing',
+ textAlign: 'text-align',
+ backgroundColor: 'background-color',
+ };
+ Object.entries(map).forEach(([prop, cssProp]) => {
+ const val = computed.getPropertyValue(cssProp);
+ if (val && val.trim()) {
+ try { content.style[prop] = val.trim(); } catch {}
+ }
+ });
+ } catch {}
+ }
+
+ openRichTextModal(editor, component) {
+ if (!this.isTextLike(component)) {
+ this.log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
+ return;
+ }
+
+ const modal = editor && editor.Modal;
+ if (!modal || this.modalOpen) return;
+ this.modalOpen = true;
+ this.allowClose = false;
+
+ try {
+ const editing = editor.getEditing && editor.getEditing();
+ if (editing && editing.model === component && editor.setEditing) {
+ editor.setEditing(null);
+ }
+ if (editor.RichTextEditor && editor.RichTextEditor.disable && component.view && component.view.el) {
+ editor.RichTextEditor.disable(component.view.el);
+ }
+ } catch {}
+
+ const closeModal = () => {
+ this.allowClose = true;
+ this.modalOpen = false;
+ if (this.B.allowModalCloseOnce) this.B.allowModalCloseOnce();
+ if (typeof modal.close === 'function') {
+ modal.close();
+ } else if (modal.getModel && modal.getModel().set) {
+ modal.getModel().set('open', false);
+ }
+ this.allowClose = false;
+ };
+
+ const doc = document;
+ const container = doc.createElement('div');
+ container.style.display = 'flex';
+ container.style.flexDirection = 'column';
+ container.style.gap = '10px';
+ container.style.height = '100%';
+ container.style.minHeight = '360px';
+ container.className = 'bridge-rte-container';
+ container.setAttribute('data-bridge-rte', 'container');
+
+ const toolbar = doc.createElement('div');
+ toolbar.style.display = 'flex';
+ toolbar.style.flexWrap = 'wrap';
+ toolbar.style.gap = '6px';
+ toolbar.style.alignItems = 'center';
+ toolbar.className = 'bridge-rte-toolbar';
+ toolbar.setAttribute('data-bridge-rte', 'toolbar');
+
+ const content = doc.createElement(this.getEditableTag(component));
+ content.contentEditable = 'true';
+ content.style.flex = '1';
+ content.style.minHeight = '280px';
+ content.style.border = '1px solid #cbd5f5';
+ content.style.borderRadius = '6px';
+ content.style.padding = '12px';
+ content.style.background = '#ffffff';
+ content.style.overflow = 'auto';
+ content.style.fontFamily = 'Arial, sans-serif';
+ content.style.fontSize = '14px';
+ content.className = [content.className, 'bridge-rte-content'].filter(Boolean).join(' ');
+ content.setAttribute('data-bridge-rte', 'content');
+ if (component && component.view && component.view.el) {
+ const viewEl = component.view.el;
+ if (viewEl.className) {
+ content.className = [content.className, viewEl.className].filter(Boolean).join(' ');
+ }
+ if (viewEl.id) content.id = viewEl.id;
+ }
+
+ const modelContent = (component.get && component.get('content')) || '';
+ const viewHtml = (component.view && component.view.el && component.view.el.innerHTML) || '';
+ let htmlSource = viewHtml || modelContent || '';
+ if (!htmlSource && component && typeof component.toHTML === 'function') {
try {
- if (component.view && component.view.el) {
- component.view.el.innerHTML = html;
+ const fullHtml = String(component.toHTML() || '');
+ if (fullHtml) {
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = fullHtml;
+ const first = wrapper.firstElementChild;
+ if (first && first.innerHTML !== undefined) {
+ htmlSource = first.innerHTML;
+ } else {
+ htmlSource = wrapper.innerHTML;
+ }
}
} catch {}
- }, 120);
- closeModal();
- });
+ }
+ const initialHtml = this.sanitizeInlineHtml(htmlSource, modelContent || '');
+ content.innerHTML = initialHtml;
+ const existingStyle = component && component.get && component.get('style') ? component.get('style') : null;
+ if (existingStyle && typeof existingStyle === 'object') {
+ if (existingStyle.fontFamily) {
+ content.style.fontFamily = existingStyle.fontFamily;
+ }
+ if (existingStyle.fontSize) {
+ content.style.fontSize = existingStyle.fontSize;
+ }
+ }
+ this.applyComponentPreviewStyles(component, content);
+ this.applyComputedPreviewStyles(component, content);
- actions.appendChild(cancelBtn);
- actions.appendChild(saveBtn);
- container.appendChild(actions);
-
- modal.setTitle('Richtext Editor');
- modal.setContent(container);
- const mdl = modal.getModel && modal.getModel();
- if (mdl && mdl.set) {
- mdl.set('closeOnEsc', false);
- mdl.set('closeOnClick', false);
- if (!mdl.__bridgeRteGuarded && typeof mdl.on === 'function') {
- mdl.__bridgeRteGuarded = true;
- mdl.on('change:open', () => {
- if (!mdl.get('open') && !editor.__bridgeRteAllowClose) {
- mdl.set('open', true, { silent: true });
+ let savedRange = null;
+ const saveSelection = () => {
+ try {
+ const sel = (content.ownerDocument || document).getSelection();
+ if (!sel || sel.rangeCount === 0) return;
+ const range = sel.getRangeAt(0);
+ if (content.contains(range.commonAncestorContainer)) {
+ savedRange = range.cloneRange();
}
- });
- }
- }
- if (modal.el) {
- modal.el.classList.add('bridge-rte-modal');
- const closeBtn = modal.el.querySelector('.gjs-mdl-btn-close');
- if (closeBtn) closeBtn.style.display = 'none';
- const backdrop = modal.el.querySelector('.gjs-mdl-dialog');
- if (backdrop) {
- backdrop.addEventListener('click', (evt) => {
- evt.stopPropagation();
- });
- }
- }
- const styleEl = document.getElementById('bridge-rte-modal-style') || document.createElement('style');
- styleEl.id = 'bridge-rte-modal-style';
- styleEl.textContent = '.bridge-rte-modal .gjs-mdl-btn-close{display:none!important;}';
- if (!styleEl.parentNode) {
- document.head.appendChild(styleEl);
- }
- modal.open();
- };
+ } catch {}
+ };
+ const restoreSelection = () => {
+ try {
+ const sel = (content.ownerDocument || document).getSelection();
+ if (!sel || !savedRange) return;
+ sel.removeAllRanges();
+ sel.addRange(savedRange);
+ } catch {}
+ };
+ const exec = (cmd, value) => {
+ try {
+ content.focus();
+ restoreSelection();
+ const docRef = content.ownerDocument || document;
+ docRef.execCommand(cmd, false, value);
+ saveSelection();
+ } catch {}
+ };
+ const getSelectionRange = () => {
+ try {
+ const docRef = content.ownerDocument || document;
+ const sel = docRef.getSelection();
+ if (!sel || sel.rangeCount === 0) return null;
+ const range = sel.getRangeAt(0);
+ if (!content.contains(range.commonAncestorContainer)) return null;
+ return range;
+ } catch {
+ return null;
+ }
+ };
+ const applyInlineStyle = (styleProp, value) => {
+ try {
+ content.focus();
+ restoreSelection();
+ const docRef = content.ownerDocument || document;
+ const range = getSelectionRange();
+ if (!range || range.collapsed) return;
+ const wrapper = docRef.createElement('span');
+ wrapper.style[styleProp] = value;
+ const fragment = range.extractContents();
+ wrapper.appendChild(fragment);
+ range.insertNode(wrapper);
+ const sel = docRef.getSelection();
+ if (sel) {
+ sel.removeAllRanges();
+ const newRange = docRef.createRange();
+ newRange.selectNodeContents(wrapper);
+ sel.addRange(newRange);
+ }
+ saveSelection();
+ } catch {}
+ };
- const ensureTextToolbarButton = (component) => {
- if (!isTextLike(component) || !component.get) return;
- const toolbar = component.get('toolbar') || [];
- if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
- toolbar.push({
- label: '',
- attributes: { title: 'Richtext bearbeiten' },
- command: 'bridge-open-richtext',
- });
- component.set('toolbar', toolbar);
- };
+ const addButton = (labelHtml, title, cmd, valueGetter, handler) => {
+ const btn = doc.createElement('button');
+ btn.type = 'button';
+ btn.innerHTML = labelHtml;
+ btn.title = title;
+ btn.setAttribute('aria-label', title);
+ btn.className = 'bridge-rte-btn';
+ btn.setAttribute('data-bridge-rte', 'button');
+ btn.style.padding = '4px 8px';
+ btn.style.border = '1px solid #cbd5f5';
+ btn.style.borderRadius = '4px';
+ btn.style.background = '#f8fafc';
+ btn.style.cursor = 'pointer';
+ btn.addEventListener('mousedown', (evt) => {
+ evt.preventDefault();
+ saveSelection();
+ });
+ btn.addEventListener('click', () => {
+ if (typeof handler === 'function') {
+ handler();
+ return;
+ }
+ const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
+ if (value === null || value === undefined) return;
+ if (cmd === 'createLink' && !value) return;
+ exec(cmd, value);
+ });
+ toolbar.appendChild(btn);
+ };
+
+ const addSelect = (options, title, onChange) => {
+ const select = doc.createElement('select');
+ select.title = title;
+ select.setAttribute('aria-label', title);
+ select.className = 'bridge-rte-select';
+ select.setAttribute('data-bridge-rte', 'select');
+ select.style.padding = '4px 8px';
+ select.style.border = '1px solid #cbd5f5';
+ select.style.borderRadius = '4px';
+ select.style.background = '#ffffff';
+ options.forEach((opt) => {
+ const optEl = doc.createElement('option');
+ optEl.value = opt.value;
+ optEl.textContent = opt.label;
+ select.appendChild(optEl);
+ });
+ select.addEventListener('mousedown', () => {
+ saveSelection();
+ });
+ select.addEventListener('change', () => {
+ const value = select.value;
+ if (value) onChange(value);
+ });
+ toolbar.appendChild(select);
+ return select;
+ };
+
+ const applyComponentStyle = (styleObj, opts = {}) => {
+ try {
+ if (opts.preview && content) {
+ Object.entries(styleObj).forEach(([key, val]) => {
+ content.style[key] = val;
+ });
+ return;
+ }
+ const normalizeKeys = (obj) => {
+ const out = {};
+ Object.entries(obj || {}).forEach(([key, val]) => {
+ if (key === 'fontFamily') out['font-family'] = val;
+ else if (key === 'fontSize') out['font-size'] = val;
+ else out[key] = val;
+ });
+ return out;
+ };
+ if (component && component.setStyle && component.getStyle) {
+ const current = component.getStyle() || {};
+ const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
+ component.setStyle({ ...safeCurrent, ...normalizeKeys(styleObj) });
+ } else if (component && component.set) {
+ const current = component.get && component.get('style') ? component.get('style') : {};
+ const safeCurrent = (current && typeof current === 'object' && !Array.isArray(current)) ? current : {};
+ component.set('style', { ...safeCurrent, ...normalizeKeys(styleObj) });
+ }
+ if (component && component.view && component.view.el) {
+ Object.entries(styleObj).forEach(([key, val]) => {
+ component.view.el.style[key] = val;
+ });
+ }
+ } catch {}
+ };
+
+ const pendingComponentStyle = {};
+ const ui = this.B.RTE_UI || {};
+ const ctx = {
+ editor,
+ component,
+ modal,
+ toolbar,
+ content,
+ addButton,
+ addSelect,
+ exec,
+ applyInlineStyle,
+ getSelectionRange,
+ applyComponentStyle,
+ pendingComponentStyle,
+ saveSelection,
+ restoreSelection,
+ };
+ if (typeof ui.beforeOpen === 'function') {
+ try { ui.beforeOpen(ctx); } catch {}
+ }
+ if (typeof ui.buildToolbar === 'function') {
+ try { ui.buildToolbar(ctx); } catch {}
+ }
+ if (!ui.overrideToolbar) {
+ const icon = (path) => ``;
+ addButton(
+ icon('M6 4h5a3 3 0 0 1 0 6H6V4zm0 8h6a3 3 0 0 1 0 6H6v-6z'),
+ 'Fett',
+ 'bold',
+ null,
+ () => applyInlineStyle('fontWeight', '700')
+ );
+ addButton(
+ icon('M10 4h8v2h-3l-4 12h3v2H6v-2h3l4-12h-3V4z'),
+ 'Kursiv',
+ 'italic',
+ null,
+ () => applyInlineStyle('fontStyle', 'italic')
+ );
+ addButton(
+ icon('M5 4h14v2h-6v3h4a4 4 0 0 1 0 8H7v-2h10a2 2 0 0 0 0-4h-4V6H5V4z'),
+ 'Unterstrichen',
+ 'underline',
+ null,
+ () => applyInlineStyle('textDecoration', 'underline')
+ );
+ addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
+ addButton(icon('M4 6h4v2H4V6zm0 4h4v2H4v-2zm0 4h4v2H4v-2zm6-8h10v2H10V6zm0 4h10v2H10v-2zm0 4h10v2H10v-2z'), 'Liste (geordnet)', 'insertOrderedList');
+ addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
+ addButton(icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
+ addButton(icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbuendig', 'justifyRight');
+ addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
+ addButton(icon('M7 7h10l-1.5 1.5-3-3-5.5 5.5v5h5l5.5-5.5-3-3L17 7z'), 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://'));
+ addButton(icon('M7 7h4v2H7v4H5V9a2 2 0 0 1 2-2zm10 0a2 2 0 0 1 2 2v4h-2V9h-4V7h4zm0 10h-4v-2h4v-4h2v4a2 2 0 0 1-2 2zm-10 0a2 2 0 0 1-2-2v-4h2v4h4v2H7z'), 'Link entfernen', 'unlink');
+ addButton(icon('M7 6h10v2H7zM9 10h6v2H9zM10 14h4v2h-4z'), 'Tiefgestellt', 'subscript');
+ addButton(icon('M7 6h10v2H7zM9 10h6v2H9zM8 14h8v2H8z'), 'Hochgestellt', 'superscript');
+ addButton(icon('M7 4h4v4H7V4zm6 12h4v4h-4v-4zM7 10h10v2H7v-2z'), 'Einzug', 'indent');
+ addButton(icon('M13 4h4v4h-4V4zM7 16h4v4H7v-4zM7 10h10v2H7v-2z'), 'Ausruecken', 'outdent');
+ addButton(icon('M5 5h14v2H5zM5 9h10v2H5zM5 13h14v2H5zM5 17h10v2H5z'), 'Formatierung entfernen', 'removeFormat');
+
+ const fontOptions = (this.B.RTE_FONTS && Array.isArray(this.B.RTE_FONTS) && this.B.RTE_FONTS.length)
+ ? this.B.RTE_FONTS
+ : [
+ { label: 'Arial', value: 'Arial, sans-serif' },
+ { label: 'Calibri', value: 'Calibri, sans-serif' },
+ { label: 'Cambria', value: 'Cambria, serif' },
+ { label: 'Georgia', value: 'Georgia, serif' },
+ { label: 'Tahoma', value: 'Tahoma, sans-serif' },
+ { label: 'Times New Roman', value: 'Times New Roman, serif' },
+ { label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
+ { label: 'Verdana', value: 'Verdana, sans-serif' },
+ ];
+ const fontSelect = addSelect([{ label: 'Schriftart', value: '' }, ...fontOptions], 'Schriftart', (value) => {
+ if (!value) return;
+ pendingComponentStyle.fontFamily = value;
+ applyComponentStyle({ fontFamily: value }, { preview: true });
+ });
+ if (existingStyle && existingStyle.fontFamily && fontSelect) {
+ fontSelect.value = existingStyle.fontFamily;
+ }
+ addSelect([
+ { label: 'Groesse', value: '' },
+ { label: '10px', value: '10' },
+ { label: '12px', value: '12' },
+ { label: '14px', value: '14' },
+ { label: '16px', value: '16' },
+ { label: '18px', value: '18' },
+ { label: '24px', value: '24' },
+ { label: '32px', value: '32' },
+ { label: 'Custom…', value: '__custom__' },
+ ], 'Schriftgroesse', (value) => {
+ if (value === '__custom__') {
+ const raw = prompt('Schriftgroesse in px', '14');
+ const num = Number(raw || 0);
+ if (!Number.isFinite(num) || num <= 0) return;
+ const range = getSelectionRange();
+ if (range && !range.collapsed) {
+ applyInlineStyle('fontSize', `${num}px`);
+ } else {
+ pendingComponentStyle.fontSize = `${num}px`;
+ applyComponentStyle({ fontSize: `${num}px` }, { preview: true });
+ }
+ return;
+ }
+ const range = getSelectionRange();
+ if (range && !range.collapsed) {
+ applyInlineStyle('fontSize', `${value}px`);
+ return;
+ }
+ pendingComponentStyle.fontSize = `${value}px`;
+ applyComponentStyle({ fontSize: `${value}px` }, { preview: true });
+ });
+ }
+
+ const injectedStyle = doc.createElement('style');
+ injectedStyle.setAttribute('data-bridge-rte-style', '1');
+ const fontCss = (this.B && typeof this.B.RTE_FONT_FACE_CSS === 'string' && this.B.RTE_FONT_FACE_CSS.trim())
+ ? this.B.RTE_FONT_FACE_CSS.trim()
+ : '';
+ let editorCss = '';
+ try {
+ editorCss = editor && typeof editor.getCss === 'function' ? String(editor.getCss() || '') : '';
+ } catch {}
+ const frameCss = this.collectFrameCss(editor);
+ injectedStyle.textContent = `${fontCss}\n${editorCss}\n${frameCss}`.trim();
+ if (injectedStyle.textContent) {
+ container.appendChild(injectedStyle);
+ }
+
+ container.appendChild(toolbar);
+ container.appendChild(content);
+ content.addEventListener('keyup', saveSelection);
+ content.addEventListener('mouseup', saveSelection);
+ content.addEventListener('focus', saveSelection);
+
+ const actions = doc.createElement('div');
+ actions.style.display = 'flex';
+ actions.style.justifyContent = 'flex-end';
+ actions.style.gap = '8px';
+ actions.className = 'bridge-rte-actions';
+ actions.setAttribute('data-bridge-rte', 'actions');
+
+ const cancelBtn = doc.createElement('button');
+ cancelBtn.type = 'button';
+ cancelBtn.textContent = 'Abbrechen';
+ cancelBtn.className = 'bridge-rte-btn bridge-rte-btn-cancel';
+ cancelBtn.style.padding = '6px 12px';
+ cancelBtn.style.border = '1px solid #cbd5f5';
+ cancelBtn.style.borderRadius = '4px';
+ cancelBtn.style.background = '#f8fafc';
+ cancelBtn.style.cursor = 'pointer';
+ cancelBtn.addEventListener('click', closeModal);
+
+ const saveBtn = doc.createElement('button');
+ saveBtn.type = 'button';
+ saveBtn.textContent = 'Speichern';
+ saveBtn.className = 'bridge-rte-btn bridge-rte-btn-save';
+ saveBtn.style.padding = '6px 12px';
+ saveBtn.style.border = '1px solid #0ea5e9';
+ saveBtn.style.borderRadius = '4px';
+ saveBtn.style.background = '#0ea5e9';
+ saveBtn.style.color = '#ffffff';
+ saveBtn.style.cursor = 'pointer';
+ saveBtn.addEventListener('click', () => {
+ const rawHtml = content.innerHTML || '';
+ const html = this.sanitizeInlineHtml(rawHtml, content.textContent || '');
+ this.lastContent.set(component, html);
+ this.logConsoleSnapshot(editor, component, 'before-save');
+ const forceApply = () => {
+ if (Object.keys(pendingComponentStyle).length) {
+ applyComponentStyle(pendingComponentStyle);
+ }
+ this.applyContentToComponent(editor, component, html);
+ this.logConsoleSnapshot(editor, component, 'after-save');
+ };
+ forceApply();
+ setTimeout(forceApply, 0);
+ setTimeout(forceApply, 50);
+ setTimeout(() => {
+ try {
+ if (component.view && component.view.el) {
+ component.view.el.innerHTML = html;
+ }
+ } catch {}
+ }, 120);
+ closeModal();
+ });
+
+ if (typeof ui.buildFooter === 'function') {
+ try { ui.buildFooter({ ...ctx, actions, cancelBtn, saveBtn }); } catch {}
+ }
+ if (!ui.overrideFooter) {
+ actions.appendChild(cancelBtn);
+ actions.appendChild(saveBtn);
+ }
+ container.appendChild(actions);
+
+ modal.setTitle('Richtext Editor');
+ modal.setContent(container);
+ const mdl = modal.getModel && modal.getModel();
+ if (mdl && mdl.set) {
+ mdl.set('closeOnEsc', false);
+ mdl.set('closeOnClick', false);
+ }
+ if (modal.el) {
+ modal.el.classList.add('bridge-rte-modal');
+ const closeBtn = modal.el.querySelector('.gjs-mdl-btn-close');
+ if (closeBtn) closeBtn.style.display = 'none';
+ const backdrop = modal.el.querySelector('.gjs-mdl-dialog');
+ if (backdrop) {
+ backdrop.addEventListener('click', (evt) => {
+ evt.stopPropagation();
+ });
+ }
+ }
+ const styleEl = document.getElementById('bridge-rte-modal-style') || document.createElement('style');
+ styleEl.id = 'bridge-rte-modal-style';
+ styleEl.textContent = '.bridge-rte-modal .gjs-mdl-btn-close{display:none!important;}';
+ if (!styleEl.parentNode) {
+ document.head.appendChild(styleEl);
+ }
+ if (typeof ui.afterOpen === 'function') {
+ try { ui.afterOpen(ctx); } catch {}
+ }
+ modal.open();
+ }
+
+ ensureTextToolbarButton(component) {
+ if (!this.isTextLike(component) || !component.get) return;
+ const toolbar = component.get('toolbar') || [];
+ if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return;
+ toolbar.push({
+ label: '',
+ attributes: { title: 'Richtext bearbeiten' },
+ command: 'bridge-open-richtext',
+ });
+ component.set('toolbar', toolbar);
+ }
+
+ setupEditor(editor) {
+ if (!editor) return;
+ this.editor = editor;
+
+ if (editor.Commands && editor.Commands.add) {
+ editor.Commands.add('bridge-open-richtext', {
+ run: (ed, sender, opts = {}) => {
+ if (sender && sender.set) sender.set('active', 0);
+ const component = opts.component || ed.getSelected();
+ this.openRichTextModal(ed, component);
+ },
+ });
+ }
+
+ const cfg = editor.getConfig ? editor.getConfig() : {};
+ cfg.richTextEditor = cfg.richTextEditor || {};
+ const actions = Array.isArray(cfg.richTextEditor.actions)
+ ? cfg.richTextEditor.actions.slice()
+ : ['bold', 'italic', 'underline', 'strikethrough', 'link'];
+ if (!actions.includes('bridge-open-richtext')) actions.push('bridge-open-richtext');
+ cfg.richTextEditor.actions = actions;
+
+ const restoreIfCollapsed = (model) => {
+ if (!model || this.restoring.has(model)) return;
+ const last = this.lastContent.get(model);
+ if (!last || !model.get) return;
+ const current = String(model.get('content') || '').trim();
+ if (!current || current === '