/* /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 placeholderSchemaStore = { promise: null, tables: [], status: null, statusPromise: null, }; 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 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 openPlaceholderModal = (editor, component) => { if (!editor || !component || !component.is || !component.is(PLACEHOLDER_COMPONENT)) return; const modal = editor.Modal; if (!modal) return; const attrs = component.getAttributes ? component.getAttributes() : {}; 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([]); }); }; tableSelect.addEventListener('change', () => { populateColumns(tableSelect.value); }); typeSelect.addEventListener('change', () => { toggleSections(); if (typeSelect.value === 'database' && (!tablesCache || !tablesCache.length)) { loadTables(); } }); cancelBtn.addEventListener('click', (e) => { e.preventDefault(); modal.close(); }); 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; } component.addAttributes({ 'data-placeholder-type': 'custom', 'data-placeholder-key': key, 'data-placeholder-table': '', 'data-placeholder-column': '', }); } 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; } component.addAttributes({ 'data-placeholder-type': 'database', 'data-placeholder-key': '', 'data-placeholder-table': table, 'data-placeholder-column': column, }); } modal.close(); }); toggleSections(); if (typeSelect.value === 'database') { loadTables(); } modal.setTitle('Placeholder bearbeiten'); modal.setContent(container); modal.open(); if (editor.getSelected() !== component) { editor.select && editor.select(component); } }; 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 ensurePlaceholderComponent = (editor) => { const domc = editor.DomComponents; if (domc.getType(PLACEHOLDER_COMPONENT)) return; const baseType = domc.getType('text') || domc.getType('default') || {}; const BaseModel = baseType.model || editor.DomComponents.Component; const BaseView = baseType.view || editor.DomComponents.View; const placeholderDefaults = {}; if (BaseModel.prototype && BaseModel.prototype.defaults) { for (const key in BaseModel.prototype.defaults) { placeholderDefaults[key] = BaseModel.prototype.defaults[key]; } } placeholderDefaults.name = 'Placeholder'; placeholderDefaults.tagName = 'span'; placeholderDefaults.droppable = false; placeholderDefaults.attributes = { 'data-placeholder-type': 'custom', 'data-placeholder-key': 'UEBERSCHRIFT', }; placeholderDefaults.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, }, ]; const baseToolbar = Array.isArray(placeholderDefaults.toolbar) ? placeholderDefaults.toolbar.slice() : []; baseToolbar.push({ attributes: { class: 'fa fa-edit', title: 'Placeholder bearbeiten' }, command: 'bridge-placeholder:edit', }); placeholderDefaults.toolbar = baseToolbar; const PlaceholderModel = BaseModel.extend({ init() { this.listenTo(this, 'change:attributes', this.updatePlaceholderState); this.updatePlaceholderState(); fetchPlaceholderSchema() .then(() => this.updateSchemaTraits()) .catch(() => this.updateSchemaTraits([])); }, updatePlaceholderState() { const attrs = this.getAttributes(); const type = attrs['data-placeholder-type'] || 'custom'; if (type === 'database' && placeholderSchemaStore.tables.length === 0) { this.addAttributes({ 'data-placeholder-type': 'custom' }); } 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 colOpts = table ? table.columns.map(function (col) { return { id: col.name, label: col.name + ' (' + 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 }]); } } }); PlaceholderModel.prototype.defaults = placeholderDefaults; PlaceholderModel.isComponent = function (el) { if (el && el.hasAttribute && el.hasAttribute('data-placeholder-type')) { return { type: PLACEHOLDER_COMPONENT }; } return false; }; const PlaceholderView = BaseView.extend({ render() { BaseView.prototype.render.apply(this, arguments); this.el.classList.add('placeholder-block'); this.el.style.display = 'inline-block'; 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, }); }; 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); 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 (window.__GJS_IS_PARSING) return; if (cmp.__bridgePlaceholderPrompted) return; cmp.__bridgePlaceholderPrompted = true; setTimeout(() => openPlaceholderModal(editor, cmp), 50); }); } addOnce(bm, 'cust-placeholder-custom', { id: 'cust-placeholder-custom', label: '🔖 Placeholder (Text)', content: `{{UEBERSCHRIFT}}` }); fetchPlaceholderSchema() .then(tables => { if (!tables || !tables.length) { log('PLACEHOLDER INFO', 'Keine Tabellen – DB Placeholder Block wird nicht angezeigt.', '#888'); return; } const firstTable = tables[0] || {}; const tableName = firstTable.name || 'tabelle'; const columns = Array.isArray(firstTable.columns) ? firstTable.columns : []; const firstColumn = columns.length ? columns[0].name : 'feld'; const placeholderLabel = (tableName + '.' + firstColumn).toUpperCase(); addOnce(bm, 'cust-placeholder-db', { id: 'cust-placeholder-db', label: '🗄️ Placeholder (DB)', content: `{{${placeholderLabel}}}` }); }) .catch(() => { log('PLACEHOLDER WARN', 'DB Placeholder Block ausgeblendet (Schemafehler).', '#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'); } })();