/* /assets/js/bridge/rte-editor.js */ (function () { const PluginName = 'bridge-rte-editor'; const B = window.BridgeParts || (window.BridgeParts = {}); const log = (type, message, color = '#94a3b8', logType = 'info', force = false) => { if (typeof B.log === 'function') { B.log(PluginName, `[${type}] ${message}`, color, logType, force); } }; 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 needsLeadBreak = !!(prev && (prev.nodeType === 3 ? prev.textContent.trim() : (prev.tagName && prev.tagName !== 'BR'))); if (needsLeadBreak) { frag.appendChild(document.createElement('br')); } } while (el.firstChild) { frag.appendChild(el.firstChild); } if (addBreak) { frag.appendChild(document.createElement('br')); } 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'].includes(name)) { el.removeAttribute(attr.name); } } else if (name !== 'style') { 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) { component.components(content); } 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 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; 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.__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 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 initialHtml = (component.get && component.get('content')) || (component.view && component.view.el && component.view.el.innerHTML) || ''; content.innerHTML = initialHtml; const exec = (cmd, value) => { try { content.focus(); const docRef = content.ownerDocument || document; docRef.execCommand(cmd, false, value); } catch {} }; 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 || ''); component.__bridgeRteLastContent = html; logConsoleSnapshot(editor, component, 'before-save'); applyContentToComponent(editor, component, html); logConsoleSnapshot(editor, component, 'after-save'); 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; const restoreIfCollapsed = (model) => { if (!model || model.__bridgeRteRestoring) return; const last = model.__bridgeRteLastContent; if (!last || !model.get) return; const current = String(model.get('content') || '').trim(); if (!current || current === '
' || current === '

' || current === '


') { model.__bridgeRteRestoring = true; logConsoleSnapshot(editor, model, 'restore-start'); applyContentToComponent(editor, model, last); logConsoleSnapshot(editor, model, 'restore-end'); model.__bridgeRteRestoring = false; } }; editor.on('component:update', (model) => restoreIfCollapsed(model)); editor.on('component:input', (model) => restoreIfCollapsed(model)); 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; })();