// SPDX-FileCopyrightText: 2022 Felix Hamann ,Gregor Kleen ,Sarah Vaupel ,Sarah Vaupel // // SPDX-License-Identifier: AGPL-3.0-or-later import { Utility } from '../../core/utility'; import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import { AUTO_SUBMIT_BUTTON_UTIL_SELECTOR } from './auto-submit-button'; import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from './auto-submit-input'; import { InteractiveFieldset } from './interactive-fieldset'; import { Datepicker } from './datepicker'; import defer from 'lodash.defer'; /** * Key generator from an arbitrary number of FormData objects. * @param {...any} formDatas FormData objects */ function* generatorFromFormDatas(...formDatas) { for (let formData of formDatas) { yield* formData.keys(); } } const NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS = 'navigate-away-prompt--initialized'; const NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT = '[uw-no-navigate-away-prompt]'; @Utility({ selector: 'form', }) export class NavigateAwayPrompt { _element; _app; _eventManager; _initFormData; _unloadDueToSubmit = false; constructor(element, app) { if (!element) { throw new Error('Navigate Away Prompt utility needs to be passed an element!'); } this._element = element; this._app = app; this._eventManager = new EventManager(); if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) { return; } // ignore forms that get submitted automatically if (this._element.querySelector(AUTO_SUBMIT_BUTTON_UTIL_SELECTOR) || this._element.querySelector(AUTO_SUBMIT_INPUT_UTIL_SELECTOR)) { return; } if (this._element.matches(NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT)) { return; } if (this._element.attributes.target === '_blank') { return; } // mark initialized this._element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); } static startAfter = [ Datepicker, InteractiveFieldset ]; start() { if (!this._isActive()) return; this._initFormData = new FormData(this._element); const beforeUnloadEvent = new EventWrapper(EVENT_TYPE.BEFOREUNLOAD, this._beforeUnloadHandler.bind(this), window); this._eventManager.registerNewListener(beforeUnloadEvent); const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, (() => { this._unloadDueToSubmit = true; defer(() => { this._unloadDueToSubmit = false; } ); // Restore state after event loop is settled }).bind(this), this._element); this._eventManager.registerNewListener(submitEvent); } destroy() { this._eventManager.cleanUp(); this._element.classList.remove(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); } _isActive() { return this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); } _beforeUnloadHandler(event) { if (!this._isActive() || !this._initFormData) return; // compare every value of the current FormData with every corresponding value of the initial FormData and set formDataHasChanged to true if there is at least one change const currentFormData = new FormData(this._element); let formDataHasChanged = false; for (const key of generatorFromFormDatas(this._initFormData, currentFormData)) { if (currentFormData.get(key) !== this._initFormData.get(key)) { formDataHasChanged = true; break; } } // allow the event to happen if the form was not touched by the // user (i.e. if the current FormData is equal to the initial FormData) // or the unload event was initiated by a form submit if (!formDataHasChanged || this._unloadDueToSubmit || this._parentModalIsClosed()) return; // cancel the unload event. This is the standard to force the prompt to appear. event.preventDefault(); // chrome event.returnValue = true; // for all non standard compliant browsers we return a truthy value to activate the prompt. return true; } _parentModalIsClosed() { const parentModal = this._element.closest('.modal'); if (!parentModal) return false; const modalClosed = !parentModal.classList.contains('modal--open'); return modalClosed; } }