352 lines
18 KiB
JavaScript
352 lines
18 KiB
JavaScript
/* /assets/js/bridge/blocks-custom.js (FINAL & LOG-KONTROLLIERT) */
|
||
(function () {
|
||
|
||
const PluginName = 'blocks-custom';
|
||
const B = window.BridgeParts || (window.BridgeParts = {});
|
||
|
||
// ----------------------------------------------------------------------
|
||
// 🎯 NEU: LOKALE LOG-KONFIGURATION UND WRAPPER
|
||
// ----------------------------------------------------------------------
|
||
// Setzen Sie dies auf 'false' in der config.js oder hier, um alle Logs NUR für dieses Plugin zu deaktivieren.
|
||
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
|
||
B.LOG_CONFIG.PLUGINS[PluginName] = false; // <-- HIER IST IHR SCHALTER
|
||
}
|
||
|
||
// NEUER LOKALER WRAPPER, der die zentrale B.log Funktion verwendet:
|
||
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') {
|
||
// Fallback für kritische Fehler, wenn B.log fehlt
|
||
console.error(`%c[${PluginName} - ${type}] %c${message}`, `color:red; font-weight:bold;`, 'color:inherit;');
|
||
}
|
||
};
|
||
// ----------------------------------------------------------------------
|
||
|
||
log('FILE CHECK', 'Datei-IIFE startet.'); // NEU: Kontrollierbarer Start-Log
|
||
|
||
if (window.__CUSTOM_BLOCKS_LOADED) return;
|
||
window.__CUSTOM_BLOCKS_LOADED = true;
|
||
|
||
const TARGET_CAT_ID = 'bausteine';
|
||
const ALL_CUSTOM_BLOCK_IDS = [];
|
||
|
||
function addOnce(bm, id, def) {
|
||
// Hinzufügen des Blocks und Sicherstellen der Kategorie-Zuweisung
|
||
try {
|
||
bm.add(id, { ...def, category: TARGET_CAT_ID });
|
||
ALL_CUSTOM_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 css = o => Object.entries(o).map(([k,v]) => `${k}:${v}`).join(';');
|
||
|
||
const PLACEHOLDER_COMPONENT = 'placeholder-block';
|
||
const placeholderSchemaStore = {
|
||
promise: null,
|
||
tables: [],
|
||
};
|
||
|
||
const fetchPlaceholderSchema = () => {
|
||
if (placeholderSchemaStore.promise) return placeholderSchemaStore.promise;
|
||
const base = B.API_KERNEL_URL || '/api.php';
|
||
const sep = base.includes('?') ? '&' : '?';
|
||
const url = `${base}${sep}action=placeholders.schema`;
|
||
placeholderSchemaStore.promise = fetch(url, { credentials: 'include' })
|
||
.then(res => {
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
return res.json();
|
||
})
|
||
.then(data => {
|
||
placeholderSchemaStore.tables = Array.isArray(data?.tables) ? data.tables : [];
|
||
return placeholderSchemaStore.tables;
|
||
})
|
||
.catch(err => {
|
||
placeholderSchemaStore.tables = [];
|
||
placeholderSchemaStore.promise = null;
|
||
log('PLACEHOLDER ERROR', `Schema konnte nicht geladen werden: ${err.message || err}`, 'red', 'error');
|
||
throw err;
|
||
});
|
||
return placeholderSchemaStore.promise;
|
||
};
|
||
|
||
const getTraitByName = (model, name) => {
|
||
if (typeof model.getTrait === 'function') return model.getTrait(name);
|
||
const traits = model.get('traits');
|
||
if (!traits) return null;
|
||
if (typeof traits.where === 'function') {
|
||
const found = traits.where({ name });
|
||
return found && found[0];
|
||
}
|
||
if (Array.isArray(traits.models)) {
|
||
return traits.models.find(t => t.get && t.get('name') === name) || null;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const ensurePlaceholderComponent = (editor) => {
|
||
const domc = editor.DomComponents;
|
||
if (domc.getType(PLACEHOLDER_COMPONENT)) return;
|
||
|
||
const baseType = domc.getType('text') ? 'text' : 'default';
|
||
domc.addType(PLACEHOLDER_COMPONENT, {
|
||
extend: baseType,
|
||
isComponent(el) {
|
||
if (el && el.hasAttribute && el.hasAttribute('data-placeholder-type')) {
|
||
return { type: PLACEHOLDER_COMPONENT };
|
||
}
|
||
return false;
|
||
},
|
||
model: {
|
||
defaults: {
|
||
name: 'Placeholder',
|
||
tagName: 'span',
|
||
droppable: false,
|
||
attributes: {
|
||
'data-placeholder-type': 'custom',
|
||
'data-placeholder-key': 'UEBERSCHRIFT',
|
||
},
|
||
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,
|
||
},
|
||
],
|
||
},
|
||
|
||
init() {
|
||
this.listenTo(this, 'change:attributes', this.updatePlaceholderState);
|
||
this.updatePlaceholderState();
|
||
fetchPlaceholderSchema()
|
||
.then(() => this.updateSchemaTraits())
|
||
.catch(() => this.updateSchemaTraits([]));
|
||
},
|
||
|
||
updatePlaceholderState() {
|
||
const attrs = this.getAttributes();
|
||
const type = attrs['data-placeholder-type'] || 'custom';
|
||
if (type === 'database' && placeholderSchemaStore.tables.length === 0) {
|
||
this.addAttributes({ 'data-placeholder-type': 'custom' });
|
||
}
|
||
this.updateTraitVisibility();
|
||
this.updateSchemaTraits();
|
||
this.updateLabel();
|
||
},
|
||
|
||
updateTraitVisibility() {
|
||
const attrs = this.getAttributes();
|
||
const type = attrs['data-placeholder-type'] || 'custom';
|
||
const isDb = type === 'database';
|
||
const tableTrait = getTraitByName(this, 'data-placeholder-table');
|
||
const columnTrait = getTraitByName(this, 'data-placeholder-column');
|
||
const keyTrait = getTraitByName(this, 'data-placeholder-key');
|
||
|
||
if (tableTrait?.view?.el) {
|
||
tableTrait.view.el.style.display = isDb ? '' : 'none';
|
||
}
|
||
if (columnTrait?.view?.el) {
|
||
columnTrait.view.el.style.display = isDb ? '' : 'none';
|
||
}
|
||
if (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(tbl => ({ 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(tbl => tbl.name.toLowerCase() === tableName);
|
||
const colOpts = table
|
||
? table.columns.map(col => ({ id: col.name, label: `${col.name} (${col.type})` }))
|
||
: [{ id: '', label: table ? 'Keine Felder' : 'Feld wählen', disabled: !table }];
|
||
setTraitOptions(columnTrait, colOpts);
|
||
}
|
||
},
|
||
|
||
updateLabel() {
|
||
const attrs = this.getAttributes();
|
||
const type = attrs['data-placeholder-type'] || 'custom';
|
||
let label;
|
||
if (type === 'database') {
|
||
const table = attrs['data-placeholder-table'] || 'TABELLE';
|
||
const column = attrs['data-placeholder-column'] || 'FELD';
|
||
label = `${table}.${column}`.toUpperCase();
|
||
} else {
|
||
label = (attrs['data-placeholder-key'] || 'PLATZHALTER').toUpperCase();
|
||
}
|
||
const text = `{{${label}}}`;
|
||
const comps = this.components();
|
||
if (comps.length === 1 && comps.at(0).is('textnode')) {
|
||
comps.at(0).set('content', text);
|
||
} else {
|
||
comps.reset([{ type: 'textnode', content: text }]);
|
||
}
|
||
},
|
||
},
|
||
view: editor.DomComponents.View.extend({
|
||
render() {
|
||
editor.DomComponents.View.prototype.render.apply(this, arguments);
|
||
this.el.classList.add('placeholder-block');
|
||
this.el.style.display = 'inline-block';
|
||
this.el.style.padding = '2px 8px';
|
||
this.el.style.border = '1px dashed #94a3b8';
|
||
this.el.style.borderRadius = '6px';
|
||
this.el.style.background = '#f1f5f9';
|
||
this.el.style.fontFamily = 'monospace';
|
||
this.el.style.fontSize = '12px';
|
||
return this;
|
||
},
|
||
}),
|
||
});
|
||
};
|
||
|
||
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 Block-Registrierung für ${TARGET_CAT_ID}.`, '#DAA520');
|
||
|
||
const bm = editor.BlockManager;
|
||
ensurePlaceholderComponent(editor);
|
||
|
||
// --- Custom-Blöcke DEFINIEREN ---
|
||
|
||
// PLACEHOLDER
|
||
addOnce(bm, 'cust-placeholder', {
|
||
id: 'cust-placeholder',
|
||
label: '🔖 Placeholder',
|
||
content: `<span data-gjs-type="${PLACEHOLDER_COMPONENT}" data-placeholder-type="custom" data-placeholder-key="UEBERSCHRIFT">{{UEBERSCHRIFT}}</span>`
|
||
});
|
||
|
||
// TEXT
|
||
addOnce(bm, 'cust-text', { id:'cust-text', label:'📝 Text',
|
||
content:`<div style="${css({'font-family':'Arial,sans-serif','font-size':'14px','line-height':'1.5',color:'#0f172a',margin:'0 0 12px'})}">
|
||
<p style="${css({margin:'0 0 12px'})}">Dies ist ein Absatz. Doppelklick zum Bearbeiten.</p></div>` });
|
||
|
||
// IMAGE
|
||
addOnce(bm, 'cust-image', { id:'cust-image', label:'🖼️ Bild',
|
||
content:`<div style="${css({'text-align':'center',margin:'0 0 16px'})}">
|
||
<img src="https://placehold.co/600x300" alt="Bild" style="${css({width:'100%',height:'auto','max-width':'600px',border:'0',display:'inline-block'})}"></div>` });
|
||
|
||
// BUTTON
|
||
addOnce(bm, 'cust-button', { id:'cust-button', label:'🔘 Button',
|
||
content:`<div style="${css({'text-align':'center',margin:'0 0 16px'})}">
|
||
<a href="#" style="${css({display:'inline-block','background-color':'#0ea5e9',color:'#fff','text-decoration':'none',padding:'10px 18px','border-radius':'6px','font-family':'Arial,sans-serif','font-size':'14px'})}">Call To Action</a></div>` });
|
||
|
||
// DIVIDER
|
||
addOnce(bm, 'cust-divider',{ id:'cust-divider',label:'⎯ Divider',
|
||
content:`<hr style="${css({border:'0',height:'1px','background-color':'#e2e8f0',margin:'16px 0'})}">` });
|
||
|
||
// SPACER
|
||
addOnce(bm, 'cust-spacer', { id:'cust-spacer', label:'↕ Spacer',
|
||
content:`<div style="${css({height:'24px'})}"></div>` });
|
||
|
||
// 2 COL
|
||
addOnce(bm, 'cust-2col', { id:'cust-2col', label:'🧩 2 Spalten',
|
||
content:`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${css({'font-family':'Arial,sans-serif','border-collapse':'collapse','margin-bottom':'16px'})}">
|
||
<tr><td width="50%" valign="top" style="${css({padding:'0 8px 0 0'})}">
|
||
<div style="${css({'font-size':'14px','line-height':'1.5',color:'#0f172a'})}"><p style="${css({margin:'0 0 12px'})}">Linke Spalte – Inhalt hier.</p></div>
|
||
</td><td width="50%" valign="top" style="${css({padding:'0 0 0 8px'})}">
|
||
<div style="${css({'font-size':'14px','line-height':'1.5',color:'#0f172a'})}"><p style="${css({margin:'0 0 12px'})}">Rechte Spalte – Inhalt hier.</p></div>
|
||
</td></tr></table>` });
|
||
|
||
// MEDIA LEFT
|
||
addOnce(bm, 'cust-media-left', { id:'cust-media-left', label:'🖼️◀ Text',
|
||
content:`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${css({'font-family':'Arial,sans-serif','border-collapse':'collapse','margin-bottom':'16px'})}">
|
||
<tr><td width="40%" valign="top" style="${css({padding:'0 8px 0 0'})}">
|
||
<img src="https://placehold.co/400x260" alt="Bild" style="${css({width:'100%',height:'auto',border:'0',display:'block'})}">
|
||
</td><td width="60%" valign="top" style="${css({padding:'0 0 0 8px'})}">
|
||
<h3 style="${css({margin:'0 0 8px','font-size':'18px',color:'#0f172a'})}">Überschrift</h3>
|
||
<p style="${css({margin:'0 0 12px','font-size':'14px',color:'#0f172a','line-height':'1.5'})}">Beschreibungstext …</p>
|
||
<a href="#" style="${css({display:'inline-block','background-color':'#0ea5e9',color:'#fff','text-decoration':'none',padding:'8px 14px','border-radius':'6px','font-size':'14px'})}">Mehr erfahren</a>
|
||
</td></tr></table>` });
|
||
|
||
// HERO
|
||
addOnce(bm, 'cust-hero', { id:'cust-hero', label:'🌄 Hero',
|
||
content:`<div style="${css({'text-align':'center',margin:'0 0 16px',padding:'12px','background-color':'#eef2ff',color:'#1e3a8a','border':'1px solid #c7d2fe','border-radius':'8px'})}">
|
||
<img src="https://placehold.co/640x240" alt="Hero" style="${css({width:'100%',height:'auto','max-width':'640px',border:'0',display:'inline-block','border-radius':'6px'})}">
|
||
<h2 style="${css({'font-family':'Arial,sans-serif',margin:'12px 0 8px','font-size':'22px'})}">Titel des Newsletters</h2>
|
||
<p style="${css({'font-size':'14px',margin:'0 0 12px'})}">Kurzer Untertitel oder Einleitung.</p>
|
||
</div>` });
|
||
|
||
// FOOTER
|
||
addOnce(bm, 'cust-footer', { id:'cust-footer', label:'⚓ Footer',
|
||
content:`<div style="${css({'font-family':'Arial,sans-serif','font-size':'12px',color:'#475569','line-height':'1.5','border-top':'1px solid #e2e8f0',padding:'12px 0','text-align':'center'})}">
|
||
<p style="${css({margin:'0 0 6px'})}"><strong>Dein Unternehmen GmbH</strong> • Musterstraße 1 • 12345 Berlin</p>
|
||
<p style="${css({margin:'0'})}"><a href="#" style="${css({color:'#0ea5e9','text-decoration':'none'})}">Abmelden</a> ·
|
||
<a href="#" style="${css({color:'#0ea5e9','text-decoration':'none'})}">Impressum</a> ·
|
||
<a href="#" style="${css({color:'#0ea5e9','text-decoration':'none'})}">Datenschutz</a></p>
|
||
</div>` });
|
||
|
||
log('SUCCESS', `Registrierung abgeschlossen. ${ALL_CUSTOM_BLOCK_IDS.length} Blöcke erstellt.`, '#008000', 'info', true);
|
||
}
|
||
|
||
// 🛑 KRITISCHE EXPORT-KORREKTUR: Exportiere 'register', um den Fehler in bridge-core.js zu beheben
|
||
window.BridgeBlocksCustom = {
|
||
IDS: ALL_CUSTOM_BLOCK_IDS,
|
||
register: register // <--- NEU: Exportiert die Register-Funktion
|
||
};
|
||
|
||
// Registriere das Modul als GrapesJS Plugin
|
||
if (B && B.registerGrapesJSPlugin && typeof register === 'function') {
|
||
B.registerGrapesJSPlugin('bridge-blocks-custom', register);
|
||
log('PLUGIN REGISTER', `'bridge-blocks-custom' erfolgreich zur Bridge Plugin Registry hinzugefügt.`, '#008000');
|
||
} else {
|
||
log('CRITICAL ERROR', `BridgeParts oder registerGrapesJSPlugin fehlt! Plugin-Registrierung gescheitert.`, 'red', 'error');
|
||
}
|
||
|
||
})();
|