From 4fe142422a5198b5053c0ece7da40b31551830cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Mummert?= Date: Sun, 5 Jul 2026 20:24:09 +0200 Subject: [PATCH] Warn on leaving the editor with unsaved changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ergänzt einen beforeunload-Handler im Frontend-Editor: Beim Verlassen der Seite (Link, Zurück, Tab schließen) wird gewarnt, sobald mindestens ein Editor-Formular ungespeicherte Änderungen hat. Der Handler koppelt an die bestehende Dirty-Erkennung (data-survey-dirty-form) und wird nur registriert, wenn solche Formulare existieren – also nur im Editor, nicht in Liste/Reader/Ergebnissen. Ein bewusstes Absenden (Speichern oder Aktion) setzt über einen globalen submit-Listener ein Flag, sodass der normale Speicher-Flow nicht warnt. Bewusst kein Auto-Save (getrennte Formulare mit eigenen CSRF-Tokens, Struktur-Sperre nach Teilnahmen/Veröffentlichung); Dirty-Hinweis plus beforeunload sind der risikoarme Kompromiss. Co-Authored-By: Claude Opus 4.8 --- AI_HANDOVER.md | 8 +++ .../frontend/_survey_assets.html.twig | 61 +++++++++++++++---- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/AI_HANDOVER.md b/AI_HANDOVER.md index 5d04a00..7a2335f 100644 --- a/AI_HANDOVER.md +++ b/AI_HANDOVER.md @@ -170,6 +170,14 @@ Aktueller Aufbau: - Fragen im Frontend nicht mehr änderbar - Metadaten im Frontend ebenfalls nicht mehr bearbeitbar +Ungespeicherte Änderungen (bewusst kein Auto-Save): + +- Jedes Editor-Formular (`data-survey-dirty-form`: Metadaten, jede Frage, neue Frage) trackt clientseitig, ob sich der Inhalt gegenüber dem Ladezustand geändert hat, und blendet den roten „Ungespeicherte Änderungen"-Hinweis ein. +- Zusätzlich gibt es einen `beforeunload`-Handler (`_survey_assets.html.twig`), der beim Verlassen der Seite (Link, Zurück, Tab schließen) warnt, sobald mindestens ein Formular ungespeicherte Änderungen hat. + - Der Handler wird nur registriert, wenn dirty-getrackte Formulare existieren (also nur im Editor, nicht in Liste/Reader/Ergebnissen). + - Ein bewusstes Absenden (Speichern oder eine Aktion) setzt ein `surveyIsSubmitting`-Flag über einen globalen `submit`-Listener, damit der normale Speicher-Flow **nicht** warnt. +- Auto-Save wurde bewusst nicht umgesetzt (getrennte Formulare mit eigenen CSRF-Tokens, Struktur-Sperre nach Teilnahmen/Veröffentlichung). Der Dirty-Hinweis plus die `beforeunload`-Warnung sind der gewählte, risikoarme Kompromiss. + Wichtige Entscheidung: - Die Header-Badges im Fragen-Akkordeon wurden bewusst entfernt. diff --git a/contao/templates/frontend/_survey_assets.html.twig b/contao/templates/frontend/_survey_assets.html.twig index 545375b..1da714c 100644 --- a/contao/templates/frontend/_survey_assets.html.twig +++ b/contao/templates/frontend/_survey_assets.html.twig @@ -1166,6 +1166,9 @@ }); }); + var surveyDirtyChecks = []; + var surveyIsSubmitting = false; + document.querySelectorAll('[data-survey-dirty-form]').forEach(function (form) { var formId = form.getAttribute('id') || ''; var hint = form.querySelector('[data-survey-dirty-hint]'); @@ -1174,10 +1177,6 @@ hint = document.querySelector('[data-survey-dirty-hint-for="' + formId + '"]'); } - if (!hint) { - return; - } - var serializeForm = function () { var entries = []; var formData = new FormData(form); @@ -1191,25 +1190,61 @@ var initialState = serializeForm(); + var isDirty = function () { + return serializeForm() !== initialState; + }; + + surveyDirtyChecks.push(isDirty); + var updateDirtyState = function () { - hint.hidden = serializeForm() === initialState; + if (hint) { + hint.hidden = !isDirty(); + } }; - var markDirty = function () { - updateDirtyState(); - }; + if (hint) { + hint.hidden = true; + } - hint.hidden = true; - - form.addEventListener('input', markDirty); - form.addEventListener('change', markDirty); + form.addEventListener('input', updateDirtyState); + form.addEventListener('change', updateDirtyState); form.addEventListener('reset', function () { window.setTimeout(function () { - hint.hidden = true; + if (hint) { + hint.hidden = true; + } }, 0); }); }); + if (surveyDirtyChecks.length > 0) { + // Ein bewusstes Absenden (Speichern oder eine Aktion) darf nicht warnen. + document.addEventListener('submit', function () { + surveyIsSubmitting = true; + }, true); + + window.addEventListener('beforeunload', function (event) { + if (surveyIsSubmitting) { + return undefined; + } + + var hasUnsavedChanges = surveyDirtyChecks.some(function (isDirty) { + return isDirty(); + }); + + if (!hasUnsavedChanges) { + return undefined; + } + + // Moderne Browser zeigen einen generischen Hinweis; der Text wird + // ignoriert, muss aber gesetzt werden, damit der Dialog erscheint. + event.preventDefault(); + event.returnValue = ''; + + return ''; + }); + } + if (typeof handorgel === 'function') { document.querySelectorAll('[data-survey-handorgel]').forEach(function (element) { var accordion = new handorgel(element, {