commit
This commit is contained in:
525
public/api (Kopie).php
Normal file
525
public/api (Kopie).php
Normal file
@@ -0,0 +1,525 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// =================================================================
|
||||
// 🚨 KRITISCHE STARTSEQUENZ 🚨
|
||||
// Diese Blöcke MÜSSEN VOR jeder Ausgabe erfolgen.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// 1. Composer Autoload (Sie haben dies selbst eingebunden)
|
||||
// ACHTUNG: Der Pfad wurde auf das Standard-Vendor-Verzeichnis korrigiert: 'vendor'
|
||||
$composerAutoload = __DIR__ . '/../vendor/autoload.php';
|
||||
if (is_file($composerAutoload)) {
|
||||
require_once $composerAutoload;
|
||||
}
|
||||
|
||||
// 2. Session Start (Muss VOR dem Senden des Session-Cookies/Headers erfolgen)
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
/* ===================== Utilities ===================== */
|
||||
function respond($data, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
echo is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
function fail(string $msg, $detail = null, int $code = 400): void {
|
||||
respond(['ok'=>false,'error'=>$msg,'detail'=>$detail], $code);
|
||||
}
|
||||
|
||||
function load_config(): array {
|
||||
$paths = [
|
||||
__DIR__ . '/../inc/config.php',
|
||||
__DIR__ . '/inc/config.php',
|
||||
__DIR__ . '/config.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $p) {
|
||||
if (is_file($p)) {
|
||||
$conf = @include $p;
|
||||
if (is_array($conf)) return $conf;
|
||||
}
|
||||
}
|
||||
fail('Invalid config.php', 'config.php not found or not returning array', 500);
|
||||
}
|
||||
|
||||
function cors(array $conf): void {
|
||||
$cors = $conf['cors'] ?? '*';
|
||||
if ($cors) {
|
||||
header('Access-Control-Allow-Origin: ' . $cors);
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') respond(['ok'=>true]);
|
||||
}
|
||||
|
||||
function get_input(): array {
|
||||
$data = [];
|
||||
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if (stripos($ct, 'application/json') !== false) {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw !== false && $raw !== '') {
|
||||
$js = json_decode($raw, true);
|
||||
if (is_array($js)) $data = $js;
|
||||
}
|
||||
}
|
||||
foreach ($_POST as $k=>$v) $data[$k]=$v;
|
||||
foreach ($_GET as $k=>$v) if (!array_key_exists($k,$data)) $data[$k]=$v;
|
||||
return $data;
|
||||
}
|
||||
|
||||
function val(array $in, $keys, $default=null) {
|
||||
if (!is_array($keys)) $keys = [$keys];
|
||||
foreach ($keys as $k) if (array_key_exists($k,$in)) return $in[$k];
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline-Styling für E-Mail-Templates (nutzt TijsVerkoyen/CssToInlineStyles)
|
||||
*
|
||||
* Diese Funktion steht nur bereit, wird aber noch nicht verwendet.
|
||||
* @param string $html
|
||||
* @param string|null $css
|
||||
* @return string
|
||||
*/
|
||||
function inline_css(string $html, ?string $css = null): string {
|
||||
// 1. Klasse existiert nicht
|
||||
if (!class_exists('\TijsVerkoyen\CssToInlineStyles\CssToInlineStyles')) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// 2. Direkte Instanziierung und Aufruf in einer einzigen Zeile
|
||||
$result = (new \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles($html, $css))->convert();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/* ===================== DB helpers ===================== */
|
||||
function pdo_templates(array $conf): PDO {
|
||||
if (!isset($conf['templates']) || !is_array($conf['templates'])) {
|
||||
fail('Missing templates DB config', null, 500);
|
||||
}
|
||||
$c = $conf['templates'];
|
||||
$host = $c['db_host'] ?? 'localhost';
|
||||
$db = $c['db_name'] ?? ($c['database'] ?? '');
|
||||
$user = $c['db_user'] ?? ($c['username'] ?? '');
|
||||
$pass = $c['db_pass'] ?? ($c['password'] ?? '');
|
||||
$charset = $c['db_charset'] ?? 'utf8mb4';
|
||||
$port = $c['db_port'] ?? 3306;
|
||||
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
];
|
||||
return new PDO($dsn, $user, $pass, $opt);
|
||||
}
|
||||
|
||||
function verify_password(string $input, string $stored, array $authDbConf): bool {
|
||||
if (preg_match('~^\$2[aby]\$~', $stored) || strpos($stored, '$argon2') === 0) return password_verify($input, $stored);
|
||||
$legacy = strtolower($authDbConf['legacy'] ?? '');
|
||||
if ($legacy === 'md5') return hash_equals($stored, md5($input));
|
||||
if ($legacy === 'sha1') return hash_equals($stored, sha1($input));
|
||||
if ($legacy === 'plain')return hash_equals($stored, $input);
|
||||
if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored);
|
||||
return hash_equals($stored, $input);
|
||||
}
|
||||
|
||||
/* ============ schema helpers ============ */
|
||||
function table_columns(PDO $pdo, string $table): array {
|
||||
$cols = [];
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `$table`");
|
||||
foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field'];
|
||||
return $cols;
|
||||
}
|
||||
|
||||
function primary_key(PDO $pdo, string $table): ?string {
|
||||
$stmt = $pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
return $row['Column_name'] ?? null;
|
||||
}
|
||||
|
||||
function first_existing(array $columns, array $candidates): ?string {
|
||||
foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c;
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ===================== Boot ===================== */
|
||||
try {
|
||||
$conf = load_config();
|
||||
cors($conf);
|
||||
// if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
if (!empty($conf['auth']['cookie'])) {
|
||||
$c = $conf['auth']['cookie'];
|
||||
$params = session_get_cookie_params();
|
||||
$params['lifetime'] = $c['lifetime'] ?? $params['lifetime'];
|
||||
$params['path'] = $c['path'] ?? $params['path'];
|
||||
$params['domain'] = $c['domain'] ?? $params['domain'];
|
||||
$params['secure'] = $c['secure'] ?? $params['secure'];
|
||||
$params['httponly'] = $c['httponly'] ?? $params['httponly'];
|
||||
if (isset($c['samesite'])) $params['samesite'] = $c['samesite'];
|
||||
session_set_cookie_params($params);
|
||||
}
|
||||
|
||||
$in = get_input();
|
||||
|
||||
/* ---- Compat: ?resource=blocks&action=list -> blocks.list ---- */
|
||||
$action = val($in, 'action', '');
|
||||
$resource = val($in, 'resource', null);
|
||||
$allowedResources = ['templates','sections','blocks','snippets'];
|
||||
if ($resource && in_array($resource, $allowedResources, true) && strpos((string)$action, '.') === false) {
|
||||
$verb = strtolower((string)$action);
|
||||
if (in_array($verb, ['list','get','create','update','delete'], true)) $action = $resource.'.'.$verb;
|
||||
}
|
||||
|
||||
/* ---- Multi-tenant ---- */
|
||||
$multi = $conf['multi'] ?? [];
|
||||
$tenantCol = $multi['tenant_col'] ?? null;
|
||||
$mapSess = $multi['map_session_to'] ?? 'id'; // 'id'|'email'|'name'
|
||||
|
||||
$tenantWhere = function(array $session) use ($tenantCol, $mapSess) {
|
||||
if (!$tenantCol) return ['', []];
|
||||
if (!$session) return [' AND 1=0 ', []];
|
||||
$val = $session[$mapSess] ?? null;
|
||||
if ($val===null || $val==='') return [' AND 1=0 ', []];
|
||||
return [" AND `$tenantCol` = :__tenant", [':__tenant'=>$val]];
|
||||
};
|
||||
|
||||
$tenantAssign = function(array $session, array $columns) use ($tenantCol, $mapSess) {
|
||||
if (!$tenantCol || !in_array($tenantCol, $columns, true)) return [];
|
||||
$val = $session[$mapSess] ?? null;
|
||||
return ($val===null || $val==='') ? [] : [$tenantCol => $val];
|
||||
};
|
||||
|
||||
$requireAuth = function() {
|
||||
if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401);
|
||||
return $_SESSION['auth'];
|
||||
};
|
||||
|
||||
/* ---- config tables/columns ---- */
|
||||
$tables = $conf['tables'] ?? [];
|
||||
$tableMap = [
|
||||
'templates' => $tables['templates'] ?? 'emailtemplate_templates',
|
||||
'sections' => $tables['sections'] ?? 'emailtemplate_sections',
|
||||
'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks',
|
||||
'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets',
|
||||
];
|
||||
|
||||
$colsDefault = [
|
||||
'id' => 'id',
|
||||
'name' => 'name',
|
||||
'desc' => 'description',
|
||||
'cat' => 'category',
|
||||
'upd' => 'updated_at',
|
||||
];
|
||||
|
||||
$pdo = pdo_templates($conf);
|
||||
|
||||
/* helper: resolve id column for a table */
|
||||
$resolveIdCol = function(string $kind) use ($conf, $colsDefault, $tableMap, $pdo) {
|
||||
$t = $tableMap[$kind];
|
||||
$cfg = $conf['columns'][$kind] ?? [];
|
||||
$cols = table_columns($pdo, $t);
|
||||
$idCol = $cfg['id'] ?? (in_array('id', $cols, true) ? 'id' : primary_key($pdo, $t));
|
||||
if (!$idCol) $idCol = 'id'; // fallback
|
||||
return [$idCol, $cols];
|
||||
};
|
||||
|
||||
/* helper: accept many id aliases */
|
||||
$pullId = function(array $src) {
|
||||
$aliases = ['id','item_id','template_id','tpl_id','section_id','sec_id','block_id','blk_id','snippet_id','snip_id'];
|
||||
foreach ($aliases as $a) if (isset($src[$a]) && $src[$a] !== '') return $src[$a];
|
||||
return null;
|
||||
};
|
||||
|
||||
/* ===================== Router ===================== */
|
||||
switch ($action) {
|
||||
case 'health':
|
||||
respond(['ok'=>true,'time'=>date('c')]);
|
||||
|
||||
/* ---------- AUTH ---------- */
|
||||
case 'auth.login': {
|
||||
$identifier = trim((string)val($in, ['username','user','email','login'], ''));
|
||||
$password = (string)val($in, ['password','pass','pwd'], '');
|
||||
if ($identifier === '' || $password === '') fail('username/password required', null, 422);
|
||||
$authDb = $conf['auth']['db'] ?? [];
|
||||
$table = $authDb['table'] ?? 'emailtemplate_users';
|
||||
$colUser = $authDb['col_user'] ?? 'email';
|
||||
$colPass = $authDb['col_pass'] ?? 'password';
|
||||
$colName = $authDb['col_name'] ?? 'name';
|
||||
$colId = $authDb['col_id'] ?? 'id';
|
||||
$colStatus = $authDb['col_status']?? null;
|
||||
$activeValues = $authDb['active_values'] ?? ['active','1',1];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM `$table` WHERE `$colUser` = :u LIMIT 1");
|
||||
|
||||
$stmt->execute([':u'=>$identifier]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) fail('Invalid credentials', null, 401);
|
||||
if ($colStatus && isset($row[$colStatus])) {
|
||||
if (!in_array($row[$colStatus], $activeValues, true)) {
|
||||
fail('Account inactive', null, 403);
|
||||
}
|
||||
}
|
||||
|
||||
$stored = (string)($row[$colPass] ?? '');
|
||||
if ($stored === '' || !verify_password($password, $stored, $authDb)) {
|
||||
fail('Invalid credentials', null, 401);
|
||||
}
|
||||
|
||||
$_SESSION['auth'] = [
|
||||
'id' => $row[$colId] ?? null,
|
||||
'name' => $row[$colName] ?? ($row[$colUser] ?? $identifier),
|
||||
'email' => $row[$colUser] ?? $identifier,
|
||||
'at' => time(),
|
||||
];
|
||||
$token = base64_encode(hash('sha256', ($_SESSION['auth']['id'] ?? $identifier).'|'.session_id(), true));
|
||||
respond(['ok'=>true,'user'=>$_SESSION['auth'],'token'=>$token]);
|
||||
}
|
||||
|
||||
case 'auth.me':
|
||||
if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401);
|
||||
respond(['ok'=>true,'user'=>$_SESSION['auth']]);
|
||||
case 'auth.logout':
|
||||
$_SESSION = [];
|
||||
if (session_id() !== '') session_destroy();
|
||||
|
||||
respond(['ok'=>true]);
|
||||
|
||||
/* ---------- LIST (mit Parent-Filtern) ---------- */
|
||||
case 'templates.list':
|
||||
case 'sections.list':
|
||||
case 'blocks.list':
|
||||
case 'snippets.list': {
|
||||
$auth = $requireAuth();
|
||||
$kind = explode('.', $action)[0];
|
||||
$t = $tableMap[$kind];
|
||||
[$idCol, $allCols] = $resolveIdCol($kind);
|
||||
$cfg = $conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol);
|
||||
$descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']);
|
||||
$catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']);
|
||||
$updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']);
|
||||
$q = trim((string)val($in,'q',''));
|
||||
$limit = max(1, (int)val($in,'limit', 500));
|
||||
$offset = max(0, (int)val($in,'offset',0));
|
||||
$where = ' WHERE 1=1 ';
|
||||
$params = [];
|
||||
if ($q !== '') {
|
||||
$parts = ["`$nameCol` LIKE :q"];
|
||||
if ($descCol) $parts[] = "`$descCol` LIKE :q";
|
||||
if ($catCol) $parts[] = "`$catCol` LIKE :q";
|
||||
$where .= " AND (".implode(' OR ', $parts).") ";
|
||||
$params[':q'] = '%'.$q.'%';
|
||||
}
|
||||
|
||||
// Parent-Filtern (falls Spalten existieren)
|
||||
$parentFilters = [
|
||||
'template_id' => val($in, ['template_id','tpl_id'], null),
|
||||
'section_id' => val($in, ['section_id','sec_id'], null),
|
||||
'block_id' => val($in, ['block_id','blk_id'], null),
|
||||
];
|
||||
foreach ($parentFilters as $col => $v) {
|
||||
if ($v === null || $v === '') continue;
|
||||
if (in_array($col, $allCols, true)) { $where .= " AND `$col` = :$col "; $params[":$col"] = $v; }
|
||||
}
|
||||
|
||||
// Mandant
|
||||
[$tw,$tp] = $tenantWhere($auth);
|
||||
$where .= $tw; foreach ($tp as $k=>$v) $params[$k]=$v;
|
||||
$order = $updCol ? " ORDER BY `$updCol` DESC " : " ORDER BY `$nameCol` ASC ";
|
||||
$sql = "SELECT * FROM `$t` $where $order LIMIT :off,:lim";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach ($params as $k=>$v) $stmt->bindValue($k,$v,is_int($v)?PDO::PARAM_INT:PDO::PARAM_STR);
|
||||
$stmt->bindValue(':off',$offset,PDO::PARAM_INT);
|
||||
$stmt->bindValue(':lim',$limit,PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$item = [
|
||||
'id' => $r[$idCol] ?? null,
|
||||
'name' => $r[$nameCol] ?? null,
|
||||
];
|
||||
if ($descCol && isset($r[$descCol])) $item['desc'] = $r[$descCol];
|
||||
if ($catCol && isset($r[$catCol])) $item['category'] = $r[$catCol];
|
||||
if ($updCol && isset($r[$updCol])) $item['updated_at'] = $r[$updCol];
|
||||
$out[] = $item;
|
||||
}
|
||||
respond(['ok'=>true,'kind'=>$kind,'items'=>$out,'data'=>$out,'count'=>count($out),'offset'=>$offset,'limit'=>$limit]);
|
||||
}
|
||||
|
||||
/* ---------- GET (JETZT mit top-level html/content) ---------- */
|
||||
case 'templates.get':
|
||||
case 'sections.get':
|
||||
case 'blocks.get':
|
||||
case 'snippets.get': {
|
||||
$auth = $requireAuth();
|
||||
$kind = explode('.', $action)[0];
|
||||
$t = $tableMap[$kind];
|
||||
[$idCol, $allCols] = $resolveIdCol($kind);
|
||||
$id = $pullId($in);
|
||||
if ($id === null || $id === '') fail('id required', null, 422);
|
||||
[$tw,$tp] = $tenantWhere($auth);
|
||||
$sql = "SELECT * FROM `$t` WHERE `$idCol` = :id".$tw." LIMIT 1";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id);
|
||||
foreach ($tp as $k=>$v) $stmt->bindValue($k,$v);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) fail('Not found', ['kind'=>$kind,'id'=>$id], 404);
|
||||
$rowOut = ['id' => $row[$idCol] ?? $id] + $row;
|
||||
|
||||
// NEU: Spalten für HTML/JSON erkennen und top-level ausgeben
|
||||
$htmlCol = first_existing($allCols, ['html','body','markup']);
|
||||
$jsonCol = first_existing($allCols, ['content_json','json','content','structure_json']);
|
||||
$topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null;
|
||||
$topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null;
|
||||
|
||||
respond([
|
||||
'ok'=>true,
|
||||
'kind'=>$kind,
|
||||
'id'=>$rowOut['id'],
|
||||
'item'=>$rowOut,
|
||||
'data'=>$rowOut,
|
||||
'html'=>$topHtml, // <— wichtig für Vorschau
|
||||
'content'=>$topContent // optional (z. B. GrapesJS JSON)
|
||||
]);
|
||||
}
|
||||
|
||||
/* ---------- CREATE ---------- */
|
||||
case 'templates.create':
|
||||
case 'sections.create':
|
||||
case 'blocks.create':
|
||||
case 'snippets.create': {
|
||||
$auth = $requireAuth();
|
||||
$kind = explode('.', $action)[0];
|
||||
$t = $tableMap[$kind];
|
||||
[$idCol, $allCols] = $resolveIdCol($kind);
|
||||
$cfg = $conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol);
|
||||
$descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']);
|
||||
$catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']);
|
||||
$updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']);
|
||||
$name = trim((string)val($in, ['name','title'], ''));
|
||||
if ($name === '') fail('name required', null, 422);
|
||||
$desc = (string)val($in, ['description','desc'], null);
|
||||
$cat = (string)val($in, ['category','cat'], null);
|
||||
$html = (string)val($in, ['html','body','markup'], null);
|
||||
$json = val($in, ['content_json','json','content','structure_json'], null);
|
||||
$settings = val($in, ['settings_json','settings'], null);
|
||||
$templateId = val($in, ['template_id','tpl_id'], null);
|
||||
$sectionId = val($in, ['section_id','sec_id'], null);
|
||||
$blockId = val($in, ['block_id','blk_id'], null);
|
||||
$data = [ $nameCol => $name ];
|
||||
if ($desc !== null && $descCol) $data[$descCol] = $desc;
|
||||
if ($cat !== null && $catCol) $data[$catCol] = $cat;
|
||||
$c = first_existing($allCols, ['html','body','markup']); if ($c && $html !== null) $data[$c]=$html;
|
||||
$c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($c && $json !== null) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
$c = first_existing($allCols, ['settings_json','settings']); if ($c && $settings !== null) $data[$c]= is_string($settings)?$settings:json_encode($settings, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
if ($templateId !== null && in_array('template_id',$allCols,true)) $data['template_id']=$templateId;
|
||||
if ($sectionId !== null && in_array('section_id',$allCols,true)) $data['section_id']=$sectionId;
|
||||
if ($blockId !== null && in_array('block_id',$allCols,true)) $data['block_id']=$blockId;
|
||||
$data = $data + $tenantAssign($_SESSION['auth'] ?? [], $allCols);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$createdCol = first_existing($allCols, ['created_at','created','createdAt']);
|
||||
if ($createdCol) $data[$createdCol] = $now;
|
||||
if ($updCol) $data[$updCol] = $now;
|
||||
$fields = array_keys($data);
|
||||
$place = array_map(fn($c)=>":$c", $fields);
|
||||
$sql = "INSERT INTO `$t` (".implode(',', array_map(fn($c)=>"`$c`",$fields)).") VALUES (".implode(',', $place).")";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach ($data as $k=>$v) $stmt->bindValue(":$k", $v);
|
||||
$stmt->execute();
|
||||
$newId = $pdo->lastInsertId();
|
||||
$out = ['id'=>$newId,'name'=>$name];
|
||||
if ($desc !== null) $out['desc']=$desc;
|
||||
if ($cat !== null) $out['category']=$cat;
|
||||
respond(['ok'=>true,'kind'=>$kind,'id'=>$newId,'item'=>$out,'data'=>$out]);
|
||||
}
|
||||
|
||||
/* ---------- UPDATE ---------- */
|
||||
case 'templates.update':
|
||||
case 'sections.update':
|
||||
case 'blocks.update':
|
||||
case 'snippets.update': {
|
||||
$auth = $requireAuth();
|
||||
$kind = explode('.', $action)[0];
|
||||
$t = $tableMap[$kind];
|
||||
[$idCol, $allCols] = $resolveIdCol($kind);
|
||||
$cfg = $conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol);
|
||||
$descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']);
|
||||
$catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']);
|
||||
$updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']);
|
||||
$id = $pullId($in);
|
||||
if ($id === null || $id === '') fail('id required', null, 422);
|
||||
$data = [];
|
||||
$name = val($in, ['name','title'], null);
|
||||
$desc = val($in, ['description','desc'], null);
|
||||
$cat = val($in, ['category','cat'], null);
|
||||
$html = val($in, ['html','body','markup'], null);
|
||||
$json = val($in, ['content_json','json','content','structure_json'], null);
|
||||
$settings = val($in, ['settings_json','settings'], null);
|
||||
if ($name !== null) $data[$nameCol] = (string)$name;
|
||||
if ($desc !== null && $descCol) $data[$descCol] = (string)$desc;
|
||||
if ($cat !== null && $catCol) $data[$catCol] = (string)$cat;
|
||||
$c = first_existing($allCols, ['html','body','markup']); if ($html !== null && $c) $data[$c]=(string)$html;
|
||||
$c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($json !== null && $c) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
$c = first_existing($allCols, ['settings_json','settings']); if ($settings !== null && $c) $data[$c]= is_string($settings)?$settings:json_encode($settings, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
$tpl = val($in, ['template_id','tpl_id'], null); if ($tpl !== null && in_array('template_id',$allCols,true)) $data['template_id']=$tpl;
|
||||
$sec = val($in, ['section_id','sec_id'], null); if ($sec !== null && in_array('section_id',$allCols,true)) $data['section_id']=$sec;
|
||||
$blk = val($in, ['block_id','blk_id'], null); if ($blk !== null && in_array('block_id',$allCols,true)) $data['block_id']=$blk;
|
||||
if ($updCol) $data[$updCol] = date('Y-m-d H:i:s');
|
||||
if (!$data) fail('nothing to update', null, 422);
|
||||
[$tw,$tp] = $tenantWhere($auth);
|
||||
$set = []; foreach (array_keys($data) as $c) $set[] = "`$c` = :$c";
|
||||
$sql = "UPDATE `$t` SET ".implode(',',$set)." WHERE `$idCol` = :__id".$tw." LIMIT 1";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach ($data as $k=>$v) $stmt->bindValue(":$k", $v);
|
||||
$stmt->bindValue(':__id', $id);
|
||||
foreach ($tp as $k=>$v) $stmt->bindValue($k,$v);
|
||||
$stmt->execute();
|
||||
respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'updated'=>array_keys($data)]);
|
||||
}
|
||||
|
||||
/* ---------- DELETE ---------- */
|
||||
case 'templates.delete':
|
||||
case 'sections.delete':
|
||||
case 'blocks.delete':
|
||||
case 'snippets.delete': {
|
||||
$auth = $requireAuth();
|
||||
$kind = explode('.', $action)[0];
|
||||
$t = $tableMap[$kind];
|
||||
[$idCol, $allCols] = $resolveIdCol($kind);
|
||||
$id = $pullId($in);
|
||||
if ($id === null || $id === '') fail('id required', null, 422);
|
||||
[$tw,$tp] = $tenantWhere($auth);
|
||||
$sql = "DELETE FROM `$t` WHERE `$idCol` = :__id".$tw." LIMIT 1";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindValue(':__id', $id);
|
||||
foreach ($tp as $k=>$v) $stmt->bindValue($k,$v);
|
||||
$stmt->execute();
|
||||
respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'deleted'=>true]);
|
||||
}
|
||||
|
||||
/* ---------- Platzhalter für kommende Features ---------- */
|
||||
case 'render.preview':
|
||||
fail('Not implemented', 'Preview wird im Feature-Patch geliefert', 501);
|
||||
case 'templates.test_send':
|
||||
fail('Not implemented', 'Testversand wird im Feature-Patch geliefert', 501);
|
||||
case 'export.render':
|
||||
fail('Not implemented', 'Export-API wird im Feature-Patch geliefert', 501);
|
||||
default:
|
||||
fail('Unknown action', $action ?: 'missing', 404);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
fail('Server error', get_class($e).': '.$e->getMessage(), 500);
|
||||
}
|
||||
1017
public/api-original.php
Normal file
1017
public/api-original.php
Normal file
File diff suppressed because it is too large
Load Diff
15
public/api.php
Normal file
15
public/api.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// 1. Composer Autoload (Falls nicht schon im Webserver-Setup enthalten)
|
||||
$composerAutoload = __DIR__ . '/../vendor/autoload.php';
|
||||
if (is_file($composerAutoload)) {
|
||||
require_once $composerAutoload;
|
||||
}
|
||||
|
||||
// 2. Lade die Service-Klasse (API-Kernel)
|
||||
require_once __DIR__ . '/../inc/ApiKernel.php';
|
||||
|
||||
// 3. Erstelle eine Instanz und führe sie aus
|
||||
$api = new ApiKernel();
|
||||
$api->run();
|
||||
492
public/api.php.txt
Normal file
492
public/api.php.txt
Normal file
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
// api.php — Multi-User API mit Legacy-Kompatibilität
|
||||
// Stand: 2025-09-05
|
||||
|
||||
declare(strict_types=1);
|
||||
header('X-API-Version: 2025-09-05');
|
||||
|
||||
/* ------------------------- Fehler als JSON ------------------------- */
|
||||
set_error_handler(static function ($severity, $message, $file, $line) {
|
||||
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
set_exception_handler(static function (Throwable $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'internal',
|
||||
'type' => get_class($e),
|
||||
'msg' => $e->getMessage(),
|
||||
'file' => basename($e->getFile()),
|
||||
'line' => $e->getLine(),
|
||||
], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
});
|
||||
|
||||
/* ------------------------- Helpers ------------------------- */
|
||||
function json_out(array $data, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
function in_json(): array {
|
||||
$raw = file_get_contents('php://input') ?: '';
|
||||
if ($raw === '') return [];
|
||||
$j = json_decode($raw, true);
|
||||
return is_array($j) ? $j : [];
|
||||
}
|
||||
|
||||
/* ------------------------- Config laden ------------------------- */
|
||||
// Parent /inc/config.php (wie von dir beschrieben)
|
||||
$cfgFile = dirname(__DIR__) . '/inc/config.php';
|
||||
if (!is_file($cfgFile)) {
|
||||
json_out(['ok'=>false,'error'=>'config_missing','hint'=>'config.php nicht gefunden','path'=>$cfgFile], 500);
|
||||
}
|
||||
$CFG = include $cfgFile;
|
||||
if (!is_array($CFG)) {
|
||||
json_out(['ok'=>false,'error'=>'config_invalid','hint'=>'config.php muss ein Array zurückgeben'], 500);
|
||||
}
|
||||
$ENV = $CFG['env'] ?? 'prod';
|
||||
|
||||
/* ------------------------- PDO-Factories ------------------------- */
|
||||
function pdo_from_cfg(?array $dbc): ?PDO {
|
||||
if (!$dbc) return null;
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$dbc['db_host'] ?? 'localhost',
|
||||
$dbc['db_name'] ?? '',
|
||||
$dbc['db_charset']?? 'utf8mb4'
|
||||
);
|
||||
return new PDO($dsn, $dbc['db_user'] ?? '', $dbc['db_pass'] ?? '', [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
$pdoTpl = pdo_from_cfg($CFG['templates'] ?? null); // Template-Daten
|
||||
$pdoCust = pdo_from_cfg(($CFG['customers'] ?? null) ?: ($CFG['templates'] ?? null)); // Kunden/Users
|
||||
|
||||
$TPL_DB = $CFG['templates']['db_name'] ?? null; // für information_schema
|
||||
|
||||
function has_column(PDO $pdo, ?string $db, string $table, string $col): bool {
|
||||
if (!$db) return false;
|
||||
$st = $pdo->prepare("SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=:db AND TABLE_NAME=:t AND COLUMN_NAME=:c
|
||||
LIMIT 1");
|
||||
$st->execute([':db'=>$db, ':t'=>$table, ':c'=>$col]);
|
||||
return (bool)$st->fetchColumn();
|
||||
}
|
||||
|
||||
/* ------------------------- Auth (Helper oder Fallback) ------------------------- */
|
||||
$authFile = dirname(__DIR__) . '/inc/auth_helpers.php';
|
||||
$useHelpers = is_file($authFile);
|
||||
if ($useHelpers) {
|
||||
require_once $authFile; // stellt auth_start_session(), auth_require(), auth_logout(), require_role() bereit
|
||||
} else {
|
||||
// interner Fallback – kompatibel zu deinen Erwartungen
|
||||
function auth_start_session(array $CFG): void {
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
session_set_cookie_params([
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
'secure' => $secure,
|
||||
'path' => rtrim(dirname($_SERVER['SCRIPT_NAME']),'/').'/'
|
||||
]);
|
||||
session_name('et_session');
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
||||
}
|
||||
function auth_require(array $CFG): void {
|
||||
auth_start_session($CFG);
|
||||
if (empty($_SESSION['user'])) {
|
||||
json_out(['ok'=>false,'error'=>'unauthorized'], 401);
|
||||
}
|
||||
}
|
||||
function auth_logout(array $CFG): void {
|
||||
auth_start_session($CFG);
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time()-42000, $p['path'], $p['domain'] ?? '', $p['secure'], $p['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
function require_role(array $CFG, array $roles): void {
|
||||
auth_start_session($CFG);
|
||||
$r = $_SESSION['user']['role'] ?? null;
|
||||
if (!$r || !in_array($r, $roles, true)) json_out(['ok'=>false,'error'=>'forbidden'], 403);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------- Routing + Legacy-Mapping ------------------------- */
|
||||
$action = $_GET['action'] ?? $_POST['action'] ?? null;
|
||||
|
||||
// Alt: ?resource=blocks&action=list|get|create|update|delete|sync
|
||||
if (!empty($_GET['resource'])) {
|
||||
$res = (string)$_GET['resource'];
|
||||
$act = (string)($_GET['action'] ?? '');
|
||||
$allowed = ['templates','sections','blocks','snippets','assets','template_items','section_items'];
|
||||
if (in_array($res, $allowed, true)) {
|
||||
if ($act === 'list') $action = $res.'.list';
|
||||
if ($act === 'get') $action = $res.'.get';
|
||||
if ($act === 'create') $action = $res.'.create';
|
||||
if ($act === 'update') $action = $res.'.update';
|
||||
if ($act === 'delete') $action = $res.'.delete';
|
||||
if ($act === 'sync') $action = $res.'.sync';
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------- Meta/Health ------------------------- */
|
||||
if ($action === 'health' || $action === 'ping') json_out(['ok'=>true,'env'=>$ENV,'time'=>date('c')]);
|
||||
if ($action === 'version') json_out(['ok'=>true,'version'=>'2025-09-05','env'=>$ENV]);
|
||||
|
||||
/* ------------------------- Diagnose (leichtgewichtig) ------------------------- */
|
||||
if ($action === 'debug.diag') {
|
||||
$diag = [
|
||||
'php' => PHP_VERSION,
|
||||
'pdo' => extension_loaded('pdo'),
|
||||
'pdo_mysql'=> extension_loaded('pdo_mysql'),
|
||||
'cfg' => ['templates'=>!!$pdoTpl, 'customers'=>!!$pdoCust],
|
||||
];
|
||||
json_out(['ok'=>true,'diag'=>$diag]);
|
||||
}
|
||||
|
||||
/* ------------------------- STAGING: User-Debug ------------------------- */
|
||||
if (in_array($action, ['debug.users','debug.users.check','debug.users.setpass','debug.users.peek'], true)) {
|
||||
if ($ENV !== 'staging') json_out(['ok'=>false,'error'=>'forbidden'], 403);
|
||||
if (!$pdoCust) json_out(['ok'=>false,'error'=>'customers_db_not_configured'], 500);
|
||||
|
||||
if ($action === 'debug.users') {
|
||||
$email = isset($_GET['email']) ? trim((string)$_GET['email']) : '';
|
||||
if ($email !== '') {
|
||||
$st = $pdoCust->prepare("SELECT id, customer_id, email, role, is_active, created_at, updated_at
|
||||
FROM customer_users WHERE email=:email");
|
||||
$st->execute([':email'=>$email]);
|
||||
$rows = $st->fetchAll();
|
||||
} else {
|
||||
$st = $pdoCust->query("SELECT id, customer_id, email, role, is_active, created_at, updated_at
|
||||
FROM customer_users ORDER BY id DESC LIMIT 50");
|
||||
$rows = $st->fetchAll();
|
||||
}
|
||||
json_out(['ok'=>true,'items'=>$rows]);
|
||||
}
|
||||
|
||||
if ($action === 'debug.users.check') {
|
||||
$in = in_json();
|
||||
$email = trim((string)($in['email'] ?? ''));
|
||||
$pass = (string)($in['password'] ?? '');
|
||||
if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_params'], 400);
|
||||
$st = $pdoCust->prepare("SELECT id, customer_id, email, password_hash, role, is_active FROM customer_users
|
||||
WHERE email=:email LIMIT 1");
|
||||
$st->execute([':email'=>$email]);
|
||||
$u = $st->fetch();
|
||||
if (!$u) json_out(['ok'=>true,'exists'=>false,'password_match'=>false]);
|
||||
json_out(['ok'=>true,'exists'=>true,'password_match'=>password_verify($pass,$u['password_hash'])]);
|
||||
}
|
||||
|
||||
if ($action === 'debug.users.setpass') {
|
||||
$in = in_json();
|
||||
$email = trim((string)($in['email'] ?? ''));
|
||||
$pass = (string)($in['password'] ?? '');
|
||||
if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_params'], 400);
|
||||
$st = $pdoCust->prepare("SELECT id FROM customer_users WHERE email=:email LIMIT 1");
|
||||
$st->execute([':email'=>$email]);
|
||||
$u = $st->fetch();
|
||||
if (!$u) json_out(['ok'=>false,'error'=>'user_not_found'], 404);
|
||||
$hash = password_hash($pass, PASSWORD_DEFAULT);
|
||||
$upd = $pdoCust->prepare("UPDATE customer_users SET password_hash=:h, is_active=1 WHERE id=:id");
|
||||
$upd->execute([':h'=>$hash, ':id'=>$u['id']]);
|
||||
json_out(['ok'=>true,'set'=>true]);
|
||||
}
|
||||
|
||||
if ($action === 'debug.users.peek') {
|
||||
$email = isset($_GET['email']) ? trim((string)$_GET['email']) : '';
|
||||
if ($email==='') json_out(['ok'=>false,'error'=>'missing_email'], 400);
|
||||
$st = $pdoCust->prepare("SELECT id, customer_id, email, LENGTH(password_hash) len
|
||||
FROM customer_users WHERE email=:email");
|
||||
$st->execute([':email'=>$email]);
|
||||
json_out(['ok'=>true,'user'=>$st->fetch()]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------- AUTH: Login / Logout / Me ------------------------- */
|
||||
if ($action === 'auth.login') {
|
||||
$in = in_json();
|
||||
$email = trim(strtolower((string)($in['email'] ?? '')));
|
||||
$pass = (string)($in['password'] ?? '');
|
||||
if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_credentials'], 400);
|
||||
if (!$pdoCust) json_out(['ok'=>false,'error'=>'customers_db_not_configured'], 500);
|
||||
|
||||
// Mehrfachkunden mit gleicher Mail erlauben → best match über password_verify
|
||||
$st = $pdoCust->prepare("SELECT cu.id, cu.customer_id, cu.email, cu.password_hash, cu.role, cu.is_active,
|
||||
c.slug AS customer_slug, c.plan, c.status
|
||||
FROM customer_users cu
|
||||
JOIN customers c ON c.id = cu.customer_id
|
||||
WHERE cu.email=:email");
|
||||
$st->execute([':email'=>$email]);
|
||||
$rows = $st->fetchAll();
|
||||
|
||||
$match = null;
|
||||
foreach ($rows as $r) {
|
||||
if ((int)$r['is_active'] === 1 && !empty($r['password_hash']) && password_verify($pass, $r['password_hash'])) {
|
||||
$match = $r; break;
|
||||
}
|
||||
}
|
||||
if (!$match) json_out(['ok'=>false,'error'=>'invalid_credentials'], 401);
|
||||
if (($match['status'] ?? 'active') !== 'active') json_out(['ok'=>false,'error'=>'customer_inactive'], 403);
|
||||
|
||||
auth_start_session($CFG);
|
||||
$_SESSION['user'] = [
|
||||
'id' => (int)$match['id'],
|
||||
'email' => $match['email'],
|
||||
'role' => $match['role'],
|
||||
'customer_id' => (int)$match['customer_id'],
|
||||
'customer_slug' => $match['customer_slug'],
|
||||
'plan' => $match['plan'] ?? null,
|
||||
];
|
||||
json_out(['ok'=>true,'user'=>$_SESSION['user']]);
|
||||
}
|
||||
|
||||
if ($action === 'auth.logout') { auth_logout($CFG); json_out(['ok'=>true]); }
|
||||
|
||||
if ($action === 'auth.me') {
|
||||
auth_start_session($CFG);
|
||||
json_out(['ok'=>!empty($_SESSION['user']), 'user'=>$_SESSION['user'] ?? null]);
|
||||
}
|
||||
|
||||
/* ------------------------- ab hier: geschützt ------------------------- */
|
||||
$public = ['auth.login','auth.logout','auth.me','health','ping','version','debug.diag','debug.users','debug.users.check','debug.users.setpass','debug.users.peek'];
|
||||
if (!in_array($action, $public, true)) auth_require($CFG);
|
||||
|
||||
$customerId = (int)($_SESSION['user']['customer_id'] ?? 0);
|
||||
|
||||
/* ------------------------- Templates ------------------------- */
|
||||
if ($action === 'templates.list') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$st = $pdoTpl->prepare("SELECT id, name, updated_at
|
||||
FROM emailtemplate_templates
|
||||
WHERE customer_id = :cid
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 1000");
|
||||
$st->execute([':cid'=>$customerId]);
|
||||
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
|
||||
}
|
||||
|
||||
if ($action === 'templates.get') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
|
||||
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
|
||||
$hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html');
|
||||
$cols = $hasHtml ? "id, customer_id, name, html, updated_at" : "id, customer_id, name, NULL AS html, updated_at";
|
||||
$st = $pdoTpl->prepare("SELECT $cols FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid LIMIT 1");
|
||||
$st->execute([':id'=>$id, ':cid'=>$customerId]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
|
||||
json_out(['ok'=>true,'item'=>$row]);
|
||||
}
|
||||
|
||||
if ($action === 'templates.create') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$in = in_json();
|
||||
$name = trim((string)($in['name'] ?? ''));
|
||||
$html = (string)($in['html'] ?? '');
|
||||
if ($name==='') json_out(['ok'=>false,'error'=>'name_required'], 400);
|
||||
$hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html');
|
||||
|
||||
if ($hasHtml) {
|
||||
$st = $pdoTpl->prepare("INSERT INTO emailtemplate_templates (customer_id,name,html,created_at,updated_at)
|
||||
VALUES (:cid,:name,:html,NOW(),NOW())");
|
||||
$st->execute([':cid'=>$customerId, ':name'=>$name, ':html'=>$html]);
|
||||
} else {
|
||||
$st = $pdoTpl->prepare("INSERT INTO emailtemplate_templates (customer_id,name,created_at,updated_at)
|
||||
VALUES (:cid,:name,NOW(),NOW())");
|
||||
$st->execute([':cid'=>$customerId, ':name'=>$name]);
|
||||
}
|
||||
json_out(['ok'=>true,'id'=>(int)$pdoTpl->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'templates.update') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$in = in_json();
|
||||
$id = (int)($in['id'] ?? 0);
|
||||
$name = array_key_exists('name',$in) ? trim((string)$in['name']) : null;
|
||||
$html = array_key_exists('html',$in) ? (string)$in['html'] : null;
|
||||
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
|
||||
|
||||
$hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html');
|
||||
$sets=[]; $p=[':id'=>$id, ':cid'=>$customerId];
|
||||
if ($name!==null) { $sets[]="name=:name"; $p[':name']=$name; }
|
||||
if ($hasHtml && $html!==null) { $sets[]="html=:html"; $p[':html']=$html; }
|
||||
if (!$sets) json_out(['ok'=>false,'error'=>'nothing_to_update'], 400);
|
||||
|
||||
$sql = "UPDATE emailtemplate_templates SET ".implode(',',$sets).", updated_at=NOW() WHERE id=:id AND customer_id=:cid";
|
||||
$st = $pdoTpl->prepare($sql);
|
||||
$st->execute($p);
|
||||
json_out(['ok'=>true,'updated'=>$st->rowCount()]);
|
||||
}
|
||||
|
||||
if ($action === 'templates.delete') {
|
||||
require_role($CFG, ['owner','admin']);
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$in = in_json(); $id=(int)($in['id'] ?? 0);
|
||||
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
|
||||
$pdoTpl->beginTransaction();
|
||||
try {
|
||||
$pdoTpl->prepare("DELETE FROM emailtemplate_template_items WHERE template_id=:id AND customer_id=:cid")->execute([':id'=>$id, ':cid'=>$customerId]);
|
||||
$pdoTpl->prepare("DELETE FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid")->execute([':id'=>$id, ':cid'=>$customerId]);
|
||||
$pdoTpl->commit();
|
||||
json_out(['ok'=>true]);
|
||||
} catch (Throwable $e) { $pdoTpl->rollBack(); throw $e; }
|
||||
}
|
||||
|
||||
/* ------------------------- Sections ------------------------- */
|
||||
if ($action === 'sections.list') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$templateId = isset($_GET['template_id']) ? (int)$_GET['template_id'] : 0;
|
||||
if ($templateId>0) {
|
||||
$st = $pdoTpl->prepare("SELECT id, template_id, name, z_index, type, updated_at
|
||||
FROM emailtemplate_sections
|
||||
WHERE customer_id=:cid AND template_id=:tid
|
||||
ORDER BY z_index ASC, id ASC
|
||||
LIMIT 1000");
|
||||
$st->execute([':cid'=>$customerId, ':tid'=>$templateId]);
|
||||
} else {
|
||||
$st = $pdoTpl->prepare("SELECT id, template_id, name, z_index, type, updated_at
|
||||
FROM emailtemplate_sections
|
||||
WHERE customer_id=:cid
|
||||
ORDER BY template_id ASC, z_index ASC, id ASC
|
||||
LIMIT 1000");
|
||||
$st->execute([':cid'=>$customerId]);
|
||||
}
|
||||
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
|
||||
}
|
||||
|
||||
if ($action === 'sections.get') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
|
||||
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
|
||||
$st = $pdoTpl->prepare("SELECT id, customer_id, template_id, name, z_index, type, updated_at
|
||||
FROM emailtemplate_sections
|
||||
WHERE id=:id AND customer_id=:cid
|
||||
LIMIT 1");
|
||||
$st->execute([':id'=>$id, ':cid'=>$customerId]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
|
||||
json_out(['ok'=>true,'item'=>$row]);
|
||||
}
|
||||
|
||||
/* ------------------------- Blocks ------------------------- */
|
||||
if ($action === 'blocks.list') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$st = $pdoTpl->prepare("SELECT id, name, category, updated_at
|
||||
FROM emailtemplate_blocks
|
||||
WHERE customer_id=:cid
|
||||
ORDER BY name ASC
|
||||
LIMIT 1000");
|
||||
$st->execute([':cid'=>$customerId]);
|
||||
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
|
||||
}
|
||||
|
||||
if ($action === 'blocks.get') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
|
||||
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
|
||||
$st = $pdoTpl->prepare("SELECT id, customer_id, name, category, updated_at
|
||||
FROM emailtemplate_blocks
|
||||
WHERE id=:id AND customer_id=:cid
|
||||
LIMIT 1");
|
||||
$st->execute([':id'=>$id, ':cid'=>$customerId]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
|
||||
json_out(['ok'=>true,'item'=>$row]);
|
||||
}
|
||||
|
||||
if ($action === 'blocks.create') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$in=in_json(); $name=trim((string)($in['name']??'')); $cat=trim((string)($in['category']??''));
|
||||
if ($name==='') json_out(['ok'=>false,'error'=>'name_required'], 400);
|
||||
$st=$pdoTpl->prepare("INSERT INTO emailtemplate_blocks (customer_id,name,category,created_at,updated_at)
|
||||
VALUES (:cid,:name,COALESCE(NULLIF(:cat,''),'Default'),NOW(),NOW())");
|
||||
$st->execute([':cid'=>$customerId, ':name'=>$name, ':cat'=>$cat]);
|
||||
json_out(['ok'=>true,'id'=>(int)$pdoTpl->lastInsertId()]);
|
||||
}
|
||||
|
||||
if ($action === 'blocks.update') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$in=in_json(); $id=(int)($in['id']??0); $name=trim((string)($in['name']??'')); $cat=trim((string)($in['category']??''));
|
||||
if ($id<=0 || $name==='') json_out(['ok'=>false,'error'=>'invalid_params'], 400);
|
||||
$st=$pdoTpl->prepare("UPDATE emailtemplate_blocks
|
||||
SET name=:name, category=COALESCE(NULLIF(:cat,''),category), updated_at=NOW()
|
||||
WHERE id=:id AND customer_id=:cid");
|
||||
$st->execute([':name'=>$name, ':cat'=>$cat, ':id'=>$id, ':cid'=>$customerId]);
|
||||
json_out(['ok'=>true,'updated'=>$st->rowCount()]);
|
||||
}
|
||||
|
||||
if ($action === 'blocks.delete') {
|
||||
require_role($CFG, ['owner','admin']);
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$in=in_json(); $id=(int)($in['id']??0);
|
||||
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
|
||||
$st=$pdoTpl->prepare("DELETE FROM emailtemplate_blocks WHERE id=:id AND customer_id=:cid");
|
||||
$st->execute([':id'=>$id, ':cid'=>$customerId]);
|
||||
json_out(['ok'=>true,'deleted'=>$st->rowCount()]);
|
||||
}
|
||||
|
||||
/* ------------------------- Snippets ------------------------- */
|
||||
if ($action === 'snippets.list') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$st=$pdoTpl->prepare("SELECT id, name, category, updated_at
|
||||
FROM emailtemplate_snippets
|
||||
WHERE customer_id=:cid
|
||||
ORDER BY name ASC
|
||||
LIMIT 1000");
|
||||
$st->execute([':cid'=>$customerId]);
|
||||
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
|
||||
}
|
||||
|
||||
if ($action === 'snippets.get') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
|
||||
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
|
||||
$st=$pdoTpl->prepare("SELECT id, customer_id, name, category, updated_at
|
||||
FROM emailtemplate_snippets
|
||||
WHERE id=:id AND customer_id=:cid
|
||||
LIMIT 1");
|
||||
$st->execute([':id'=>$id, ':cid'=>$customerId]);
|
||||
$row=$st->fetch();
|
||||
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
|
||||
json_out(['ok'=>true,'item'=>$row]);
|
||||
}
|
||||
|
||||
/* ------------------------- Assets (READ) ------------------------- */
|
||||
if ($action === 'assets.list') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$st=$pdoTpl->prepare("SELECT id, name, type, mime_type, size_bytes, public_url, updated_at
|
||||
FROM emailtemplate_assets
|
||||
WHERE customer_id=:cid
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 200");
|
||||
$st->execute([':cid'=>$customerId]);
|
||||
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
|
||||
}
|
||||
|
||||
/* ------------------------- Editor-Referenzen (Placeholders) ------------------------- */
|
||||
if ($action === 'template_items.sync') { json_out(['ok'=>true]); }
|
||||
if ($action === 'section_items.sync') { json_out(['ok'=>true]); }
|
||||
|
||||
/* ------------------------- Render (Fallback) ------------------------- */
|
||||
if ($action === 'render.preview') {
|
||||
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
|
||||
$in = in_json();
|
||||
$templateId = (int)($in['template_id'] ?? 0);
|
||||
if ($templateId<=0) json_out(['ok'=>false,'error'=>'template_id_required'], 400);
|
||||
$st = $pdoTpl->prepare("SELECT id, name FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid LIMIT 1");
|
||||
$st->execute([':id'=>$templateId, ':cid'=>$customerId]);
|
||||
$tpl = $st->fetch();
|
||||
if (!$tpl) json_out(['ok'=>false,'error'=>'not_found'], 404);
|
||||
$html = "<!-- preview {$tpl['name']} (#{$tpl['id']}) -->\n<div style=\"padding:16px;font:14px/1.4 system-ui\">Preview okay.</div>";
|
||||
json_out(['ok'=>true, 'template'=>$tpl, 'html'=>$html]);
|
||||
}
|
||||
|
||||
/* ------------------------- Fallback ------------------------- */
|
||||
json_out(['ok'=>false,'error'=>'unknown_action','action'=>$action], 404);
|
||||
|
||||
167
public/assets/css/admin.css
Normal file
167
public/assets/css/admin.css
Normal file
@@ -0,0 +1,167 @@
|
||||
/* ============================================================
|
||||
Admin Theme (SCOPED)
|
||||
- wirkt NUR unter <body class="page-admin"> bzw. .page-login
|
||||
- keine globalen Resets (html/body/a/…)
|
||||
- GrapesJS (gjs-…) wird nicht angetastet
|
||||
============================================================ */
|
||||
|
||||
/* ---- Farb- & UI-Variablen ---- */
|
||||
body.page-admin,
|
||||
body.page-login {
|
||||
--bg: #f7f7fb;
|
||||
--panel: #ffffff;
|
||||
--text: #222222;
|
||||
--muted: #666666;
|
||||
--border: #e7e7ee;
|
||||
--accent: #5b7cff;
|
||||
--accent-600:#3f5ff7;
|
||||
--danger: #e74c3c;
|
||||
--ok: #2ecc71;
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
}
|
||||
|
||||
/* ---- Links / Text ---- */
|
||||
.page-admin a,
|
||||
.page-login a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.page-admin .muted,
|
||||
.page-login .muted { color: var(--muted); }
|
||||
|
||||
/* ---- Container ---- */
|
||||
.page-admin .container { max-width: 1200px; margin: 20px auto; padding: 0 16px; }
|
||||
|
||||
/* ============================================================
|
||||
ADMIN (Dashboard, Listen, Editor-Rahmen)
|
||||
============================================================ */
|
||||
|
||||
/* ---- Topbar ---- */
|
||||
.page-admin .topbar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 10px; padding: 10px 16px; background: rgba(255,255,255,.9);
|
||||
backdrop-filter: saturate(140%) blur(6px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.page-admin .brand { font-size: 16px; margin: 0; font-weight: 600; }
|
||||
.page-admin .topbar .right { display: flex; gap: 8px; align-items: center; }
|
||||
|
||||
/* ---- Buttons (leicht, neutral) ---- */
|
||||
.page-admin .btn {
|
||||
display:inline-flex; align-items:center; gap:.5rem;
|
||||
padding:.38rem .75rem; border-radius:.7rem;
|
||||
border:1px solid var(--border); background:#fff;
|
||||
font-size:.9rem; cursor:pointer; transition:.15s background-color, .15s border-color, .15s box-shadow;
|
||||
}
|
||||
.page-admin .btn:hover { background:#f8fafc; }
|
||||
.page-admin .btn-primary { border-color: var(--accent); background: var(--accent); color:#fff; }
|
||||
.page-admin .btn-primary:hover { background: var(--accent-600); border-color: var(--accent-600); }
|
||||
.page-admin .btn-danger { border-color:#fecaca; color:#b91c1c; }
|
||||
.page-admin .btn-danger:hover { background:#fef2f2; }
|
||||
|
||||
/* ---- Chips ---- */
|
||||
.page-admin .chip {
|
||||
display:inline-flex; align-items:center; gap:.35rem;
|
||||
padding:.15rem .55rem; border-radius:999px;
|
||||
background:#f1f5f9; color:#334155; font-size:.75rem; border:1px solid var(--border);
|
||||
}
|
||||
.page-admin .chip .dot { width:.5rem; height:.5rem; border-radius:999px; background:#64748b; }
|
||||
|
||||
/* ---- Cards & Rows ---- */
|
||||
.page-admin .card {
|
||||
background: var(--panel); border:1px solid var(--border);
|
||||
border-radius: 12px; padding: 16px; margin-bottom: 16px;
|
||||
}
|
||||
.page-admin .row {
|
||||
display:flex; gap:12px; align-items:center; justify-content:space-between; flex-wrap:wrap;
|
||||
}
|
||||
.page-admin .row .left,
|
||||
.page-admin .row .right { display:flex; gap:10px; align-items:center; }
|
||||
|
||||
/* ---- Tabs (Header-Tabs) ---- */
|
||||
.page-admin .tab {
|
||||
padding:8px 12px; border:1px solid var(--border);
|
||||
border-radius:999px; background:#fff; cursor:pointer; font-size:.9rem;
|
||||
}
|
||||
.page-admin .tab.active { background:var(--accent); border-color:var(--accent); color:#fff; }
|
||||
|
||||
/* ---- Tabellen/Listen ---- */
|
||||
.page-admin .list {
|
||||
width:100%; border-collapse: separate; border-spacing: 0; background: #fff;
|
||||
border:1px solid var(--border); border-radius: 12px; overflow: hidden;
|
||||
}
|
||||
.page-admin .list thead th {
|
||||
text-align:left; padding:10px 12px; background:#fafafe; color:#374151; font-weight:600; font-size:.92rem;
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.page-admin .list tbody td {
|
||||
padding:10px 12px; border-bottom:1px solid var(--border); vertical-align: top; font-size:.92rem;
|
||||
}
|
||||
.page-admin .list tbody tr:hover td { background:#fafafa; }
|
||||
.page-admin .empty {
|
||||
text-align:center; padding:28px; color:var(--muted); font-size:.95rem; background:#fff; border:1px dashed var(--border); border-radius:12px;
|
||||
}
|
||||
|
||||
/* ---- Formulare ---- */
|
||||
.page-admin input[type="text"],
|
||||
.page-admin input[type="email"],
|
||||
.page-admin input[type="number"],
|
||||
.page-admin input[type="search"],
|
||||
.page-admin textarea,
|
||||
.page-admin select {
|
||||
width:100%; border:1px solid var(--border); border-radius:10px; padding:10px 12px; margin:8px 0; background:#fff;
|
||||
font: inherit; color: inherit;
|
||||
}
|
||||
.page-admin textarea { min-height: 110px; resize: vertical; }
|
||||
.page-admin label { display:block; margin:12px 0 6px; color:#334155; font-size:.92rem; }
|
||||
|
||||
/* ---- Dialoge / Backdrop ---- */
|
||||
.page-admin dialog::backdrop { background: rgba(15,23,42,.35); }
|
||||
.page-admin .dialog-head {
|
||||
display:flex; align-items:center; gap:10px; justify-content:space-between;
|
||||
padding:10px 14px; background:var(--panel); border-bottom:1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ---- Preview Dialog ---- */
|
||||
.page-admin .previewDialog { width:min(900px,95vw); border:none; border-radius:12px; padding:0; overflow:hidden; }
|
||||
.page-admin .previewHead { display:flex; justify-content:space-between; align-items:center; gap:10px; padding:10px 14px; background:var(--panel); border-bottom:1px solid var(--border); }
|
||||
.page-admin .previewBody { height:min(70vh,700px); }
|
||||
.page-admin .previewBody iframe { width:100%; height:100%; border:0; background:#fafafa; }
|
||||
|
||||
/* ---- Utility ---- */
|
||||
.page-admin .truncate { max-width:22rem; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
||||
.page-admin .hidden { display:none !important; }
|
||||
.page-admin .muted-12 { font-size:12px; color:var(--muted); }
|
||||
|
||||
/* ============================================================
|
||||
LOGIN (Karte in der Mitte)
|
||||
============================================================ */
|
||||
|
||||
.page-login .loginWrap { display:grid; place-items:center; min-height: 55vh; }
|
||||
.page-login .loginCard {
|
||||
width:min(420px,95vw); background: var(--panel); border:1px solid var(--border);
|
||||
border-radius:16px; box-shadow:0 10px 30px rgba(2,6,23,.06); padding:28px;
|
||||
}
|
||||
.page-login h1 { margin:0 0 8px; font-size:20px; }
|
||||
.page-login p { margin:0 0 18px; color:#475569; }
|
||||
.page-login label { display:block; margin:12px 0 6px; color:#334155; }
|
||||
.page-login input[type="email"],
|
||||
.page-login input[type="text"],
|
||||
.page-login input[type="password"] {
|
||||
width:100%; padding:12px; border:1px solid #cbd5e1; border-radius:10px; font-size:15px; background:#fff;
|
||||
}
|
||||
.page-login .btn-login {
|
||||
width:100%; margin-top:16px; padding:12px; border:0; border-radius:12px;
|
||||
background:#111827; color:#fff; font-weight:600; cursor:pointer;
|
||||
}
|
||||
.page-login .mini { margin-top:10px; text-align:center; }
|
||||
.page-login .hint { font-size:12px; color:var(--muted); }
|
||||
|
||||
/* ABSOLUTER UI-FIX: Versteckt die hartnäckige Bibliothek-Kategorie */
|
||||
.gjs-block-category[data-id="Bibliothek"] {
|
||||
display: none !important;
|
||||
}
|
||||
25
public/assets/css/app.css
Normal file
25
public/assets/css/app.css
Normal file
@@ -0,0 +1,25 @@
|
||||
/* Auth-Guard: bis zum erfolgreichen auth.me nichts anzeigen */
|
||||
html.auth-pending header,
|
||||
html.auth-pending main {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root { color-scheme: light; }
|
||||
|
||||
.btn{
|
||||
display:inline-flex;align-items:center;gap:.5rem;
|
||||
padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;
|
||||
background:#fff;font-size:.9rem;cursor:pointer;
|
||||
}
|
||||
.btn:hover{background:#f8fafc}
|
||||
.btn-danger{border-color:#fecaca;color:#b91c1c}
|
||||
.btn-danger:hover{background:#fef2f2}
|
||||
|
||||
.chip{display:inline-flex;align-items:center;gap:.35rem;padding:.15rem .5rem;border-radius:999px;background:#f1f5f9;color:#334155;font-size:.75rem;border:1px solid #e5e7eb}
|
||||
.chip .dot{width:.5rem;height:.5rem;border-radius:999px;background:#64748b}
|
||||
|
||||
dialog::backdrop{background:rgba(15,23,42,.3)}
|
||||
#toast-root{z-index:2147483647}
|
||||
.truncate{max-width:22rem;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
||||
.hidden{display:none}
|
||||
|
||||
48
public/assets/css/toast.css
Normal file
48
public/assets/css/toast.css
Normal file
@@ -0,0 +1,48 @@
|
||||
/* assets/css/toast.css */
|
||||
/* Works whether the root is in <body> or inside a <dialog open> (top layer) */
|
||||
.toast-root, #toast-root {
|
||||
position: fixed; /* relative to viewport even inside dialog's top layer */
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2147483647;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.toast {
|
||||
pointer-events: auto;
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
min-width: 240px;
|
||||
max-width: 420px;
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 10px 30px rgba(2, 6, 23, .25), 0 2px 8px rgba(2,6,23,.12);
|
||||
color: #0f172a;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(148,163,184,.35);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
animation: toast-slide-in .18s ease-out;
|
||||
}
|
||||
|
||||
.toast + .toast { margin-top: 12px; }
|
||||
|
||||
.toast .icon { font-size: 18px; line-height: 1; margin-top: 2px; }
|
||||
.toast .content { flex: 1; font-size: 14px; line-height: 1.35; }
|
||||
.toast .close { appearance: none; border: 0; background: transparent; font-size: 16px; color: #475569; cursor: pointer; padding: 2px 4px; }
|
||||
|
||||
/* success (green) by default */
|
||||
.toast.success { border-color: #86efac; background: #ecfdf5; color: #065f46; }
|
||||
.toast.success .icon { color: #10b981; }
|
||||
|
||||
/* error (red) */
|
||||
.toast.error { border-color: #fecaca; background: #fff1f2; color: #7f1d1d; }
|
||||
.toast.error .icon { color: #ef4444; }
|
||||
|
||||
@keyframes toast-slide-in {
|
||||
from { transform: translateY(-6px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
109
public/assets/js/api.js
Normal file
109
public/assets/js/api.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// assets/js/api.js
|
||||
const API = "api.php";
|
||||
|
||||
/** ---- intern: Hilfen ---- */
|
||||
function withTs(url) {
|
||||
const sep = url.includes("?") ? "&" : "?";
|
||||
return `${url}${sep}t=${Date.now()}`; // no-store Absicherung
|
||||
}
|
||||
|
||||
async function parseJsonSafe(res) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error("API: invalid JSON", { status: res.status, text });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ... oberer Teil unverändert ...
|
||||
|
||||
/** zentraler Fetch-Wrapper: Credentials, no-store, 401→Login */
|
||||
async function apiFetch(url, init = {}) {
|
||||
const res = await fetch(withTs(url), {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
...init,
|
||||
});
|
||||
if (res.status === 401) {
|
||||
window.location.href = "/login.php";
|
||||
throw new Error("unauthorized");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/** ---- Public API ---- */
|
||||
|
||||
/**
|
||||
* Action-Call:
|
||||
* - apiAction('auth.me')
|
||||
* - apiAction('sections.list', { method:'GET', data:{ template_id: 123 } })
|
||||
* - apiAction('templates.create', { method:'POST', data:{ name:'...' } })
|
||||
*/
|
||||
export async function apiAction(
|
||||
action,
|
||||
{ method = "GET", data = null, headers = {} } = {}
|
||||
) {
|
||||
let url = `${API}?action=${encodeURIComponent(action)}`;
|
||||
const init = { method, headers: { ...headers } };
|
||||
|
||||
// GET/HEAD → data als Query-String anhängen (kein Body!)
|
||||
if ((method === "GET" || method === "HEAD") && data && typeof data === "object") {
|
||||
const params = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (v !== undefined && v !== null) params.append(k, String(v));
|
||||
}
|
||||
const qs = params.toString();
|
||||
if (qs) url += `&${qs}`;
|
||||
} else if (data != null) {
|
||||
init.headers["Content-Type"] = "application/json";
|
||||
init.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const res = await apiFetch(url, init);
|
||||
return await parseJsonSafe(res);
|
||||
}
|
||||
|
||||
// ... Rest (apiList, apiCreate, apiUpdate, apiDelete, toast) unverändert ...
|
||||
|
||||
/**
|
||||
* Listen-Helper für Ressourcen – ruft `${res}.list` auf.
|
||||
* Optional kannst du query-Objekte mitgeben, z.B. { template_id: 123 } für sections.
|
||||
*/
|
||||
export async function apiList(res, query = {}) {
|
||||
const q = new URLSearchParams(query);
|
||||
const qs = q.toString() ? `&${q.toString()}` : "";
|
||||
const r = await apiAction(`${res}.list`, { method: "GET" });
|
||||
// Falls du query serverseitig brauchst (z.B. template_id), nutze eine Action-Variante:
|
||||
// return await apiAction(`${res}.list`, { method:"GET", data: query });
|
||||
return r?.items ?? [];
|
||||
}
|
||||
|
||||
/** GET by id: nur nutzen, wenn du eine `${res}.get`-Action hast */
|
||||
export async function apiGet(res, id) {
|
||||
return await apiAction(`${res}.get`, { method: "GET", data: { id } });
|
||||
}
|
||||
|
||||
/** CREATE / UPDATE / DELETE – sprechen `${res}.create|update|delete` an */
|
||||
export async function apiCreate(res, payload) {
|
||||
return await apiAction(`${res}.create`, { method: "POST", data: payload });
|
||||
}
|
||||
|
||||
export async function apiUpdate(res, id, payload) {
|
||||
return await apiAction(`${res}.update`, { method: "POST", data: { id, ...payload } });
|
||||
}
|
||||
|
||||
export async function apiDelete(res, id) {
|
||||
return await apiAction(`${res}.delete`, { method: "POST", data: { id } });
|
||||
}
|
||||
|
||||
/** optionaler Toast-Fallback (keine harte Abhängigkeit) */
|
||||
export function toast(msg, ok = true, opts = {}) {
|
||||
if (window.Toast?.show) {
|
||||
window.Toast.show(msg, { type: ok ? "success" : "error", duration: 2200, ...opts });
|
||||
} else {
|
||||
(ok ? console.log : console.error)(msg);
|
||||
}
|
||||
}
|
||||
|
||||
84
public/assets/js/app.js
Normal file
84
public/assets/js/app.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// assets/js/app.js
|
||||
import { initTabs } from './ui-tabs.js';
|
||||
import { initLists } from './ui-list.js';
|
||||
import { initCreate } from './ui-create.js';
|
||||
import { initEditor } from './ui-editor.js';
|
||||
import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
|
||||
import { apiAction } from './api.js';
|
||||
|
||||
|
||||
/**
|
||||
* Zeigt die App erst, wenn Auth validiert ist.
|
||||
* Wichtig: KEIN finally → nur im Erfolgsfall UI freigeben (verhindert Flashing für Gäste).
|
||||
*/
|
||||
async function ensureAuthenticated() {
|
||||
try {
|
||||
const me = await apiAction('auth.me', { method: 'GET' });
|
||||
if (!me?.ok || !me?.user) {
|
||||
window.location.href = '/login.php';
|
||||
return false;
|
||||
}
|
||||
// ✅ nur für eingeloggte Nutzer: UI freigeben
|
||||
document.documentElement.classList.remove('auth-pending');
|
||||
const appRoot = document.getElementById('app');
|
||||
if (appRoot && appRoot.hasAttribute('hidden')) appRoot.removeAttribute('hidden');
|
||||
return true;
|
||||
} catch {
|
||||
// apiAction leitet bei 401 ohnehin um
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initAppFeatures() {
|
||||
initTabs();
|
||||
initLists();
|
||||
initCreate();
|
||||
initEditor();
|
||||
|
||||
// Logout-Buttons
|
||||
mountLogoutButton('#btn-logout', { redirect: '/login.php' });
|
||||
ensureFloatingLogout({ redirect: '/login.php' });
|
||||
}
|
||||
|
||||
// Sync-Nachrichten aus dem Editor-Iframe (unverändert, aber mit credentials)
|
||||
async function handleEditorMessages(ev) {
|
||||
const msg = ev.data || {};
|
||||
if (msg.source !== 'email-editor' || msg.type !== 'save') return;
|
||||
|
||||
try {
|
||||
const ctx = window.__currentEditorCtx || {};
|
||||
const id = ctx.id;
|
||||
const mode = (ctx.mode || msg.mode || '').toLowerCase();
|
||||
const refs = Array.isArray(msg.refs) ? msg.refs : [];
|
||||
if (!id || !mode) return;
|
||||
|
||||
if (mode === 'templates') {
|
||||
await fetch('./api.php?resource=template_items&action=sync', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ template_id: id, items: refs })
|
||||
});
|
||||
} else if (mode === 'sections') {
|
||||
await fetch('./api.php?resource=section_items&action=sync', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ section_id: id, items: refs.filter(r => r.ref_type === 'block') })
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('refs sync failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const ok = await ensureAuthenticated();
|
||||
if (!ok) return; // Gast → Redirect, UI bleibt verborgen
|
||||
initAppFeatures(); // Eingeloggt → App initialisieren
|
||||
});
|
||||
|
||||
window.addEventListener('message', handleEditorMessages);
|
||||
|
||||
207
public/assets/js/bridge/blocks-api (Kopie).js
Normal file
207
public/assets/js/bridge/blocks-api (Kopie).js
Normal file
@@ -0,0 +1,207 @@
|
||||
/* /assets/js/bridge/blocks-api.js (SCHRITT 16: Finaler Stabilitäts-Fix) */
|
||||
(function (B) {
|
||||
|
||||
// 🛑 KRITISCHER FIX: Nur minimale Prüfung, um synchrone Initialisierung zu garantieren
|
||||
if (!B || typeof grapesjs === 'undefined') {
|
||||
console.warn("[BRIDGE-API] blocks-api.js: BridgeParts (B) oder GrapesJS fehlt. Exit.");
|
||||
return;
|
||||
}
|
||||
|
||||
const PluginName = 'bridge-blocks-api';
|
||||
const qs = new URLSearchParams(location.search);
|
||||
B.EDITOR_MODE = (qs.get('mode') || 'templates').toUpperCase();
|
||||
console.log(`%c[${PluginName} - INIT] Editor Modus: ${B.EDITOR_MODE} (SCHRITT 16 - FINAL STABLE)`, 'color: #1E90FF; font-weight: bold;');
|
||||
|
||||
const TARGET_CAT_ID = 'custom';
|
||||
const PLACEHOLDER_ID = 'api-placeholder-loading';
|
||||
const REFERENCE_COMPONENT_TYPE = 'library-reference';
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (1) Kern-Logik: Platzhalter und Kategorien registrieren (SYNCHRON)
|
||||
// --------------------------------------------------------
|
||||
const preRegisterCategoriesAndPlaceholders = (editor) => {
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
bm.add(PLACEHOLDER_ID, {
|
||||
label: 'Lade Custom-Blöcke...',
|
||||
category: TARGET_CAT_ID,
|
||||
content: '<div style="padding: 10px; color: #1e3a8a; background-color: #eef2ff; border: 1px solid #c7d2fe; text-align: center;">⚙️ Custom-Blöcke werden geladen...</div>',
|
||||
attributes: { class: 'gjs-block__api-placeholder' },
|
||||
});
|
||||
|
||||
const cat = bm.getCategories().get(TARGET_CAT_ID);
|
||||
if (!cat) {
|
||||
bm.addCategory(TARGET_CAT_ID, { label: 'Custom', open: true, order: 1 });
|
||||
}
|
||||
console.log(`%c[${PluginName}] Platzhalter und Kategorie registriert.`, 'color: #008000;');
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (2) Komponenten-Logik (ASYNCHRONER WORKAROUND & FIX)
|
||||
// --------------------------------------------------------
|
||||
const registerReferenceComponent = (editor) => {
|
||||
const domc = editor.DomComponents;
|
||||
const defaultType = domc.getType('default');
|
||||
|
||||
if (!defaultType) return;
|
||||
|
||||
// KRITISCHER WORKAROUND: Registrierung wird minimal verzögert
|
||||
setTimeout(() => {
|
||||
domc.addType(REFERENCE_COMPONENT_TYPE, {
|
||||
model: defaultType.model.extend({
|
||||
|
||||
init() {
|
||||
// Setze die Attribute sicher im init() (Fix für "defaults" TypeError)
|
||||
if (this.get('type') !== REFERENCE_COMPONENT_TYPE) {
|
||||
this.set('type', REFERENCE_COMPONENT_TYPE);
|
||||
this.set('tagName', 'div');
|
||||
this.set('lib-kind', '');
|
||||
this.set('lib-id', '');
|
||||
}
|
||||
|
||||
this.on('change:lib-kind change:lib-id', this.reloadComponentContent);
|
||||
const editorInstance = this.em.get('Editor');
|
||||
if (editorInstance && this.get('lib-id')) {
|
||||
// Prüft hier nur auf getApiItem, da es für die Referenz-Komponente essenziell ist
|
||||
if(B.getApiItem) {
|
||||
editorInstance.on('load', this.reloadComponentContent.bind(this), { once: true });
|
||||
} else {
|
||||
console.warn(`[${PluginName}] B.getApiItem fehlt. Inhalte für 'library-reference' können nicht geladen werden.`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reloadComponentContent(opts = {}) {
|
||||
const kind = this.get('lib-kind');
|
||||
const id = this.get('lib-id');
|
||||
|
||||
if (!kind || !id) {
|
||||
this.set('content', '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: API-Referenz unvollständig.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!B.getApiItem) {
|
||||
this.set('content', '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: API-Kernfunktion getApiItem fehlt.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
B.getApiItem(kind, id)
|
||||
.then(item => {
|
||||
if (item && item.html) {
|
||||
this.set('content', item.html);
|
||||
console.log(`[${PluginName}] Geladenen Inhalt für ${kind}/${id} gesetzt.`);
|
||||
} else {
|
||||
this.set('content', `<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: Inhalt für ${kind}/${id} nicht gefunden.</div>`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[${PluginName}] Fehler beim Abruf von ${kind}/${id}:`, error);
|
||||
this.set('content', `<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler beim Laden von ${kind}/${id}.</div>`);
|
||||
});
|
||||
},
|
||||
}, {}),
|
||||
view: defaultType.view,
|
||||
});
|
||||
|
||||
console.log(`%c[${PluginName}] Komponententyp '${REFERENCE_COMPONENT_TYPE}' ASYNCHRON registriert.`, 'color: #008000;');
|
||||
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (3) Asynchrone Logik: API-Blöcke registrieren (FINAL CLEAN)
|
||||
// --------------------------------------------------------
|
||||
const loadAndRegisterApiBlocks = (editor) => {
|
||||
const bm = editor.BlockManager;
|
||||
const targetCatId = TARGET_CAT_ID;
|
||||
|
||||
// KRITISCHER FIX: Stelle sicher, dass der Platzhalter existiert.
|
||||
if (!bm.get(PLACEHOLDER_ID)) {
|
||||
bm.add(PLACEHOLDER_ID, {
|
||||
label: 'Lade Custom-Blöcke...',
|
||||
category: targetCatId,
|
||||
content: '<div style="padding: 10px; color: #1e3a8a; background-color: #eef2ff; border: 1px solid #c7d2fe; text-align: center;">⚙️ Custom-Blöcke werden geladen...</div>',
|
||||
attributes: { class: 'gjs-block__api-placeholder' },
|
||||
});
|
||||
console.log(`%c[${PluginName}] Platzhalter erneut hinzugefügt (Überlebens-Check).`, 'color: orange;');
|
||||
}
|
||||
|
||||
// 🛑 NEUER CHECK: Prüfe die fetch*-Funktionen erst HIER.
|
||||
if (!B.fetchSections || !B.fetchBlocks || !B.fetchSnippets) {
|
||||
console.error(`%c[${PluginName}] FEHLER: Eine der API-Ladefunktionen (fetchSections/Blocks/Snippets) fehlt.`, 'color: #dc3545; font-weight: bold;');
|
||||
// Platzhalter bleibt, da die Kategorie sonst verschwindet.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Explizite Promise.all mit allen fetch*-Funktionen
|
||||
Promise.all([
|
||||
B.fetchSections().then(items => items.map(i => ({ ...i, kind: 'sections' }))),
|
||||
B.fetchBlocks().then(items => items.map(i => ({ ...i, kind: 'blocks' }))),
|
||||
B.fetchSnippets().then(items => items.map(i => ({ ...i, kind: 'snippets' })))
|
||||
])
|
||||
.then(results => {
|
||||
// Führe alle Ergebnisse zu einem flachen Array zusammen
|
||||
const apiItems = results.flat().filter(item => item && item.id);
|
||||
|
||||
// Array-Ausgabe zur Bestätigung der Daten
|
||||
console.log(`%c[${PluginName}] API-Daten Array:`, 'color: #9400D3; font-weight: bold;', apiItems);
|
||||
|
||||
console.log(`%c[${PluginName}] API-Daten geladen: ${apiItems.length} Blöcke/Sektionen gefunden.`, 'color: #1E90FF; font-weight: bold;');
|
||||
|
||||
if (apiItems.length === 0) {
|
||||
// Platzhalter bleibt, um die leere Kategorie sichtbar zu halten.
|
||||
console.warn(`[${PluginName}] Keine API-Daten gefunden, Platzhalter bleibt (leer) erhalten.`);
|
||||
} else {
|
||||
apiItems.forEach(item => {
|
||||
const blockId = `lib-${item.kind}-${item.id}`;
|
||||
const label = item.name || item.label || 'Unbenannter Block';
|
||||
const itemKindUpper = item.kind.toUpperCase();
|
||||
|
||||
const blockDefinition = {
|
||||
label: label,
|
||||
category: targetCatId,
|
||||
content: {
|
||||
type: REFERENCE_COMPONENT_TYPE,
|
||||
attributes: { 'lib-kind': item.kind, 'lib-id': item.id },
|
||||
},
|
||||
attributes: { 'title': itemKindUpper },
|
||||
media: item.preview_url ? `<img src="${item.preview_url}">` : '',
|
||||
};
|
||||
bm.add(blockId, blockDefinition);
|
||||
});
|
||||
|
||||
// Platzhalter entfernen, da Blöcke erfolgreich geladen wurden
|
||||
bm.remove(PLACEHOLDER_ID);
|
||||
console.log(`%c[${PluginName}] ${apiItems.length} API-Blöcke registriert. Platzhalter entfernt.`, 'color: #008000; font-weight: bold;');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`%c[${PluginName}] FEHLER beim Laden der API-Blöcke:`, 'color: #dc3545; font-weight: bold;', error);
|
||||
// Platzhalter entfernen, um nicht im ewigen Ladezustand zu bleiben.
|
||||
bm.remove(PLACEHOLDER_ID);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (4) Plugin-Funktion
|
||||
// --------------------------------------------------------
|
||||
const plugin = (editor) => {
|
||||
preRegisterCategoriesAndPlaceholders(editor);
|
||||
registerReferenceComponent(editor);
|
||||
|
||||
editor.on('load', () => {
|
||||
console.log(`%c[${PluginName}] GrapesJS 'load' Event: Starte asynchrones Laden der API-Blöcke.`, 'color: #1E90FF; font-weight: bold;');
|
||||
loadAndRegisterApiBlocks(editor);
|
||||
});
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (5) Export an Bridge Core
|
||||
// --------------------------------------------------------
|
||||
if (B.registerGrapesJSPlugin) {
|
||||
B.registerGrapesJSPlugin(PluginName, plugin);
|
||||
}
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
270
public/assets/js/bridge/blocks-api.js
Normal file
270
public/assets/js/bridge/blocks-api.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/* /assets/js/bridge/blocks-api.js (UI-KERN UND KOMPONENTEN-SCHICHT) */
|
||||
|
||||
(function (B) {
|
||||
|
||||
const PluginName = 'bridge-blocks-api';
|
||||
|
||||
if (!B || typeof grapesjs === 'undefined') {
|
||||
console.warn(`%c[${PluginName}] %cBridgeParts (B) oder GrapesJS fehlt. Exit.`, 'color:orange; font-weight:bold;', 'color:inherit;');
|
||||
return;
|
||||
}
|
||||
|
||||
B.LOG_CONFIG = B.LOG_CONFIG || { PLUGINS: {} };
|
||||
B.LOG_CONFIG.PLUGINS[PluginName] = true;
|
||||
|
||||
const log = (message, color = '#1E90FF', type = 'info', force = false) => B.log(PluginName, message, color, type, force);
|
||||
|
||||
const qs = new URLSearchParams(location.search);
|
||||
B.EDITOR_MODE = (qs.get('mode') || 'templates').toUpperCase();
|
||||
log(`START: SKRIPT-AUSFÜHRUNG GESTARTET. Editor Modus: ${B.EDITOR_MODE}.`, '#DC143C');
|
||||
|
||||
const TARGET_CAT_ID = 'custom';
|
||||
const PLACEHOLDER_ID = 'api-placeholder-loading';
|
||||
const REFERENCE_COMPONENT_TYPE = 'library-reference';
|
||||
|
||||
// --- NEUE KONSTANTEN FÜR SPEICHERN-LOGIK ---
|
||||
// Annahme: ID der aktuellen Seite/Template ist global in B verfügbar
|
||||
const CURRENT_ENTITY_ID = B.CURRENT_ENTITY_ID || qs.get('id') || 0;
|
||||
// Annahme: Basis-URL der API ist in B verfügbar
|
||||
const API_KERNEL_URL = B.API_KERNEL_URL || '/api/ApiKernel.php';
|
||||
// -------------------------------------------
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (1) Kern-Logik: Platzhalter und Kategorien registrieren (SYNCHRON)
|
||||
// --------------------------------------------------------
|
||||
const preRegisterCategoriesAndPlaceholders = (editor) => {
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
bm.add(PLACEHOLDER_ID, {
|
||||
label: 'Lade Custom-Blöcke...',
|
||||
category: TARGET_CAT_ID,
|
||||
content: '<div style="padding: 10px; color: #1e3a8a; background-color: #eef2ff; border: 1px solid #c7d2fe; text-align: center;">⚙️ Custom-Blöcke werden geladen...</div>',
|
||||
attributes: { class: 'gjs-block__api-placeholder' },
|
||||
});
|
||||
|
||||
const cat = bm.getCategories().get(TARGET_CAT_ID);
|
||||
if (!cat) {
|
||||
bm.addCategory(TARGET_CAT_ID, { label: 'Custom', open: true, order: 1 });
|
||||
}
|
||||
log('Platzhalter und Kategorie registriert.', '#008000');
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (2) Komponenten-Logik (ASYNCHRONER WORKAROUND & FIX)
|
||||
// --------------------------------------------------------
|
||||
const registerReferenceComponent = (editor) => {
|
||||
const domc = editor.DomComponents;
|
||||
const defaultType = domc.getType('default');
|
||||
|
||||
if (!defaultType) return;
|
||||
|
||||
log(`Starte Registrierung des Komponententyps '${REFERENCE_COMPONENT_TYPE}'.`, '#1E90FF');
|
||||
|
||||
setTimeout(() => {
|
||||
domc.addType(REFERENCE_COMPONENT_TYPE, {
|
||||
model: defaultType.model.extend({
|
||||
|
||||
getCachedApiItem(kind, id) {
|
||||
const key = `${kind}-${id}`;
|
||||
const item = B.ApiItemCache?.[key];
|
||||
return item;
|
||||
},
|
||||
|
||||
init() {
|
||||
const id = this.get('lib-id');
|
||||
const kind = this.get('lib-kind');
|
||||
const startContent = this.get('startContent');
|
||||
|
||||
log(`INIT LÄUFT. lib-kind: ${kind}, lib-id: ${id}. (Bestätigung des Element-Drops/Load)`, '#8A2BE2');
|
||||
|
||||
if (startContent) {
|
||||
// 💡 NEUER FIX: Beim Drop nur die 'content'-Eigenschaft setzen, NICHT als Unterkomponenten parsen
|
||||
this.set('content', startContent);
|
||||
this.unset('startContent');
|
||||
log(`INHALT erfolgreich als REINES HTML aus 'startContent' gesetzt: ${kind}/${id}`, '#008000');
|
||||
}
|
||||
|
||||
this.on('change:lib-kind change:lib-id', this.reloadComponentContent);
|
||||
|
||||
if (!startContent && kind && id) {
|
||||
this.reloadComponentContent({ forced: true, reason: 'INIT_LOAD_FROM_CACHE' });
|
||||
}
|
||||
},
|
||||
|
||||
reloadComponentContent(opts = {}) {
|
||||
const kind = this.get('lib-kind');
|
||||
const id = this.get('lib-id');
|
||||
const reason = opts.reason || (opts.forced ? 'FORCED_INTERNAL' : 'EVENT_CHANGE');
|
||||
log(`RELOAD START (${reason}). Kind: ${kind}, ID: ${id}.`, '#8A2BE2');
|
||||
|
||||
if (!kind || !id) {
|
||||
log('RELOAD FEHLER: lib-kind oder lib-id fehlt. Setze Fehler-Placeholder.', '#dc3545', 'error', true);
|
||||
// 💡 FIX: Setze reinen HTML-String als content
|
||||
this.set('content', '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: API-Referenz unvollständig.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.getCachedApiItem(kind, id);
|
||||
|
||||
if (item && (item.html || item.content)) {
|
||||
const content = item.html || item.content;
|
||||
// 💡 FIX: Verwende set('content', ...) statt components(...)
|
||||
// Dadurch wird der Inhalt als reiner HTML-String in die Komponente gesetzt
|
||||
// und nicht als neue, bearbeitbare GrapesJS-Komponenten geparst.
|
||||
this.set('content', content);
|
||||
log(`INHALT erfolgreich für ${kind}/${id} geladen und als REINER HTML-STRING gesetzt.`, '#008000');
|
||||
} else {
|
||||
log(`RELOAD FEHLER: Inhalt für ${kind}/${id} NICHT im Cache gefunden.`, '#dc3545', 'error', true);
|
||||
// 💡 FIX: Setze reinen HTML-HTML-String als content
|
||||
this.set('content', `<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: Inhalt für ${kind}/${id} nicht im Cache gefunden.</div>`);
|
||||
}
|
||||
},
|
||||
}, {
|
||||
isComponent: el => el && el.nodeType === 1 && el.hasAttribute('lib-id'),
|
||||
extend: 'default',
|
||||
model: {
|
||||
defaults: {
|
||||
...defaultType.model.prototype.defaults,
|
||||
// 🛑 KRITISCHE FIXES FÜR REFERENZEN
|
||||
components: '', // Darf keine Unterkomponenten haben, die geparst werden
|
||||
editable: false, // ❌ Nicht bearbeitbar (Inline-Editierung verhindern)
|
||||
removable: true,
|
||||
draggable: true,
|
||||
copyable: true,
|
||||
droppable: false, // ❌ Darf keine anderen Komponenten aufnehmen
|
||||
// ---------------------------------
|
||||
traits: [
|
||||
{ type: 'text', name: 'lib-id', label: 'Library ID', changeProp: true },
|
||||
{ type: 'text', name: 'lib-kind', label: 'Library Kind', changeProp: true },
|
||||
],
|
||||
'lib-id': '',
|
||||
'lib-kind': '',
|
||||
startContent: '',
|
||||
content: '', // Inhalt, der das gerenderte HTML hält
|
||||
}
|
||||
}
|
||||
}),
|
||||
// 💡 WICHTIG: Die View muss den Content als reinen HTML-Inhalt rendern (defaultType macht das).
|
||||
view: defaultType.view,
|
||||
});
|
||||
|
||||
log(`Komponententyp '${REFERENCE_COMPONENT_TYPE}' ASYNCHRON registriert.`, '#008000');
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (3) HINZUGEFÜGT: Speichern-Befehl (Command)
|
||||
// --------------------------------------------------------
|
||||
const registerSaveCommand = (editor) => {
|
||||
|
||||
editor.Commands.add('save-data', {
|
||||
run: function(editor, sender) {
|
||||
// 💡 FIX: Sicherstellen, dass sender existiert und die 'set'-Methode hat (nur bei Buttons)
|
||||
if (sender && typeof sender.set === 'function') {
|
||||
sender.set('active', 0); // Schaltet den Button nach dem Klick ab
|
||||
}
|
||||
|
||||
if (!CURRENT_ENTITY_ID) {
|
||||
log('SAVE ABORT', 'Speichern abgebrochen: Keine Entity ID verfügbar (B.CURRENT_ENTITY_ID fehlt oder ist 0).', 'red', 'error', true);
|
||||
alert('Speichern fehlgeschlagen: Die ID des aktuellen Elements fehlt.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Daten extrahieren
|
||||
const htmlContent = editor.getHtml() + '<style>' + editor.getCss() + '</style>';
|
||||
// 2. KRITISCH: Holt die JSON-Repräsentation des Editors
|
||||
const jsonProjectData = editor.getProjectData();
|
||||
|
||||
log('SAVE START', 'Starte Speichern des Inhalts an die API...', '#FF4500');
|
||||
|
||||
// 3. Daten für den POST-Request vorbereiten
|
||||
const dataToSend = {
|
||||
action: 'blocks.update', // Oder 'templates.update', je nach Entity
|
||||
id: CURRENT_ENTITY_ID,
|
||||
html: htmlContent,
|
||||
// 🚨 KRITISCH: Korrigiert auf 'json_content' für das PHP-Backend
|
||||
json_content: jsonProjectData,
|
||||
name: B.CURRENT_ENTITY_NAME || 'Unbenannt', // Optional
|
||||
};
|
||||
|
||||
// 4. API-Aufruf (fetch)
|
||||
fetch(API_KERNEL_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
// Wichtig: JSON-Daten senden
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(dataToSend),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
log('SAVE FAILED (HTTP)', `Speichern fehlgeschlagen: HTTP-Status ${response.status}.`, 'red', 'error', true);
|
||||
throw new Error(`HTTP Error: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.ok === false) {
|
||||
log('SAVE FAILED (API)', `Speichern fehlgeschlagen: API-Fehler: ${data.error || 'Unbekannt'}`, 'red', 'error', true);
|
||||
alert(`Speichern fehlgeschlagen: ${data.error || 'API-Fehler'}`);
|
||||
} else {
|
||||
log('SAVE SUCCESS', 'Speichern erfolgreich. JSON-Daten wurden gesendet.', '#008000', 'info', true);
|
||||
// 💡 HINZUGEFÜGT: Bestätigung an das Elternfenster senden
|
||||
window.parent.postMessage({ source: 'editor', type: 'save:success' }, '*');
|
||||
editor.refresh(); // Optional: Editor-Ansicht aktualisieren
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
log('SAVE FAILED (FETCH)', `FEHLER beim Speichern: ${error.message}`, 'red', 'error', true);
|
||||
alert('Speichern fehlgeschlagen. Netzwerk- oder JSON-Parse-Fehler.');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Eventuell den Button in der Toolbar registrieren (falls noch nicht geschehen)
|
||||
editor.Panels.addButton('options', {
|
||||
id: 'save-data',
|
||||
className: 'fa fa-floppy-o',
|
||||
command: 'save-data',
|
||||
attributes: { title: 'Speichern (Strg/Cmd + S)' }
|
||||
});
|
||||
|
||||
// Tastenkürzel für Speichern hinzufügen
|
||||
editor.Keymaps.add('ctrl-s', 'save-data', 'ctrl+s');
|
||||
editor.Keymaps.add('cmd-s', 'save-data', 'cmd+s');
|
||||
|
||||
log('Speichern-Command und Button/Keymap registriert.', '#FF4500');
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (4) Plugin-Funktion (AKTUALISIERT)
|
||||
// --------------------------------------------------------
|
||||
const plugin = (editor) => {
|
||||
preRegisterCategoriesAndPlaceholders(editor);
|
||||
registerReferenceComponent(editor);
|
||||
registerSaveCommand(editor); // HINZUGEFÜGT: Speichern-Logik
|
||||
|
||||
editor.on('load', () => {
|
||||
log("GrapesJS 'load' Event: Delegiere asynchrones Laden der API-Blöcke an library-api.", '#1E90FF');
|
||||
if (B.loadAndRegisterApiBlocks) {
|
||||
setTimeout(() => {
|
||||
B.loadAndRegisterApiBlocks(editor);
|
||||
}, 500);
|
||||
} else {
|
||||
log(`FEHLER: B.loadAndRegisterApiBlocks ist nicht definiert. library-api.js wurde nicht geladen oder nicht richtig initialisiert.`, 'red', 'error', true);
|
||||
editor.BlockManager.remove(PLACEHOLDER_ID);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// (5) Export an Bridge Core (unverändert)
|
||||
// --------------------------------------------------------
|
||||
if (B.registerGrapesJSPlugin) {
|
||||
B.registerGrapesJSPlugin(PluginName, plugin);
|
||||
log(`PLUGIN REGISTER: '${PluginName}' zur Bridge Plugin Registry hinzugefügt.`, '#008000');
|
||||
} else {
|
||||
log(`FEHLER: B.registerGrapesJSPlugin fehlt. Plugin-Registrierung gescheitert.`, 'red', 'error', true);
|
||||
}
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
131
public/assets/js/bridge/blocks-custom.js
Normal file
131
public/assets/js/bridge/blocks-custom.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/* /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(';');
|
||||
|
||||
function register(editor) {
|
||||
log('EXECUTION', `Starte Block-Registrierung für ${TARGET_CAT_ID}.`, '#DAA520');
|
||||
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
// --- Custom-Blöcke DEFINIEREN ---
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
})();
|
||||
139
public/assets/js/bridge/blocks-standard.js
Normal file
139
public/assets/js/bridge/blocks-standard.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/* /assets/js/bridge/blocks-standard.js (FINAL & LOG-KONTROLLIERT) */
|
||||
(function () {
|
||||
|
||||
const PluginName = 'blocks-standard';
|
||||
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 = '#0000FF', 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;');
|
||||
} else {
|
||||
// Fallback für sonstige Logs
|
||||
console.log(`%c[${PluginName} - ${type}] %c${message}`, `color:${color}; font-weight:bold;`, 'color:inherit;');
|
||||
}
|
||||
};
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
log('FILE CHECK', 'Datei-IIFE startet.');
|
||||
|
||||
// Kritische Prüfung, um doppelte Ausführung zu verhindern
|
||||
if (window.__STANDARD_BLOCKS_LOADED) return;
|
||||
window.__STANDARD_BLOCKS_LOADED = true;
|
||||
|
||||
const TARGET_CAT_ID = 'mysnips';
|
||||
const TARGET_CAT_LABEL = 'Bibliothek';
|
||||
const ALL_STANDARD_BLOCK_IDS = [];
|
||||
const css = o => Object.entries(o).map(([k,v]) => `${k}:${v}`).join(';');
|
||||
|
||||
/**
|
||||
* Fügt einen Block hinzu oder aktualisiert ihn SICHER
|
||||
*/
|
||||
function addOrUpdate(bm, id, def) {
|
||||
if (bm.get(id)) {
|
||||
bm.remove(id);
|
||||
log('UPDATE', `Entferne alte Block-Definition: ${id}`, 'gray');
|
||||
}
|
||||
|
||||
const finalDef = {
|
||||
...def,
|
||||
category: TARGET_CAT_ID,
|
||||
force: true
|
||||
};
|
||||
|
||||
try {
|
||||
bm.add(id, finalDef);
|
||||
} catch (e) {
|
||||
log('CRITICAL ERROR', `KRITISCHER FEHLER beim Hinzufügen von '${id}': ${e.message}`, 'red', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
ALL_STANDARD_BLOCK_IDS.push(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die eigentliche Plugin-Funktion, die von GrapesJS/Bridge aufgerufen wird.
|
||||
*/
|
||||
const pluginFunction = (editor) => {
|
||||
// Aggressiver Log zur Prüfung der Ausführung
|
||||
log('EXECUTION CHECK', `Starte Block-Registrierung für ${TARGET_CAT_ID}.`, '#993300');
|
||||
|
||||
if (!editor || !editor.BlockManager) {
|
||||
log('EXECUTION CHECK', 'Fehler: GrapesJS Editor Instanz ist ungültig.', 'red', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
// =======================================================
|
||||
// I. GRAPESJS DEFAULT BLÖCKE (ALLE STANDARD ELEMENTE)
|
||||
// =======================================================
|
||||
|
||||
// TEXT (Registriert als 'std-text')
|
||||
addOrUpdate(bm, 'std-text', { label:'Text (Basis)',
|
||||
content:`<div data-gjs-type="text" style="${css({'font-family':'Arial,sans-serif','font-size':'14px',color:'#0f172a',margin:'0 0 12px'})}">Absatztext.</div>` });
|
||||
|
||||
// IMAGE (Registriert als 'std-image')
|
||||
addOrUpdate(bm, 'std-image', { label:'Bild (Basis)',
|
||||
content:`<img data-gjs-type="image" src="https://placehold.co/600x300" alt="Bild" style="${css({width:'100%',height:'auto','max-width':'600px',border:'0',display:'block'})}">` });
|
||||
|
||||
// LINK (Registriert als 'std-link')
|
||||
addOrUpdate(bm, 'std-link', { label:'Link (Basis)',
|
||||
content:`<a href="#" data-gjs-type="link" style="${css({color:'#0ea5e9','text-decoration':'none','font-family':'Arial,sans-serif','font-size':'14px'})}">Hyperlink</a>` });
|
||||
|
||||
// SECTION (Registriert als 'std-section')
|
||||
addOrUpdate(bm, 'std-section', { label:'Sektion',
|
||||
content:`<section style="${css({padding:'20px'})}" data-gjs-type="section">
|
||||
<div style="${css({'font-family':'Arial,sans-serif','font-size':'14px',color:'#0f172a'})}">Inhalt der Sektion.</div>
|
||||
</section>` });
|
||||
|
||||
// COLUMN (Registriert als 'std-column')
|
||||
addOrUpdate(bm, 'std-column', { label:'Spalte',
|
||||
content:`<div style="${css({padding:'10px','min-height':'50px','border':'1px dashed #ccc'})}" data-gjs-type="column">
|
||||
<div style="${css({'font-family':'Arial,sans-serif','font-size':'12px',color:'#555'})}">Spalteninhalt</div>
|
||||
</div>` });
|
||||
|
||||
// BUTTON (Registriert als 'std-button')
|
||||
addOrUpdate(bm, 'std-button', { label:'Button (Basis)',
|
||||
content:`<a href="#" data-gjs-type="button" 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'})}">Button</a>` });
|
||||
|
||||
// DIVIDER (Registriert als 'std-divider')
|
||||
addOrUpdate(bm, 'std-divider',{ label:'Trenner (Basis)',
|
||||
content:`<hr data-gjs-type="divider" style="${css({border:'0',height:'1px','background-color':'#e2e8f0',margin:'16px 0'})}">` });
|
||||
|
||||
// MAP (Registriert als 'std-map')
|
||||
addOrUpdate(bm, 'std-map', { label:'Karte',
|
||||
content:`<iframe data-gjs-type="map" src="https://maps.google.com/maps?width=100%25&height=600&hl=de&q=Berlin&t=&z=14&ie=UTF8&iwloc=B&output=embed" width="100%" height="300" frameborder="0" style="${css({'border':'0',width:'100%',height:'300px'})}"></iframe>` });
|
||||
|
||||
// Löst die notwendigen Events für den Bridge Core / Cleanup aus.
|
||||
editor.trigger('block:add');
|
||||
|
||||
log('SUCCESS', `Erfolgreich ${ALL_STANDARD_BLOCK_IDS.length} Standardblöcke in Kategorie '${TARGET_CAT_LABEL}' registriert.`, '#008000', 'info', true);
|
||||
};
|
||||
|
||||
// Exportiere für den manuellen Aufruf in bridge-core.js
|
||||
window.BridgeBlocksStandard = {
|
||||
IDS: ALL_STANDARD_BLOCK_IDS,
|
||||
register: pluginFunction,
|
||||
};
|
||||
|
||||
// Registriere das Modul als GrapesJS Plugin (für den Fall, dass es doch anderswo benötigt wird)
|
||||
if (B && B.registerGrapesJSPlugin) {
|
||||
B.registerGrapesJSPlugin('bridge-blocks-standard', window.BridgeBlocksStandard.register);
|
||||
log('PLUGIN REGISTER', `'bridge-blocks-standard' erfolgreich zur Bridge Plugin Registry hinzugefügt.`, '#008000');
|
||||
} else {
|
||||
log('CRITICAL ERROR', `BridgeParts oder registerGrapesJSPlugin fehlt! Plugin-Registrierung gescheitert.`, 'red', 'error');
|
||||
}
|
||||
|
||||
})();
|
||||
265
public/assets/js/bridge/categorization-cleanup.js
Normal file
265
public/assets/js/bridge/categorization-cleanup.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/* /assets/js/bridge/categorization-cleanup.js (FINAL & LOG-KONTROLLIERT) */
|
||||
(function(B){
|
||||
|
||||
if (!B || typeof grapesjs === 'undefined') return;
|
||||
|
||||
// 🛑 NEUER NAME: Dies wird das Plugin in GrapesJS registrieren
|
||||
const PluginName = 'bridge-categorization-cleanup';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🎯 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 = '#228B22', logType = 'info', force = false) => {
|
||||
// Wir verwenden B.log, das die B.LOG_CONFIG.PLUGINS[PluginName] prüft
|
||||
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 (sollte nicht passieren)
|
||||
console.error(`%c[${PluginName} - ${type}] %c${message}`, `color:red; font-weight:bold;`, 'color:inherit;');
|
||||
}
|
||||
};
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// 🛑 WICHTIG: Liste aller unerwünschten IDs/Labels
|
||||
const UNWANTED_UNCATEGORIZED_ID = 'Uncategorized';
|
||||
// Fügen Sie die gängigen IDs des gjs-preset-newsletter hinzu
|
||||
const PRESET_UNWANTED_IDS = ['Basic', 'Layout', 'Extra', 'Components', 'Forms'];
|
||||
|
||||
// Alle IDs, die gelöscht werden müssen. Enthält NICHT mehr 'Bibliothek'.
|
||||
const ALL_FORBIDDEN_CAT_IDS = [UNWANTED_UNCATEGORIZED_ID, ...PRESET_UNWANTED_IDS];
|
||||
|
||||
const UNWANTED_BLOCK_ID = 'gjs-lbr-block-disabled';
|
||||
const UNWANTED_BLOCK_LABEL = 'Bibliothek-disabled';
|
||||
|
||||
const FALLBACK_CATEGORY_ID = 'mysnips';
|
||||
|
||||
const CUSTOM_BLOCK_IDS = (window.BridgeBlocksCustom && window.BridgeBlocksCustom.IDS) || [];
|
||||
|
||||
let normalizationRunCount = 0;
|
||||
let normalizationIsRunning = false;
|
||||
let maxNormalizationRuns = 5;
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// HILFSFUNKTION: Entfernt hartnäckige Kategorie-DOM-Elemente
|
||||
// ----------------------------------------------------------------------
|
||||
const zapUnwantedCategoryDom = (editor) => {
|
||||
const blocksPanelEl = editor.BlockManager.getContainer();
|
||||
if (blocksPanelEl) {
|
||||
let removedCount = 0;
|
||||
blocksPanelEl.querySelectorAll('.gjs-block-cat').forEach(catEl => {
|
||||
const catTitleEl = catEl.querySelector('.gjs-title');
|
||||
|
||||
if (catTitleEl) {
|
||||
const catTitle = catTitleEl.textContent.trim();
|
||||
// Prüft, ob der Titel eine der unerwünschten IDs ist (z.B. 'Basic')
|
||||
if (ALL_FORBIDDEN_CAT_IDS.includes(catTitle)) {
|
||||
catEl.remove();
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (removedCount > 0) {
|
||||
log('DOM FIX', `${removedCount} unerwünschte Kategorie-DOM-Elemente entfernt.`, 'orange', 'warn');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Hilfsfunktion: Erzwingt das Neu-Rendern der Block-View
|
||||
// ----------------------------------------------------------------------
|
||||
const renderBlocks = (editor) => {
|
||||
zapUnwantedCategoryDom(editor);
|
||||
log('RENDER', 'DOM-Cleanup ausgeführt.', 'green');
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 1. Funktion zum Ausblenden/Normalisieren der Kategorien (Kernlogik)
|
||||
// ----------------------------------------------------------------------
|
||||
const normalizeCategories = (editor) => {
|
||||
if (normalizationIsRunning || normalizationRunCount >= maxNormalizationRuns) {
|
||||
if (normalizationRunCount >= maxNormalizationRuns) {
|
||||
log('SKIP', `normalizeCategories übersprungen: Maximale Läufe (${maxNormalizationRuns}) erreicht.`, 'red', 'warn');
|
||||
} else {
|
||||
log('SKIP', 'normalizeCategories übersprungen: Läuft bereits.', 'red', 'warn');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
normalizationIsRunning = true;
|
||||
normalizationRunCount++;
|
||||
// Nur das Start-Log kann eine Gruppen-Markierung sein
|
||||
log('START', `Starte normalizeCategories Run #${normalizationRunCount}`, '#191970');
|
||||
|
||||
const bm = editor.BlockManager;
|
||||
const config = B.CATEGORY_CONFIG || {};
|
||||
const configuredCategoryIds = Object.keys(config);
|
||||
|
||||
log('CONFIG', `Konfigurierte Kategorie-IDs: ${configuredCategoryIds.join(', ')}`, '#555555');
|
||||
|
||||
// --- A. Explizites Erstellen der Kategorien (Sicherheits-Fallback) ---
|
||||
const catsToEnsure = new Set(configuredCategoryIds);
|
||||
|
||||
catsToEnsure.forEach(catId => {
|
||||
const catConf = config[catId];
|
||||
if (!catConf) return;
|
||||
|
||||
if (!bm.getCategories().get(catId)) {
|
||||
bm.getCategories().add({
|
||||
id: catId,
|
||||
label: catConf.label,
|
||||
open: catConf.open !== false,
|
||||
order: catConf.ord || 999
|
||||
});
|
||||
log('CAT FALLBACK', `Kategorie '${catId}' fehlte und wurde JETZT erstellt!`, 'red', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- B. Zwangszuweisung der Blöcke und Bereinigung von Blöcken ---
|
||||
bm.getAll().each(block => {
|
||||
const id = block.get('id');
|
||||
const label = block.get('label');
|
||||
let catId = block.get('category');
|
||||
|
||||
if (typeof catId === 'object' && catId.id) {
|
||||
catId = catId.id;
|
||||
}
|
||||
|
||||
// 1. Lösche unerwünschten hartnäckigen Block (DEAKTIVIERT)
|
||||
if (id === UNWANTED_BLOCK_ID || label === UNWANTED_BLOCK_LABEL) {
|
||||
// ... (Block removal logic commented out)
|
||||
// log('BLOCK REMOVE', `Lösche unerwünschten Block: ${id}`, 'red', 'warn');
|
||||
// bm.remove(id);
|
||||
}
|
||||
|
||||
// 2. Setze Blöcke ohne oder mit unerwünschter/unbekannter Kategorie auf den Fallback (mysnips)
|
||||
const isUnconfiguredOrForbidden = !catId || !configuredCategoryIds.includes(catId) || ALL_FORBIDDEN_CAT_IDS.includes(catId);
|
||||
|
||||
if (isUnconfiguredOrForbidden) {
|
||||
if (id) {
|
||||
log('BLOCK FIX', `Block '${id}' ('${label}') verschoben nach '${FALLBACK_CATEGORY_ID}' (Ursprüngliche Kat ID: ${catId || 'keine/leer'}).`, 'orange', 'warn');
|
||||
block.set('category', FALLBACK_CATEGORY_ID);
|
||||
}
|
||||
} else {
|
||||
log('BLOCK OK', `Block '${id}' ('${label}') bleibt in Kategorie '${catId}'.`, 'green');
|
||||
}
|
||||
|
||||
// 3. Custom Blocks schützen
|
||||
if (CUSTOM_BLOCK_IDS.includes(id) && catId !== 'bausteine') {
|
||||
log('BLOCK FIX', `Custom Block '${id}' auf 'bausteine' korrigiert.`, 'orange', 'warn');
|
||||
block.set('category', 'bausteine');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- C. Kategorien erzwingen, Label korrigieren und Löschen von Modellen ---
|
||||
const categories = bm.getCategories().models || bm.getCategories();
|
||||
let visibleCategories = [];
|
||||
|
||||
categories.forEach(catModel => {
|
||||
const catId = catModel.get('id');
|
||||
const catConf = config[catId];
|
||||
|
||||
// 1. Aggressives Löschen von unerwünschten Preset-Kategorien
|
||||
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
|
||||
log('CAT REMOVE', `Lösche unerwünschtes Category Model: ${catId} (Da in ALL_FORBIDDEN_CAT_IDS).`, 'red', 'error');
|
||||
bm.getCategories().remove(catModel);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeConf = catConf;
|
||||
|
||||
// 2. Finde eine existierende, aber nicht konfigurierte Kategorie, und blende sie aus
|
||||
if (!activeConf && catId) {
|
||||
log('CAT HIDE', `Kategorie '${catId}' existiert, ist aber nicht in CATEGORY_CONFIG. Wird ausgeblendet.`, 'orange', 'warn');
|
||||
catModel.set('visible', false);
|
||||
catModel.set('open', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Korrigiere Label, Sortierung und Sichtbarkeit (Konfigurierte Kategorien)
|
||||
if (activeConf) {
|
||||
const oldLabel = catModel.get('label');
|
||||
const newLabel = activeConf.label;
|
||||
const visibility = true;
|
||||
|
||||
if (oldLabel !== newLabel) {
|
||||
log('CAT UPDATE', `Korrigiere Label von '${catId}' von '${oldLabel}' auf '${newLabel}'.`, '#00BFFF');
|
||||
catModel.set('label', newLabel, { silent: true });
|
||||
}
|
||||
|
||||
catModel.set('visible', visibility);
|
||||
catModel.set('open', activeConf.open !== false);
|
||||
catModel.set('order', activeConf.ord || 999);
|
||||
visibleCategories.push(catId);
|
||||
|
||||
log('CAT FINAL', `Kategorie '${catId}' auf Visible: ${visibility}, Order: ${catModel.get('order')}.`, 'green');
|
||||
}
|
||||
});
|
||||
|
||||
// --- D. Cleanup und Neu-Sortierung erzwingen ---
|
||||
categories.sort((a, b) => (a.get('order') || 999) - (b.get('order') || 999));
|
||||
|
||||
B.sortBlocksByPrefixAndLabel && B.sortBlocksByPrefixAndLabel(bm.getAll().models);
|
||||
|
||||
// DOM Cleanup wird über renderBlocks aufgerufen
|
||||
renderBlocks(editor);
|
||||
|
||||
log('END', `Kategorisierung abgeschlossen. Sichtbare Kategorien (Modelle): ${visibleCategories.sort().join(', ')}.`, 'green', 'info', true); // FINAL Log ist forced=true für Abschlussmeldung
|
||||
|
||||
normalizationIsRunning = false;
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// GrapesJS Plugin Registrierung
|
||||
// ----------------------------------------------------------------------
|
||||
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
// 1. Initialer, verspäteter Lauf bei Ladevorgang
|
||||
editor.on('load', () => {
|
||||
setTimeout(() => {
|
||||
log('FINAL RUN', `Starte finalen Normalisierungslauf nach 2500ms.`, 'orange', 'warn');
|
||||
normalizeCategories(editor);
|
||||
}, 2500);
|
||||
});
|
||||
|
||||
// 2. WATCHDOG gegen Label-Überschreibung oder unerwünschte Adds
|
||||
bm.getCategories().on('add change:label', (categoryModel) => {
|
||||
const catId = categoryModel.get('id');
|
||||
const newLabel = categoryModel.get('label');
|
||||
const expectedLabel = B.CATEGORY_CONFIG?.[catId]?.label;
|
||||
|
||||
// WATCHDOG-ADD
|
||||
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
|
||||
log('WATCHDOG-ADD', `Unerwünschte Kategorie '${catId}' wurde hinzugefügt! Starte Sofort-Korrektur.`, 'red', 'error');
|
||||
bm.getCategories().remove(categoryModel);
|
||||
setTimeout(() => normalizeCategories(editor), 1);
|
||||
}
|
||||
|
||||
// WATCHDOG-LABEL
|
||||
if (expectedLabel && newLabel !== expectedLabel) {
|
||||
log('WATCHDOG-CHANGE', `Externe Label-Manipulation von '${catId}' erkannt: Korrigiere von '${newLabel}' auf '${expectedLabel}'.`, 'orange', 'warn');
|
||||
categoryModel.set('label', expectedLabel, { silent: true });
|
||||
setTimeout(() => normalizeCategories(editor), 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Exporte beibehalten, falls sie in bridge-core.js verwendet werden
|
||||
B.normalizeCategories = normalizeCategories;
|
||||
B.renderBlocks = renderBlocks;
|
||||
|
||||
log('INIT', 'Master-Koordinator registriert.', '#008080');
|
||||
});
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
@@ -0,0 +1,239 @@
|
||||
/* /assets/js/bridge/categorization-master.js (FINALE KORREKTUR V3: Entfernt aggressives bm.render()) */
|
||||
(function(B){
|
||||
|
||||
if (!B || typeof grapesjs === 'undefined') return;
|
||||
|
||||
const PluginName = 'bridge-categorization-master';
|
||||
|
||||
// 🛑 WICHTIG: Liste aller unerwünschten IDs/Labels
|
||||
const UNWANTED_CATEGORY_ID = 'Bibliothek';
|
||||
const UNWANTED_UNCATEGORIZED_ID = 'Uncategorized';
|
||||
// Fügen Sie die gängigen IDs des gjs-preset-newsletter hinzu
|
||||
const PRESET_UNWANTED_IDS = ['Basic', 'Layout', 'Extra', 'Components', 'Forms'];
|
||||
|
||||
// Alle IDs, die gelöscht werden müssen
|
||||
const ALL_FORBIDDEN_CAT_IDS = [UNWANTED_CATEGORY_ID, UNWANTED_UNCATEGORIZED_ID, ...PRESET_UNWANTED_IDS];
|
||||
|
||||
const UNWANTED_BLOCK_ID = 'gjs-lbr-block';
|
||||
const UNWANTED_BLOCK_LABEL = 'Bibliothek';
|
||||
|
||||
const FALLBACK_CATEGORY_ID = 'mysnips';
|
||||
|
||||
const CUSTOM_BLOCK_IDS = (window.BridgeBlocksCustom && window.BridgeBlocksCustom.IDS) || [];
|
||||
|
||||
let normalizationRunCount = 0;
|
||||
let normalizationIsRunning = false;
|
||||
let maxNormalizationRuns = 5;
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// HILFSFUNKTION: Entfernt hartnäckige Kategorie-DOM-Elemente
|
||||
// ----------------------------------------------------------------------
|
||||
const zapUnwantedCategoryDom = (editor) => {
|
||||
const blocksPanelEl = editor.Panels.getPanel('blocks')?.get('el');
|
||||
if (blocksPanelEl) {
|
||||
let removedCount = 0;
|
||||
blocksPanelEl.querySelectorAll('.gjs-block-cat').forEach(catEl => {
|
||||
const catTitleEl = catEl.querySelector('.gjs-title');
|
||||
|
||||
if (catTitleEl) {
|
||||
const catTitle = catTitleEl.textContent.trim();
|
||||
// Prüfe auf unerwünschte Titel (sowohl Standard als auch Presets)
|
||||
if (ALL_FORBIDDEN_CAT_IDS.includes(catTitle) || catTitle === UNWANTED_UNCATEGORIZED_ID || catTitle === UNWANTED_CATEGORY_ID) {
|
||||
catEl.remove();
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (removedCount > 0) {
|
||||
console.warn(`[${PluginName}][DOM Fix] ${removedCount} unerwünschte Kategorie-DOM-Elemente entfernt.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Hilfsfunktion: Erzwingt das Neu-Rendern der Block-View (NUR DOM-CLEANUP)
|
||||
// ----------------------------------------------------------------------
|
||||
const renderBlocks = (editor) => {
|
||||
// 🛑 KRITISCHE KORREKTUR: Entferne bm.render() – nur DOM-Cleanup ist hier nötig,
|
||||
// da das Setzen der Model-Eigenschaften (label, visible) das Rendering übernehmen sollte.
|
||||
zapUnwantedCategoryDom(editor);
|
||||
console.log(`[${PluginName}][Render] DOM-Cleanup ausgeführt.`);
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 1. Funktion zum Ausblenden/Normalisieren der Kategorien (Kernlogik)
|
||||
// ----------------------------------------------------------------------
|
||||
const normalizeCategories = (editor) => {
|
||||
if (normalizationIsRunning || normalizationRunCount >= maxNormalizationRuns) {
|
||||
if (normalizationRunCount >= maxNormalizationRuns) {
|
||||
console.warn(`[${PluginName}] normalizeCategories übersprungen: Maximale Läufe (${maxNormalizationRuns}) erreicht.`);
|
||||
} else {
|
||||
console.warn(`[${PluginName}] normalizeCategories übersprungen: Läuft bereits.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
normalizationIsRunning = true;
|
||||
normalizationRunCount++;
|
||||
console.group(`[${PluginName}] normalizeCategories Run #${normalizationRunCount}`);
|
||||
|
||||
const bm = editor.BlockManager;
|
||||
const config = B.CATEGORY_CONFIG || {};
|
||||
const configuredCategoryIds = Object.keys(config);
|
||||
|
||||
|
||||
// 🛑 NEUER FALLBACK-FIX: Stellen Sie sicher, dass alle konfigurierten Kategorien existieren,
|
||||
// bevor Blöcke zugewiesen werden.
|
||||
configuredCategoryIds.forEach(catId => {
|
||||
const catConf = config[catId];
|
||||
if (!bm.getCategories().get(catId)) {
|
||||
bm.getCategories().add({
|
||||
id: catId,
|
||||
label: catConf.label,
|
||||
open: catConf.open !== false,
|
||||
order: catConf.ord || 999
|
||||
});
|
||||
// Nur als Warnung, da dies bei 'mysnips' oft passiert, wenn es leer ist.
|
||||
console.warn(`[${PluginName}][Cat Fallback] Kategorie '${catId}' wurde nachträglich erstellt.`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- A. Zwangszuweisung der Blöcke und Bereinigung von Blöcken ---
|
||||
bm.getAll().each(block => {
|
||||
const id = block.get('id');
|
||||
const label = block.get('label');
|
||||
let catId = block.get('category');
|
||||
|
||||
if (typeof catId === 'object' && catId.id) {
|
||||
catId = catId.id; // Behandle Category-Objekte
|
||||
}
|
||||
|
||||
// 1. Lösche unerwünschten hartnäckigen Block (z.B. gjs-lbr-block)
|
||||
if (id === UNWANTED_BLOCK_ID || label === UNWANTED_BLOCK_LABEL) {
|
||||
console.log(`[${PluginName}][Block Fix] Unerwünschter Block '${id}' ('${label}') entfernt.`);
|
||||
bm.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Setze Blöcke ohne oder mit unerwünschter/unbekannter Kategorie auf den Fallback (mysnips)
|
||||
// HINWEIS: 'custom' ist in configuredCategoryIds, falls es noch keine Blöcke von der API hat.
|
||||
if (!catId || !configuredCategoryIds.includes(catId) || ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
|
||||
// Nur wenn der Block nicht leer ist
|
||||
if (id) {
|
||||
console.log(`[${PluginName}][Block Fix] Block '${id}' ('${label}') verschoben nach '${FALLBACK_CATEGORY_ID}' (von Kat: ${catId || 'keine'}).`);
|
||||
block.set('category', FALLBACK_CATEGORY_ID);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Custom Blocks schützen
|
||||
if (CUSTOM_BLOCK_IDS.includes(id) && catId !== 'bausteine') {
|
||||
block.set('category', 'bausteine');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- B. Kategorien erzwingen, Label korrigieren und Löschen von Modellen ---
|
||||
const categories = bm.getCategories().models || bm.getCategories();
|
||||
let visibleCategories = [];
|
||||
|
||||
// Gehe alle Category Models durch
|
||||
categories.forEach(catModel => {
|
||||
const catId = catModel.get('id');
|
||||
const catConf = config[catId];
|
||||
|
||||
// Aggressives Löschen von unerwünschten Preset-Kategorien
|
||||
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
|
||||
console.warn(`[${PluginName}][Cat Fix] Lösche unerwünschtes Category Model: ${catId}`);
|
||||
bm.getCategories().remove(catModel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Finde eine existierende, aber nicht konfigurierte Kategorie, und blende sie aus
|
||||
// WICHTIG: mysnips ist der FALLBACK_CATEGORY_ID, hier darf es NICHT ausgeblendet werden.
|
||||
if (!catConf && catId && catId !== FALLBACK_CATEGORY_ID) {
|
||||
catModel.set('visible', false);
|
||||
catModel.set('open', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Korrigiere Label, Sortierung und Sichtbarkeit der konfigurierten Kategorien
|
||||
if (catConf) {
|
||||
// 🛑 KRITISCHER FIX: Garantiertes Setzen des korrekten Labels (LÖST KLEINSCHREIBUNGS-PROBLEM)
|
||||
if (catModel.get('label') !== catConf.label) {
|
||||
console.log(`[${PluginName}][Cat Fix] Korrigiere Label von '${catId}' auf '${catConf.label}'.`);
|
||||
catModel.set('label', catConf.label);
|
||||
}
|
||||
|
||||
// ** Das Setzen von 'visible' und 'open' sollte die UI-Aktualisierung (Kategorie anzeigen) auslösen. **
|
||||
catModel.set('visible', true);
|
||||
catModel.set('open', catConf.open !== false);
|
||||
catModel.set('order', catConf.ord || 999);
|
||||
visibleCategories.push(catId);
|
||||
}
|
||||
});
|
||||
|
||||
// --- C. Cleanup und Neu-Rendern erzwingen ---
|
||||
categories.sort((a, b) => (a.get('order') || 999) - (b.get('order') || 999));
|
||||
|
||||
B.sortBlocksByPrefixAndLabel && B.sortBlocksByPrefixAndLabel(bm.getAll().models);
|
||||
|
||||
// 🛑 KRITISCH: Rendering WIRD NICHT mehr erzwungen – nur DOM Cleanup.
|
||||
renderBlocks(editor);
|
||||
|
||||
console.log(`Kategorisierung abgeschlossen. Sichtbare Kategorien: ${visibleCategories.sort().join(', ')}.`);
|
||||
console.groupEnd();
|
||||
normalizationIsRunning = false;
|
||||
};
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// GrapesJS Plugin Registrierung
|
||||
// ----------------------------------------------------------------------
|
||||
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
// 1. Initialer, verspäteter Lauf bei Ladevorgang
|
||||
editor.on('load', () => {
|
||||
// FINALER LAUF: Läuft, wenn ALLE Standard-Plugins fertig sind
|
||||
setTimeout(() => {
|
||||
console.warn(`[${PluginName}][FINAL RUN] Starte finalen Normalisierungslauf nach 2500ms.`);
|
||||
normalizeCategories(editor);
|
||||
}, 2500);
|
||||
});
|
||||
|
||||
// 2. WATCHDOG gegen Label-Überschreibung oder unerwünschte Adds
|
||||
bm.getCategories().on('add change:label', (categoryModel) => {
|
||||
const catId = categoryModel.get('id');
|
||||
const newLabel = categoryModel.get('label');
|
||||
const expectedLabel = B.CATEGORY_CONFIG?.[catId]?.label;
|
||||
|
||||
// WATCHDOG-ADD: Entfernt unerwünschte Kategorien sofort, falls sie erstellt werden
|
||||
if (ALL_FORBIDDEN_CAT_IDS.includes(catId)) {
|
||||
console.error(`[${PluginName}][WATCHDOG-ADD] Unerwünschte Kategorie '${catId}' wurde hinzugefügt! Starte Sofort-Korrektur.`);
|
||||
bm.getCategories().remove(categoryModel);
|
||||
setTimeout(() => normalizeCategories(editor), 1);
|
||||
}
|
||||
|
||||
// WATCHDOG-LABEL: Korrigiert falsche Labels (z.B. "bausteine" -> "🧱 Bausteine")
|
||||
if (expectedLabel && newLabel !== expectedLabel) {
|
||||
console.warn(`[${PluginName}][WATCHDOG-CHANGE] Externe Label-Manipulation von '${catId}' erkannt: Korrigiere von '${newLabel}' auf '${expectedLabel}'.`);
|
||||
|
||||
// Sofortiges Zurücksetzen des Labels auf den korrekten Wert
|
||||
categoryModel.set('label', expectedLabel, { silent: true });
|
||||
|
||||
// Triggere einen Normalize-Lauf, damit die UI die Korrektur sieht und die Sortierung passt.
|
||||
setTimeout(() => normalizeCategories(editor), 1);
|
||||
}
|
||||
});
|
||||
|
||||
B.normalizeCategories = normalizeCategories;
|
||||
B.renderBlocks = renderBlocks;
|
||||
|
||||
console.log(`[${PluginName}] Master-Koordinator registriert.`);
|
||||
});
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
14
public/assets/js/bridge/categorization-master.js
Normal file
14
public/assets/js/bridge/categorization-master.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* /assets/js/bridge/categorization-master.js (PLATZHALTER - LOGIK IN CLEANUP.JS VERSCHOBEN) */
|
||||
(function(B){
|
||||
|
||||
if (!B || typeof grapesjs === 'undefined') return;
|
||||
|
||||
const PluginName = 'bridge-categorization-master';
|
||||
|
||||
// Dies ist nun ein leeres Plugin. Die Logik wurde nach categorization-cleanup.js verschoben.
|
||||
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
|
||||
// Leere Plugin-Funktion. Führt keine Aufräumarbeiten, Normalisierung oder Exporte durch.
|
||||
console.log(`[${PluginName}] Plugin existiert (Logik nach cleanup.js verschoben).`);
|
||||
});
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
55
public/assets/js/bridge/category-config.js
Normal file
55
public/assets/js/bridge/category-config.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/* /assets/js/bridge/category-config.js (FINALE KORREKTUR DER BLAUPASE) */
|
||||
(function(B) {
|
||||
if (!B) return;
|
||||
|
||||
// NEU: Map, die Ressourcentyp ('kind') zu API-Basis-URL zuordnet. Wird von library-api.js gelesen.
|
||||
B.RESOURCE_API_BASES = B.RESOURCE_API_BASES || {};
|
||||
const API_BASE_DEFAULT = (B.API_BASE || '/api/editor');
|
||||
|
||||
// DEFINITION DER ZIEL-KATEGORIEN
|
||||
B.CATEGORY_CONFIG = {
|
||||
// --- 1. BIBLIOTHEK (mysnips) ---
|
||||
mysnips: {
|
||||
// ... (Bleibt unverändert, da es kein API-Async-Laden nutzt)
|
||||
ord: 20,
|
||||
open: false,
|
||||
label: '📚 Bibliothek',
|
||||
files: ['blocks-standard.js'],
|
||||
registration_mode: 'sync',
|
||||
},
|
||||
// --- 2. BAUSTEINE (bausteine) ---
|
||||
bausteine: {
|
||||
// ... (Bleibt unverändert, da es kein API-Async-Laden nutzt)
|
||||
ord: 10,
|
||||
open: true,
|
||||
label: '🧱 Bausteine',
|
||||
files: ['blocks-custom.js'],
|
||||
registration_mode: 'sync',
|
||||
},
|
||||
// --- 3. API Custom-Blocks (Standard-API) ----
|
||||
custom: {
|
||||
ord: 1,
|
||||
label: 'Custom',
|
||||
open: true,
|
||||
files: ['library-api.js','blocks-api.js'],
|
||||
registration_mode: 'async',
|
||||
// NEU: API-Konfiguration für diese Kategorie
|
||||
api_config: {
|
||||
base_url: '/api/editor', // Nutzt die Standard-API
|
||||
resources: ['templates','sections', 'blocks', 'snippets'] // Ressourcen, die von dort geladen werden
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- Initialisierung der zentralen RESOURCE_API_BASES Map ---
|
||||
// Diese Logik stellt sicher, dass library-api.js weiß, welchen Endpunkt es für welchen "kind" nutzen muss.
|
||||
Object.values(B.CATEGORY_CONFIG).forEach(config => {
|
||||
if (config.api_config && Array.isArray(config.api_config.resources)) {
|
||||
const baseUrl = config.api_config.base_url || API_BASE_DEFAULT;
|
||||
config.api_config.resources.forEach(resourceKind => {
|
||||
B.RESOURCE_API_BASES[resourceKind] = baseUrl;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
28
public/assets/js/bridge/category-config.js WORKED
Normal file
28
public/assets/js/bridge/category-config.js WORKED
Normal file
@@ -0,0 +1,28 @@
|
||||
/* /assets/js/bridge/category-config.js (FINAL: Zentrale Konfiguration) */
|
||||
(function(w){
|
||||
var B = w.BridgeParts = w.BridgeParts || {};
|
||||
|
||||
/**
|
||||
* Zentrale Konfiguration für Block-Kategorien und deren Sortierprioritäten.
|
||||
*/
|
||||
B.CATEGORY_CONFIG = {
|
||||
// Prio 1
|
||||
'lib-templates': { label:'Bibliothek: Templates (Ref)', ord: 1, open: true },
|
||||
|
||||
// Prio 2 (Custom)A
|
||||
'custom': { label:'Custom', ord: 2, open: true },
|
||||
'custom-fix': { label:'Custom', ord: 2, open: true },
|
||||
'custom-flex': { label:'Custom', ord: 2, open: true },
|
||||
|
||||
// Prio 3 (Bausteine)
|
||||
'bausteine': { label:'Bausteine', ord: 3, open: true },
|
||||
|
||||
// Prio 4 (Bibliothek)
|
||||
'mysnips': { label:'Bibliothek', ord: 4, open: true },
|
||||
|
||||
// INTERNE (Werden später im Plugin auf Prio 2 umgeleitet und sortiert)
|
||||
'lib-sections': { label:'Bibliothek: Sections', ord: 99, open: true },
|
||||
'lib-blocks': { label:'Bibliothek: Blöcke', ord: 99, open: true },
|
||||
};
|
||||
|
||||
})(window);
|
||||
143
public/assets/js/bridge/custom-blocks-plugin.js
Normal file
143
public/assets/js/bridge/custom-blocks-plugin.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/* /assets/js/bridge/custom-blocks-plugin.js (FINALE VERSION 2.0: Erzwungene Sortierung) */
|
||||
(function(gjs, B){
|
||||
if (!gjs || !B || !B.CATEGORY_CONFIG) return;
|
||||
|
||||
// --- 1. Block-Sortierlogik (Wieder logische Gewichte: 1 < 2 < 3) -----------------------
|
||||
const getSortWeight = (id) => {
|
||||
// Logisch: Section (1) < Block (2) < Snippet (3)
|
||||
if (id.startsWith('custom-section-') || id.startsWith('lib-sec-')) return 1;
|
||||
if (id.startsWith('custom-block-') || id.startsWith('lib-blk-')) return 2;
|
||||
if (id.startsWith('custom-snippet-') || id.startsWith('snip-')) return 3;
|
||||
return 99;
|
||||
};
|
||||
|
||||
const sortBlocksByPrefixAndLabel = (blocks) => {
|
||||
blocks.sort((a, b) => {
|
||||
const aId = String((a.get ? a.get('id') : a.id) || '');
|
||||
const bId = String((b.get ? b.get('id') : b.id) || '');
|
||||
// Hier nutzen wir die Rohdaten (aSnippet, bSnippet)
|
||||
const aLabel = String((a.get ? a.get('label') : a.label) || '').toLowerCase();
|
||||
const bLabel = String((b.get ? b.get('label') : b.label) || '').toLowerCase();
|
||||
|
||||
const aWeight = getSortWeight(aId);
|
||||
const bWeight = getSortWeight(bId);
|
||||
|
||||
// 1. Sortierung nach Gewicht (1, 2, 3)
|
||||
if (aWeight !== bWeight) return aWeight - bWeight;
|
||||
|
||||
// 2. Sortierung alphabetisch (a vor b)
|
||||
if (aLabel < bLabel) return -1;
|
||||
if (aLabel > bLabel) return 1;
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
// -----------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
gjs.plugins.add('bridge-custom-blocks', (editor, opts = {}) => {
|
||||
const config = B.CATEGORY_CONFIG;
|
||||
const validCatIds = Object.keys(config);
|
||||
|
||||
// IDs zur internen Zuweisung
|
||||
const CAT_CUSTOM_MAIN_ID = 'custom';
|
||||
const CAT_BAUSTEINE_ID = 'bausteine';
|
||||
const CAT_BIBLIOTHEK_ID = 'mysnips';
|
||||
const CAT_LIB_TEMPLATES_ID = 'lib-templates';
|
||||
|
||||
|
||||
const normalizeAndSort = (ed) => {
|
||||
try {
|
||||
const bm = ed.BlockManager;
|
||||
const categories = bm.getCategories ? bm.getCategories() : null;
|
||||
|
||||
let allBlocks = bm.getAll().models || bm.getAll();
|
||||
let customBlocksArray = [];
|
||||
let otherBlocksArray = [];
|
||||
|
||||
// 1. Blöcke neu kategorisieren & trennen
|
||||
(allBlocks || []).forEach(b => {
|
||||
const id = String((b.get ? b.get('id') : b.id) || '');
|
||||
let targetCatId = null;
|
||||
|
||||
if (id.startsWith('lib-tpl-ref-')) {
|
||||
targetCatId = CAT_LIB_TEMPLATES_ID;
|
||||
} else if (id.startsWith('custom-') || id.startsWith('lib-sec-') || id.startsWith('lib-blk-') || id.startsWith('snip-')) {
|
||||
targetCatId = CAT_CUSTOM_MAIN_ID;
|
||||
} else if (id.startsWith('blk-') || id.startsWith('std-')) {
|
||||
targetCatId = CAT_BAUSTEINE_ID;
|
||||
} else if (!b.get('category')) {
|
||||
targetCatId = CAT_BIBLIOTHEK_ID;
|
||||
}
|
||||
|
||||
if (targetCatId) {
|
||||
b.set('category', targetCatId);
|
||||
if (targetCatId === CAT_CUSTOM_MAIN_ID) {
|
||||
customBlocksArray.push(b);
|
||||
} else {
|
||||
otherBlocksArray.push(b);
|
||||
}
|
||||
} else {
|
||||
// Blöcke, die nicht zugewiesen wurden (z.B. basic/extra), behalten
|
||||
otherBlocksArray.push(b);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Block-Sortierung INNERHALB der "custom" Kategorie
|
||||
if (customBlocksArray.length > 0) {
|
||||
sortBlocksByPrefixAndLabel(customBlocksArray);
|
||||
console.log('[DEBUG PLUGIN] Custom Blocks intern sortiert.');
|
||||
}
|
||||
|
||||
// NEU: Gesamte BlockManager-Kollektion mit sortierten Custom-Blöcken überschreiben
|
||||
// Wir nehmen alle sortierten Custom-Blöcke und fügen die anderen Blöcke danach an.
|
||||
const newBlockOrder = customBlocksArray.concat(otherBlocksArray);
|
||||
|
||||
// Dieser Hack sollte die Reihenfolge in der Seitenleiste erzwingen.
|
||||
if (bm.getAll().reset) {
|
||||
bm.getAll().reset(newBlockOrder);
|
||||
console.log('[DEBUG PLUGIN] Gesamte Block-Kollektion mit neuer Sortierung überschrieben.');
|
||||
}
|
||||
|
||||
// 3. Kategorien Aufräumen & Sortieren (wie zuvor)
|
||||
if (categories && categories.models) {
|
||||
// Aufräumen
|
||||
categories.models.slice().forEach(cat => {
|
||||
const catId = (cat.get('id') || cat.id || '').toLowerCase();
|
||||
if (!validCatIds.includes(catId) && catId !== 'basic' && catId !== 'extra') {
|
||||
categories.remove(cat);
|
||||
}
|
||||
});
|
||||
|
||||
// Labels korrigieren und Kategorie-Sortierung erzwingen
|
||||
categories.models.forEach(cat => {
|
||||
const catId = (cat.get('id') || cat.id || '').toLowerCase();
|
||||
if (config[catId]) {
|
||||
cat.set('label', config[catId].label);
|
||||
cat.set('open', config[catId].open ?? true);
|
||||
}
|
||||
});
|
||||
|
||||
const catOrder = (m) => config[String((m.get('id') || m.id)).toLowerCase()]?.ord || 99;
|
||||
const arr = categories.models.slice().sort((a,b) => catOrder(a) - catOrder(b));
|
||||
categories.reset(arr);
|
||||
}
|
||||
|
||||
// 4. Finaler DOM-Sweep
|
||||
B.enforceCategoryOrder && B.enforceCategoryOrder(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
|
||||
} catch(e) {
|
||||
console.error('[CustomPlugin] Error during normalize:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Listener
|
||||
editor.on('block:add block:remove block:reset', () => normalizeAndSort(editor));
|
||||
editor.on('load', () => {
|
||||
normalizeAndSort(editor);
|
||||
setTimeout(() => normalizeAndSort(editor), 100);
|
||||
setTimeout(() => normalizeAndSort(editor), 800);
|
||||
setTimeout(() => normalizeAndSort(editor), 1500);
|
||||
});
|
||||
});
|
||||
})(window.grapesjs, window.BridgeParts);
|
||||
143
public/assets/js/bridge/custom-blocks-plugin.js WORKED
Normal file
143
public/assets/js/bridge/custom-blocks-plugin.js WORKED
Normal file
@@ -0,0 +1,143 @@
|
||||
/* /assets/js/bridge/custom-blocks-plugin.js (FINALE VERSION 2.0: Erzwungene Sortierung) */
|
||||
(function(gjs, B){
|
||||
if (!gjs || !B || !B.CATEGORY_CONFIG) return;
|
||||
|
||||
// --- 1. Block-Sortierlogik (Wieder logische Gewichte: 1 < 2 < 3) -----------------------
|
||||
const getSortWeight = (id) => {
|
||||
// Logisch: Section (1) < Block (2) < Snippet (3)
|
||||
if (id.startsWith('custom-section-') || id.startsWith('lib-sec-')) return 1;
|
||||
if (id.startsWith('custom-block-') || id.startsWith('lib-blk-')) return 2;
|
||||
if (id.startsWith('custom-snippet-') || id.startsWith('snip-')) return 3;
|
||||
return 99;
|
||||
};
|
||||
|
||||
const sortBlocksByPrefixAndLabel = (blocks) => {
|
||||
blocks.sort((a, b) => {
|
||||
const aId = String((a.get ? a.get('id') : a.id) || '');
|
||||
const bId = String((b.get ? b.get('id') : b.id) || '');
|
||||
// Hier nutzen wir die Rohdaten (aSnippet, bSnippet)
|
||||
const aLabel = String((a.get ? a.get('label') : a.label) || '').toLowerCase();
|
||||
const bLabel = String((b.get ? b.get('label') : b.label) || '').toLowerCase();
|
||||
|
||||
const aWeight = getSortWeight(aId);
|
||||
const bWeight = getSortWeight(bId);
|
||||
|
||||
// 1. Sortierung nach Gewicht (1, 2, 3)
|
||||
if (aWeight !== bWeight) return aWeight - bWeight;
|
||||
|
||||
// 2. Sortierung alphabetisch (a vor b)
|
||||
if (aLabel < bLabel) return -1;
|
||||
if (aLabel > bLabel) return 1;
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
// -----------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
gjs.plugins.add('bridge-custom-blocks', (editor, opts = {}) => {
|
||||
const config = B.CATEGORY_CONFIG;
|
||||
const validCatIds = Object.keys(config);
|
||||
|
||||
// IDs zur internen Zuweisung
|
||||
const CAT_CUSTOM_MAIN_ID = 'custom';
|
||||
const CAT_BAUSTEINE_ID = 'bausteine';
|
||||
const CAT_BIBLIOTHEK_ID = 'mysnips';
|
||||
const CAT_LIB_TEMPLATES_ID = 'lib-templates';
|
||||
|
||||
|
||||
const normalizeAndSort = (ed) => {
|
||||
try {
|
||||
const bm = ed.BlockManager;
|
||||
const categories = bm.getCategories ? bm.getCategories() : null;
|
||||
|
||||
let allBlocks = bm.getAll().models || bm.getAll();
|
||||
let customBlocksArray = [];
|
||||
let otherBlocksArray = [];
|
||||
|
||||
// 1. Blöcke neu kategorisieren & trennen
|
||||
(allBlocks || []).forEach(b => {
|
||||
const id = String((b.get ? b.get('id') : b.id) || '');
|
||||
let targetCatId = null;
|
||||
|
||||
if (id.startsWith('lib-tpl-ref-')) {
|
||||
targetCatId = CAT_LIB_TEMPLATES_ID;
|
||||
} else if (id.startsWith('custom-') || id.startsWith('lib-sec-') || id.startsWith('lib-blk-') || id.startsWith('snip-')) {
|
||||
targetCatId = CAT_CUSTOM_MAIN_ID;
|
||||
} else if (id.startsWith('blk-') || id.startsWith('std-')) {
|
||||
targetCatId = CAT_BAUSTEINE_ID;
|
||||
} else if (!b.get('category')) {
|
||||
targetCatId = CAT_BIBLIOTHEK_ID;
|
||||
}
|
||||
|
||||
if (targetCatId) {
|
||||
b.set('category', targetCatId);
|
||||
if (targetCatId === CAT_CUSTOM_MAIN_ID) {
|
||||
customBlocksArray.push(b);
|
||||
} else {
|
||||
otherBlocksArray.push(b);
|
||||
}
|
||||
} else {
|
||||
// Blöcke, die nicht zugewiesen wurden (z.B. basic/extra), behalten
|
||||
otherBlocksArray.push(b);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Block-Sortierung INNERHALB der "custom" Kategorie
|
||||
if (customBlocksArray.length > 0) {
|
||||
sortBlocksByPrefixAndLabel(customBlocksArray);
|
||||
console.log('[DEBUG PLUGIN] Custom Blocks intern sortiert.');
|
||||
}
|
||||
|
||||
// NEU: Gesamte BlockManager-Kollektion mit sortierten Custom-Blöcken überschreiben
|
||||
// Wir nehmen alle sortierten Custom-Blöcke und fügen die anderen Blöcke danach an.
|
||||
const newBlockOrder = customBlocksArray.concat(otherBlocksArray);
|
||||
|
||||
// Dieser Hack sollte die Reihenfolge in der Seitenleiste erzwingen.
|
||||
if (bm.getAll().reset) {
|
||||
bm.getAll().reset(newBlockOrder);
|
||||
console.log('[DEBUG PLUGIN] Gesamte Block-Kollektion mit neuer Sortierung überschrieben.');
|
||||
}
|
||||
|
||||
// 3. Kategorien Aufräumen & Sortieren (wie zuvor)
|
||||
if (categories && categories.models) {
|
||||
// Aufräumen
|
||||
categories.models.slice().forEach(cat => {
|
||||
const catId = (cat.get('id') || cat.id || '').toLowerCase();
|
||||
if (!validCatIds.includes(catId) && catId !== 'basic' && catId !== 'extra') {
|
||||
categories.remove(cat);
|
||||
}
|
||||
});
|
||||
|
||||
// Labels korrigieren und Kategorie-Sortierung erzwingen
|
||||
categories.models.forEach(cat => {
|
||||
const catId = (cat.get('id') || cat.id || '').toLowerCase();
|
||||
if (config[catId]) {
|
||||
cat.set('label', config[catId].label);
|
||||
cat.set('open', config[catId].open ?? true);
|
||||
}
|
||||
});
|
||||
|
||||
const catOrder = (m) => config[String((m.get('id') || m.id)).toLowerCase()]?.ord || 99;
|
||||
const arr = categories.models.slice().sort((a,b) => catOrder(a) - catOrder(b));
|
||||
categories.reset(arr);
|
||||
}
|
||||
|
||||
// 4. Finaler DOM-Sweep
|
||||
B.enforceCategoryOrder && B.enforceCategoryOrder(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
|
||||
} catch(e) {
|
||||
console.error('[CustomPlugin] Error during normalize:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Listener
|
||||
editor.on('block:add block:remove block:reset', () => normalizeAndSort(editor));
|
||||
editor.on('load', () => {
|
||||
normalizeAndSort(editor);
|
||||
setTimeout(() => normalizeAndSort(editor), 100);
|
||||
setTimeout(() => normalizeAndSort(editor), 800);
|
||||
setTimeout(() => normalizeAndSort(editor), 1500);
|
||||
});
|
||||
});
|
||||
})(window.grapesjs, window.BridgeParts);
|
||||
6
public/assets/js/bridge/custom-plugin.js
Normal file
6
public/assets/js/bridge/custom-plugin.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/* /assets/js/bridge/custom-plugin.js (FINALE VERSION: API-Logik Hülle) */
|
||||
(function(B){
|
||||
if (!B || typeof grapesjs === 'undefined') return;
|
||||
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
118
public/assets/js/bridge/general-functions.js
Normal file
118
public/assets/js/bridge/general-functions.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/* /assets/js/bridge/general-functions.js (LOGIK-FIX: GLOBAL_DEBUG prüft als erstes) */
|
||||
(function(B){
|
||||
if (!B) return;
|
||||
|
||||
// --- 🎯 1. ZENTRALE LOG-KONTROLLE (Konfiguration & Defaults) ---
|
||||
B.LOG_CONFIG = B.LOG_CONFIG || {};
|
||||
|
||||
// Globale Steuerung: Deaktiviert ALLE Logs (außer force=true)
|
||||
// 🛑 KRITISCHE KORREKTUR: Wir setzen den Wert nur, wenn er noch nicht gesetzt wurde (z.B. durch bridge-core.js)
|
||||
B.LOG_CONFIG.GLOBAL_DEBUG = B.LOG_CONFIG.GLOBAL_DEBUG !== undefined ? B.LOG_CONFIG.GLOBAL_DEBUG : true;
|
||||
|
||||
// Steuerung nach Log-Ebenen (wirken nur, wenn GLOBAL_DEBUG = true)
|
||||
B.LOG_CONFIG.INFO_ENABLED = B.LOG_CONFIG.INFO_ENABLED !== undefined ? B.LOG_CONFIG.INFO_ENABLED : true;
|
||||
B.LOG_CONFIG.WARN_ENABLED = B.LOG_CONFIG.WARN_ENABLED !== undefined ? B.LOG_CONFIG.WARN_ENABLED : true;
|
||||
B.LOG_CONFIG.ERROR_ENABLED = B.LOG_CONFIG.ERROR_ENABLED !== undefined ? B.LOG_CONFIG.ERROR_ENABLED : true;
|
||||
|
||||
// Steuerung für große Datenmengen (B.logData)
|
||||
B.LOG_CONFIG.DATA_ENABLED = B.LOG_CONFIG.DATA_ENABLED !== undefined ? B.LOG_CONFIG.DATA_ENABLED : true;
|
||||
|
||||
// NEU: Objekt zur Speicherung des individuellen Log-Status pro Plugin (Standard: leeres Objekt)
|
||||
B.LOG_CONFIG.PLUGINS = B.LOG_CONFIG.PLUGINS || {};
|
||||
|
||||
/**
|
||||
* Zentrale Log-Funktion mit Prüfung auf globale Schalter, Log-Ebenen und Plugin-spezifische Schalter.
|
||||
* @param {string} pluginName - Der Name des aufrufenden Plugins (KRITISCH für die neue Logik).
|
||||
* @param {string} message - Die zu loggende Nachricht.
|
||||
* @param {string} color - CSS-Farbe für die Nachricht (optional).
|
||||
* @param {string} type - Log-Typ ('info', 'warn', 'error').
|
||||
* @param {boolean} force - Wenn true, wird geloggt, auch wenn GLOBAL_DEBUG/Plugin-Log false ist.
|
||||
*/
|
||||
B.log = (pluginName, message, color = 'inherit', type = 'info', force = false) => {
|
||||
const config = B.LOG_CONFIG;
|
||||
|
||||
// 1. Prüfe auf force (immer loggen)
|
||||
if (!force) {
|
||||
// 🛑 KRITISCHE KORREKTUR: Prüfe auf GLOBAL_DEBUG an 2. Stelle (wenn nicht 'force')
|
||||
if (!config.GLOBAL_DEBUG) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Prüfe den PLUGIN-SPEZIFISCHEN SCHALTER
|
||||
// Wenn der Schalter im PLUGINS-Objekt existiert UND auf false gesetzt ist, abbrechen.
|
||||
const pluginStatus = config.PLUGINS[pluginName];
|
||||
if (pluginStatus === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Prüfe auf spezifische Log-Ebenen-Schalter
|
||||
if (type === 'info' && !config.INFO_ENABLED) return;
|
||||
if (type === 'warn' && !config.WARN_ENABLED) return;
|
||||
if (type === 'error' && !config.ERROR_ENABLED) return;
|
||||
}
|
||||
|
||||
// Führe das Logging aus
|
||||
const stylePlugin = `color:orange; font-weight:bold;`;
|
||||
const styleMessage = `color:${color}; font-weight:normal;`;
|
||||
|
||||
const logFn = (type === 'error') ? console.error : (type === 'warn' ? console.warn : console.log);
|
||||
|
||||
logFn(`%c[${pluginName}] %c${message}`, stylePlugin, styleMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Spezielle Funktion zum Loggen großer Datenmengen (prüft B.LOG_CONFIG.DATA_ENABLED und Plugin-Schalter).
|
||||
* Wird jetzt als Wrapper für B.log verwendet.
|
||||
*/
|
||||
B.logData = (pluginName, data) => {
|
||||
// 1. Prüfe, ob das Daten-Logging global erlaubt ist
|
||||
if (!B.LOG_CONFIG.DATA_ENABLED) return;
|
||||
|
||||
// 2. Den "Daten-Ausgabe..." Log durch B.log schicken, um die Filter zu durchlaufen
|
||||
// Wir verwenden force=false, damit GLOBAL_DEBUG und Plugin-Schalter angewendet werden
|
||||
B.log(pluginName, 'Daten-Ausgabe (nächste Zeile):', 'gray', 'info', false);
|
||||
|
||||
// 3. Wenn B.log den Filter passiert hätte, loggen wir hier das eigentliche Objekt (nur wenn GLOBAL_DEBUG true)
|
||||
const pluginStatus = B.LOG_CONFIG.PLUGINS?.[pluginName];
|
||||
if (B.LOG_CONFIG.GLOBAL_DEBUG && pluginStatus !== false) {
|
||||
console.log(data); // Das eigentliche Objekt-Log ohne Formatierung
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- 2. Hilfsfunktion zur Sortiergewichtung ---
|
||||
const getSortWeight = (id) => {
|
||||
if (['text', 'image', 'link', 'section', 'column', 'button', 'divider', 'map'].includes(id)) return 99;
|
||||
if (id.startsWith('cust-')) return 1;
|
||||
if (id.startsWith('lib-')) return 2;
|
||||
if (id.endsWith('-fix') || id.endsWith('-flex')) {
|
||||
return 3;
|
||||
}
|
||||
if (!id.includes('-')) return 99;
|
||||
return 50;
|
||||
};
|
||||
|
||||
// --- 3. Hilfsfunktion zur Sortierung ---
|
||||
const sortBlocksByPrefixAndLabel = (blocks) => {
|
||||
blocks.sort((a, b) => {
|
||||
const aId = String((a.get ? a.get('id') : a.id) || '');
|
||||
const bId = String((b.get ? b.get('id') : b.id) || '');
|
||||
const aLabel = String((a.get ? a.get('label') : a.label) || '').toLowerCase();
|
||||
const bLabel = String((b.get ? b.get('label') : b.label) || '').toLowerCase();
|
||||
|
||||
const aWeight = getSortWeight(aId);
|
||||
const bWeight = getSortWeight(bId);
|
||||
|
||||
if (aWeight !== bWeight) return aWeight - bWeight;
|
||||
if (aLabel < bLabel) return -1;
|
||||
if (aLabel > bLabel) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// --- 4. Funktionen zum BridgeParts-Objekt hinzufügen ---
|
||||
B.getSortWeight = getSortWeight;
|
||||
B.sortBlocksByPrefixAndLabel = sortBlocksByPrefixAndLabel;
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
39
public/assets/js/bridge/helpers.js
Normal file
39
public/assets/js/bridge/helpers.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/* /assets/js/bridge/helpers.js — Namespace + Utilities (kein ES-Module) */
|
||||
(function(w){
|
||||
var B = w.BridgeParts = w.BridgeParts || {};
|
||||
|
||||
B.post = function(m){ try { parent.postMessage(m,'*'); } catch{} };
|
||||
B.send = function(type, payload){ B.post(Object.assign({ source:'email-editor', type:type }, payload||{})); };
|
||||
B.log = function(msg){ B.post({ source:'bridge', type:'log', detail:String(msg||'') }); };
|
||||
|
||||
B.ready = function(cb){ (function t(){ if (w.grapesjs) return cb(); setTimeout(t,40); })(); };
|
||||
|
||||
B.BADGE = function(){ return document.getElementById('badge'); };
|
||||
B.badgeSay = function(txt, tone){
|
||||
var b = B.BADGE(); if(!b) return;
|
||||
b.textContent = txt;
|
||||
var cfg = {
|
||||
ok: ['#ecfeff','#155e75','#a5f3fc'],
|
||||
warn: ['#fef3c7','#92400e','#fde68a'],
|
||||
err: ['#fee2e2','#7f1d1d','#fecaca'],
|
||||
base: ['#eef2ff','#1e3a8a','#c7d2fe']
|
||||
}[tone||'base'];
|
||||
b.style.background = cfg[0]; b.style.color = cfg[1]; b.style.borderColor = cfg[2];
|
||||
};
|
||||
|
||||
B.waitForBlocks = function(ed, opt){
|
||||
opt = opt || {};
|
||||
var timeoutMs = opt.timeoutMs || 4000, interval = opt.interval || 80;
|
||||
var bm = ed.BlockManager, t0 = Date.now();
|
||||
return new Promise(function(resolve){
|
||||
(function tick(){
|
||||
var n = (bm && bm.getAll && bm.getAll().length) || 0;
|
||||
if (n > 0 || Date.now() - t0 > timeoutMs) return resolve(n);
|
||||
setTimeout(tick, interval);
|
||||
})();
|
||||
});
|
||||
};
|
||||
|
||||
B.renderBlocks = function(ed){ try { ed.BlockManager.render(); } catch{} };
|
||||
})(window);
|
||||
|
||||
246
public/assets/js/bridge/library-api.js
Normal file
246
public/assets/js/bridge/library-api.js
Normal file
@@ -0,0 +1,246 @@
|
||||
/* /assets/js/bridge/library-api.js (FINAL & KORRIGIERT FÜR FLEXIBLE API-BASES) */
|
||||
|
||||
(function(B){
|
||||
|
||||
// 🛑 WICHTIG: Globalen Cache-Speicher initialisieren (wird von blocks-api.js gelesen)
|
||||
B.ApiItemCache = B.ApiItemCache || {};
|
||||
|
||||
if (!B || typeof grapesjs === 'undefined') return;
|
||||
|
||||
const PluginName = 'bridge-library-api';
|
||||
|
||||
// NEU: Standard-API-Basis für Abwärtskompatibilität, falls nichts konfiguriert
|
||||
const API_BASE_FALLBACK = (B.API_BASE || '/api/editor');
|
||||
|
||||
// Konstanten
|
||||
const TARGET_CAT_ID = 'custom';
|
||||
const PLACEHOLDER_ID = 'api-placeholder-loading';
|
||||
const REFERENCE_COMPONENT_TYPE = 'library-reference';
|
||||
|
||||
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
|
||||
B.LOG_CONFIG.PLUGINS[PluginName] = true;
|
||||
}
|
||||
|
||||
const log = (type, message, color = '#6A5ACD', 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;');
|
||||
}
|
||||
}
|
||||
};
|
||||
const logApiData = (data) => B.logData(PluginName, data);
|
||||
|
||||
log('INIT', 'API-Schicht initialisiert.');
|
||||
|
||||
// --- HILFSFUNKTIONEN ---
|
||||
|
||||
/**
|
||||
* Gibt die korrekte API-Basis-URL für einen Ressourcentyp (kind) zurück.
|
||||
* Nutzt die zentrale Map B.RESOURCE_API_BASES, die in category-config.js gefüllt wurde.
|
||||
*/
|
||||
const getApiBase = (resource) => {
|
||||
// Fallback auf die konfigurierte Standard-Basis, falls die Map noch nicht existiert oder der Eintrag fehlt.
|
||||
return (B.RESOURCE_API_BASES && B.RESOURCE_API_BASES[resource]) || API_BASE_FALLBACK;
|
||||
};
|
||||
|
||||
const buildApiUrl = (resource, action='list', params = {}) => {
|
||||
// KORREKTUR: Nutzt jetzt die dynamisch ermittelte API-Basis
|
||||
const apiBase = getApiBase(resource);
|
||||
|
||||
const url = new URL(apiBase, window.location.origin);
|
||||
|
||||
url.searchParams.set('resource', resource);
|
||||
url.searchParams.set('action', action);
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) url.searchParams.set(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const shouldLoad = (resource) => {
|
||||
const mode = (B.EDITOR_MODE || 'TEMPLATES').toUpperCase();
|
||||
|
||||
// HINWEIS: Hier muss für neue Ressourcen (wie 'products') ggf. der mode angepasst werden,
|
||||
// falls sie nicht in TEMPLATES geladen werden sollen.
|
||||
switch (mode) {
|
||||
case 'TEMPLATES':
|
||||
const templateResources = ['templates', 'sections', 'blocks', 'snippets', 'products']; // Beispiel: products hinzugefügt
|
||||
return templateResources.includes(resource);
|
||||
|
||||
case 'SECTIONS':
|
||||
const sectionResources = ['blocks', 'snippets'];
|
||||
return sectionResources.includes(resource);
|
||||
|
||||
case 'BLOCKS':
|
||||
return resource === 'snippets';
|
||||
|
||||
default:
|
||||
log('MODE WARN', `Unbekannter Editor Modus '${mode}' festgestellt.`, 'orange', 'warn');
|
||||
return resource === 'snippets';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchData = (resource, action='list', params = {}) => {
|
||||
// ... (Rest der fetchData-Funktion bleibt unverändert, nutzt aber die korrigierte buildApiUrl)
|
||||
const url = buildApiUrl(resource, action, params);
|
||||
const cacheKey = action === 'get' ? `${resource}-${params.id}` : null;
|
||||
|
||||
// Cache-Check verwendet B.ApiItemCache
|
||||
if (cacheKey && B.ApiItemCache.hasOwnProperty(cacheKey)) {
|
||||
log('CACHE HIT', `Cache Hit für /${resource}-${cacheKey}.`, '#708090', 'info');
|
||||
return Promise.resolve(B.ApiItemCache[cacheKey]);
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
log('API ERROR', `API-Aufruf fehlgeschlagen für /${resource}/${action}: ${response.status} (${response.statusText})`, 'red', 'error');
|
||||
// 💡 KORREKTUR: Bei HTTP-Fehler immer ein leeres Array für LIST und leeres Objekt für GET zurückgeben.
|
||||
return action === 'get' ? {} : { items: [] };
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.ok === false) {
|
||||
log('API ERROR', `API-Fehler für /${resource}: ${data.error || 'Unbekannt'}`, 'red', 'error');
|
||||
// 💡 KORREKTUR: Bei API-Fehler ('ok: false') immer leeres Array/Objekt zurückgeben.
|
||||
return action === 'get' ? {} : { items: [] };
|
||||
}
|
||||
|
||||
const result = data.items || data.data || data.item;
|
||||
const finalResult = result ? (Array.isArray(result) ? result : (action === 'list' ? (result.items || []) : result)) : (action === 'list' ? [] : {});
|
||||
|
||||
const resultIsArray = Array.isArray(finalResult);
|
||||
const resultLength = resultIsArray ? finalResult.length : (Object.keys(finalResult).length > 0 ? 1 : 0);
|
||||
|
||||
log('EXTRACT SUCCESS', `Extrahiert ${resultLength} Elemente (Typ: ${action}) für /${resource}.`);
|
||||
|
||||
// Cache-Speicherung verwendet B.ApiItemCache
|
||||
if (cacheKey && resultLength > 0) {
|
||||
B.ApiItemCache[cacheKey] = finalResult;
|
||||
}
|
||||
|
||||
// 💡 KORREKTUR: Bei LIST (action='list') geben wir immer ein Array zurück, sonst das Objekt
|
||||
return finalResult;
|
||||
})
|
||||
.catch(error => {
|
||||
log('FETCH ERROR', `FEHLER beim Fetchen oder Parsen von /${resource}: ${error.message}`, 'red', 'error', true);
|
||||
return action === 'get' ? {} : [];
|
||||
});
|
||||
};
|
||||
|
||||
// --- Exportierte Core-Funktionen (jetzt generisch) ---
|
||||
|
||||
// NEU: Generische Fetch-Funktion für jeden Ressourcentyp ('kind')
|
||||
B.fetchResource = (kind) => {
|
||||
if (!shouldLoad(kind)) {
|
||||
log('BLOCKED', `Blockiert: ${kind} (Modus: ${B.EDITOR_MODE})`, '#708090', 'info');
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return fetchData(kind).then(items => Array.isArray(items) ? items : []);
|
||||
};
|
||||
|
||||
// Die alten hardcodierten Funktionen verwenden jetzt die neue generische Funktion
|
||||
B.fetchTemplates = () => B.fetchResource('templates');
|
||||
B.fetchSnippets = () => B.fetchResource('snippets');
|
||||
B.fetchSections = () => B.fetchResource('sections');
|
||||
B.fetchBlocks = () => B.fetchResource('blocks');
|
||||
|
||||
B.getApiItem = (kind, id) => fetchData(kind, 'get', { id: id });
|
||||
|
||||
B.clearApiCache = () => {
|
||||
B.ApiItemCache = {}; // Cache leeren
|
||||
log('CACHE CLEAR', `API-Cache geleert.`, 'orange', 'warn');
|
||||
};
|
||||
|
||||
// 🚀 Zentrale Funktion zum Laden und Registrieren der Blöcke
|
||||
B.loadAndRegisterApiBlocks = (editor) => {
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
// NEU: Ressourcen-Kinds aus der Konfiguration sammeln
|
||||
const resourceKindsToLoad = Object.keys(B.RESOURCE_API_BASES || {});
|
||||
|
||||
if (resourceKindsToLoad.length === 0) {
|
||||
log('FEHLER', 'Keine Ressourcen-Kind-Konfiguration (B.RESOURCE_API_BASES) gefunden.', '#dc3545', 'error', true);
|
||||
bm.remove(PLACEHOLDER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map aller Fetch-Promises erstellen
|
||||
const fetchPromises = resourceKindsToLoad.map(kind =>
|
||||
B.fetchResource(kind).then(items => items.map(i => ({ ...i, kind: kind })))
|
||||
);
|
||||
|
||||
|
||||
log('API START', `Starte Promise.all für API-Abruf der Blöcke/Sektionen (${resourceKindsToLoad.join(', ')})...`, '#1E90FF');
|
||||
|
||||
Promise.all(fetchPromises)
|
||||
.then(results => {
|
||||
const apiItems = results.flat().filter(item => item && item.id);
|
||||
|
||||
log(`API SUCCESS`, `${apiItems.length} Elemente gefunden.`, '#9400D3');
|
||||
logApiData(apiItems);
|
||||
|
||||
if (apiItems.length === 0) {
|
||||
log('NO DATA', 'Keine API-Daten gefunden.', 'orange', 'warn', true);
|
||||
} else {
|
||||
apiItems.forEach(item => {
|
||||
const blockId = `lib-${item.kind}-${item.id}`;
|
||||
const label = item.name || item.label || 'Unbenannter Block';
|
||||
const itemKindUpper = item.kind.toUpperCase();
|
||||
|
||||
// Hier wird der Block-Manager-Block registriert
|
||||
// ... (Der Rest der Logik bleibt unverändert) ...
|
||||
const blockDefinition = {
|
||||
label: label,
|
||||
category: TARGET_CAT_ID,
|
||||
// 💡 KORREKTUR: Immer die library-reference-Komponente verwenden, um die Referenz-Logik
|
||||
// (mit editable: false) aus blocks-api.js zu erzwingen.
|
||||
content: {
|
||||
type: REFERENCE_COMPONENT_TYPE,
|
||||
'lib-kind': item.kind,
|
||||
'lib-id': item.id,
|
||||
// NEU: startContent wird nur als reines HTML übergeben.
|
||||
// Die Logik in blocks-api.js (init/reloadComponentContent) kümmert sich um die Anzeige.
|
||||
startContent: item.html || item.content || '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: Inhalt fehlte beim Laden.</div>',
|
||||
content: '', // Wichtig: Beim Drop keinen GrapesJS-Content setzen
|
||||
},
|
||||
attributes: { 'title': itemKindUpper },
|
||||
media: item.preview_url ? `<img src="${item.preview_url}">` : '',
|
||||
};
|
||||
bm.add(blockId, blockDefinition);
|
||||
});
|
||||
|
||||
bm.remove(PLACEHOLDER_ID);
|
||||
log(`REGISTRATION`, `${apiItems.length} API-Blöcke registriert. Platzhalter entfernt.`, '#008000');
|
||||
|
||||
const reloadExistingComponents = () => {
|
||||
const allComponents = editor.DomComponents.getWrapper().find(`[data-gjs-type="${REFERENCE_COMPONENT_TYPE}"]`);
|
||||
allComponents.forEach(component => {
|
||||
if (component.get('lib-id') && component.components().length === 0 && typeof component.reloadComponentContent === 'function') {
|
||||
log(`RELOAD START`, `Lade ${component.get('lib-kind')}/${component.get('lib-id')} nach Cache-Füllung (Sicherheitsnetz).`, '#FF4500');
|
||||
component.reloadComponentContent({ forced: true, reason: 'EXISTING_CONTENT_RELOAD' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(reloadExistingComponents, 100);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Hier wird der Fehler von fetchData oder map abgefangen
|
||||
log('FETCH ERROR', `FEHLER beim Laden der API-Blöcke: ${error.message}`, '#dc3545', 'error', true);
|
||||
bm.remove(PLACEHOLDER_ID);
|
||||
});
|
||||
};
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
17
public/assets/js/bridge/library-parts.js
Normal file
17
public/assets/js/bridge/library-parts.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* /assets/js/bridge/library-parts.js (BEREINIGT) */
|
||||
|
||||
(function(B){
|
||||
// Alle API-spezifischen Funktionen (fetchData, fetchTemplates, etc.)
|
||||
// und der apiItemCache wurden nach library-api.js verschoben.
|
||||
|
||||
if (!B || typeof grapesjs === 'undefined') return;
|
||||
|
||||
const PluginName = 'bridge-library-parts-core';
|
||||
|
||||
if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
|
||||
B.LOG_CONFIG.PLUGINS[PluginName] = false;
|
||||
}
|
||||
|
||||
// Zusätzliche Core-Funktionen, die nicht API-spezifisch sind, würden hier verbleiben.
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
19
public/assets/js/bridge/library-plugin.js_NV
Normal file
19
public/assets/js/bridge/library-plugin.js_NV
Normal file
@@ -0,0 +1,19 @@
|
||||
/* /assets/js/bridge/library-plugin.js (Plugin für Standard-Bausteine) */
|
||||
(function(B){
|
||||
if (!B || typeof grapesjs === 'undefined') return;
|
||||
|
||||
const PluginName = 'bridge-library-plugin';
|
||||
|
||||
/**
|
||||
* GrapesJS Plugin Registrierung: bridge-library-plugin
|
||||
* Dieses Plugin dient als Platzhalter für die zukünftige Konfiguration
|
||||
* der Standard-Blöcke (Bausteine). Es fügt aktuell keine Blöcke hinzu,
|
||||
* sondern wartet auf die Blöcke aus dem Preset (gjs-preset-newsletter).
|
||||
* Der Categorization Master wird diese Blöcke später in die Kategorie 'bausteine' verschieben.
|
||||
*/
|
||||
grapesjs.plugins.add(PluginName, (editor, opts = {}) => {
|
||||
console.log(`[${PluginName}] Plugin registriert. Erwartet Blöcke vom Preset.`);
|
||||
// ToDo: Zukünftige Standardblöcke hier hinzufügen
|
||||
});
|
||||
|
||||
})(window.BridgeParts || (window.BridgeParts = {}));
|
||||
166
public/assets/js/bridge/library.js
Normal file
166
public/assets/js/bridge/library.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/* /assets/js/bridge/library.js — Kategorien, Defaults, Snippets (FINAL UND KORRIGIERT) */
|
||||
(function(w){
|
||||
var B = w.BridgeParts = w.BridgeParts || {};
|
||||
if (!B.CATEGORY_CONFIG) B.CATEGORY_CONFIG = {}; // Muss vorhanden sein
|
||||
|
||||
/* Panels/Views sicherstellen (Unverändert) */
|
||||
B.ensureViews = function(ed){
|
||||
var pn = ed.Panels;
|
||||
if (!pn.getPanel('views')) pn.addPanel({ id:'views' });
|
||||
if (!pn.getButton('views','open-blocks'))
|
||||
pn.addButton('views',[{ id:'open-blocks', command:'open-blocks', togglable:1, className:'gjs-pn-btn' }]);
|
||||
if (!pn.getButton('views','open-layers'))
|
||||
pn.addButton('views',[{ id:'open-layers', command:'open-layers', togglable:1, className:'gjs-pn-btn' }]);
|
||||
if (!pn.getButton('views','open-sm'))
|
||||
pn.addButton('views',[{ id:'open-sm', command:'open-sm', togglable:1, className:'gjs-pn-btn' }]);
|
||||
try{ var b=pn.getButton('views','open-blocks'); b && b.set('active',true); }catch{}
|
||||
};
|
||||
|
||||
/* Helpers --------------------------------------------------------------- */
|
||||
function addOnce(bm, id, def){
|
||||
if (!id || typeof id !== 'string') return;
|
||||
try{ if (bm.get && bm.get(id)) return; bm.add(id, def); }catch{}
|
||||
}
|
||||
|
||||
// Kategorien erzeugen (Funktion unverändert)
|
||||
function forceCategory(bm, id, label, open){
|
||||
try{
|
||||
if (typeof bm.addCategory === 'function') {
|
||||
var cts = bm.getCategories && bm.getCategories();
|
||||
var find = function(){
|
||||
if (!cts) return null;
|
||||
if (typeof cts.findWhere === 'function') {
|
||||
return cts.findWhere({ id }) || cts.findWhere({ label });
|
||||
}
|
||||
var arr = cts.models || cts || [];
|
||||
for (var i=0;i<arr.length;i++){
|
||||
var m = arr[i], lid = (m.get?m.get('id'):m.id), lbl=(m.get?m.get('label'):m.label);
|
||||
if (lid===id || lbl===label) return m;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
var c = find();
|
||||
if (!c) c = bm.addCategory({ id:id, label:label, open:!!open });
|
||||
try { c.set && c.set('open', !!open); } catch {}
|
||||
return c;
|
||||
}
|
||||
}catch{}
|
||||
return { id:id, label:label, open:!!open, __labelOnly:true };
|
||||
}
|
||||
|
||||
function ensureCategories(bm){
|
||||
// Stellt sicher, dass die vier Hauptkategorien erstellt werden.
|
||||
var C_CUSTOM = forceCategory(bm,'custom', B.CATEGORY_CONFIG.custom.label, true);
|
||||
var C_STD = forceCategory(bm,'bausteine', B.CATEGORY_CONFIG.bausteine.label, true);
|
||||
var C_LIB = forceCategory(bm,'mysnips', B.CATEGORY_CONFIG.mysnips.label, true);
|
||||
var C_TPLS = forceCategory(bm,'lib-templates', B.CATEGORY_CONFIG['lib-templates'].label, true);
|
||||
|
||||
return { C_CUSTOM, C_STD, C_LIB, C_TPLS };
|
||||
}
|
||||
B._ensureCategories = ensureCategories;
|
||||
|
||||
/* Harte Zuordnung von Custom-IDs → Kategorien (ENTFERNT) */
|
||||
B.forceFixFlexCategories = function(ed){
|
||||
// Logik liegt jetzt im Plugin
|
||||
};
|
||||
|
||||
// **Harte** Sortierung der Kategorien – Sammlung + DOM (DOM-Fallback)
|
||||
B.enforceCategoryOrder = function(ed){
|
||||
try{
|
||||
var bm = ed.BlockManager;
|
||||
setTimeout(function(){
|
||||
try{
|
||||
var cont = bm.getContainer && bm.getContainer();
|
||||
if (!cont) return;
|
||||
// Notfall-DOM-Sortierung (falls das Plugin versagt)
|
||||
var nodes = Array.prototype.slice.call(cont.querySelectorAll('.gjs-block-category'));
|
||||
if (!nodes.length) return;
|
||||
|
||||
function nodeRank(n){
|
||||
var t = (n.querySelector('.gjs-title') || n).textContent || '';
|
||||
var s = t.trim().toLowerCase();
|
||||
if (s==='bibliothek: templates (ref)') return 1; // Prio 1
|
||||
if (s==='custom') return 2; // Prio 2
|
||||
if (s==='bausteine') return 3; // Prio 3
|
||||
if (s.startsWith('bibliothek')) return 4; // Prio 4+
|
||||
return 99;
|
||||
}
|
||||
|
||||
nodes.sort(function(a,b){ return nodeRank(a)-nodeRank(b); })
|
||||
.forEach(function(n){ cont.appendChild(n); });
|
||||
}catch{}
|
||||
},0);
|
||||
}catch{}
|
||||
};
|
||||
|
||||
/* Kategorien normalisieren + sortieren (ENTFERNT) */
|
||||
B.normalizeCategories = function(ed){
|
||||
// Logik liegt jetzt im Plugin
|
||||
};
|
||||
|
||||
/* Bausteine (Explizite Zuweisung zur ID 'bausteine') */
|
||||
B.addDefaultBlocks = function(ed){
|
||||
var bm = ed.BlockManager;
|
||||
B._ensureCategories(bm); // Stellen Sie sicher, dass alle Hauptkategorien existieren
|
||||
|
||||
// Explizite Kategorie-Definition basierend auf der ID 'bausteine'
|
||||
var cat_bausteine = { id:'bausteine', label:B.CATEGORY_CONFIG.bausteine.label, open:true };
|
||||
|
||||
addOnce(bm,'blk-img', { label:'Bild', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="3" y="5" width="18" height="14" fill="none" stroke="currentColor" stroke-width="2"/><path d="M8 13l3-3 5 6" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="9" cy="9" r="1.5" fill="currentColor"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td align="center"><img src="https://via.placeholder.com/600x200" alt="" width="600" style="max-width:100%;display:block;border:0;" /></td></tr></table>' });
|
||||
addOnce(bm,'blk-btn', { label:'Button', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="4" y="7" width="16" height="10" rx="5" fill="none" stroke="currentColor" stroke-width="2"/></svg>', content:'<table role="presentation" cellpadding="0" cellspacing="0" align="center" style="font-family:Arial,sans-serif;margin:16px auto;"><tr><td><a href="#" style="background:#0ea5e9;color:#fff;text-decoration:none;padding:12px 20px;border-radius:6px;display:inline-block;">Call to Action</a></td></tr></table>' });
|
||||
addOnce(bm,'blk-text', { label:'Text', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 7h16M4 12h10M4 17h8" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td style="font-size:16px;line-height:1.5;color:#0f172a;"><p style="margin:0 0 12px 0;">Überschrift</p><p style="margin:0;">Fließtext …</p></td></tr></table>' });
|
||||
addOnce(bm,'blk-2cols', { label:'2 Spalten', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="4" y="6" width="7" height="12" fill="none" stroke="currentColor" stroke-width="2"/><rect x="13" y="6" width="7" height="12" fill="none" stroke="currentColor" stroke-width="2"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td width="50%" valign="top" style="padding:8px;"><p style="margin:0;">Linke Spalte</p></td><td width="50%" valign="top" style="padding:8px;"><p style="margin:0;">Rechte Spalte</p></td></tr></table>' });
|
||||
addOnce(bm,'blk-600', { label:'Container 600px', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><rect x="3" y="7" width="18" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="2"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif"><tr><td align="center"><table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px;max-width:100%;background:#ffffff;border:1px solid #e5e7eb;border-radius:6px;"><tr><td style="padding:16px;"><p style="margin:0;">Inhalt hier hinein …</p></td></tr></table></td></tr></table>' });
|
||||
addOnce(bm,'blk-hr', { label:'Trenner', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 12h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><hr style="border:none;border-top:1px solid #e5e7eb;margin:0;" /></td></tr></table>' });
|
||||
addOnce(bm,'blk-spacer', { label:'Abstand', category:cat_bausteine, media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 6v12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="6" r="1.5" fill="currentColor"/><circle cx="12" cy="18" r="1.5" fill="currentColor"/></svg>', content:'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="height:16px;line-height:16px;font-size:0;"> </td></tr></table>' });
|
||||
};
|
||||
|
||||
/* Snippets → Custom (Initiale Zuweisung zur ID 'custom') */
|
||||
B.replaceSnippetBlocks = function(ed, list){
|
||||
try{
|
||||
var bm = ed.BlockManager;
|
||||
B._ensureCategories(bm);
|
||||
|
||||
var all = (bm.getAll && bm.getAll()) || [];
|
||||
(all.models || all).forEach(function(b){
|
||||
if (!b) return;
|
||||
var id=b.get&&b.get('id');
|
||||
// Entferne alte Snippets
|
||||
if(id && String(id).startsWith('snip-')) try{ bm.remove(id); }catch{}
|
||||
});
|
||||
|
||||
// Explizite Kategorie-Definition basierend auf der ID 'custom'
|
||||
var cat_custom = { id:'custom', label:B.CATEGORY_CONFIG.custom.label, open:true };
|
||||
|
||||
|
||||
(list||[]).forEach(function(raw){
|
||||
if(!raw) return;
|
||||
var html = raw.html || raw.content || '';
|
||||
if(!html) return;
|
||||
var id = 'snip-'+(raw.id ?? ('x'+Math.random().toString(36).slice(2)));
|
||||
addOnce(bm, id, {
|
||||
label: raw.name || ('Snippet '+(raw.id ?? '')),
|
||||
// Zuweisung zur 'Custom' Kategorie
|
||||
category: cat_custom,
|
||||
media:'<svg viewBox="0 0 24 24" width="22" height="22"><path d="M5 7h14M5 12h10M5 17h8" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>',
|
||||
content: html
|
||||
});
|
||||
});
|
||||
|
||||
}catch{}
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
};
|
||||
|
||||
/* Snippets nachladen (für Buttons) */
|
||||
B.fetchSnippets = async function(){
|
||||
try{
|
||||
var res = await fetch('../api.php?resource=snippets&action=list&t='+Date.now(), {
|
||||
credentials:'same-origin', cache:'no-store', headers:{'Cache-Control':'no-cache'}
|
||||
});
|
||||
var rows = await res.json();
|
||||
rows = rows && rows.items ? rows.items : (Array.isArray(rows) ? rows : []);
|
||||
return rows.map(function(r){ return { id:r.id, name:r.name, html:r.content||r.html||'' }; });
|
||||
}catch(e){ B.log && B.log('reload-snippets-error:'+e); return []; }
|
||||
};
|
||||
|
||||
})(window);
|
||||
166
public/assets/js/bridge/refs.js
Normal file
166
public/assets/js/bridge/refs.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/* /assets/js/bridge/refs.js — Referenzen & Custom Fix/Flex (FINAL KORRIGIERT: Snippet Cleanup) */
|
||||
(function (w) {
|
||||
var B = w.BridgeParts = w.BridgeParts || {};
|
||||
if (!B.CATEGORY_CONFIG) B.CATEGORY_CONFIG = {};
|
||||
|
||||
/* ---------- Basis-Konfig (unverändert) ---------- */
|
||||
var SHOW_SNIPPETS_IN_FIX_DEFAULT = false;
|
||||
var MODE = (window.__editorMode || 'templates').toLowerCase();
|
||||
|
||||
/* ---------- Hilfs-UI (unverändert) ---------- */
|
||||
B.editorRefPlaceholder = function (type, id, name) {
|
||||
var safe = (name || '').replace(/[<>&"]/g, '');
|
||||
return {
|
||||
html:
|
||||
'<div data-ref-type="' + type + '" data-ref-id="' + id + '" data-ref-name="' + safe + '" ' +
|
||||
'style="border:1px dashed #94a3b8;padding:8px;border-radius:6px;background:#f8fafc;margin:8px 0;">' +
|
||||
'<strong style="font:600 12px system-ui,Arial">Ref: ' + type + ' #' + id + '</strong>' +
|
||||
'<div style="font:12px system-ui,Arial;opacity:.8">' + safe + '</div>' +
|
||||
'</div>'
|
||||
};
|
||||
};
|
||||
|
||||
/* ---------- Loader (REST) (unverändert) ---------- */
|
||||
async function jsonList(url){
|
||||
try{
|
||||
var res = await fetch(url, { credentials:'same-origin', cache:'no-store' });
|
||||
var data = await res.json();
|
||||
return data && data.items ? data.items : (Array.isArray(data) ? data : []);
|
||||
}catch(e){ return []; }
|
||||
}
|
||||
B.fetchTemplates = async function(){
|
||||
var rows = await jsonList('../api.php?action=templates.list&t='+Date.now());
|
||||
return rows.map(r => ({ id:r.id, name:r.name }));
|
||||
};
|
||||
B.fetchTemplateFull = async function (id) {
|
||||
try {
|
||||
var url = '../api.php?action=templates.get&id=' + encodeURIComponent(id) + '&t=' + Date.now();
|
||||
var res = await fetch(url, { credentials: 'same-origin', cache: 'no-store' });
|
||||
var data = await res.json();
|
||||
var it = data && (data.item || data);
|
||||
return (it && (it.html || it.content)) ? (it.html || it.content) : '';
|
||||
} catch (e) { return ''; }
|
||||
};
|
||||
B.fetchSections = async function(){
|
||||
var rows = await jsonList('../api.php?action=sections.list&t='+Date.now());
|
||||
return rows.map(r => ({ id:r.id, name:r.name, html:r.html || '' }));
|
||||
};
|
||||
B.fetchBlocks = async function(){
|
||||
var rows = await jsonList('../api.php?action=blocks.list&t='+Date.now());
|
||||
return rows.map(r => ({ id:r.id, name:r.name, html:r.html || '' }));
|
||||
};
|
||||
B.fetchSnippets = async function(){
|
||||
var rows = await jsonList('../api.php?action=snippets.list&t='+Date.now());
|
||||
return rows.map(r => ({ id:r.id, name:r.name, html:r.html || r.content || '' }));
|
||||
};
|
||||
|
||||
/* ---------- lokale Helfer (unverändert) ---------- */
|
||||
function addOnce(bm, id, def){
|
||||
if (!id || typeof id !== 'string') return;
|
||||
try{ if (bm.get && bm.get(id)) return; bm.add(id, def); }catch{}
|
||||
}
|
||||
function removeByPrefix(bm, prefix){
|
||||
try{
|
||||
var all = (bm.getAll && bm.getAll()) || [];
|
||||
(all.models || all).forEach(function(b){
|
||||
if (!b) return;
|
||||
var id = (b.get && b.get('id')) || b.id || '';
|
||||
if (id && String(id).startsWith(prefix)) {
|
||||
try{ bm.remove(id); }catch{}
|
||||
}
|
||||
});
|
||||
}catch{}
|
||||
}
|
||||
|
||||
/* ---------- Referenzbibliothek (lib-*) (angepasst: Zuweisung zu 'custom') ---------- */
|
||||
B.addReferenceLibrary = function (ed, payload) {
|
||||
payload = payload || {};
|
||||
var templates = payload.templates || [];
|
||||
var sections = payload.sections || [];
|
||||
var blocks = payload.blocks || [];
|
||||
|
||||
// Aggressive Bereinigung aller lib-tpl-ref-* Blöcke
|
||||
removeByPrefix(ed.BlockManager, 'lib-tpl-ref-');
|
||||
|
||||
var bm = ed.BlockManager;
|
||||
if (B._ensureCategories) B._ensureCategories(bm); // Stellt sicher, dass die Hauptkategorien existieren
|
||||
|
||||
// Explizite Kategorie-Definitionen
|
||||
var cat_templates = { id:'lib-templates', label:B.CATEGORY_CONFIG['lib-templates'].label, open:true };
|
||||
var cat_custom = { id:'custom', label:B.CATEGORY_CONFIG.custom.label, open:true };
|
||||
|
||||
// Template-Referenzen (Prio 1) - NUR IM TEMPLATE-MODUS HINZUFÜGEN
|
||||
if (MODE === 'templates') {
|
||||
templates.forEach(function (t) {
|
||||
addOnce(bm, 'lib-tpl-ref-' + t.id, {
|
||||
label: (t.name || ('Vorlage #' + t.id)),
|
||||
category: cat_templates, // Zuweisung zur Prio 1
|
||||
media: '<svg viewBox="0 0 24 24" width="22" height="22"><rect x="4" y="5" width="16" height="14" rx="2" stroke="currentColor" fill="none" stroke-width="2"/></svg>',
|
||||
content: B.editorRefPlaceholder('template', t.id, t.name).html
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Sections-Referenzen (werden zu Custom umgeleitet, Prio 2)
|
||||
sections.forEach(function (s) {
|
||||
addOnce(bm, 'lib-sec-' + s.id, {
|
||||
label: s.name || ('Section #' + s.id),
|
||||
category: cat_custom, // Explizit Custom
|
||||
media: '<svg viewBox="0 0 24 24" width="22" height="22"><rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" fill="none" stroke-width="2"/></svg>',
|
||||
content: B.editorRefPlaceholder('section', s.id, s.name).html
|
||||
});
|
||||
});
|
||||
|
||||
// Blocks-Referenzen (werden zu Custom umgeleitet, Prio 2)
|
||||
blocks.forEach(function (b) {
|
||||
addOnce(bm, 'lib-blk-' + b.id, {
|
||||
label: b.name || ('Block #' + b.id),
|
||||
category: cat_custom, // Explizit Custom
|
||||
media: '<svg viewBox="0 0 24 24" width="22" height="22"><rect x="6" y="7" width="12" height="10" rx="2" stroke="currentColor" fill="none" stroke-width="2"/></svg>',
|
||||
content: B.editorRefPlaceholder('block', b.id, b.name).html
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* ---------- Custom Fix/Flex (ENTFERNT / GELEERT) ---------- */
|
||||
B.addCustomLibrary = function (ed, payload, mode) {
|
||||
/* (Keine Aktion nötig.) */
|
||||
};
|
||||
|
||||
// WICHTIGE NEUE FUNKTION: Entfernt alle alten Snippet-Blöcke
|
||||
B.addEditableTemplatesLibrary = function(ed) {
|
||||
// Aggressive Bereinigung aller alten flexiblen Snippet-Blöcke,
|
||||
// um Konflikte mit den neuen custom-snippet-* Blöcken zu vermeiden.
|
||||
removeByPrefix(ed.BlockManager, 'snip-');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
/* ---------- Ref-Sammlung für Speichern/Render (unverändert) ---------- */
|
||||
B.collectRefs = function (ed) {
|
||||
var root = ed.getWrapper && ed.getWrapper();
|
||||
var els = root && root.find ? root.find('[data-ref-type]') : [];
|
||||
var out = [];
|
||||
if (Array.isArray(els) && els.length) {
|
||||
els.forEach(function (el) {
|
||||
try {
|
||||
var m = el.getAttributes ? el.getAttributes() : {};
|
||||
var t = (m['data-ref-type'] || '').toString().toLowerCase();
|
||||
var i = parseInt(m['data-ref-id'] || '0', 10);
|
||||
if (!t || !i) return;
|
||||
if (!/^(template|section|block|snippet)$/.test(t)) return;
|
||||
if (t === 'snippet') return; // Snippets immer flex/by value
|
||||
|
||||
out.push({
|
||||
sort: out.length,
|
||||
ref_type: t === 'template' ? 'section' : t,
|
||||
ref_id: i,
|
||||
overrides_json: null,
|
||||
lock_to_version: null
|
||||
});
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
})(window);
|
||||
57
public/assets/js/toast.js
Normal file
57
public/assets/js/toast.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// assets/js/toast.js
|
||||
// Shows toast in the TOP LAYER: if a <dialog open> exists, append inside it.
|
||||
// Otherwise append to <body>. Default type = success (green).
|
||||
|
||||
window.Toast = (function () {
|
||||
// create (or reuse) a toast root inside a given container
|
||||
function ensureRoot(container) {
|
||||
// Find last (topmost) open dialog if not explicitly passed
|
||||
const host = container || (() => {
|
||||
const dialogs = Array.from(document.querySelectorAll('dialog[open]'));
|
||||
return dialogs.length ? dialogs[dialogs.length - 1] : document.body;
|
||||
})();
|
||||
|
||||
// Root per container (class-based to allow multiple roots)
|
||||
let root = host.querySelector(':scope > .toast-root');
|
||||
if (!root) {
|
||||
root = document.createElement('div');
|
||||
root.className = 'toast-root';
|
||||
host.appendChild(root);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function render(msg, type, duration, container) {
|
||||
const root = ensureRoot(container);
|
||||
|
||||
const n = document.createElement('div');
|
||||
n.className = 'toast' + (type === 'error' ? ' error' : ' success');
|
||||
n.innerHTML = `
|
||||
<span class="icon">${type === 'error' ? '⚠️' : '✅'}</span>
|
||||
<span class="content">${msg || ''}</span>
|
||||
<button class="close" aria-label="Schließen">✕</button>
|
||||
`;
|
||||
root.appendChild(n);
|
||||
|
||||
const close = () => { try { n.remove(); } catch {} };
|
||||
n.querySelector('.close')?.addEventListener('click', close);
|
||||
|
||||
const t = setTimeout(close, Number(duration) || 2200);
|
||||
// clean timer if user closes early
|
||||
n.addEventListener('remove', () => clearTimeout(t), { once:true });
|
||||
}
|
||||
|
||||
// API
|
||||
function show(msg, opt) {
|
||||
opt = opt || {};
|
||||
render(
|
||||
msg,
|
||||
opt.type === 'error' ? 'error' : 'success',
|
||||
opt.duration,
|
||||
opt.container // optional: pass a specific container
|
||||
);
|
||||
}
|
||||
|
||||
return { show };
|
||||
})();
|
||||
|
||||
69
public/assets/js/ui-auth.js
Normal file
69
public/assets/js/ui-auth.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// assets/js/ui-auth.js
|
||||
/**
|
||||
* Bindet einen Logout-Button und leitet nach dem Logout weiter (default: /login.php).
|
||||
* Usage:
|
||||
* import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
|
||||
* mountLogoutButton('#btn-logout', { redirect: '/login.php' });
|
||||
*/
|
||||
export function mountLogoutButton(selector = '#btn-logout', opts = {}) {
|
||||
const { redirect = '/login.php' } = opts;
|
||||
const btn = document.querySelector(selector);
|
||||
if (!btn) return;
|
||||
|
||||
// Doppelte Bindings verhindern
|
||||
if (btn.dataset.bound === '1') return;
|
||||
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
btn.disabled = true;
|
||||
const oldText = btn.textContent;
|
||||
btn.textContent = 'Abmelden…';
|
||||
try {
|
||||
await fetch('api.php?action=auth.logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Logout failed', err);
|
||||
// Fallback: trotzdem auf Login
|
||||
} finally {
|
||||
window.location.href = redirect;
|
||||
// Falls Redirect durch Policy o.ä. blockiert wäre:
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = oldText;
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
btn.dataset.bound = '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt bei Bedarf automatisch einen Floating-Logout-Button (oben rechts)
|
||||
* und bindet ihn (gut für Staging/Admin).
|
||||
*/
|
||||
export function ensureFloatingLogout(opts = {}) {
|
||||
if (document.querySelector('#btn-logout')) {
|
||||
mountLogoutButton('#btn-logout', opts);
|
||||
return;
|
||||
}
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'btn-logout';
|
||||
btn.textContent = 'Logout';
|
||||
Object.assign(btn.style, {
|
||||
position: 'fixed',
|
||||
right: '12px',
|
||||
top: '12px',
|
||||
zIndex: '10000',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
background: '#f5f5f5',
|
||||
cursor: 'pointer'
|
||||
});
|
||||
document.body.appendChild(btn);
|
||||
mountLogoutButton('#btn-logout', opts);
|
||||
}
|
||||
|
||||
26
public/assets/js/ui-create.js
Normal file
26
public/assets/js/ui-create.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { apiList, apiCreate, toast } from './api.js';
|
||||
export function initCreate(){
|
||||
const btn=document.getElementById('btn-new'), dlg=document.getElementById('createDialog'), form=document.getElementById('createForm'), fields=document.getElementById('createFields'), hint=document.getElementById('createHint');
|
||||
if(!btn||!dlg||!form||!fields) return;
|
||||
const curTab=()=>{ const a=document.querySelector('nav [data-tab].bg-sky-50')||document.querySelector('nav [data-tab]'); return a?a.getAttribute('data-tab'):'templates'; };
|
||||
|
||||
btn.onclick = async ()=>{
|
||||
fields.innerHTML=''; const tab=curTab();
|
||||
const name=document.createElement('input'); name.type='text'; name.required=true; name.placeholder='Name*'; name.className='w-full border rounded-lg px-3 py-2'; name.id='f-name'; fields.appendChild(name);
|
||||
async function addSel(id,label,res){ const sel=document.createElement('select'); sel.id=id; sel.className='w-full border rounded-lg px-3 py-2'; sel.innerHTML=`<option value="">(ohne ${label}-Zuordnung)</option>`; const data=await apiList(res); (data||[]).forEach(t=>{ const o=document.createElement('option'); o.value=t.id; o.textContent=`#${t.id} · ${t.name||''}`; sel.appendChild(o); }); fields.appendChild(sel); }
|
||||
if(tab==='sections') await addSel('f-template','Template','templates');
|
||||
if(tab==='blocks') await addSel('f-section','Section','sections');
|
||||
if(tab==='snippets') await addSel('f-block','Block','blocks');
|
||||
hint.textContent=`Neues ${tab} anlegen`; dlg.showModal();
|
||||
|
||||
form.onsubmit=async(e)=>{ e.preventDefault();
|
||||
const payload={ name:(document.getElementById('f-name')?.value||'').trim() }; if(!payload.name) return;
|
||||
if(tab==='snippets') payload.content=''; else payload.html='';
|
||||
if(tab==='sections') payload.template_id=document.getElementById('f-template')?.value||null;
|
||||
if(tab==='blocks') payload.section_id =document.getElementById('f-section')?.value ||null;
|
||||
if(tab==='snippets') payload.block_id =document.getElementById('f-block')?.value ||null;
|
||||
const r=await apiCreate(tab,payload); if(r&&r.id){ dlg.close(); toast('Erstellt',true); window.loadList && window.loadList(tab); } else { toast('Erstellen fehlgeschlagen',false,{duration:3000}); console.error('Create failed',r); }
|
||||
};
|
||||
};
|
||||
const cancel=document.getElementById('createCancel'); cancel && (cancel.onclick=()=>dlg.close());
|
||||
}
|
||||
438
public/assets/js/ui-editor.js
Normal file
438
public/assets/js/ui-editor.js
Normal file
@@ -0,0 +1,438 @@
|
||||
/* /assets/js/ui-editor.js (KORRIGIERT: Speichern wird an iFrame-Editor delegiert) */
|
||||
// Öffnen, Befüllen, Speichern (mit Live-HTML), Preview – Race-Schutz & Lade-Overlay.
|
||||
|
||||
import { apiUpdate, apiList, apiGet, toast, apiAction } from './api.js';
|
||||
|
||||
export function initEditor() {
|
||||
// ... (Alle Konstanten bleiben unverändert) ...
|
||||
const dlg = document.getElementById('editorDialog');
|
||||
const iframe = document.getElementById('editorFrame');
|
||||
const btnSave = document.getElementById('btn-save');
|
||||
const btnPreview = document.getElementById('btn-preview');
|
||||
const btnTest = document.getElementById('btn-test');
|
||||
const btnClose = document.getElementById('btn-close');
|
||||
const btnClear = document.getElementById('btn-clear-main');
|
||||
|
||||
const prevDlg = document.getElementById('previewDialog');
|
||||
const sendDlg = document.getElementById('sendTestDialog');
|
||||
const sendForm = document.getElementById('sendTestForm');
|
||||
const sendTo = document.getElementById('send_to');
|
||||
const sendSubject = document.getElementById('send_subject');
|
||||
const btnCancelSend= document.getElementById('btn-cancel-send');
|
||||
const btnSendNow = document.getElementById('btn-send-now');
|
||||
const prevFrame = document.getElementById('previewFrame');
|
||||
const btnPrevClose = document.getElementById('btn-close-preview');
|
||||
|
||||
let current = null; // { resource, id, name }
|
||||
let bridgeListener = null;
|
||||
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
|
||||
|
||||
const ok = (m) => toast(m, true);
|
||||
const err = (m) => toast(m, false);
|
||||
|
||||
// ---------- Hilfen ----------
|
||||
function activeMode() {
|
||||
const b = document.querySelector('nav [data-tab].bg-sky-50, nav [data-tab].text-sky-700, nav [data-tab].active');
|
||||
return (b?.dataset?.tab) || (current?.resource) || 'templates';
|
||||
}
|
||||
|
||||
function writeHtmlToFrame(html) {
|
||||
iframe.srcdoc = `<!doctype html><html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||
<body id="gjs">${html || ''}</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async function readEditedHtml() {
|
||||
const win = iframe?.contentWindow;
|
||||
const doc = iframe?.contentDocument;
|
||||
if (!win || !doc) return '';
|
||||
|
||||
const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
|
||||
if (ed && typeof ed.getHtml === 'function') {
|
||||
const html = ed.getHtml();
|
||||
const css = (typeof ed.getCss === 'function') ? ed.getCss() : '';
|
||||
return css ? `<style>${css}</style>\n${html}` : html;
|
||||
}
|
||||
const root = doc.querySelector('#gjs') || doc.body || doc.documentElement;
|
||||
return root ? root.innerHTML : '';
|
||||
}
|
||||
|
||||
function waitForEditor(maxMs = 8000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
(function poll() {
|
||||
const win = iframe?.contentWindow;
|
||||
const ed = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
|
||||
if (ed) return resolve(ed);
|
||||
if (Date.now() - start > maxMs) return reject(new Error('Editor not ready'));
|
||||
setTimeout(poll, 120);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
// 🚨 NEUE FUNKTION: Delegiert das Kommando an den Editor im iFrame
|
||||
async function delegateCommand(commandName) {
|
||||
try {
|
||||
const editor = await waitForEditor(3000);
|
||||
if (editor.Commands.has(commandName)) {
|
||||
// Führt den Command im iFrame aus (z.B. 'save-data')
|
||||
editor.runCommand(commandName);
|
||||
return true;
|
||||
} else {
|
||||
err(`Delegieren fehlgeschlagen: Command '${commandName}' nicht gefunden.`);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
err(`Delegieren fehlgeschlagen: Editor nicht bereit (${commandName}).`);
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ... (hideReadyBadge bleibt unverändert) ...
|
||||
function hideReadyBadge(doc) {
|
||||
if (!doc) return;
|
||||
const kill = () => {
|
||||
const el = doc.getElementById('badge');
|
||||
if (el) el.style.display = 'none';
|
||||
};
|
||||
kill();
|
||||
|
||||
const style = doc.createElement('style');
|
||||
style.textContent = `
|
||||
#badge { display:none !important; }
|
||||
.gjs-pn-status { display:none !important; }
|
||||
.ready-badge,
|
||||
.status-badge.ready,
|
||||
[data-status="ready"],
|
||||
[data-badge="ready"],
|
||||
.gjs-ready,
|
||||
.gjs-badge-ready { display:none !important; }
|
||||
`;
|
||||
doc.head.appendChild(style);
|
||||
|
||||
const mo = new MutationObserver(() => { kill(); /* hideByText(doc); */ });
|
||||
mo.observe(doc.documentElement, { childList: true, subtree: true });
|
||||
|
||||
setTimeout(() => { kill(); /* hideByText(doc); */ }, 150);
|
||||
setTimeout(() => { kill(); /* hideByText(doc); */ }, 500);
|
||||
setTimeout(() => { kill(); /* hideByText(doc); */ }, 1200);
|
||||
}
|
||||
|
||||
// ... (Lade-Overlay bleibt unverändert) ...
|
||||
let veilEl = null;
|
||||
function ensureVeil() {
|
||||
if (veilEl) return veilEl;
|
||||
veilEl = document.createElement('div');
|
||||
Object.assign(veilEl.style, {
|
||||
position:'absolute', inset:'0', background:'rgba(248,250,252,.85)',
|
||||
display:'flex', alignItems:'center', justifyContent:'center',
|
||||
zIndex:'2147483000', fontFamily:'system-ui, -apple-system, Segoe UI, Roboto, Arial',
|
||||
fontSize:'14px', color:'#0f172a'
|
||||
});
|
||||
veilEl.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;gap:.6rem;align-items:center;">
|
||||
<div class="spinner" style="width:28px;height:28px;border-radius:999px;border:3px solid #cbd5e1;border-top-color:#0ea5e9;animation:spin .8s linear infinite"></div>
|
||||
<div style="font-weight:500;">Lade Editor …</div>
|
||||
</div>
|
||||
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
|
||||
`;
|
||||
const host = dlg?.querySelector('.h-full, .flex, .flex-col') || dlg;
|
||||
(host || document.body).appendChild(veilEl);
|
||||
return veilEl;
|
||||
}
|
||||
function showVeil(){ ensureVeil().style.display = 'flex'; }
|
||||
function hideVeil(){ if (veilEl) veilEl.style.display = 'none'; }
|
||||
|
||||
// ... (Kontext-Filter-Ladung bleibt unverändert) ...
|
||||
async function listBlocksForTemplate(templateId){
|
||||
try {
|
||||
const direct = await apiList('blocks', { template_id: templateId });
|
||||
if (Array.isArray(direct) && direct.length) return direct;
|
||||
} catch {}
|
||||
const sections = await apiList('sections', { template_id: templateId }).catch(()=>[]);
|
||||
const out = [];
|
||||
for (const s of (sections || [])) {
|
||||
const b = await apiList('blocks', { section_id: s.id }).catch(()=>[]);
|
||||
if (b?.length) out.push(...b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Snippets eines Templates (direkt oder via Sections->Blocks als Fallback)
|
||||
async function listSnippetsForTemplate(templateId){
|
||||
try {
|
||||
const direct = await apiList('snippets', { template_id: templateId });
|
||||
if (Array.isArray(direct) && direct.length) return direct;
|
||||
} catch {}
|
||||
const sections = await apiList('sections', { template_id: templateId }).catch(()=>[]);
|
||||
const blocksAll = [];
|
||||
for (const s of (sections || [])) {
|
||||
const b = await apiList('blocks', { section_id: s.id }).catch(()=>[]);
|
||||
if (b?.length) blocksAll.push(...b);
|
||||
}
|
||||
const out = [];
|
||||
for (const b of blocksAll) {
|
||||
const sn = await apiList('snippets', { block_id: b.id }).catch(()=>[]);
|
||||
if (sn?.length) out.push(...sn);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Referenz-Bibliothek (für „Custom – Fix“)
|
||||
async function buildRefLibForContext(ctx){
|
||||
const kind = (ctx.resource || 'templates').replace(/s$/,''); // template|section|block
|
||||
const id = ctx.id;
|
||||
if (kind === 'template'){
|
||||
const [sections, blocks] = await Promise.all([
|
||||
apiList('sections', { template_id: id }).catch(()=>[]),
|
||||
listBlocksForTemplate(id)
|
||||
]);
|
||||
return { sections, blocks };
|
||||
}
|
||||
if (kind === 'section'){
|
||||
const blocks = await apiList('blocks', { section_id: id }).catch(()=>[]);
|
||||
return { sections: [], blocks };
|
||||
}
|
||||
return { sections: [], blocks: [] }; // block -> keine Sections/Blocks in Fix
|
||||
}
|
||||
// Snippets (für „Custom – Flex“) kontextabhängig
|
||||
async function buildSnippetsForContext(ctx){
|
||||
const kind = (ctx.resource || 'templates').replace(/s$/,'');
|
||||
const id = ctx.id;
|
||||
let rows = [];
|
||||
if (kind === 'template') rows = await listSnippetsForTemplate(id);
|
||||
else if (kind === 'section') rows = await apiList('snippets', { section_id: id }).catch(()=>[]);
|
||||
else if (kind === 'block') rows = await apiList('snippets', { block_id: id }).catch(()=>[]);
|
||||
else rows = await apiList('snippets').catch(()=>[]);
|
||||
return (rows || []).map(r => ({ id: r.id, name: r.name, html: r.content || r.html || '' }));
|
||||
}
|
||||
|
||||
// ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ----------
|
||||
async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token }) {
|
||||
if (token !== reqToken) return; // veraltete Anfrage ignorieren
|
||||
|
||||
const win = iframe?.contentWindow;
|
||||
const doc = iframe?.contentDocument;
|
||||
|
||||
// NEU: HTML wird NUR über postMessage gesendet. Die Bridge im iFrame ist verantwortlich
|
||||
// dafür, das HTML in GrapesJS zu setzen, NACHDEM ihre Plugins fertig sind.
|
||||
try {
|
||||
win?.postMessage({ source:'admin', type:'init', mode, html: html || '', snippets: snippets || [], ref: ref || {} }, '*');
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
// Warten auf Editor ist noch sinnvoll, um das Lade-Badge zu unterdrücken,
|
||||
// aber wir manipulieren den Editor NICHT MEHR direkt von hier aus.
|
||||
await waitForEditor(6000);
|
||||
if (token !== reqToken) return;
|
||||
|
||||
// ... (Gelöschte Logik: ed.setComponents(html) ist nun in der Bridge-Logik) ...
|
||||
} catch {
|
||||
/* Falls GJS noch nicht bereit ist, arbeiten wir nur via postMessage. */
|
||||
}
|
||||
|
||||
try { hideReadyBadge(doc); } catch {}
|
||||
if (token === reqToken) hideVeil();
|
||||
}
|
||||
|
||||
// ---------- Öffnen ----------
|
||||
async function open(item, resource) {
|
||||
current = {
|
||||
resource: String(resource || activeMode() || 'templates').toLowerCase(),
|
||||
id: Number(item?.id || 0),
|
||||
name: item?.name || ''
|
||||
};
|
||||
if (!current.id) return err('Ungültige ID');
|
||||
|
||||
// globaler Kontext
|
||||
window.__currentItemId = current.id;
|
||||
window.__currentEditorCtx = { id: current.id, mode: current.resource };
|
||||
|
||||
// Neuen Token erzeugen & alten Listener entfernen
|
||||
reqToken++;
|
||||
const myToken = reqToken;
|
||||
if (bridgeListener) window.removeEventListener('message', bridgeListener);
|
||||
bridgeListener = null;
|
||||
|
||||
// Overlay zeigen
|
||||
showVeil();
|
||||
|
||||
// Daten parallel laden (fresh HTML + kontextgefilterte Snippets + Referenzen)
|
||||
let fresh = '';
|
||||
let snippets = [];
|
||||
let refLib = { sections: [], blocks: [] };
|
||||
|
||||
await Promise.all([
|
||||
(async() => {
|
||||
try {
|
||||
const row = await apiGet(current.resource, current.id);
|
||||
// API liefert jetzt top-level html/content; fallback auf item.*
|
||||
fresh = row?.html ?? row?.content ?? row?.item?.html ?? row?.item?.content ?? '';
|
||||
} catch {}
|
||||
})(),
|
||||
(async() => { snippets = await buildSnippetsForContext(current); })(),
|
||||
(async() => { refLib = await buildRefLibForContext(current); })()
|
||||
]);
|
||||
|
||||
// iFrame-Load -> Bridge-Ready abhören
|
||||
iframe.onload = function () {
|
||||
if (myToken !== reqToken) return;
|
||||
|
||||
try { hideReadyBadge(iframe.contentDocument); } catch {}
|
||||
|
||||
bridgeListener = (ev) => {
|
||||
const d = ev?.data || {};
|
||||
if (!d) return;
|
||||
// wir erwarten Nachrichten aus der Bridge/Editor
|
||||
if (d.source !== 'bridge' && d.source !== 'editor') return;
|
||||
if (myToken !== reqToken) return;
|
||||
|
||||
// NEU: Wenn der Editor meldet, dass er *gespeichert* hat,
|
||||
// aktualisieren wir die Liste im Elternfenster
|
||||
if (d.type === 'save:success') {
|
||||
ok('Gespeichert');
|
||||
try {
|
||||
if (typeof window.reloadActiveList === 'function') window.reloadActiveList();
|
||||
else if (typeof window.__reloadList === 'function') window.__reloadList(current.resource);
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
|
||||
// neue Bridge meldet gjs:ready; ältere evtl. core-ready/bridge:ready
|
||||
if (d.type === 'gjs:ready' || d.type === 'core-ready' || d.type === 'bridge:ready' || d.type === 'bridge:booted') {
|
||||
pushInitialHtmlToEditor({
|
||||
mode: current.resource,
|
||||
html: fresh,
|
||||
snippets,
|
||||
ref: {
|
||||
sections: (refLib.sections || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' })),
|
||||
blocks: (refLib.blocks || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' }))
|
||||
},
|
||||
token: myToken
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', bridgeListener);
|
||||
|
||||
// Fallback, falls kein Ready ankommt
|
||||
setTimeout(() => {
|
||||
pushInitialHtmlToEditor({
|
||||
mode: current.resource,
|
||||
html: fresh,
|
||||
snippets,
|
||||
ref: {
|
||||
sections: (refLib.sections || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' })),
|
||||
blocks: (refLib.blocks || []).map(r => ({ id:r.id, name:r.name, html:r.html || '' }))
|
||||
},
|
||||
token: myToken
|
||||
});
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
// Jetzt den Editor-Core laden (erst NACH about:blank)
|
||||
iframe.src = `editor/editor-core.php?mode=${encodeURIComponent(current.resource)}&id=${current.id}&t=${Date.now()}`;
|
||||
|
||||
dlg?.showModal?.();
|
||||
}
|
||||
|
||||
// ---------- Speichern (DELEGIERT) ----------
|
||||
// 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!
|
||||
async function save() {
|
||||
if (!current?.id) return err('Keine aktive ID');
|
||||
|
||||
const mode = activeMode();
|
||||
if (mode !== 'snippets') { // Nur Templates/Blocks/Sections delegieren, Snippets behalten die alte Logik (NUR HTML)
|
||||
return delegateCommand('save-data');
|
||||
}
|
||||
|
||||
// Alte Snippet-Logik beibehalten (falls der Snippet-Editor nicht GrapesJS ist und nur HTML erwartet)
|
||||
const liveHtml = await readEditedHtml();
|
||||
|
||||
const payload = { id: current.id, content: liveHtml };
|
||||
|
||||
const res = await apiUpdate(mode, current.id, payload);
|
||||
if (!res?.ok) { err('Speichern fehlgeschlagen'); return; }
|
||||
|
||||
ok('Gespeichert');
|
||||
try {
|
||||
if (typeof window.reloadActiveList === 'function') await window.reloadActiveList();
|
||||
else if (typeof window.__reloadList === 'function') window.__reloadList(mode);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ... (Der Rest der Funktionen bleibt unverändert) ...
|
||||
async function clearEditor() {
|
||||
const win = iframe?.contentWindow;
|
||||
const ed = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
|
||||
if (ed) {
|
||||
ed.setComponents('');
|
||||
ed.setStyle('');
|
||||
} else {
|
||||
writeHtmlToFrame('');
|
||||
}
|
||||
}
|
||||
|
||||
async function openPreview() {
|
||||
const html = await readEditedHtml();
|
||||
prevFrame.srcdoc = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${html || '<em>(leer)</em>'}</body></html>`;
|
||||
prevDlg?.showModal?.();
|
||||
}
|
||||
|
||||
async function openSend() {
|
||||
sendSubject.value = 'Testversand';
|
||||
sendTo.value = '';
|
||||
sendDlg?.showModal?.();
|
||||
}
|
||||
function closeSend(){ sendDlg?.close?.(); }
|
||||
|
||||
async function doSend(ev){
|
||||
ev?.preventDefault?.();
|
||||
const to = sendTo.value.trim();
|
||||
if(!to){ toast("Bitte Empfänger angeben", false); return; }
|
||||
const win = iframe?.contentWindow;
|
||||
const ctx = (win && win.__currentEditorCtx) || {};
|
||||
const id = (window.__currentItemId || ctx?.id || 0);
|
||||
if(!id){ toast("Kein Template geladen", false); return; }
|
||||
// Hier wird der gespeicherte HTML-Code verwendet, nicht der Live-HTML, da apiAction
|
||||
// keine Live-Daten erwartet. Es geht um template_id.
|
||||
const r = await apiAction('templates.test_send', { method:'POST', data:{ template_id: id, to, subject: sendSubject.value || 'Testversand' } });
|
||||
if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }
|
||||
}
|
||||
function closePreview(){ prevDlg?.close?.(); }
|
||||
|
||||
function close() {
|
||||
// nächstes Öffnen invalidiert laufende asyncs
|
||||
reqToken++;
|
||||
|
||||
try { iframe.contentWindow?.postMessage({source:'admin',type:'reset'}, '*'); } catch {}
|
||||
if (bridgeListener) window.removeEventListener('message', bridgeListener);
|
||||
bridgeListener = null;
|
||||
|
||||
hideVeil();
|
||||
dlg?.close?.();
|
||||
|
||||
// iFrame zurück auf blank
|
||||
iframe.src = 'about:blank#' + Date.now();
|
||||
|
||||
// Kontext leeren
|
||||
current = null;
|
||||
window.__currentItemId = undefined;
|
||||
window.__currentEditorCtx = undefined;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
btnSave && (btnSave.onclick = save);
|
||||
btnClear && (btnClear.onclick = clearEditor);
|
||||
btnClose && (btnClose.onclick = close);
|
||||
btnPrevClose && (btnPrevClose.onclick = closePreview);
|
||||
btnPreview && (btnPreview.onclick = openPreview);
|
||||
btnTest && (btnTest.onclick = openSend);
|
||||
btnCancelSend&& (btnCancelSend.onclick= closeSend);
|
||||
sendForm && (sendForm.onsubmit = doSend);
|
||||
|
||||
// Public API
|
||||
window.EditorUI = { open, save, close, clear: clearEditor, preview: openPreview };
|
||||
}
|
||||
|
||||
// Default-Export + globaler Fallback
|
||||
export default initEditor;
|
||||
window.initEditor = initEditor;
|
||||
170
public/assets/js/ui-list.js
Normal file
170
public/assets/js/ui-list.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import { apiList, apiGet, apiDelete, apiUpdate, toast } from './api.js';
|
||||
|
||||
function esc(s=''){
|
||||
return String(s)
|
||||
.replace(/&/g,'&')
|
||||
.replace(/</g,'<')
|
||||
.replace(/>/g,'>')
|
||||
.replace(/"/g,'"')
|
||||
.replace(/'/g,''');
|
||||
}
|
||||
|
||||
async function openSnippetEditor(id){
|
||||
const dlg = document.getElementById('editSnippetDialog');
|
||||
const form = document.getElementById('editSnippetForm');
|
||||
const inpName = document.getElementById('edit_snip_name');
|
||||
const taContent = document.getElementById('edit_snip_content');
|
||||
const btnCancel = document.getElementById('editSnippetCancel');
|
||||
|
||||
// Daten laden
|
||||
let row = {};
|
||||
try { row = await apiGet('snippets', id) || {}; } catch(e){}
|
||||
|
||||
if (inpName) inpName.value = row.name || '';
|
||||
if (taContent) taContent.value = row.content || '';
|
||||
|
||||
function cleanup(){
|
||||
form && form.removeEventListener('submit', onSubmit);
|
||||
btnCancel && (btnCancel.onclick = null);
|
||||
}
|
||||
|
||||
async function onSubmit(ev){
|
||||
ev.preventDefault();
|
||||
try{
|
||||
const res = await apiUpdate('snippets', id, {
|
||||
name: inpName ? inpName.value : '',
|
||||
content: taContent ? taContent.value : ''
|
||||
});
|
||||
toast(res && res.ok ? 'Snippet gespeichert' : 'Speichern fehlgeschlagen', !!(res && res.ok));
|
||||
dlg && dlg.close();
|
||||
cleanup();
|
||||
// Liste neu laden
|
||||
loadList('snippets');
|
||||
}catch(e){
|
||||
toast('Speichern fehlgeschlagen', false);
|
||||
}
|
||||
}
|
||||
|
||||
if (form) form.addEventListener('submit', onSubmit, { once:false });
|
||||
if (btnCancel) btnCancel.onclick = () => { dlg && dlg.close(); cleanup(); };
|
||||
|
||||
dlg && dlg.showModal();
|
||||
}
|
||||
|
||||
export async function loadList(resource){
|
||||
const el=document.getElementById(`view-${resource}`); if(!el) return;
|
||||
|
||||
el.innerHTML=`<div class='rounded-2xl border bg-white overflow-hidden'>
|
||||
<div class='px-4 py-2 border-b bg-gray-50 text-sm font-medium'>${resource.charAt(0).toUpperCase()+resource.slice(1)}</div>
|
||||
<div id='list-${resource}' class='divide-y'>Lade …</div></div>`;
|
||||
|
||||
const data=await apiList(resource);
|
||||
const list=el.querySelector(`#list-${resource}`);
|
||||
|
||||
if(!Array.isArray(data)||data.length===0){
|
||||
list.innerHTML=`<div class='p-4 text-sm text-gray-500'>Keine Einträge</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
function parentBadge(r,it){
|
||||
if(r==='sections'&&it.template_id) return `<span class="chip"><span class="dot"></span> Template #${it.template_id}${it.template_name ? ' · '+esc(it.template_name) : ''}</span>`;
|
||||
if(r==='blocks'&&it.section_id) return `<span class="chip"><span class="dot"></span> Section #${it.section_id}${it.section_name ? ' · '+esc(it.section_name) : ''}</span>`;
|
||||
if(r==='snippets'&&it.block_id) return `<span class="chip"><span class="dot"></span> Block #${it.block_id}${it.block_name ? ' · '+esc(it.block_name) : ''}</span>`;
|
||||
return '<span class="chip"><span class="dot"></span> frei</span>';
|
||||
}
|
||||
|
||||
list.innerHTML=data.map(item=>{
|
||||
const name = esc(item.name||'');
|
||||
const openBtn = (['templates','sections','blocks'].includes(resource))
|
||||
? `<button class='btn' data-open='${resource}:${item.id}'>Im E-Mail-Editor öffnen</button>` : '';
|
||||
|
||||
const editBtn = (resource==='snippets')
|
||||
? `<button class='btn' data-edit='snippets:${item.id}'>Bearbeiten</button>` : '';
|
||||
|
||||
const prevBtn = `<button class='btn' data-preview='${resource}:${item.id}'>Vorschau</button>`;
|
||||
const delBtn = `<button class='btn btn-danger' data-del='${resource}:${item.id}' data-name='${name}'>Löschen</button>`;
|
||||
const debugBtn= `<a class='btn' href='api.php?resource=${resource}&action=get&id=${item.id}' target='_blank' rel='noopener'>GET</a>`;
|
||||
|
||||
return `<div class='p-3 flex items-center gap-3'>
|
||||
<div class='min-w-48 font-medium truncate' title="${name}">${name || '(ohne Name)'}</div>
|
||||
<div class='text-xs text-gray-500'>#${item.id}</div>
|
||||
<div class='text-xs'>${parentBadge(resource,item)}</div>
|
||||
<div class='ms-auto flex gap-2'>${[openBtn, editBtn, prevBtn, delBtn, debugBtn].filter(Boolean).join('')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// --- Editor öffnen (ANPASSUNG) -----------------------------------------
|
||||
list.querySelectorAll('[data-open]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const [res,id]=b.dataset.open.split(':');
|
||||
|
||||
// Detail laden, um Name + aktuellen HTML/Content zu haben
|
||||
const obj = await apiGet(res,id);
|
||||
const name = obj?.name || '';
|
||||
const html = obj ? (obj.html ?? obj.content ?? '') : '';
|
||||
|
||||
// Globale Kontexte (werden von Editor/anderen Modulen genutzt)
|
||||
window.__currentItemId = Number(id);
|
||||
window.__currentEditorCtx = { id:Number(id), mode:res };
|
||||
|
||||
// Bevorzugt EditorUI.open nutzen; Fallback: __openEditor (Bestand)
|
||||
if (window.EditorUI && typeof window.EditorUI.open === 'function') {
|
||||
window.EditorUI.open({ id:Number(id), name, html }, res);
|
||||
} else if (window.__openEditor) {
|
||||
window.__openEditor({ resource:res, id:Number(id), name, html });
|
||||
} else {
|
||||
console.warn('Kein Editor-Entry-Point gefunden (EditorUI.open / __openEditor).');
|
||||
toast('Editor ist nicht initialisiert.', false);
|
||||
}
|
||||
}));
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// edit snippet
|
||||
list.querySelectorAll('[data-edit]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const [, id] = b.dataset.edit.split(':');
|
||||
await openSnippetEditor(id);
|
||||
}));
|
||||
|
||||
// preview
|
||||
const prevDlg=document.getElementById('previewDialog'), prevFrame=document.getElementById('previewFrame');
|
||||
list.querySelectorAll('[data-preview]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const [res,id]=b.dataset.preview.split(':');
|
||||
const obj=await apiGet(res,id);
|
||||
const html=(obj?.html||obj?.content||'<em>(leer)</em)');
|
||||
prevFrame.srcdoc='<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>'+html+'</body></html>';
|
||||
prevDlg.showModal();
|
||||
}));
|
||||
|
||||
// delete
|
||||
const delDlg=document.getElementById('deleteDialog'),
|
||||
delText=document.getElementById('deleteText'),
|
||||
delForm=document.getElementById('deleteForm'),
|
||||
delCancel=document.getElementById('deleteCancel');
|
||||
|
||||
let pending=null;
|
||||
delCancel && (delCancel.onclick=()=>{pending=null;delDlg.close();});
|
||||
|
||||
list.querySelectorAll('[data-del]').forEach(b=>b.addEventListener('click',()=>{
|
||||
const [res,id]=b.dataset.del.split(':'); const nm=b.dataset.name||'';
|
||||
pending={res,id,nm};
|
||||
delText && (delText.innerHTML=`Soll <strong>${nm || '(ohne Name)'} #${id}</strong> aus <strong>${res}</strong> wirklich gelöscht werden?<br><span class="text-rose-600">Achtung:</span> Kinder-Elemente werden <em>nicht</em> automatisch mit gelöscht.`);
|
||||
delDlg.showModal();
|
||||
}));
|
||||
|
||||
delForm && (delForm.onsubmit=async(e)=>{
|
||||
e.preventDefault();
|
||||
if(!pending) return delDlg.close();
|
||||
const r=await apiDelete(pending.res,pending.id);
|
||||
delDlg.close();
|
||||
toast(r&&r.ok?'Gelöscht':'Löschen fehlgeschlagen', !!(r&&r.ok), {duration:3000});
|
||||
loadList(resource);
|
||||
});
|
||||
}
|
||||
|
||||
export function initLists(){
|
||||
loadList('templates');
|
||||
// Public reload helper (wird vom Snippet-Editor genutzt)
|
||||
window.__reloadList = loadList;
|
||||
// Backwards compat (falls woanders genutzt)
|
||||
window.loadList = loadList;
|
||||
}
|
||||
|
||||
11
public/assets/js/ui-tabs.js
Normal file
11
public/assets/js/ui-tabs.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export function initTabs(){
|
||||
const tabs=document.querySelectorAll('nav [data-tab]'); if(!tabs.length) return;
|
||||
const views={ templates:document.getElementById('view-templates'), sections:document.getElementById('view-sections'), blocks:document.getElementById('view-blocks'), snippets:document.getElementById('view-snippets') };
|
||||
tabs.forEach(btn=>btn.addEventListener('click',()=>{
|
||||
tabs.forEach(b=>b.classList.remove('bg-sky-50','text-sky-700'));
|
||||
btn.classList.add('bg-sky-50','text-sky-700');
|
||||
document.querySelectorAll('.view').forEach(v=>v.classList.add('hidden'));
|
||||
const tab=btn.dataset.tab; views[tab]?.classList.remove('hidden');
|
||||
window.loadList && window.loadList(tab);
|
||||
}));
|
||||
}
|
||||
158
public/assets/js/ui-tools.js
Normal file
158
public/assets/js/ui-tools.js
Normal file
@@ -0,0 +1,158 @@
|
||||
// assets/js/ui-tools.js
|
||||
// Öffnet API-Health (JSON), DB-Doctor (Iframe) & beliebige JSON-GETs im Popup,
|
||||
// ohne die Seite zu verlassen. Links bleiben als Fallback nutzbar.
|
||||
|
||||
(function () {
|
||||
const dlg = document.getElementById('toolsDialog');
|
||||
if (!dlg) return;
|
||||
|
||||
const title = document.getElementById('toolsTitle');
|
||||
const btnX = document.getElementById('toolsClose');
|
||||
const btnCopy = document.getElementById('toolsCopy');
|
||||
const btnDl = document.getElementById('toolsDownload');
|
||||
|
||||
const jsonWrap = document.getElementById('toolsJsonWrap');
|
||||
const jsonPre = document.getElementById('toolsJsonPre');
|
||||
const frame = document.getElementById('toolsFrame');
|
||||
|
||||
function showJson(obj, ttl) {
|
||||
title.textContent = ttl || 'Antwort (JSON)';
|
||||
const txt = (typeof obj === 'string') ? obj : JSON.stringify(obj, null, 2);
|
||||
jsonPre.textContent = txt;
|
||||
jsonWrap.classList.remove('hidden');
|
||||
frame.classList.add('hidden');
|
||||
btnCopy.classList.remove('hidden');
|
||||
btnDl.classList.remove('hidden');
|
||||
try { dlg.showModal(); } catch {}
|
||||
}
|
||||
|
||||
function showFrame(url, ttl) {
|
||||
title.textContent = ttl || 'Werkzeug';
|
||||
frame.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
|
||||
frame.classList.remove('hidden');
|
||||
jsonWrap.classList.add('hidden');
|
||||
btnCopy.classList.add('hidden');
|
||||
btnDl.classList.add('hidden');
|
||||
try { dlg.showModal(); } catch {}
|
||||
}
|
||||
|
||||
btnX?.addEventListener('click', () => {
|
||||
try { dlg.close(); } catch {}
|
||||
frame.src = 'about:blank';
|
||||
});
|
||||
|
||||
btnCopy?.addEventListener('click', async () => {
|
||||
const txt = jsonPre.textContent || '';
|
||||
try {
|
||||
await navigator.clipboard.writeText(txt);
|
||||
// optional: kleines Feedback
|
||||
(window.Toast?.show || window.toast || (()=>{}))('In Zwischenablage kopiert');
|
||||
} catch {}
|
||||
});
|
||||
|
||||
btnDl?.addEventListener('click', () => {
|
||||
const blob = new Blob([jsonPre.textContent || ''], { type:'application/json;charset=utf-8' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g,'-');
|
||||
a.download = `response-${stamp}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 0);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 1) Offizieller Weg: Links mit data-popup="json" | "frame"
|
||||
// ------------------------------------------------------------
|
||||
document.addEventListener('click', async (ev) => {
|
||||
const a = ev.target.closest('a[data-popup]');
|
||||
if (!a) return;
|
||||
|
||||
const mode = (a.getAttribute('data-popup') || '').toLowerCase();
|
||||
const href = a.getAttribute('href') || '#';
|
||||
const ttl = a.getAttribute('data-title') || a.textContent.trim() || 'Werkzeug';
|
||||
|
||||
if (!/^json|frame$/.test(mode)) return;
|
||||
// Cmd/Strg-Klick & Mittelklick respektieren (neuer Tab)
|
||||
if (ev.metaKey || ev.ctrlKey || ev.button === 1) return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
if (mode === 'frame') {
|
||||
showFrame(href, ttl);
|
||||
return;
|
||||
}
|
||||
|
||||
// mode === 'json'
|
||||
try {
|
||||
const r = await fetch(href, { credentials: 'include' });
|
||||
const txt = await r.text();
|
||||
let data = null;
|
||||
try { data = JSON.parse(txt); } catch { data = txt; }
|
||||
showJson(data, ttl);
|
||||
} catch (e) {
|
||||
showJson({ ok:false, error: String(e) }, ttl);
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 2) Fallback: *alle* API-GET-Links (action=get) ohne data-popup
|
||||
// -> automatisch im JSON-Popup öffnen
|
||||
// ------------------------------------------------------------
|
||||
document.addEventListener('click', async (ev) => {
|
||||
const a = ev.target.closest('a');
|
||||
if (!a) return;
|
||||
// bereits oben behandelt
|
||||
if (a.hasAttribute('data-popup')) return;
|
||||
|
||||
const href = a.getAttribute('href') || '';
|
||||
// nur api.php-GET-Routen mit action=get abfangen
|
||||
if (!/api\.php/i.test(href) || !/[?&]action=get(&|$)/i.test(href)) return;
|
||||
|
||||
// Cmd/Strg/Mittelklick respektieren
|
||||
if (ev.metaKey || ev.ctrlKey || ev.button === 1) return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
const makeTitle = () => {
|
||||
try {
|
||||
const u = new URL(href, location.href);
|
||||
const res = u.searchParams.get('resource') || 'resource';
|
||||
const id = u.searchParams.get('id') || '';
|
||||
// Optional: Name aus data-title wenn vorhanden
|
||||
const custom = a.getAttribute('data-title');
|
||||
return custom || `GET ${res}${id ? ` #${id}` : ''}`;
|
||||
} catch { return 'GET'; }
|
||||
};
|
||||
|
||||
const title = makeTitle();
|
||||
|
||||
// Popup öffnen (wie oben)
|
||||
try {
|
||||
const r = await fetch(href, { credentials: 'include' });
|
||||
const txt = await r.text();
|
||||
let data = null;
|
||||
try { data = JSON.parse(txt); } catch { data = txt; }
|
||||
showJson(data, title);
|
||||
} catch (e) {
|
||||
showJson({ ok:false, error: String(e) }, title);
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Utility: Öffnen aus Code
|
||||
window.AdminTools = {
|
||||
openJson(url, title) {
|
||||
fetch(url, { credentials: 'include' })
|
||||
.then(r => r.text())
|
||||
.then(txt => {
|
||||
try { showJson(JSON.parse(txt), title); }
|
||||
catch { showJson(txt, title); }
|
||||
})
|
||||
.catch(err => showJson({ ok:false, error:String(err) }, title));
|
||||
},
|
||||
openFrame(url, title) {
|
||||
showFrame(url, title);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
496
public/editor/bridge-core (Kopie).js
Normal file
496
public/editor/bridge-core (Kopie).js
Normal file
@@ -0,0 +1,496 @@
|
||||
/* /editor/bridge-core.js — Loader + Orchestrator (FINAL & LOG-KONTROLLIERT) */
|
||||
(function () {
|
||||
|
||||
// --- Initialisierung BridgeParts (B) und Plugin-Registry ---
|
||||
if (!window.BridgeParts) window.BridgeParts = {};
|
||||
const B = window.BridgeParts;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🎯 LOKALE LOG-KONFIGURATION & WRAPPER
|
||||
// ----------------------------------------------------------------------
|
||||
const PluginName = 'bridge-core';
|
||||
|
||||
// Setzen Sie dies auf 'false', um alle Logs NUR für dieses Plugin zu deaktivieren.
|
||||
if (B.LOG_CONFIG) {
|
||||
B.LOG_CONFIG.PLUGINS[PluginName] = false; // bridge-core spezifisch deaktivieren (optional)
|
||||
}
|
||||
|
||||
/**
|
||||
* NEUER LOKALER WRAPPER, der die zentrale B.log Funktion verwendet.
|
||||
* Der unformatierte Fallback WURDE ENTFERNT, da er das console.log erzeugt hat.
|
||||
* Die ersten kritischen Logs WURDEN EBENFALLS ENTFERNT, da sie vor B.log lagen.
|
||||
*/
|
||||
const log = (type, message, color = '#1E90FF', logType = 'info', force = false) => {
|
||||
// Loggt NUR, wenn B.log verfügbar ist (aus general-functions.js).
|
||||
if (typeof B.log === 'function') {
|
||||
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
|
||||
}
|
||||
// Ansonsten wird NICHTS geloggt, bis general-functions.js geladen ist.
|
||||
};
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// 🛑 GLOBALER LOG ZUR BESTÄTIGUNG DER SKRIPT-AUSFÜHRUNG
|
||||
// Dieser erste Log-Aufruf wird nun still ignoriert, da B.log noch fehlt.
|
||||
// Er wird durch den SUCCESS-Log der general-functions.js ersetzt.
|
||||
// log('START', `SKRIPT-AUSFÜHRUNG GESTARTET.`, '#DC143C', 'info', true); // DEAKTIVIERT/IGNORIERT DURCH FEHLENDEN B.log
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🛑 KONFIGURATION: NEWSLETTER-PRESET-TOGGLE
|
||||
// ----------------------------------------------------------------------
|
||||
const LOAD_NEWSLETTER_PRESET = false; // <<< KRITISCHER FIX: Auf FALSE gesetzt, um den "defaults" Konflikt zu beheben!
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
if (window.__bridgeCoreInitialized) {
|
||||
log('INIT ABORT', 'Bridge Core wurde bereits initialisiert.', 'orange');
|
||||
return;
|
||||
}
|
||||
window.__bridgeCoreInitialized = true;
|
||||
|
||||
// --- Initialisierung BridgeParts (B) und Plugin-Registry ---
|
||||
B.BASE_PATH_BRIDGE = '../assets/js/bridge/';
|
||||
B.BASE_PATH_CONFIG = B.BASE_PATH_BRIDGE;
|
||||
|
||||
B.GrapesJSPlugins = [];
|
||||
|
||||
B.registerGrapesJSPlugin = (name, pluginFn) => {
|
||||
B.GrapesJSPlugins.push({ name, pluginFn });
|
||||
log('PLUGIN REGISTER', `Plugin zur Registry hinzugefügt: ${name}`, 'yellow');
|
||||
};
|
||||
|
||||
// --- DEBUG-HELPER UND LOADER-HELPER ---
|
||||
const badgeSay = (text, type = 'info') => {
|
||||
const b=document.getElementById('badge');
|
||||
if (!b) return;
|
||||
b.textContent = text;
|
||||
switch(type) {
|
||||
case 'ok': b.style.background = '#dcfce7'; b.style.color = '#15803d'; b.style.borderColor = '#bbf7d0'; break;
|
||||
case 'error': b.style.background = '#fee2e2'; b.style.color = '#7f1d1d'; b.style.borderColor = '#fecaca'; break;
|
||||
default: b.style.background = '#eef2ff'; b.style.color = '#1e3a8a'; b.style.borderColor = '#c7d2fe';
|
||||
}
|
||||
};
|
||||
|
||||
function loadScript(url, done) {
|
||||
const filename = url.split('/').pop();
|
||||
var s = document.createElement('script');
|
||||
s.src = url + (url.indexOf('?') === -1 ? '?v=' : '&v=') + Date.now();
|
||||
s.async = false;
|
||||
|
||||
s.onload = function(){
|
||||
log('LOAD SUCCESS', `Skript geladen: ${filename}`, 'green');
|
||||
try {
|
||||
done && done();
|
||||
} catch(e){
|
||||
if (e.message.includes('setting getter-only property "defaults"')) {
|
||||
log('RUNTIME WARNING', `IGNORIERE Block-Konflikt in ${filename}: ${e.message}`, 'orange', 'warn');
|
||||
} else {
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('RUNTIME ERROR', `Fehler in Callback nach ${filename}: ${e.message}`, 'red', 'error', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
s.onerror = function(){
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('LOAD FAILED', `Skript FEHLT oder Pfad falsch: ${filename}`, 'red', 'error', false);
|
||||
badgeSay(`Ladefehler: ${filename}`, 'error');
|
||||
try { done && done(); } catch(e){}
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* HILFSFUNKTION: Wandelt den Dateinamen (z.B. blocks-standard.js) in den globalen
|
||||
* Objektnamen (z.B. BridgeBlocksStandard) um.
|
||||
*/
|
||||
function getPluginObjectName(fileName) {
|
||||
// 1. Entferne Dateiendung (.js)
|
||||
let name = fileName.replace('.js', ''); // 'blocks-standard'
|
||||
|
||||
// 2. Teile in Bestandteile zerlegen und den ersten Buchstaben groß schreiben
|
||||
const parts = name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)); // ['Blocks', 'Standard']
|
||||
|
||||
// 3. Mit 'Bridge' prefixen und zusammenfügen
|
||||
return 'Bridge' + parts.join(''); // 'BridgeBlocksStandard'
|
||||
}
|
||||
|
||||
// 🛑 NEUE FUNKTION: Erstellt alle in der Konfiguration definierten Kategorien.
|
||||
function ensureConfiguredCategories(editor) {
|
||||
const bm = editor.BlockManager;
|
||||
const config = window.BridgeParts?.CATEGORY_CONFIG || {};
|
||||
|
||||
Object.keys(config)
|
||||
.sort((a, b) => (config[a].ord || 999) - (config[b].ord || 999))
|
||||
.forEach(catId => {
|
||||
const catConf = config[catId];
|
||||
// Category wird nur erstellt, wenn sie noch nicht existiert
|
||||
if (!bm.getCategories().get(catId)) {
|
||||
bm.getCategories().add({
|
||||
id: catId,
|
||||
label: catConf.label,
|
||||
open: catConf.open !== false,
|
||||
order: catConf.ord || 999
|
||||
});
|
||||
log('CAT INIT', `Kategorie '${catId}' explizit erstellt.`, 'green');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadBridgeParts(cb){
|
||||
const base = B.BASE_PATH_BRIDGE;
|
||||
|
||||
// 🛑 LOKALES LOGGING ENTFERNT, DA ES VOR B.log LIEGT.
|
||||
// log('LOAD START', 'Starte Laden der modularen Bridge-Teile (Geordnet).');
|
||||
|
||||
const coreFiles = [
|
||||
// base + 'category-config.js',
|
||||
// base + 'general-functions.js',
|
||||
base + 'library-parts.js',
|
||||
base + 'categorization-master.js',
|
||||
base + 'categorization-cleanup.js',
|
||||
];
|
||||
|
||||
const initialLoadList = [...coreFiles];
|
||||
|
||||
function recursiveLoader(list, index = 0) {
|
||||
if (index >= list.length) {
|
||||
log('LOAD END', 'Initial-Bridge-Skripte geladen.', 'green');
|
||||
|
||||
const config = window.BridgeParts?.CATEGORY_CONFIG || {};
|
||||
let allBlockFiles = [];
|
||||
|
||||
// Dynamisches Sammeln der Block-Dateien aus der Config
|
||||
Object.keys(config)
|
||||
.sort((a, b) => (config[a].ord || 999) - (config[b].ord || 999))
|
||||
.forEach(key => {
|
||||
// Sammelt alle Dateien, egal ob sync oder async
|
||||
if (Array.isArray(config[key].files)) {
|
||||
allBlockFiles.push(...config[key].files.map(file => base + file));
|
||||
}
|
||||
});
|
||||
|
||||
// Duplikate entfernen (falls eine Datei in mehreren Kategorien gelistet ist)
|
||||
allBlockFiles = Array.from(new Set(allBlockFiles));
|
||||
|
||||
function loadBlockFiles(blockIndex = 0) {
|
||||
if (blockIndex >= allBlockFiles.length) {
|
||||
log('LOAD END', 'Alle Blöcke geladen.', 'green');
|
||||
return cb && cb(B);
|
||||
}
|
||||
log('LOADING BLOCKS', `Lade Block-Skript [${blockIndex + 1}/${allBlockFiles.length}]: ${allBlockFiles[blockIndex].split('/').pop()}`);
|
||||
loadScript(allBlockFiles[blockIndex], function(){
|
||||
loadBlockFiles(blockIndex + 1);
|
||||
});
|
||||
}
|
||||
loadBlockFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
// 🛑 LOKALES LOGGING ENTFERNT, DA ES VOR B.log LIEGT.
|
||||
log('LOADING CORE', `Lade Skript [${index + 1}/${initialLoadList.length}]: ${list[index].split('/').pop()}`); // Loggt ab dem 3. Skript, da general-functions.js an 2. Stelle geladen wird.
|
||||
|
||||
loadScript(list[index], function(){
|
||||
recursiveLoader(list, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
recursiveLoader(initialLoadList);
|
||||
}
|
||||
|
||||
try { parent.postMessage({ source:'bridge', type:'boot' }, '*'); } catch {}
|
||||
var MODE = (window.__editorMode || 'templates').toLowerCase();
|
||||
|
||||
const replaceReferenceLibrary = (editor, ref, mode) => {
|
||||
(window.BridgeParts?.addReferenceLibrary || (()=>{}))(editor, ref, mode);
|
||||
};
|
||||
const upsertCustomForBothCats = (editor, payload) => {
|
||||
(window.BridgeParts?.upsertCustomForBothCats || (()=>{}))(editor, payload);
|
||||
};
|
||||
|
||||
// --- Init & Events (Plugin integriert) ---------------------------------------------
|
||||
loadBridgeParts(function(B){
|
||||
log('INIT START', 'Alle Bridge-Teile geladen, starte GrapesJS-Initialisierung.', 'orange');
|
||||
|
||||
if (typeof grapesjs === 'undefined' || !grapesjs.init) {
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('CRITICAL ERROR', 'Das globale Objekt grapesjs ist nicht verfügbar! Laden von grapes.min.js ist fehlgeschlagen.', 'red', 'error', false);
|
||||
badgeSay('Fehler: GrapesJS nicht geladen!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 🛑 KRITISCHER FIX TEIL 1: Registriere alle gesammelten Bridge-Plugins global.
|
||||
if (typeof grapesjs.plugins.add === 'function') {
|
||||
B.GrapesJSPlugins.forEach(p => {
|
||||
grapesjs.plugins.add(p.name, p.pluginFn);
|
||||
log('PLUGIN ACTIVATION', `GrapesJS Plugin global bereitgestellt: ${p.name}`, 'lime');
|
||||
});
|
||||
} else {
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('PLUGIN ERROR', `GrapesJS Plugin-API (grapesjs.plugins.add) fehlt. Plugins können nicht registriert werden.`, 'red', 'error', false);
|
||||
}
|
||||
|
||||
// 🛑 KRITISCHER FIX: Safety Plugin MUSS die fehlenden Views Panels hinzufügen.
|
||||
function safetyPlugin(editor){
|
||||
const pn = editor.Panels, orig = pn.getButton.bind(pn);
|
||||
pn.getButton = (pid, id) => orig(pid, id) || { set(){}, get(){ return null; } };
|
||||
|
||||
// Fügen Sie das Panel 'views' hinzu, wenn es fehlt
|
||||
if(!pn.getPanel('views')) {
|
||||
pn.addPanel({ id: 'views', el: '.gjs-pn-views' });
|
||||
log('PANEL FIX', "Das 'views' Panel wurde nachträglich hinzugefügt.", 'yellow', 'warn');
|
||||
}
|
||||
|
||||
// Stellen Sie sicher, dass der Block Manager in den Views-Container rendert
|
||||
editor.Config.blockManager = editor.Config.blockManager || {};
|
||||
editor.Config.blockManager.appendTo = editor.Config.blockManager.appendTo || '.gjs-blocks';
|
||||
|
||||
// Der fehlerhafte Timeout-Block wurde entfernt.
|
||||
}
|
||||
|
||||
let pluginsList = [
|
||||
safetyPlugin,
|
||||
// 🛑 KRITISCHE ERGÄNZUNG: Aktiviert das registrierte API-Plugin
|
||||
'bridge-blocks-api',
|
||||
'bridge-categorization-master',
|
||||
'bridge-categorization-cleanup',
|
||||
];
|
||||
|
||||
if (LOAD_NEWSLETTER_PRESET) {
|
||||
pluginsList.push('gjs-preset-newsletter');
|
||||
}
|
||||
|
||||
var ed = grapesjs.init({
|
||||
container: '#gjs',
|
||||
height: '100vh',
|
||||
storageManager: false,
|
||||
plugins: pluginsList,
|
||||
pluginsOpts: {},
|
||||
// 🛑 KRITISCHE ERGÄNZUNG: Verhindert das automatische Ausblenden leerer Kategorien
|
||||
blockManager: {
|
||||
hideEmpty: false
|
||||
}
|
||||
});
|
||||
|
||||
window.__gjs = ed;
|
||||
|
||||
// 🛑 KRITISCHE KORREKTUR 1: Explizite Erstellung aller konfigurierten Kategorien
|
||||
ensureConfiguredCategories(ed);
|
||||
|
||||
// 🛑 KRITISCHE KORREKTUR 2: Sofortige Label-Korrektur
|
||||
// Überschreibt den potenziell falschen, durch GrapesJS gesetzten Label-Namen
|
||||
Object.keys(B.CATEGORY_CONFIG || {}).forEach(catId => {
|
||||
const expectedLabel = B.CATEGORY_CONFIG[catId].label;
|
||||
const categoryModel = ed.BlockManager.getCategories().get(catId);
|
||||
|
||||
if (categoryModel && categoryModel.get('label') !== expectedLabel) {
|
||||
// Setzen ohne das 'change:label' Event auszulösen (optional, aber sauber)
|
||||
categoryModel.set('label', expectedLabel, { silent: true });
|
||||
log('LABEL FIX', `Kategorie '${catId}' Label auf korrigiert: '${expectedLabel}'`, 'yellow', 'warn');
|
||||
}
|
||||
});
|
||||
// ---------------------------------------------------
|
||||
|
||||
B.ensureViews && B.ensureViews(ed);
|
||||
|
||||
log('BLOCK REGISTER', 'Registriere Bridge Blöcke, um Preset-Defaults zu überschreiben.', 'purple');
|
||||
|
||||
// 🛑 DYNAMISCHE AKTIVIERUNG DER SYNCHRONEN BLÖCKE (Ersetzt die fixen Aufrufe)
|
||||
if (B.CATEGORY_CONFIG && ed) {
|
||||
log('DYNAMIC ACTIVATION', 'Starte Aktivierung synchroner Block-Plugins (via Config).', 'purple');
|
||||
|
||||
// Iteriere über die konfigurierten Kategorien
|
||||
Object.keys(B.CATEGORY_CONFIG).forEach(catId => {
|
||||
const config = B.CATEGORY_CONFIG[catId];
|
||||
|
||||
// Verarbeite nur SYNCHRONE Plugins, die Dateien angeben
|
||||
if (config.registration_mode === 'sync' && Array.isArray(config.files)) {
|
||||
|
||||
config.files.forEach(fileName => {
|
||||
|
||||
// Korrigierte Funktion liefert jetzt z.B. 'BridgeBlocksCustom'
|
||||
const objectName = getPluginObjectName(fileName);
|
||||
const plugin = window[objectName];
|
||||
|
||||
// Prüfen, ob das Skript geladen wurde und die Register-Funktion vorhanden ist
|
||||
if (plugin && typeof plugin.register === 'function') {
|
||||
log('DYNAMIC ACTIVATION', `Registriere sync Plugin: ${objectName} (${fileName})`, 'lime');
|
||||
try {
|
||||
plugin.register(ed);
|
||||
} catch(e) {
|
||||
log('DYNAMIC ACTIVATION ERROR', `Fehler beim Registrieren von ${objectName}: ${e.message}`, 'red', 'error');
|
||||
}
|
||||
} else {
|
||||
log('DYNAMIC ACTIVATION WARNING', `Sync Plugin Objekt oder .register() Methode nicht gefunden: ${objectName} (${fileName})`, 'orange', 'warn');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// ---------------------------------------------------
|
||||
|
||||
log('INIT API', 'API-Elemente werden nun durch das Plugin bridge-blocks-api geladen.', 'orange');
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// DEBUGGING: ZÄHLE REKURSIVE EVENTS
|
||||
// ----------------------------------------------------------------------
|
||||
let eventCounts = {};
|
||||
let isParsing = false;
|
||||
const MAX_CALLS = 1000;
|
||||
|
||||
const debugEvents = [
|
||||
'component:add',
|
||||
'component:update',
|
||||
'change:components',
|
||||
'block:add',
|
||||
'change:attributes',
|
||||
'comp:update:status'
|
||||
];
|
||||
|
||||
const debugListener = (event, model) => {
|
||||
if (!isParsing) return;
|
||||
|
||||
if (!eventCounts[event]) {
|
||||
eventCounts[event] = 0;
|
||||
}
|
||||
eventCounts[event]++;
|
||||
|
||||
if (eventCounts[event] === MAX_CALLS + 1) {
|
||||
// Diese kritischen Debug-Meldungen bleiben DIREKT im console-Objekt,
|
||||
// da sie immer sichtbar sein müssen, um Endlosschleifen zu erkennen.
|
||||
console.error(`%c[DEBUG RECURSION ALARM] 🚨 Event '${event}' hat den Grenzwert von ${MAX_CALLS} überschritten!`, 'color:red; font-size: 1.1em; font-weight: bold;');
|
||||
}
|
||||
|
||||
if (eventCounts[event] > MAX_CALLS && eventCounts[event] < (MAX_CALLS + 10)) {
|
||||
const type = (model && typeof model.get === 'function') ? model.get('type') : 'N/A';
|
||||
const parentType = (model && typeof model.parent === 'function' && model.parent()) ? model.parent().get('type') : 'N/A';
|
||||
// Diese bleiben console.log aus demselben Grund
|
||||
console.log(`%c [RECURSION SOURCE] Event: ${event}, Type: ${type}, Parent: ${parentType}`, 'color: #8b0000;');
|
||||
}
|
||||
};
|
||||
|
||||
ed.on('load', function() {
|
||||
debugEvents.forEach(event => ed.on(event, (model) => debugListener(event, model)));
|
||||
|
||||
setTimeout(() => {
|
||||
(B.waitForBlocks ? B.waitForBlocks(ed) : Promise.resolve()).then(function(){
|
||||
try {
|
||||
log('CORE WARN', 'Führe finalen, verzögerten Cleanup-Lauf durch (2000ms).', 'orange', 'warn');
|
||||
|
||||
B.normalizeCategories && B.normalizeCategories(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
|
||||
} catch(e) {
|
||||
log('CORE ERROR', `Finaler Cleanup-Fehler: ${e.message}`, 'red', 'error');
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}, { once: true });
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// MESSAGE HANDLER
|
||||
// ----------------------------------------------------------------------
|
||||
window.addEventListener('message', async function(ev){
|
||||
var data = ev.data || {};
|
||||
if (data.source !== 'admin') return;
|
||||
|
||||
if (data.type === 'init'){
|
||||
B.ensureViews && B.ensureViews(ed);
|
||||
|
||||
var html = (data.html || '').trim();
|
||||
if (!html) html = '<table style="width:100%;font-family:Arial,sans-serif"><tr><td><h1>Neues Dokument</h1><p>Inhalt ...</p></td></tr></table>';
|
||||
|
||||
const applySnips = function(arr){
|
||||
const list = (Array.isArray(arr)?arr:[]).map(s => ({ id:s.id, name:s.name, html: s.html || s.content || '' }));
|
||||
|
||||
B.replaceSnippetBlocks && B.replaceSnippetBlocks(ed, list);
|
||||
|
||||
upsertCustomForBothCats(ed, {
|
||||
ref: (data.ref && (Array.isArray(data.ref.sections) || Array.isArray(data.ref.blocks))) ? {
|
||||
sections: data.ref.sections || [],
|
||||
blocks: data.ref.blocks || []
|
||||
} : { sections: [], blocks: [] },
|
||||
snippets: list
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Erneutes Normalisieren nach Laden der Snippets (falls nötig)
|
||||
B.normalizeCategories && B.normalizeCategories(ed);
|
||||
B.ensureViews && B.ensureViews(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
log('CORE WARN', 'normalize/render nach applySnips ausgeführt (1ms).', 'orange', 'warn');
|
||||
} catch(e) {
|
||||
log('CORE ERROR', `applySnips-Cleanup-Fehler: ${e.message}`, 'red', 'error');
|
||||
}
|
||||
}, 1);
|
||||
|
||||
};
|
||||
|
||||
if (Array.isArray(data.snippets) && data.snippets.length) applySnips(data.snippets);
|
||||
else (B.fetchSnippets ? B.fetchSnippets() : Promise.resolve([])).then(applySnips);
|
||||
|
||||
if (data.ref && (Array.isArray(data.ref.sections) || Array.isArray(data.ref.blocks))) {
|
||||
replaceReferenceLibrary(ed, {
|
||||
sections: data.ref.sections || [],
|
||||
blocks: data.ref.blocks || []
|
||||
}, MODE);
|
||||
}
|
||||
|
||||
// Finaler Aufruf nachrichtengesteuert (konsolidiert)
|
||||
setTimeout(() => {
|
||||
(B.waitForBlocks ? B.waitForBlocks(ed) : Promise.resolve()).then(function(){
|
||||
try {
|
||||
log('CORE WARN', 'Führe nachrichtengesteuerten Final-Cleanup-Lauf durch (100ms).', 'orange', 'warn');
|
||||
|
||||
if (!ed.__contentLoaded) {
|
||||
window.__GJS_IS_PARSING = true;
|
||||
isParsing = true;
|
||||
eventCounts = {};
|
||||
|
||||
try {
|
||||
ed.setComponents(html);
|
||||
} catch (e) {
|
||||
log('SET COMPONENTS FAILED', `setComponents Fehler: ${e.message}. Aufgerufene Event-Zähler: ${JSON.stringify(eventCounts)}`, 'red', 'error');
|
||||
// console.table(eventCounts); bleibt eine direkte Debug-Ausgabe
|
||||
throw e;
|
||||
} finally {
|
||||
window.__GJS_IS_PARSING = false;
|
||||
isParsing = false;
|
||||
log('CONTENT', 'HTML-Inhalt in den Editor geladen (FINAL FIX).', 'orange');
|
||||
|
||||
B.normalizeCategories && B.normalizeCategories(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
}
|
||||
|
||||
ed.__contentLoaded = true;
|
||||
|
||||
} else {
|
||||
B.normalizeCategories && B.normalizeCategories(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
log('CORE ERROR', `Nachrichten-Final-Cleanup-Fehler: ${e.message}. Event-Zähler (im Log-Objekt): ${JSON.stringify(eventCounts)}`, 'red', 'error');
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
|
||||
try { var b=ed.Panels.getButton('views','open-blocks'); b && b.set('active',true); } catch {}
|
||||
badgeSay('Inhalt geladen','ok');
|
||||
setTimeout(function(){ badgeSay('bereit'); }, 1200);
|
||||
}
|
||||
}, false);
|
||||
|
||||
try { B.send && B.send('core-ready', { mode: MODE }); } catch {}
|
||||
try { var bd=document.getElementById('badge'); if (bd) bd.remove(); } catch {}
|
||||
});
|
||||
|
||||
window.onerror = function(message, source, lineno, colno, error) {
|
||||
// Diese kritische Funktion MUSS console.error verwenden.
|
||||
console.error(`%c[${PluginName} - GLOBAL ERROR] Uncaught JS Error: ${message} (Quelle: ${source}:${lineno})`, 'color:red; font-weight:bold;');
|
||||
return false;
|
||||
};
|
||||
|
||||
})();
|
||||
624
public/editor/bridge-core.js
Normal file
624
public/editor/bridge-core.js
Normal file
@@ -0,0 +1,624 @@
|
||||
/* /editor/bridge-core.js — Loader + Orchestrator (FINAL & LOG-KONTROLLIERT) */
|
||||
(function () {
|
||||
|
||||
// --- Initialisierung BridgeParts (B) und Plugin-Registry ---
|
||||
if (!window.BridgeParts) window.BridgeParts = {};
|
||||
const B = window.BridgeParts;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🎯 LOKALE LOG-KONFIGURATION & WRAPPER
|
||||
// ----------------------------------------------------------------------
|
||||
const PluginName = 'bridge-core';
|
||||
|
||||
// Setzen Sie dies auf 'false', um alle Logs NUR für dieses Plugin zu deaktivieren.
|
||||
if (B.LOG_CONFIG) {
|
||||
B.LOG_CONFIG.PLUGINS[PluginName] = true; // bridge-core spezifisch deaktivieren (optional)
|
||||
}
|
||||
|
||||
/**
|
||||
* NEUER LOKALER WRAPPER, der die zentrale B.log Funktion verwendet.
|
||||
*/
|
||||
const log = (type, message, color = '#1E90FF', logType = 'info', force = false) => {
|
||||
// Loggt NUR, wenn B.log verfügbar ist (aus general-functions.js).
|
||||
if (typeof B.log === 'function') {
|
||||
B.log(PluginName, `[${type}] ${message}`, color, logType, force);
|
||||
}
|
||||
// Ansonsten wird NICHTS geloggt, bis general-functions.js geladen ist.
|
||||
};
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// 🛑 GLOBALER LOG ZUR BESTÄTIGUNG DER SKRIPT-AUSFÜHRUNG
|
||||
// log('START', `SKRIPT-AUSFÜHRUNG GESTARTET.`, '#DC143C', 'info', true); // DEAKTIVIERT/IGNORIERT DURCH FEHLENDEN B.log
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🛑 KONFIGURATION: NEWSLETTER-PRESET-TOGGLE
|
||||
// ----------------------------------------------------------------------
|
||||
const LOAD_NEWSLETTER_PRESET = false; // <<< KRITISCHER FIX: Auf FALSE gesetzt, um den "defaults" Konflikt zu beheben!
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
if (window.__bridgeCoreInitialized) {
|
||||
log('INIT ABORT', 'Bridge Core wurde bereits initialisiert.', 'orange');
|
||||
return;
|
||||
}
|
||||
window.__bridgeCoreInitialized = true;
|
||||
|
||||
// --- Initialisierung BridgeParts (B) und Plugin-Registry ---
|
||||
B.BASE_PATH_BRIDGE = '../assets/js/bridge/';
|
||||
B.BASE_PATH_CONFIG = B.BASE_PATH_BRIDGE;
|
||||
// NEU: Standard-API-Endpunkt für Fallbacks, falls category-config.js ihn nicht setzt.
|
||||
// **KORREKTUR**: Auf '/api/editor' FIX eingestellt.
|
||||
B.API_BASE = '/api/editor'; // <<< FIX AUF /api/editor
|
||||
B.STORAGE_URL_BASE = '/api/editor'; // <<< FIX: Erzwingt, dass auch der Storage Manager diesen Pfad verwendet
|
||||
|
||||
B.GrapesJSPlugins = [];
|
||||
|
||||
B.registerGrapesJSPlugin = (name, pluginFn) => {
|
||||
B.GrapesJSPlugins.push({ name, pluginFn });
|
||||
log('PLUGIN REGISTER', `Plugin zur Registry hinzugefügt: ${name}`, 'yellow');
|
||||
};
|
||||
|
||||
// --- DEBUG-HELPER UND LOADER-HELPER ---
|
||||
const badgeSay = (text, type = 'info') => {
|
||||
const b=document.getElementById('badge');
|
||||
if (!b) return;
|
||||
b.textContent = text;
|
||||
switch(type) {
|
||||
case 'ok': b.style.background = '#dcfce7'; b.style.color = '#15803d'; b.style.borderColor = '#bbf7d0'; break;
|
||||
case 'error': b.style.background = '#fee2e2'; b.style.color = '#7f1d1d'; b.style.borderColor = '#fecaca'; break;
|
||||
default: b.style.background = '#eef2ff'; b.style.color = '#1e3a8a'; b.style.borderColor = '#c7d2fe';
|
||||
}
|
||||
};
|
||||
|
||||
function loadScript(url, done) {
|
||||
const filename = url.split('/').pop();
|
||||
var s = document.createElement('script');
|
||||
s.src = url + (url.indexOf('?') === -1 ? '?v=' : '&v=') + Date.now();
|
||||
s.async = false;
|
||||
|
||||
s.onload = function(){
|
||||
log('LOAD SUCCESS', `Skript geladen: ${filename}`, 'green');
|
||||
try {
|
||||
done && done();
|
||||
} catch(e){
|
||||
if (e.message.includes('setting getter-only property "defaults"')) {
|
||||
log('RUNTIME WARNING', `IGNORIERE Block-Konflikt in ${filename}: ${e.message}`, 'orange', 'warn');
|
||||
} else {
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('RUNTIME ERROR', `Fehler in Callback nach ${filename}: ${e.message}`, 'red', 'error', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
s.onerror = function(){
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('LOAD FAILED', `Skript FEHLT oder Pfad falsch: ${filename}`, 'red', 'error', false);
|
||||
badgeSay(`Ladefehler: ${filename}`, 'error');
|
||||
try { done && done(); } catch(e){}
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* HILFSFUNKTION: Wandelt den Dateinamen (z.B. blocks-standard.js) in den globalen
|
||||
* Objektnamen (z.B. BridgeBlocksStandard) um.
|
||||
*/
|
||||
function getPluginObjectName(fileName) {
|
||||
// 1. Entferne Dateiendung (.js)
|
||||
let name = fileName.replace('.js', ''); // 'blocks-standard'
|
||||
|
||||
// 2. Teile in Bestandteile zerlegen und den ersten Buchstaben groß schreiben
|
||||
const parts = name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)); // ['Blocks', 'Standard']
|
||||
|
||||
// 3. Mit 'Bridge' prefixen und zusammenfügen
|
||||
return 'Bridge' + parts.join(''); // 'BridgeBlocksStandard'
|
||||
}
|
||||
|
||||
// 🛑 NEUE FUNKTION: Erstellt alle in der Konfiguration definierten Kategorien.
|
||||
function ensureConfiguredCategories(editor) {
|
||||
const bm = editor.BlockManager;
|
||||
// HINWEIS: B.CATEGORY_CONFIG wird in category-config.js befüllt (muss vorher geladen werden)
|
||||
const config = window.BridgeParts?.CATEGORY_CONFIG || {};
|
||||
|
||||
Object.keys(config)
|
||||
.sort((a, b) => (config[a].ord || 999) - (config[b].ord || 999))
|
||||
.forEach(catId => {
|
||||
const catConf = config[catId];
|
||||
// Category wird nur erstellt, wenn sie noch nicht existiert
|
||||
if (!bm.getCategories().get(catId)) {
|
||||
bm.getCategories().add({
|
||||
id: catId,
|
||||
label: catConf.label,
|
||||
open: catConf.open !== false,
|
||||
order: catConf.ord || 999
|
||||
});
|
||||
log('CAT INIT', `Kategorie '${catId}' explizit erstellt.`, 'green');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadBridgeParts(cb){
|
||||
const base = B.BASE_PATH_BRIDGE;
|
||||
|
||||
// 🛑 LOKALES LOGGING ENTFERNT, DA ES VOR B.log LIEGT.
|
||||
// log('LOAD START', 'Starte Laden der modularen Bridge-Teile (Geordnet).');
|
||||
|
||||
const coreFiles = [
|
||||
base + 'general-functions.js', // <<< RE-AKTIVIERT: Für B.log
|
||||
base + 'category-config.js', // <<< RE-AKTIVIERT: Für B.CATEGORY_CONFIG (und damit API-Flexibilität)
|
||||
base + 'library-parts.js',
|
||||
base + 'categorization-master.js',
|
||||
base + 'categorization-cleanup.js',
|
||||
];
|
||||
|
||||
const initialLoadList = [...coreFiles];
|
||||
|
||||
function recursiveLoader(list, index = 0) {
|
||||
if (index >= list.length) {
|
||||
log('LOAD END', 'Initial-Bridge-Skripte geladen.', 'green');
|
||||
|
||||
const config = window.BridgeParts?.CATEGORY_CONFIG || {};
|
||||
let allBlockFiles = [];
|
||||
|
||||
// Dynamisches Sammeln der Block-Dateien aus der Config
|
||||
Object.keys(config)
|
||||
.sort((a, b) => (config[a].ord || 999) - (config[b].ord || 999))
|
||||
.forEach(key => {
|
||||
// Sammelt alle Dateien, egal ob sync oder async
|
||||
if (Array.isArray(config[key].files)) {
|
||||
allBlockFiles.push(...config[key].files.map(file => base + file));
|
||||
}
|
||||
});
|
||||
|
||||
// Duplikate entfernen (falls eine Datei in mehreren Kategorien gelistet ist)
|
||||
allBlockFiles = Array.from(new Set(allBlockFiles));
|
||||
|
||||
function loadBlockFiles(blockIndex = 0) {
|
||||
if (blockIndex >= allBlockFiles.length) {
|
||||
log('LOAD END', 'Alle Blöcke geladen.', 'green');
|
||||
return cb && cb(B);
|
||||
}
|
||||
log('LOADING BLOCKS', `Lade Block-Skript [${blockIndex + 1}/${allBlockFiles.length}]: ${allBlockFiles[blockIndex].split('/').pop()}`);
|
||||
loadScript(allBlockFiles[blockIndex], function(){
|
||||
loadBlockFiles(blockIndex + 1);
|
||||
});
|
||||
}
|
||||
loadBlockFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
// 🛑 LOKALES LOGGING ENTFERNT, DA ES VOR B.log LIEGT.
|
||||
log('LOADING CORE', `Lade Skript [${index + 1}/${initialLoadList.length}]: ${list[index].split('/').pop()}`); // Loggt ab dem 3. Skript, da general-functions.js an 2. Stelle geladen wird.
|
||||
|
||||
loadScript(list[index], function(){
|
||||
recursiveLoader(list, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
recursiveLoader(initialLoadList);
|
||||
}
|
||||
|
||||
try { parent.postMessage({ source:'bridge', type:'boot' }, '*'); } catch {}
|
||||
var MODE = (window.__editorMode || 'templates').toLowerCase();
|
||||
|
||||
const replaceReferenceLibrary = (editor, ref, mode) => {
|
||||
(window.BridgeParts?.addReferenceLibrary || (()=>{}))(editor, ref, mode);
|
||||
};
|
||||
const upsertCustomForBothCats = (editor, payload) => {
|
||||
(window.BridgeParts?.upsertCustomForBothCats || (()=>{}))(editor, payload);
|
||||
};
|
||||
|
||||
// --- Init & Events (Plugin integriert) ---------------------------------------------
|
||||
loadBridgeParts(function(B){
|
||||
log('INIT START', 'Alle Bridge-Teile geladen, starte GrapesJS-Initialisierung.', 'orange');
|
||||
|
||||
if (typeof grapesjs === 'undefined' || !grapesjs.init) {
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('CRITICAL ERROR', 'Das globale Objekt grapesjs ist nicht verfügbar! Laden von grapes.min.js ist fehlgeschlagen.', 'red', 'error', false);
|
||||
badgeSay('Fehler: GrapesJS nicht geladen!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 🛑 KRITISCHER FIX TEIL 1: Registriere alle gesammelten Bridge-Plugins global.
|
||||
if (typeof grapesjs.plugins.add === 'function') {
|
||||
B.GrapesJSPlugins.forEach(p => {
|
||||
grapesjs.plugins.add(p.name, p.pluginFn);
|
||||
log('PLUGIN ACTIVATION', `GrapesJS Plugin global bereitgestellt: ${p.name}`, 'lime');
|
||||
});
|
||||
} else {
|
||||
// 🛑 KORREKTUR: force: true explizit auf false setzen (oder weglassen)
|
||||
log('PLUGIN ERROR', `GrapesJS Plugin-API (grapesjs.plugins.add) fehlt. Plugins können nicht registriert werden.`, 'red', 'error', false);
|
||||
}
|
||||
|
||||
// 🛑 KRITISCHER FIX: Safety Plugin MUSS die fehlenden Views Panels hinzufügen.
|
||||
function safetyPlugin(editor){
|
||||
const pn = editor.Panels, orig = pn.getButton.bind(pn);
|
||||
pn.getButton = (pid, id) => orig(pid, id) || { set(){}, get(){ return null; } };
|
||||
|
||||
// Fügen Sie das Panel 'views' hinzu, wenn es fehlt
|
||||
if(!pn.getPanel('views')) {
|
||||
pn.addPanel({ id: 'views', el: '.gjs-pn-views' });
|
||||
log('PANEL FIX', "Das 'views' Panel wurde nachträglich hinzugefügt.", 'yellow', 'warn');
|
||||
}
|
||||
|
||||
// Stellen Sie sicher, dass der Block Manager in den Views-Container rendert
|
||||
editor.Config.blockManager = editor.Config.blockManager || {};
|
||||
editor.Config.blockManager.appendTo = editor.Config.blockManager.appendTo || '.gjs-blocks';
|
||||
|
||||
// Der fehlerhafte Timeout-Block wurde entfernt.
|
||||
}
|
||||
|
||||
let pluginsList = [
|
||||
safetyPlugin,
|
||||
// 🛑 KRITISCHE ERGÄNZUNG: Aktiviert das registrierte API-Plugin
|
||||
'bridge-blocks-api',
|
||||
'bridge-categorization-master',
|
||||
'bridge-categorization-cleanup',
|
||||
];
|
||||
|
||||
if (LOAD_NEWSLETTER_PRESET) {
|
||||
pluginsList.push('gjs-preset-newsletter');
|
||||
}
|
||||
|
||||
// Speicherkonfiguration extrahieren, um die URL in onLoad zu verwenden.
|
||||
// 🎯 KORREKTUR für mehr Flexibilität: Verwende B.STORAGE_URL_BASE, falls gesetzt, anstatt window.location.href.
|
||||
// Verwenden Sie B.API_BASE (Standard /api/editor) als Fallback für die Storage-URL
|
||||
const storageBase = B.STORAGE_URL_BASE || B.API_BASE; // B.API_BASE sollte jetzt korrekt sein
|
||||
|
||||
// Robustes Anhängen von Query-Parametern.
|
||||
// Prüft, ob 'storageBase' bereits Query-Parameter enthält ('?')
|
||||
const actionSeparator = storageBase.indexOf('?') === -1 ? '?' : '&';
|
||||
|
||||
const loadUrl = storageBase + actionSeparator + 'action=get&resource=' + (window.__editorMode || 'templates') + '&id=' + (window.__editorId || 0); // KRITISCHE ERGÄNZUNG: Resource und ID
|
||||
const storeUrl = storageBase + actionSeparator + 'action=update&resource=' + (window.__editorMode || 'templates') + '&id=' + (window.__editorId || 0); // KRITISCHE ERGÄNZUNG: Resource und ID
|
||||
|
||||
const storageConf = {
|
||||
type: 'remote',
|
||||
// urlLoad: loadUrl, // ENTFERNT (korrekt, da customFetch verwendet wird)
|
||||
urlStore: storeUrl,
|
||||
|
||||
// 🛑 KRITISCHER ABSCHNITT: customFetch MUSS DIE ERWARTETE SIGNATUR HABEN: customFetch(url, options)
|
||||
customFetch: async (url, options) => { // <<< KORREKTUR DER SIGNATUR
|
||||
// 1. Log Start
|
||||
log('STORAGE START', 'Template wird geladen.', '#008080', 'info', true);
|
||||
// 2. Log Link
|
||||
log('API REQUEST', `Link für den API Request: ${loadUrl}`, '#4682B4', 'log', false);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
// Wichtig: Die übergebenen Optionen nicht vergessen zu mergen
|
||||
...options
|
||||
};
|
||||
|
||||
let data = {};
|
||||
let rawResponse = '';
|
||||
|
||||
try {
|
||||
// Verwendung der intern definierten loadUrl
|
||||
const response = await fetch(loadUrl, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP-Fehler ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
// Holen des Raw Texts, um ihn loggen und parsen zu können
|
||||
rawResponse = await response.text();
|
||||
|
||||
// 3. Log Result
|
||||
log('API RESPONSE', 'Result vom API Request (Raw Text/JSON):', '#4682B4', 'log', false);
|
||||
console.log(rawResponse); // Loggt den reinen String für die Analyse
|
||||
|
||||
// Versuch der JSON-Analyse (um den GrapesJS-Fehler zu vermeiden)
|
||||
try {
|
||||
data = JSON.parse(rawResponse);
|
||||
log('STORAGE PARSE', 'Raw Response als JSON geparst.', 'green');
|
||||
} catch (e) {
|
||||
log('STORAGE PARSE ERROR', `Fehler beim Parsen der Antwort: ${e.message}. Antwort war wahrscheinlich kein gültiges JSON.`, 'red', 'error', true);
|
||||
// Im Falle eines Parsing-Fehlers, leeres Objekt für Fallback-Logik
|
||||
data = {};
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log('STORAGE FETCH ERROR', `Fehler beim Abruf: ${e.message}`, 'red', 'error', true);
|
||||
// Sicherstellen, dass die Promise mit einem leeren Zustand erfüllt wird
|
||||
// Wir müssen dennoch den Log End ausführen, bevor wir zurückkehren
|
||||
}
|
||||
|
||||
// 4. Log End
|
||||
log('STORAGE END', 'Template wurde geladen.', '#008080', 'info', true);
|
||||
|
||||
// --- Logik zur Extraktion des GrapesJS States aus der API-Antwort ---
|
||||
let state = {};
|
||||
|
||||
if (data && data.gjs_data) {
|
||||
log('STORAGE LOAD', 'Voller GrapesJS State aus "gjs_data" geladen.', 'green');
|
||||
state = data.gjs_data;
|
||||
}
|
||||
else if (data && data.content) {
|
||||
try {
|
||||
const parsedState = JSON.parse(data.content);
|
||||
log('STORAGE LOAD', 'Voller GrapesJS State aus "content" (JSON-String) geladen.', 'yellow');
|
||||
state = parsedState;
|
||||
} catch (e) {
|
||||
log('STORAGE ERROR', `Fehler beim Parsen von "content": ${e.message}.`, 'red', 'error');
|
||||
}
|
||||
}
|
||||
// HINWEIS: Füge Fallback für "topContent" hinzu, basierend auf dem Server-Log
|
||||
else if (data && data.topContent) {
|
||||
try {
|
||||
const parsedState = JSON.parse(data.topContent);
|
||||
log('STORAGE LOAD', 'Voller GrapesJS State aus "topContent" (JSON-String) geladen.', 'green');
|
||||
state = parsedState;
|
||||
} catch (e) {
|
||||
log('STORAGE ERROR', `Fehler beim Parsen von "topContent": ${e.message}.`, 'red', 'error');
|
||||
}
|
||||
}
|
||||
else {
|
||||
log('STORAGE WARNING', 'Kein vollständiger GrapesJS State gefunden. Editor lädt leeren State.', 'orange', 'warn');
|
||||
}
|
||||
|
||||
// customFetch MUSS den geladenen State zurückgeben
|
||||
return state;
|
||||
},
|
||||
// --- ENDE customFetch ---
|
||||
|
||||
// onLoad ist bei customFetch nicht mehr nötig
|
||||
// onLoad: (response) => { ... },
|
||||
|
||||
// KRITISCH: Speichert den vollen State als JSON-String im Feld 'json_content'.
|
||||
onStore: (data) => {
|
||||
// ACHTUNG: ed existiert hier nicht, muss über window.__gjs geladen werden ODER ed als Argument akzeptiert werden
|
||||
const ed = window.__gjs;
|
||||
return {
|
||||
json_content: JSON.stringify(data),
|
||||
html: ed ? ed.getHtml() : '' // Fügen Sie den HTML-Output zur Abwärtskompatibilität hinzu
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
var ed = grapesjs.init({
|
||||
container: '#gjs',
|
||||
height: '100vh',
|
||||
|
||||
// 🛑 KRITISCHE KORREKTUR: storageManager aktivieren und konfigurieren
|
||||
storageManager: storageConf,
|
||||
|
||||
plugins: pluginsList,
|
||||
pluginsOpts: {},
|
||||
// 🛑 KRITISCHE ERGÄNZUNG: Verhindert das automatische Ausblenden leerer Kategorien
|
||||
blockManager: {
|
||||
hideEmpty: false
|
||||
}
|
||||
});
|
||||
|
||||
window.__gjs = ed;
|
||||
|
||||
// 🛑 KRITISCHE KORREKTUR 1: Explizite Erstellung aller konfigurierten Kategorien
|
||||
ensureConfiguredCategories(ed);
|
||||
|
||||
// 🛑 KRITISCHE KORREKTUR 2: Sofortige Label-Korrektur
|
||||
// Überschreibt den potenziell falschen, durch GrapesJS gesetzten Label-Namen
|
||||
Object.keys(B.CATEGORY_CONFIG || {}).forEach(catId => {
|
||||
const expectedLabel = B.CATEGORY_CONFIG[catId].label;
|
||||
const categoryModel = ed.BlockManager.getCategories().get(catId);
|
||||
|
||||
if (categoryModel && categoryModel.get('label') !== expectedLabel) {
|
||||
// Setzen ohne das 'change:label' Event auszulösen (optional, aber sauber)
|
||||
categoryModel.set('label', expectedLabel, { silent: true });
|
||||
log('LABEL FIX', `Kategorie '${catId}' Label auf korrigiert: '${expectedLabel}'`, 'yellow', 'warn');
|
||||
}
|
||||
});
|
||||
// ---------------------------------------------------
|
||||
|
||||
B.ensureViews && B.ensureViews(ed);
|
||||
|
||||
log('BLOCK REGISTER', 'Registriere Bridge Blöcke, um Preset-Defaults zu überschreiben.', 'purple');
|
||||
|
||||
// 🛑 DYNAMISCHE AKTIVIERUNG DER SYNCHRONEN BLÖCKE (Ersetzt die fixen Aufrufe)
|
||||
if (B.CATEGORY_CONFIG && ed) {
|
||||
log('DYNAMIC ACTIVATION', 'Starte Aktivierung synchroner Block-Plugins (via Config).', 'purple');
|
||||
|
||||
// Iteriere über die konfigurierten Kategorien
|
||||
Object.keys(B.CATEGORY_CONFIG).forEach(catId => {
|
||||
const config = B.CATEGORY_CONFIG[catId];
|
||||
|
||||
// Verarbeite nur SYNCHRONE Plugins, die Dateien angeben
|
||||
if (config.registration_mode === 'sync' && Array.isArray(config.files)) {
|
||||
|
||||
config.files.forEach(fileName => {
|
||||
|
||||
// Korrigierte Funktion liefert jetzt z.B. 'BridgeBlocksCustom'
|
||||
const objectName = getPluginObjectName(fileName);
|
||||
const plugin = window[objectName];
|
||||
|
||||
// Prüfen, ob das Skript geladen wurde und die Register-Funktion vorhanden ist
|
||||
if (plugin && typeof plugin.register === 'function') {
|
||||
log('DYNAMIC ACTIVATION', `Registriere sync Plugin: ${objectName} (${fileName})`, 'lime');
|
||||
try {
|
||||
plugin.register(ed);
|
||||
} catch(e) {
|
||||
log('DYNAMIC ACTIVATION ERROR', `Fehler beim Registrieren von ${objectName}: ${e.message}`, 'red', 'error');
|
||||
}
|
||||
} else {
|
||||
log('DYNAMIC ACTIVATION WARNING', `Sync Plugin Objekt oder .register() Methode nicht gefunden: ${objectName} (${fileName})`, 'orange', 'warn');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// ---------------------------------------------------
|
||||
|
||||
log('INIT API', 'API-Elemente werden nun durch das Plugin bridge-blocks-api geladen. (ASYNCHRON)', 'orange');
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// DEBUGGING: ZÄHLE REKURSIVE EVENTS
|
||||
// ----------------------------------------------------------------------
|
||||
let eventCounts = {};
|
||||
let isParsing = false;
|
||||
const MAX_CALLS = 1000;
|
||||
|
||||
const debugEvents = [
|
||||
'component:add',
|
||||
'component:update',
|
||||
'change:components',
|
||||
'block:add',
|
||||
'change:attributes',
|
||||
'comp:update:status'
|
||||
];
|
||||
|
||||
const debugListener = (event, model) => {
|
||||
if (!isParsing) return;
|
||||
|
||||
if (!eventCounts[event]) {
|
||||
eventCounts[event] = 0;
|
||||
}
|
||||
eventCounts[event]++;
|
||||
|
||||
if (eventCounts[event] === MAX_CALLS + 1) {
|
||||
// Diese kritischen Debug-Meldungen bleiben DIREKT im console-Objekt,
|
||||
// da sie immer sichtbar sein müssen, um Endlosschleifen zu erkennen.
|
||||
console.error(`%c[DEBUG RECURSION ALARM] 🚨 Event '${event}' hat den Grenzwert von ${MAX_CALLS} überschritten!`, 'color:red; font-size: 1.1em; font-weight: bold;');
|
||||
}
|
||||
|
||||
if (eventCounts[event] > MAX_CALLS && eventCounts[event] < (MAX_CALLS + 10)) {
|
||||
const type = (model && typeof model.get === 'function') ? model.get('type') : 'N/A';
|
||||
const parentType = (model && typeof model.parent === 'function' && model.parent()) ? model.parent().get('type') : 'N/A';
|
||||
// Diese bleiben console.log aus demselben Grund
|
||||
console.log(`%c [RECURSION SOURCE] Event: ${event}, Type: ${type}, Parent: ${parentType}`, 'color: #8b0000;');
|
||||
}
|
||||
};
|
||||
|
||||
ed.on('load', function() {
|
||||
debugEvents.forEach(event => ed.on(event, (model) => debugListener(event, model)));
|
||||
|
||||
setTimeout(() => {
|
||||
(B.waitForBlocks ? B.waitForBlocks(ed) : Promise.resolve()).then(function(){
|
||||
try {
|
||||
log('CORE WARN', 'Führe finalen, verzögerten Cleanup-Lauf durch (2000ms).', 'orange', 'warn');
|
||||
|
||||
B.normalizeCategories && B.normalizeCategories(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
|
||||
} catch(e) {
|
||||
log('CORE ERROR', `Finaler Cleanup-Fehler: ${e.message}`, 'red', 'error');
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}, { once: true });
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// MESSAGE HANDLER
|
||||
// ----------------------------------------------------------------------
|
||||
window.addEventListener('message', async function(ev){
|
||||
var data = ev.data || {};
|
||||
if (data.source !== 'admin') return;
|
||||
|
||||
if (data.type === 'init'){
|
||||
B.ensureViews && B.ensureViews(ed);
|
||||
|
||||
var html = (data.html || '').trim();
|
||||
if (!html) html = '<table style="width:100%;font-family:Arial,sans-serif"><tr><td><h1>Neues Dokument</h1><p>Inhalt ...</p></td></tr></table>';
|
||||
|
||||
const applySnips = function(arr){
|
||||
const list = (Array.isArray(arr)?arr:[]).map(s => ({ id:s.id, name:s.name, html: s.html || s.content || '' }));
|
||||
|
||||
B.replaceSnippetBlocks && B.replaceSnippetBlocks(ed, list);
|
||||
|
||||
upsertCustomForBothCats(ed, {
|
||||
ref: (data.ref && (Array.isArray(data.ref.sections) || Array.isArray(data.ref.blocks))) ? {
|
||||
sections: data.ref.sections || [],
|
||||
blocks: data.ref.blocks || []
|
||||
} : { sections: [], blocks: [] },
|
||||
snippets: list
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Erneutes Normalisieren nach Laden der Snippets (falls nötig)
|
||||
B.normalizeCategories && B.normalizeCategories(ed);
|
||||
B.ensureViews && B.ensureViews(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
log('CORE WARN', 'normalize/render nach applySnips ausgeführt (1ms).', 'orange', 'warn');
|
||||
} catch(e) {
|
||||
log('CORE ERROR', `applySnips-Cleanup-Fehler: ${e.message}`, 'red', 'error');
|
||||
}
|
||||
}, 1);
|
||||
|
||||
};
|
||||
|
||||
if (Array.isArray(data.snippets) && data.snippets.length) applySnips(data.snippets);
|
||||
else (B.fetchSnippets ? B.fetchSnippets() : Promise.resolve([])).then(applySnips);
|
||||
|
||||
if (data.ref && (Array.isArray(data.ref.sections) || Array.isArray(data.ref.blocks))) {
|
||||
replaceReferenceLibrary(ed, {
|
||||
sections: data.ref.sections || [],
|
||||
blocks: data.ref.blocks || []
|
||||
}, MODE);
|
||||
}
|
||||
|
||||
// Finaler Aufruf nachrichtengesteuert (konsolidiert)
|
||||
setTimeout(() => {
|
||||
(B.waitForBlocks ? B.waitForBlocks(ed) : Promise.resolve()).then(function(){
|
||||
try {
|
||||
log('CORE WARN', 'Führe nachrichtengesteuerten Final-Cleanup-Lauf durch (100ms).', 'orange', 'warn');
|
||||
|
||||
// 🛑 KRITISCHE KORREKTUR: Entferne das erzwungene ed.setComponents(html)
|
||||
// Das Laden des Inhalts wird jetzt vom storageManager übernommen (via customFetch).
|
||||
if (!ed.__contentLoaded) {
|
||||
log('CONTENT', 'Erster Ladevorgang (storageManager) ist abgeschlossen.', 'orange');
|
||||
|
||||
// HINWEIS: Wenn der Editor initial leer lädt (z.B. neue Vorlage),
|
||||
// MUSS hier der initiale HTML-Code eingefügt werden.
|
||||
// Da der storageManager aber automatisch lädt,
|
||||
// sollte dieser Block nur für den Initialfall "Neu" greifen.
|
||||
if (html && !ed.getComponents().length) {
|
||||
window.__GJS_IS_PARSING = true;
|
||||
isParsing = true;
|
||||
eventCounts = {};
|
||||
try {
|
||||
ed.setComponents(html);
|
||||
} catch (e) {
|
||||
log('SET COMPONENTS FAILED', `setComponents Fehler: ${e.message}. Aufgerufene Event-Zähler: ${JSON.stringify(eventCounts)}`, 'red', 'error');
|
||||
throw e;
|
||||
} finally {
|
||||
window.__GJS_IS_PARSING = false;
|
||||
isParsing = false;
|
||||
log('CONTENT', 'HTML-Inhalt in den Editor geladen (FALLBACK).', 'orange');
|
||||
}
|
||||
}
|
||||
|
||||
ed.__contentLoaded = true;
|
||||
|
||||
}
|
||||
|
||||
// Normalisierung am Ende
|
||||
B.normalizeCategories && B.normalizeCategories(ed);
|
||||
B.renderBlocks && B.renderBlocks(ed);
|
||||
|
||||
} catch(e) {
|
||||
log('CORE ERROR', `Nachrichten-Final-Cleanup-Fehler: ${e.message}. Event-Zähler (im Log-Objekt): ${JSON.stringify(eventCounts)}`, 'red', 'error');
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
|
||||
try { var b=ed.Panels.getButton('views','open-blocks'); b && b.set('active',true); } catch {}
|
||||
badgeSay('Inhalt geladen','ok');
|
||||
setTimeout(function(){ badgeSay('bereit'); }, 1200);
|
||||
}
|
||||
}, false);
|
||||
|
||||
try { B.send && B.send('core-ready', { mode: MODE }); } catch {}
|
||||
try { var bd=document.getElementById('badge'); if (bd) bd.remove(); } catch {}
|
||||
});
|
||||
|
||||
window.onerror = function(message, source, lineno, colno, error) {
|
||||
// Diese kritische Funktion MUSS console.error verwenden.
|
||||
console.error(`%c[${PluginName} - GLOBAL ERROR] Uncaught JS Error: ${message} (Quelle: ${source}:${lineno})`, 'color:red; font-weight:bold;');
|
||||
return false;
|
||||
};
|
||||
|
||||
})();
|
||||
76
public/editor/config.js
Normal file
76
public/editor/config.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/* /editor/config.js (SCHRITT 35: LOG-EBENEN-KONTROLLE) */
|
||||
(function() {
|
||||
|
||||
// Stelle sicher, dass BridgeParts existiert und hole die registrierten Plugins.
|
||||
const B = window.BridgeParts || {};
|
||||
|
||||
// --- 🎯 ZENTRALE LOG-KONFIGURATION (Überschreibt general-functions.js Defaults) ---
|
||||
// HINWEIS: Dies muss NACH general-functions.js geladen werden.
|
||||
B.LOG_CONFIG = B.LOG_CONFIG || {};
|
||||
|
||||
// 1. HAUPTSCHALTER: Deaktiviert alle normalen Logs (muss auf 'true' sein, damit die Ebenen-Schalter wirken)
|
||||
B.LOG_CONFIG.GLOBAL_DEBUG = true;
|
||||
|
||||
// 2. EBENEN-SCHALTER (wirken nur, wenn GLOBAL_DEBUG = true):
|
||||
B.LOG_CONFIG.INFO_ENABLED = true; // Aktiviert/Deaktiviert alle Info-Logs (B.log mit type='info')
|
||||
B.LOG_CONFIG.WARN_ENABLED = true; // Aktiviert/Deaktiviert alle Warn-Logs (B.log mit type='warn')
|
||||
B.LOG_CONFIG.ERROR_ENABLED = true; // Aktiviert/Deaktiviert alle Error-Logs (B.log mit type='error')
|
||||
|
||||
// 3. DATEN-SCHALTER: Aktiviert/Deaktiviert die Ausgabe großer Array-Daten (B.logData)
|
||||
B.LOG_CONFIG.DATA_ENABLED = true;
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
|
||||
|
||||
// Sammle alle dynamisch registrierten Plugin-Namen.
|
||||
// Der Array B.GrapesJSPlugins wurde von bridge-core.js, blocks-api.js etc. gefüllt.
|
||||
const dynamicPluginNames = B.GrapesJSPlugins
|
||||
? B.GrapesJSPlugins.map(p => p.name)
|
||||
: [];
|
||||
|
||||
// Optional: Fügen Sie statische GrapesJS-Plugins hinzu
|
||||
const staticPlugins = [
|
||||
'gjs-preset-newsletter' // Beispiel: Fügt den Newsletter-Preset hinzu, falls gewünscht
|
||||
];
|
||||
|
||||
// Kombiniere alle Plugin-Namen zu einer eindeutigen Liste
|
||||
const uniquePlugins = [...new Set([
|
||||
...dynamicPluginNames,
|
||||
...staticPlugins
|
||||
])];
|
||||
|
||||
|
||||
// Definiere die Haupt-Konfiguration für GrapesJS
|
||||
const editorConfig = {
|
||||
// 1. WICHTIG: Ersetze 'gjs' durch die ID des Containers
|
||||
container: '#gjs',
|
||||
|
||||
// 2. KRITISCH: Die dynamisch erstellte Plugin-Liste
|
||||
plugins: uniquePlugins,
|
||||
|
||||
// 3. Plugin-Optionen (können leer bleiben, wenn keine Optionen benötigt werden)
|
||||
pluginsOpts: {
|
||||
// Hier Optionen für einzelne Plugins eintragen
|
||||
},
|
||||
|
||||
// --- Andere Basis-Konfigurationen ---
|
||||
panels: {
|
||||
defaults: [
|
||||
{ id: 'options', el: '.panel__options', buttons: [{ id: 'save', label: 'Speichern', className: 'fa fa-floppy-o' }] },
|
||||
{ id: 'views', el: '.panel__views' },
|
||||
]
|
||||
},
|
||||
|
||||
// ... Fügen Sie hier weitere GrapesJS-Optionen ein (z.B. device buttons)
|
||||
};
|
||||
|
||||
// Starte GrapesJS
|
||||
// window.GrapesJS.init wurde in bridge-core.js definiert, um GrapesJS zu starten.
|
||||
if (window.GrapesJS && window.GrapesJS.init) {
|
||||
// Übergebe editorConfig und die Liste der Plugin-Funktionen
|
||||
window.GrapesJS.init(editorConfig, B.GrapesJSPlugins.map(p => p.name), B.GrapesJSPlugins);
|
||||
} else {
|
||||
console.error('GrapesJS.init ist in window.GrapesJS nicht verfügbar. Wurde bridge-core.js geladen?');
|
||||
}
|
||||
|
||||
})();
|
||||
64
public/editor/editor-core (Kopie).php
Normal file
64
public/editor/editor-core (Kopie).php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
$mode = strtolower($_GET['mode'] ?? 'templates');
|
||||
$ts = time();
|
||||
?><!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Editor</title>
|
||||
<link rel="stylesheet" href="../vendor/grapesjs/grapes.min.css" />
|
||||
<style>
|
||||
html,body{height:100%}body{margin:0;background:#f8fafc;color:#0f172a}#gjs{height:100vh}
|
||||
|
||||
.gjs-one-bg{background-color:#fff!important}.gjs-two-color{color:#0f172a!important}
|
||||
.gjs-three-bg{background-color:#f8fafc!important}.gjs-four-color{color:#334155!important}
|
||||
#badge{position:fixed;right:8px;top:8px;background:#eef2ff;color:#1e3a8a;border:1px solid #c7d2fe;border-radius:999px;padding:4px 10px;font:12px system-ui;z-index:2147483647;opacity:.9}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="badge">lädt …</div>
|
||||
<div id="gjs"></div>
|
||||
|
||||
<div id="blocks"></div>
|
||||
|
||||
<script>
|
||||
function logToParent(type, detail){ try{ parent.postMessage({source:'editor-core',type:type,detail:String(detail||'')},'*'); }catch(e){} }
|
||||
window.addEventListener('error', function(e){
|
||||
var b=document.getElementById('badge');
|
||||
if(b){ b.textContent='Fehler: '+(e&&e.message?e.message:'unbekannt'); b.style.background='#fee2e2'; b.style.color='#7f1d1d'; b.style.borderColor='#fecaca'; }
|
||||
logToParent('window-error', e && e.message ? e.message : 'unknown');
|
||||
});
|
||||
function loadLocalScript(src, onok){
|
||||
var s=document.createElement('script'); s.src=src; s.async=false;
|
||||
s.onload=function(){ logToParent('script-ok', src); onok&&onok(); };
|
||||
s.onerror=function(){ var b=document.getElementById('badge'); if(b){ b.textContent='Fehlt: '+src; b.style.background='#fee2e2'; b.style.color='#7f1d1d'; b.style.borderColor='#fecaca'; } logToParent('script-missing', src); };
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
logToParent('boot','start');
|
||||
|
||||
// 1) GrapesJS laden
|
||||
loadLocalScript('../vendor/grapesjs/grapes.min.js?v=<?=$ts?>', function(){
|
||||
if(typeof window.grapesjs==='undefined'){ document.getElementById('badge').textContent='grapesjs nicht verfügbar'; logToParent('gjs-missing','window.grapesjs undefined'); return; }
|
||||
|
||||
// 2) Plugin laden
|
||||
loadLocalScript('../vendor/grapesjs-preset-newsletter/grapesjs-preset-newsletter.min.js?v=<?=$ts?>', function(){
|
||||
|
||||
// 3) BRIDGE ZUERST (mit Cache-Bust) – meldet sich sofort mit bridge:boot
|
||||
loadLocalScript('bridge-core.js?v=<?=$ts?>', function(){
|
||||
// 4) Danach config.js (Bibliothek)
|
||||
// loadLocalScript('config.js?v=<?=$ts?>');
|
||||
});
|
||||
|
||||
// Heartbeat vom Core (sichtbar im Hauptfenster)
|
||||
var hb=0, timer=setInterval(function(){ hb++; if(hb>60){clearInterval(timer);return;} logToParent('hb','tick '+hb); }, 200);
|
||||
|
||||
// Mode für die Bridge bereitstellen
|
||||
window.__editorMode = "<?=htmlspecialchars($mode,ENT_QUOTES)?>";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
79
public/editor/editor-core.php
Normal file
79
public/editor/editor-core.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
$mode = strtolower($_GET['mode'] ?? 'templates');
|
||||
$ts = time();
|
||||
?><!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Editor</title>
|
||||
<link rel="stylesheet" href="../vendor/grapesjs/grapes.min.css" />
|
||||
<style>
|
||||
html,body{height:100%}body{margin:0;background:#f8fafc;color:#0f172a}#gjs{height:100vh}
|
||||
|
||||
.gjs-one-bg{background-color:#fff!important}.gjs-two-color{color:#0f172a!important}
|
||||
.gjs-three-bg{background-color:#f8fafc!important}.gjs-four-color{color:#334155!important}
|
||||
#badge{position:fixed;right:8px;top:8px;background:#eef2ff;color:#1e3a8a;border:1px solid #c7d2fe;border-radius:999px;padding:4px 10px;font:12px system-ui;z-index:2147483647;opacity:.9}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="badge">lädt …</div>
|
||||
<div id="gjs"></div>
|
||||
|
||||
<div id="blocks"></div>
|
||||
|
||||
<script>
|
||||
function logToParent(type, detail){ try{ parent.postMessage({source:'editor-core',type:type,detail:String(detail||'')},'*'); }catch(e){} }
|
||||
window.addEventListener('error', function(e){
|
||||
var b=document.getElementById('badge');
|
||||
if(b){ b.textContent='Fehler: '+(e&&e.message?e.message:'unbekannt'); b.style.background='#fee2e2'; b.style.color='#7f1d1d'; b.style.borderColor='#fecaca'; }
|
||||
logToParent('window-error', e && e.message ? e.message : 'unknown');
|
||||
});
|
||||
function loadLocalScript(src, onok){
|
||||
// Hinzufügen des Cache-Bust-Parameters zur URL
|
||||
// Die Variable $ts wird durch PHP im HTML-Kontext eingefügt
|
||||
const url = src + (src.indexOf('?') === -1 ? '?v=' : '&v=') + <?=$ts?>;
|
||||
var s=document.createElement('script'); s.src=url; s.async=false;
|
||||
s.onload=function(){ logToParent('script-ok', src); onok&&onok(); };
|
||||
s.onerror=function(){ var b=document.getElementById('badge'); if(b){ b.textContent='Fehlt: '+src; b.style.background='#fee2e2'; b.style.color='#7f1d1d'; b.style.borderColor='#fecaca'; } logToParent('script-missing', src); };
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
logToParent('boot','start');
|
||||
|
||||
// 1) GrapesJS laden
|
||||
loadLocalScript('../vendor/grapesjs/grapes.min.js', function(){
|
||||
if(typeof window.grapesjs==='undefined'){
|
||||
document.getElementById('badge').textContent='grapesjs nicht verfügbar';
|
||||
logToParent('gjs-missing','window.grapesjs undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.A) KRITISCHE HELPER ZUERST LADEN (Category Config)
|
||||
loadLocalScript('../assets/js/bridge/category-config.js', function() {
|
||||
|
||||
// 2.B) Dann die zentrale Log-Funktion
|
||||
// Diese muss geladen sein, bevor bridge-core.js startet!
|
||||
loadLocalScript('../assets/js/bridge/general-functions.js', function() {
|
||||
|
||||
// 3) Plugin laden (GrapesJS Preset Newsletter)
|
||||
loadLocalScript('../vendor/grapesjs-preset-newsletter/grapesjs-preset-newsletter.min.js', function(){
|
||||
|
||||
// 4) BRIDGE-CORE laden – jetzt kann es B.LOG_CONFIG auf false setzen!
|
||||
loadLocalScript('bridge-core.js', function(){
|
||||
// 5) Danach config.js (Bibliothek)
|
||||
// loadLocalScript('config.js');
|
||||
});
|
||||
|
||||
// Heartbeat vom Core (sichtbar im Hauptfenster)
|
||||
var hb=0, timer=setInterval(function(){ hb++; if(hb>60){clearInterval(timer);return;} logToParent('hb','tick '+hb); }, 200);
|
||||
|
||||
// Mode für die Bridge bereitstellen
|
||||
window.__editorMode = "<?=htmlspecialchars($mode,ENT_QUOTES)?>";
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
169
public/index.php
Normal file
169
public/index.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
$base = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') ?: '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Email Template System – Admin</title>
|
||||
|
||||
<!-- UI bis zur Auth verdecken -->
|
||||
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||
<style>
|
||||
html.auth-pending body { visibility: hidden; }
|
||||
</style>
|
||||
|
||||
<!-- Tailwind zuerst -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Admin-Theme (neu) -->
|
||||
<link rel="stylesheet" href="assets/css/admin.css?v=2025-10-061">
|
||||
|
||||
<!-- Toast danach -->
|
||||
<link rel="stylesheet" href="assets/css/toast.css">
|
||||
|
||||
<!-- Kleine Hilfs-Utilities (belassen, falls admin.css andere Werte hat) -->
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||
.btn:hover{background:#f8fafc}.btn-danger{border-color:#fecaca;color:#b91c1c}.btn-danger:hover{background:#fef2f2}
|
||||
.chip{display:inline-flex;align-items:center;gap:.35rem;padding:.15rem .5rem;border-radius:999px;background:#f1f5f9;color:#334155;font-size:.75rem;border:1px solid #e5e7eb}
|
||||
.chip .dot{width:.5rem;height:.5rem;border-radius:999px;background:#64748b}
|
||||
dialog::backdrop{background:rgba(15,23,42,.3)}
|
||||
#toast-root{z-index:2147483647}
|
||||
.truncate{max-width:22rem;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
||||
.hidden{display:none}
|
||||
</style>
|
||||
</head>
|
||||
<body class="page-admin bg-slate-50 text-slate-800">
|
||||
<header class="sticky top-0 z-30 bg-white/90 backdrop-blur border-b">
|
||||
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center gap-3">
|
||||
<h1 class="font-semibold text-lg">Email Template System</h1>
|
||||
<nav class="isolate inline-flex rounded-2xl shadow-sm border bg-white overflow-hidden ms-6">
|
||||
<button type="button" data-tab="templates" class="px-4 py-2 text-sm border-e bg-sky-50 text-sky-700">Templates</button>
|
||||
<button type="button" data-tab="sections" class="px-4 py-2 text-sm border-e">Sections</button>
|
||||
<button type="button" data-tab="blocks" class="px-4 py-2 text-sm border-e">Blocks</button>
|
||||
<button type="button" data-tab="snippets" class="px-4 py-2 text-sm">Snippets</button>
|
||||
</nav>
|
||||
<div class="ms-auto flex gap-2">
|
||||
<button id="btn-new" type="button" class="btn">Neu …</button>
|
||||
|
||||
<!-- Tools: werden von ui-tools.js abgefangen und im Popup gezeigt -->
|
||||
<a href="api.php?action=health"
|
||||
class="btn btn-light"
|
||||
data-popup="json"
|
||||
data-title="API Health">API-Health</a>
|
||||
|
||||
<a href="tools/db-doctor.php?profile=template"
|
||||
class="btn btn-light"
|
||||
data-popup="frame"
|
||||
data-title="DB-Doctor">DB-Doctor</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-6xl mx-auto p-4">
|
||||
<section id="view-templates" class="view"></section>
|
||||
<section id="view-sections" class="view hidden"></section>
|
||||
<section id="view-blocks" class="view hidden"></section>
|
||||
<section id="view-snippets" class="view hidden"></section>
|
||||
</main>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
<dialog id="createDialog" class="rounded-2xl p-0 w-[540px]">
|
||||
<form id="createForm" method="dialog" class="p-4 bg-white rounded-2xl">
|
||||
<h3 class="text-lg font-semibold mb-2">Neues Element erstellen</h3>
|
||||
<p id="createHint" class="text-sm text-slate-600 mb-3"></p>
|
||||
<div class="space-y-3" id="createFields"></div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" id="createCancel" class="btn">Abbrechen</button>
|
||||
<button type="submit" id="createSubmit" class="btn">Erstellen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Delete Confirm Dialog -->
|
||||
<dialog id="deleteDialog" class="rounded-2xl p-0 w-[520px]">
|
||||
<form id="deleteForm" method="dialog" class="p-4 bg-white rounded-2xl">
|
||||
<h3 class="text-lg font-semibold mb-2">Eintrag löschen?</h3>
|
||||
<p id="deleteText" class="text-sm text-slate-600 mb-4"></p>
|
||||
<div class="mt-1 flex justify-end gap-2">
|
||||
<button type="button" id="deleteCancel" class="btn">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-danger">Löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Editor Dialog -->
|
||||
<dialog id="editorDialog" class="rounded-2xl p-0 w-[95vw] h-[90vh]">
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="px-4 py-2 border-b flex items-center gap-2 bg-white/80 backdrop-blur">
|
||||
<strong class="me-auto">E-Mail Editor</strong>
|
||||
<button id="btn-clear-main" type="button" class="btn" title="Leeren">🧹</button>
|
||||
<button id="btn-save" type="button" class="btn">Speichern</button>
|
||||
<button id="btn-close" type="button" class="btn">Schließen</button>
|
||||
</div>
|
||||
<iframe id="editorFrame" src="about:blank" class="flex-1 w-full"></iframe>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Preview Dialog -->
|
||||
<dialog id="previewDialog" class="rounded-2xl p-0 w-[90vw] h-[90vh]">
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="px-4 py-2 border-b flex items-center gap-2 bg-white/80 backdrop-blur">
|
||||
<strong class="me-auto">Vorschau</strong>
|
||||
<button id="btn-close-preview" type="button" class="btn">Schließen</button>
|
||||
</div>
|
||||
<iframe id="previewFrame" class="flex-1 w-full"></iframe>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Edit Snippet Dialog -->
|
||||
<dialog id="editSnippetDialog" class="rounded-2xl p-0 w-[700px]">
|
||||
<form id="editSnippetForm" method="dialog" class="p-4 bg-white rounded-2xl">
|
||||
<h3 class="text-lg font-semibold mb-2">Snippet bearbeiten</h3>
|
||||
<div class="space-y-3">
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-600">Name</span>
|
||||
<input id="edit_snip_name" type="text" class="w-full border rounded-lg px-3 py-2" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-600">Content (HTML)</span>
|
||||
<textarea id="edit_snip_content" class="w-full border rounded-lg px-3 py-2 h-64 font-mono text-sm"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" id="editSnippetCancel" class="btn">Abbrechen</button>
|
||||
<button type="submit" id="editSnippetSave" class="btn">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Tools Dialog (NEU) -->
|
||||
<dialog id="toolsDialog" class="rounded-2xl p-0 w-[92vw] h-[86vh]">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="px-4 py-2 border-b bg-white/80 backdrop-blur flex items-center gap-3">
|
||||
<strong id="toolsTitle" class="me-auto">Werkzeug</strong>
|
||||
<button id="toolsCopy" type="button" class="btn hidden">Kopieren</button>
|
||||
<button id="toolsDownload" type="button" class="btn hidden">Download</button>
|
||||
<button id="toolsClose" type="button" class="btn">Schließen</button>
|
||||
</div>
|
||||
|
||||
<!-- JSON Ansicht -->
|
||||
<div id="toolsJsonWrap" class="flex-1 overflow-auto hidden bg-slate-50">
|
||||
<pre id="toolsJsonPre" class="p-4 text-sm leading-5 font-mono text-slate-800"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Iframe Ansicht -->
|
||||
<iframe id="toolsFrame" class="flex-1 w-full hidden bg-white"></iframe>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div id="toast-root"></div>
|
||||
|
||||
<script src="assets/js/toast.js"></script>
|
||||
<script type="module" src="assets/js/app.js?v=20250907"></script>
|
||||
<script type="module" src="assets/js/ui-tools.js?v=20250907"></script>
|
||||
</body>
|
||||
</html>
|
||||
70
public/login.php
Normal file
70
public/login.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
// login.php – Staging Login
|
||||
?><!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Login – EmailTemplate</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- Admin-Theme (neu) -->
|
||||
<link rel="stylesheet" href="/assets/css/app.css?v=2025-10-061">
|
||||
<link rel="stylesheet" href="/assets/css/admin.css?v=2025-10-061">
|
||||
|
||||
<!-- Toast -->
|
||||
<link rel="stylesheet" href="/assets/css/toast.css">
|
||||
<script src="/assets/js/toast.js" defer></script>
|
||||
|
||||
<!-- Klein & lokal: Nur falls admin.css kein eigenes Login-Layout setzt -->
|
||||
<style>
|
||||
:root{--bg:#f6f7fb;--card:#fff;--bd:#e5e7eb;--txt:#0f172a}
|
||||
body.login-fallback{background:var(--bg)}
|
||||
.wrap{max-width:380px;margin:10vh auto;background:var(--card);border:1px solid var(--bd);
|
||||
border-radius:16px;box-shadow:0 10px 30px rgba(2,6,23,.06);padding:28px}
|
||||
h1{margin:0 0 8px;font-size:20px}
|
||||
p{margin:0 0 18px;color:#475569}
|
||||
label{display:block;margin:12px 0 6px;color:#334155}
|
||||
input{width:100%;padding:12px;border:1px solid #cbd5e1;border-radius:10px;font-size:15px}
|
||||
button{width:100%;margin-top:16px;padding:12px;border:0;border-radius:12px;background:#111827;color:#fff;font-weight:600;cursor:pointer}
|
||||
.mini{margin-top:10px;text-align:center}
|
||||
a{color:#111827}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
async function already(){
|
||||
try{
|
||||
const r = await fetch('api.php?action=auth.me',{credentials:'include'});
|
||||
const j = await r.json();
|
||||
if(j && j.ok){ location.replace('index.php'); }
|
||||
}catch(e){}
|
||||
}
|
||||
already();
|
||||
|
||||
async function doLogin(e){
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const res = await fetch('api.php?action=auth.login', {
|
||||
method:'POST', credentials:'include',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({email, password})
|
||||
});
|
||||
const j = await res.json().catch(()=>({}));
|
||||
if(j.ok){ location.replace('index.php'); }
|
||||
else { window.showToast?.('Login fehlgeschlagen', {type:'error'}); }
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="page-login login-fallback">
|
||||
<form class="wrap" onsubmit="doLogin(event)">
|
||||
<h1>Willkommen zurück</h1>
|
||||
<p>Melde dich an, um deine kundenspezifischen Templates zu verwalten.</p>
|
||||
<label for="email">E-Mail</label>
|
||||
<input id="email" type="email" required autocomplete="username" />
|
||||
<label for="password">Passwort</label>
|
||||
<input id="password" type="password" required autocomplete="current-password" />
|
||||
<button type="submit">Anmelden</button>
|
||||
<div class="mini"><a href="#" onclick="window.showToast?.('Passwort-Reset kommt später');return false;">Passwort vergessen?</a></div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
9
public/tools/config-doctor.php
Normal file
9
public/tools/config-doctor.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$path = realpath(__DIR__ . '/../../inc/config.php');
|
||||
$out = ['path_expected'=>$path,'exists'=>false,'readable'=>false,'type'=>null,'keys'=>[], 'notes'=>[]];
|
||||
if (is_file($path)) { $out['exists']=true; $out['readable']=is_readable($path);
|
||||
try { $cfg = require $path; $out['type']=gettype($cfg); if (is_array($cfg)) { $out['keys']=array_keys($cfg); } }
|
||||
catch (Throwable $e) { $out['notes'][] = $e->getMessage(); }
|
||||
} else { $out['notes'][]='file not found'; }
|
||||
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
118
public/tools/db-doctor.php
Normal file
118
public/tools/db-doctor.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
$conf = @include __DIR__ . '/../../inc/config.php';
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8'); }
|
||||
|
||||
if (!is_array($conf) || !isset($conf['templates'])) {
|
||||
echo 'Invalid config.php (expected return array with keys templates/project)'; exit;
|
||||
}
|
||||
|
||||
$profile = $_GET['profile'] ?? 'templates';
|
||||
$cfg = ($profile==='project') ? ($conf['project'] ?? null) : $conf['templates'];
|
||||
$prefix = (string)(($profile==='project') ? ($conf['project']['prefix'] ?? '') : ($conf['templates']['prefix'] ?? ''));
|
||||
|
||||
$attempts=[]; $pdo=null;
|
||||
$mkPdo=function(array $cfg) use(&$attempts){
|
||||
$host = $cfg['db_host'] ?? null;
|
||||
$socket = $cfg['db_socket'] ?? null;
|
||||
$name = $cfg['db_name'] ?? '';
|
||||
$user = $cfg['db_user'] ?? '';
|
||||
$pass = $cfg['db_pass'] ?? '';
|
||||
$charset = $cfg['db_charset'] ?? 'utf8mb4';
|
||||
$port = (int)($cfg['db_port'] ?? 3306);
|
||||
|
||||
$dsn = $socket
|
||||
? "mysql:unix_socket={$socket};dbname={$name};charset={$charset}"
|
||||
: "mysql:host=".($host?:'127.0.0.1').";port={$port};dbname={$name};charset={$charset}";
|
||||
|
||||
try{
|
||||
$pdo = new PDO($dsn,$user,$pass,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES=>false]);
|
||||
$attempts[]=['dsn'=>$dsn,'ok'=>true];
|
||||
return $pdo;
|
||||
}catch(Throwable $e){
|
||||
$attempts[]=['dsn'=>$dsn,'ok'=>false,'error'=>$e->getMessage()];
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (is_array($cfg)) $pdo=$mkPdo($cfg);
|
||||
|
||||
$tables = [
|
||||
$prefix.'templates',
|
||||
$prefix.'sections',
|
||||
$prefix.'blocks',
|
||||
$prefix.'snippets',
|
||||
$prefix.'template_items',
|
||||
$prefix.'section_items',
|
||||
];
|
||||
|
||||
$tblStatus=[];
|
||||
if ($pdo){
|
||||
foreach($tables as $t){
|
||||
try{ $pdo->query("SELECT 1 FROM {$t} LIMIT 1"); $tblStatus[$t]='ok'; }
|
||||
catch(Throwable $e){ $tblStatus[$t]='missing/invalid: '.$e->getMessage(); }
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<meta charset="utf-8">
|
||||
<title>DB-Doctor (<?=h($profile)?>)</title>
|
||||
<style>
|
||||
body{font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#f8fafc;color:#0f172a;margin:0;padding:24px;}
|
||||
.nav a{display:inline-block;margin-right:8px;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;text-decoration:none;color:#0f172a}
|
||||
.nav .active{background:#eef2ff;border-color:#c7d2fe;}
|
||||
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin:16px 0;}
|
||||
table{border-collapse:collapse;width:100%;}
|
||||
th,td{border-bottom:1px solid #e5e7eb;padding:8px 6px;text-align:left;}
|
||||
.ok{color:#166534} .bad{color:#991b1b}
|
||||
code{background:#0b1020;color:#e5e7eb;padding:2px 6px;border-radius:6px}
|
||||
</style>
|
||||
|
||||
<h1>DB-Doctor <small style="font-weight:400;color:#475569">(Profil: <?=h($profile)?>)</small></h1>
|
||||
|
||||
<div class="nav">
|
||||
<a href="?profile=templates" class="<?= $profile==='templates'?'active':'' ?>">Templates</a>
|
||||
<a href="?profile=project" class="<?= $profile==='project' ?'active':'' ?>">Project</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Verbindungsversuche</h3>
|
||||
<table>
|
||||
<tr><th>DSN</th><th>Ergebnis</th><th>Detail</th></tr>
|
||||
<?php foreach($attempts as $a): ?>
|
||||
<tr>
|
||||
<td><code><?=h($a['dsn'])?></code></td>
|
||||
<td><?= !empty($a['ok']) ? '<span class="ok">OK</span>' : '<span class="bad">FAIL</span>' ?></td>
|
||||
<td><?= h($a['error'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Tabellen-Check (Templates-Schema)</h3>
|
||||
<table>
|
||||
<tr><th>Tabelle</th><th>Status</th></tr>
|
||||
<?php foreach($tables as $t): ?>
|
||||
<tr>
|
||||
<td><code><?=h($t)?></code></td>
|
||||
<td>
|
||||
<?php $s=$tblStatus[$t]??'not checked';
|
||||
echo ($s==='ok') ? '<span class="ok">OK</span>' : '<span class="bad">'.h($s).'</span>'; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Rohdaten</h3>
|
||||
<pre><?=h(json_encode([
|
||||
'prefix'=>$prefix,
|
||||
'hasPdo'=>!!$pdo,
|
||||
'configKeys'=>array_keys($conf),
|
||||
], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES))?></pre>
|
||||
</div>
|
||||
</html>
|
||||
|
||||
1
public/vendor/grapesjs-preset-newsletter/.keep
vendored
Normal file
1
public/vendor/grapesjs-preset-newsletter/.keep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Place vendor files here (grapesjs, grapesjs-preset-newsletter).
|
||||
15
public/vendor/grapesjs-preset-newsletter/grapesjs-preset-newsletter.min.js
vendored
Normal file
15
public/vendor/grapesjs-preset-newsletter/grapesjs-preset-newsletter.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/vendor/grapesjs/.keep
vendored
Normal file
1
public/vendor/grapesjs/.keep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Place vendor files here (grapesjs, grapesjs-preset-newsletter).
|
||||
1
public/vendor/grapesjs/grapes.min.css
vendored
Normal file
1
public/vendor/grapesjs/grapes.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/vendor/grapesjs/grapes.min.js
vendored
Normal file
3
public/vendor/grapesjs/grapes.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/version.php
Normal file
3
public/version.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
echo phpinfo();
|
||||
?>
|
||||
Reference in New Issue
Block a user