611 lines
18 KiB
JavaScript
611 lines
18 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
var formUtilities = [];
|
|
|
|
/**
|
|
*
|
|
* Reactive Submit Button Utility
|
|
* disables a forms LAST sumit button as long as the required inputs are invalid
|
|
* (only checks if the value of the inputs are not empty)
|
|
*
|
|
* Attribute: [none]
|
|
* (automatically setup on all form tags)
|
|
*
|
|
* Params:
|
|
* data-formnorequired: string
|
|
* If present the submit button will never get disabled
|
|
*
|
|
* Example usage:
|
|
* <form uw-reactive-submit-button>
|
|
* <input type="text" required>
|
|
* <button type="submit">
|
|
* </form>
|
|
*/
|
|
|
|
var REACTIVE_SUBMIT_BUTTON_UTIL_NAME = 'reactiveSubmitButton';
|
|
var REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR = 'form';
|
|
|
|
var REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized';
|
|
|
|
var reactiveSubmitButtonUtil = function(element) {
|
|
var requiredInputs;
|
|
var submitButton;
|
|
|
|
function init() {
|
|
if (!element) {
|
|
throw new Error('Reactive Submit Button utility cannot be setup without an element!');
|
|
}
|
|
|
|
if (element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
|
|
return false;
|
|
}
|
|
|
|
// abort if form has param data-formnorequired
|
|
if (element.dataset.formnorequired !== undefined) {
|
|
throw new Error('Form has formnorequired data attribute. Will skip setup of reactive submit button.');
|
|
}
|
|
|
|
requiredInputs = Array.from(element.querySelectorAll('[required]'));
|
|
if (!requiredInputs) {
|
|
// abort if form has no required inputs
|
|
throw new Error('Submit button has formnorequired data attribute. Will skip setup of reactive submit button.');
|
|
}
|
|
|
|
var submitButtons = Array.from(element.querySelectorAll('[type="submit"]'));
|
|
if (!submitButtons || !submitButtons.length) {
|
|
throw new Error('Reactive Submit Button utility couldn\'t find any submit buttons!');
|
|
}
|
|
submitButton = submitButtons.reverse()[0];
|
|
// abort if form has param data-formnorequired
|
|
if (submitButton.dataset.formnorequired !== undefined) {
|
|
return false;
|
|
}
|
|
|
|
setupInputs();
|
|
updateButtonState();
|
|
|
|
element.classList.add(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
|
|
|
return {
|
|
name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
|
|
element: element,
|
|
destroy: function() {},
|
|
};
|
|
}
|
|
|
|
function setupInputs() {
|
|
requiredInputs.forEach(function(el) {
|
|
var checkbox = el.getAttribute('type') === 'checkbox';
|
|
var eventType = checkbox ? 'change' : 'input';
|
|
el.addEventListener(eventType, function() {
|
|
updateButtonState();
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateButtonState() {
|
|
if (inputsValid()) {
|
|
submitButton.removeAttribute('disabled');
|
|
} else {
|
|
submitButton.setAttribute('disabled', 'true');
|
|
}
|
|
}
|
|
|
|
function inputsValid() {
|
|
var done = true;
|
|
requiredInputs.forEach(function(inp) {
|
|
var len = inp.value.trim().length;
|
|
if (done && len === 0) {
|
|
done = false;
|
|
}
|
|
});
|
|
return done;
|
|
}
|
|
|
|
return init();
|
|
};
|
|
|
|
// skipping reactiveButtonUtil (for now)
|
|
// the button did not properly re-enable after filling out a form for some safari users.
|
|
// if maybe in the future there is going to be a proper way of (asynchronously) and
|
|
// meaningfully validating forms this can be re-activated by commenting in the next few lines
|
|
// formUtilities.push({
|
|
// name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
|
|
// selector: REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR,
|
|
// setup: reactiveSubmitButtonUtil,
|
|
// });
|
|
|
|
/**
|
|
*
|
|
* Interactive Fieldset Utility
|
|
* shows/hides inputs based on value of particular input
|
|
*
|
|
* Attribute: uw-interactive-fieldset
|
|
*
|
|
* Params:
|
|
* data-conditional-input: string
|
|
* Selector for the input that this fieldset watches for changes
|
|
* data-conditional-value: string
|
|
* The value the conditional input needs to be set to for this fieldset to be shown
|
|
* Can be omitted if conditionalInput is a checkbox
|
|
*
|
|
* Example usage:
|
|
* ## example with text input
|
|
* <input id="input-0" type="text">
|
|
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="yes">...</fieldset>
|
|
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="no">...</fieldset>
|
|
* ## example with <select>
|
|
* <select id="select-0">
|
|
* <option value="0">Zero
|
|
* <option value="1">One
|
|
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="0">...</fieldset>
|
|
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="1">...</fieldset>
|
|
* ## example with checkbox
|
|
* <input id="checkbox-0" type="checkbox">
|
|
* <input id="checkbox-1" type="checkbox">
|
|
* <fieldset uw-interactive-fieldset data-conditional-input="#checkbox-0">...</fieldset>
|
|
* <fieldset uw-interactive-fieldset data-conditional-input="#checkbox-1">...</fieldset>
|
|
*/
|
|
|
|
var INTERACTIVE_FIELDSET_UTIL_NAME = 'interactiveFieldset';
|
|
var INTERACTIVE_FIELDSET_UTIL_SELECTOR = '[uw-interactive-fieldset]';
|
|
var INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR = '.interactive-fieldset__target';
|
|
|
|
var INTERACTIVE_FIELDSET_INITIALIZED_CLASS = 'interactive-fieldset--initialized';
|
|
var INTERACTIVE_FIELDSET_CHILD_SELECTOR = 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])';
|
|
|
|
var interactiveFieldsetUtil = function(element) {
|
|
var conditionalInput;
|
|
var conditionalValue;
|
|
var target;
|
|
var childInputs;
|
|
|
|
function init() {
|
|
if (!element) {
|
|
throw new Error('Interactive Fieldset utility cannot be setup without an element!');
|
|
}
|
|
|
|
if (element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) {
|
|
return false;
|
|
}
|
|
|
|
// param conditionalInput
|
|
if (!element.dataset.conditionalInput) {
|
|
throw new Error('Interactive Fieldset needs a selector for a conditional input!');
|
|
}
|
|
conditionalInput = document.querySelector('#' + element.dataset.conditionalInput);
|
|
if (!conditionalInput) {
|
|
// abort if form has no required inputs
|
|
throw new Error('Couldn\'t find the conditional input. Aborting setup for interactive fieldset.');
|
|
}
|
|
|
|
// param conditionalValue
|
|
if (!element.dataset.conditionalValue && !isCheckbox()) {
|
|
throw new Error('Interactive Fieldset needs a conditional value!');
|
|
}
|
|
conditionalValue = element.dataset.conditionalValue;
|
|
|
|
target = element.closest(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR);
|
|
if (!target || element.matches(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR)) {
|
|
target = element;
|
|
}
|
|
|
|
childInputs = Array.from(element.querySelectorAll(INTERACTIVE_FIELDSET_CHILD_SELECTOR));
|
|
|
|
// add event listener
|
|
var observer = new MutationObserver(function(mutationsList, observer) {
|
|
updateVisibility();
|
|
});
|
|
observer.observe(conditionalInput, { attributes: true, attributeFilter: ['disabled'] });
|
|
conditionalInput.addEventListener('input', updateVisibility);
|
|
|
|
// initial visibility update
|
|
updateVisibility();
|
|
|
|
// mark as initialized
|
|
element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS);
|
|
|
|
return {
|
|
name: INTERACTIVE_FIELDSET_UTIL_NAME,
|
|
element: element,
|
|
destroy: function() {},
|
|
};
|
|
}
|
|
|
|
function updateVisibility() {
|
|
var active = matchesConditionalValue() && !conditionalInput.disabled;
|
|
|
|
target.classList.toggle('hidden', !active);
|
|
|
|
childInputs.forEach(function(el) {
|
|
el.disabled = !active;
|
|
if (el._flatpickr) {
|
|
el._flatpickr.altInput.disabled = !active;
|
|
}
|
|
});
|
|
}
|
|
|
|
function matchesConditionalValue() {
|
|
if (isCheckbox()) {
|
|
return conditionalInput.checked === true;
|
|
}
|
|
|
|
return conditionalInput.value === conditionalValue;
|
|
}
|
|
|
|
function isCheckbox() {
|
|
return conditionalInput.getAttribute('type') === 'checkbox';
|
|
}
|
|
|
|
return init();
|
|
};
|
|
|
|
formUtilities.push({
|
|
name: INTERACTIVE_FIELDSET_UTIL_NAME,
|
|
selector: INTERACTIVE_FIELDSET_UTIL_SELECTOR,
|
|
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
|
|
* Hides submit buttons in forms that are submitted programmatically
|
|
* We hide the button using JavaScript so no-js users will still be able to submit the form
|
|
*
|
|
* Attribute: uw-auto-submit-button
|
|
*
|
|
* Example usage:
|
|
* <button type="submit" uw-auto-submit-button>Submit
|
|
*/
|
|
|
|
var AUTO_SUBMIT_BUTTON_UTIL_NAME = 'autoSubmitButton';
|
|
var AUTO_SUBMIT_BUTTON_UTIL_SELECTOR = '[uw-auto-submit-button]';
|
|
|
|
var AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS = 'auto-submit-button--initialized';
|
|
var AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden';
|
|
|
|
var autoSubmitButtonUtil = function(element) {
|
|
if (!element) {
|
|
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);
|
|
|
|
return {
|
|
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
|
|
element: element,
|
|
destroy: function() {},
|
|
};
|
|
};
|
|
|
|
formUtilities.push({
|
|
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
|
|
selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR,
|
|
setup: autoSubmitButtonUtil,
|
|
});
|
|
|
|
/**
|
|
*
|
|
* Auto Submit Input Utility
|
|
* Programmatically submits forms when a certain input changes value
|
|
*
|
|
* Attribute: uw-auto-submit-input
|
|
*
|
|
* Example usage:
|
|
* <input type="text" uw-auto-submit-input />
|
|
*/
|
|
|
|
var AUTO_SUBMIT_INPUT_UTIL_NAME = 'autoSubmitInput';
|
|
var AUTO_SUBMIT_INPUT_UTIL_SELECTOR = '[uw-auto-submit-input]';
|
|
|
|
var AUTO_SUBMIT_INPUT_INITIALIZED_CLASS = 'auto-submit-input--initialized';
|
|
|
|
var autoSubmitInputUtil = function(element) {
|
|
var form;
|
|
var debouncedHandler;
|
|
|
|
function autoSubmit() {
|
|
form.submit();
|
|
}
|
|
|
|
function init() {
|
|
if (!element) {
|
|
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');
|
|
}
|
|
|
|
debouncedHandler = debounce(autoSubmit, 500);
|
|
|
|
element.addEventListener('input', debouncedHandler);
|
|
|
|
element.classList.add(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS);
|
|
|
|
return {
|
|
name: AUTO_SUBMIT_INPUT_UTIL_NAME,
|
|
element: element,
|
|
destroy: function() {
|
|
element.removeEventListener('input', debouncedHandler);
|
|
},
|
|
};
|
|
}
|
|
|
|
return init();
|
|
};
|
|
|
|
formUtilities.push({
|
|
name: AUTO_SUBMIT_INPUT_UTIL_NAME,
|
|
selector: AUTO_SUBMIT_INPUT_UTIL_SELECTOR,
|
|
setup: autoSubmitInputUtil,
|
|
});
|
|
|
|
/**
|
|
*
|
|
* Form Error Remover Utility
|
|
* Removes errors from inputs when they are focused
|
|
*
|
|
* Attribute: [none]
|
|
* (automatically setup on all form tags)
|
|
*
|
|
* Example usage:
|
|
* (any regular form that can show input errors)
|
|
*/
|
|
|
|
var FORM_ERROR_REMOVER_UTIL_NAME = 'formErrorRemover';
|
|
var FORM_ERROR_REMOVER_UTIL_SELECTOR = 'form';
|
|
|
|
var FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized';
|
|
var FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select';
|
|
|
|
var FORM_GROUP_SELECTOR = '.form-group';
|
|
var FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error';
|
|
|
|
|
|
var formErrorRemoverUtil = function(element) {
|
|
var formGroups;
|
|
|
|
function init() {
|
|
if (!element) {
|
|
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));
|
|
|
|
formGroups.forEach(function(formGroup) {
|
|
if (!formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS)) {
|
|
return;
|
|
}
|
|
|
|
var inputElements = Array.from(formGroup.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR));
|
|
if (!inputElements) {
|
|
return false;
|
|
}
|
|
|
|
inputElements.forEach(function(inputElement) {
|
|
inputElement.addEventListener('input', function() {
|
|
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS);
|
|
});
|
|
});
|
|
});
|
|
|
|
// mark initialized
|
|
element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
|
|
|
|
return {
|
|
name: FORM_ERROR_REMOVER_UTIL_NAME,
|
|
element: element,
|
|
destroy: function() {},
|
|
};
|
|
}
|
|
|
|
return init();
|
|
};
|
|
|
|
formUtilities.push({
|
|
name: FORM_ERROR_REMOVER_UTIL_NAME,
|
|
selector: FORM_ERROR_REMOVER_UTIL_SELECTOR,
|
|
setup: formErrorRemoverUtil,
|
|
});
|
|
|
|
/**
|
|
*
|
|
* Datepicker Utility
|
|
* Provides UI for entering dates and times
|
|
*
|
|
* Attribute: [none]
|
|
* (automatically setup on all relevant input tags)
|
|
*
|
|
* Example usage:
|
|
* (any form that uses inputs of type date, time, or datetime-local)
|
|
*/
|
|
|
|
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,
|
|
altInput: true,
|
|
altFormat: "j. F Y, H:i", // maybe interpolate these formats for locale
|
|
dateFormat: "Y-m-dTH:i",
|
|
time_24hr: true
|
|
},
|
|
"date": {
|
|
altFormat: "j. F Y",
|
|
dateFormat: "Y-m-d",
|
|
altInput: true
|
|
},
|
|
"time": {
|
|
enableTime: true,
|
|
noCalendar: true,
|
|
altFormat: "H:i",
|
|
dateFormat: "H:i",
|
|
altInput: true,
|
|
time_24hr: true
|
|
}
|
|
};
|
|
|
|
var datepickerUtil = function(element) {
|
|
var flatpickrInstance;
|
|
|
|
function init() {
|
|
if (!element) {
|
|
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) {
|
|
throw new Error('Datepicker utility called on unsupported element!');
|
|
}
|
|
|
|
flatpickrInstance = flatpickr(element, flatpickrConfig);
|
|
|
|
// mark initialized
|
|
element.classList.add(DATEPICKER_INITIALIZED_CLASS);
|
|
|
|
return {
|
|
name: DATEPICKER_UTIL_NAME,
|
|
element: element,
|
|
destroy: function() { flatpickrInstance.destroy(); },
|
|
};
|
|
}
|
|
|
|
return init();
|
|
};
|
|
|
|
formUtilities.push({
|
|
name: DATEPICKER_UTIL_NAME,
|
|
selector: DATEPICKER_UTIL_SELECTOR,
|
|
setup: datepickerUtil,
|
|
});
|
|
|
|
// debounce function, taken from Underscore.js
|
|
function debounce(func, wait, immediate) {
|
|
var timeout;
|
|
return function() {
|
|
var context = this, args = arguments;
|
|
var later = function() {
|
|
timeout = null;
|
|
if (!immediate) func.apply(context, args);
|
|
};
|
|
var callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if (callNow) func.apply(context, args);
|
|
};
|
|
}
|
|
|
|
// register the collected form utilities
|
|
if (UtilRegistry) {
|
|
formUtilities.forEach(UtilRegistry.register);
|
|
}
|
|
|
|
})();
|