fradrive/frontend/src/utils/form/navigate-away-prompt.js
2022-10-12 09:35:16 +02:00

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;
}
}