diff --git a/public/editor/bridge-core.js b/public/editor/bridge-core.js index 518891a..febbb96 100644 --- a/public/editor/bridge-core.js +++ b/public/editor/bridge-core.js @@ -263,6 +263,319 @@ autosave: false, }; + const execRteCommand = (rte, cmd, value) => { + try { + if (rte && typeof rte.exec === 'function') { + rte.exec(cmd, value); + return true; + } + } catch {} + try { + const ok = document.execCommand(cmd, false, value); + if (ok === false && cmd === 'insertText') { + document.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 || !component.is('text')) return; + const toolbar = (component.get && component.get('toolbar')) || []; + if (toolbar.some((item) => item && item.command === 'bridge-open-richtext')) return; + toolbar.push({ + attributes: { class: 'fa fa-pencil', title: 'Richtext bearbeiten' }, + command: 'bridge-open-richtext', + }); + component.set && component.set('toolbar', toolbar); + }; + + const openRichTextModal = (editor, component) => { + if (!component || !component.is || !component.is('text')) { + log('RTE', 'Bitte zuerst ein Text-Element auswaehlen.', '#888'); + return; + } + + const modal = editor.Modal; + if (!modal) return; + + 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 = (label, title, cmd, valueGetter) => { + const btn = doc.createElement('button'); + btn.type = 'button'; + btn.textContent = label; + 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); + }); + 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, '>')); + } + }; + + addButton('B', 'Fett', 'bold'); + addButton('I', 'Kursiv', 'italic'); + addButton('U', 'Unterstrichen', 'underline'); + addButton('S', 'Durchgestrichen', 'strikethrough'); + addButton('Link', 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://')); + addButton('Unlink', 'Link entfernen', 'unlink'); + addButton('UL', 'Liste (ungeordnet)', 'insertUnorderedList'); + addButton('OL', 'Liste (geordnet)', 'insertOrderedList'); + addButton('L', 'Linksbundig', 'justifyLeft'); + addButton('C', 'Zentriert', 'justifyCenter'); + addButton('R', 'Rechtsbundig', 'justifyRight'); + addButton('J', 'Blocksatz', 'justifyFull'); + addButton('Sub', 'Tiefgestellt', 'subscript'); + addButton('Sup', 'Hochgestellt', 'superscript'); + addButton('Einr.', 'Einzug', 'indent'); + addButton('Aus.', 'Ausruecken', 'outdent'); + addButton('Clear', 'Formatierung entfernen', 'removeFormat'); + + addSelect([ + { label: 'Schriftart', value: '' }, + { label: 'Arial', value: 'Arial, sans-serif' }, + { label: 'Georgia', value: 'Georgia, serif' }, + { label: 'Tahoma', value: 'Tahoma, sans-serif' }, + { label: 'Times New Roman', value: 'Times New Roman, serif' }, + { label: 'Verdana', value: 'Verdana, sans-serif' }, + ], 'Schriftart', (value) => execRteCommand(null, '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) => execRteCommand(null, '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) 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', () => modal.close()); + + const saveBtn = doc.createElement('button'); + saveBtn.type = 'button'; + saveBtn.textContent = 'Uebernehmen'; + 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; + if (component.is && component.is('text')) { + component.set && component.set('content', html); + } else if (component.components) { + component.components(html); + } + if (component.view && component.view.render) { + component.view.render(); + } + modal.close(); + }); + + actions.appendChild(cancelBtn); + actions.appendChild(saveBtn); + container.appendChild(actions); + + modal.setTitle('Richtext Editor'); + modal.setContent(container); + modal.open(); + }; + + const setupRichTextEditor = (editor) => { + if (!editor || !editor.RichTextEditor) return; + const rte = editor.RichTextEditor; + + 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', 'L', 'Linksbundig', 'justifyLeft'); + addAction('bridge-align-center', 'C', 'Zentriert', 'justifyCenter'); + addAction('bridge-align-right', 'R', 'Rechtsbundig', 'justifyRight'); + addAction('bridge-align-justify', 'J', 'Blocksatz', 'justifyFull'); + addAction('bridge-ul', 'UL', 'Liste (ungeordnet)', 'insertUnorderedList'); + addAction('bridge-ol', 'OL', 'Liste (geordnet)', 'insertOrderedList'); + addAction('bridge-emoji', ':-)', 'Emoticon einfuegen', 'insertText', () => prompt('Emoticon eingeben', ':)')); + addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => prompt('Schriftart (z.B. Arial, Georgia)', '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; + }); + addAction('bridge-open-richtext', 'RTE', 'Richtext Editor', 'execCommand', () => { + const component = editor.getSelected && editor.getSelected(); + openRichTextModal(editor, component); + return null; + }); + + if (!editor.Commands.get('bridge-open-richtext')) { + 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', + ]); + + editor.on('component:selected', (model) => ensureTextToolbarButton(editor, model)); + editor.on('component:add', (model) => ensureTextToolbarButton(editor, model)); + }; + var ed = grapesjs.init({ container: '#gjs', height: '100vh', @@ -309,6 +622,8 @@ log('CORE WARN', `textTags Konfiguration fehlgeschlagen: ${e.message}`, 'orange', 'warn'); } + setupRichTextEditor(ed); + // Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern. // Keep the fix off live updates to avoid cursor jumps; run only on RTE close.