/* /assets/js/bridge/rte-editor.js */ (function () { const PluginName = 'bridge-rte-editor'; const editorInstances = new WeakMap(); 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(); this.restoreUntil = new WeakMap(); this.restoreWindowMs = 3000; } static serializeHtml(editor) { if (!editor || typeof editor.getHtml !== 'function') return ''; let html = editor.getHtml() || ''; if (!html) { try { const wrapper = editor.getWrapper && editor.getWrapper(); if (wrapper && typeof wrapper.toHTML === 'function') { html = wrapper.toHTML(); } } catch {} } if (!html) { try { const wrapper = editor.getWrapper && editor.getWrapper(); const el = wrapper && wrapper.view && wrapper.view.el ? wrapper.view.el : null; if (el && el.innerHTML) { html = String(el.innerHTML || ''); } } catch {} } if (!html) { try { const body = editor.Canvas && editor.Canvas.getBody ? editor.Canvas.getBody() : null; if (body && body.innerHTML) { html = String(body.innerHTML || ''); } } catch {} } try { const wrapper = editor.getWrapper && editor.getWrapper(); if (wrapper && wrapper.find) { const doc = new DOMParser().parseFromString(html, 'text/html'); 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')); if (!content || !id) return; const el = doc.getElementById(id); if (el) { el.innerHTML = content; } }); if (doc.body && doc.body.innerHTML !== undefined) { html = doc.body.innerHTML; } } } catch {} if (html) { const bodyMatch = html.match(/]*>([\s\S]*?)<\/body>/i); if (bodyMatch) html = bodyMatch[1]; } return html; } 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; const tagLower = tag.toLowerCase(); if (inlineTags.has(tag)) { if (tagLower === 'a' && !el.getAttribute('href')) { unwrap(el, false); return; } Array.from(el.attributes).forEach((attr) => { const name = attr.name.toLowerCase(); if (tagLower === 'a') { if (!['href', 'target', 'rel', '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 = 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 { if (!this.B || !this.B.DEBUG_RTE) return; 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 || ''); const isText = (component.is && component.is('text')) || (component.get && component.get('type') === 'text'); try { if (isText && component.components) { try { component.components(content); } catch {} } if (component.set) component.set('content', content); } catch {} if (component.view && typeof component.view.render === 'function') { component.view.render(); } if (isText && component.view && component.view.el) { try { const currentHtml = String(component.view.el.innerHTML || '').trim(); const targetHtml = String(content || '').trim(); if (targetHtml && currentHtml !== targetHtml) { component.view.el.innerHTML = targetHtml; } } catch {} } if (component.trigger) { component.trigger('change:content'); component.trigger('change:components'); } if (editor && typeof editor.trigger === 'function') { editor.trigger('component:update', component); } if (isText && component.view && component.view.el) { const reapply = () => { try { const currentHtml = String(component.view.el.innerHTML || '').trim(); const targetHtml = String(content || '').trim(); if (targetHtml && currentHtml !== targetHtml) { component.view.el.innerHTML = targetHtml; } } catch {} }; setTimeout(reapply, 0); setTimeout(reapply, 50); } } 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'); } 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'; } 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'])); } 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; let rteInstance = null; let rteTargetEl = null; 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) { rteInstance = editor.RichTextEditor; rteTargetEl = component.view.el; editor.RichTextEditor.disable(component.view.el); } } catch {} const closeModal = () => { try { window.__bridgeRteOpen = false; } catch {} this.allowClose = true; this.modalOpen = false; if (rteInstance && typeof rteInstance.enable === 'function' && rteTargetEl) { try { const res = rteInstance.enable(rteTargetEl); if (res && typeof res.catch === 'function') { res.catch(() => {}); } } catch {} } 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'); try { window.__bridgeRteOpen = true; } catch {} container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.gap = '10px'; container.style.height = '100%'; container.style.minHeight = '360px'; container.style.position = 'relative'; 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 toolbarSecondary = doc.createElement('div'); toolbarSecondary.style.display = 'flex'; toolbarSecondary.style.flexWrap = 'wrap'; toolbarSecondary.style.gap = '6px'; toolbarSecondary.style.alignItems = 'center'; toolbarSecondary.className = 'bridge-rte-toolbar bridge-rte-toolbar-secondary'; toolbarSecondary.setAttribute('data-bridge-rte', 'toolbar-secondary'); 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 { 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 = 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); 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 findLinkAtSelection = () => { const range = getSelectionRange(); if (!range) return null; let node = range.commonAncestorContainer; if (node && node.nodeType === 3) node = node.parentNode; while (node && node !== content) { if (node.tagName && node.tagName.toUpperCase() === 'A') return node; node = node.parentNode; } 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 selectionHasStyle = (range, styleProp, tags, styleMatch) => { if (!range) return false; try { const fragment = range.cloneContents(); const walker = (node) => { if (!node) return false; if (node.nodeType === 1) { const tag = node.tagName ? node.tagName.toUpperCase() : ''; if (tags && tags.includes(tag)) return true; if (styleProp && node.style) { if (styleMatch && styleMatch(node.style[styleProp] || '', node)) return true; } for (const child of Array.from(node.childNodes)) { if (walker(child)) return true; } } return false; }; for (const child of Array.from(fragment.childNodes)) { if (walker(child)) return true; } const checkAncestors = (node) => { let cur = node; while (cur && cur !== content) { if (cur.nodeType === 1) { const tag = cur.tagName ? cur.tagName.toUpperCase() : ''; if (tags && tags.includes(tag)) return true; if (styleProp && cur.style) { if (styleMatch && styleMatch(cur.style[styleProp] || '', cur)) return true; } } cur = cur.parentNode; } return false; }; if (checkAncestors(range.startContainer)) return true; if (checkAncestors(range.endContainer)) return true; } catch {} return false; }; const removeInlineStyle = (styleProp, tags, clearFn) => { try { content.focus(); restoreSelection(); const docRef = content.ownerDocument || document; const range = getSelectionRange(); if (!range || range.collapsed) return; const unwrap = (node) => { const parent = node.parentNode; if (!parent) return; while (node.firstChild) parent.insertBefore(node.firstChild, node); parent.removeChild(node); }; const depth = (node) => { let d = 0; let cur = node; while (cur && cur !== content) { d += 1; cur = cur.parentNode; } return d; }; const nodes = []; const walker = docRef.createTreeWalker(content, NodeFilter.SHOW_ELEMENT, null); let node = walker.nextNode(); while (node) { if (node !== content && range.intersectsNode(node)) { nodes.push(node); } node = walker.nextNode(); } nodes.sort((a, b) => depth(b) - depth(a)); nodes.forEach((el) => { const tag = el.tagName ? el.tagName.toUpperCase() : ''; if (tag === 'BR') return; if (tags && tags.includes(tag)) { unwrap(el); return; } if (styleProp && el.style) { if (typeof clearFn === 'function') { clearFn(el.style); } else { el.style[styleProp] = ''; } const styleAttr = el.getAttribute('style'); if ((!styleAttr || !String(styleAttr).trim()) && tag === 'SPAN') { unwrap(el); } } }); saveSelection(); } catch {} }; const toggleInlineStyle = (cmd, styleProp, value, tags, styleMatch, clearFn) => { const range = getSelectionRange(); if (!range || range.collapsed) return; if (selectionHasStyle(range, styleProp, tags, styleMatch)) { if (cmd) exec(cmd); removeInlineStyle(styleProp, tags, clearFn); return; } if (cmd) exec(cmd); if (styleProp) applyInlineStyle(styleProp, value); }; const removeInlineFormatting = () => { try { content.focus(); restoreSelection(); const docRef = content.ownerDocument || document; const range = getSelectionRange(); if (!range || range.collapsed) return; const unwrap = (node) => { const parent = node.parentNode; if (!parent) return; while (node.firstChild) parent.insertBefore(node.firstChild, node); parent.removeChild(node); }; const depth = (node) => { let d = 0; let cur = node; while (cur && cur !== content) { d += 1; cur = cur.parentNode; } return d; }; const nodes = []; const walker = docRef.createTreeWalker(content, NodeFilter.SHOW_ELEMENT, null); let node = walker.nextNode(); while (node) { if (node !== content && range.intersectsNode(node)) { nodes.push(node); } node = walker.nextNode(); } nodes.sort((a, b) => depth(b) - depth(a)); nodes.forEach((el) => { const tag = el.tagName ? el.tagName.toUpperCase() : ''; if (tag === 'BR') return; if (tag === 'B' || tag === 'STRONG' || tag === 'I' || tag === 'EM' || tag === 'U' || tag === 'S' || tag === 'STRIKE') { unwrap(el); return; } if (tag === 'SPAN') { try { el.style.fontWeight = ''; el.style.fontStyle = ''; el.style.textDecoration = ''; el.style.textDecorationLine = ''; el.style.textDecorationStyle = ''; } catch {} const styleAttr = el.getAttribute('style'); if (!styleAttr || !String(styleAttr).trim()) { unwrap(el); } } }); saveSelection(); } catch {} }; const addButton = (labelHtml, title, cmd, valueGetter, handler, target) => { 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.classList.add('bridge-rte-btn--tool'); btn.style.padding = '8px 12px'; btn.style.border = '1px solid #cbd5f5'; btn.style.borderRadius = '4px'; btn.style.background = '#f8fafc'; btn.style.cursor = 'pointer'; btn.style.display = 'inline-flex'; btn.style.alignItems = 'center'; btn.style.justifyContent = 'center'; btn.style.gap = '8px'; btn.addEventListener('mousedown', (evt) => { evt.preventDefault(); saveSelection(); }); btn.addEventListener('click', () => { if (typeof handler === 'function') { handler(); return; } const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter; const hasValue = typeof valueGetter !== 'undefined'; if (hasValue && (value === null || value === undefined)) return; if (cmd === 'createLink' && !value) return; exec(cmd, value); }); (target || toolbar).appendChild(btn); }; const addSelect = (options, title, onChange, target) => { 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 = '8px 12px'; 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); }); (target || 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, toolbarSecondary, 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( 'B', 'Fett', 'bold', null, () => toggleInlineStyle( 'bold', 'fontWeight', '700', ['B', 'STRONG'], (val) => { const num = parseInt(String(val || '').replace(/[^0-9]/g, ''), 10); return Number.isFinite(num) ? num >= 600 : /bold/i.test(String(val || '')); }, (style) => { style.fontWeight = ''; } ) ); addButton( 'I', 'Kursiv', 'italic', null, () => toggleInlineStyle( 'italic', 'fontStyle', 'italic', ['I', 'EM'], (val) => String(val || '').toLowerCase() === 'italic', (style) => { style.fontStyle = ''; } ) ); addButton( 'U', 'Unterstrichen', 'underline', null, () => toggleInlineStyle( 'underline', 'textDecoration', 'underline', ['U'], (val) => /underline/i.test(String(val || '')), (style) => { style.textDecoration = ''; style.textDecorationLine = ''; } ) ); 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'); const linkModal = doc.createElement('div'); linkModal.style.display = 'none'; linkModal.style.position = 'absolute'; linkModal.style.inset = '0'; linkModal.style.background = 'rgba(15,23,42,0.35)'; linkModal.style.alignItems = 'center'; linkModal.style.justifyContent = 'center'; linkModal.style.zIndex = '20'; linkModal.className = 'bridge-rte-link-modal'; linkModal.setAttribute('data-bridge-rte', 'link-modal'); const linkPanel = doc.createElement('div'); linkPanel.style.display = 'flex'; linkPanel.style.flexDirection = 'column'; linkPanel.style.gap = '10px'; linkPanel.style.padding = '12px'; linkPanel.style.border = '1px solid #cbd5f5'; linkPanel.style.borderRadius = '8px'; linkPanel.style.background = '#ffffff'; linkPanel.style.boxShadow = '0 6px 24px rgba(15,23,42,0.2)'; linkPanel.style.minWidth = '320px'; const linkTitle = doc.createElement('div'); linkTitle.textContent = 'Link einfuegen'; linkTitle.style.fontWeight = '600'; linkTitle.style.fontSize = '14px'; const linkInput = doc.createElement('input'); linkInput.type = 'text'; linkInput.placeholder = 'https://...'; linkInput.style.padding = '6px 8px'; linkInput.style.border = '1px solid #cbd5f5'; linkInput.style.borderRadius = '4px'; linkInput.style.fontSize = '14px'; const linkActions = doc.createElement('div'); linkActions.style.display = 'flex'; linkActions.style.justifyContent = 'flex-end'; linkActions.style.gap = '8px'; const linkApply = doc.createElement('button'); linkApply.type = 'button'; linkApply.textContent = 'OK'; linkApply.className = 'bridge-rte-btn'; linkApply.style.padding = '6px 10px'; const linkRemove = doc.createElement('button'); linkRemove.type = 'button'; linkRemove.textContent = 'Entfernen'; linkRemove.className = 'bridge-rte-btn'; linkRemove.style.padding = '6px 10px'; const linkCancel = doc.createElement('button'); linkCancel.type = 'button'; linkCancel.textContent = 'Abbrechen'; linkCancel.className = 'bridge-rte-btn'; linkCancel.style.padding = '6px 10px'; linkActions.appendChild(linkCancel); linkActions.appendChild(linkRemove); linkActions.appendChild(linkApply); linkPanel.appendChild(linkTitle); linkPanel.appendChild(linkInput); linkPanel.appendChild(linkActions); linkModal.appendChild(linkPanel); container.appendChild(linkModal); const hideLinkPanel = () => { linkModal.style.display = 'none'; }; const showLinkPanel = () => { saveSelection(); const linkEl = findLinkAtSelection(); linkInput.value = linkEl && linkEl.getAttribute ? (linkEl.getAttribute('href') || '') : ''; linkModal.style.display = 'flex'; linkInput.focus(); linkInput.select(); }; addButton( icon('M7 7h10l-1.5 1.5-3-3-5.5 5.5v5h5l5.5-5.5-3-3L17 7z'), 'Link einfuegen/bearbeiten', null, null, () => { showLinkPanel(); } ); linkApply.addEventListener('click', () => { const url = String(linkInput.value || '').trim(); if (!url) { hideLinkPanel(); return; } restoreSelection(); try { const linkEl = findLinkAtSelection(); if (linkEl && linkEl.setAttribute) { linkEl.setAttribute('href', url); hideLinkPanel(); return; } } catch {} const range = savedRange ? savedRange.cloneRange() : getSelectionRange(); if (!range || range.collapsed) { hideLinkPanel(); return; } try { const docRef = content.ownerDocument || document; const anchor = docRef.createElement('a'); anchor.setAttribute('href', url); anchor.setAttribute('target', '_blank'); anchor.setAttribute('rel', 'noopener'); const fragment = range.extractContents(); anchor.appendChild(fragment); range.insertNode(anchor); } catch { exec('createLink', url); } hideLinkPanel(); }); linkRemove.addEventListener('click', () => { try { const linkEl = findLinkAtSelection(); if (linkEl && linkEl.parentNode) { while (linkEl.firstChild) { linkEl.parentNode.insertBefore(linkEl.firstChild, linkEl); } linkEl.parentNode.removeChild(linkEl); hideLinkPanel(); return; } } catch {} restoreSelection(); exec('unlink'); const stillLinked = findLinkAtSelection(); if (stillLinked && stillLinked.parentNode) { while (stillLinked.firstChild) { stillLinked.parentNode.insertBefore(stillLinked.firstChild, stillLinked); } stillLinked.parentNode.removeChild(stillLinked); } hideLinkPanel(); }); linkCancel.addEventListener('click', hideLinkPanel); 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('M3 15.5L14.5 4 20 9.5 8.5 21H3v-5.5zm2.5 3H8l9.5-9.5-2.5-2.5L5.5 16v2.5z'), 'Formatierung entfernen', null, null, () => { exec('removeFormat'); removeInlineFormatting(); } ); const buildPlaceholderLabel = (payload) => { const type = payload && payload.type === 'database' ? 'database' : 'custom'; if (type === 'database') { const table = (payload.table || 'TABELLE').toUpperCase(); const column = (payload.column || 'FELD').toUpperCase(); return `${table}.${column}`; } return (payload.key || 'PLATZHALTER').toUpperCase(); }; const buildPlaceholderText = (payload) => `{{${buildPlaceholderLabel(payload)}}}`; const createInlineModal = () => { const overlay = doc.createElement('div'); overlay.style.display = 'none'; overlay.style.position = 'absolute'; overlay.style.inset = '0'; overlay.style.background = 'rgba(15,23,42,0.35)'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; overlay.style.zIndex = '30'; const panel = doc.createElement('div'); panel.style.display = 'flex'; panel.style.flexDirection = 'column'; panel.style.gap = '10px'; panel.style.padding = '12px'; panel.style.border = '1px solid #cbd5f5'; panel.style.borderRadius = '8px'; panel.style.background = '#ffffff'; panel.style.boxShadow = '0 6px 24px rgba(15,23,42,0.2)'; panel.style.minWidth = '320px'; panel.style.maxWidth = '640px'; panel.style.maxHeight = '80vh'; panel.style.overflow = 'auto'; const titleEl = doc.createElement('div'); titleEl.style.fontWeight = '600'; titleEl.style.fontSize = '14px'; const bodyEl = doc.createElement('div'); panel.appendChild(titleEl); panel.appendChild(bodyEl); overlay.appendChild(panel); container.appendChild(overlay); let closeCb = null; return { el: overlay, setTitle(text) { titleEl.textContent = text || ''; }, setContent(node) { bodyEl.innerHTML = ''; if (node) bodyEl.appendChild(node); }, open() { overlay.style.display = 'flex'; }, close() { overlay.style.display = 'none'; if (typeof closeCb === 'function') closeCb(); }, onceClose(cb) { closeCb = cb; }, }; }; const insertTextAtSelection = (text) => { try { content.focus(); restoreSelection(); const range = getSelectionRange(); if (!range) return false; range.deleteContents(); const docRef = content.ownerDocument || document; const node = docRef.createTextNode(text); range.insertNode(node); range.setStartAfter(node); range.setEndAfter(node); const sel = docRef.getSelection(); if (sel) { sel.removeAllRanges(); sel.addRange(range); } saveSelection(); return true; } catch { return false; } }; 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' }, ]; addButton( '{}Placeholder', 'Placeholder einfuegen', null, null, () => { saveSelection(); const api = window.BridgeBlocksPlaceholder; if (!api || typeof api.openModal !== 'function') return; const inlineModal = createInlineModal(); api.openModal(editor, null, { modal: inlineModal, onCancel: () => { if (content && typeof content.focus === 'function') { setTimeout(() => content.focus(), 0); } }, onSubmit: (payload) => { const text = buildPlaceholderText(payload || {}); restoreSelection(); if (!insertTextAtSelection(text)) { exec('insertText', text); } if (content && typeof content.focus === 'function') { setTimeout(() => content.focus(), 0); } return true; }, }); }, toolbarSecondary ); const fontSelect = addSelect([{ label: 'Schriftart', value: '' }, ...fontOptions], 'Schriftart', (value) => { if (!value) return; const range = getSelectionRange(); if (range && !range.collapsed) { applyInlineStyle('fontFamily', value); return; } pendingComponentStyle.fontFamily = value; applyComponentStyle({ fontFamily: value }, { preview: true }); }, toolbarSecondary); 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 }); }, toolbarSecondary); } 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); const rteUiCss = ` .bridge-rte-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } .bridge-rte-toolbar-secondary { margin-top: 6px; padding-top: 6px; border-top: 1px dashed #e2e8f0; } .bridge-rte-btn { font-size: 14px; line-height: 1; min-height: 30px; min-width: 30px; font-weight: 600; color: #0f172a; background: #f8fafc; } .bridge-rte-btn--tool { font-size: 16px; min-height: 40px; min-width: 40px; } .bridge-rte-btn--tool svg { width: 18px; height: 18px; } .bridge-rte-select { font-size: 15px; min-height: 40px; } .bridge-rte-btn:hover { background: #e2e8f0; } .bridge-rte-btn:active { transform: translateY(1px); } .bridge-rte-actions .bridge-rte-btn { min-width: 88px; } `.trim(); injectedStyle.textContent = [fontCss, editorCss, frameCss, rteUiCss].filter(Boolean).join('\n'); if (injectedStyle.textContent) { container.appendChild(injectedStyle); } container.appendChild(toolbar); container.appendChild(toolbarSecondary); 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); try { this.restoreUntil.set(component, Date.now() + this.restoreWindowMs); } catch {} 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); if (this.B && this.B.DEBUG_RTE) { try { const gjsHtml = editor && typeof editor.getHtml === 'function' ? editor.getHtml() : ''; const serHtml = BridgeRTE.serializeHtml(editor); console.group('[RTE DEBUG] save snapshot'); console.log('component id', component && (component.getId ? component.getId() : component.get && component.get('id'))); console.log('model content', component && component.get ? component.get('content') : ''); console.log('view html', component?.view?.el?.innerHTML || ''); console.log('editor.getHtml len', String(gjsHtml || '').length); console.log('serializeHtml len', String(serHtml || '').length); console.log('editor.getHtml', gjsHtml); console.log('serializeHtml', serHtml); console.groupEnd(); const modelContent = String(component && component.get ? component.get('content') : ''); const viewHtml = String(component?.view?.el?.innerHTML || ''); const visibleText = String(component?.view?.el?.textContent || ''); const summary = { source: 'rte', modelId: component && (component.getId ? component.getId() : component.get && component.get('id')), expectedFromRte: String(html || '').slice(0, 500), actualModelContent: modelContent.slice(0, 500), visibleText: visibleText.slice(0, 500), viewHtml: viewHtml.slice(0, 500), viewHtmlLen: viewHtml.length, modelContentLen: modelContent.length, visibleTextLen: visibleText.length, editorHtmlLen: String(gjsHtml || '').length, serializeHtmlLen: String(serHtml || '').length, }; console.warn('[EDIT SUMMARY]', JSON.stringify(summary)); } catch {} } closeModal(); // RTE-Reaktivierung kann den DOM-Stand überschreiben; danach nochmal anwenden. setTimeout(() => this.applyContentToComponent(editor, component, html), 0); setTimeout(() => this.applyContentToComponent(editor, component, html), 50); }); 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;}', '.bridge-rte-modal .gjs-mdl-dialog{max-height:90vh;overflow:hidden;}', '.bridge-rte-container{max-height:80vh;}', '.bridge-rte-content{max-height:55vh;overflow:auto;}', ].join(''); 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 restoreDeadline = this.restoreUntil.get(model); if (!restoreDeadline || Date.now() > restoreDeadline) return; const last = this.lastContent.get(model); if (!last || !model.get) return; const viewEl = model.view && model.view.el; if (viewEl && (viewEl.isContentEditable || viewEl.getAttribute('contenteditable') === 'true')) return; const viewHtml = String(viewEl && viewEl.innerHTML || '').trim(); if (viewHtml) return; const current = String(model.get('content') || '').trim(); if (!current || current === '
' || current === '

' || current === '


') { this.restoring.add(model); this.logConsoleSnapshot(editor, model, 'restore-start'); this.applyContentToComponent(editor, model, last); this.logConsoleSnapshot(editor, model, 'restore-end'); this.restoring.delete(model); } }; editor.on('component:update', (model) => restoreIfCollapsed(model)); editor.on('component:input', (model) => restoreIfCollapsed(model)); editor.on('component:selected', (model) => this.ensureTextToolbarButton(model)); editor.on('component:add', (model) => this.ensureTextToolbarButton(model)); editor.on('component:dblclick', (model) => { if (this.isTextLike(model)) this.openRichTextModal(editor, model); }); } mount(editor) { this.setupEditor(editor); } } const setupRichTextEditor = (editor) => { if (!editor) return; let instance = editorInstances.get(editor); if (!instance) { instance = new BridgeRTE(window.BridgeParts || (window.BridgeParts = {})); editorInstances.set(editor, instance); } instance.mount(editor); }; const B = window.BridgeParts || (window.BridgeParts = {}); B.BridgeRTE = BridgeRTE; B.setupRichTextEditor = setupRichTextEditor; })();