/* /assets/js/bridge/blocks-placeholder.js (LOG-KONTROLLIERT) */ (function () { const PluginName = 'blocks-placeholder'; const B = window.BridgeParts || (window.BridgeParts = {}); if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) { B.LOG_CONFIG.PLUGINS[PluginName] = false; // Lokaler Schalter für dieses Plugin } const log = (type, message, color = '#FFD700', logType = 'info', force = false) => { if (typeof B.log === 'function') { B.log(PluginName, `[${type}] ${message}`, color, logType, force); } else if (logType === 'error') { console.error(`%c[${PluginName} - ${type}] %c${message}`, `color:red; font-weight:bold;`, 'color:inherit;'); } }; log('FILE CHECK', 'Placeholder-Datei-IIFE startet.'); if (window.__PLACEHOLDER_BLOCKS_LOADED) return; window.__PLACEHOLDER_BLOCKS_LOADED = true; const TARGET_CAT_ID = 'placeholders'; const PLACEHOLDER_COMPONENT = 'placeholder-block'; const ALL_PLACEHOLDER_BLOCK_IDS = []; const INLINE_PLACEHOLDER_CLASS = 'bridge-placeholder-inline'; const PLACEHOLDER_MARKER_ATTR = 'data-placeholder-marker'; const placeholderSchemaStore = { promise: null, tables: [], status: null, statusPromise: null, }; let inlineStyleInjected = false; let suppressPlaceholderPrompt = 0; const ensureInlinePlaceholderStyles = () => { if (inlineStyleInjected || typeof document === 'undefined') return; inlineStyleInjected = true; const style = document.createElement('style'); style.id = 'bridge-placeholder-inline-style'; style.textContent = ` .${INLINE_PLACEHOLDER_CLASS}{ display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border:1px dashed #94a3b8; border-radius:6px; background:#f1f5f9; font-family:monospace; font-size:12px; cursor:pointer; user-select:none; } .${INLINE_PLACEHOLDER_CLASS}:hover{ background:#e2e8f0; } `; document.head && document.head.appendChild(style); }; const createEl = (tag, props = {}, children = []) => { const el = document.createElement(tag); Object.entries(props || {}).forEach(([key, value]) => { if (value === undefined || value === null) return; if (key === 'className') { el.className = value; } else if (key === 'text') { el.textContent = value; } else if (key === 'html') { el.innerHTML = value; } else if (key in el) { el[key] = value; } else { el.setAttribute(key, value); } }); (Array.isArray(children) ? children : [children]).forEach(child => { if (child) el.appendChild(child); }); return el; }; const applyInputStyles = (el) => { if (!el) return el; el.style.width = '100%'; el.style.boxSizing = 'border-box'; el.style.padding = '6px 8px'; el.style.border = '1px solid #cbd5f5'; el.style.borderRadius = '4px'; el.style.fontSize = '13px'; el.style.fontFamily = 'inherit'; return el; }; const syncSelectOptions = (selectEl, options, selected) => { if (!selectEl) return; while (selectEl.firstChild) { selectEl.removeChild(selectEl.firstChild); } (options || []).forEach(opt => { const optionEl = document.createElement('option'); optionEl.value = opt.id ?? opt.value ?? ''; optionEl.textContent = opt.label ?? opt.text ?? ''; if (opt.disabled) optionEl.disabled = true; selectEl.appendChild(optionEl); }); if (selected !== undefined && selected !== null) { selectEl.value = selected; } }; const safeRemoveComponent = (component) => { if (!component || typeof component.remove !== 'function') return; try { component.remove(); } catch (err) { log('PLACEHOLDER WARN', `Komponente konnte nicht entfernt werden: ${err && err.message ? err.message : err}`, '#b45309'); } }; const refreshPlaceholderComponent = (component) => { if (!component) return; if (typeof component.updatePlaceholderState === 'function') { try { component.updatePlaceholderState(); } catch (err) { log('PLACEHOLDER WARN', `updatePlaceholderState Fehler: ${err && err.message ? err.message : err}`, '#b45309'); } } else if (typeof component.trigger === 'function') { component.trigger('change:attributes'); } if (component.view && typeof component.view.render === 'function') { component.view.render(); } if (component.em && typeof component.em.trigger === 'function') { component.em.trigger('component:update', component); } }; const buildPlaceholderLabel = (payload) => { const type = 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 buildPlaceholderHTML = (payload) => { const type = payload.type === 'database' ? 'database' : 'custom'; const attrs = [ `data-gjs-type="${PLACEHOLDER_COMPONENT}"`, `data-placeholder-type="${type}"`, `contenteditable="false"`, `class="${INLINE_PLACEHOLDER_CLASS}"`, ]; if (type === 'database') { attrs.push(`data-placeholder-table="${payload.table || ''}"`); attrs.push(`data-placeholder-column="${payload.column || ''}"`); } else { attrs.push(`data-placeholder-key="${payload.key || ''}"`); } const label = buildPlaceholderLabel(payload); return `{{${label}}}`; }; const buildField = (labelText, control) => { const controlId = `bridge-placeholder-field-${Math.random().toString(36).slice(2)}`; control.id = controlId; const label = createEl('label', { htmlFor: controlId, text: labelText }); label.style.display = 'block'; label.style.fontWeight = '600'; label.style.fontSize = '13px'; label.style.marginBottom = '4px'; const wrapper = createEl('div', { className: 'bridge-placeholder-field' }, [label, control]); wrapper.style.marginBottom = '12px'; return wrapper; }; const buildTextnodeComponents = (el) => { if (!el) return []; const nodes = Array.from(el.childNodes || []); const comps = []; nodes.forEach((node) => { if (!node) return; if (node.nodeType === 3) { const text = node.textContent; if (text !== null && text !== undefined && text !== '') { comps.push({ type: 'textnode', content: text }); } return; } if (node.nodeType === 1 && node.outerHTML) { comps.push(node.outerHTML); } }); return comps; }; const reparseTextComponent = (component, editor) => { if (!component || typeof component.components !== 'function') return; const viewEl = component.view && component.view.el; if (!viewEl) return; const comps = buildTextnodeComponents(viewEl); try { if (comps.length) { component.components(comps); } if (component.em && typeof component.em.trigger === 'function') { component.em.trigger('component:update', component); } else if (editor && typeof editor.trigger === 'function') { editor.trigger('component:update', component); } } catch (err) { log('PLACEHOLDER ERROR', `Reparsing des Text-Elements fehlgeschlagen: ${err && err.message ? err.message : err}`, 'red', 'error'); } }; const captureRteSelection = (rteInstance) => { const doc = rteInstance && rteInstance.doc ? rteInstance.doc : (rteInstance && rteInstance.el && rteInstance.el.ownerDocument) || document; if (!doc || !doc.getSelection) return null; const sel = doc.getSelection(); if (!sel || !sel.rangeCount) return null; return { doc, range: sel.getRangeAt(0).cloneRange() }; }; const restoreRteSelection = (snapshot) => { if (!snapshot || !snapshot.doc || !snapshot.range) return false; const sel = snapshot.doc.getSelection && snapshot.doc.getSelection(); if (!sel) return false; try { sel.removeAllRanges(); sel.addRange(snapshot.range); return true; } catch (err) { log('PLACEHOLDER WARN', `Auswahl konnte nicht wiederhergestellt werden: ${err && err.message ? err.message : err}`, '#b45309'); return false; } }; const insertPlaceholderIntoSelection = (rteInstance, html) => { const doc = rteInstance && rteInstance.doc ? rteInstance.doc : (rteInstance && rteInstance.el && rteInstance.el.ownerDocument) || document; if (!doc) return false; const sel = doc.getSelection && doc.getSelection(); if (!sel || !sel.rangeCount) return false; const range = sel.getRangeAt(0); if (!range) return false; range.deleteContents(); const temp = doc.createElement('div'); temp.innerHTML = html; const fragment = doc.createDocumentFragment(); while (temp.firstChild) { fragment.appendChild(temp.firstChild); } range.insertNode(fragment); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); return true; }; const insertCaretMarker = (rteInstance, markerId) => { if (!markerId) return false; const markerHtml = ``; if (insertPlaceholderIntoSelection(rteInstance, markerHtml)) { return true; } if (rteInstance && typeof rteInstance.insertHTML === 'function') { rteInstance.insertHTML(markerHtml); return true; } if (typeof document !== 'undefined' && document.execCommand) { document.execCommand('insertHTML', false, markerHtml); return true; } return false; }; const removeMarkerFromComponent = (component, markerId, editor) => { if (!component || !markerId) return false; const viewEl = component.view && component.view.el; if (!viewEl) return false; const selector = `[${PLACEHOLDER_MARKER_ATTR}="${markerId}"]`; const marker = viewEl.querySelector(selector); if (!marker || !marker.parentNode) return false; marker.parentNode.removeChild(marker); reparseTextComponent(component, editor); return true; }; const replaceMarkerWithPlaceholder = (component, markerId, payload, editor) => { if (!component || !markerId) return false; const viewEl = component.view && component.view.el; if (!viewEl) return false; const marker = viewEl.querySelector(`[${PLACEHOLDER_MARKER_ATTR}="${markerId}"]`); if (!marker) return false; if (typeof document === 'undefined') return false; const temp = document.createElement('div'); temp.innerHTML = buildPlaceholderHTML(payload); const placeholderNode = temp.firstElementChild || temp.firstChild; if (!placeholderNode) return false; if (typeof marker.replaceWith === 'function') { marker.replaceWith(placeholderNode); } else if (marker.parentNode) { marker.parentNode.insertBefore(placeholderNode, marker); marker.parentNode.removeChild(marker); } else { return false; } reparseTextComponent(component, editor); return true; }; const openPlaceholderModal = (editor, component, opts = {}) => { if (!editor) return; const modal = editor.Modal; if (!modal) return; ensureInlinePlaceholderStyles(); if (component && (!component.is || !component.is(PLACEHOLDER_COMPONENT))) { log('PLACEHOLDER WARN', 'openPlaceholderModal wurde ohne gültige Placeholder-Komponente aufgerufen.', '#b45309'); return; } let didSave = false; let didCancel = false; const fireCancel = (reason) => { if (didSave || didCancel) return; didCancel = true; if (typeof opts.onCancel === 'function') { try { opts.onCancel({ reason, component, modal }); } catch (err) { log('PLACEHOLDER WARN', `onCancel Fehler: ${err && err.message ? err.message : err}`, '#b45309'); } } }; const attrs = component && component.getAttributes ? component.getAttributes() : (opts.initial || {}); const initialType = attrs['data-placeholder-type'] || 'custom'; const initialKey = attrs['data-placeholder-key'] || 'UEBERSCHRIFT'; const initialTable = attrs['data-placeholder-table'] || ''; const initialColumn = attrs['data-placeholder-column'] || ''; const container = createEl('div', { className: 'bridge-placeholder-modal' }); container.style.padding = '8px 4px'; const form = createEl('form', { className: 'bridge-placeholder-form' }); form.style.display = 'flex'; form.style.flexDirection = 'column'; const typeSelect = applyInputStyles(createEl('select')); [ { id: 'custom', label: 'Placeholder (Text)' }, { id: 'database', label: 'Placeholder (DB)' }, ].forEach(opt => { const optionEl = createEl('option', { value: opt.id, text: opt.label }); typeSelect.appendChild(optionEl); }); typeSelect.value = initialType; const keyInput = applyInputStyles(createEl('input', { type: 'text', value: initialKey })); keyInput.placeholder = 'z.B. UEBERSCHRIFT'; const tableSelect = applyInputStyles(createEl('select')); const columnSelect = applyInputStyles(createEl('select')); tableSelect.disabled = true; columnSelect.disabled = true; const dbMessage = createEl('p', { className: 'bridge-placeholder-hint', text: '' }); dbMessage.style.fontSize = '12px'; dbMessage.style.margin = '4px 0 0'; dbMessage.style.color = '#475569'; const columnMessage = createEl('p', { className: 'bridge-placeholder-hint', text: '' }); columnMessage.style.fontSize = '12px'; columnMessage.style.margin = '4px 0 0'; columnMessage.style.color = '#475569'; const customWrap = createEl('div', { className: 'bridge-placeholder-section' }, [ buildField('Bezeichner', keyInput), ]); const dbWrap = createEl('div', { className: 'bridge-placeholder-section' }, [ buildField('Tabelle', tableSelect), dbMessage, buildField('Feld', columnSelect), columnMessage, ]); const errorBox = createEl('p', { className: 'bridge-placeholder-error', text: '' }); errorBox.style.fontSize = '12px'; errorBox.style.color = '#b91c1c'; errorBox.style.margin = '4px 0 0'; form.appendChild(buildField('Typ', typeSelect)); form.appendChild(customWrap); form.appendChild(dbWrap); form.appendChild(errorBox); const actions = createEl('div', { className: 'bridge-placeholder-actions' }); actions.style.marginTop = '16px'; actions.style.display = 'flex'; actions.style.justifyContent = 'flex-end'; actions.style.gap = '8px'; const cancelBtn = createEl('button', { type: 'button', text: 'Abbrechen' }); cancelBtn.style.padding = '6px 12px'; cancelBtn.style.border = '1px solid #cbd5f5'; cancelBtn.style.borderRadius = '4px'; cancelBtn.style.background = '#f8fafc'; cancelBtn.style.cursor = 'pointer'; const saveBtn = createEl('button', { type: 'submit', text: 'Speichern' }); saveBtn.style.padding = '6px 16px'; saveBtn.style.border = '1px solid #0ea5e9'; saveBtn.style.background = '#0ea5e9'; saveBtn.style.color = '#fff'; saveBtn.style.borderRadius = '4px'; saveBtn.style.cursor = 'pointer'; actions.appendChild(cancelBtn); actions.appendChild(saveBtn); form.appendChild(actions); container.appendChild(form); let tablesCache = placeholderSchemaStore.tables || []; const toggleSections = () => { const type = typeSelect.value || 'custom'; customWrap.style.display = type === 'custom' ? '' : 'none'; dbWrap.style.display = type === 'database' ? '' : 'none'; }; const populateColumns = (tableName, preferredColumn) => { const table = (tablesCache || []).find(tbl => (tbl.name || '').toLowerCase() === (tableName || '').toLowerCase()); const columns = table && Array.isArray(table.columns) ? table.columns : []; if (!columns.length) { syncSelectOptions(columnSelect, [{ id: '', label: columns.length ? '– Feld wählen –' : 'Keine Felder verfügbar' }], ''); columnSelect.disabled = true; columnMessage.textContent = table ? 'Diese Tabelle enthält keine Felder.' : 'Bitte zuerst eine Tabelle wählen.'; return; } const opts = columns.map(col => ({ id: col.name, label: col.name + (col.type ? ` (${col.type})` : ''), })); syncSelectOptions(columnSelect, opts, preferredColumn || initialColumn || opts[0].id); columnSelect.disabled = false; columnMessage.textContent = ''; }; const populateTables = (tables) => { tablesCache = Array.isArray(tables) ? tables : []; if (!tablesCache.length) { syncSelectOptions(tableSelect, [{ id: '', label: 'Keine Tabellen gefunden' }], ''); tableSelect.disabled = true; columnSelect.disabled = true; dbMessage.textContent = 'Kein Tabellen-Schema verfügbar. Bitte Bridge-Setup prüfen.'; columnMessage.textContent = ''; return; } const opts = tablesCache.map(tbl => ({ id: tbl.name, label: tbl.name })); syncSelectOptions(tableSelect, opts, initialTable || opts[0].id); tableSelect.disabled = false; dbMessage.textContent = ''; populateColumns(tableSelect.value, initialColumn); }; const loadTables = () => { if (placeholderSchemaStore.tables && placeholderSchemaStore.tables.length) { populateTables(placeholderSchemaStore.tables); return; } dbMessage.textContent = 'Tabellen werden geladen …'; fetchPlaceholderSchema() .then(populateTables) .catch(() => { populateTables([]); }); }; if (tablesCache.length) { populateTables(tablesCache); } else if (placeholderSchemaStore.promise) { placeholderSchemaStore.promise.then(populateTables).catch(() => populateTables([])); } else if (placeholderSchemaStore.status !== false) { fetchPlaceholderSchema() .then(populateTables) .catch(() => populateTables([])); } tableSelect.addEventListener('change', () => { populateColumns(tableSelect.value); }); typeSelect.addEventListener('change', () => { toggleSections(); if (typeSelect.value === 'database' && (!tablesCache || !tablesCache.length)) { loadTables(); } }); cancelBtn.addEventListener('click', (e) => { e.preventDefault(); fireCancel('cancel-button'); modal.close(); }); const applyPayload = (payload) => { if (typeof opts.onSubmit === 'function') { const res = opts.onSubmit(payload, { component, modal }); return res !== false; } if (!component || typeof component.addAttributes !== 'function') { log('PLACEHOLDER INFO', 'Keine Ziel-Komponente gefunden – Placeholder-Daten werden verworfen.', '#888'); return true; } if (payload.type === 'custom') { component.addAttributes({ 'data-placeholder-type': 'custom', 'data-placeholder-key': payload.key, 'data-placeholder-table': '', 'data-placeholder-column': '', }); } else { component.addAttributes({ 'data-placeholder-type': 'database', 'data-placeholder-key': '', 'data-placeholder-table': payload.table, 'data-placeholder-column': payload.column, }); } refreshPlaceholderComponent(component); component.__bridgePlaceholderNew = false; return true; }; form.addEventListener('submit', (e) => { e.preventDefault(); errorBox.textContent = ''; const type = typeSelect.value || 'custom'; if (type === 'custom') { const key = (keyInput.value || '').trim(); if (!key) { errorBox.textContent = 'Bitte einen Bezeichner eingeben.'; keyInput.focus(); return; } const payload = { type: 'custom', key, table: '', column: '', }; if (!applyPayload(payload)) { return; } } else { const table = tableSelect.value || ''; const column = columnSelect.value || ''; if (!table || !column) { errorBox.textContent = 'Bitte Tabelle und Feld auswählen.'; if (!table) { tableSelect.focus(); } else { columnSelect.focus(); } return; } const payload = { type: 'database', key: '', table, column, }; if (!applyPayload(payload)) { return; } } didSave = true; modal.close(); }); toggleSections(); if (typeSelect.value === 'database') { loadTables(); } modal.setTitle('Placeholder konfigurieren'); modal.setContent(container); if (typeof modal.onceClose === 'function') { modal.onceClose(() => fireCancel('modal-close')); } else if (modal.getModel && typeof modal.getModel === 'function') { const mdl = modal.getModel(); if (mdl && typeof mdl.on === 'function') { const handler = () => { if (!mdl.get('open')) { mdl.off && mdl.off('change:open', handler); fireCancel('modal-close'); } }; mdl.on && mdl.on('change:open', handler); } } modal.open(); if (component && editor.getSelected && editor.getSelected() !== component) { editor.select && editor.select(component); } }; const ensureRtePlaceholderButton = (editor) => { const rte = editor && editor.RichTextEditor; if (!rte || rte.__bridgePlaceholderButton) return; rte.__bridgePlaceholderButton = true; rte.add('bridge-placeholder', { icon: '{}', attributes: { title: 'Placeholder einfügen' }, result: (rteInstance) => { const target = editor && editor.getSelected && editor.getSelected(); if (!target || !target.is || !target.is('text')) { log('PLACEHOLDER INFO', 'Bitte zuerst ein Text-Element auswählen.', '#888'); return; } const selectionSnapshot = captureRteSelection(rteInstance); if (!selectionSnapshot) { log('PLACEHOLDER ERROR', 'Keine Text-Selektion gefunden. Bitte Cursor setzen und erneut versuchen.', 'red', 'error'); return; } const markerId = `bridge-placeholder-marker-${Date.now()}-${Math.random().toString(36).slice(2)}`; const restoreSelection = () => restoreRteSelection(selectionSnapshot); let markerInserted = false; if (restoreSelection()) { markerInserted = insertCaretMarker(rteInstance, markerId); if (markerInserted) { setTimeout(() => reparseTextComponent(target, editor), 0); } } const cleanupMarker = () => { if (markerInserted) { removeMarkerFromComponent(target, markerId, editor); markerInserted = false; } }; openPlaceholderModal(editor, null, { onCancel: () => { cleanupMarker(); if (rteInstance && typeof rteInstance.focus === 'function') { setTimeout(() => rteInstance.focus(), 0); } }, onSubmit: (payload) => { if (editor) { editor.__bridgePlaceholderSuppressAdd = (editor.__bridgePlaceholderSuppressAdd || 0) + 1; } let replaced = false; if (markerInserted) { replaced = replaceMarkerWithPlaceholder(target, markerId, payload, editor); } if (!replaced && restoreSelection()) { const html = buildPlaceholderHTML(payload); if (insertPlaceholderIntoSelection(rteInstance, html)) { setTimeout(() => reparseTextComponent(target, editor), 0); replaced = true; } } if (!replaced) { cleanupMarker(); const viewEl = target.view && target.view.el; if (viewEl) { viewEl.insertAdjacentHTML('beforeend', buildPlaceholderHTML(payload)); reparseTextComponent(target, editor); replaced = true; } else { log('PLACEHOLDER ERROR', 'Placeholder konnte nicht eingefügt werden (kein Ziel).', 'red', 'error'); return false; } } if (rteInstance && typeof rteInstance.focus === 'function') { setTimeout(() => rteInstance.focus(), 0); } return replaced; } }); }, }); log('RTE', 'Placeholder-Button im RichTextEditor registriert.', '#DAA520'); }; function addOnce(bm, id, def, category = TARGET_CAT_ID) { try { bm.add(id, { ...def, category }); ALL_PLACEHOLDER_BLOCK_IDS.push(id); log('BLOCK ADD', `Block '${id}' erfolgreich hinzugefügt.`, '#B8860B'); } catch (e) { log('BLOCK ERROR', `Fehler beim Hinzufügen von Block '${id}': ${e.message}`, 'red', 'error'); } } const ensureBridgeAvailability = () => { if (placeholderSchemaStore.status !== null) { return Promise.resolve(placeholderSchemaStore.status); } if (placeholderSchemaStore.statusPromise) { return placeholderSchemaStore.statusPromise; } const base = B.API_KERNEL_URL || '/api.php'; const sep = base.includes('?') ? '&' : '?'; const url = `${base}${sep}action=placeholders.status`; placeholderSchemaStore.statusPromise = fetch(url, { credentials: 'include' }) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(data => { const available = !!(data && (data.available || (data.settings && data.settings.available))); placeholderSchemaStore.status = available; placeholderSchemaStore.statusPromise = null; if (!available) { log('PLACEHOLDER INFO', 'Bridge-Placeholders nicht konfiguriert – DB-Funktionen deaktiviert.', '#64748b'); } return available; }) .catch(err => { placeholderSchemaStore.status = false; placeholderSchemaStore.statusPromise = null; log('PLACEHOLDER WARN', `Bridge-Status konnte nicht geprüft werden: ${err && err.message ? err.message : err}`, '#b45309'); return false; }); return placeholderSchemaStore.statusPromise; }; const fetchPlaceholderSchema = () => { if (placeholderSchemaStore.promise) return placeholderSchemaStore.promise; placeholderSchemaStore.promise = ensureBridgeAvailability().then(isAvailable => { if (!isAvailable) throw new Error('Bridge not available'); const base = B.API_KERNEL_URL || '/api.php'; const sep = base.includes('?') ? '&' : '?'; const url = `${base}${sep}action=placeholders.schema`; return fetch(url, { credentials: 'include' }) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(data => { const tbls = data && Array.isArray(data.tables) ? data.tables : []; placeholderSchemaStore.tables = tbls; return placeholderSchemaStore.tables; }); }).catch(err => { placeholderSchemaStore.tables = []; placeholderSchemaStore.promise = null; const msg = err && err.message ? err.message : err; if (msg === 'Bridge not available') { log('PLACEHOLDER INFO', 'Schema-Abfrage übersprungen (keine Bridge verfügbar).', '#64748b'); } else { log('PLACEHOLDER ERROR', `Schema konnte nicht geladen werden: ${msg}`, 'red', 'error'); } throw err; }); return placeholderSchemaStore.promise; }; const getTraitByName = (model, name) => { if (typeof model.getTrait === 'function') return model.getTrait(name); const traits = model.get('traits'); if (!traits) return null; if (typeof traits.where === 'function') { const found = traits.where({ name }); return found && found[0]; } if (Array.isArray(traits.models)) { return traits.models.find(t => t.get && t.get('name') === name) || null; } return null; }; const cloneValue = (value) => { if (Array.isArray(value)) { return value.map(cloneValue); } if (value && typeof value === 'object') { const copy = {}; Object.keys(value).forEach(key => { copy[key] = cloneValue(value[key]); }); return copy; } return value; }; const defaultPlaceholderProps = { name: 'Placeholder', tagName: 'span', droppable: false, attributes: { 'data-placeholder-type': 'custom', 'data-placeholder-key': 'UEBERSCHRIFT', 'contenteditable': 'false', 'class': INLINE_PLACEHOLDER_CLASS, }, traits: [ { type: 'select', name: 'data-placeholder-type', label: 'Typ', options: [ { id: 'custom', label: 'Allgemein' }, { id: 'database', label: 'Datenbank' }, ], changeProp: true, }, { type: 'text', name: 'data-placeholder-key', label: 'Bezeichner', placeholder: 'UEBERSCHRIFT', changeProp: true, }, { type: 'select', name: 'data-placeholder-table', label: 'Tabelle', options: [], changeProp: true, }, { type: 'select', name: 'data-placeholder-column', label: 'Feld', options: [], changeProp: true, }, ], toolbar: [ { attributes: { class: 'fa fa-edit', title: 'Placeholder bearbeiten' }, command: 'bridge-placeholder:edit', }, ], }; const applyPlaceholderDefaults = (model) => { if (!model || typeof model.get !== 'function' || typeof model.set !== 'function') { return; } Object.entries(defaultPlaceholderProps).forEach(([key, value]) => { if (typeof model.get(key) === 'undefined') { model.set(key, cloneValue(value)); } }); }; const ensurePlaceholderComponent = (editor) => { const domc = editor.DomComponents; if (domc.getType(PLACEHOLDER_COMPONENT)) return; ensureInlinePlaceholderStyles(); const baseType = domc.getType('text') || domc.getType('default') || {}; const BaseModel = baseType.model || editor.DomComponents.Component; const BaseView = baseType.view || editor.DomComponents.View; const PlaceholderModel = BaseModel.extend({ init() { applyPlaceholderDefaults(this); this.listenTo(this, 'change:attributes', this.updatePlaceholderState); this.updatePlaceholderState(); fetchPlaceholderSchema() .then(() => this.updateSchemaTraits()) .catch(() => this.updateSchemaTraits([])); }, updatePlaceholderState() { const attrs = this.getAttributes(); if (!attrs['data-placeholder-type']) { this.addAttributes({ 'data-placeholder-type': 'custom' }); } if (attrs['contenteditable'] !== 'false') { this.addAttributes({ 'contenteditable': 'false' }); } this.updateTraitVisibility(); this.updateSchemaTraits(); this.updateLabel(); }, updateTraitVisibility() { const attrs = this.getAttributes(); const type = attrs['data-placeholder-type'] || 'custom'; const isDb = type === 'database'; const tableTrait = getTraitByName(this, 'data-placeholder-table'); const columnTrait = getTraitByName(this, 'data-placeholder-column'); const keyTrait = getTraitByName(this, 'data-placeholder-key'); if (tableTrait && tableTrait.view && tableTrait.view.el) { tableTrait.view.el.style.display = isDb ? '' : 'none'; } if (columnTrait && columnTrait.view && columnTrait.view.el) { columnTrait.view.el.style.display = isDb ? '' : 'none'; } if (keyTrait && keyTrait.view && keyTrait.view.el) { keyTrait.view.el.style.display = isDb ? 'none' : ''; } }, updateSchemaTraits(tablesOverride) { const tables = Array.isArray(tablesOverride) ? tablesOverride : placeholderSchemaStore.tables; const tableTrait = getTraitByName(this, 'data-placeholder-table'); const columnTrait = getTraitByName(this, 'data-placeholder-column'); const loading = !tablesOverride && placeholderSchemaStore.promise && !tables.length; if (tableTrait) { let opts; if (tables.length) { opts = tables.map(function (tbl) { return { id: tbl.name, label: tbl.name }; }); } else if (loading) { opts = [{ id: '', label: 'Tabellen werden geladen…', disabled: true }]; } else { opts = [{ id: '', label: 'Keine Tabellen verfügbar', disabled: true }]; } setTraitOptions(tableTrait, opts); } if (columnTrait) { const attrs = this.getAttributes(); const tableName = (attrs['data-placeholder-table'] || '').toLowerCase(); const table = tables.find(function (tbl) { return (tbl.name || '').toLowerCase() === tableName; }); const columns = table && Array.isArray(table.columns) ? table.columns : []; const colOpts = columns.length ? columns.map(function (col) { return { id: col.name, label: col.name + (col.type ? ' (' + col.type + ')' : '') }; }) : [{ id: '', label: table ? 'Keine Felder' : 'Feld wählen', disabled: !table }]; setTraitOptions(columnTrait, colOpts); } }, updateLabel() { const attrs = this.getAttributes(); const type = attrs['data-placeholder-type'] || 'custom'; let label; if (type === 'database') { const table = attrs['data-placeholder-table'] || 'TABELLE'; const column = attrs['data-placeholder-column'] || 'FELD'; label = (table + '.' + column).toUpperCase(); } else { label = (attrs['data-placeholder-key'] || 'PLATZHALTER').toUpperCase(); } const text = '{{' + label + '}}'; const comps = this.components(); const onlyText = comps.length === 1 && comps.at(0).is('textnode'); if (onlyText) { comps.at(0).set('content', text); } else { comps.reset([{ type: 'textnode', content: text }]); } } }); const PlaceholderView = BaseView.extend({ render() { BaseView.prototype.render.apply(this, arguments); this.el.classList.add('placeholder-block'); this.el.classList.add(INLINE_PLACEHOLDER_CLASS); this.el.setAttribute('contenteditable', 'false'); this.el.style.display = 'inline-flex'; this.el.style.alignItems = 'center'; this.el.style.gap = '4px'; this.el.style.padding = '2px 8px'; this.el.style.border = '1px dashed #94a3b8'; this.el.style.borderRadius = '6px'; this.el.style.background = '#f1f5f9'; this.el.style.fontFamily = 'monospace'; this.el.style.fontSize = '12px'; return this; }, }); domc.addType(PLACEHOLDER_COMPONENT, { model: PlaceholderModel, view: PlaceholderView, isComponent(el) { if (el && el.hasAttribute && el.hasAttribute('data-placeholder-type')) { return { type: PLACEHOLDER_COMPONENT }; } return false; }, }); }; const patchTextComponentDroppable = (component) => { if (!component || !component.is || !component.is('text')) { return; } if (component.__bridgePlaceholderDroppable) { return; } component.__bridgePlaceholderDroppable = true; const originalDroppable = component.get('droppable'); const allowFn = function (source, cmp) { if (cmp && cmp.get && cmp.get('type') === PLACEHOLDER_COMPONENT) { return true; } if (typeof originalDroppable === 'function') { return originalDroppable.call(this, source, cmp); } if (typeof originalDroppable === 'undefined') { return true; } return originalDroppable; }; component.set('droppable', allowFn); }; const ensureTextSupportsPlaceholders = (editor) => { const wrapper = editor.getWrapper && editor.getWrapper(); if (wrapper) { if (typeof wrapper.findType === 'function') { wrapper.findType('text').forEach(patchTextComponentDroppable); } else if (typeof wrapper.find === 'function') { wrapper.find('[data-gjs-type="text"]').forEach(patchTextComponentDroppable); } } editor.on('component:add', (cmp) => patchTextComponentDroppable(cmp)); }; function setTraitOptions(trait, options) { if (!trait) return; trait.set('options', options); if (trait.view && typeof trait.view.render === 'function') { trait.view.render(); } } function register(editor) { log('EXECUTION', `Starte Placeholder-Registrierung für ${TARGET_CAT_ID}.`, '#DAA520'); const bm = editor.BlockManager; ensurePlaceholderComponent(editor); ensureTextSupportsPlaceholders(editor); if (!placeholderSchemaStore.promise && placeholderSchemaStore.status !== false) { fetchPlaceholderSchema().catch(() => {}); } if (!editor.Commands.get('bridge-placeholder:edit')) { editor.Commands.add('bridge-placeholder:edit', { run(ed, sender, opts = {}) { const component = opts.component || ed.getSelected(); if (component && component.is && component.is(PLACEHOLDER_COMPONENT)) { openPlaceholderModal(ed, component); } else { log('PLACEHOLDER INFO', 'Kein Placeholder ausgewählt.', '#888'); } }, }); } if (!editor.__bridgePlaceholderEventsBound) { editor.__bridgePlaceholderEventsBound = true; editor.on('component:dblclick', (cmp) => { if (cmp && cmp.is && cmp.is(PLACEHOLDER_COMPONENT)) { openPlaceholderModal(editor, cmp); return false; } return undefined; }); editor.on('component:add', (cmp) => { if (!cmp || !cmp.is || !cmp.is(PLACEHOLDER_COMPONENT)) return; if (editor.__bridgePlaceholderSuppressAdd) { editor.__bridgePlaceholderSuppressAdd = Math.max(0, (editor.__bridgePlaceholderSuppressAdd || 1) - 1); return; } if (window.__GJS_IS_PARSING) return; if (cmp.__bridgePlaceholderPrompted) return; cmp.__bridgePlaceholderPrompted = true; cmp.__bridgePlaceholderNew = true; setTimeout(() => openPlaceholderModal(editor, cmp, { onCancel: () => { if (cmp.__bridgePlaceholderNew) { safeRemoveComponent(cmp); } } }), 50); }); } const bindRteButton = () => ensureRtePlaceholderButton(editor); if (editor.RichTextEditor) { bindRteButton(); } else if (typeof editor.on === 'function') { editor.on('load', bindRteButton, { once: true }); } addOnce(bm, 'cust-placeholder-custom', { id: 'cust-placeholder-custom', label: '🔖 Placeholder (Text)', content: `{{UEBERSCHRIFT}}` }); const ensureDbBlock = (tables) => { const fallback = { table: 'tabelle', column: 'feld' }; let tableName = fallback.table; let columnName = fallback.column; if (Array.isArray(tables) && tables.length) { const tableWithColumns = tables.find(tbl => Array.isArray(tbl.columns) && tbl.columns.length) || tables[0]; tableName = tableWithColumns && tableWithColumns.name ? tableWithColumns.name : fallback.table; const firstColumn = tableWithColumns && Array.isArray(tableWithColumns.columns) && tableWithColumns.columns[0]; columnName = firstColumn && firstColumn.name ? firstColumn.name : fallback.column; } const payload = { type: 'database', table: tableName, column: columnName }; const content = buildPlaceholderHTML(payload); const blockId = 'cust-placeholder-db'; const label = '🗄️ Placeholder (DB)'; const existing = typeof bm.get === 'function' ? bm.get(blockId) : null; if (existing && typeof existing.set === 'function') { existing.set('content', content); existing.set('label', label); } else { addOnce(bm, blockId, { id: blockId, label, content, }); } }; ensureDbBlock(); fetchPlaceholderSchema() .then((tables) => { ensureDbBlock(tables); }) .catch(() => { log('PLACEHOLDER WARN', 'DB Placeholder Schema konnte nicht geladen werden – Fallback bleibt aktiv.', '#b45309'); }); log('SUCCESS', `Placeholder-Registrierung abgeschlossen. ${ALL_PLACEHOLDER_BLOCK_IDS.length} Blöcke erstellt.`, '#008000', 'info', true); } window.BridgeBlocksPlaceholder = { IDS: ALL_PLACEHOLDER_BLOCK_IDS, register, openModal: openPlaceholderModal }; if (B && B.registerGrapesJSPlugin && typeof register === 'function') { B.registerGrapesJSPlugin('bridge-blocks-placeholder', register); log('PLUGIN REGISTER', `'bridge-blocks-placeholder' erfolgreich registriert.`, '#008000'); } else { log('CRITICAL ERROR', `BridgeParts oder registerGrapesJSPlugin fehlt! Placeholder Plugin-Registrierung gescheitert.`, 'red', 'error'); } })();