diff --git a/public/assets/js/bridge/blocks-placeholder.js b/public/assets/js/bridge/blocks-placeholder.js
index e50809b..181f970 100644
--- a/public/assets/js/bridge/blocks-placeholder.js
+++ b/public/assets/js/bridge/blocks-placeholder.js
@@ -20,16 +20,44 @@
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 TARGET_CAT_ID = 'placeholders';
+const PLACEHOLDER_COMPONENT = 'placeholder-block';
+const ALL_PLACEHOLDER_BLOCK_IDS = [];
+const INLINE_PLACEHOLDER_CLASS = 'bridge-placeholder-inline';
- const placeholderSchemaStore = {
- promise: null,
- tables: [],
- status: null,
- statusPromise: null,
- };
+const placeholderSchemaStore = {
+ promise: null,
+ tables: [],
+ status: null,
+ statusPromise: null,
+};
+let inlineStyleInjected = false;
+
+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);
@@ -53,7 +81,7 @@
return el;
};
- const applyInputStyles = (el) => {
+const applyInputStyles = (el) => {
if (!el) return el;
el.style.width = '100%';
el.style.boxSizing = 'border-box';
@@ -65,7 +93,7 @@
return el;
};
- const syncSelectOptions = (selectEl, options, selected) => {
+const syncSelectOptions = (selectEl, options, selected) => {
if (!selectEl) return;
while (selectEl.firstChild) {
selectEl.removeChild(selectEl.firstChild);
@@ -80,11 +108,20 @@
if (selected !== undefined && selected !== null) {
selectEl.value = selected;
}
- };
+};
- const refreshPlaceholderComponent = (component) => {
- if (!component) return;
- if (typeof component.updatePlaceholderState === 'function') {
+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) {
@@ -111,12 +148,14 @@
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}"`,
- ];
+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 || ''}"`);
@@ -181,16 +220,58 @@
return true;
};
+ 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 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';
@@ -348,6 +429,7 @@
cancelBtn.addEventListener('click', (e) => {
e.preventDefault();
+ fireCancel('cancel-button');
modal.close();
});
@@ -377,6 +459,7 @@
});
}
refreshPlaceholderComponent(component);
+ component.__bridgePlaceholderNew = false;
return true;
};
@@ -422,6 +505,7 @@
return;
}
}
+ didSave = true;
modal.close();
});
@@ -432,6 +516,20 @@
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);
@@ -452,8 +550,20 @@
log('PLACEHOLDER INFO', 'Bitte zuerst ein Text-Element auswählen.', '#888');
return;
}
+ const selectionSnapshot = captureRteSelection(rteInstance);
openPlaceholderModal(editor, null, {
+ onCancel: () => {
+ if (selectionSnapshot) {
+ restoreRteSelection(selectionSnapshot);
+ }
+ if (rteInstance && typeof rteInstance.focus === 'function') {
+ setTimeout(() => rteInstance.focus(), 0);
+ }
+ },
onSubmit: (payload) => {
+ if (selectionSnapshot) {
+ restoreRteSelection(selectionSnapshot);
+ }
const html = buildPlaceholderHTML(payload);
let inserted = false;
if (insertPlaceholderIntoSelection(rteInstance, html)) {
@@ -589,6 +699,8 @@
attributes: {
'data-placeholder-type': 'custom',
'data-placeholder-key': 'UEBERSCHRIFT',
+ 'contenteditable': 'false',
+ 'class': INLINE_PLACEHOLDER_CLASS,
},
traits: [
{
@@ -645,6 +757,7 @@
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;
@@ -662,10 +775,12 @@
updatePlaceholderState() {
const attrs = this.getAttributes();
- const type = attrs['data-placeholder-type'] || 'custom';
- if (type === 'database' && placeholderSchemaStore.tables.length === 0) {
+ 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();
@@ -711,9 +826,12 @@
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 + ')' }; })
+ 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);
}
@@ -745,7 +863,11 @@
render() {
BaseView.prototype.render.apply(this, arguments);
this.el.classList.add('placeholder-block');
- this.el.style.display = 'inline-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';
@@ -848,7 +970,14 @@
if (window.__GJS_IS_PARSING) return;
if (cmp.__bridgePlaceholderPrompted) return;
cmp.__bridgePlaceholderPrompted = true;
- setTimeout(() => openPlaceholderModal(editor, cmp), 50);
+ cmp.__bridgePlaceholderNew = true;
+ setTimeout(() => openPlaceholderModal(editor, cmp, {
+ onCancel: () => {
+ if (cmp.__bridgePlaceholderNew) {
+ safeRemoveComponent(cmp);
+ }
+ }
+ }), 50);
});
}
@@ -865,25 +994,40 @@
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}}}`
+ 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 Block ausgeblendet (Schemafehler).', '#b45309');
+ 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);