diff --git a/public/assets/js/bridge/rte-editor.js b/public/assets/js/bridge/rte-editor.js new file mode 100644 index 0000000..f1ea16e --- /dev/null +++ b/public/assets/js/bridge/rte-editor.js @@ -0,0 +1,545 @@ +/* /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 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 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'); + return; + } + + 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.close === 'function') { + modal.close(); + } else { + 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 (!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 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.view && component.view.el && component.view.el.innerHTML) + || (component.get && component.get('content')) + || ''; + 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'; + saveBtn.addEventListener('click', () => { + const html = content.innerHTML; + const isTextLike = component && component.is && (component.is('text') || component.is('button') || component.is('link')); + const hasTags = /<[^>]+>/.test(html); + try { + if (component.components) { + component.components(html); + } + if (component.set) { + component.set('content', html); + component.trigger && component.trigger('change:content'); + component.trigger && component.trigger('change:components'); + } + 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); + } + 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); + } + }; + + 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); + }); + }; + + B.setupRichTextEditor = setupRichTextEditor; +})(); diff --git a/public/editor/bridge-core.js b/public/editor/bridge-core.js index af479ca..7adf10d 100644 --- a/public/editor/bridge-core.js +++ b/public/editor/bridge-core.js @@ -152,6 +152,9 @@         const initialLoadList = B.ENABLE_EDITOR_EXTENSIONS === false             ? [base + 'general-functions.js']             : [...coreFiles]; + if (B.ENABLE_EDITOR_EXTENSIONS !== false && B.ENABLE_RTE) { + initialLoadList.push(base + 'rte-editor.js'); + } if (B.ENABLE_EDITOR_EXTENSIONS !== false && B.ENABLE_TABLE_BUILDER) { initialLoadList.push(base + 'table-builder.js'); } @@ -275,537 +278,7 @@ autosave: false, }; - 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 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'); - return; - } - - 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.close === 'function') { - modal.close(); - } else { - 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 (!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 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.view && component.view.el && component.view.el.innerHTML) - || (component.get && component.get('content')) - || ''; - 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'; - saveBtn.addEventListener('click', () => { - const html = content.innerHTML; - const isTextLike = component && component.is && (component.is('text') || component.is('button') || component.is('link')); - const hasTags = /<[^>]+>/.test(html); - try { - if (component.components) { - component.components(html); - } - if (component.set) { - component.set('content', html); - component.trigger && component.trigger('change:content'); - component.trigger && component.trigger('change:components'); - } - 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); - } - 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); - } - }; - - 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); - }); - }; + // RichText-Editor ausgelagert nach /assets/js/bridge/rte-editor.js const setupPlainTextPreserver = (editor) => { const isTextLike = (model) => !!(model && model.is && (model.is('text') || model.is('button') || model.is('link') || model.is('textnode'))); @@ -1032,6 +505,13 @@ } } + if (B.ENABLE_EDITOR_EXTENSIONS !== false && B.ENABLE_RTE) { + if (typeof B.setupRichTextEditor === 'function') { + B.setupRichTextEditor(ed); + } else { + log('RTE WARN', 'RichText-Editor nicht geladen.', 'orange', 'warn'); + } + } if (B.ENABLE_EDITOR_EXTENSIONS !== false && (B.ENABLE_EDITOR_BEHAVIOR !== false || B.ENABLE_TABLE_BUILDER)) { if (typeof B.setupTableBuilder === 'function') { B.setupTableBuilder(ed); diff --git a/public/editor/editor-core.php b/public/editor/editor-core.php index d3c7b3b..22b9a22 100644 --- a/public/editor/editor-core.php +++ b/public/editor/editor-core.php @@ -53,6 +53,7 @@ if ($fontSources) { window.BridgeParts.ENABLE_EDITOR_BEHAVIOR = false; window.BridgeParts.ENABLE_PLACEHOLDERS = true; window.BridgeParts.ENABLE_TABLE_BUILDER = true; + window.BridgeParts.ENABLE_RTE = true; window.BridgeParts.LOG_CONFIG = window.BridgeParts.LOG_CONFIG || {}; window.BridgeParts.LOG_CONFIG.INFO_ENABLED = false; window.BridgeParts.LOG_CONFIG.DATA_ENABLED = false;