1049 lines
42 KiB
JavaScript
1049 lines
42 KiB
JavaScript
/* /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 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);
|
||
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 `<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 reparseTextComponent = (component, editor) => {
|
||
if (!component || typeof component.components !== 'function') return;
|
||
const viewEl = component.view && component.view.el;
|
||
if (!viewEl) return;
|
||
const html = viewEl.innerHTML;
|
||
try {
|
||
component.components(html);
|
||
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 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 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';
|
||
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();
|
||
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: '<i class="fa fa-bookmark"></i>',
|
||
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);
|
||
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)) {
|
||
inserted = true;
|
||
} else if (rteInstance && typeof rteInstance.insertHTML === 'function') {
|
||
rteInstance.insertHTML(html);
|
||
inserted = true;
|
||
} else if (typeof document !== 'undefined' && document.execCommand) {
|
||
document.execCommand('insertHTML', false, html);
|
||
inserted = true;
|
||
} else {
|
||
log('PLACEHOLDER ERROR', 'Placeholder konnte nicht eingefügt werden (kein RTE).', 'red', 'error');
|
||
return false;
|
||
}
|
||
if (rteInstance && typeof rteInstance.focus === 'function') {
|
||
rteInstance.focus();
|
||
}
|
||
setTimeout(() => reparseTextComponent(target, editor), 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;
|
||
ensurePlaceholderComponent(editor);
|
||
ensureTextSupportsPlaceholders(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;
|
||
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: `<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', 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');
|
||
}
|
||
})();
|