diff --git a/.gitignore b/.gitignore index 62fb128..2526405 100755 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ # Node node_modules +package-lock.json test/results test/coverage +# Compiled +docs/dist/js/*.min.*.js +static/js/*.min.*.js + ## OS X .DS_Store ._* diff --git a/README.md b/README.md index 46f3a4d..6924287 100755 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ -# Validate.js [![Build Status](https://travis-ci.org/cferdinandi/validate.svg)](https://travis-ci.org/cferdinandi/validate) +# Validate.js A lightweight form validation script that augments native HTML5 form validation elements and attributes, providing a better user experience and giving you more control. -When a visitor leaves a field, Validate.js immediately validates the field and displays an error if applicable. It also validates the entire form on submit, and provides support for custom `onSubmit()` functions (for example, Ajax form submission). +When a visitor leaves a field, Validate.js immediately validates the field and displays an error if applicable. It also validates the entire form on submit, and provides support for custom `onSubmit()` functions (for example, Ajax form submission). You can pass an option to activate live validation on fields while the visitor is still typing. It allows for custom errors. -[Download Validate](https://github.com/cferdinandi/validate/archive/master.zip) / [View the demo](http://cferdinandi.github.io/validate/) +Regarding the original script by Chris Ferdinandi: [Download Validate](https://github.com/cferdinandi/validate/archive/master.zip) / [View the demo](http://cferdinandi.github.io/validate/) +[Download this fork of Validate](https://github.com/michapietsch/validate/archive/master.zip) + +This fork builds on v1.1.3 and adds the following features: +- optional live validation while the visitor is still typing (v1.2.0) — see below for how to pass options +- delay live validation for invalid values while still typing, but immediately show positive feedback for valid values ("reward early, punish late") (v1.4.0) +- integration for custom errors by providing a wrapper around the HTML5 Constraint API setCustomValidity() function (v.1.3.0) — see below for an example to use the setCustomError() method
-### Want to learn how to write your own vanilla JS plugins? Get my free [daily developer tips](https://gomakethings.com/newsletter/) and level-up as a web developer. 🚀 +### Want to learn how to write your own vanilla JS plugins? Get Chris Ferdinandi's free [daily developer tips](https://gomakethings.com/newsletter/) and level-up as a web developer. 🚀
@@ -152,6 +158,11 @@ validate.init({ messageRangeUnderflow: 'Please select a value that is no less than {min}.', // Displayed with the `mind` attribute is used and the input value is too low messageGeneric: 'The value you entered for this field is invalid.', // A catchall error, displayed when the field fails validation and none of the other conditions apply + // Live Validation + useLiveValidation: false, // Update errors instantly while the visitor is typing + rewardEarlyPunishLate: true, // Delay feedback for invalid values, update errors instantly for valid values only + punishLateTimeout: 1000, // Milliseconds to delay live validation feedback for invalid values + // Form Submission disableSubmit: false, // If true, don't submit the form to the server (for Ajax for submission) onSubmit: function (form, fields) {}, // Function to run if the form successfully validates @@ -240,6 +251,29 @@ var field = document.querySelector('[name="email"]'); validate.removeError(field); ``` +#### setCustomError() +Set or remove a custom error. This wraps around the HTML5 Constraint API setCustomValidity() function. + +```javascript +/** + * Set or remove a custom error + * @public + * @param {Node} field The field to set the custom error on + * @param {String} errorMessage Custom error message or empty string to remove a custom error + */ +validate.setCustomError( + field, // The field to set the custom error on + errorMessage // Custom error message or empty string to remove a custom error +); +``` + +**Example** + +```javascript +var field = document.querySelector('[name="email"]'); +validate.setCustomError(field, 'This email address is already registered') +``` + #### destroy() Destroy the current `validate.init()`. Removes all errors and resets the DOM. This is called automatically during the `init` function to remove any existing initializations. diff --git a/dist/js/validate.js b/dist/js/validate.js deleted file mode 100644 index b7bfd52..0000000 --- a/dist/js/validate.js +++ /dev/null @@ -1,512 +0,0 @@ -/*! - * validate v1.1.2: A lightweight form validation script that augments native HTML5 form validation elements and attributes. - * (c) 2018 Chris Ferdinandi - * MIT License - * http://github.com/cferdinandi/validate - */ - -(function (root, factory) { - if ( typeof define === 'function' && define.amd ) { - define([], factory(root)); - } else if ( typeof exports === 'object' ) { - module.exports = factory(root); - } else { - root.validate = factory(root); - } -})(typeof global !== 'undefined' ? global : this.window || this.global, (function (root) { - - 'use strict'; - - // - // Variables - // - - var validate = {}; // Object for public APIs - var supports = 'querySelector' in document && 'addEventListener' in root; // Feature test - var settings; - - // Default settings - var defaults = { - - // Classes and Selectors - selector: '[data-validate]', - fieldClass: 'error', - errorClass: 'error-message', - - // Messages - messageValueMissing: 'Please fill out this field.', - messageValueMissingSelect: 'Please select a value.', - messageValueMissingSelectMulti: 'Please select at least one value.', - messageTypeMismatchEmail: 'Please enter an email address.', - messageTypeMismatchURL: 'Please enter a URL.', - messageTooShort: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.', - messageTooLong: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.', - messagePatternMismatch: 'Please match the requested format.', - messageBadInput: 'Please enter a number.', - messageStepMismatch: 'Please select a valid value.', - messageRangeOverflow: 'Please select a value that is no more than {max}.', - messageRangeUnderflow: 'Please select a value that is no less than {min}.', - messageGeneric: 'The value you entered for this field is invalid.', - - // Form Submission - disableSubmit: false, - onSubmit: function () {}, - - // Callbacks - beforeShowError: function () {}, - afterShowError: function () {}, - beforeRemoveError: function () {}, - afterRemoveError: function () {} - - }; - - - // - // Methods - // - - // Element.matches() polyfill - if (!Element.prototype.matches) { - Element.prototype.matches = - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector || - function(s) { - var matches = (this.document || this.ownerDocument).querySelectorAll(s), - i = matches.length; - while (--i >= 0 && matches.item(i) !== this) {} - return i > -1; - }; - } - - /** - * Merge two or more objects. Returns a new object. - * @private - * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] - * @param {Object} objects The objects to merge together - * @returns {Object} Merged values of defaults and options - */ - var extend = function () { - - // Variables - var extended = {}; - var deep = false; - var i = 0; - var length = arguments.length; - - // Check if a deep merge - if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { - deep = arguments[0]; - i++; - } - - // Merge the object into the extended object - var merge = function (obj) { - for ( var prop in obj ) { - if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { - // If deep merge and property is an object, merge properties - if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { - extended[prop] = extend( true, extended[prop], obj[prop] ); - } else { - extended[prop] = obj[prop]; - } - } - } - }; - - // Loop through each object and conduct a merge - for ( ; i < length; i++ ) { - var obj = arguments[i]; - merge(obj); - } - - return extended; - - }; - - /** - * Get the closest matching element up the DOM tree. - * @private - * @param {Element} elem Starting element - * @param {String} selector Selector to match against - * @return {Boolean|Element} Returns null if not match found - */ - var getClosest = function ( elem, selector ) { - for ( ; elem && elem !== document; elem = elem.parentNode ) { - if ( elem.matches( selector ) ) return elem; - } - return null; - }; - - /** - * Validate a form field - * @public - * @param {Node} field The field to validate - * @param {Object} options User options - * @return {String} The error message - */ - validate.hasError = function (field, options) { - - // Merge user options with existing settings or defaults - var localSettings = extend(settings || defaults, options || {}); - - // Don't validate submits, buttons, file and reset inputs, and disabled fields - if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return; - - // Get validity - var validity = field.validity; - - // If valid, return null - if (validity.valid) return; - - // If field is required and empty - if (validity.valueMissing) { - - if (field.type === 'select-multiple') return localSettings.messageValueMissingSelectMulti; - - if (field.type === 'select-one') return localSettings.messageValueMissingSelect; - - return localSettings.messageValueMissing; - } - - // If not the right type - if (validity.typeMismatch) { - - // Email - if (field.type === 'email') return localSettings.messageTypeMismatchEmail; - - // URL - if (field.type === 'url') return localSettings.messageTypeMismatchURL; - - } - - // If too short - if (validity.tooShort) return localSettings.messageTooShort.replace('{minLength}', field.getAttribute('minLength')).replace('{length}', field.value.length); - - // If too long - if (validity.tooLong) return localSettings.messageTooLong.replace('{minLength}', field.getAttribute('maxLength')).replace('{length}', field.value.length); - - // If number input isn't a number - if (validity.badInput) return localSettings.messageBadInput; - - // If a number value doesn't match the step interval - if (validity.stepMismatch) return localSettings.messageStepMismatch; - - // If a number field is over the max - if (validity.rangeOverflow) return localSettings.messageRangeOverflow.replace('{max}', field.getAttribute('max')); - - // If a number field is below the min - if (validity.rangeUnderflow) return localSettings.messageRangeUnderflow.replace('{min}', field.getAttribute('min')); - - // If pattern doesn't match - if (validity.patternMismatch) { - - // If pattern info is included, return custom error - if (field.hasAttribute('title')) return field.getAttribute('title'); - - // Otherwise, generic error - return localSettings.messagePatternMismatch; - - } - - // If all else fails, return a generic catchall error - return localSettings.messageGeneric; - - }; - - /** - * Show an error message on a field - * @public - * @param {Node} field The field to show an error message for - * @param {String} error The error message to show - * @param {Object} options User options - */ - validate.showError = function (field, error, options) { - - // Merge user options with existing settings or defaults - var localSettings = extend(settings || defaults, options || {}); - - // Before show error callback - localSettings.beforeShowError(field, error); - - // Add error class to field - field.classList.add(localSettings.fieldClass); - - // If the field is a radio button and part of a group, error all and get the last item in the group - if (field.type === 'radio' && field.name) { - var group = document.getElementsByName(field.name); - if (group.length > 0) { - for (var i = 0; i < group.length; i++) { - if (group[i].form !== field.form) continue; // Only check fields in current form - group[i].classList.add(localSettings.fieldClass); - } - field = group[group.length - 1]; - } - } - - // Get field id or name - var id = field.id || field.name; - if (!id) return; - - // Check if error message field already exists - // If not, create one - var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id ); - if (!message) { - message = document.createElement('div'); - message.className = localSettings.errorClass; - message.id = 'error-for-' + id; - - // If the field is a radio button or checkbox, insert error after the label - var label; - if (field.type === 'radio' || field.type ==='checkbox') { - label = field.form.querySelector('label[for="' + id + '"]') || getClosest(field, 'label'); - if (label) { - label.parentNode.insertBefore( message, label.nextSibling ); - } - } - - // Otherwise, insert it after the field - if (!label) { - field.parentNode.insertBefore( message, field.nextSibling ); - } - } - - // Add ARIA role to the field - field.setAttribute('aria-describedby', 'error-for-' + id); - - // Update error message - message.innerHTML = error; - - // Remove any existing styles hiding the error message - message.style.display = ''; - message.style.visibility = ''; - - // After show error callback - localSettings.afterShowError(field, error); - - }; - - /** - * Remove an error message from a field - * @public - * @param {Node} field The field to remove the error from - * @param {Object} options User options - */ - validate.removeError = function (field, options) { - - // Merge user options with existing settings or defaults - var localSettings = extend(settings || defaults, options || {}); - - // Before remove error callback - localSettings.beforeRemoveError(field); - - // Remove ARIA role from the field - field.removeAttribute('aria-describedby'); - - // Remove error class to field - field.classList.remove(localSettings.fieldClass); - - // If the field is a radio button and part of a group, remove error from all and get the last item in the group - if (field.type === 'radio' && field.name) { - var group = document.getElementsByName(field.name); - if (group.length > 0) { - for (var i = 0; i < group.length; i++) { - if (group[i].form !== field.form) continue; // Only check fields in current form - group[i].classList.remove(localSettings.fieldClass); - } - field = group[group.length - 1]; - } - } - - // Get field id or name - var id = field.id || field.name; - if (!id) return; - - // Check if an error message is in the DOM - var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id + ''); - if (!message) return; - - // If so, hide it - message.innerHTML = ''; - message.style.display = 'none'; - message.style.visibility = 'hidden'; - - // After remove error callback - localSettings.afterRemoveError(field); - - }; - - /** - * Add the `novalidate` attribute to all forms - * @private - * @param {Boolean} remove If true, remove the `novalidate` attribute - */ - var addNoValidate = function (remove) { - var forms = document.querySelectorAll(settings.selector); - for (var i = 0; i < forms.length; i++) { - if (remove) { - forms[i].removeAttribute('novalidate'); - continue; - } - forms[i].setAttribute('novalidate', true); - } - }; - - /** - * Check field validity when it loses focus - * @private - * @param {Event} event The blur event - */ - var blurHandler = function (event) { - - // Only run if the field is in a form to be validated - if (!event.target.form || !event.target.form.matches(settings.selector)) return; - - // Validate the field - var error = validate.hasError(event.target); - - // If there's an error, show it - if (error) { - validate.showError(event.target, error); - return; - } - - // Otherwise, remove any errors that exist - validate.removeError(event.target); - - }; - - /** - * Check radio and checkbox field validity when clicked - * @private - * @param {Event} event The click event - */ - var clickHandler = function (event) { - - // Only run if the field is in a form to be validated - if (!event.target.form || !event.target.form.matches(settings.selector)) return; - - // Only run if the field is a checkbox or radio - var type = event.target.getAttribute('type'); - if (!(type === 'checkbox' || type === 'radio')) return; - - // Validate the field - var error = validate.hasError(event.target); - - // If there's an error, show it - if (error) { - validate.showError(event.target, error); - return; - } - - // Otherwise, remove any errors that exist - validate.removeError(event.target); - - }; - - /** - * Check all fields on submit - * @private - * @param {Event} event The submit event - */ - var submitHandler = function (event) { - - // Only run on forms flagged for validation - if (!event.target.matches(settings.selector)) return; - - // Get all of the form elements - var fields = event.target.elements; - - // Validate each field - // Store the first field with an error to a variable so we can bring it into focus later - var hasErrors; - for (var i = 0; i < fields.length; i++) { - var error = validate.hasError(fields[i]); - if (error) { - validate.showError(fields[i], error); - if (!hasErrors) { - hasErrors = fields[i]; - } - } - } - - // Prevent form from submitting if there are errors or submission is disabled - if (hasErrors || settings.disableSubmit) { - event.preventDefault(); - } - - // If there are errrors, focus on first element with error - if (hasErrors) { - hasErrors.focus(); - return; - } - - // Otherwise, submit the form - settings.onSubmit(event.target, fields); - - }; - - /** - * Destroy the current initialization. - * @public - */ - validate.destroy = function () { - - // If plugin isn't already initialized, stop - if ( !settings ) return; - - // Remove event listeners - document.removeEventListener('blur', blurHandler, true); - document.removeEventListener('click', clickHandler, false); - document.removeEventListener('submit', submitHandler, false); - - // Remove all errors - var fields = document.querySelectorAll(settings.errorClass); - for (var i = 0; i < fields.length; i++) { - validate.removeError(fields[i]); - } - - // Remove `novalidate` from forms - addNoValidate(true); - - // Reset variables - settings = null; - - }; - - /** - * Initialize Validate - * @public - * @param {Object} options User settings - */ - validate.init = function (options) { - - // feature test - if (!supports) return; - - // Destroy any existing initializations - validate.destroy(); - - // Merge user options with defaults - settings = extend(defaults, options || {}); - - // Add the `novalidate` attribute to all forms - addNoValidate(); - - // Event listeners - document.addEventListener('blur', blurHandler, true); - document.addEventListener('click', clickHandler, true); - document.addEventListener('submit', submitHandler, false); - - }; - - - // - // Public APIs - // - - return validate; - -})); \ No newline at end of file diff --git a/dist/js/validate.min.js b/dist/js/validate.min.js deleted file mode 100644 index cf9d81a..0000000 --- a/dist/js/validate.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! validate v1.1.2 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ -!(function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.validate=t(e)})("undefined"!=typeof global?global:this.window||this.global,(function(e){"use strict";var t,r={},a="querySelector"in document&&"addEventListener"in e,o={selector:"[data-validate]",fieldClass:"error",errorClass:"error-message",messageValueMissing:"Please fill out this field.",messageValueMissingSelect:"Please select a value.",messageValueMissingSelectMulti:"Please select at least one value.",messageTypeMismatchEmail:"Please enter an email address.",messageTypeMismatchURL:"Please enter a URL.",messageTooShort:"Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.",messageTooLong:"Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.",messagePatternMismatch:"Please match the requested format.",messageBadInput:"Please enter a number.",messageStepMismatch:"Please select a valid value.",messageRangeOverflow:"Please select a value that is no more than {max}.",messageRangeUnderflow:"Please select a value that is no less than {min}.",messageGeneric:"The value you entered for this field is invalid.",disableSubmit:!1,onSubmit:function(){},beforeShowError:function(){},afterShowError:function(){},beforeRemoveError:function(){},afterRemoveError:function(){}};Element.prototype.matches||(Element.prototype.matches=Element.prototype.matchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector||Element.prototype.oMatchesSelector||Element.prototype.webkitMatchesSelector||function(e){for(var t=(this.document||this.ownerDocument).querySelectorAll(e),r=t.length;--r>=0&&t.item(r)!==this;);return r>-1});var s=function(){var e={},t=!1,r=0,a=arguments.length;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(t=arguments[0],r++);for(;r0){for(var m=0;m0){for(var i=0;i 0) { - for (var i = 0; i < group.length; i++) { - if (group[i].form === field.form && field.checked) { - field = group[i]; - break; - } - } - } - } - - // Run validity checks - var checkValidity = { - badInput: (isNum && length > 0 && !/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(field.value)), // value of a number field is not a number - patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern - rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 0 && Number(field.value) > Number(field.getAttribute('max'))), // value of a number field is higher than the max attribute - rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 0 && Number(field.value) < Number(field.getAttribute('min'))), // value of a number field is lower than the min attribute - stepMismatch: (isNum && ((field.hasAttribute('step') && field.getAttribute('step') !== 'any' && Number(field.value) % Number(field.getAttribute('step')) !== 0) || (!field.hasAttribute('step') && Number(field.value) % 1 !== 0))), // value of a number field does not conform to the stepattribute - tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength - tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength - typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL - valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && (field.selectedIndex === -1 || field.options[field.selectedIndex].value.length < 1)) || (type !== 'checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value - }; - - // Run browser's own validation if available - var fieldTagName = field.tagName.toLowerCase(); - var browserValidity = fieldTagName in browserValidityFunctions ? browserValidityFunctions[fieldTagName].call(field) : {}; - - // Check if any errors - for (var key in checkValidity) { - if (checkValidity.hasOwnProperty(key)) { - // If browser has detected an error, adopt it to our validity object - if (key in browserValidity && browserValidity[key]) { - checkValidity[key] = true; - } - - // If there's an error, change valid value - if (checkValidity[key]) { - valid = false; - } - } - } - - // Add valid property to validity object - checkValidity.valid = valid; - - // Return object - return checkValidity; - - }; - - // If the full set of ValidityState features aren't supported, polyfill - if (!supported()) { - Object.defineProperty(HTMLInputElement.prototype, 'validity', { - get: function ValidityState() { - return getValidityState(this); - }, - configurable: true, - }); - Object.defineProperty(HTMLButtonElement.prototype, 'validity', { - get: function ValidityState() { - return getValidityState(this); - }, - configurable: true, - }); - Object.defineProperty(HTMLSelectElement.prototype, 'validity', { - get: function ValidityState() { - return getValidityState(this); - }, - configurable: true, - }); - Object.defineProperty(HTMLTextAreaElement.prototype, 'validity', { - get: function ValidityState() { - return getValidityState(this); - }, - configurable: true, - }); - } - -})(window, document); \ No newline at end of file diff --git a/dist/js/validityState-polyfill.min.js b/dist/js/validityState-polyfill.min.js deleted file mode 100644 index 06d2667..0000000 --- a/dist/js/validityState-polyfill.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! validate v1.1.2 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ -!(function(t,e,i){"use strict";var r=(function(){var t=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"validity"),e=Object.getOwnPropertyDescriptor(HTMLButtonElement.prototype,"validity"),i=Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,"validity"),r=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,"validity"),x={};return t&&(x.input=t.get),e&&(x.button=e.get),i&&(x.select=i.get),r&&(x.textarea=r.get),x})(),x=function(t){var i=t.getAttribute("type")||t.nodeName.toLowerCase(),x="number"===i||"range"===i,a=t.value.length,n=!0;if("radio"===t.type&&t.name){var u=e.getElementsByName(t.name);if(u.length>0)for(var o=0;o0&&!/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(t.value),patternMismatch:t.hasAttribute("pattern")&&a>0&&!1===new RegExp(t.getAttribute("pattern")).test(t.value),rangeOverflow:t.hasAttribute("max")&&x&&t.value>0&&Number(t.value)>Number(t.getAttribute("max")),rangeUnderflow:t.hasAttribute("min")&&x&&t.value>0&&Number(t.value)0&&a>parseInt(t.getAttribute("maxLength"),10),tooShort:t.hasAttribute("minLength")&&t.getAttribute("minLength")>0&&a>0&&a0&&("email"===i&&!/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(t.value)||"url"===i&&!/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(t.value)),valueMissing:t.hasAttribute("required")&&(("checkbox"===i||"radio"===i)&&!t.checked||"select"===i&&(-1===t.selectedIndex||t.options[t.selectedIndex].value.length<1)||"checkbox"!==i&&"radio"!==i&&"select"!==i&&a<1)},f=t.tagName.toLowerCase(),l=f in r?r[f].call(t):{};for(var c in d)d.hasOwnProperty(c)&&(c in l&&l[c]&&(d[c]=!0),d[c]&&(n=!1));return d.valid=n,d};(function(){var t=e.createElement("input");return"validity"in t&&"badInput"in t.validity&&"patternMismatch"in t.validity&&"rangeOverflow"in t.validity&&"rangeUnderflow"in t.validity&&"stepMismatch"in t.validity&&"tooLong"in t.validity&&"tooShort"in t.validity&&"typeMismatch"in t.validity&&"valid"in t.validity&&"valueMissing"in t.validity})()||(Object.defineProperty(HTMLInputElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLButtonElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLSelectElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLTextAreaElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}))})(window,document); \ No newline at end of file diff --git a/docs/dist/js/validate.js b/docs/dist/js/validate.js index b7bfd52..12e06c4 100644 --- a/docs/dist/js/validate.js +++ b/docs/dist/js/validate.js @@ -1,5 +1,5 @@ /*! - * validate v1.1.2: A lightweight form validation script that augments native HTML5 form validation elements and attributes. + * validate v1.4.1: A lightweight form validation script that augments native HTML5 form validation elements and attributes. * (c) 2018 Chris Ferdinandi * MIT License * http://github.com/cferdinandi/validate @@ -24,6 +24,7 @@ var validate = {}; // Object for public APIs var supports = 'querySelector' in document && 'addEventListener' in root; // Feature test var settings; + var timeout; // Default settings var defaults = { @@ -48,6 +49,11 @@ messageRangeUnderflow: 'Please select a value that is no less than {min}.', messageGeneric: 'The value you entered for this field is invalid.', + // Live Validation + useLiveValidation: false, + rewardEarlyPunishLate: true, + punishLateTimeout: 1000, + // Form Submission disableSubmit: false, onSubmit: function () {}, @@ -211,6 +217,11 @@ } + // If a custom error is set + if (validity.customError) { + return (localSettings.messageCustom && localSettings.messageCustom[field.validationMessage]) || field.validationMessage; + } + // If all else fails, return a generic catchall error return localSettings.messageGeneric; @@ -338,6 +349,17 @@ }; + /** + * Set or remove a custom error + * @public + * @param {Node} field The field to set the custom error on + * @param {String} errorMessage Custom error message or empty string + */ + validate.setCustomError = function (field, errorMessage) { + field.setCustomValidity(errorMessage); + field.dispatchEvent(new Event('customValiditySet')); + }; + /** * Add the `novalidate` attribute to all forms * @private @@ -406,6 +428,95 @@ }; + /** + * Check text field validity when typing + * @private + * @param {Event} event The keyup event + */ + var keyupHandler = function (event) { + + clearTimeout(timeout); + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Only run if the field is some kind of text field + var type = event.target.getAttribute('type'); + var textLikeInputs = ['text', 'email', 'url', 'tel', 'number', 'password', 'search', 'date', 'time', 'datetime', 'month', 'week']; + if (!(textLikeInputs.indexOf(type) > -1 || event.target.nodeName === 'TEXTAREA')) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + if (settings.rewardEarlyPunishLate === true) { + timeout = setTimeout((function () { + validate.showError(event.target, error); + }), settings.punishLateTimeout); + } else { + validate.showError(event.target, error); + } + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + + /** + * Check select field validity on change + * @private + * @param {Event} event The change event + */ + var changeHandler = function (event) { + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Only run if the field is a select + var type = event.target.getAttribute('type'); + if (!(event.target.nodeName === 'SELECT')) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + validate.showError(event.target, error); + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + + /** + * Check field validity when a custom error is set or removed + * @private + * @param {Event} event The customValiditySet event + */ + var customHandler = function (event) { + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + validate.showError(event.target, error); + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + /** * Check all fields on submit * @private @@ -429,6 +540,8 @@ if (!hasErrors) { hasErrors = fields[i]; } + } else { + validate.removeError(fields[i]); } } @@ -462,6 +575,11 @@ document.removeEventListener('click', clickHandler, false); document.removeEventListener('submit', submitHandler, false); + if (settings.useLiveValidation) { + document.removeEventListener('change', changeHandler, true); + document.removeEventListener('keyup', keyupHandler, true); + } + // Remove all errors var fields = document.querySelectorAll(settings.errorClass); for (var i = 0; i < fields.length; i++) { @@ -499,6 +617,12 @@ document.addEventListener('blur', blurHandler, true); document.addEventListener('click', clickHandler, true); document.addEventListener('submit', submitHandler, false); + document.addEventListener('customValiditySet', customHandler, true); + + if (settings.useLiveValidation) { + document.addEventListener('change', changeHandler, true); + document.addEventListener('keyup', keyupHandler, true); + } }; diff --git a/docs/dist/js/validate.min.js b/docs/dist/js/validate.min.js deleted file mode 100644 index cf9d81a..0000000 --- a/docs/dist/js/validate.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! validate v1.1.2 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ -!(function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.validate=t(e)})("undefined"!=typeof global?global:this.window||this.global,(function(e){"use strict";var t,r={},a="querySelector"in document&&"addEventListener"in e,o={selector:"[data-validate]",fieldClass:"error",errorClass:"error-message",messageValueMissing:"Please fill out this field.",messageValueMissingSelect:"Please select a value.",messageValueMissingSelectMulti:"Please select at least one value.",messageTypeMismatchEmail:"Please enter an email address.",messageTypeMismatchURL:"Please enter a URL.",messageTooShort:"Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.",messageTooLong:"Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.",messagePatternMismatch:"Please match the requested format.",messageBadInput:"Please enter a number.",messageStepMismatch:"Please select a valid value.",messageRangeOverflow:"Please select a value that is no more than {max}.",messageRangeUnderflow:"Please select a value that is no less than {min}.",messageGeneric:"The value you entered for this field is invalid.",disableSubmit:!1,onSubmit:function(){},beforeShowError:function(){},afterShowError:function(){},beforeRemoveError:function(){},afterRemoveError:function(){}};Element.prototype.matches||(Element.prototype.matches=Element.prototype.matchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector||Element.prototype.oMatchesSelector||Element.prototype.webkitMatchesSelector||function(e){for(var t=(this.document||this.ownerDocument).querySelectorAll(e),r=t.length;--r>=0&&t.item(r)!==this;);return r>-1});var s=function(){var e={},t=!1,r=0,a=arguments.length;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(t=arguments[0],r++);for(;r0){for(var m=0;m0){for(var i=0;i 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength typeMismatch: (length > 0 && ((type === 'email' && !/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL - valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && (field.selectedIndex === -1 || field.options[field.selectedIndex].value.length < 1)) || (type !== 'checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value + valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.hasAttribute('checked')) || (type === 'select' && (field.selectedIndex === -1 || field.options[field.selectedIndex].value.length < 1)) || (type !== 'checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value }; // Run browser's own validation if available @@ -100,6 +100,11 @@ // Add valid property to validity object checkValidity.valid = valid; + // Assume a custom error if there is a validation message present but all the other checks returned valid + if (checkValidity.valid && field.validationMessage !== '') { + checkValidity.customError = true + } + // Return object return checkValidity; diff --git a/docs/dist/js/validityState-polyfill.min.js b/docs/dist/js/validityState-polyfill.min.js deleted file mode 100644 index 06d2667..0000000 --- a/docs/dist/js/validityState-polyfill.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! validate v1.1.2 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ -!(function(t,e,i){"use strict";var r=(function(){var t=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"validity"),e=Object.getOwnPropertyDescriptor(HTMLButtonElement.prototype,"validity"),i=Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,"validity"),r=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,"validity"),x={};return t&&(x.input=t.get),e&&(x.button=e.get),i&&(x.select=i.get),r&&(x.textarea=r.get),x})(),x=function(t){var i=t.getAttribute("type")||t.nodeName.toLowerCase(),x="number"===i||"range"===i,a=t.value.length,n=!0;if("radio"===t.type&&t.name){var u=e.getElementsByName(t.name);if(u.length>0)for(var o=0;o0&&!/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(t.value),patternMismatch:t.hasAttribute("pattern")&&a>0&&!1===new RegExp(t.getAttribute("pattern")).test(t.value),rangeOverflow:t.hasAttribute("max")&&x&&t.value>0&&Number(t.value)>Number(t.getAttribute("max")),rangeUnderflow:t.hasAttribute("min")&&x&&t.value>0&&Number(t.value)0&&a>parseInt(t.getAttribute("maxLength"),10),tooShort:t.hasAttribute("minLength")&&t.getAttribute("minLength")>0&&a>0&&a0&&("email"===i&&!/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(t.value)||"url"===i&&!/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(t.value)),valueMissing:t.hasAttribute("required")&&(("checkbox"===i||"radio"===i)&&!t.checked||"select"===i&&(-1===t.selectedIndex||t.options[t.selectedIndex].value.length<1)||"checkbox"!==i&&"radio"!==i&&"select"!==i&&a<1)},f=t.tagName.toLowerCase(),l=f in r?r[f].call(t):{};for(var c in d)d.hasOwnProperty(c)&&(c in l&&l[c]&&(d[c]=!0),d[c]&&(n=!1));return d.valid=n,d};(function(){var t=e.createElement("input");return"validity"in t&&"badInput"in t.validity&&"patternMismatch"in t.validity&&"rangeOverflow"in t.validity&&"rangeUnderflow"in t.validity&&"stepMismatch"in t.validity&&"tooLong"in t.validity&&"tooShort"in t.validity&&"typeMismatch"in t.validity&&"valid"in t.validity&&"valueMissing"in t.validity})()||(Object.defineProperty(HTMLInputElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLButtonElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLSelectElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLTextAreaElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}))})(window,document); \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index cf87419..3a07a16 100755 --- a/docs/index.html +++ b/docs/index.html @@ -286,6 +286,7 @@

Validate.js

validate.init({ disableSubmit: true, + useLiveValidation: true, onSubmit: function () { alert('Form submitted successfully!'); }, diff --git a/gulpfile.js b/gulpfile.js index 6868bc5..35229b9 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -4,11 +4,11 @@ var settings = { scripts: true, // Turn on/off script tasks - styles: false, // Turn on/off style tasks - svgs: false, // Turn on/off SVG tasks - images: false, // Turn on/off image tasks - docs: false, // Turn on/off documentation generation - cacheBust: false // Turn on/off cache busting (adds a version number to minified files) + styles: true, // Turn on/off style tasks + svgs: true, // Turn on/off SVG tasks + images: true, // Turn on/off image tasks + docs: true, // Turn on/off documentation generation + cacheBust: true // Turn on/off cache busting (adds a version number to minified files) }; diff --git a/package.json b/package.json index 9a78fc8..0a56586 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "validate", - "version": "1.1.3", + "version": "1.4.1", "description": "A lightweight form validation script that augments native HTML5 form validation elements and attributes.", "main": "./dist/js/validate.js", "author": { diff --git a/src/docs/_templates/_footer.html b/src/docs/_templates/_footer.html index ce2caf5..388b00e 100755 --- a/src/docs/_templates/_footer.html +++ b/src/docs/_templates/_footer.html @@ -59,6 +59,7 @@ validate.init({ disableSubmit: true, + useLiveValidation: true, onSubmit: function () { alert('Form submitted successfully!'); }, diff --git a/src/js/validate.js b/src/js/validate.js index 461622d..1504882 100644 --- a/src/js/validate.js +++ b/src/js/validate.js @@ -17,6 +17,7 @@ var validate = {}; // Object for public APIs var supports = 'querySelector' in document && 'addEventListener' in root; // Feature test var settings; + var timeout; // Default settings var defaults = { @@ -41,6 +42,11 @@ messageRangeUnderflow: 'Please select a value that is no less than {min}.', messageGeneric: 'The value you entered for this field is invalid.', + // Live Validation + useLiveValidation: false, + rewardEarlyPunishLate: true, + punishLateTimeout: 1000, + // Form Submission disableSubmit: false, onSubmit: function () {}, @@ -204,6 +210,11 @@ } + // If a custom error is set + if (validity.customError) { + return (localSettings.messageCustom && localSettings.messageCustom[field.validationMessage]) || field.validationMessage; + } + // If all else fails, return a generic catchall error return localSettings.messageGeneric; @@ -331,6 +342,17 @@ }; + /** + * Set or remove a custom error + * @public + * @param {Node} field The field to set the custom error on + * @param {String} errorMessage Custom error message or empty string + */ + validate.setCustomError = function (field, errorMessage) { + field.setCustomValidity(errorMessage); + field.dispatchEvent(new Event('customValiditySet')); + }; + /** * Add the `novalidate` attribute to all forms * @private @@ -399,6 +421,95 @@ }; + /** + * Check text field validity when typing + * @private + * @param {Event} event The keyup event + */ + var keyupHandler = function (event) { + + clearTimeout(timeout); + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Only run if the field is some kind of text field + var type = event.target.getAttribute('type'); + var textLikeInputs = ['text', 'email', 'url', 'tel', 'number', 'password', 'search', 'date', 'time', 'datetime', 'month', 'week']; + if (!(textLikeInputs.indexOf(type) > -1 || event.target.nodeName === 'TEXTAREA')) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + if (settings.rewardEarlyPunishLate === true) { + timeout = setTimeout(function () { + validate.showError(event.target, error); + }, settings.punishLateTimeout); + } else { + validate.showError(event.target, error); + } + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + + /** + * Check select field validity on change + * @private + * @param {Event} event The change event + */ + var changeHandler = function (event) { + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Only run if the field is a select + var type = event.target.getAttribute('type'); + if (!(event.target.nodeName === 'SELECT')) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + validate.showError(event.target, error); + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + + /** + * Check field validity when a custom error is set or removed + * @private + * @param {Event} event The customValiditySet event + */ + var customHandler = function (event) { + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + validate.showError(event.target, error); + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + /** * Check all fields on submit * @private @@ -422,6 +533,8 @@ if (!hasErrors) { hasErrors = fields[i]; } + } else { + validate.removeError(fields[i]); } } @@ -455,6 +568,11 @@ document.removeEventListener('click', clickHandler, false); document.removeEventListener('submit', submitHandler, false); + if (settings.useLiveValidation) { + document.removeEventListener('change', changeHandler, true); + document.removeEventListener('keyup', keyupHandler, true); + } + // Remove all errors var fields = document.querySelectorAll(settings.errorClass); for (var i = 0; i < fields.length; i++) { @@ -492,6 +610,12 @@ document.addEventListener('blur', blurHandler, true); document.addEventListener('click', clickHandler, true); document.addEventListener('submit', submitHandler, false); + document.addEventListener('customValiditySet', customHandler, true); + + if (settings.useLiveValidation) { + document.addEventListener('change', changeHandler, true); + document.addEventListener('keyup', keyupHandler, true); + } }; diff --git a/src/js/validityState-polyfill.js b/src/js/validityState-polyfill.js index 7f4de64..d4d6684 100644 --- a/src/js/validityState-polyfill.js +++ b/src/js/validityState-polyfill.js @@ -93,6 +93,11 @@ // Add valid property to validity object checkValidity.valid = valid; + // Assume a custom error if there is a validation message present but all the other checks returned valid + if (checkValidity.valid && field.validationMessage !== '') { + checkValidity.customError = true + } + // Return object return checkValidity; diff --git a/static/js/validate.js b/static/js/validate.js index 1f3f34f..12e06c4 100644 --- a/static/js/validate.js +++ b/static/js/validate.js @@ -1,5 +1,5 @@ /*! - * validate v1.1.3: A lightweight form validation script that augments native HTML5 form validation elements and attributes. + * validate v1.4.1: A lightweight form validation script that augments native HTML5 form validation elements and attributes. * (c) 2018 Chris Ferdinandi * MIT License * http://github.com/cferdinandi/validate @@ -24,6 +24,7 @@ var validate = {}; // Object for public APIs var supports = 'querySelector' in document && 'addEventListener' in root; // Feature test var settings; + var timeout; // Default settings var defaults = { @@ -48,6 +49,11 @@ messageRangeUnderflow: 'Please select a value that is no less than {min}.', messageGeneric: 'The value you entered for this field is invalid.', + // Live Validation + useLiveValidation: false, + rewardEarlyPunishLate: true, + punishLateTimeout: 1000, + // Form Submission disableSubmit: false, onSubmit: function () {}, @@ -211,6 +217,11 @@ } + // If a custom error is set + if (validity.customError) { + return (localSettings.messageCustom && localSettings.messageCustom[field.validationMessage]) || field.validationMessage; + } + // If all else fails, return a generic catchall error return localSettings.messageGeneric; @@ -338,6 +349,17 @@ }; + /** + * Set or remove a custom error + * @public + * @param {Node} field The field to set the custom error on + * @param {String} errorMessage Custom error message or empty string + */ + validate.setCustomError = function (field, errorMessage) { + field.setCustomValidity(errorMessage); + field.dispatchEvent(new Event('customValiditySet')); + }; + /** * Add the `novalidate` attribute to all forms * @private @@ -406,6 +428,95 @@ }; + /** + * Check text field validity when typing + * @private + * @param {Event} event The keyup event + */ + var keyupHandler = function (event) { + + clearTimeout(timeout); + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Only run if the field is some kind of text field + var type = event.target.getAttribute('type'); + var textLikeInputs = ['text', 'email', 'url', 'tel', 'number', 'password', 'search', 'date', 'time', 'datetime', 'month', 'week']; + if (!(textLikeInputs.indexOf(type) > -1 || event.target.nodeName === 'TEXTAREA')) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + if (settings.rewardEarlyPunishLate === true) { + timeout = setTimeout((function () { + validate.showError(event.target, error); + }), settings.punishLateTimeout); + } else { + validate.showError(event.target, error); + } + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + + /** + * Check select field validity on change + * @private + * @param {Event} event The change event + */ + var changeHandler = function (event) { + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Only run if the field is a select + var type = event.target.getAttribute('type'); + if (!(event.target.nodeName === 'SELECT')) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + validate.showError(event.target, error); + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + + /** + * Check field validity when a custom error is set or removed + * @private + * @param {Event} event The customValiditySet event + */ + var customHandler = function (event) { + + // Only run if the field is in a form to be validated + if (!event.target.form || !event.target.form.matches(settings.selector)) return; + + // Validate the field + var error = validate.hasError(event.target); + + // If there's an error, show it + if (error) { + validate.showError(event.target, error); + return; + } + + // Otherwise, remove any errors that exist + validate.removeError(event.target); + + }; + /** * Check all fields on submit * @private @@ -429,6 +540,8 @@ if (!hasErrors) { hasErrors = fields[i]; } + } else { + validate.removeError(fields[i]); } } @@ -462,6 +575,11 @@ document.removeEventListener('click', clickHandler, false); document.removeEventListener('submit', submitHandler, false); + if (settings.useLiveValidation) { + document.removeEventListener('change', changeHandler, true); + document.removeEventListener('keyup', keyupHandler, true); + } + // Remove all errors var fields = document.querySelectorAll(settings.errorClass); for (var i = 0; i < fields.length; i++) { @@ -499,6 +617,12 @@ document.addEventListener('blur', blurHandler, true); document.addEventListener('click', clickHandler, true); document.addEventListener('submit', submitHandler, false); + document.addEventListener('customValiditySet', customHandler, true); + + if (settings.useLiveValidation) { + document.addEventListener('change', changeHandler, true); + document.addEventListener('keyup', keyupHandler, true); + } }; diff --git a/static/js/validate.min.js b/static/js/validate.min.js deleted file mode 100644 index a9ebb13..0000000 --- a/static/js/validate.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! validate v1.1.3 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ -!(function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.validate=t(e)})("undefined"!=typeof global?global:this.window||this.global,(function(e){"use strict";var t,r={},a="querySelector"in document&&"addEventListener"in e,o={selector:"[data-validate]",fieldClass:"error",errorClass:"error-message",messageValueMissing:"Please fill out this field.",messageValueMissingSelect:"Please select a value.",messageValueMissingSelectMulti:"Please select at least one value.",messageTypeMismatchEmail:"Please enter an email address.",messageTypeMismatchURL:"Please enter a URL.",messageTooShort:"Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.",messageTooLong:"Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.",messagePatternMismatch:"Please match the requested format.",messageBadInput:"Please enter a number.",messageStepMismatch:"Please select a valid value.",messageRangeOverflow:"Please select a value that is no more than {max}.",messageRangeUnderflow:"Please select a value that is no less than {min}.",messageGeneric:"The value you entered for this field is invalid.",disableSubmit:!1,onSubmit:function(){},beforeShowError:function(){},afterShowError:function(){},beforeRemoveError:function(){},afterRemoveError:function(){}};Element.prototype.matches||(Element.prototype.matches=Element.prototype.matchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector||Element.prototype.oMatchesSelector||Element.prototype.webkitMatchesSelector||function(e){for(var t=(this.document||this.ownerDocument).querySelectorAll(e),r=t.length;--r>=0&&t.item(r)!==this;);return r>-1});var s=function(){var e={},t=!1,r=0,a=arguments.length;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(t=arguments[0],r++);for(;r0){for(var m=0;m0){for(var i=0;i0)for(var o=0;o0&&!/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(t.value),patternMismatch:t.hasAttribute("pattern")&&a>0&&!1===new RegExp(t.getAttribute("pattern")).test(t.value),rangeOverflow:t.hasAttribute("max")&&x&&t.value>0&&Number(t.value)>Number(t.getAttribute("max")),rangeUnderflow:t.hasAttribute("min")&&x&&t.value>0&&Number(t.value)0&&a>parseInt(t.getAttribute("maxLength"),10),tooShort:t.hasAttribute("minLength")&&t.getAttribute("minLength")>0&&a>0&&a0&&("email"===i&&!/^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test(t.value)||"url"===i&&!/^(?:(?:https?|HTTPS?|ftp|FTP):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)(?:\.(?:[a-zA-Z\u00a1-\uffff0-9]-*)*[a-zA-Z\u00a1-\uffff0-9]+)*)(?::\d{2,5})?(?:[\/?#]\S*)?$/.test(t.value)),valueMissing:t.hasAttribute("required")&&(("checkbox"===i||"radio"===i)&&!t.hasAttribute("checked")||"select"===i&&(-1===t.selectedIndex||t.options[t.selectedIndex].value.length<1)||"checkbox"!==i&&"radio"!==i&&"select"!==i&&a<1)},f=t.tagName.toLowerCase(),l=f in r?r[f].call(t):{};for(var c in d)d.hasOwnProperty(c)&&(c in l&&l[c]&&(d[c]=!0),d[c]&&(n=!1));return d.valid=n,d};(function(){var t=e.createElement("input");return"validity"in t&&"badInput"in t.validity&&"patternMismatch"in t.validity&&"rangeOverflow"in t.validity&&"rangeUnderflow"in t.validity&&"stepMismatch"in t.validity&&"tooLong"in t.validity&&"tooShort"in t.validity&&"typeMismatch"in t.validity&&"valid"in t.validity&&"valueMissing"in t.validity})()||(Object.defineProperty(HTMLInputElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLButtonElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLSelectElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}),Object.defineProperty(HTMLTextAreaElement.prototype,"validity",{get:function(){return x(this)},configurable:!0}))})(window,document); \ No newline at end of file