134 lines
4.2 KiB
JavaScript
134 lines
4.2 KiB
JavaScript
// SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
|
|
//
|
|
// 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;
|
|
}
|
|
}
|