From ee35e335d76bfd8edef6cf7b642a59be07a8841a Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Wed, 5 Nov 2025 21:40:14 +1030 Subject: [PATCH] feat(unsaved-changes): implement utility for detecting and handling unsaved changes --- .../ui/modal/UnsavedChangesModal.svelte | 31 ++++++++ src/lib/client/utils/unsavedChanges.svelte.ts | 78 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/lib/client/ui/modal/UnsavedChangesModal.svelte create mode 100644 src/lib/client/utils/unsavedChanges.svelte.ts diff --git a/src/lib/client/ui/modal/UnsavedChangesModal.svelte b/src/lib/client/ui/modal/UnsavedChangesModal.svelte new file mode 100644 index 0000000..f3c0e66 --- /dev/null +++ b/src/lib/client/ui/modal/UnsavedChangesModal.svelte @@ -0,0 +1,31 @@ + + + unsavedChanges.confirmDiscard()} + on:cancel={() => unsavedChanges.cancelDiscard()} +/> diff --git a/src/lib/client/utils/unsavedChanges.svelte.ts b/src/lib/client/utils/unsavedChanges.svelte.ts new file mode 100644 index 0000000..2c02f64 --- /dev/null +++ b/src/lib/client/utils/unsavedChanges.svelte.ts @@ -0,0 +1,78 @@ +/** + * Utility for detecting and handling unsaved changes + */ + +let hasUnsavedChanges = $state(false); +let showWarningModal = $state(false); +let resolveNavigation: ((value: boolean) => void) | null = null; + +export function useUnsavedChanges() { + return { + /** + * Mark the page as having unsaved changes + */ + markDirty() { + hasUnsavedChanges = true; + }, + + /** + * Mark the page as clean (changes saved) + */ + markClean() { + hasUnsavedChanges = false; + }, + + /** + * Check if there are unsaved changes + */ + get isDirty() { + return hasUnsavedChanges; + }, + + /** + * Get modal state + */ + get showModal() { + return showWarningModal; + }, + + /** + * Request navigation confirmation + * Returns a promise that resolves to true if navigation should proceed + */ + confirmNavigation(): Promise { + if (!hasUnsavedChanges) { + return Promise.resolve(true); + } + + showWarningModal = true; + + return new Promise((resolve) => { + resolveNavigation = resolve; + }); + }, + + /** + * User confirmed navigation (discard changes) + */ + confirmDiscard() { + showWarningModal = false; + hasUnsavedChanges = false; + if (resolveNavigation) { + resolveNavigation(true); + resolveNavigation = null; + } + }, + + /** + * User cancelled navigation (stay on page) + */ + cancelDiscard() { + showWarningModal = false; + if (resolveNavigation) { + resolveNavigation(false); + resolveNavigation = null; + } + } + }; +}