feat(modal): warn before closing with unsaved changes

This commit is contained in:
Ulas Kalayci
2026-04-26 19:03:38 +02:00
parent 798f8ca87a
commit ed0f8b2d57
27 changed files with 112 additions and 40 deletions
+39 -2
View File
@@ -16,6 +16,7 @@ import { t } from '/i18n.js';
let activeOverlay = null;
let previouslyFocused = null;
let focusTrapHandler = null;
let _initialFormSnapshot = null;
// Overlay-Dimming: theme-color abdunkeln im Standalone-Modus
const OVERLAY_THEME_COLOR = '#1A1A1A';
@@ -98,6 +99,20 @@ function trapFocus(container) {
}
}
// --------------------------------------------------------
// Dirty-Check Helpers
// --------------------------------------------------------
function serializeForm(container) {
const inputs = container.querySelectorAll('input, select, textarea');
return Array.from(inputs).map((el) => `${el.name || el.id}=${el.value}`).join('&');
}
function isFormDirty(container) {
if (!_initialFormSnapshot) return false;
return serializeForm(container) !== _initialFormSnapshot;
}
// --------------------------------------------------------
// Escape-Handler
// --------------------------------------------------------
@@ -204,9 +219,10 @@ export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}
// ID sofort entfernen damit getElementById() nach dem Einfügen des neuen Modals
// nicht die noch animierende alte Instanz zurückgibt sonst landen alle
// Event-Listener am falschen Element und Buttons reagieren nicht.
// force=true: kein Dirty-Check beim programmatischen Ersetzen (z.B. confirmModal öffnet sich).
if (activeOverlay) {
activeOverlay.removeAttribute('id');
closeModal();
closeModal({ force: true });
}
// Focus-Restore vorbereiten
@@ -243,6 +259,14 @@ export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}
const panel = activeOverlay.querySelector('.modal-panel');
trapFocus(panel);
// Snapshot für Dirty-Check (kurzer Delay: Felder könnten noch per JS befüllt werden)
_initialFormSnapshot = null;
setTimeout(() => {
if (activeOverlay) {
_initialFormSnapshot = serializeForm(activeOverlay.querySelector('.modal-panel') ?? activeOverlay);
}
}, 150);
// Swipe-to-Close auf Mobile
if (window.innerWidth < 768) {
_wireSheetSwipe(panel);
@@ -279,9 +303,22 @@ export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}
// closeModal
// --------------------------------------------------------
export function closeModal() {
export async function closeModal({ force = false } = {}) {
if (!activeOverlay) return;
if (!force) {
const panel = activeOverlay.querySelector('.modal-panel');
if (panel && isFormDirty(panel)) {
const confirmed = await confirmModal(t('modal.unsavedChanges'), {
danger: false,
confirmLabel: t('modal.discardChanges'),
});
if (!confirmed) return;
}
}
_initialFormSnapshot = null;
document.removeEventListener('keydown', onEscape);
// Overlay sofort sichern: Bei Mobile-Animation öffnet openModal() ein neues Modal