Store the initial FormData of a form and only prompt if any current value is actually different than the corresponding initial value (i.e. do not trigger prompt just by change events)
90 lines
2.7 KiB
JavaScript
90 lines
2.7 KiB
JavaScript
import { Utility } from '../../core/utility';
|
|
import { AUTO_SUBMIT_BUTTON_UTIL_SELECTOR } from './auto-submit-button';
|
|
import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from './auto-submit-input';
|
|
|
|
/**
|
|
* 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;
|
|
|
|
_initFormData;
|
|
_unloadDueToSubmit = false;
|
|
|
|
constructor(element) {
|
|
if (!element) {
|
|
throw new Error('Navigate Away Prompt utility needs to be passed an element!');
|
|
}
|
|
|
|
this._element = element;
|
|
this._initFormData = new FormData(this._element);
|
|
|
|
if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) {
|
|
return false;
|
|
}
|
|
|
|
// ignore forms that get submitted automatically
|
|
if (this._element.querySelector(AUTO_SUBMIT_BUTTON_UTIL_SELECTOR) || this._element.querySelector(AUTO_SUBMIT_INPUT_UTIL_SELECTOR)) {
|
|
return false;
|
|
}
|
|
|
|
if (this._element.matches(NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT)) {
|
|
return false;
|
|
}
|
|
|
|
window.addEventListener('beforeunload', this._beforeUnloadHandler);
|
|
|
|
this._element.addEventListener('submit', () => {
|
|
this._unloadDueToSubmit = true;
|
|
});
|
|
|
|
// mark initialized
|
|
this._element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS);
|
|
|
|
}
|
|
|
|
destroy() {
|
|
window.removeEventListener('beforeunload', this._beforeUnloadHandler);
|
|
}
|
|
|
|
_beforeUnloadHandler = (event) => {
|
|
// 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);
|
|
var formDataHasChanged = false;
|
|
for (let 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) {
|
|
return false;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|