Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
216 lines
6.9 KiB
JavaScript
216 lines
6.9 KiB
JavaScript
import Singleton from '../abstracts/Singleton';
|
|
|
|
/**
|
|
* Adds AJAX-driven form validation to Snowboard requests.
|
|
*
|
|
* Documentation for this feature can be found here:
|
|
* https://wintercms.com/docs/snowboard/extras#ajax-validation
|
|
*
|
|
* @copyright 2022 Winter.
|
|
* @author Ben Thomson <git@alfreido.com>
|
|
*/
|
|
export default class FormValidation extends Singleton {
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
construct() {
|
|
this.errorBags = [];
|
|
}
|
|
|
|
/**
|
|
* Defines listeners.
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
listens() {
|
|
return {
|
|
ready: 'ready',
|
|
ajaxStart: 'clearValidation',
|
|
ajaxValidationErrors: 'doValidation',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Ready event handler.
|
|
*/
|
|
ready() {
|
|
this.collectErrorBags(document);
|
|
}
|
|
|
|
/**
|
|
* Retrieves validation errors from an AJAX response and passes them through to the error bags.
|
|
*
|
|
* This handler returns false to cancel any further validation handling, and prevents the flash
|
|
* message that is displayed by default for field errors in AJAX requests from showing.
|
|
*
|
|
* @param {HTMLFormElement} form
|
|
* @param {Object} invalidFields
|
|
* @param {Request} request
|
|
* @returns {Boolean}
|
|
*/
|
|
doValidation(form, invalidFields, request) {
|
|
if (request.element && request.element.dataset.requestValidate === undefined) {
|
|
return null;
|
|
}
|
|
if (!form) {
|
|
return null;
|
|
}
|
|
|
|
const errorBags = this.errorBags.filter((errorBag) => errorBag.form === form);
|
|
errorBags.forEach((errorBag) => {
|
|
this.showErrorBag(errorBag, invalidFields);
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Clears any validation errors in the given form.
|
|
*
|
|
* @param {Promise} promise
|
|
* @param {Request} request
|
|
* @returns {void}
|
|
*/
|
|
clearValidation(promise, request) {
|
|
if (request.element && request.element.dataset.requestValidate === undefined) {
|
|
return;
|
|
}
|
|
if (!request.form) {
|
|
return;
|
|
}
|
|
|
|
const errorBags = this.errorBags.filter((errorBag) => errorBag.form === request.form);
|
|
errorBags.forEach((errorBag) => {
|
|
this.hideErrorBag(errorBag);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Collects error bags (elements with "data-validate-error" attribute) and links them to a
|
|
* placeholder and form.
|
|
*
|
|
* The error bags will be initially hidden, and will only show when validation errors occur.
|
|
*
|
|
* @param {HTMLElement} rootNode
|
|
*/
|
|
collectErrorBags(rootNode) {
|
|
rootNode.querySelectorAll('[data-validate-error], [data-validate-for]').forEach((errorBag) => {
|
|
const form = errorBag.closest('form[data-request-validate]');
|
|
|
|
// If this error bag does not reside within a validating form, remove it
|
|
if (!form) {
|
|
errorBag.parentNode.removeChild(errorBag);
|
|
return;
|
|
}
|
|
|
|
// Find message list node, if available
|
|
let messageListElement = null;
|
|
if (errorBag.matches('[data-validate-error]')) {
|
|
messageListElement = errorBag.querySelector('[data-message]');
|
|
}
|
|
|
|
// Create a placeholder node
|
|
const placeholder = document.createComment('');
|
|
|
|
// Register error bag and replace with placeholder
|
|
const errorBagData = {
|
|
element: errorBag,
|
|
form,
|
|
validateFor: (errorBag.dataset.validateFor)
|
|
? errorBag.dataset.validateFor.split(/\s*,\s*/)
|
|
: '*',
|
|
placeholder,
|
|
messageListElement: (messageListElement)
|
|
? messageListElement.cloneNode(true)
|
|
: null,
|
|
messageListAnchor: null,
|
|
customMessage: (errorBag.dataset.validateFor)
|
|
? (errorBag.textContent !== '' || errorBag.childNodes.length > 0)
|
|
: false,
|
|
};
|
|
|
|
// If an message list element exists, create another placeholder to act as an anchor point
|
|
if (messageListElement) {
|
|
const messageListAnchor = document.createComment('');
|
|
messageListElement.parentNode.replaceChild(messageListAnchor, messageListElement);
|
|
errorBagData.messageListAnchor = messageListAnchor;
|
|
}
|
|
|
|
errorBag.parentNode.replaceChild(placeholder, errorBag);
|
|
|
|
this.errorBags.push(errorBagData);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hides an error bag, replacing the error messages with a placeholder node.
|
|
*
|
|
* @param {Object} errorBag
|
|
*/
|
|
hideErrorBag(errorBag) {
|
|
if (errorBag.element.isConnected) {
|
|
errorBag.element.parentNode.replaceChild(errorBag.placeholder, errorBag.element);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows an error bag with the given invalid fields.
|
|
*
|
|
* @param {Object} errorBag
|
|
* @param {Object} invalidFields
|
|
*/
|
|
showErrorBag(errorBag, invalidFields) {
|
|
if (!this.errorBagValidatesField(errorBag, invalidFields)) {
|
|
return;
|
|
}
|
|
|
|
if (!errorBag.element.isConnected) {
|
|
errorBag.placeholder.parentNode.replaceChild(errorBag.element, errorBag.placeholder);
|
|
}
|
|
|
|
if (errorBag.validateFor !== '*') {
|
|
if (!errorBag.customMessage) {
|
|
const firstField = Object.keys(invalidFields)
|
|
.filter((field) => errorBag.validateFor.includes(field))
|
|
.shift();
|
|
[errorBag.element.innerHTML] = invalidFields[firstField];
|
|
}
|
|
} else if (errorBag.messageListElement) {
|
|
// Remove previous error messages
|
|
errorBag.element.querySelectorAll('[data-validation-message]').forEach((message) => {
|
|
message.parentNode.removeChild(message);
|
|
});
|
|
|
|
Object.entries(invalidFields).forEach((entry) => {
|
|
const [, errors] = entry;
|
|
|
|
errors.forEach((error) => {
|
|
const messageElement = errorBag.messageListElement.cloneNode(true);
|
|
messageElement.dataset.validationMessage = '';
|
|
messageElement.innerHTML = error;
|
|
errorBag.messageListAnchor.after(messageElement);
|
|
});
|
|
});
|
|
} else {
|
|
[errorBag.element.innerHTML] = invalidFields[Object.keys(invalidFields).shift()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if a given error bag applies for the given invalid fields.
|
|
*
|
|
* @param {Object} errorBag
|
|
* @param {Object} invalidFields
|
|
* @returns {Boolean}
|
|
*/
|
|
errorBagValidatesField(errorBag, invalidFields) {
|
|
if (errorBag.validateFor === '*') {
|
|
return true;
|
|
}
|
|
|
|
return Object.keys(invalidFields)
|
|
.filter((field) => errorBag.validateFor.includes(field))
|
|
.length > 0;
|
|
}
|
|
}
|