Files
emailtemplate.it/public/assets/js/bridge/blocks-placeholder.js
2026-01-16 02:49:13 +01:00

1127 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* /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 buildPlaceholderText = (payload) => {
const label = buildPlaceholderLabel(payload);
return `{{${label}}}`;
};
const escapeAttr = (value) => {
return String(value || '')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
};
const buildPlaceholderHTML = (payload) => {
const type = payload && payload.type === 'database' ? 'database' : 'custom';
const key = payload && payload.key ? payload.key : 'UEBERSCHRIFT';
const table = payload && payload.table ? payload.table : 'tabelle';
const column = payload && payload.column ? payload.column : 'feld';
const label = buildPlaceholderText(payload || {});
const attrs = [
`data-gjs-type="${PLACEHOLDER_COMPONENT}"`,
`data-placeholder-type="${escapeAttr(type)}"`,
`contenteditable="false"`,
`class="${INLINE_PLACEHOLDER_CLASS}"`
];
if (type === 'database') {
attrs.push(`data-placeholder-table="${escapeAttr(table)}"`);
attrs.push(`data-placeholder-column="${escapeAttr(column)}"`);
} else {
attrs.push(`data-placeholder-key="${escapeAttr(key)}"`);
}
return `<span ${attrs.join(' ')}>${label}</span>`;
};
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 = () => {};
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 insertTextIntoSelection = (rteInstance, text) => {
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 textNode = doc.createTextNode(text);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return true;
};
const getRteDocument = (rteInstance) => {
return rteInstance && rteInstance.doc
? rteInstance.doc
: (rteInstance && rteInstance.el && rteInstance.el.ownerDocument) || document;
};
const insertCaretMarker = (rteInstance) => {
const doc = getRteDocument(rteInstance);
if (!doc || !doc.getSelection) return null;
const sel = doc.getSelection();
if (!sel || !sel.rangeCount) return null;
const range = sel.getRangeAt(0);
if (!range || !range.collapsed) return null;
const markerId = `bridge-placeholder-marker-${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
const marker = doc.createElement('span');
marker.setAttribute(PLACEHOLDER_MARKER_ATTR, markerId);
marker.style.display = 'inline-block';
marker.style.width = '0';
marker.style.height = '0';
marker.style.padding = '0';
marker.style.margin = '0';
marker.style.lineHeight = '0';
marker.style.overflow = 'hidden';
range.insertNode(marker);
range.setStartAfter(marker);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return { doc, id: markerId };
};
const removeMarkerFromComponent = (markerInfo, rteInstance) => {
if (!markerInfo || !markerInfo.id) return false;
const doc = markerInfo.doc || getRteDocument(rteInstance);
if (!doc || !doc.querySelector) return false;
const marker = doc.querySelector(`[${PLACEHOLDER_MARKER_ATTR}="${markerInfo.id}"]`);
if (!marker || !marker.parentNode) return false;
marker.parentNode.removeChild(marker);
return true;
};
const replaceMarkerWithPlaceholder = (markerInfo, rteInstance, text) => {
if (!markerInfo || !markerInfo.id) return false;
const doc = markerInfo.doc || getRteDocument(rteInstance);
if (!doc || !doc.querySelector) return false;
const marker = doc.querySelector(`[${PLACEHOLDER_MARKER_ATTR}="${markerInfo.id}"]`);
if (!marker || !marker.parentNode) return false;
const textNode = doc.createTextNode(text);
marker.parentNode.replaceChild(textNode, marker);
const sel = doc.getSelection && doc.getSelection();
if (sel && doc.createRange) {
const range = doc.createRange();
range.setStartAfter(textNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
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: '<span style="font-weight:bold;font-family:monospace;">{}</span>',
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 markerInfo = insertCaretMarker(rteInstance);
const restoreSelection = () => restoreRteSelection(selectionSnapshot);
openPlaceholderModal(editor, null, {
onCancel: () => {
if (markerInfo) {
removeMarkerFromComponent(markerInfo, rteInstance);
}
if (rteInstance && typeof rteInstance.focus === 'function') {
setTimeout(() => rteInstance.focus(), 0);
}
},
onSubmit: (payload) => {
const text = buildPlaceholderText(payload);
if (markerInfo && replaceMarkerWithPlaceholder(markerInfo, rteInstance, text)) {
if (rteInstance && typeof rteInstance.focus === 'function') {
setTimeout(() => rteInstance.focus(), 0);
}
return true;
}
if (markerInfo) {
removeMarkerFromComponent(markerInfo, rteInstance);
}
if (!restoreSelection()) {
log('PLACEHOLDER ERROR', 'Cursor-Position konnte nicht wiederhergestellt werden.', 'red', 'error');
return false;
}
if (!insertTextIntoSelection(rteInstance, text)) {
log('PLACEHOLDER ERROR', 'Placeholder konnte nicht eingefügt werden.', 'red', 'error');
return false;
}
if (rteInstance && typeof rteInstance.focus === 'function') {
setTimeout(() => rteInstance.focus(), 0);
}
return true;
}
});
},
});
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;
if (!placeholderSchemaStore.promise && placeholderSchemaStore.status !== false) {
fetchPlaceholderSchema().catch(() => {});
}
const existingCmd = editor.Commands.get('bridge-placeholder:edit');
if (!existingCmd || existingCmd.__bridgePlaceholderStub) {
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;
}
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: `<span data-gjs-type="${PLACEHOLDER_COMPONENT}" data-placeholder-type="custom" data-placeholder-key="UEBERSCHRIFT">{{UEBERSCHRIFT}}</span>`
});
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');
}
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');
}
})();