diff --git a/public/assets/js/bridge/rte-editor.js b/public/assets/js/bridge/rte-editor.js
index 2cea681..cb963f2 100644
--- a/public/assets/js/bridge/rte-editor.js
+++ b/public/assets/js/bridge/rte-editor.js
@@ -9,575 +9,329 @@
}
};
- const execRteCommand = (rte, cmd, value, docOverride) => {
- try {
- if (rte && typeof rte.exec === 'function') {
- rte.exec(cmd, value);
- return true;
- }
- } catch {}
- try {
- const doc = docOverride
- || rte?.doc
- || rte?.el?.ownerDocument
- || document;
- if (rte?.el && typeof rte.el.focus === 'function') {
- rte.el.focus();
- }
- const ok = doc.execCommand(cmd, false, value);
- if (ok === false && cmd === 'insertText') {
- doc.execCommand('insertHTML', false, String(value || '').replace(//g, '>'));
- }
- return true;
- } catch {}
- return false;
+ 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();
+ while (el.firstChild) {
+ frag.appendChild(el.firstChild);
+ }
+ if (addBreak) {
+ frag.appendChild(document.createElement('br'));
+ }
+ el.replaceWith(frag);
};
- const normalizeRteActionList = (editor, names) => {
- const cfg = editor.getConfig ? editor.getConfig() : {};
- cfg.richTextEditor = cfg.richTextEditor || {};
- const base = Array.isArray(cfg.richTextEditor.actions)
- ? cfg.richTextEditor.actions.slice()
- : ['bold', 'italic', 'underline', 'strikethrough', 'link'];
- names.forEach((name) => {
- const exists = base.some((item) => (typeof item === 'string' ? item === name : item && item.name === name));
- if (!exists) base.push(name);
- });
- cfg.richTextEditor.actions = base;
- };
-
- const ensureTextToolbarButton = (editor, component) => {
- if (!component || !component.is) return;
- const isTextLike = component.is('text') || component.is('button') || component.is('link');
- if (!isTextLike) return;
- const toolbar = (component.get && 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 && component.set('toolbar', toolbar);
- };
-
- const openRichTextModal = (editor, component) => {
- if (!component || !component.is || !(component.is('text') || component.is('button') || component.is('link'))) {
- log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888');
+ 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'].includes(name)) {
+ el.removeAttribute(attr.name);
+ }
+ } else if (name !== 'style') {
+ el.removeAttribute(attr.name);
+ }
+ });
return;
}
+ unwrap(el, blockTags.has(tag));
+ });
- const modal = editor.Modal;
- if (!modal) return;
- if (editor.__bridgeRteModalOpen) return;
- editor.__bridgeRteModalOpen = true;
- editor.__bridgeRteAllowClose = false;
- let reopenGuard = false;
- const closeModal = () => {
- editor.__bridgeRteAllowClose = true;
- editor.__bridgeRteModalOpen = false;
- if (typeof modal.__bridgeOriginalClose === 'function') {
- modal.__bridgeOriginalClose();
- return;
- }
- if (typeof modal.close === 'function') {
- modal.close();
- return;
- }
- const mdl = modal.getModel && modal.getModel();
- if (mdl && typeof mdl.set === 'function') {
- mdl.set('open', false);
- } else if (modal.el) {
- modal.el.style.display = 'none';
- }
- };
+ let html = wrapper.innerHTML
+ .replace(/
/gi, '
')
+ .replace(/(
)+$/g, '')
+ .trim();
+ if (!html) {
+ const text = String(fallbackText || wrapper.textContent || '').trim();
+ if (text) html = escapeHtml(text);
+ }
+ return html;
+ };
- if (!modal.__bridgeCloseLocked) {
- modal.__bridgeCloseLocked = true;
- modal.__bridgeOriginalClose = modal.close ? modal.close.bind(modal) : null;
- modal.close = function (...args) {
- if (!editor.__bridgeRteModalOpen && typeof modal.__bridgeOriginalClose === 'function') {
- return modal.__bridgeOriginalClose(...args);
- }
- if (editor.__bridgeRteAllowClose && typeof modal.__bridgeOriginalClose === 'function') {
- editor.__bridgeRteAllowClose = false;
- return modal.__bridgeOriginalClose(...args);
- }
- if (!editor.__bridgeRteModalOpen && !modal.__bridgeOriginalClose) {
- const mdl = modal.getModel && modal.getModel();
- if (mdl && typeof mdl.set === 'function') {
- mdl.set('open', false);
- } else if (modal.el) {
- modal.el.style.display = 'none';
- }
- }
- if (editor.__bridgeRteAllowClose && !modal.__bridgeOriginalClose) {
- editor.__bridgeRteAllowClose = false;
- const mdl = modal.getModel && modal.getModel();
- if (mdl && typeof mdl.set === 'function') {
- mdl.set('open', false);
- } else if (modal.el) {
- modal.el.style.display = 'none';
- }
- }
- };
+ const isTextLike = (component) => !!(component && component.is && (component.is('text') || component.is('button') || component.is('link')));
+
+ const applyContentToComponent = (editor, component, html) => {
+ if (!component) return;
+ const content = String(html || '');
+ if (component.set) component.set('content', content);
+ 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 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;
+
+ const closeModal = () => {
+ editor.__bridgeRteModalOpen = false;
+ if (typeof modal.close === 'function') {
+ modal.close();
+ } else if (modal.getModel && modal.getModel().set) {
+ modal.getModel().set('open', 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 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 toolbar = doc.createElement('div');
+ toolbar.style.display = 'flex';
+ toolbar.style.flexWrap = 'wrap';
+ toolbar.style.gap = '6px';
+ toolbar.style.alignItems = 'center';
- const content = doc.createElement('div');
- 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';
+ const content = doc.createElement('div');
+ 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';
- const initialHtml = (component.view && component.view.el && component.view.el.innerHTML)
- || (component.get && component.get('content'))
- || '';
- content.innerHTML = initialHtml;
+ const initialHtml = (component.get && component.get('content'))
+ || (component.view && component.view.el && component.view.el.innerHTML)
+ || '';
+ content.innerHTML = initialHtml;
- const addButton = (labelHtml, title, cmd, valueGetter) => {
- const btn = doc.createElement('button');
- btn.type = 'button';
- btn.innerHTML = labelHtml;
- btn.title = 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('click', () => {
- content.focus();
- const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
- if (value === null || value === undefined) return;
- if (cmd === 'createLink' && !value) return;
- execRteCommand(null, cmd, value, content.ownerDocument);
- });
- toolbar.appendChild(btn);
- };
-
- const addSelect = (options, title, onChange) => {
- const select = doc.createElement('select');
- select.title = 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('change', () => {
- const value = select.value;
- content.focus();
- if (value) onChange(value);
- });
- toolbar.appendChild(select);
- return select;
- };
-
- const insertText = (text) => {
- content.focus();
- if (!execRteCommand(null, 'insertText', text)) {
- execRteCommand(null, 'insertHTML', String(text).replace(//g, '>'));
- }
- };
-
- const icon = (path) => ``;
- addButton('B', 'Fett', 'bold');
- addButton('I', 'Kursiv', 'italic');
- addButton('U', 'Unterstrichen', 'underline');
- addButton('S', 'Durchgestrichen', 'strikethrough');
- addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
- addButton(icon('M4 7h14v2H4zM4 11h14v2H4zM4 15h14v2H4z') + '1.', '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'), 'Rechtsbundig', 'justifyRight');
- addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
- addButton('Link', 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://'));
- addButton('Unlink', 'Link entfernen', 'unlink');
- addButton('Sub', 'Tiefgestellt', 'subscript');
- addButton('Sup', 'Hochgestellt', 'superscript');
- addButton('Einr.', 'Einzug', 'indent');
- addButton('Aus.', 'Ausruecken', 'outdent');
- addButton('Clear', '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' },
- ];
- addSelect([
- { label: 'Schriftart', value: '' },
- ...fontOptions,
- ], 'Schriftart', (value) => execRteCommand(null, 'fontName', value, content.ownerDocument));
-
- addSelect([
- { label: 'Groesse', value: '' },
- { label: '10px', value: '1' },
- { label: '12px', value: '2' },
- { label: '14px', value: '3' },
- { label: '16px', value: '4' },
- { label: '18px', value: '5' },
- { label: '24px', value: '6' },
- { label: '32px', value: '7' },
- ], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value, content.ownerDocument));
-
- const emojiBtn = doc.createElement('button');
- emojiBtn.type = 'button';
- emojiBtn.textContent = ':-)';
- emojiBtn.title = 'Emoticon einfuegen';
- emojiBtn.style.padding = '4px 8px';
- emojiBtn.style.border = '1px solid #cbd5f5';
- emojiBtn.style.borderRadius = '4px';
- emojiBtn.style.background = '#f8fafc';
- emojiBtn.style.cursor = 'pointer';
- emojiBtn.addEventListener('click', () => {
- const pick = prompt('Emoticon eingeben', ':)');
- if (pick) insertText(pick);
- });
- toolbar.appendChild(emojiBtn);
-
- container.appendChild(toolbar);
- container.appendChild(content);
-
- 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';
- const normalizeRteHtml = (rawHtml) => {
- const wrapper = doc.createElement('div');
- wrapper.innerHTML = String(rawHtml || '').trim();
- // Unwrap single empty div wrapper inserted by contenteditable
- if (wrapper.children.length === 1 && wrapper.firstElementChild.tagName === 'DIV' && wrapper.firstElementChild.attributes.length === 0) {
- wrapper.innerHTML = wrapper.firstElementChild.innerHTML;
- }
- // Drop empty div/br artifacts
- wrapper.querySelectorAll('div').forEach((el) => {
- if (!el.attributes.length && !el.textContent.trim() && !el.querySelector('img,br,span,b,strong,i,em,u,a,ul,ol,li')) {
- el.remove();
- }
- });
- let html = wrapper.innerHTML.replace(/
/gi, '').trim();
- if (!html) {
- const fallbackText = wrapper.textContent.trim();
- if (fallbackText) {
- html = fallbackText.replace(//g, '>');
- }
- }
- return html;
- };
-
- saveBtn.addEventListener('click', () => {
- const rawHtml = content.innerHTML || '';
- let html = normalizeRteHtml(rawHtml);
- try {
- console.warn('[RTE SAVE DEBUG]', {
- rawHtml,
- normalizedHtml: html,
- textContent: String(content.textContent || '').trim()
- });
- } catch {}
- const isTextLike = component && component.is && (component.is('text') || component.is('button') || component.is('link'));
- try {
- if (html) {
- if (!isTextLike && component.components) {
- component.components(html);
- }
- if (component.set) {
- component.set('content', html);
- component.trigger && component.trigger('change:content');
- component.trigger && component.trigger('change:components');
- }
- } else {
- console.warn('[RTE SAVE DEBUG] Skip empty content update.');
- }
- if (component.view && component.view.render) {
- component.view.render();
- }
- if (editor && typeof editor.trigger === 'function') {
- editor.trigger('component:update', component);
- }
- } catch {}
- closeModal();
- });
-
- actions.appendChild(cancelBtn);
- actions.appendChild(saveBtn);
- container.appendChild(actions);
-
- modal.setTitle('Richtext Editor');
- modal.setContent(container);
- const mdl = modal.getModel && modal.getModel();
- if (mdl && typeof mdl.set === 'function') {
- mdl.set('closeOnEsc', false);
- mdl.set('closeOnClick', false);
- }
- if (typeof modal.onceClose === 'function') {
- modal.onceClose(() => {
- editor.__bridgeRteModalOpen = false;
- editor.__bridgeRteAllowClose = false;
- });
- }
- if (mdl && typeof mdl.on === 'function') {
- const handler = () => {
- if (editor.__bridgeRteAllowClose) {
- mdl.off && mdl.off('change:open', handler);
- return;
- }
- if (!mdl.get('open') && !reopenGuard) {
- reopenGuard = true;
- mdl.set('open', true);
- setTimeout(() => { reopenGuard = false; }, 0);
- }
- };
- mdl.on('change:open', handler);
- }
+ const exec = (cmd, value) => {
try {
- modal.open({ closeOnEsc: false, closeOnClick: false });
- } catch {
- modal.open();
- }
-
- const modalEl = modal.getEl ? modal.getEl() : modal.el;
- const closeBtn = modalEl && modalEl.querySelector ? modalEl.querySelector('.gjs-mdl-btn-close') : null;
- if (closeBtn && !closeBtn.__bridgeRteBound) {
- closeBtn.__bridgeRteBound = true;
- closeBtn.addEventListener('click', () => {
- editor.__bridgeRteAllowClose = true;
- }, true);
- }
+ content.focus();
+ const docRef = content.ownerDocument || document;
+ docRef.execCommand(cmd, false, value);
+ } catch {}
};
- const setupRichTextEditor = (editor) => {
- if (!editor || !editor.RichTextEditor) return;
- const rte = editor.RichTextEditor;
- const icon = (path) => ``;
- const logRteBlur = (label, detail) => {
- const msg = `[RTE BLUR] ${label}${detail ? ' | ' + detail : ''}`;
- try { console.log(msg); } catch {}
- log('RTE BLUR', msg, '#888');
- try {
- if (window.parent && window.parent !== window) {
- window.parent.postMessage({ source: 'bridge-core', type: 'rte-blur', detail: msg }, '*');
- }
- } catch {}
- };
- const resolveFontOptions = () => (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 addAction = (name, icon, title, command, valueGetter) => {
- if (rte.get && rte.get(name)) return;
- rte.add(name, {
- icon,
- attributes: { title },
- result: (rteInstance) => {
- const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
- if (value === null || value === undefined) return;
- if ((command === 'insertText' || command === 'fontName' || command === 'fontSize' || command === 'createLink') && !value) {
- return;
- }
- execRteCommand(rteInstance, command, value);
- },
- });
- };
-
- addAction('bridge-align-left', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
- addAction('bridge-align-center', icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
- addAction('bridge-align-right', icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight');
- addAction('bridge-align-justify', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
- addAction('bridge-ul', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
- addAction('bridge-ol', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Liste (geordnet)', 'insertOrderedList');
- addAction('bridge-emoji', ':-)', 'Emoticon einfuegen', 'insertText', () => prompt('Emoticon eingeben', ':)'));
- addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => {
- const fonts = resolveFontOptions();
- const example = fonts.map((f) => f.label).slice(0, 5).join(', ');
- return prompt(`Schriftart (z.B. ${example})`, fonts[0]?.label || 'Arial');
- });
- addAction('bridge-font-size', 'Px', 'Schriftgroesse', 'fontSize', () => {
- const raw = prompt('Schriftgroesse in px (10-32)', '14');
- const val = Number(raw || 14);
- if (Number.isNaN(val)) return '3';
- const map = [
- { px: 10, cmd: '1' },
- { px: 12, cmd: '2' },
- { px: 14, cmd: '3' },
- { px: 16, cmd: '4' },
- { px: 18, cmd: '5' },
- { px: 24, cmd: '6' },
- { px: 32, cmd: '7' },
- ];
- let best = map[0];
- map.forEach((entry) => {
- if (Math.abs(entry.px - val) < Math.abs(best.px - val)) best = entry;
- });
- return best.cmd;
- });
- if (!(rte.get && rte.get('bridge-open-richtext'))) {
- rte.add('bridge-open-richtext', {
- icon: '',
- attributes: { title: 'Richtext Editor' },
- result: () => {
- const component = editor.getSelected && editor.getSelected();
- openRichTextModal(editor, component);
- },
- });
- }
-
- 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();
- openRichTextModal(ed, component);
- },
- });
- }
-
- normalizeRteActionList(editor, [
- 'bridge-open-richtext',
- 'bridge-font-family',
- 'bridge-font-size',
- 'bridge-align-left',
- 'bridge-align-center',
- 'bridge-align-right',
- 'bridge-align-justify',
- 'bridge-ul',
- 'bridge-ol',
- 'bridge-emoji',
- 'bridge-placeholder',
- ]);
-
- const isTextLike = (model) => !!(model && model.is && (model.is('text') || model.is('button') || model.is('link')));
- let lastTextSelection = { id: null, ts: 0 };
- let lastTextComponent = null;
- editor.on('component:selected', (model) => {
- ensureTextToolbarButton(editor, model);
- if (!isTextLike(model)) return;
- const now = Date.now();
- if (lastTextSelection.id === model.cid && (now - lastTextSelection.ts) < 450) {
- openRichTextModal(editor, model);
- }
- lastTextComponent = model;
- lastTextSelection = { id: model.cid, ts: now };
- });
- editor.on('component:deselected', (model) => {
- if (isTextLike(model)) {
- lastTextComponent = model;
- }
- });
- editor.on('component:add', (model) => ensureTextToolbarButton(editor, model));
- editor.on('component:dblclick', (model) => {
- if (isTextLike(model)) {
- openRichTextModal(editor, model);
- }
- });
- editor.on('canvas:frame:load', () => {
- const body = editor.Canvas && editor.Canvas.getBody && editor.Canvas.getBody();
- if (!body || body.__bridgeRteDblclickBound) return;
- body.__bridgeRteDblclickBound = true;
- body.addEventListener('dblclick', () => {
- const selected = editor.getSelected && editor.getSelected();
- if (isTextLike(selected)) {
- openRichTextModal(editor, selected);
- }
- }, true);
- const blurHandler = (evt) => {
- const target = evt && evt.target;
- if (!target) return;
- const isEditable = !!(target.isContentEditable || (target.getAttribute && target.getAttribute('contenteditable') === 'true'));
- if (!isEditable) return;
- const selected = lastTextComponent || (editor.getSelected && editor.getSelected());
- const selectedEl = selected && selected.view && selected.view.el;
- if (selected && selectedEl && (selectedEl === target || selectedEl.contains(target))) {
- const html = String(target.innerHTML || '').trim();
- try {
- const content = selected && selected.get ? selected.get('content') : '';
- console.warn('[RTE BLUR DEBUG]', {
- tag: target.tagName,
- htmlLen: html.length,
- contentLen: String(content || '').length,
- modelType: selected && selected.get ? selected.get('type') : undefined,
- modelId: selected && (selected.getId ? selected.getId() : selected.get && selected.get('id')),
- });
- } catch {}
- }
- let contentInfo = '';
- try {
- const content = selected && selected.get ? selected.get('content') : '';
- contentInfo = String(content || '').trim() ? 'Content vorhanden' : 'Content leer';
- } catch {}
- logRteBlur('contenteditable blur', contentInfo);
- };
- body.addEventListener('blur', blurHandler, true);
- body.addEventListener('focusout', blurHandler, true);
- });
- editor.on('rte:disable', (model) => {
- const target = model || (editor.getSelected && editor.getSelected());
- if (!isTextLike(target)) return;
- const content = target && target.get ? target.get('content') : '';
- const msg = String(content || '').trim() ? 'Content vorhanden' : 'Content leer';
- logRteBlur('rte:disable fuer Text-Komponente', msg);
+ const addButton = (labelHtml, title, cmd, valueGetter) => {
+ const btn = doc.createElement('button');
+ btn.type = 'button';
+ btn.innerHTML = labelHtml;
+ btn.title = 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('click', () => {
+ 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.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('change', () => {
+ const value = select.value;
+ if (value) onChange(value);
+ });
+ toolbar.appendChild(select);
+ return select;
+ };
+
+ const icon = (path) => ``;
+ addButton('B', 'Fett', 'bold');
+ addButton('I', 'Kursiv', 'italic');
+ addButton('U', 'Unterstrichen', 'underline');
+ addButton('S', 'Durchgestrichen', 'strikethrough');
+ addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
+ addButton(icon('M4 7h14v2H4zM4 11h14v2H4zM4 15h14v2H4z') + '1.', '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'), 'Rechtsbundig', 'justifyRight');
+ addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
+ addButton('Link', 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://'));
+ addButton('Unlink', 'Link entfernen', 'unlink');
+ addButton('Sub', 'Tiefgestellt', 'subscript');
+ addButton('Sup', 'Hochgestellt', 'superscript');
+ addButton('Einr.', 'Einzug', 'indent');
+ addButton('Aus.', 'Ausruecken', 'outdent');
+ addButton('Clear', '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' },
+ ];
+ addSelect([{ label: 'Schriftart', value: '' }, ...fontOptions], 'Schriftart', (value) => exec('fontName', value));
+ addSelect([
+ { label: 'Groesse', value: '' },
+ { label: '10px', value: '1' },
+ { label: '12px', value: '2' },
+ { label: '14px', value: '3' },
+ { label: '16px', value: '4' },
+ { label: '18px', value: '5' },
+ { label: '24px', value: '6' },
+ { label: '32px', value: '7' },
+ ], 'Schriftgroesse', (value) => exec('fontSize', value));
+
+ const emojiBtn = doc.createElement('button');
+ emojiBtn.type = 'button';
+ emojiBtn.textContent = ':-)';
+ emojiBtn.title = 'Emoticon einfuegen';
+ emojiBtn.style.padding = '4px 8px';
+ emojiBtn.style.border = '1px solid #cbd5f5';
+ emojiBtn.style.borderRadius = '4px';
+ emojiBtn.style.background = '#f8fafc';
+ emojiBtn.style.cursor = 'pointer';
+ emojiBtn.addEventListener('click', () => {
+ const pick = prompt('Emoticon eingeben', ':)');
+ if (pick) exec('insertText', pick);
+ });
+ toolbar.appendChild(emojiBtn);
+
+ container.appendChild(toolbar);
+ container.appendChild(content);
+
+ 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 || '');
+ applyContentToComponent(editor, component, html);
+ closeModal();
+ });
+
+ 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', true);
+ mdl.set('closeOnClick', true);
+ }
+ modal.open();
+ };
+
+ 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 setupRichTextEditor = (editor) => {
+ if (!editor) return;
+ 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();
+ 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;
+
+ editor.on('component:selected', (model) => ensureTextToolbarButton(model));
+ editor.on('component:add', (model) => ensureTextToolbarButton(model));
+ editor.on('component:dblclick', (model) => {
+ if (isTextLike(model)) openRichTextModal(editor, model);
+ });
+ };
+
B.setupRichTextEditor = setupRichTextEditor;
})();