112112
113113<script setup lang="ts">
114114import { ref , computed , unref , watch , onUnmounted } from ' vue'
115+ import { useRouter } from ' vue-router'
115116import { useGettext } from ' vue3-gettext'
116117import { storeToRefs } from ' pinia'
117118import { useGroupwareAccountsStore , useModals } from ' @opencloud-eu/web-pkg'
@@ -132,6 +133,7 @@ type ComposeAttachment = ComposeFormState['attachments'][number]
132133
133134const { $gettext } = useGettext ()
134135const { dispatchModal } = useModals ()
136+ const router = useRouter ()
135137const appliedDraftId = ref <string | null >(null )
136138
137139const SAVED_HINT_DURATION_MS = 2000
@@ -293,6 +295,18 @@ const { draftId, isDirty, isSaving, markDirty, resetDraft, saveAsDraft, discardD
293295 }
294296 })
295297
298+ const hasUnsavedChanges = computed (() => {
299+ return unref (hasMeaningfulChanges ) && unref (isDirty )
300+ })
301+
302+ const isMailRoute = (route : { path: string }) => {
303+ return route .path .startsWith (' /mail' )
304+ }
305+
306+ const preventUnload = (event : BeforeUnloadEvent ) => {
307+ event .preventDefault ()
308+ }
309+
296310const onSend = async () => {
297311 if (
298312 ! unref (currentAccountId ) ||
@@ -346,6 +360,23 @@ const doClose = () => {
346360 emit (' close' )
347361}
348362
363+ const requestUnsavedComposeSave = ({
364+ onCancel ,
365+ onSave
366+ }: {
367+ onCancel: () => void
368+ onSave: () => void | Promise <void >
369+ }) => {
370+ dispatchModal ({
371+ title: $gettext (' Unsaved changes' ),
372+ message: $gettext (' Your email isn’t finished yet. Save it as draft before leaving.' ),
373+ confirmText: $gettext (' Save as draft' ),
374+ hasInput: false ,
375+ onConfirm: onSave ,
376+ onCancel
377+ })
378+ }
379+
349380const requestClose = () => {
350381 if (! unref (hasMeaningfulChanges )) {
351382 doClose ()
@@ -357,15 +388,11 @@ const requestClose = () => {
357388 return
358389 }
359390
360- dispatchModal ({
361- title: $gettext (' Leave this screen?' ),
362- message: $gettext (
363- ' Your email isn’t finished yet. You can save it as a draft or exit without saving.'
364- ),
365- confirmText: $gettext (' Save as draft' ),
366- hasInput: false ,
367- onConfirm : () => onSaveDraftAndClose (),
368- onCancel : () => onDiscardAndClose ()
391+ requestUnsavedComposeSave ({
392+ onCancel : () => {
393+ // Stay in compose.
394+ },
395+ onSave: onSaveDraftAndClose
369396 })
370397}
371398
@@ -377,11 +404,6 @@ const onSaveDraftAndClose = async () => {
377404 doClose ()
378405}
379406
380- const onDiscardAndClose = async () => {
381- await discardDraft ()
382- doClose ()
383- }
384-
385407const canAutoSaveNow = computed (() => {
386408 return unref (canSaveDraft ) && unref (hasMeaningfulChanges ) && unref (isDirty ) && ! unref (isSaving )
387409})
@@ -398,7 +420,53 @@ useAutoSaveDraft({
398420 }
399421})
400422
423+ watch (hasUnsavedChanges , (dirty ) => {
424+ if (dirty ) {
425+ window .addEventListener (' beforeunload' , preventUnload )
426+ } else {
427+ window .removeEventListener (' beforeunload' , preventUnload )
428+ }
429+ })
430+
431+ let isNavigationConfirmationOpen = false
432+
433+ const removeNavigationGuard = router .beforeEach ((to , _from , next ) => {
434+ if (isMailRoute (to ) || ! unref (hasUnsavedChanges )) {
435+ next ()
436+ return
437+ }
438+
439+ if (isNavigationConfirmationOpen ) {
440+ next (false )
441+ return
442+ }
443+
444+ if (! unref (canSaveDraft )) {
445+ next (false )
446+ return
447+ }
448+
449+ isNavigationConfirmationOpen = true
450+
451+ requestUnsavedComposeSave ({
452+ onCancel : () => {
453+ isNavigationConfirmationOpen = false
454+ next (false )
455+ },
456+ onSave : async () => {
457+ try {
458+ await onSaveDraftAndClose ()
459+ next ()
460+ } finally {
461+ isNavigationConfirmationOpen = false
462+ }
463+ }
464+ })
465+ })
466+
401467onUnmounted (() => {
468+ removeNavigationGuard ()
469+ window .removeEventListener (' beforeunload' , preventUnload )
402470 resetCompose ()
403471})
404472
0 commit comments