commit
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user