diff --git a/public/assets/js/bridge/blocks-placeholder.js b/public/assets/js/bridge/blocks-placeholder.js index e195e9b..25787a5 100644 --- a/public/assets/js/bridge/blocks-placeholder.js +++ b/public/assets/js/bridge/blocks-placeholder.js @@ -31,6 +31,287 @@ 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 }); @@ -173,6 +454,12 @@ 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() { @@ -308,6 +595,39 @@ 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)', @@ -340,7 +660,8 @@ window.BridgeBlocksPlaceholder = { IDS: ALL_PLACEHOLDER_BLOCK_IDS, - register + register, + openModal: openPlaceholderModal }; if (B && B.registerGrapesJSPlugin && typeof register === 'function') {