This commit is contained in:
2026-01-20 23:57:46 +01:00
parent 921e3bad25
commit 9e67b8e30f
2 changed files with 75 additions and 20 deletions

View File

@@ -26,10 +26,14 @@ export function initEditor() {
  const sendInfo     = document.getElementById('send_template_info');   const sendInfo     = document.getElementById('send_template_info');
  const btnCancelSend= document.getElementById('btn-cancel-send');   const btnCancelSend= document.getElementById('btn-cancel-send');
  const btnSendNow   = document.getElementById('btn-send-now');   const btnSendNow   = document.getElementById('btn-send-now');
  const sendSender   = document.getElementById('send_sender'); const sendSender   = document.getElementById('send_sender');
  const sendSenderHint = document.getElementById('send_sender_hint'); const sendSenderHint = document.getElementById('send_sender_hint');
  const prevFrame    = document.getElementById('previewFrame'); const prevFrame    = document.getElementById('previewFrame');
  const btnPrevClose = document.getElementById('btn-close-preview'); const btnPrevClose = document.getElementById('btn-close-preview');
const unsavedDialog = document.getElementById('unsavedDialog');
const btnUnsavedCancel = document.getElementById('btn-unsaved-cancel');
const btnUnsavedDiscard = document.getElementById('btn-unsaved-discard');
const btnUnsavedSave = document.getElementById('btn-unsaved-save');
let current = null; // { resource, id, name, section } let current = null; // { resource, id, name, section }
let bridgeListener = null; let bridgeListener = null;
@@ -39,6 +43,7 @@ export function initEditor() {
let currentEditorType = 'grapesjs'; let currentEditorType = 'grapesjs';
let versionItems = []; let versionItems = [];
let savedSnapshot = ''; let savedSnapshot = '';
let lastVersionSelection = '';
  const ok  = (m) => toast(m, true);   const ok  = (m) => toast(m, true);
  const err = (m) => toast(m, false);   const err = (m) => toast(m, false);
@@ -153,16 +158,41 @@ export function initEditor() {
return currentSnapshot !== savedSnapshot; return currentSnapshot !== savedSnapshot;
} }
function showUnsavedDialog() {
return new Promise((resolve) => {
if (!unsavedDialog || typeof unsavedDialog.showModal !== 'function') {
resolve('discard');
return;
}
const cleanup = () => {
btnUnsavedCancel && btnUnsavedCancel.removeEventListener('click', onCancel);
btnUnsavedDiscard && btnUnsavedDiscard.removeEventListener('click', onDiscard);
btnUnsavedSave && btnUnsavedSave.removeEventListener('click', onSave);
};
const closeWith = (choice) => {
cleanup();
unsavedDialog.close();
resolve(choice);
};
const onCancel = () => closeWith('cancel');
const onDiscard = () => closeWith('discard');
const onSave = () => closeWith('save');
btnUnsavedCancel && btnUnsavedCancel.addEventListener('click', onCancel);
btnUnsavedDiscard && btnUnsavedDiscard.addEventListener('click', onDiscard);
btnUnsavedSave && btnUnsavedSave.addEventListener('click', onSave);
unsavedDialog.showModal();
});
}
async function confirmUnsavedChanges() { async function confirmUnsavedChanges() {
const dirty = await hasUnsavedChanges(); const dirty = await hasUnsavedChanges();
if (!dirty) return true; if (!dirty) return 'ok';
const shouldSave = window.confirm('Ungespeicherte Änderungen gefunden. Jetzt speichern?'); const choice = await showUnsavedDialog();
if (shouldSave) { if (choice === 'save') {
const okSave = await save(); const okSave = await save();
if (!okSave) return false; if (!okSave) return 'cancel';
return true;
} }
return true; return choice;
} }
function renderVersionOptions(items) { function renderVersionOptions(items) {
@@ -187,6 +217,11 @@ export function initEditor() {
opt.textContent = label; opt.textContent = label;
versionSelect.appendChild(opt); versionSelect.appendChild(opt);
}); });
if (lastVersionSelection) {
versionSelect.value = lastVersionSelection;
} else {
versionSelect.value = '';
}
} }
async function loadVersionsForCurrent() { async function loadVersionsForCurrent() {
@@ -209,6 +244,7 @@ export function initEditor() {
setEditorType(targetType); setEditorType(targetType);
if (targetType === 'craftjs') { if (targetType === 'craftjs') {
craftEditor?.setContent(data.html || '', data.craftJson || ''); craftEditor?.setContent(data.html || '', data.craftJson || '');
setSavedSnapshotFromData(payload);
return; return;
} }
const editor = await waitForEditor(3000); const editor = await waitForEditor(3000);
@@ -217,17 +253,18 @@ export function initEditor() {
try { try {
const project = JSON.parse(jsonRaw); const project = JSON.parse(jsonRaw);
editor.loadProjectData(project); editor.loadProjectData(project);
setSavedSnapshotFromData(payload);
return; return;
} catch {} } catch {}
} }
const html = normalizeSnapshotValue(data.html); const html = normalizeSnapshotValue(data.html);
editor.setComponents(html); editor.setComponents(html);
setSavedSnapshotFromData(payload);
} }
async function loadLatestContentFromServer() { async function loadLatestContentFromServer() {
const res = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } }); const res = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
await applyVersionPayload(res || {}); await applyVersionPayload(res || {});
setSavedSnapshotFromData(res || {});
} }
  function writeHtmlToFrame(html) {   function writeHtmlToFrame(html) {
@@ -451,11 +488,11 @@ export function initEditor() {
async function open(item, resource, sectionOverride) { async function open(item, resource, sectionOverride) {
const section = item?.section || sectionOverride || window.__activeSection || null; const section = item?.section || sectionOverride || window.__activeSection || null;
current = { current = {
      resource: 'content', resource: 'content',
      id: Number(item?.id || 0), id: Number(item?.id || 0),
      name: item?.name || '', name: item?.name || '',
section: section, section: section,
    }; };
    if (!current.id) return err('Ungültige ID');     if (!current.id) return err('Ungültige ID');
if (!current.section) return err('Section nicht gefunden'); if (!current.section) return err('Section nicht gefunden');
@@ -465,6 +502,8 @@ export function initEditor() {
setSendContext(current.section?.is_template ? current.id : 0, current.name); setSendContext(current.section?.is_template ? current.id : 0, current.name);
if (btnTest) btnTest.classList.toggle('hidden', !current.section?.is_template); if (btnTest) btnTest.classList.toggle('hidden', !current.section?.is_template);
setVersionUiVisible(true); setVersionUiVisible(true);
if (versionSelect) versionSelect.value = '';
lastVersionSelection = '';
    // Neuen Token erzeugen & alten Listener entfernen     // Neuen Token erzeugen & alten Listener entfernen
    reqToken++;     reqToken++;
@@ -699,8 +738,8 @@ export function initEditor() {
  function closePreview(){ prevDlg?.close?.(); }   function closePreview(){ prevDlg?.close?.(); }
async function close() { async function close() {
const proceed = await confirmUnsavedChanges(); const decision = await confirmUnsavedChanges();
if (!proceed) return; if (decision === 'cancel') return;
// nächstes Öffnen invalidiert laufende asyncs // nächstes Öffnen invalidiert laufende asyncs
reqToken++; reqToken++;
@@ -780,22 +819,26 @@ export function initEditor() {
editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value)); editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value));
versionSelect && (versionSelect.onchange = async () => { versionSelect && (versionSelect.onchange = async () => {
if (!current?.id) return; if (!current?.id) return;
const proceed = await confirmUnsavedChanges(); const previousSelection = lastVersionSelection;
if (!proceed) { const decision = await confirmUnsavedChanges();
versionSelect.value = ''; if (decision === 'cancel') {
versionSelect.value = previousSelection;
return; return;
} }
const versionId = Number(versionSelect.value || 0); const versionId = Number(versionSelect.value || 0);
if (!versionId) { if (!versionId) {
await loadLatestContentFromServer(); await loadLatestContentFromServer();
lastVersionSelection = '';
return; return;
} }
try { try {
const res = await apiAction('content_versions.get', { method: 'GET', data: { id: versionId, content_id: current.id } }); const res = await apiAction('content_versions.get', { method: 'GET', data: { id: versionId, content_id: current.id } });
if (!res?.ok) throw new Error(res?.error || 'Version konnte nicht geladen werden'); if (!res?.ok) throw new Error(res?.error || 'Version konnte nicht geladen werden');
await applyVersionPayload(res?.item || res); await applyVersionPayload(res?.item || res);
lastVersionSelection = String(versionId);
} catch (e) { } catch (e) {
err(e.message || 'Version konnte nicht geladen werden'); err(e.message || 'Version konnte nicht geladen werden');
versionSelect.value = previousSelection;
} }
}); });
btnRestoreVersion && (btnRestoreVersion.onclick = async () => { btnRestoreVersion && (btnRestoreVersion.onclick = async () => {

View File

@@ -114,6 +114,18 @@ require __DIR__ . '/../partials/structure/layout_start.php';
</form> </form>
</dialog> </dialog>
<dialog id="unsavedDialog" class="rounded-2xl p-0 w-[440px]">
<div class="p-4 bg-white rounded-2xl space-y-4">
<h3 class="text-lg font-semibold">Ungespeicherte Änderungen</h3>
<p class="text-sm text-slate-600">Es gibt ungespeicherte Änderungen. Möchtest du diese speichern?</p>
<div class="flex justify-end gap-2">
<button type="button" id="btn-unsaved-cancel" class="btn">Abbrechen</button>
<button type="button" id="btn-unsaved-discard" class="btn">Nicht speichern</button>
<button type="button" id="btn-unsaved-save" class="btn">Speichern</button>
</div>
</div>
</dialog>
<!-- Edit Snippet Dialog --> <!-- Edit Snippet Dialog -->
<dialog id="editSnippetDialog" class="rounded-2xl p-0 w-[700px]"> <dialog id="editSnippetDialog" class="rounded-2xl p-0 w-[700px]">
<form id="editSnippetForm" method="dialog" class="p-4 bg-white rounded-2xl"> <form id="editSnippetForm" method="dialog" class="p-4 bg-white rounded-2xl">