diff --git a/static/js/utils/form.js b/static/js/utils/form.js index da02abd5d..ed8b0fa9a 100644 --- a/static/js/utils/form.js +++ b/static/js/utils/form.js @@ -247,6 +247,88 @@ setup: interactiveFieldsetUtil, }); + /** + * + * Navigate Away Prompt Utility + * This utility asks the user if (s)he really wants to navigate away + * from a page containing a form if (s)he already touched an input. + * Form-Submits will not trigger the prompt. + * Utility will ignore forms that contain auto submit elements (buttons, inputs). + * + * Attribute: [none] + * (automatically setup on all form tags that dont automatically submit, see AutoSubmitButtonUtil) + * + * Example usage: + * (any page with a form) + */ + + var NAVIGATE_AWAY_PROMPT_UTIL_NAME = 'navigateAwayPrompt'; + var NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR = 'form'; + + var NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS = 'navigate-away-prompt--initialized'; + + var navigateAwayPromptUtil = function(element) { + var touched = false; + var unloadDueToSubmit = false; + + function init() { + if (!element) { + throw new Error('Navigate Away Prompt utility needs to be passed an element!'); + } + + if (element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) { + return false; + } + + // ignore forms that get submitted automatically + if (element.querySelector(AUTO_SUBMIT_BUTTON_UTIL_SELECTOR) || element.querySelector(AUTO_SUBMIT_INPUT_UTIL_SELECTOR)) { + return false; + } + + window.addEventListener('beforeunload', beforeUnloadHandler); + + element.addEventListener('submit', function() { + unloadDueToSubmit = true; + }); + element.addEventListener('change', function() { + touched = true; + unloadDueToSubmit = false; + }); + + // mark initialized + element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); + + return { + name: NAVIGATE_AWAY_PROMPT_UTIL_NAME, + element: element, + destroy: function() { + window.removeEventListener('beforeunload', beforeUnloadHandler); + }, + }; + } + + function beforeUnloadHandler(event) { + // allow the event to happen if the form was not touched by the + // user or the unload event was initiated by a form submit + if (!touched || unloadDueToSubmit) { + return false; + } + + // cancel the unload event. This is the standard to force the prompt to appear. + event.preventDefault(); + // for all non standard compliant browsers we return a truthy value to activate the prompt. + return true; + } + + return init(); + }; + + formUtilities.push({ + name: NAVIGATE_AWAY_PROMPT_UTIL_NAME, + selector: NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR, + setup: navigateAwayPromptUtil, + }); + /** * * Auto Submit Button Utility @@ -270,6 +352,10 @@ throw new Error('Auto Submit Button utility needs to be passed an element!'); } + if (element.classList.contains(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS)) { + return false; + } + // hide and mark initialized element.classList.add(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS); @@ -315,6 +401,10 @@ throw new Error('Auto Submit Input utility needs to be passed an element!'); } + if (element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) { + return false; + } + form = element.form; if (!form) { throw new Error('Could not determine associated form for auto submit input'); @@ -374,6 +464,10 @@ throw new Error('Form Error Remover utility needs to be passed an element!'); } + if (element.classList.contains(FORM_ERROR_REMOVER_INITIALIZED_CLASS)) { + return false; + } + // find form groups formGroups = Array.from(element.querySelectorAll(FORM_GROUP_SELECTOR)); @@ -428,6 +522,8 @@ var DATEPICKER_UTIL_NAME = 'datepicker'; var DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]'; + var DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized'; + var DATEPICKER_CONFIG = { "datetime-local": { enableTime: true, @@ -459,6 +555,10 @@ throw new Error('Datepicker utility needs to be passed an element!'); } + if (element.classList.contains(DATEPICKER_INITIALIZED_CLASS)) { + return false; + } + var flatpickrConfig = DATEPICKER_CONFIG[element.getAttribute("type")]; if (!flatpickrConfig) { @@ -467,6 +567,9 @@ flatpickrInstance = flatpickr(element, flatpickrConfig); + // mark initialized + element.classList.add(DATEPICKER_INITIALIZED_CLASS); + return { name: DATEPICKER_UTIL_NAME, element: element,