diff --git a/Configuration/TypoScript/Setup/Page/include.typoscript b/Configuration/TypoScript/Setup/Page/include.typoscript new file mode 100644 index 0000000..ea77d55 --- /dev/null +++ b/Configuration/TypoScript/Setup/Page/include.typoscript @@ -0,0 +1,7 @@ +page = PAGE + +# tw_forms critical javascript and polyfills. Must be included inside of the document! +page.includeJSLibs.tw_forms_critical = EXT:tw_forms/Resources/Public/tw-forms.critical.min.js + +# tw_forms default javascript. Depends on tw-forms.critical.min.js +page.includeJSFooter.tw_forms_default = EXT:tw_forms/Resources/Public/tw-forms.default.min.js diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index dd956b8..5ee89cf 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -1,3 +1,4 @@ # Imports all files ending on .typoscript from the specified folder. It does however not import files from sub folders. @import 'EXT:tw_forms/Configuration/TypoScript/Setup/*.typoscript' +@import 'EXT:tw_forms/Configuration/TypoScript/Setup/Page/*.typoscript' @import 'EXT:tw_forms/Configuration/TypoScript/Setup/Plugin/*.typoscript' diff --git a/Resources/Public/Javascript/000.polyfills.critical.js b/Resources/Public/Javascript/000.polyfills.critical.js deleted file mode 100644 index e172ad4..0000000 --- a/Resources/Public/Javascript/000.polyfills.critical.js +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["s", "e"] }] */ -(function iefe(w, e, s, svg) { - // NodeList.forEach - if (window.NodeList && !NodeList.prototype.forEach) { - NodeList.prototype.forEach = function foreEach(callback, thisArg) { - for (let i = 0; i < this.length; ++i) { - callback.call(thisArg || window, this[i], i, this); - } - }; - } - - // Element.matches - if (!e.matches) { - e.matches = e.matchesSelector - || e.mozMatchesSelector - || e.msMatchesSelector - || e.oMatchesSelector - || e.webkitMatchesSelector - || ((str) => { - const matches = (this.document || this.ownerDocument).querySelectorAll(str); - let i = matches.length - 1; - while ((i >= 0) && (matches.item(i) !== this)) { - i -= 1; - } - return i > -1; - }); - } - - // Element.closest - if (!e.closest) { - e.closest = function closest(str) { - let el = this; - do { - if (el.matches(str)) return el; - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1); - return null; - }; - } - - // String.format - if (!s.format) { - s.format = function format(...args) { - return this.replace( - /{(\d+)}/g, - (match, number) => (typeof args[number] !== 'undefined' ? args[number] : match) - ); - }; - } - - // classList support for IE11 - if (!('classList' in svg)) { - Object.defineProperty(svg, 'classList', { - get() { - return { - contains: (className) => this.className.baseVal.split(' ').indexOf(className) !== -1, - add: (className) => this.setAttribute( - 'class', - `${this.getAttribute('class')} ${className}` - ), - remove: (className) => { - const removedClass = this.getAttribute('class') - .replace(new RegExp(`(\\s|^)${className}(\\s|$)`, 'g'), '$2'); - if (this.classList.contains(className)) { - this.setAttribute('class', removedClass); - } - } - }; - } - }); - } -}(window, Element.prototype, String.prototype, SVGElement.prototype)); diff --git a/Resources/Public/Javascript/100.observer.critical.js b/Resources/Public/Javascript/100.observer.critical.js deleted file mode 100644 index 602cea8..0000000 --- a/Resources/Public/Javascript/100.observer.critical.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * tw_forms namespace - * - * @type {Object} - */ -// eslint-disable-next-line no-unused-vars -const tw_forms = window.tw_forms || { has: {} }; -window.tw_forms = tw_forms; - -(function (w, d) { - if (((typeof exports !== 'undefined') && exports.Observer) || w.tw_forms.Observer) { - return; - } - - /** - * Observer constructor - * - * @constructor - */ - function Observer() { - this.observed = [['[data-mutate-recursive]', this.process.bind(this)]]; - - const checkNode = this.checkNode.bind(this); - const observer = new MutationObserver((mutations) => mutations - .forEach((mutation) => Array.prototype.slice.call(mutation.addedNodes) - .filter((node) => node.nodeType === 1).forEach(checkNode))); - - observer.observe(d.documentElement, { - characterData: true, attributes: false, childList: true, subtree: true - }); - } - - /** - * Register a new selector / callback pair - * - * @param {String} selectors Selectors - * @param {Function} callback Callback - */ - Observer.prototype.register = function (selectors, callback) { - this.observed.push([selectors, callback]); - }; - - /** - * Check whether a newly created node should be processed - * - * @param {Element} node Node - */ - Observer.prototype.checkNode = function (node) { - this.observed.filter((observer) => { - return node.matches(observer[0]) - }).forEach((observer) => { - observer[1](node); - }); - }; - - /** - * Run all callbacks on a particular node and its children - * - * @param {Element} node Node - */ - Observer.prototype.process = function (node) { - if (node.nodeType === 1) { - this.observed.forEach((observer) => node.querySelectorAll(observer[0]) - .forEach((subnode) => observer[1](subnode))); - } - }; - - // Export as CommonJS module - if (typeof exports !== 'undefined') { - exports.Observer = new Observer(); - - // Else create a global instance - } else { - // eslint-disable-next-line no-param-reassign - w.tw_forms.Observer = new Observer(); - } -}(typeof global !== 'undefined' ? global : window, document)); diff --git a/Resources/Public/Javascript/110.formfield.critical.js b/Resources/Public/tw-forms.critical.js similarity index 60% rename from Resources/Public/Javascript/110.formfield.critical.js rename to Resources/Public/tw-forms.critical.js index 5d7bf98..b0cc192 100644 --- a/Resources/Public/Javascript/110.formfield.critical.js +++ b/Resources/Public/tw-forms.critical.js @@ -1,217 +1,382 @@ -/* global tw_forms */ -/* eslint no-restricted-properties: [0, {"object": "Map", "property": "pow"}] */ -(function formFields(w, d) { - let scrollIntoView = d.documentElement.scrollIntoView ? 'scrollIntoView' : null; - if (d.documentElement.scrollIntoViewIfNeeded) { - scrollIntoView = 'scrollIntoViewIfNeeded'; - } - - /** - * Form field constructor - * - * @param {Element} element Form element - * @constructor - */ - function FormField(element) { - this.element = element; - this.element.enhancer = this; - this.wrapper = this.element.closest('.FormField'); - this.groupPrimaryEnhancer = null; - this.groupEnhancers = []; - - // Check if this is a field group and determine the primary enhancer - this.isGroup = this.element.classList.contains('FormMultiCheckbox'); - if (this.isGroup) { - for (let e = 0; e < this.element.form.elements.length; ++e) { - const elementId = this.element.form.elements[e].id; - const elementName = this.element.form.elements[e].name; - if (elementId && elementName - && (this.element.name === elementName) - && (this.element.id !== elementId) - ) { - this.groupPrimaryEnhancer = this.element.form.elements[e].enhancer; - break; - } - } - } - - // If this field is part of a group but not the primary field: Link to the primary enhancer - if (this.isGroup && this.groupPrimaryEnhancer) { - this.groupPrimaryEnhancer.groupEnhancers.push(this); - this.element.addEventListener('input', this.validate.bind(this.groupPrimaryEnhancer, true)); - return; - } - - this.lastConstraints = 0; - this.errorMessages = {}; - this.errorMessageBag = this.element.hasAttribute('aria-errormessage') - ? d.getElementById(this.element.getAttribute('aria-errormessage')) : null; - this.recursiveErrorMessages = null; - - // If there's an error message display - if (this.errorMessageBag) { - this.constraints.forEach((constraint) => { - const errorMessageKey = `errormsg${constraint.substr(0, 1) - .toUpperCase()}${constraint.substr(1) - .toLowerCase()}`; - if (this.element.dataset[errorMessageKey]) { - this.errorMessages[constraint] = this.element.dataset[errorMessageKey]; - } - }); - this.errorMessageBag.querySelectorAll('[data-constraint]') - .forEach((c) => { - const constraintIndex = this.constraints.indexOf(c.dataset.constraint); - if (constraintIndex >= 0) { - this.lastConstraints += Math.pow(2, constraintIndex); - } - }); - this.element.addEventListener('input', this.validate.bind(this, true)); - } - } - - /** - * Constraints - * - * @type {string[]} - */ - FormField.prototype.constraints = [ - 'badInput', - 'patternMismatch', - 'rangeOverflow', - 'rangeUndeflow', - 'stepMismatch', - 'tooLong', - 'tooShort', - 'typeMismatch', - 'valueMissing' - ]; - - /** - * Validate the form field - * - * @param {boolean} includeMissing Include missing value errors - * @return {array} errorMessages - */ - FormField.prototype.validate = function validate(includeMissing) { - // If there's already a validation process running: Return error messages - if (this.recursiveErrorMessages) { - return this.recursiveErrorMessages; - } - - const errorMessages = {}; - let constraints = 0; - const validity = (this.isGroup && !this.groupPrimaryEnhancer) ? this.validateGroup() : this.element.validity; - - if (includeMissing || !validity.valueMissing) { - for (const constraint in this.errorMessages) { - if ( - Object.prototype.hasOwnProperty.call(this.errorMessages, constraint) - && (constraint in validity) - && validity[constraint] - ) { - errorMessages[constraint] = this.errorMessages[constraint]; - constraints += Math.pow(2, this.constraints.indexOf(constraint)); - } - } - } - this.recursiveErrorMessages = errorMessages; - this.updateErrorMessageBag(constraints, errorMessages); - this.recursiveErrorMessages = null; - return errorMessages; - }; - - /** - * Validate all fields of a group - * - * @return {Object} Pseudo validity state - */ - FormField.prototype.validateGroup = function validateGroup() { - if (this.element.validity.valueMissing) { - for (let e = 0; e < this.groupEnhancers.length; ++e) { - if (this.groupEnhancers[e].element.checked) { - return this.overrideValidityState({ valueMissing: false }); - } - } - } - - return this.element.validity; - }; - - /** - * Override the validity state of this element - * - * @param {Object} override Override values - * @return {Object} Pseudo validity state - */ - FormField.prototype.overrideValidityState = function overrideValidityState(override) { - const clonedValidityState = { valid: true }; - this.constraints.forEach((c) => { - clonedValidityState[c] = (c in override) ? override[c] : this.element.validity[c]; - clonedValidityState.valid = clonedValidityState.valid && !clonedValidityState[c]; - }); - return clonedValidityState; - }; - - /** - * Update the error message bag with a set of error messages - * - * @param {array} constraints Current constraints (bitmask) - * @param {object} errorMessages Error messages - */ - FormField.prototype.updateErrorMessageBag = function updateErrorMessageBag(constraints, errorMessages) { - if (constraints !== this.lastConstraints) { - if (constraints) { - this.errorMessageBag.removeAttribute('hidden'); - this.element.setAttribute('aria-invalid', 'true'); - this.wrapper.classList.add('FormField--has-error'); - } else { - this.errorMessageBag.setAttribute('hidden', 'hidden'); - this.element.setAttribute('aria-invalid', 'false'); - this.wrapper.classList.remove('FormField--has-error'); - } - while (this.errorMessageBag.childNodes.length) { - this.errorMessageBag.removeChild(this.errorMessageBag.lastChild); - } - for (const c in errorMessages) { - if (Object.prototype.hasOwnProperty.call(errorMessages, c)) { - const errorMessage = d.createElement('span'); - errorMessage.setAttribute('data-constraint', c); - errorMessage.textContent = errorMessages[c]; - this.errorMessageBag.appendChild(errorMessage); - } - } - this.lastConstraints = constraints; - this.focusWrapper(); - - // If this form field is part of an enhanced form: Update error summary - if (this.element.form.enhancer) { - this.element.form.enhancer.update(); - } - } - }; - - /** - * Scroll the fields wrapper into the view - */ - FormField.prototype.focusWrapper = function focusWrapper() { - if (scrollIntoView) { - this.wrapper[scrollIntoView]({ - block: 'center', - inline: 'start', - behavior: 'smooth' - }); - } - }; - - /** - * Focus the form field and bring its wrapper into the view - */ - FormField.prototype.focus = function focus() { - this.element.focus(); - this.focusWrapper(); - }; - - // Observing for form fields - tw_forms.Observer.register('.FormField__input, .FormField__textarea', (field) => new FormField(field)); - -}(typeof global !== 'undefined' ? global : window, document)); +/** + * Polyfills + */ +/* eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["s", "e"] }] */ +(function iefe(w, e, s, svg) { + // NodeList.forEach + if (window.NodeList && !NodeList.prototype.forEach) { + NodeList.prototype.forEach = function foreEach(callback, thisArg) { + for (let i = 0; i < this.length; ++i) { + callback.call(thisArg || window, this[i], i, this); + } + }; + } + + // Element.matches + if (!e.matches) { + e.matches = e.matchesSelector + || e.mozMatchesSelector + || e.msMatchesSelector + || e.oMatchesSelector + || e.webkitMatchesSelector + || ((str) => { + const matches = (this.document || this.ownerDocument).querySelectorAll(str); + let i = matches.length - 1; + while ((i >= 0) && (matches.item(i) !== this)) { + i -= 1; + } + return i > -1; + }); + } + + // Element.closest + if (!e.closest) { + e.closest = function closest(str) { + let el = this; + do { + if (el.matches(str)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; + } + + // String.format + if (!s.format) { + s.format = function format(...args) { + return this.replace( + /{(\d+)}/g, + (match, number) => (typeof args[number] !== 'undefined' ? args[number] : match) + ); + }; + } + + // classList support for IE11 + if (!('classList' in svg)) { + Object.defineProperty(svg, 'classList', { + get() { + return { + contains: (className) => this.className.baseVal.split(' ').indexOf(className) !== -1, + add: (className) => this.setAttribute( + 'class', + `${this.getAttribute('class')} ${className}` + ), + remove: (className) => { + const removedClass = this.getAttribute('class') + .replace(new RegExp(`(\\s|^)${className}(\\s|$)`, 'g'), '$2'); + if (this.classList.contains(className)) { + this.setAttribute('class', removedClass); + } + } + }; + } + }); + } +}(window, Element.prototype, String.prototype, SVGElement.prototype)); + + + +/** + * Observer + */ +/** + * tw_forms namespace + * + * @type {Object} + */ +// eslint-disable-next-line no-unused-vars +const tw_forms = window.tw_forms || { has: {} }; +window.tw_forms = tw_forms; + +(function (w, d) { + if (((typeof exports !== 'undefined') && exports.Observer) || w.tw_forms.Observer) { + return; + } + + /** + * Observer constructor + * + * @constructor + */ + function Observer() { + this.observed = [['[data-mutate-recursive]', this.process.bind(this)]]; + + const checkNode = this.checkNode.bind(this); + const observer = new MutationObserver((mutations) => mutations + .forEach((mutation) => Array.prototype.slice.call(mutation.addedNodes) + .filter((node) => node.nodeType === 1).forEach(checkNode))); + + observer.observe(d.documentElement, { + characterData: true, attributes: false, childList: true, subtree: true + }); + } + + /** + * Register a new selector / callback pair + * + * @param {String} selectors Selectors + * @param {Function} callback Callback + */ + Observer.prototype.register = function (selectors, callback) { + this.observed.push([selectors, callback]); + }; + + /** + * Check whether a newly created node should be processed + * + * @param {Element} node Node + */ + Observer.prototype.checkNode = function (node) { + this.observed.filter((observer) => { + return node.matches(observer[0]) + }).forEach((observer) => { + observer[1](node); + }); + }; + + /** + * Run all callbacks on a particular node and its children + * + * @param {Element} node Node + */ + Observer.prototype.process = function (node) { + if (node.nodeType === 1) { + this.observed.forEach((observer) => node.querySelectorAll(observer[0]) + .forEach((subnode) => observer[1](subnode))); + } + }; + + // Export as CommonJS module + if (typeof exports !== 'undefined') { + exports.Observer = new Observer(); + + // Else create a global instance + } else { + // eslint-disable-next-line no-param-reassign + w.tw_forms.Observer = new Observer(); + } +}(typeof global !== 'undefined' ? global : window, document)); + + + +/** + * Formfield + */ + +/* global tw_forms */ +/* eslint no-restricted-properties: [0, {"object": "Map", "property": "pow"}] */ +(function formFields(w, d) { + let scrollIntoView = d.documentElement.scrollIntoView ? 'scrollIntoView' : null; + if (d.documentElement.scrollIntoViewIfNeeded) { + scrollIntoView = 'scrollIntoViewIfNeeded'; + } + + /** + * Form field constructor + * + * @param {Element} element Form element + * @constructor + */ + function FormField(element) { + this.element = element; + this.element.enhancer = this; + this.wrapper = this.element.closest('.FormField'); + this.groupPrimaryEnhancer = null; + this.groupEnhancers = []; + + // Check if this is a field group and determine the primary enhancer + this.isGroup = this.element.classList.contains('FormMultiCheckbox'); + if (this.isGroup) { + for (let e = 0; e < this.element.form.elements.length; ++e) { + const elementId = this.element.form.elements[e].id; + const elementName = this.element.form.elements[e].name; + if (elementId && elementName + && (this.element.name === elementName) + && (this.element.id !== elementId) + ) { + this.groupPrimaryEnhancer = this.element.form.elements[e].enhancer; + break; + } + } + } + + // If this field is part of a group but not the primary field: Link to the primary enhancer + if (this.isGroup && this.groupPrimaryEnhancer) { + this.groupPrimaryEnhancer.groupEnhancers.push(this); + this.element.addEventListener('input', this.validate.bind(this.groupPrimaryEnhancer, true)); + return; + } + + this.lastConstraints = 0; + this.errorMessages = {}; + this.errorMessageBag = this.element.hasAttribute('aria-errormessage') + ? d.getElementById(this.element.getAttribute('aria-errormessage')) : null; + this.recursiveErrorMessages = null; + + // If there's an error message display + if (this.errorMessageBag) { + this.constraints.forEach((constraint) => { + const errorMessageKey = `errormsg${constraint.substr(0, 1) + .toUpperCase()}${constraint.substr(1) + .toLowerCase()}`; + if (this.element.dataset[errorMessageKey]) { + this.errorMessages[constraint] = this.element.dataset[errorMessageKey]; + } + }); + this.errorMessageBag.querySelectorAll('[data-constraint]') + .forEach((c) => { + const constraintIndex = this.constraints.indexOf(c.dataset.constraint); + if (constraintIndex >= 0) { + this.lastConstraints += Math.pow(2, constraintIndex); + } + }); + this.element.addEventListener('input', this.validate.bind(this, true)); + } + } + + /** + * Constraints + * + * @type {string[]} + */ + FormField.prototype.constraints = [ + 'badInput', + 'patternMismatch', + 'rangeOverflow', + 'rangeUndeflow', + 'stepMismatch', + 'tooLong', + 'tooShort', + 'typeMismatch', + 'valueMissing' + ]; + + /** + * Validate the form field + * + * @param {boolean} includeMissing Include missing value errors + * @return {array} errorMessages + */ + FormField.prototype.validate = function validate(includeMissing) { + // If there's already a validation process running: Return error messages + if (this.recursiveErrorMessages) { + return this.recursiveErrorMessages; + } + + const errorMessages = {}; + let constraints = 0; + const validity = (this.isGroup && !this.groupPrimaryEnhancer) ? this.validateGroup() : this.element.validity; + + if (includeMissing || !validity.valueMissing) { + for (const constraint in this.errorMessages) { + if ( + Object.prototype.hasOwnProperty.call(this.errorMessages, constraint) + && (constraint in validity) + && validity[constraint] + ) { + errorMessages[constraint] = this.errorMessages[constraint]; + constraints += Math.pow(2, this.constraints.indexOf(constraint)); + } + } + } + this.recursiveErrorMessages = errorMessages; + this.updateErrorMessageBag(constraints, errorMessages); + this.recursiveErrorMessages = null; + return errorMessages; + }; + + /** + * Validate all fields of a group + * + * @return {Object} Pseudo validity state + */ + FormField.prototype.validateGroup = function validateGroup() { + if (this.element.validity.valueMissing) { + for (let e = 0; e < this.groupEnhancers.length; ++e) { + if (this.groupEnhancers[e].element.checked) { + return this.overrideValidityState({ valueMissing: false }); + } + } + } + + return this.element.validity; + }; + + /** + * Override the validity state of this element + * + * @param {Object} override Override values + * @return {Object} Pseudo validity state + */ + FormField.prototype.overrideValidityState = function overrideValidityState(override) { + const clonedValidityState = { valid: true }; + this.constraints.forEach((c) => { + clonedValidityState[c] = (c in override) ? override[c] : this.element.validity[c]; + clonedValidityState.valid = clonedValidityState.valid && !clonedValidityState[c]; + }); + return clonedValidityState; + }; + + /** + * Update the error message bag with a set of error messages + * + * @param {array} constraints Current constraints (bitmask) + * @param {object} errorMessages Error messages + */ + FormField.prototype.updateErrorMessageBag = function updateErrorMessageBag(constraints, errorMessages) { + if (constraints !== this.lastConstraints) { + if (constraints) { + this.errorMessageBag.removeAttribute('hidden'); + this.element.setAttribute('aria-invalid', 'true'); + this.wrapper.classList.add('FormField--has-error'); + } else { + this.errorMessageBag.setAttribute('hidden', 'hidden'); + this.element.setAttribute('aria-invalid', 'false'); + this.wrapper.classList.remove('FormField--has-error'); + } + while (this.errorMessageBag.childNodes.length) { + this.errorMessageBag.removeChild(this.errorMessageBag.lastChild); + } + for (const c in errorMessages) { + if (Object.prototype.hasOwnProperty.call(errorMessages, c)) { + const errorMessage = d.createElement('span'); + errorMessage.setAttribute('data-constraint', c); + errorMessage.textContent = errorMessages[c]; + this.errorMessageBag.appendChild(errorMessage); + } + } + this.lastConstraints = constraints; + this.focusWrapper(); + + // If this form field is part of an enhanced form: Update error summary + if (this.element.form.enhancer) { + this.element.form.enhancer.update(); + } + } + }; + + /** + * Scroll the fields wrapper into the view + */ + FormField.prototype.focusWrapper = function focusWrapper() { + if (scrollIntoView) { + this.wrapper[scrollIntoView]({ + block: 'center', + inline: 'start', + behavior: 'smooth' + }); + } + }; + + /** + * Focus the form field and bring its wrapper into the view + */ + FormField.prototype.focus = function focus() { + this.element.focus(); + this.focusWrapper(); + }; + + // Observing for form fields + tw_forms.Observer.register('.FormField__input, .FormField__textarea', (field) => new FormField(field)); + +}(typeof global !== 'undefined' ? global : window, document)); diff --git a/Resources/Public/tw-forms.critical.min.js b/Resources/Public/tw-forms.critical.min.js new file mode 100644 index 0000000..e9a1e12 --- /dev/null +++ b/Resources/Public/tw-forms.critical.min.js @@ -0,0 +1 @@ +!function(e,t,s,r){window.NodeList&&!NodeList.prototype.forEach&&(NodeList.prototype.forEach=function(e,t){for(let s=0;s{const t=(this.document||this.ownerDocument).querySelectorAll(e);let s=t.length-1;for(;s>=0&&t.item(s)!==this;)s-=1;return s>-1})),t.closest||(t.closest=function(e){let t=this;do{if(t.matches(e))return t;t=t.parentElement||t.parentNode}while(null!==t&&1===t.nodeType);return null}),s.format||(s.format=function(...e){return this.replace(/{(\d+)}/g,((t,s)=>void 0!==e[s]?e[s]:t))}),"classList"in r||Object.defineProperty(r,"classList",{get(){return{contains:e=>-1!==this.className.baseVal.split(" ").indexOf(e),add:e=>this.setAttribute("class",`${this.getAttribute("class")} ${e}`),remove:e=>{const t=this.getAttribute("class").replace(new RegExp(`(\\s|^)${e}(\\s|$)`,"g"),"$2");this.classList.contains(e)&&this.setAttribute("class",t)}}}})}(window,Element.prototype,String.prototype,SVGElement.prototype);const e=window.tw_forms||{has:{}};window.tw_forms=e,function(e,t){function s(){this.observed=[["[data-mutate-recursive]",this.process.bind(this)]];const e=this.checkNode.bind(this);new MutationObserver((t=>t.forEach((t=>Array.prototype.slice.call(t.addedNodes).filter((e=>1===e.nodeType)).forEach(e))))).observe(t.documentElement,{characterData:!0,attributes:!1,childList:!0,subtree:!0})}"undefined"!=typeof exports&&exports.Observer||e.tw_forms.Observer||(s.prototype.register=function(e,t){this.observed.push([e,t])},s.prototype.checkNode=function(e){this.observed.filter((t=>e.matches(t[0]))).forEach((t=>{t[1](e)}))},s.prototype.process=function(e){1===e.nodeType&&this.observed.forEach((t=>e.querySelectorAll(t[0]).forEach((e=>t[1](e)))))},"undefined"!=typeof exports?exports.Observer=new s:e.tw_forms.Observer=new s)}("undefined"!=typeof global?global:window,document),function(t,s){let r=s.documentElement.scrollIntoView?"scrollIntoView":null;function i(e){if(this.element=e,this.element.enhancer=this,this.wrapper=this.element.closest(".FormField"),this.groupPrimaryEnhancer=null,this.groupEnhancers=[],this.isGroup=this.element.classList.contains("FormMultiCheckbox"),this.isGroup)for(let e=0;e{const t=`errormsg${e.substr(0,1).toUpperCase()}${e.substr(1).toLowerCase()}`;this.element.dataset[t]&&(this.errorMessages[e]=this.element.dataset[t])})),this.errorMessageBag.querySelectorAll("[data-constraint]").forEach((e=>{const t=this.constraints.indexOf(e.dataset.constraint);t>=0&&(this.lastConstraints+=Math.pow(2,t))})),this.element.addEventListener("input",this.validate.bind(this,!0)))}s.documentElement.scrollIntoViewIfNeeded&&(r="scrollIntoViewIfNeeded"),i.prototype.constraints=["badInput","patternMismatch","rangeOverflow","rangeUndeflow","stepMismatch","tooLong","tooShort","typeMismatch","valueMissing"],i.prototype.validate=function(e){if(this.recursiveErrorMessages)return this.recursiveErrorMessages;const t={};let s=0;const r=this.isGroup&&!this.groupPrimaryEnhancer?this.validateGroup():this.element.validity;if(e||!r.valueMissing)for(const e in this.errorMessages)Object.prototype.hasOwnProperty.call(this.errorMessages,e)&&e in r&&r[e]&&(t[e]=this.errorMessages[e],s+=Math.pow(2,this.constraints.indexOf(e)));return this.recursiveErrorMessages=t,this.updateErrorMessageBag(s,t),this.recursiveErrorMessages=null,t},i.prototype.validateGroup=function(){if(this.element.validity.valueMissing)for(let e=0;e{t[s]=s in e?e[s]:this.element.validity[s],t.valid=t.valid&&!t[s]})),t},i.prototype.updateErrorMessageBag=function(e,t){if(e!==this.lastConstraints){for(e?(this.errorMessageBag.removeAttribute("hidden"),this.element.setAttribute("aria-invalid","true"),this.wrapper.classList.add("FormField--has-error")):(this.errorMessageBag.setAttribute("hidden","hidden"),this.element.setAttribute("aria-invalid","false"),this.wrapper.classList.remove("FormField--has-error"));this.errorMessageBag.childNodes.length;)this.errorMessageBag.removeChild(this.errorMessageBag.lastChild);for(const e in t)if(Object.prototype.hasOwnProperty.call(t,e)){const r=s.createElement("span");r.setAttribute("data-constraint",e),r.textContent=t[e],this.errorMessageBag.appendChild(r)}this.lastConstraints=e,this.focusWrapper(),this.element.form.enhancer&&this.element.form.enhancer.update()}},i.prototype.focusWrapper=function(){r&&this.wrapper[r]({block:"center",inline:"start",behavior:"smooth"})},i.prototype.focus=function(){this.element.focus(),this.focusWrapper()},e.Observer.register(".FormField__input, .FormField__textarea",(e=>new i(e)))}("undefined"!=typeof global?global:window,document); diff --git a/Resources/Public/Javascript/200.form.js b/Resources/Public/tw-forms.default.js similarity index 97% rename from Resources/Public/Javascript/200.form.js rename to Resources/Public/tw-forms.default.js index 2708bd2..c6a09e7 100644 --- a/Resources/Public/Javascript/200.form.js +++ b/Resources/Public/tw-forms.default.js @@ -1,151 +1,154 @@ -(function formValidation(w, d) { - const errorLinkHandler = function errorLinkHandler(e) { - e.preventDefault(); - this.fields[e.target.href.split('#') - .pop()].focus(); - return false; - }; - - /** - * Form validation constructor - * - * @param {Element} element Form element - * @constructor - */ - function FormValidation(element) { - this.element = element; - this.element.enhancer = this; - this.element.setAttribute('novalidate', 'novalidate'); - this.element.addEventListener('submit', this.validate.bind(this)); - this.errorNavigation = null; - this.fields = {}; - for (let e = 0; e < this.element.elements.length; ++e) { - const elementId = this.element.elements[e].id; - if (elementId && this.element.elements[e].enhancer) { - if (!this.element.elements[e].enhancer.isGroup - || !this.element.elements[e].enhancer.groupPrimaryEnhancer - ) { - this.fields[elementId] = this.element.elements[e].enhancer; - } - } - } - - this.initializeErrorSummary(this.element.querySelector('.Form__error-summary')); - } - - /** - * Initialize an error summary - * - * @param {Element} summary - */ - FormValidation.prototype.initializeErrorSummary = function initializeErrorSummary(summary) { - this.errorSummary = summary; - if (this.errorSummary) { - this.errorNavigation = this.errorSummary.closest('.Form__error-navigation'); - this.errorSummary.querySelectorAll('a.Form__error-link').forEach((l) => { - const href = l.href.split('#'); - if ((href.length === 2) && (href[1] in this.fields)) { - l.addEventListener('click', errorLinkHandler.bind(this)); - } - }); - } - }; - - /** - * Validate before submission - * - * @param {Event} e Submit event - */ - FormValidation.prototype.validate = function validate(e) { - const novalidate = e && e.submitter && e.submitter.hasAttribute('formnovalidate'); - - if (!novalidate) { - const errorMessages = {}; - for (const f in this.fields) { - if (Object.prototype.hasOwnProperty.call(this.fields, f)) { - const fieldErrors = this.fields[f].validate(true, null); - if (this.fields[f].lastConstraints) { - errorMessages[f] = fieldErrors; - } - } - } - if (!this.updateErrorSummary(errorMessages)) { - this.errorNavigation.removeAttribute('hidden'); - if (e) { - this.errorNavigation.focus(); - e.preventDefault(); - e.stopImmediatePropagation(); - } - return false; - } - } - - this.errorNavigation.setAttribute('hidden', 'hidden'); - return false; // ? - }; - - /** - * Update the error summary if it's currently visible - */ - FormValidation.prototype.update = function update() { - if (!this.errorNavigation.hidden) { - this.validate(null); - } - }; - - /** - * Update the error summary - * - * @param {object} errorMessages Error messages - * @return {boolean} No errors / form is valid - */ - FormValidation.prototype.updateErrorSummary = function updateErrorSummary(errorMessages) { - - let errorCount = 0; - - // Remove all present errors - while (this.errorSummary.childNodes.length) { - this.errorSummary.removeChild(this.errorSummary.firstChild); - } - - // Run through all invalid fields and create error descriptions & links - for (const f in errorMessages) { - if (Object.prototype.hasOwnProperty.call(errorMessages, f)) { - errorCount += 1; - const errorLink = d.createElement('a'); - errorLink.className = 'Form__error-link'; - errorLink.href = `#${f}`; - errorLink.textContent = Object.values(errorMessages[f]) - .join('; '); - errorLink.addEventListener('click', errorLinkHandler.bind(this)); - const errorListItem = d.createElement('li'); - errorListItem.className = 'Form__error-description'; - errorListItem.appendChild(errorLink); - this.errorSummary.appendChild(errorListItem); - } - } - - // Update the summary heading - const summaryHeading = this.errorNavigation.querySelector('.Heading'); - if (summaryHeading) { - const templateAttribute = `data-heading-${(errorCount === 1) ? 'single' : 'multiple'}`; - summaryHeading.textContent = summaryHeading.getAttribute(templateAttribute) - .split('%s') - .join(errorCount); - } - - // Update the page title - let title = this.errorNavigation.getAttribute('data-title'); - if (errorCount) { - title = this.errorNavigation.getAttribute('data-title-errors').format(errorCount, title); - } - document.title = title; - return !errorCount; - }; - - d.addEventListener('DOMContentLoaded', () => { - d.querySelectorAll('form.Form--custom-validation') - .forEach((f) => new FormValidation(f)); - }); -}(typeof global !== 'undefined' ? global : window, document)); - +/** + * Form + */ +(function formValidation(w, d) { + const errorLinkHandler = function errorLinkHandler(e) { + e.preventDefault(); + this.fields[e.target.href.split('#') + .pop()].focus(); + return false; + }; + + /** + * Form validation constructor + * + * @param {Element} element Form element + * @constructor + */ + function FormValidation(element) { + this.element = element; + this.element.enhancer = this; + this.element.setAttribute('novalidate', 'novalidate'); + this.element.addEventListener('submit', this.validate.bind(this)); + this.errorNavigation = null; + this.fields = {}; + for (let e = 0; e < this.element.elements.length; ++e) { + const elementId = this.element.elements[e].id; + if (elementId && this.element.elements[e].enhancer) { + if (!this.element.elements[e].enhancer.isGroup + || !this.element.elements[e].enhancer.groupPrimaryEnhancer + ) { + this.fields[elementId] = this.element.elements[e].enhancer; + } + } + } + + this.initializeErrorSummary(this.element.querySelector('.Form__error-summary')); + } + + /** + * Initialize an error summary + * + * @param {Element} summary + */ + FormValidation.prototype.initializeErrorSummary = function initializeErrorSummary(summary) { + this.errorSummary = summary; + if (this.errorSummary) { + this.errorNavigation = this.errorSummary.closest('.Form__error-navigation'); + this.errorSummary.querySelectorAll('a.Form__error-link').forEach((l) => { + const href = l.href.split('#'); + if ((href.length === 2) && (href[1] in this.fields)) { + l.addEventListener('click', errorLinkHandler.bind(this)); + } + }); + } + }; + + /** + * Validate before submission + * + * @param {Event} e Submit event + */ + FormValidation.prototype.validate = function validate(e) { + const novalidate = e && e.submitter && e.submitter.hasAttribute('formnovalidate'); + + if (!novalidate) { + const errorMessages = {}; + for (const f in this.fields) { + if (Object.prototype.hasOwnProperty.call(this.fields, f)) { + const fieldErrors = this.fields[f].validate(true, null); + if (this.fields[f].lastConstraints) { + errorMessages[f] = fieldErrors; + } + } + } + if (!this.updateErrorSummary(errorMessages)) { + this.errorNavigation.removeAttribute('hidden'); + if (e) { + this.errorNavigation.focus(); + e.preventDefault(); + e.stopImmediatePropagation(); + } + return false; + } + } + + this.errorNavigation.setAttribute('hidden', 'hidden'); + return false; // ? + }; + + /** + * Update the error summary if it's currently visible + */ + FormValidation.prototype.update = function update() { + if (!this.errorNavigation.hidden) { + this.validate(null); + } + }; + + /** + * Update the error summary + * + * @param {object} errorMessages Error messages + * @return {boolean} No errors / form is valid + */ + FormValidation.prototype.updateErrorSummary = function updateErrorSummary(errorMessages) { + + let errorCount = 0; + + // Remove all present errors + while (this.errorSummary.childNodes.length) { + this.errorSummary.removeChild(this.errorSummary.firstChild); + } + + // Run through all invalid fields and create error descriptions & links + for (const f in errorMessages) { + if (Object.prototype.hasOwnProperty.call(errorMessages, f)) { + errorCount += 1; + const errorLink = d.createElement('a'); + errorLink.className = 'Form__error-link'; + errorLink.href = `#${f}`; + errorLink.textContent = Object.values(errorMessages[f]) + .join('; '); + errorLink.addEventListener('click', errorLinkHandler.bind(this)); + const errorListItem = d.createElement('li'); + errorListItem.className = 'Form__error-description'; + errorListItem.appendChild(errorLink); + this.errorSummary.appendChild(errorListItem); + } + } + + // Update the summary heading + const summaryHeading = this.errorNavigation.querySelector('.Heading'); + if (summaryHeading) { + const templateAttribute = `data-heading-${(errorCount === 1) ? 'single' : 'multiple'}`; + summaryHeading.textContent = summaryHeading.getAttribute(templateAttribute) + .split('%s') + .join(errorCount); + } + + // Update the page title + let title = this.errorNavigation.getAttribute('data-title'); + if (errorCount) { + title = this.errorNavigation.getAttribute('data-title-errors').format(errorCount, title); + } + document.title = title; + return !errorCount; + }; + + d.addEventListener('DOMContentLoaded', () => { + d.querySelectorAll('form.Form--custom-validation') + .forEach((f) => new FormValidation(f)); + }); +}(typeof global !== 'undefined' ? global : window, document)); + diff --git a/Resources/Public/tw-forms.default.min.js b/Resources/Public/tw-forms.default.min.js new file mode 100644 index 0000000..773c31f --- /dev/null +++ b/Resources/Public/tw-forms.default.min.js @@ -0,0 +1 @@ +!function(e,t){const r=function(e){return e.preventDefault(),this.fields[e.target.href.split("#").pop()].focus(),!1};function i(e){this.element=e,this.element.enhancer=this,this.element.setAttribute("novalidate","novalidate"),this.element.addEventListener("submit",this.validate.bind(this)),this.errorNavigation=null,this.fields={};for(let e=0;e{const t=e.href.split("#");2===t.length&&t[1]in this.fields&&e.addEventListener("click",r.bind(this))})))},i.prototype.validate=function(e){if(!(e&&e.submitter&&e.submitter.hasAttribute("formnovalidate"))){const t={};for(const e in this.fields)if(Object.prototype.hasOwnProperty.call(this.fields,e)){const r=this.fields[e].validate(!0,null);this.fields[e].lastConstraints&&(t[e]=r)}if(!this.updateErrorSummary(t))return this.errorNavigation.removeAttribute("hidden"),e&&(this.errorNavigation.focus(),e.preventDefault(),e.stopImmediatePropagation()),!1}return this.errorNavigation.setAttribute("hidden","hidden"),!1},i.prototype.update=function(){this.errorNavigation.hidden||this.validate(null)},i.prototype.updateErrorSummary=function(e){let i=0;for(;this.errorSummary.childNodes.length;)this.errorSummary.removeChild(this.errorSummary.firstChild);for(const n in e)if(Object.prototype.hasOwnProperty.call(e,n)){i+=1;const o=t.createElement("a");o.className="Form__error-link",o.href=`#${n}`,o.textContent=Object.values(e[n]).join("; "),o.addEventListener("click",r.bind(this));const s=t.createElement("li");s.className="Form__error-description",s.appendChild(o),this.errorSummary.appendChild(s)}const n=this.errorNavigation.querySelector(".Heading");if(n){const e="data-heading-"+(1===i?"single":"multiple");n.textContent=n.getAttribute(e).split("%s").join(i)}let o=this.errorNavigation.getAttribute("data-title");return i&&(o=this.errorNavigation.getAttribute("data-title-errors").format(i,o)),document.title=o,!i},t.addEventListener("DOMContentLoaded",(()=>{t.querySelectorAll("form.Form--custom-validation").forEach((e=>new i(e)))}))}("undefined"!=typeof global?global:window,document);