├── .github ├── CONTRIBUTING.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── dist ├── validate.js ├── validate.min.js ├── validate.polyfills.js └── validate.polyfills.min.js ├── docs ├── dist │ ├── validate.js │ ├── validate.min.js │ ├── validate.polyfills.js │ └── validate.polyfills.min.js └── index.html ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── docs │ ├── _templates │ │ ├── _footer.html │ │ └── _header.html │ └── index.md └── js │ ├── _validityState.polyfill.js │ └── validate.js └── static └── js ├── validate.js ├── validate.min.js ├── validityState-polyfill.js └── validityState-polyfill.min.js /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Bugs, Questions, and Feature Requests 2 | 3 | Report bugs, ask questions, and request features using [GitHub Issues](https://github.com/cferdinandi/smooth-scroll/issues). 4 | 5 | **Before posting, do a search to make sure your issue or question hasn't already been reported or discussed.** If no matching issue exists, go ahead and create one. 6 | 7 | **Please be sure to include all of the following:** 8 | 9 | 1. A clear, descriptive title (ie. "A bug" is not a good title). 10 | 2. [A reduced test case.](https://css-tricks.com/reduced-test-cases/) 11 | - Clearly demonstrate the bug or issue. 12 | - Include the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug. 13 | - A link to your production site is **not** a reduced test case. 14 | - You can create one by [forking this JSFiddle](https://jsfiddle.net/cferdinandi/yhqepa3x/). 15 | 3. The browser and OS that you're using. 16 | 17 | Duplicates and issues without a reduced test case may be closed without comment. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Test case:** https://jsfiddle.net/cferdinandi/yhqepa3x/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | test/results 4 | test/coverage 5 | 6 | ## OS X 7 | .DS_Store 8 | ._* 9 | .Spotlight-V100 10 | .Trashes -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | before_script: 5 | - npm install -g gulp 6 | script: gulp -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Go Make Things, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validate.js [![Build Status](https://travis-ci.org/cferdinandi/validate.svg)](https://travis-ci.org/cferdinandi/validate) 2 | A lightweight form validation script that augments native HTML5 form validation elements and attributes, providing a better user experience and giving you more control. 3 | 4 | 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). 5 | 6 | [View the demo](http://cferdinandi.github.io/validate/) 7 | 8 | ## Deprecation Notice 9 | 10 | This plugin has been deprecated and replaced with [Bouncer](https://github.com/cferdinandi/bouncer), which uses the came conventions but has better under-the-hood engineering. 11 | 12 | Validate.js attempted to use the Constraint Validation API to validate fields. The API is buggy at best, and smoothing it across browsers became an increasingly difficult task. As a result, I wrote an entirely new plugin from the ground-up. 13 | 14 | Because this plugin was featured in [my CSS Tricks series](https://css-tricks.com/form-validation-part-1-constraint-validation-html/), I'm leaving it up in read only mode for archival purposes. 15 | 16 | It will no longer be maintained or updated. 17 | 18 | 19 |
20 | 21 | ### Want to learn how to write your own vanilla JS plugins? Check out my [Vanilla JS Pocket Guides](https://vanillajsguides.com/) or join the [Vanilla JS Academy](https://vanillajsacademy.com) and level-up as a web developer. 🚀 22 | 23 |
24 | 25 | 26 | 27 | ## Getting Started 28 | 29 | Compiled and production-ready code can be found in the `dist` directory. The `src` directory contains development code. 30 | 31 | ### 1. Include Validate.js on your site. 32 | 33 | There are two versions of Validate: the standalone version, and one that comes preloaded with a polyfill for the Validaty State API, which is only supported in newer browsers and implemented inconsistently. 34 | 35 | If you're including your own polyfill or don't want to enable this feature for older browsers, use the standalone version. Otherwise, use the version with the polyfill. 36 | 37 | **Direct Download** 38 | 39 | You can [download the files directly from GitHub](https://github.com/cferdinandi/validate/archive/master.zip). 40 | 41 | ```html 42 | 43 | ``` 44 | 45 | **CDN** 46 | 47 | You can also use the [jsDelivr CDN](https://cdn.jsdelivr.net/gh/cferdinandi/validate/dist/). I recommend linking to a specific version number or version range to prevent major updates from breaking your site. Validate uses semantic versioning. 48 | 49 | ```html 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ``` 66 | 67 | ### 2. Use HTML5 semantic input types and validation-related attributes on your form fields. 68 | 69 | Add the `required` attribute to required fields. Use `type="email"` and `type="url"` for email addresses and URLs, respectively. Include `pattern` attributes, `min` and `max`, and so on to set validation criteria for your form fields. View a [full list of types and attributes on MDN](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation#Intrinsic_and_basic_constraints). 70 | 71 | ```html 72 |
73 | 74 | 75 |
76 | 77 |
78 | 79 | 80 |
81 | ``` 82 | 83 | If you're using validation patterns, you can also include a `title` with a custom validation message. This will display in the error message. 84 | 85 | ```html 86 |
87 | 88 | 89 |
90 | ``` 91 | 92 | ### 3. Flag your form for validation. 93 | 94 | Add the `[data-validate]` attribute to any forms you want validated. 95 | 96 | ```html 97 |
98 | ... 99 |
100 | ``` 101 | 102 | ### 4. Initialize Validate.js. 103 | 104 | In the footer of your page, after the content, initialize Validate.js. And that's it, you're done. Nice work! 105 | 106 | ```html 107 | 110 | ``` 111 | 112 | 113 | 114 | ## ES6 Modules 115 | 116 | Validate does not have a default export, but does support CommonJS and can be used with native ES6 module imports. 117 | 118 | ```js 119 | import('/path/to/validate.polyfills.min.js') 120 | .then(function () { 121 | validate.init(); 122 | }); 123 | ``` 124 | 125 | It uses a UMD pattern, and should also work in most major module bundlers and package managers. 126 | 127 | 128 | 129 | ## Styling Errors 130 | 131 | Validate.js does not come pre-packaged with any styles for fields with errors or error messages. Use the `.error` class to style fields, and the `.error-message` class to style error messages. 132 | 133 | Need a starting point? Here's some really lightweight CSS you can use. 134 | 135 | ```css 136 | /** 137 | * Form Validation Errors 138 | */ 139 | .error { 140 | border-color: red; 141 | } 142 | 143 | .error-message { 144 | color: red; 145 | font-style: italic; 146 | margin-bottom: 1em; 147 | } 148 | ``` 149 | 150 | 151 | 152 | ## Working with the Source Files 153 | 154 | If you would prefer, you can work with the development code in the `src` directory using the included [Gulp build system](http://gulpjs.com/). This compiles, lints, and minifies code. 155 | 156 | ### Dependencies 157 | Make sure these are installed first. 158 | 159 | * [Node.js](http://nodejs.org) 160 | * [Gulp](http://gulpjs.com) `sudo npm install -g gulp` 161 | 162 | ### Quick Start 163 | 164 | 1. In bash/terminal/command line, `cd` into your project directory. 165 | 2. Run `npm install` to install required files. 166 | 3. When it's done installing, run one of the task runners to get going: 167 | * `gulp` manually compiles files. 168 | * `gulp watch` automatically compiles files when changes are made and applies changes using [LiveReload](http://livereload.com/). 169 | 170 | 171 | 172 | ## Options and Settings 173 | 174 | Validate.js includes smart defaults and works right out of the box. But if you want to customize things, it also has a robust API that provides multiple ways for you to adjust the default options and settings. 175 | 176 | ### Global Settings 177 | 178 | You can pass options and callbacks into Validate through the `init()` function: 179 | 180 | ```javascript 181 | validate.init({ 182 | 183 | // Classes and Selectors 184 | selector: '[data-validate]', // The selector for forms to validate 185 | fieldClass: 'error', // The class to apply to fields with errors 186 | errorClass: 'error-message', // The class to apply to error messages 187 | 188 | // Messages 189 | messageValueMissing: 'Please fill out this field.', // Displayed when a required field is left empty 190 | messageValueMissingCheckbox: 'This field is required.', // Displayed when a required checkbox isn't checked 191 | messageValueMissingRadio: 'Please select a value.', // Displayed when a required radio button isn't selected 192 | messageValueMissingSelect: 'Please select a value.', // Displayed when an option from a required select menu isn't selected 193 | messageValueMissingSelectMulti: 'Please select at least one value.', // Displayed when an option from a require multi-select menu isn't selected 194 | messageTypeMismatchEmail: 'Please enter an email address.', // Displayed when a `type="email"` field isn't a valid email 195 | messageTypeMismatchURL: 'Please enter a URL.', // Displayed when a `type="url"` field isn't a valid URL 196 | messageTooShort: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.', // Displayed with the `minLength` attribute is used and the input value is too short 197 | messageTooLong: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.', // Displayed with the `maxLength` attribute is used and the input value is too long 198 | messagePatternMismatch: 'Please match the requested format.', // Displayed with the `pattern` attribute is used and the pattern doesn't match (if a `title` attribute is used, that's displayed instead) 199 | messageBadInput: 'Please enter a number.', // Displayed when the field is numeric (ex. `type="number"`) but the value is not a number 200 | messageStepMismatch: 'Please select a valid value.', // Displayed when the `step` attribute is used and the value doesn't adhere to it 201 | messageRangeOverflow: 'Please select a value that is no more than {max}.', // Displayed with the `max` attribute is used and the input value is too hight 202 | 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 203 | 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 204 | 205 | // Form Submission 206 | disableSubmit: false, // If true, don't submit the form to the server (for Ajax for submission) 207 | onSubmit: function (form, fields) {}, // Function to run if the form successfully validates 208 | 209 | // Callbacks 210 | beforeShowError: function (field, error) {}, // Function to run before an error is display 211 | afterShowError: function (field, error) {}, // Function to run after an error is display 212 | beforeRemoveError: function (field) {}, // Function to run before an error is removed 213 | afterRemoveError: function (field) {}, // Function to run after an error is removed 214 | 215 | }); 216 | ``` 217 | 218 | ### Use Validate.js events in your own scripts 219 | 220 | You can also call Validate.js's public methods in your own scripts. 221 | 222 | #### hasError() 223 | Check if a field has a validation error. 224 | 225 | ```javascript 226 | validate.hasError( 227 | field, // The field to validate 228 | options // User settings, same as the ones passed in during validate.init() [optional] 229 | ); 230 | ``` 231 | 232 | **Example** 233 | 234 | ```javascript 235 | var field = document.querySelector('[name="email"]'); 236 | var error = validate.hasError(field); 237 | 238 | if (error) { 239 | // Do something... 240 | } 241 | ``` 242 | 243 | #### showError() 244 | Show an error message on a field. 245 | 246 | ```javascript 247 | validate.showError( 248 | field, // The field to show an error message for 249 | error, // The error message to show 250 | options // User settings, same as the ones passed in during validate.init() [optional] 251 | ); 252 | ``` 253 | 254 | **Example 1: Write your own error** 255 | 256 | ```javascript 257 | var field = document.querySelector('[name="email"]'); 258 | var error = 'This field is wrong, dude!'; 259 | validate.showError(field, error); 260 | ``` 261 | 262 | **Example 2: Using `hasError()`** 263 | 264 | ```javascript 265 | var field = document.querySelector('[name="url"]'); 266 | var error = validate.hasError(field); 267 | validate.showError(field, error); 268 | ``` 269 | 270 | #### removeError() 271 | Remove the error message from a field. 272 | 273 | ```javascript 274 | /** 275 | * Remove an error message from a field 276 | * @public 277 | * @param {Node} field The field to remove the error from 278 | * @param {Object} options User options 279 | */ 280 | validate.removeError( 281 | field, // The field to remove the error from 282 | options // User settings, same as the ones passed in during validate.init() [optional] 283 | ); 284 | ``` 285 | 286 | **Example** 287 | 288 | ```javascript 289 | var field = document.querySelector('[name="email"]'); 290 | validate.removeError(field); 291 | ``` 292 | 293 | #### destroy() 294 | 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. 295 | 296 | ```javascript 297 | validate.destroy(); 298 | ``` 299 | 300 | 301 | ## Browser Compatibility 302 | 303 | Validate.js works in all modern browsers, and (mostly) IE 10 and above. 304 | 305 | Unfortunately, not all validation types are supported by all versions of IE and Edge consistently. For example, IE10 and IE11 will check if a form input is too long (using the `maxLength` attribute), but Edge will not. And no version of IE or Edge will check if it's too short (using the `minLength` attribute). 306 | 307 | ### Polyfills 308 | 309 | Use the included polyfill version of Validate (or include your own) to push support back to IE10, and add missing features to partially supported browsers. 310 | 311 | If you also include [Eli Grey's classList.js polyfill](https://github.com/eligrey/classList.js/), you can push support even further, back to IE9. 312 | 313 | 314 | 315 | ## License 316 | 317 | The code is available under the [MIT License](LICENSE.md). 318 | -------------------------------------------------------------------------------- /dist/validate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * validate v2.2.0: A lightweight form validation script that augments native HTML5 form validation elements and attributes. 3 | * (c) 2018 Chris Ferdinandi 4 | * MIT License 5 | * http://github.com/cferdinandi/validate 6 | */ 7 | 8 | (function (root, factory) { 9 | if ( typeof define === 'function' && define.amd ) { 10 | define([], factory(root)); 11 | } else if ( typeof exports === 'object' ) { 12 | module.exports = factory(root); 13 | } else { 14 | root.validate = factory(root); 15 | } 16 | })(typeof global !== 'undefined' ? global : this.window || this.global, (function (root) { 17 | 18 | 'use strict'; 19 | 20 | // 21 | // Variables 22 | // 23 | 24 | var validate = {}; // Object for public APIs 25 | var settings; 26 | 27 | // Default settings 28 | var defaults = { 29 | 30 | // Classes and Selectors 31 | selector: '[data-validate]', 32 | fieldClass: 'error', 33 | errorClass: 'error-message', 34 | 35 | // Messages 36 | messageValueMissing: 'Please fill out this field.', 37 | messageValueMissingCheckbox: 'This field is required.', 38 | messageValueMissingRadio: 'Please select a value.', 39 | messageValueMissingSelect: 'Please select a value.', 40 | messageValueMissingSelectMulti: 'Please select at least one value.', 41 | messageTypeMismatchEmail: 'Please enter an email address.', 42 | messageTypeMismatchURL: 'Please enter a URL.', 43 | messageTooShort: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.', 44 | messageTooLong: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.', 45 | messagePatternMismatch: 'Please match the requested format.', 46 | messageBadInput: 'Please enter a number.', 47 | messageStepMismatch: 'Please select a valid value.', 48 | messageRangeOverflow: 'Please select a value that is no more than {max}.', 49 | messageRangeUnderflow: 'Please select a value that is no less than {min}.', 50 | messageGeneric: 'The value you entered for this field is invalid.', 51 | 52 | // Form Submission 53 | disableSubmit: false, 54 | onSubmit: function () {}, 55 | 56 | // Callbacks 57 | beforeShowError: function () {}, 58 | afterShowError: function () {}, 59 | beforeRemoveError: function () {}, 60 | afterRemoveError: function () {} 61 | 62 | }; 63 | 64 | 65 | // 66 | // Methods 67 | // 68 | 69 | /** 70 | * Element.matches() polyfill (simple version) 71 | * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill 72 | */ 73 | if (!Element.prototype.matches) { 74 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 75 | } 76 | 77 | /** 78 | * Feature test 79 | * @return {Boolean} Returns true if required methods and APIs are supported by the browser 80 | */ 81 | var supports = function () { 82 | return 'querySelector' in document && 'addEventListener' in root; 83 | }; 84 | 85 | /** 86 | * Merge two or more objects. Returns a new object. 87 | * @private 88 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 89 | * @param {Object} objects The objects to merge together 90 | * @returns {Object} Merged values of defaults and options 91 | */ 92 | var extend = function () { 93 | 94 | // Variables 95 | var extended = {}; 96 | var deep = false; 97 | var i = 0; 98 | var length = arguments.length; 99 | 100 | // Check if a deep merge 101 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 102 | deep = arguments[0]; 103 | i++; 104 | } 105 | 106 | // Merge the object into the extended object 107 | var merge = function (obj) { 108 | for ( var prop in obj ) { 109 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 110 | // If deep merge and property is an object, merge properties 111 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 112 | extended[prop] = extend( true, extended[prop], obj[prop] ); 113 | } else { 114 | extended[prop] = obj[prop]; 115 | } 116 | } 117 | } 118 | }; 119 | 120 | // Loop through each object and conduct a merge 121 | for ( ; i < length; i++ ) { 122 | var obj = arguments[i]; 123 | merge(obj); 124 | } 125 | 126 | return extended; 127 | 128 | }; 129 | 130 | /** 131 | * Get the closest matching element up the DOM tree. 132 | * @private 133 | * @param {Element} elem Starting element 134 | * @param {String} selector Selector to match against 135 | * @return {Boolean|Element} Returns null if not match found 136 | */ 137 | var getClosest = function ( elem, selector ) { 138 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 139 | if ( elem.matches( selector ) ) return elem; 140 | } 141 | return null; 142 | }; 143 | 144 | /** 145 | * Validate a form field 146 | * @public 147 | * @param {Node} field The field to validate 148 | * @param {Object} options User options 149 | * @return {String} The error message 150 | */ 151 | validate.hasError = function (field, options) { 152 | 153 | // Merge user options with existing settings or defaults 154 | var localSettings = extend(settings || defaults, options || {}); 155 | 156 | // Don't validate submits, buttons, file and reset inputs, and disabled fields 157 | if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return; 158 | 159 | // Get validity 160 | var validity = field.validity; 161 | 162 | // If valid, return null 163 | if (validity.valid) return; 164 | 165 | // If field is required and empty 166 | if (validity.valueMissing) { 167 | 168 | if (field.type === 'checkbox') return localSettings.messageValueMissingCheckbox; 169 | 170 | if (field.type === 'radio') return localSettings.messageValueMissingRadio; 171 | 172 | if (field.type === 'select-multiple') return localSettings.messageValueMissingSelectMulti; 173 | 174 | if (field.type === 'select-one') return localSettings.messageValueMissingSelect; 175 | 176 | return localSettings.messageValueMissing; 177 | } 178 | 179 | // If not the right type 180 | if (validity.typeMismatch) { 181 | 182 | // Email 183 | if (field.type === 'email') return localSettings.messageTypeMismatchEmail; 184 | 185 | // URL 186 | if (field.type === 'url') return localSettings.messageTypeMismatchURL; 187 | 188 | } 189 | 190 | // If too short 191 | if (validity.tooShort) return localSettings.messageTooShort.replace('{minLength}', field.getAttribute('minLength')).replace('{length}', field.value.length); 192 | 193 | // If too long 194 | if (validity.tooLong) return localSettings.messageTooLong.replace('{minLength}', field.getAttribute('maxLength')).replace('{length}', field.value.length); 195 | 196 | // If number input isn't a number 197 | if (validity.badInput) return localSettings.messageBadInput; 198 | 199 | // If a number value doesn't match the step interval 200 | if (validity.stepMismatch) return localSettings.messageStepMismatch; 201 | 202 | // If a number field is over the max 203 | if (validity.rangeOverflow) return localSettings.messageRangeOverflow.replace('{max}', field.getAttribute('max')); 204 | 205 | // If a number field is below the min 206 | if (validity.rangeUnderflow) return localSettings.messageRangeUnderflow.replace('{min}', field.getAttribute('min')); 207 | 208 | // If pattern doesn't match 209 | if (validity.patternMismatch) { 210 | 211 | // If pattern info is included, return custom error 212 | if (field.hasAttribute('title')) return field.getAttribute('title'); 213 | 214 | // Otherwise, generic error 215 | return localSettings.messagePatternMismatch; 216 | 217 | } 218 | 219 | // If all else fails, return a generic catchall error 220 | return localSettings.messageGeneric; 221 | 222 | }; 223 | 224 | /** 225 | * Show an error message on a field 226 | * @public 227 | * @param {Node} field The field to show an error message for 228 | * @param {String} error The error message to show 229 | * @param {Object} options User options 230 | */ 231 | validate.showError = function (field, error, options) { 232 | 233 | // Merge user options with existing settings or defaults 234 | var localSettings = extend(settings || defaults, options || {}); 235 | 236 | // Before show error callback 237 | localSettings.beforeShowError(field, error); 238 | 239 | // Add error class to field 240 | field.classList.add(localSettings.fieldClass); 241 | 242 | // If the field is a radio button and part of a group, error all and get the last item in the group 243 | if (field.type === 'radio' && field.name) { 244 | var group = document.getElementsByName(field.name); 245 | if (group.length > 0) { 246 | for (var i = 0; i < group.length; i++) { 247 | if (group[i].form !== field.form) continue; // Only check fields in current form 248 | group[i].classList.add(localSettings.fieldClass); 249 | } 250 | field = group[group.length - 1]; 251 | } 252 | } 253 | 254 | // Get field id or name 255 | var id = field.id || field.name; 256 | if (!id) return; 257 | 258 | // Check if error message field already exists 259 | // If not, create one 260 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id ); 261 | if (!message) { 262 | message = document.createElement('div'); 263 | message.className = localSettings.errorClass; 264 | message.id = 'error-for-' + id; 265 | 266 | // If the field is a radio button or checkbox, insert error after the label 267 | var label; 268 | if (field.type === 'radio' || field.type ==='checkbox') { 269 | label = field.form.querySelector('label[for="' + id + '"]') || getClosest(field, 'label'); 270 | if (label) { 271 | label.parentNode.insertBefore( message, label.nextSibling ); 272 | } 273 | } 274 | 275 | // Otherwise, insert it after the field 276 | if (!label) { 277 | field.parentNode.insertBefore( message, field.nextSibling ); 278 | } 279 | } 280 | 281 | // Add ARIA role to the field 282 | field.setAttribute('aria-describedby', 'error-for-' + id); 283 | 284 | // Update error message 285 | message.innerHTML = error; 286 | 287 | // Remove any existing styles hiding the error message 288 | message.style.display = ''; 289 | message.style.visibility = ''; 290 | 291 | // After show error callback 292 | localSettings.afterShowError(field, error); 293 | 294 | }; 295 | 296 | /** 297 | * Remove an error message from a field 298 | * @public 299 | * @param {Node} field The field to remove the error from 300 | * @param {Object} options User options 301 | */ 302 | validate.removeError = function (field, options) { 303 | 304 | // Merge user options with existing settings or defaults 305 | var localSettings = extend(settings || defaults, options || {}); 306 | 307 | // Before remove error callback 308 | localSettings.beforeRemoveError(field); 309 | 310 | // Remove ARIA role from the field 311 | field.removeAttribute('aria-describedby'); 312 | 313 | // Remove error class to field 314 | field.classList.remove(localSettings.fieldClass); 315 | 316 | // If the field is a radio button and part of a group, remove error from all and get the last item in the group 317 | if (field.type === 'radio' && field.name) { 318 | var group = document.getElementsByName(field.name); 319 | if (group.length > 0) { 320 | for (var i = 0; i < group.length; i++) { 321 | if (group[i].form !== field.form) continue; // Only check fields in current form 322 | group[i].classList.remove(localSettings.fieldClass); 323 | } 324 | field = group[group.length - 1]; 325 | } 326 | } 327 | 328 | // Get field id or name 329 | var id = field.id || field.name; 330 | if (!id) return; 331 | 332 | // Check if an error message is in the DOM 333 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id + ''); 334 | if (!message) return; 335 | 336 | // If so, hide it 337 | message.innerHTML = ''; 338 | message.style.display = 'none'; 339 | message.style.visibility = 'hidden'; 340 | 341 | // After remove error callback 342 | localSettings.afterRemoveError(field); 343 | 344 | }; 345 | 346 | /** 347 | * Add the `novalidate` attribute to all forms 348 | * @private 349 | * @param {Boolean} remove If true, remove the `novalidate` attribute 350 | */ 351 | var addNoValidate = function (remove) { 352 | var forms = document.querySelectorAll(settings.selector); 353 | for (var i = 0; i < forms.length; i++) { 354 | if (remove) { 355 | forms[i].removeAttribute('novalidate'); 356 | continue; 357 | } 358 | forms[i].setAttribute('novalidate', true); 359 | } 360 | }; 361 | 362 | /** 363 | * Check field validity when it loses focus 364 | * @private 365 | * @param {Event} event The blur event 366 | */ 367 | var blurHandler = function (event) { 368 | 369 | // Only run if the field is in a form to be validated 370 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 371 | 372 | // Validate the field 373 | var error = validate.hasError(event.target); 374 | 375 | // If there's an error, show it 376 | if (error) { 377 | validate.showError(event.target, error); 378 | return; 379 | } 380 | 381 | // Otherwise, remove any errors that exist 382 | validate.removeError(event.target); 383 | 384 | }; 385 | 386 | /** 387 | * Check radio and checkbox field validity when clicked 388 | * @private 389 | * @param {Event} event The click event 390 | */ 391 | var clickHandler = function (event) { 392 | 393 | // Only run if the field is in a form to be validated 394 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 395 | 396 | // Only run if the field is a checkbox or radio 397 | var type = event.target.getAttribute('type'); 398 | if (!(type === 'checkbox' || type === 'radio')) return; 399 | 400 | // Validate the field 401 | var error = validate.hasError(event.target); 402 | 403 | // If there's an error, show it 404 | if (error) { 405 | validate.showError(event.target, error); 406 | return; 407 | } 408 | 409 | // Otherwise, remove any errors that exist 410 | validate.removeError(event.target); 411 | 412 | }; 413 | 414 | /** 415 | * Check all fields on submit 416 | * @private 417 | * @param {Event} event The submit event 418 | */ 419 | var submitHandler = function (event) { 420 | 421 | // Only run on forms flagged for validation 422 | if (!event.target.matches(settings.selector)) return; 423 | 424 | // Get all of the form elements 425 | var fields = event.target.elements; 426 | 427 | // Validate each field 428 | // Store the first field with an error to a variable so we can bring it into focus later 429 | var hasErrors; 430 | for (var i = 0; i < fields.length; i++) { 431 | var error = validate.hasError(fields[i]); 432 | if (error) { 433 | validate.showError(fields[i], error); 434 | if (!hasErrors) { 435 | hasErrors = fields[i]; 436 | } 437 | } 438 | } 439 | 440 | // Prevent form from submitting if there are errors or submission is disabled 441 | if (hasErrors || settings.disableSubmit) { 442 | event.preventDefault(); 443 | } 444 | 445 | // If there are errrors, focus on first element with error 446 | if (hasErrors) { 447 | hasErrors.focus(); 448 | return; 449 | } 450 | 451 | // Otherwise, submit the form 452 | settings.onSubmit(event.target, fields); 453 | 454 | }; 455 | 456 | /** 457 | * Destroy the current initialization. 458 | * @public 459 | */ 460 | validate.destroy = function () { 461 | 462 | // If plugin isn't already initialized, stop 463 | if ( !settings ) return; 464 | 465 | // Remove event listeners 466 | document.removeEventListener('blur', blurHandler, true); 467 | document.removeEventListener('click', clickHandler, false); 468 | document.removeEventListener('submit', submitHandler, false); 469 | 470 | // Remove all errors 471 | var fields = document.querySelectorAll(settings.errorClass); 472 | for (var i = 0; i < fields.length; i++) { 473 | validate.removeError(fields[i]); 474 | } 475 | 476 | // Remove `novalidate` from forms 477 | addNoValidate(true); 478 | 479 | // Reset variables 480 | settings = null; 481 | 482 | }; 483 | 484 | /** 485 | * Initialize Validate 486 | * @public 487 | * @param {Object} options User settings 488 | */ 489 | validate.init = function (options) { 490 | 491 | // feature test 492 | if (!supports()) return; 493 | 494 | // Destroy any existing initializations 495 | validate.destroy(); 496 | 497 | // Merge user options with defaults 498 | settings = extend(defaults, options || {}); 499 | 500 | // Add the `novalidate` attribute to all forms 501 | addNoValidate(); 502 | 503 | // Event listeners 504 | document.addEventListener('blur', blurHandler, true); 505 | document.addEventListener('click', clickHandler, true); 506 | document.addEventListener('submit', submitHandler, false); 507 | 508 | }; 509 | 510 | 511 | // 512 | // Public APIs 513 | // 514 | 515 | return validate; 516 | 517 | })); -------------------------------------------------------------------------------- /dist/validate.min.js: -------------------------------------------------------------------------------- 1 | /*! validate v2.2.0 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ 2 | !(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={selector:"[data-validate]",fieldClass:"error",errorClass:"error-message",messageValueMissing:"Please fill out this field.",messageValueMissingCheckbox:"This field is required.",messageValueMissingRadio:"Please select a value.",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.msMatchesSelector||Element.prototype.webkitMatchesSelector);var s=function(){return"querySelector"in document&&"addEventListener"in e},o=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) { 59 | for (var i = 0; i < group.length; i++) { 60 | if (group[i].form === field.form && field.checked) { 61 | field = group[i]; 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | 68 | // Run validity checks 69 | var checkValidity = { 70 | badInput: (isNum && length > 0 && !/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(field.value)), // value of a number field is not a number 71 | patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern 72 | 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 73 | 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 74 | 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 75 | 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 76 | 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 77 | 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 78 | 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 79 | }; 80 | 81 | // Run browser's own validation if available 82 | var fieldTagName = field.tagName.toLowerCase(); 83 | var browserValidity = fieldTagName in browserValidityFunctions ? browserValidityFunctions[fieldTagName].call(field) : {}; 84 | 85 | // Check if any errors 86 | for (var key in checkValidity) { 87 | if (checkValidity.hasOwnProperty(key)) { 88 | // If browser has detected an error, adopt it to our validity object 89 | if (key in browserValidity && browserValidity[key]) { 90 | checkValidity[key] = true; 91 | } 92 | 93 | // If there's an error, change valid value 94 | if (checkValidity[key]) { 95 | valid = false; 96 | } 97 | } 98 | } 99 | 100 | // Add valid property to validity object 101 | checkValidity.valid = valid; 102 | 103 | // Return object 104 | return checkValidity; 105 | 106 | }; 107 | 108 | // If the full set of ValidityState features aren't supported, polyfill 109 | if (!supported()) { 110 | Object.defineProperty(HTMLInputElement.prototype, 'validity', { 111 | get: function ValidityState() { 112 | return getValidityState(this); 113 | }, 114 | configurable: true, 115 | }); 116 | Object.defineProperty(HTMLButtonElement.prototype, 'validity', { 117 | get: function ValidityState() { 118 | return getValidityState(this); 119 | }, 120 | configurable: true, 121 | }); 122 | Object.defineProperty(HTMLSelectElement.prototype, 'validity', { 123 | get: function ValidityState() { 124 | return getValidityState(this); 125 | }, 126 | configurable: true, 127 | }); 128 | Object.defineProperty(HTMLTextAreaElement.prototype, 'validity', { 129 | get: function ValidityState() { 130 | return getValidityState(this); 131 | }, 132 | configurable: true, 133 | }); 134 | } 135 | 136 | })(window, document); 137 | (function (root, factory) { 138 | if ( typeof define === 'function' && define.amd ) { 139 | define([], factory(root)); 140 | } else if ( typeof exports === 'object' ) { 141 | module.exports = factory(root); 142 | } else { 143 | root.validate = factory(root); 144 | } 145 | })(typeof global !== 'undefined' ? global : this.window || this.global, (function (root) { 146 | 147 | 'use strict'; 148 | 149 | // 150 | // Variables 151 | // 152 | 153 | var validate = {}; // Object for public APIs 154 | var settings; 155 | 156 | // Default settings 157 | var defaults = { 158 | 159 | // Classes and Selectors 160 | selector: '[data-validate]', 161 | fieldClass: 'error', 162 | errorClass: 'error-message', 163 | 164 | // Messages 165 | messageValueMissing: 'Please fill out this field.', 166 | messageValueMissingCheckbox: 'This field is required.', 167 | messageValueMissingRadio: 'Please select a value.', 168 | messageValueMissingSelect: 'Please select a value.', 169 | messageValueMissingSelectMulti: 'Please select at least one value.', 170 | messageTypeMismatchEmail: 'Please enter an email address.', 171 | messageTypeMismatchURL: 'Please enter a URL.', 172 | messageTooShort: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.', 173 | messageTooLong: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.', 174 | messagePatternMismatch: 'Please match the requested format.', 175 | messageBadInput: 'Please enter a number.', 176 | messageStepMismatch: 'Please select a valid value.', 177 | messageRangeOverflow: 'Please select a value that is no more than {max}.', 178 | messageRangeUnderflow: 'Please select a value that is no less than {min}.', 179 | messageGeneric: 'The value you entered for this field is invalid.', 180 | 181 | // Form Submission 182 | disableSubmit: false, 183 | onSubmit: function () {}, 184 | 185 | // Callbacks 186 | beforeShowError: function () {}, 187 | afterShowError: function () {}, 188 | beforeRemoveError: function () {}, 189 | afterRemoveError: function () {} 190 | 191 | }; 192 | 193 | 194 | // 195 | // Methods 196 | // 197 | 198 | /** 199 | * Element.matches() polyfill (simple version) 200 | * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill 201 | */ 202 | if (!Element.prototype.matches) { 203 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 204 | } 205 | 206 | /** 207 | * Feature test 208 | * @return {Boolean} Returns true if required methods and APIs are supported by the browser 209 | */ 210 | var supports = function () { 211 | return 'querySelector' in document && 'addEventListener' in root; 212 | }; 213 | 214 | /** 215 | * Merge two or more objects. Returns a new object. 216 | * @private 217 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 218 | * @param {Object} objects The objects to merge together 219 | * @returns {Object} Merged values of defaults and options 220 | */ 221 | var extend = function () { 222 | 223 | // Variables 224 | var extended = {}; 225 | var deep = false; 226 | var i = 0; 227 | var length = arguments.length; 228 | 229 | // Check if a deep merge 230 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 231 | deep = arguments[0]; 232 | i++; 233 | } 234 | 235 | // Merge the object into the extended object 236 | var merge = function (obj) { 237 | for ( var prop in obj ) { 238 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 239 | // If deep merge and property is an object, merge properties 240 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 241 | extended[prop] = extend( true, extended[prop], obj[prop] ); 242 | } else { 243 | extended[prop] = obj[prop]; 244 | } 245 | } 246 | } 247 | }; 248 | 249 | // Loop through each object and conduct a merge 250 | for ( ; i < length; i++ ) { 251 | var obj = arguments[i]; 252 | merge(obj); 253 | } 254 | 255 | return extended; 256 | 257 | }; 258 | 259 | /** 260 | * Get the closest matching element up the DOM tree. 261 | * @private 262 | * @param {Element} elem Starting element 263 | * @param {String} selector Selector to match against 264 | * @return {Boolean|Element} Returns null if not match found 265 | */ 266 | var getClosest = function ( elem, selector ) { 267 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 268 | if ( elem.matches( selector ) ) return elem; 269 | } 270 | return null; 271 | }; 272 | 273 | /** 274 | * Validate a form field 275 | * @public 276 | * @param {Node} field The field to validate 277 | * @param {Object} options User options 278 | * @return {String} The error message 279 | */ 280 | validate.hasError = function (field, options) { 281 | 282 | // Merge user options with existing settings or defaults 283 | var localSettings = extend(settings || defaults, options || {}); 284 | 285 | // Don't validate submits, buttons, file and reset inputs, and disabled fields 286 | if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return; 287 | 288 | // Get validity 289 | var validity = field.validity; 290 | 291 | // If valid, return null 292 | if (validity.valid) return; 293 | 294 | // If field is required and empty 295 | if (validity.valueMissing) { 296 | 297 | if (field.type === 'checkbox') return localSettings.messageValueMissingCheckbox; 298 | 299 | if (field.type === 'radio') return localSettings.messageValueMissingRadio; 300 | 301 | if (field.type === 'select-multiple') return localSettings.messageValueMissingSelectMulti; 302 | 303 | if (field.type === 'select-one') return localSettings.messageValueMissingSelect; 304 | 305 | return localSettings.messageValueMissing; 306 | } 307 | 308 | // If not the right type 309 | if (validity.typeMismatch) { 310 | 311 | // Email 312 | if (field.type === 'email') return localSettings.messageTypeMismatchEmail; 313 | 314 | // URL 315 | if (field.type === 'url') return localSettings.messageTypeMismatchURL; 316 | 317 | } 318 | 319 | // If too short 320 | if (validity.tooShort) return localSettings.messageTooShort.replace('{minLength}', field.getAttribute('minLength')).replace('{length}', field.value.length); 321 | 322 | // If too long 323 | if (validity.tooLong) return localSettings.messageTooLong.replace('{minLength}', field.getAttribute('maxLength')).replace('{length}', field.value.length); 324 | 325 | // If number input isn't a number 326 | if (validity.badInput) return localSettings.messageBadInput; 327 | 328 | // If a number value doesn't match the step interval 329 | if (validity.stepMismatch) return localSettings.messageStepMismatch; 330 | 331 | // If a number field is over the max 332 | if (validity.rangeOverflow) return localSettings.messageRangeOverflow.replace('{max}', field.getAttribute('max')); 333 | 334 | // If a number field is below the min 335 | if (validity.rangeUnderflow) return localSettings.messageRangeUnderflow.replace('{min}', field.getAttribute('min')); 336 | 337 | // If pattern doesn't match 338 | if (validity.patternMismatch) { 339 | 340 | // If pattern info is included, return custom error 341 | if (field.hasAttribute('title')) return field.getAttribute('title'); 342 | 343 | // Otherwise, generic error 344 | return localSettings.messagePatternMismatch; 345 | 346 | } 347 | 348 | // If all else fails, return a generic catchall error 349 | return localSettings.messageGeneric; 350 | 351 | }; 352 | 353 | /** 354 | * Show an error message on a field 355 | * @public 356 | * @param {Node} field The field to show an error message for 357 | * @param {String} error The error message to show 358 | * @param {Object} options User options 359 | */ 360 | validate.showError = function (field, error, options) { 361 | 362 | // Merge user options with existing settings or defaults 363 | var localSettings = extend(settings || defaults, options || {}); 364 | 365 | // Before show error callback 366 | localSettings.beforeShowError(field, error); 367 | 368 | // Add error class to field 369 | field.classList.add(localSettings.fieldClass); 370 | 371 | // If the field is a radio button and part of a group, error all and get the last item in the group 372 | if (field.type === 'radio' && field.name) { 373 | var group = document.getElementsByName(field.name); 374 | if (group.length > 0) { 375 | for (var i = 0; i < group.length; i++) { 376 | if (group[i].form !== field.form) continue; // Only check fields in current form 377 | group[i].classList.add(localSettings.fieldClass); 378 | } 379 | field = group[group.length - 1]; 380 | } 381 | } 382 | 383 | // Get field id or name 384 | var id = field.id || field.name; 385 | if (!id) return; 386 | 387 | // Check if error message field already exists 388 | // If not, create one 389 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id ); 390 | if (!message) { 391 | message = document.createElement('div'); 392 | message.className = localSettings.errorClass; 393 | message.id = 'error-for-' + id; 394 | 395 | // If the field is a radio button or checkbox, insert error after the label 396 | var label; 397 | if (field.type === 'radio' || field.type ==='checkbox') { 398 | label = field.form.querySelector('label[for="' + id + '"]') || getClosest(field, 'label'); 399 | if (label) { 400 | label.parentNode.insertBefore( message, label.nextSibling ); 401 | } 402 | } 403 | 404 | // Otherwise, insert it after the field 405 | if (!label) { 406 | field.parentNode.insertBefore( message, field.nextSibling ); 407 | } 408 | } 409 | 410 | // Add ARIA role to the field 411 | field.setAttribute('aria-describedby', 'error-for-' + id); 412 | 413 | // Update error message 414 | message.innerHTML = error; 415 | 416 | // Remove any existing styles hiding the error message 417 | message.style.display = ''; 418 | message.style.visibility = ''; 419 | 420 | // After show error callback 421 | localSettings.afterShowError(field, error); 422 | 423 | }; 424 | 425 | /** 426 | * Remove an error message from a field 427 | * @public 428 | * @param {Node} field The field to remove the error from 429 | * @param {Object} options User options 430 | */ 431 | validate.removeError = function (field, options) { 432 | 433 | // Merge user options with existing settings or defaults 434 | var localSettings = extend(settings || defaults, options || {}); 435 | 436 | // Before remove error callback 437 | localSettings.beforeRemoveError(field); 438 | 439 | // Remove ARIA role from the field 440 | field.removeAttribute('aria-describedby'); 441 | 442 | // Remove error class to field 443 | field.classList.remove(localSettings.fieldClass); 444 | 445 | // If the field is a radio button and part of a group, remove error from all and get the last item in the group 446 | if (field.type === 'radio' && field.name) { 447 | var group = document.getElementsByName(field.name); 448 | if (group.length > 0) { 449 | for (var i = 0; i < group.length; i++) { 450 | if (group[i].form !== field.form) continue; // Only check fields in current form 451 | group[i].classList.remove(localSettings.fieldClass); 452 | } 453 | field = group[group.length - 1]; 454 | } 455 | } 456 | 457 | // Get field id or name 458 | var id = field.id || field.name; 459 | if (!id) return; 460 | 461 | // Check if an error message is in the DOM 462 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id + ''); 463 | if (!message) return; 464 | 465 | // If so, hide it 466 | message.innerHTML = ''; 467 | message.style.display = 'none'; 468 | message.style.visibility = 'hidden'; 469 | 470 | // After remove error callback 471 | localSettings.afterRemoveError(field); 472 | 473 | }; 474 | 475 | /** 476 | * Add the `novalidate` attribute to all forms 477 | * @private 478 | * @param {Boolean} remove If true, remove the `novalidate` attribute 479 | */ 480 | var addNoValidate = function (remove) { 481 | var forms = document.querySelectorAll(settings.selector); 482 | for (var i = 0; i < forms.length; i++) { 483 | if (remove) { 484 | forms[i].removeAttribute('novalidate'); 485 | continue; 486 | } 487 | forms[i].setAttribute('novalidate', true); 488 | } 489 | }; 490 | 491 | /** 492 | * Check field validity when it loses focus 493 | * @private 494 | * @param {Event} event The blur event 495 | */ 496 | var blurHandler = function (event) { 497 | 498 | // Only run if the field is in a form to be validated 499 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 500 | 501 | // Validate the field 502 | var error = validate.hasError(event.target); 503 | 504 | // If there's an error, show it 505 | if (error) { 506 | validate.showError(event.target, error); 507 | return; 508 | } 509 | 510 | // Otherwise, remove any errors that exist 511 | validate.removeError(event.target); 512 | 513 | }; 514 | 515 | /** 516 | * Check radio and checkbox field validity when clicked 517 | * @private 518 | * @param {Event} event The click event 519 | */ 520 | var clickHandler = function (event) { 521 | 522 | // Only run if the field is in a form to be validated 523 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 524 | 525 | // Only run if the field is a checkbox or radio 526 | var type = event.target.getAttribute('type'); 527 | if (!(type === 'checkbox' || type === 'radio')) return; 528 | 529 | // Validate the field 530 | var error = validate.hasError(event.target); 531 | 532 | // If there's an error, show it 533 | if (error) { 534 | validate.showError(event.target, error); 535 | return; 536 | } 537 | 538 | // Otherwise, remove any errors that exist 539 | validate.removeError(event.target); 540 | 541 | }; 542 | 543 | /** 544 | * Check all fields on submit 545 | * @private 546 | * @param {Event} event The submit event 547 | */ 548 | var submitHandler = function (event) { 549 | 550 | // Only run on forms flagged for validation 551 | if (!event.target.matches(settings.selector)) return; 552 | 553 | // Get all of the form elements 554 | var fields = event.target.elements; 555 | 556 | // Validate each field 557 | // Store the first field with an error to a variable so we can bring it into focus later 558 | var hasErrors; 559 | for (var i = 0; i < fields.length; i++) { 560 | var error = validate.hasError(fields[i]); 561 | if (error) { 562 | validate.showError(fields[i], error); 563 | if (!hasErrors) { 564 | hasErrors = fields[i]; 565 | } 566 | } 567 | } 568 | 569 | // Prevent form from submitting if there are errors or submission is disabled 570 | if (hasErrors || settings.disableSubmit) { 571 | event.preventDefault(); 572 | } 573 | 574 | // If there are errrors, focus on first element with error 575 | if (hasErrors) { 576 | hasErrors.focus(); 577 | return; 578 | } 579 | 580 | // Otherwise, submit the form 581 | settings.onSubmit(event.target, fields); 582 | 583 | }; 584 | 585 | /** 586 | * Destroy the current initialization. 587 | * @public 588 | */ 589 | validate.destroy = function () { 590 | 591 | // If plugin isn't already initialized, stop 592 | if ( !settings ) return; 593 | 594 | // Remove event listeners 595 | document.removeEventListener('blur', blurHandler, true); 596 | document.removeEventListener('click', clickHandler, false); 597 | document.removeEventListener('submit', submitHandler, false); 598 | 599 | // Remove all errors 600 | var fields = document.querySelectorAll(settings.errorClass); 601 | for (var i = 0; i < fields.length; i++) { 602 | validate.removeError(fields[i]); 603 | } 604 | 605 | // Remove `novalidate` from forms 606 | addNoValidate(true); 607 | 608 | // Reset variables 609 | settings = null; 610 | 611 | }; 612 | 613 | /** 614 | * Initialize Validate 615 | * @public 616 | * @param {Object} options User settings 617 | */ 618 | validate.init = function (options) { 619 | 620 | // feature test 621 | if (!supports()) return; 622 | 623 | // Destroy any existing initializations 624 | validate.destroy(); 625 | 626 | // Merge user options with defaults 627 | settings = extend(defaults, options || {}); 628 | 629 | // Add the `novalidate` attribute to all forms 630 | addNoValidate(); 631 | 632 | // Event listeners 633 | document.addEventListener('blur', blurHandler, true); 634 | document.addEventListener('click', clickHandler, true); 635 | document.addEventListener('submit', submitHandler, false); 636 | 637 | }; 638 | 639 | 640 | // 641 | // Public APIs 642 | // 643 | 644 | return validate; 645 | 646 | })); -------------------------------------------------------------------------------- /dist/validate.polyfills.min.js: -------------------------------------------------------------------------------- 1 | /*! validate v2.2.0 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ 2 | !(function(e,t,r){"use strict";var a=(function(){var e=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"validity"),t=Object.getOwnPropertyDescriptor(HTMLButtonElement.prototype,"validity"),r=Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,"validity"),a=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,"validity"),i={};return e&&(i.input=e.get),t&&(i.button=t.get),r&&(i.select=r.get),a&&(i.textarea=a.get),i})(),i=function(e){var r=e.getAttribute("type")||e.nodeName.toLowerCase(),i="number"===r||"range"===r,n=e.value.length,o=!0;if("radio"===e.type&&e.name){var s=t.getElementsByName(e.name);if(s.length>0)for(var l=0;l0&&!/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(e.value),patternMismatch:e.hasAttribute("pattern")&&n>0&&!1===new RegExp(e.getAttribute("pattern")).test(e.value),rangeOverflow:e.hasAttribute("max")&&i&&e.value>0&&Number(e.value)>Number(e.getAttribute("max")),rangeUnderflow:e.hasAttribute("min")&&i&&e.value>0&&Number(e.value)0&&n>parseInt(e.getAttribute("maxLength"),10),tooShort:e.hasAttribute("minLength")&&e.getAttribute("minLength")>0&&n>0&&n0&&("email"===r&&!/^([^\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(e.value)||"url"===r&&!/^(?:(?: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(e.value)),valueMissing:e.hasAttribute("required")&&(("checkbox"===r||"radio"===r)&&!e.hasAttribute("checked")||"select"===r&&(-1===e.selectedIndex||e.options[e.selectedIndex].value.length<1)||"checkbox"!==r&&"radio"!==r&&"select"!==r&&n<1)},c=e.tagName.toLowerCase(),m=c in a?a[c].call(e):{};for(var f in u)u.hasOwnProperty(f)&&(f in m&&m[f]&&(u[f]=!0),u[f]&&(o=!1));return u.valid=o,u};(function(){var e=t.createElement("input");return"validity"in e&&"badInput"in e.validity&&"patternMismatch"in e.validity&&"rangeOverflow"in e.validity&&"rangeUnderflow"in e.validity&&"stepMismatch"in e.validity&&"tooLong"in e.validity&&"tooShort"in e.validity&&"typeMismatch"in e.validity&&"valid"in e.validity&&"valueMissing"in e.validity})()||(Object.defineProperty(HTMLInputElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}),Object.defineProperty(HTMLButtonElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}),Object.defineProperty(HTMLSelectElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}),Object.defineProperty(HTMLTextAreaElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}))})(window,document),(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={selector:"[data-validate]",fieldClass:"error",errorClass:"error-message",messageValueMissing:"Please fill out this field.",messageValueMissingCheckbox:"This field is required.",messageValueMissingRadio:"Please select a value.",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.msMatchesSelector||Element.prototype.webkitMatchesSelector);var i=function(){return"querySelector"in document&&"addEventListener"in e},n=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 u=0;u0){for(var s=0;s 0) { 246 | for (var i = 0; i < group.length; i++) { 247 | if (group[i].form !== field.form) continue; // Only check fields in current form 248 | group[i].classList.add(localSettings.fieldClass); 249 | } 250 | field = group[group.length - 1]; 251 | } 252 | } 253 | 254 | // Get field id or name 255 | var id = field.id || field.name; 256 | if (!id) return; 257 | 258 | // Check if error message field already exists 259 | // If not, create one 260 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id ); 261 | if (!message) { 262 | message = document.createElement('div'); 263 | message.className = localSettings.errorClass; 264 | message.id = 'error-for-' + id; 265 | 266 | // If the field is a radio button or checkbox, insert error after the label 267 | var label; 268 | if (field.type === 'radio' || field.type ==='checkbox') { 269 | label = field.form.querySelector('label[for="' + id + '"]') || getClosest(field, 'label'); 270 | if (label) { 271 | label.parentNode.insertBefore( message, label.nextSibling ); 272 | } 273 | } 274 | 275 | // Otherwise, insert it after the field 276 | if (!label) { 277 | field.parentNode.insertBefore( message, field.nextSibling ); 278 | } 279 | } 280 | 281 | // Add ARIA role to the field 282 | field.setAttribute('aria-describedby', 'error-for-' + id); 283 | 284 | // Update error message 285 | message.innerHTML = error; 286 | 287 | // Remove any existing styles hiding the error message 288 | message.style.display = ''; 289 | message.style.visibility = ''; 290 | 291 | // After show error callback 292 | localSettings.afterShowError(field, error); 293 | 294 | }; 295 | 296 | /** 297 | * Remove an error message from a field 298 | * @public 299 | * @param {Node} field The field to remove the error from 300 | * @param {Object} options User options 301 | */ 302 | validate.removeError = function (field, options) { 303 | 304 | // Merge user options with existing settings or defaults 305 | var localSettings = extend(settings || defaults, options || {}); 306 | 307 | // Before remove error callback 308 | localSettings.beforeRemoveError(field); 309 | 310 | // Remove ARIA role from the field 311 | field.removeAttribute('aria-describedby'); 312 | 313 | // Remove error class to field 314 | field.classList.remove(localSettings.fieldClass); 315 | 316 | // If the field is a radio button and part of a group, remove error from all and get the last item in the group 317 | if (field.type === 'radio' && field.name) { 318 | var group = document.getElementsByName(field.name); 319 | if (group.length > 0) { 320 | for (var i = 0; i < group.length; i++) { 321 | if (group[i].form !== field.form) continue; // Only check fields in current form 322 | group[i].classList.remove(localSettings.fieldClass); 323 | } 324 | field = group[group.length - 1]; 325 | } 326 | } 327 | 328 | // Get field id or name 329 | var id = field.id || field.name; 330 | if (!id) return; 331 | 332 | // Check if an error message is in the DOM 333 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id + ''); 334 | if (!message) return; 335 | 336 | // If so, hide it 337 | message.innerHTML = ''; 338 | message.style.display = 'none'; 339 | message.style.visibility = 'hidden'; 340 | 341 | // After remove error callback 342 | localSettings.afterRemoveError(field); 343 | 344 | }; 345 | 346 | /** 347 | * Add the `novalidate` attribute to all forms 348 | * @private 349 | * @param {Boolean} remove If true, remove the `novalidate` attribute 350 | */ 351 | var addNoValidate = function (remove) { 352 | var forms = document.querySelectorAll(settings.selector); 353 | for (var i = 0; i < forms.length; i++) { 354 | if (remove) { 355 | forms[i].removeAttribute('novalidate'); 356 | continue; 357 | } 358 | forms[i].setAttribute('novalidate', true); 359 | } 360 | }; 361 | 362 | /** 363 | * Check field validity when it loses focus 364 | * @private 365 | * @param {Event} event The blur event 366 | */ 367 | var blurHandler = function (event) { 368 | 369 | // Only run if the field is in a form to be validated 370 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 371 | 372 | // Validate the field 373 | var error = validate.hasError(event.target); 374 | 375 | // If there's an error, show it 376 | if (error) { 377 | validate.showError(event.target, error); 378 | return; 379 | } 380 | 381 | // Otherwise, remove any errors that exist 382 | validate.removeError(event.target); 383 | 384 | }; 385 | 386 | /** 387 | * Check radio and checkbox field validity when clicked 388 | * @private 389 | * @param {Event} event The click event 390 | */ 391 | var clickHandler = function (event) { 392 | 393 | // Only run if the field is in a form to be validated 394 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 395 | 396 | // Only run if the field is a checkbox or radio 397 | var type = event.target.getAttribute('type'); 398 | if (!(type === 'checkbox' || type === 'radio')) return; 399 | 400 | // Validate the field 401 | var error = validate.hasError(event.target); 402 | 403 | // If there's an error, show it 404 | if (error) { 405 | validate.showError(event.target, error); 406 | return; 407 | } 408 | 409 | // Otherwise, remove any errors that exist 410 | validate.removeError(event.target); 411 | 412 | }; 413 | 414 | /** 415 | * Check all fields on submit 416 | * @private 417 | * @param {Event} event The submit event 418 | */ 419 | var submitHandler = function (event) { 420 | 421 | // Only run on forms flagged for validation 422 | if (!event.target.matches(settings.selector)) return; 423 | 424 | // Get all of the form elements 425 | var fields = event.target.elements; 426 | 427 | // Validate each field 428 | // Store the first field with an error to a variable so we can bring it into focus later 429 | var hasErrors; 430 | for (var i = 0; i < fields.length; i++) { 431 | var error = validate.hasError(fields[i]); 432 | if (error) { 433 | validate.showError(fields[i], error); 434 | if (!hasErrors) { 435 | hasErrors = fields[i]; 436 | } 437 | } 438 | } 439 | 440 | // Prevent form from submitting if there are errors or submission is disabled 441 | if (hasErrors || settings.disableSubmit) { 442 | event.preventDefault(); 443 | } 444 | 445 | // If there are errrors, focus on first element with error 446 | if (hasErrors) { 447 | hasErrors.focus(); 448 | return; 449 | } 450 | 451 | // Otherwise, submit the form 452 | settings.onSubmit(event.target, fields); 453 | 454 | }; 455 | 456 | /** 457 | * Destroy the current initialization. 458 | * @public 459 | */ 460 | validate.destroy = function () { 461 | 462 | // If plugin isn't already initialized, stop 463 | if ( !settings ) return; 464 | 465 | // Remove event listeners 466 | document.removeEventListener('blur', blurHandler, true); 467 | document.removeEventListener('click', clickHandler, false); 468 | document.removeEventListener('submit', submitHandler, false); 469 | 470 | // Remove all errors 471 | var fields = document.querySelectorAll(settings.errorClass); 472 | for (var i = 0; i < fields.length; i++) { 473 | validate.removeError(fields[i]); 474 | } 475 | 476 | // Remove `novalidate` from forms 477 | addNoValidate(true); 478 | 479 | // Reset variables 480 | settings = null; 481 | 482 | }; 483 | 484 | /** 485 | * Initialize Validate 486 | * @public 487 | * @param {Object} options User settings 488 | */ 489 | validate.init = function (options) { 490 | 491 | // feature test 492 | if (!supports()) return; 493 | 494 | // Destroy any existing initializations 495 | validate.destroy(); 496 | 497 | // Merge user options with defaults 498 | settings = extend(defaults, options || {}); 499 | 500 | // Add the `novalidate` attribute to all forms 501 | addNoValidate(); 502 | 503 | // Event listeners 504 | document.addEventListener('blur', blurHandler, true); 505 | document.addEventListener('click', clickHandler, true); 506 | document.addEventListener('submit', submitHandler, false); 507 | 508 | }; 509 | 510 | 511 | // 512 | // Public APIs 513 | // 514 | 515 | return validate; 516 | 517 | })); -------------------------------------------------------------------------------- /docs/dist/validate.min.js: -------------------------------------------------------------------------------- 1 | /*! validate v2.2.0 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ 2 | !(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={selector:"[data-validate]",fieldClass:"error",errorClass:"error-message",messageValueMissing:"Please fill out this field.",messageValueMissingCheckbox:"This field is required.",messageValueMissingRadio:"Please select a value.",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.msMatchesSelector||Element.prototype.webkitMatchesSelector);var s=function(){return"querySelector"in document&&"addEventListener"in e},o=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) { 59 | for (var i = 0; i < group.length; i++) { 60 | if (group[i].form === field.form && field.checked) { 61 | field = group[i]; 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | 68 | // Run validity checks 69 | var checkValidity = { 70 | badInput: (isNum && length > 0 && !/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(field.value)), // value of a number field is not a number 71 | patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern 72 | 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 73 | 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 74 | 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 75 | 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 76 | 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 77 | 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 78 | 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 79 | }; 80 | 81 | // Run browser's own validation if available 82 | var fieldTagName = field.tagName.toLowerCase(); 83 | var browserValidity = fieldTagName in browserValidityFunctions ? browserValidityFunctions[fieldTagName].call(field) : {}; 84 | 85 | // Check if any errors 86 | for (var key in checkValidity) { 87 | if (checkValidity.hasOwnProperty(key)) { 88 | // If browser has detected an error, adopt it to our validity object 89 | if (key in browserValidity && browserValidity[key]) { 90 | checkValidity[key] = true; 91 | } 92 | 93 | // If there's an error, change valid value 94 | if (checkValidity[key]) { 95 | valid = false; 96 | } 97 | } 98 | } 99 | 100 | // Add valid property to validity object 101 | checkValidity.valid = valid; 102 | 103 | // Return object 104 | return checkValidity; 105 | 106 | }; 107 | 108 | // If the full set of ValidityState features aren't supported, polyfill 109 | if (!supported()) { 110 | Object.defineProperty(HTMLInputElement.prototype, 'validity', { 111 | get: function ValidityState() { 112 | return getValidityState(this); 113 | }, 114 | configurable: true, 115 | }); 116 | Object.defineProperty(HTMLButtonElement.prototype, 'validity', { 117 | get: function ValidityState() { 118 | return getValidityState(this); 119 | }, 120 | configurable: true, 121 | }); 122 | Object.defineProperty(HTMLSelectElement.prototype, 'validity', { 123 | get: function ValidityState() { 124 | return getValidityState(this); 125 | }, 126 | configurable: true, 127 | }); 128 | Object.defineProperty(HTMLTextAreaElement.prototype, 'validity', { 129 | get: function ValidityState() { 130 | return getValidityState(this); 131 | }, 132 | configurable: true, 133 | }); 134 | } 135 | 136 | })(window, document); 137 | (function (root, factory) { 138 | if ( typeof define === 'function' && define.amd ) { 139 | define([], factory(root)); 140 | } else if ( typeof exports === 'object' ) { 141 | module.exports = factory(root); 142 | } else { 143 | root.validate = factory(root); 144 | } 145 | })(typeof global !== 'undefined' ? global : this.window || this.global, (function (root) { 146 | 147 | 'use strict'; 148 | 149 | // 150 | // Variables 151 | // 152 | 153 | var validate = {}; // Object for public APIs 154 | var settings; 155 | 156 | // Default settings 157 | var defaults = { 158 | 159 | // Classes and Selectors 160 | selector: '[data-validate]', 161 | fieldClass: 'error', 162 | errorClass: 'error-message', 163 | 164 | // Messages 165 | messageValueMissing: 'Please fill out this field.', 166 | messageValueMissingCheckbox: 'This field is required.', 167 | messageValueMissingRadio: 'Please select a value.', 168 | messageValueMissingSelect: 'Please select a value.', 169 | messageValueMissingSelectMulti: 'Please select at least one value.', 170 | messageTypeMismatchEmail: 'Please enter an email address.', 171 | messageTypeMismatchURL: 'Please enter a URL.', 172 | messageTooShort: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.', 173 | messageTooLong: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.', 174 | messagePatternMismatch: 'Please match the requested format.', 175 | messageBadInput: 'Please enter a number.', 176 | messageStepMismatch: 'Please select a valid value.', 177 | messageRangeOverflow: 'Please select a value that is no more than {max}.', 178 | messageRangeUnderflow: 'Please select a value that is no less than {min}.', 179 | messageGeneric: 'The value you entered for this field is invalid.', 180 | 181 | // Form Submission 182 | disableSubmit: false, 183 | onSubmit: function () {}, 184 | 185 | // Callbacks 186 | beforeShowError: function () {}, 187 | afterShowError: function () {}, 188 | beforeRemoveError: function () {}, 189 | afterRemoveError: function () {} 190 | 191 | }; 192 | 193 | 194 | // 195 | // Methods 196 | // 197 | 198 | /** 199 | * Element.matches() polyfill (simple version) 200 | * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill 201 | */ 202 | if (!Element.prototype.matches) { 203 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 204 | } 205 | 206 | /** 207 | * Feature test 208 | * @return {Boolean} Returns true if required methods and APIs are supported by the browser 209 | */ 210 | var supports = function () { 211 | return 'querySelector' in document && 'addEventListener' in root; 212 | }; 213 | 214 | /** 215 | * Merge two or more objects. Returns a new object. 216 | * @private 217 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 218 | * @param {Object} objects The objects to merge together 219 | * @returns {Object} Merged values of defaults and options 220 | */ 221 | var extend = function () { 222 | 223 | // Variables 224 | var extended = {}; 225 | var deep = false; 226 | var i = 0; 227 | var length = arguments.length; 228 | 229 | // Check if a deep merge 230 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 231 | deep = arguments[0]; 232 | i++; 233 | } 234 | 235 | // Merge the object into the extended object 236 | var merge = function (obj) { 237 | for ( var prop in obj ) { 238 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 239 | // If deep merge and property is an object, merge properties 240 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 241 | extended[prop] = extend( true, extended[prop], obj[prop] ); 242 | } else { 243 | extended[prop] = obj[prop]; 244 | } 245 | } 246 | } 247 | }; 248 | 249 | // Loop through each object and conduct a merge 250 | for ( ; i < length; i++ ) { 251 | var obj = arguments[i]; 252 | merge(obj); 253 | } 254 | 255 | return extended; 256 | 257 | }; 258 | 259 | /** 260 | * Get the closest matching element up the DOM tree. 261 | * @private 262 | * @param {Element} elem Starting element 263 | * @param {String} selector Selector to match against 264 | * @return {Boolean|Element} Returns null if not match found 265 | */ 266 | var getClosest = function ( elem, selector ) { 267 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 268 | if ( elem.matches( selector ) ) return elem; 269 | } 270 | return null; 271 | }; 272 | 273 | /** 274 | * Validate a form field 275 | * @public 276 | * @param {Node} field The field to validate 277 | * @param {Object} options User options 278 | * @return {String} The error message 279 | */ 280 | validate.hasError = function (field, options) { 281 | 282 | // Merge user options with existing settings or defaults 283 | var localSettings = extend(settings || defaults, options || {}); 284 | 285 | // Don't validate submits, buttons, file and reset inputs, and disabled fields 286 | if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return; 287 | 288 | // Get validity 289 | var validity = field.validity; 290 | 291 | // If valid, return null 292 | if (validity.valid) return; 293 | 294 | // If field is required and empty 295 | if (validity.valueMissing) { 296 | 297 | if (field.type === 'checkbox') return localSettings.messageValueMissingCheckbox; 298 | 299 | if (field.type === 'radio') return localSettings.messageValueMissingRadio; 300 | 301 | if (field.type === 'select-multiple') return localSettings.messageValueMissingSelectMulti; 302 | 303 | if (field.type === 'select-one') return localSettings.messageValueMissingSelect; 304 | 305 | return localSettings.messageValueMissing; 306 | } 307 | 308 | // If not the right type 309 | if (validity.typeMismatch) { 310 | 311 | // Email 312 | if (field.type === 'email') return localSettings.messageTypeMismatchEmail; 313 | 314 | // URL 315 | if (field.type === 'url') return localSettings.messageTypeMismatchURL; 316 | 317 | } 318 | 319 | // If too short 320 | if (validity.tooShort) return localSettings.messageTooShort.replace('{minLength}', field.getAttribute('minLength')).replace('{length}', field.value.length); 321 | 322 | // If too long 323 | if (validity.tooLong) return localSettings.messageTooLong.replace('{minLength}', field.getAttribute('maxLength')).replace('{length}', field.value.length); 324 | 325 | // If number input isn't a number 326 | if (validity.badInput) return localSettings.messageBadInput; 327 | 328 | // If a number value doesn't match the step interval 329 | if (validity.stepMismatch) return localSettings.messageStepMismatch; 330 | 331 | // If a number field is over the max 332 | if (validity.rangeOverflow) return localSettings.messageRangeOverflow.replace('{max}', field.getAttribute('max')); 333 | 334 | // If a number field is below the min 335 | if (validity.rangeUnderflow) return localSettings.messageRangeUnderflow.replace('{min}', field.getAttribute('min')); 336 | 337 | // If pattern doesn't match 338 | if (validity.patternMismatch) { 339 | 340 | // If pattern info is included, return custom error 341 | if (field.hasAttribute('title')) return field.getAttribute('title'); 342 | 343 | // Otherwise, generic error 344 | return localSettings.messagePatternMismatch; 345 | 346 | } 347 | 348 | // If all else fails, return a generic catchall error 349 | return localSettings.messageGeneric; 350 | 351 | }; 352 | 353 | /** 354 | * Show an error message on a field 355 | * @public 356 | * @param {Node} field The field to show an error message for 357 | * @param {String} error The error message to show 358 | * @param {Object} options User options 359 | */ 360 | validate.showError = function (field, error, options) { 361 | 362 | // Merge user options with existing settings or defaults 363 | var localSettings = extend(settings || defaults, options || {}); 364 | 365 | // Before show error callback 366 | localSettings.beforeShowError(field, error); 367 | 368 | // Add error class to field 369 | field.classList.add(localSettings.fieldClass); 370 | 371 | // If the field is a radio button and part of a group, error all and get the last item in the group 372 | if (field.type === 'radio' && field.name) { 373 | var group = document.getElementsByName(field.name); 374 | if (group.length > 0) { 375 | for (var i = 0; i < group.length; i++) { 376 | if (group[i].form !== field.form) continue; // Only check fields in current form 377 | group[i].classList.add(localSettings.fieldClass); 378 | } 379 | field = group[group.length - 1]; 380 | } 381 | } 382 | 383 | // Get field id or name 384 | var id = field.id || field.name; 385 | if (!id) return; 386 | 387 | // Check if error message field already exists 388 | // If not, create one 389 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id ); 390 | if (!message) { 391 | message = document.createElement('div'); 392 | message.className = localSettings.errorClass; 393 | message.id = 'error-for-' + id; 394 | 395 | // If the field is a radio button or checkbox, insert error after the label 396 | var label; 397 | if (field.type === 'radio' || field.type ==='checkbox') { 398 | label = field.form.querySelector('label[for="' + id + '"]') || getClosest(field, 'label'); 399 | if (label) { 400 | label.parentNode.insertBefore( message, label.nextSibling ); 401 | } 402 | } 403 | 404 | // Otherwise, insert it after the field 405 | if (!label) { 406 | field.parentNode.insertBefore( message, field.nextSibling ); 407 | } 408 | } 409 | 410 | // Add ARIA role to the field 411 | field.setAttribute('aria-describedby', 'error-for-' + id); 412 | 413 | // Update error message 414 | message.innerHTML = error; 415 | 416 | // Remove any existing styles hiding the error message 417 | message.style.display = ''; 418 | message.style.visibility = ''; 419 | 420 | // After show error callback 421 | localSettings.afterShowError(field, error); 422 | 423 | }; 424 | 425 | /** 426 | * Remove an error message from a field 427 | * @public 428 | * @param {Node} field The field to remove the error from 429 | * @param {Object} options User options 430 | */ 431 | validate.removeError = function (field, options) { 432 | 433 | // Merge user options with existing settings or defaults 434 | var localSettings = extend(settings || defaults, options || {}); 435 | 436 | // Before remove error callback 437 | localSettings.beforeRemoveError(field); 438 | 439 | // Remove ARIA role from the field 440 | field.removeAttribute('aria-describedby'); 441 | 442 | // Remove error class to field 443 | field.classList.remove(localSettings.fieldClass); 444 | 445 | // If the field is a radio button and part of a group, remove error from all and get the last item in the group 446 | if (field.type === 'radio' && field.name) { 447 | var group = document.getElementsByName(field.name); 448 | if (group.length > 0) { 449 | for (var i = 0; i < group.length; i++) { 450 | if (group[i].form !== field.form) continue; // Only check fields in current form 451 | group[i].classList.remove(localSettings.fieldClass); 452 | } 453 | field = group[group.length - 1]; 454 | } 455 | } 456 | 457 | // Get field id or name 458 | var id = field.id || field.name; 459 | if (!id) return; 460 | 461 | // Check if an error message is in the DOM 462 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id + ''); 463 | if (!message) return; 464 | 465 | // If so, hide it 466 | message.innerHTML = ''; 467 | message.style.display = 'none'; 468 | message.style.visibility = 'hidden'; 469 | 470 | // After remove error callback 471 | localSettings.afterRemoveError(field); 472 | 473 | }; 474 | 475 | /** 476 | * Add the `novalidate` attribute to all forms 477 | * @private 478 | * @param {Boolean} remove If true, remove the `novalidate` attribute 479 | */ 480 | var addNoValidate = function (remove) { 481 | var forms = document.querySelectorAll(settings.selector); 482 | for (var i = 0; i < forms.length; i++) { 483 | if (remove) { 484 | forms[i].removeAttribute('novalidate'); 485 | continue; 486 | } 487 | forms[i].setAttribute('novalidate', true); 488 | } 489 | }; 490 | 491 | /** 492 | * Check field validity when it loses focus 493 | * @private 494 | * @param {Event} event The blur event 495 | */ 496 | var blurHandler = function (event) { 497 | 498 | // Only run if the field is in a form to be validated 499 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 500 | 501 | // Validate the field 502 | var error = validate.hasError(event.target); 503 | 504 | // If there's an error, show it 505 | if (error) { 506 | validate.showError(event.target, error); 507 | return; 508 | } 509 | 510 | // Otherwise, remove any errors that exist 511 | validate.removeError(event.target); 512 | 513 | }; 514 | 515 | /** 516 | * Check radio and checkbox field validity when clicked 517 | * @private 518 | * @param {Event} event The click event 519 | */ 520 | var clickHandler = function (event) { 521 | 522 | // Only run if the field is in a form to be validated 523 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 524 | 525 | // Only run if the field is a checkbox or radio 526 | var type = event.target.getAttribute('type'); 527 | if (!(type === 'checkbox' || type === 'radio')) return; 528 | 529 | // Validate the field 530 | var error = validate.hasError(event.target); 531 | 532 | // If there's an error, show it 533 | if (error) { 534 | validate.showError(event.target, error); 535 | return; 536 | } 537 | 538 | // Otherwise, remove any errors that exist 539 | validate.removeError(event.target); 540 | 541 | }; 542 | 543 | /** 544 | * Check all fields on submit 545 | * @private 546 | * @param {Event} event The submit event 547 | */ 548 | var submitHandler = function (event) { 549 | 550 | // Only run on forms flagged for validation 551 | if (!event.target.matches(settings.selector)) return; 552 | 553 | // Get all of the form elements 554 | var fields = event.target.elements; 555 | 556 | // Validate each field 557 | // Store the first field with an error to a variable so we can bring it into focus later 558 | var hasErrors; 559 | for (var i = 0; i < fields.length; i++) { 560 | var error = validate.hasError(fields[i]); 561 | if (error) { 562 | validate.showError(fields[i], error); 563 | if (!hasErrors) { 564 | hasErrors = fields[i]; 565 | } 566 | } 567 | } 568 | 569 | // Prevent form from submitting if there are errors or submission is disabled 570 | if (hasErrors || settings.disableSubmit) { 571 | event.preventDefault(); 572 | } 573 | 574 | // If there are errrors, focus on first element with error 575 | if (hasErrors) { 576 | hasErrors.focus(); 577 | return; 578 | } 579 | 580 | // Otherwise, submit the form 581 | settings.onSubmit(event.target, fields); 582 | 583 | }; 584 | 585 | /** 586 | * Destroy the current initialization. 587 | * @public 588 | */ 589 | validate.destroy = function () { 590 | 591 | // If plugin isn't already initialized, stop 592 | if ( !settings ) return; 593 | 594 | // Remove event listeners 595 | document.removeEventListener('blur', blurHandler, true); 596 | document.removeEventListener('click', clickHandler, false); 597 | document.removeEventListener('submit', submitHandler, false); 598 | 599 | // Remove all errors 600 | var fields = document.querySelectorAll(settings.errorClass); 601 | for (var i = 0; i < fields.length; i++) { 602 | validate.removeError(fields[i]); 603 | } 604 | 605 | // Remove `novalidate` from forms 606 | addNoValidate(true); 607 | 608 | // Reset variables 609 | settings = null; 610 | 611 | }; 612 | 613 | /** 614 | * Initialize Validate 615 | * @public 616 | * @param {Object} options User settings 617 | */ 618 | validate.init = function (options) { 619 | 620 | // feature test 621 | if (!supports()) return; 622 | 623 | // Destroy any existing initializations 624 | validate.destroy(); 625 | 626 | // Merge user options with defaults 627 | settings = extend(defaults, options || {}); 628 | 629 | // Add the `novalidate` attribute to all forms 630 | addNoValidate(); 631 | 632 | // Event listeners 633 | document.addEventListener('blur', blurHandler, true); 634 | document.addEventListener('click', clickHandler, true); 635 | document.addEventListener('submit', submitHandler, false); 636 | 637 | }; 638 | 639 | 640 | // 641 | // Public APIs 642 | // 643 | 644 | return validate; 645 | 646 | })); -------------------------------------------------------------------------------- /docs/dist/validate.polyfills.min.js: -------------------------------------------------------------------------------- 1 | /*! validate v2.2.0 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ 2 | !(function(e,t,r){"use strict";var a=(function(){var e=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"validity"),t=Object.getOwnPropertyDescriptor(HTMLButtonElement.prototype,"validity"),r=Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,"validity"),a=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,"validity"),i={};return e&&(i.input=e.get),t&&(i.button=t.get),r&&(i.select=r.get),a&&(i.textarea=a.get),i})(),i=function(e){var r=e.getAttribute("type")||e.nodeName.toLowerCase(),i="number"===r||"range"===r,n=e.value.length,o=!0;if("radio"===e.type&&e.name){var s=t.getElementsByName(e.name);if(s.length>0)for(var l=0;l0&&!/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(e.value),patternMismatch:e.hasAttribute("pattern")&&n>0&&!1===new RegExp(e.getAttribute("pattern")).test(e.value),rangeOverflow:e.hasAttribute("max")&&i&&e.value>0&&Number(e.value)>Number(e.getAttribute("max")),rangeUnderflow:e.hasAttribute("min")&&i&&e.value>0&&Number(e.value)0&&n>parseInt(e.getAttribute("maxLength"),10),tooShort:e.hasAttribute("minLength")&&e.getAttribute("minLength")>0&&n>0&&n0&&("email"===r&&!/^([^\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(e.value)||"url"===r&&!/^(?:(?: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(e.value)),valueMissing:e.hasAttribute("required")&&(("checkbox"===r||"radio"===r)&&!e.hasAttribute("checked")||"select"===r&&(-1===e.selectedIndex||e.options[e.selectedIndex].value.length<1)||"checkbox"!==r&&"radio"!==r&&"select"!==r&&n<1)},c=e.tagName.toLowerCase(),m=c in a?a[c].call(e):{};for(var f in u)u.hasOwnProperty(f)&&(f in m&&m[f]&&(u[f]=!0),u[f]&&(o=!1));return u.valid=o,u};(function(){var e=t.createElement("input");return"validity"in e&&"badInput"in e.validity&&"patternMismatch"in e.validity&&"rangeOverflow"in e.validity&&"rangeUnderflow"in e.validity&&"stepMismatch"in e.validity&&"tooLong"in e.validity&&"tooShort"in e.validity&&"typeMismatch"in e.validity&&"valid"in e.validity&&"valueMissing"in e.validity})()||(Object.defineProperty(HTMLInputElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}),Object.defineProperty(HTMLButtonElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}),Object.defineProperty(HTMLSelectElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}),Object.defineProperty(HTMLTextAreaElement.prototype,"validity",{get:function(){return i(this)},configurable:!0}))})(window,document),(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={selector:"[data-validate]",fieldClass:"error",errorClass:"error-message",messageValueMissing:"Please fill out this field.",messageValueMissingCheckbox:"This field is required.",messageValueMissingRadio:"Please select a value.",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.msMatchesSelector||Element.prototype.webkitMatchesSelector);var i=function(){return"querySelector"in document&&"addEventListener"in e},n=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 u=0;u0){for(var s=0;s 2 | 3 | 4 | 5 | 6 | Validate.js 7 | 8 | 9 | 10 | 102 | 103 | 104 | 105 | 106 |
107 | 108 | 113 | 114 |
115 |
116 |
117 | 118 | 119 |
120 | 121 |
122 | 123 | 124 |
125 | 126 |
127 | 128 | 129 |
130 | 131 |
132 | 133 | 134 |
135 | 136 |
137 | 138 | 139 | 140 |
141 | 142 |
143 | 144 | 145 | 146 |
147 | 148 |
149 | 150 | 151 | 152 |
153 | 154 |
155 | 156 | 157 |
158 | 159 |
160 | 161 | 162 |
163 | 164 |
165 | 166 | 167 |
168 | 169 |
170 | 171 | 172 |
173 | 174 |
175 | 176 | 177 |
178 | 179 |
180 | 181 | 182 |
183 | 184 |
185 | 186 | 193 |
194 | 195 |
196 | Radio Buttons 197 | 201 | 205 |
206 | 207 |
208 | Checkboxes 209 | 213 | 217 | 221 | 225 |
226 | 227 | 228 |
229 |
230 |
231 | 232 | 233 |

234 |
235 |
236 |

Want to learn how to write your own vanilla JS plugins? Get my free daily developer tips and level-up as a web developer. 🚀

237 |
238 | 239 | 240 | 241 | 242 | 246 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Settings 3 | */ 4 | 5 | var settings = { 6 | scripts: true, // Turn on/off script tasks 7 | polyfills: true, // Turn on/off polyfill tasks 8 | styles: false, // Turn on/off style tasks 9 | svgs: false, // Turn on/off SVG tasks 10 | images: false, // Turn on/off image tasks 11 | static: false, // Turn on/off static file copying 12 | docs: true, // Turn on/off documentation generation 13 | deploy: false, // Turn on/off all deployment tasks 14 | cacheBust: false, // Turn on/off cache busting (adds a version number to minified files) 15 | }; 16 | 17 | 18 | /** 19 | * Gulp Packages 20 | */ 21 | 22 | // General 23 | var gulp = require('gulp'); 24 | var fs = require('fs'); 25 | var del = require('del'); 26 | var exec = require('child_process').exec; 27 | var lazypipe = require('lazypipe'); 28 | var plumber = require('gulp-plumber'); 29 | var flatten = require('gulp-flatten'); 30 | var tap = require('gulp-tap'); 31 | var rename = require('gulp-rename'); 32 | var header = require('gulp-header'); 33 | var footer = require('gulp-footer'); 34 | var watch = require('gulp-watch'); 35 | var livereload = require('gulp-livereload'); 36 | var package = require('./package.json'); 37 | 38 | // Scripts 39 | var jshint = settings.scripts ? require('gulp-jshint') : null; 40 | var stylish = settings.scripts ? require('jshint-stylish') : null; 41 | var concat = settings.scripts ? require('gulp-concat') : null; 42 | var uglify = settings.scripts ? require('gulp-uglify') : null; 43 | var optimizejs = settings.scripts ? require('gulp-optimize-js') : null; 44 | 45 | // Styles 46 | var sass = settings.styles ? require('gulp-sass') : null; 47 | var prefix = settings.styles ? require('gulp-autoprefixer') : null; 48 | var minify = settings.styles ? require('gulp-cssnano') : null; 49 | 50 | // SVGs 51 | var svgmin = settings.svgs ? require('gulp-svgmin') : null; 52 | var svgstore = settings.svgs ? require('gulp-svgstore') : null; 53 | 54 | // Docs 55 | var markdown = settings.docs ? require('gulp-markdown') : null; 56 | var fileinclude = settings.docs ? require('gulp-file-include') : null; 57 | 58 | 59 | /** 60 | * Paths to project folders 61 | */ 62 | 63 | var paths = { 64 | input: 'src/**/*', 65 | output: 'dist/', 66 | scripts: { 67 | input: 'src/js/*', 68 | polyfills: '!src/js/*.polyfill.js', 69 | output: 'dist/' 70 | }, 71 | styles: { 72 | input: 'src/sass/**/*.{scss,sass}', 73 | output: 'dist/css/' 74 | }, 75 | svgs: { 76 | input: 'src/svg/*', 77 | output: 'dist/svg/' 78 | }, 79 | images: { 80 | input: 'src/img/*', 81 | output: 'dist/img/' 82 | }, 83 | static: { 84 | input: 'src/static/*', 85 | output: 'dist/' 86 | }, 87 | docs: { 88 | input: 'src/docs/*.{html,md,markdown}', 89 | output: 'docs/', 90 | templates: 'src/docs/_templates/', 91 | assets: 'src/docs/assets/**' 92 | } 93 | }; 94 | 95 | 96 | /** 97 | * Template for banner to add to file headers 98 | */ 99 | 100 | var banner = { 101 | full : 102 | '/*!\n' + 103 | ' * <%= package.name %> v<%= package.version %>: <%= package.description %>\n' + 104 | ' * (c) ' + new Date().getFullYear() + ' <%= package.author.name %>\n' + 105 | ' * <%= package.license %> License\n' + 106 | ' * <%= package.repository.url %>\n' + 107 | ' */\n\n', 108 | min : 109 | '/*!' + 110 | ' <%= package.name %> v<%= package.version %>' + 111 | ' | (c) ' + new Date().getFullYear() + ' <%= package.author.name %>' + 112 | ' | <%= package.license %> License' + 113 | ' | <%= package.repository.url %>' + 114 | ' */\n' 115 | }; 116 | 117 | 118 | /** 119 | * File Version 120 | */ 121 | 122 | var fileVersion = settings.cacheBust ? '.' + package.version : ''; 123 | 124 | 125 | /** 126 | * Gulp Tasks 127 | */ 128 | 129 | var jsTasks = lazypipe() 130 | .pipe(header, banner.full, { package : package }) 131 | .pipe(optimizejs) 132 | .pipe(gulp.dest, paths.scripts.output) 133 | .pipe(rename, { suffix: '.min' + fileVersion }) 134 | .pipe(uglify) 135 | .pipe(optimizejs) 136 | .pipe(header, banner.min, { package : package }) 137 | .pipe(gulp.dest, paths.scripts.output); 138 | 139 | // Lint, minify, and concatenate scripts 140 | gulp.task('build:scripts', ['clean:dist'], function() { 141 | if ( !settings.scripts ) return; 142 | 143 | return gulp.src([paths.scripts.input, paths.scripts.polyfills]) 144 | .pipe(plumber()) 145 | .pipe(tap(function (file, t) { 146 | if ( file.isDirectory() ) { 147 | var name = file.relative + '.js'; 148 | return gulp.src(file.path + '/*.js') 149 | .pipe(concat(name)) 150 | .pipe(jsTasks()); 151 | } 152 | })) 153 | .pipe(jsTasks()); 154 | }); 155 | 156 | // Create scripts with polyfills 157 | gulp.task('build:polyfills', ['clean:dist'], function() { 158 | if ( !settings.polyfills ) return; 159 | 160 | return gulp.src(paths.scripts.input) 161 | .pipe(plumber()) 162 | .pipe(concat(package.name + '.js')) 163 | .pipe(rename({ 164 | suffix: ".polyfills" 165 | })) 166 | .pipe(jsTasks()); 167 | }); 168 | 169 | // Process, lint, and minify Sass files 170 | gulp.task('build:styles', ['clean:dist'], function() { 171 | if ( !settings.styles ) return; 172 | 173 | return gulp.src(paths.styles.input) 174 | .pipe(plumber()) 175 | .pipe(sass({ 176 | outputStyle: 'expanded', 177 | sourceComments: true 178 | })) 179 | .pipe(flatten()) 180 | .pipe(prefix({ 181 | browsers: ['last 2 version', '> 1%'], 182 | cascade: true, 183 | remove: true 184 | })) 185 | .pipe(header(banner.full, { package : package })) 186 | .pipe(gulp.dest(paths.styles.output)) 187 | .pipe(rename({ suffix: '.min' + fileVersion })) 188 | .pipe(minify({ 189 | discardComments: { 190 | removeAll: true 191 | } 192 | })) 193 | .pipe(header(banner.min, { package : package })) 194 | .pipe(gulp.dest(paths.styles.output)); 195 | }); 196 | 197 | // Generate SVG sprites 198 | gulp.task('build:svgs', ['clean:dist'], function () { 199 | if ( !settings.svgs ) return; 200 | 201 | return gulp.src(paths.svgs.input) 202 | .pipe(plumber()) 203 | .pipe(tap(function (file, t) { 204 | if ( file.isDirectory() ) { 205 | var name = file.relative + '.svg'; 206 | return gulp.src(file.path + '/*.svg') 207 | .pipe(svgmin()) 208 | .pipe(svgstore({ 209 | fileName: name, 210 | prefix: 'icon-', 211 | inlineSvg: true 212 | })) 213 | .pipe(gulp.dest(paths.svgs.output)); 214 | } 215 | })) 216 | .pipe(svgmin()) 217 | .pipe(gulp.dest(paths.svgs.output)); 218 | }); 219 | 220 | // Copy image files into output folder 221 | gulp.task('build:images', ['clean:dist'], function() { 222 | if ( !settings.images ) return; 223 | 224 | return gulp.src(paths.images.input) 225 | .pipe(plumber()) 226 | .pipe(gulp.dest(paths.images.output)); 227 | }); 228 | 229 | // Copy static files into output folder 230 | gulp.task('build:static', ['clean:dist'], function() { 231 | if ( !settings.static ) return; 232 | 233 | return gulp.src(paths.static.input) 234 | .pipe(plumber()) 235 | .pipe(gulp.dest(paths.static.output)); 236 | }); 237 | 238 | // Lint scripts 239 | gulp.task('lint:scripts', function () { 240 | if ( !settings.scripts ) return; 241 | 242 | return gulp.src(paths.scripts.input) 243 | .pipe(plumber()) 244 | .pipe(jshint()) 245 | .pipe(jshint.reporter('jshint-stylish')); 246 | }); 247 | 248 | // Remove pre-existing content from output folders 249 | gulp.task('clean:dist', function () { 250 | del.sync([ 251 | paths.output 252 | ]); 253 | }); 254 | 255 | // Generate documentation 256 | gulp.task('build:docs', ['compile', 'clean:docs'], function() { 257 | if ( !settings.docs ) return; 258 | 259 | return gulp.src(paths.docs.input) 260 | .pipe(plumber()) 261 | .pipe(fileinclude({ 262 | prefix: '@@', 263 | basepath: '@file' 264 | })) 265 | .pipe(tap(function (file, t) { 266 | if ( /\.md|\.markdown/.test(file.path) ) { 267 | return t.through(markdown); 268 | } 269 | })) 270 | .pipe(header(fs.readFileSync(paths.docs.templates + '/_header.html', 'utf8'))) 271 | .pipe(footer(fs.readFileSync(paths.docs.templates + '/_footer.html', 'utf8'))) 272 | .pipe(gulp.dest(paths.docs.output)); 273 | }); 274 | 275 | // Copy distribution files to docs 276 | gulp.task('copy:dist', ['compile', 'clean:docs'], function() { 277 | if ( !settings.docs ) return; 278 | 279 | return gulp.src(paths.output + '/**') 280 | .pipe(plumber()) 281 | .pipe(gulp.dest(paths.docs.output + '/dist')); 282 | }); 283 | 284 | // Copy documentation assets to docs 285 | gulp.task('copy:assets', ['clean:docs'], function() { 286 | if ( !settings.docs ) return; 287 | 288 | return gulp.src(paths.docs.assets) 289 | .pipe(plumber()) 290 | .pipe(gulp.dest(paths.docs.output + '/assets')); 291 | }); 292 | 293 | // Remove prexisting content from docs folder 294 | gulp.task('clean:docs', function () { 295 | if ( !settings.docs ) return; 296 | return del.sync(paths.docs.output); 297 | }); 298 | 299 | // Spin up livereload server and listen for file changes 300 | gulp.task('listen', function () { 301 | livereload.listen(); 302 | gulp.watch(paths.input).on('change', function(file) { 303 | gulp.start('default'); 304 | gulp.start('refresh'); 305 | }); 306 | }); 307 | 308 | // Run livereload after file change 309 | gulp.task('refresh', ['compile', 'docs'], function () { 310 | livereload.changed(); 311 | }); 312 | 313 | 314 | /** 315 | * Task Runners 316 | */ 317 | 318 | // Compile files 319 | gulp.task('compile', [ 320 | 'lint:scripts', 321 | 'clean:dist', 322 | 'build:scripts', 323 | 'build:polyfills', 324 | 'build:styles', 325 | 'build:images', 326 | 'build:static', 327 | 'build:svgs' 328 | ]); 329 | 330 | // Generate documentation 331 | gulp.task('docs', [ 332 | 'clean:docs', 333 | 'build:docs', 334 | 'copy:dist', 335 | 'copy:assets' 336 | ]); 337 | 338 | // Compile files and generate docs (default) 339 | gulp.task('default', [ 340 | 'compile', 341 | 'docs' 342 | ]); 343 | 344 | // Compile files and generate docs when something changes 345 | gulp.task('watch', [ 346 | 'listen', 347 | 'default' 348 | ]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validate", 3 | "version": "2.2.0", 4 | "description": "A lightweight form validation script that augments native HTML5 form validation elements and attributes.", 5 | "main": "./dist/validate.polyfills.min.js", 6 | "author": { 7 | "name": "Chris Ferdinandi", 8 | "url": "http://gomakethings.com" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/cferdinandi/validate" 14 | }, 15 | "devDependencies": { 16 | "gulp": "^3.9.1", 17 | "node-fs": "^0.1.7", 18 | "del": "^2.2.2", 19 | "lazypipe": "^1.0.1", 20 | "gulp-plumber": "^1.1.0", 21 | "gulp-flatten": "^0.3.1", 22 | "gulp-tap": "^0.1.3", 23 | "gulp-rename": "^1.2.2", 24 | "gulp-header": "^1.8.8", 25 | "gulp-footer": "^1.0.5", 26 | "gulp-watch": "^4.3.11", 27 | "gulp-livereload": "^3.8.1", 28 | "jshint": "^2.9.4", 29 | "gulp-jshint": "^2.0.4", 30 | "jshint-stylish": "^2.2.1", 31 | "gulp-concat": "^2.6.1", 32 | "gulp-uglify": "^2.1.2", 33 | "gulp-optimize-js": "^1.1.0", 34 | "gulp-markdown": "^1.2.0", 35 | "gulp-file-include": "^0.14.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/docs/_templates/_footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

7 |
8 |
9 |

Want to learn how to write your own vanilla JS plugins? Get my free daily developer tips and level-up as a web developer. 🚀

10 |
11 | 12 | 13 | 14 | 15 | 19 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/docs/_templates/_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Validate.js 7 | 8 | 9 | 10 | 102 | 103 | 104 | 105 | 106 |
107 | 108 | 113 | 114 |
115 | -------------------------------------------------------------------------------- /src/docs/index.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 |
35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 |
56 | 57 | 58 |
59 | 60 |
61 | 62 | 63 |
64 | 65 |
66 | 67 | 68 |
69 | 70 |
71 | 72 | 79 |
80 | 81 |
82 | Radio Buttons 83 | 87 | 91 |
92 | 93 |
94 | Checkboxes 95 | 99 | 103 | 107 | 111 |
112 | 113 | 114 |
-------------------------------------------------------------------------------- /src/js/_validityState.polyfill.js: -------------------------------------------------------------------------------- 1 | ;(function (window, document, undefined) { 2 | 3 | 'use strict'; 4 | 5 | // Make sure that ValidityState is supported in full (all features) 6 | var supported = function () { 7 | var input = document.createElement('input'); 8 | return ('validity' in input && 'badInput' in input.validity && 'patternMismatch' in input.validity && 'rangeOverflow' in input.validity && 'rangeUnderflow' in input.validity && 'stepMismatch' in input.validity && 'tooLong' in input.validity && 'tooShort' in input.validity && 'typeMismatch' in input.validity && 'valid' in input.validity && 'valueMissing' in input.validity); 9 | }; 10 | 11 | // Save browser's own implementation if available 12 | var browserValidityFunctions = (function() { 13 | var inputValidity = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'validity'); 14 | var buttonValidity = Object.getOwnPropertyDescriptor(HTMLButtonElement.prototype, 'validity'); 15 | var selectValidity = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'validity'); 16 | var textareaValidity = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'validity'); 17 | 18 | var functions = {}; 19 | if (inputValidity) { 20 | functions.input = inputValidity.get; 21 | } 22 | if (buttonValidity) { 23 | functions.button = buttonValidity.get; 24 | } 25 | if (selectValidity) { 26 | functions.select = selectValidity.get; 27 | } 28 | if (textareaValidity) { 29 | functions.textarea = textareaValidity.get; 30 | } 31 | 32 | return functions; 33 | })(); 34 | 35 | /** 36 | * Generate the field validity object 37 | * @param {Node]} field The field to validate 38 | * @return {Object} The validity object 39 | */ 40 | var getValidityState = function (field) { 41 | 42 | // Variables 43 | var type = field.getAttribute('type') || field.nodeName.toLowerCase(); 44 | var isNum = type === 'number' || type === 'range'; 45 | var length = field.value.length; 46 | var valid = true; 47 | 48 | // If radio group, get selected field 49 | if (field.type === 'radio' && field.name) { 50 | var group = document.getElementsByName(field.name); 51 | if (group.length > 0) { 52 | for (var i = 0; i < group.length; i++) { 53 | if (group[i].form === field.form && field.checked) { 54 | field = group[i]; 55 | break; 56 | } 57 | } 58 | } 59 | } 60 | 61 | // Run validity checks 62 | var checkValidity = { 63 | badInput: (isNum && length > 0 && !/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(field.value)), // value of a number field is not a number 64 | patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern 65 | 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 66 | 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 67 | 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 68 | 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 69 | 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 70 | 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 71 | 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 72 | }; 73 | 74 | // Run browser's own validation if available 75 | var fieldTagName = field.tagName.toLowerCase(); 76 | var browserValidity = fieldTagName in browserValidityFunctions ? browserValidityFunctions[fieldTagName].call(field) : {}; 77 | 78 | // Check if any errors 79 | for (var key in checkValidity) { 80 | if (checkValidity.hasOwnProperty(key)) { 81 | // If browser has detected an error, adopt it to our validity object 82 | if (key in browserValidity && browserValidity[key]) { 83 | checkValidity[key] = true; 84 | } 85 | 86 | // If there's an error, change valid value 87 | if (checkValidity[key]) { 88 | valid = false; 89 | } 90 | } 91 | } 92 | 93 | // Add valid property to validity object 94 | checkValidity.valid = valid; 95 | 96 | // Return object 97 | return checkValidity; 98 | 99 | }; 100 | 101 | // If the full set of ValidityState features aren't supported, polyfill 102 | if (!supported()) { 103 | Object.defineProperty(HTMLInputElement.prototype, 'validity', { 104 | get: function ValidityState() { 105 | return getValidityState(this); 106 | }, 107 | configurable: true, 108 | }); 109 | Object.defineProperty(HTMLButtonElement.prototype, 'validity', { 110 | get: function ValidityState() { 111 | return getValidityState(this); 112 | }, 113 | configurable: true, 114 | }); 115 | Object.defineProperty(HTMLSelectElement.prototype, 'validity', { 116 | get: function ValidityState() { 117 | return getValidityState(this); 118 | }, 119 | configurable: true, 120 | }); 121 | Object.defineProperty(HTMLTextAreaElement.prototype, 'validity', { 122 | get: function ValidityState() { 123 | return getValidityState(this); 124 | }, 125 | configurable: true, 126 | }); 127 | } 128 | 129 | })(window, document); -------------------------------------------------------------------------------- /src/js/validate.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if ( typeof define === 'function' && define.amd ) { 3 | define([], factory(root)); 4 | } else if ( typeof exports === 'object' ) { 5 | module.exports = factory(root); 6 | } else { 7 | root.validate = factory(root); 8 | } 9 | })(typeof global !== 'undefined' ? global : this.window || this.global, function (root) { 10 | 11 | 'use strict'; 12 | 13 | // 14 | // Variables 15 | // 16 | 17 | var validate = {}; // Object for public APIs 18 | var settings; 19 | 20 | // Default settings 21 | var defaults = { 22 | 23 | // Classes and Selectors 24 | selector: '[data-validate]', 25 | fieldClass: 'error', 26 | errorClass: 'error-message', 27 | 28 | // Messages 29 | messageValueMissing: 'Please fill out this field.', 30 | messageValueMissingCheckbox: 'This field is required.', 31 | messageValueMissingRadio: 'Please select a value.', 32 | messageValueMissingSelect: 'Please select a value.', 33 | messageValueMissingSelectMulti: 'Please select at least one value.', 34 | messageTypeMismatchEmail: 'Please enter an email address.', 35 | messageTypeMismatchURL: 'Please enter a URL.', 36 | messageTooShort: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.', 37 | messageTooLong: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.', 38 | messagePatternMismatch: 'Please match the requested format.', 39 | messageBadInput: 'Please enter a number.', 40 | messageStepMismatch: 'Please select a valid value.', 41 | messageRangeOverflow: 'Please select a value that is no more than {max}.', 42 | messageRangeUnderflow: 'Please select a value that is no less than {min}.', 43 | messageGeneric: 'The value you entered for this field is invalid.', 44 | 45 | // Form Submission 46 | disableSubmit: false, 47 | onSubmit: function () {}, 48 | 49 | // Callbacks 50 | beforeShowError: function () {}, 51 | afterShowError: function () {}, 52 | beforeRemoveError: function () {}, 53 | afterRemoveError: function () {} 54 | 55 | }; 56 | 57 | 58 | // 59 | // Methods 60 | // 61 | 62 | /** 63 | * Element.matches() polyfill (simple version) 64 | * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill 65 | */ 66 | if (!Element.prototype.matches) { 67 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 68 | } 69 | 70 | /** 71 | * Feature test 72 | * @return {Boolean} Returns true if required methods and APIs are supported by the browser 73 | */ 74 | var supports = function () { 75 | return 'querySelector' in document && 'addEventListener' in root; 76 | }; 77 | 78 | /** 79 | * Merge two or more objects. Returns a new object. 80 | * @private 81 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 82 | * @param {Object} objects The objects to merge together 83 | * @returns {Object} Merged values of defaults and options 84 | */ 85 | var extend = function () { 86 | 87 | // Variables 88 | var extended = {}; 89 | var deep = false; 90 | var i = 0; 91 | var length = arguments.length; 92 | 93 | // Check if a deep merge 94 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 95 | deep = arguments[0]; 96 | i++; 97 | } 98 | 99 | // Merge the object into the extended object 100 | var merge = function (obj) { 101 | for ( var prop in obj ) { 102 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 103 | // If deep merge and property is an object, merge properties 104 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 105 | extended[prop] = extend( true, extended[prop], obj[prop] ); 106 | } else { 107 | extended[prop] = obj[prop]; 108 | } 109 | } 110 | } 111 | }; 112 | 113 | // Loop through each object and conduct a merge 114 | for ( ; i < length; i++ ) { 115 | var obj = arguments[i]; 116 | merge(obj); 117 | } 118 | 119 | return extended; 120 | 121 | }; 122 | 123 | /** 124 | * Get the closest matching element up the DOM tree. 125 | * @private 126 | * @param {Element} elem Starting element 127 | * @param {String} selector Selector to match against 128 | * @return {Boolean|Element} Returns null if not match found 129 | */ 130 | var getClosest = function ( elem, selector ) { 131 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 132 | if ( elem.matches( selector ) ) return elem; 133 | } 134 | return null; 135 | }; 136 | 137 | /** 138 | * Validate a form field 139 | * @public 140 | * @param {Node} field The field to validate 141 | * @param {Object} options User options 142 | * @return {String} The error message 143 | */ 144 | validate.hasError = function (field, options) { 145 | 146 | // Merge user options with existing settings or defaults 147 | var localSettings = extend(settings || defaults, options || {}); 148 | 149 | // Don't validate submits, buttons, file and reset inputs, and disabled fields 150 | if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return; 151 | 152 | // Get validity 153 | var validity = field.validity; 154 | 155 | // If valid, return null 156 | if (validity.valid) return; 157 | 158 | // If field is required and empty 159 | if (validity.valueMissing) { 160 | 161 | if (field.type === 'checkbox') return localSettings.messageValueMissingCheckbox; 162 | 163 | if (field.type === 'radio') return localSettings.messageValueMissingRadio; 164 | 165 | if (field.type === 'select-multiple') return localSettings.messageValueMissingSelectMulti; 166 | 167 | if (field.type === 'select-one') return localSettings.messageValueMissingSelect; 168 | 169 | return localSettings.messageValueMissing; 170 | } 171 | 172 | // If not the right type 173 | if (validity.typeMismatch) { 174 | 175 | // Email 176 | if (field.type === 'email') return localSettings.messageTypeMismatchEmail; 177 | 178 | // URL 179 | if (field.type === 'url') return localSettings.messageTypeMismatchURL; 180 | 181 | } 182 | 183 | // If too short 184 | if (validity.tooShort) return localSettings.messageTooShort.replace('{minLength}', field.getAttribute('minLength')).replace('{length}', field.value.length); 185 | 186 | // If too long 187 | if (validity.tooLong) return localSettings.messageTooLong.replace('{minLength}', field.getAttribute('maxLength')).replace('{length}', field.value.length); 188 | 189 | // If number input isn't a number 190 | if (validity.badInput) return localSettings.messageBadInput; 191 | 192 | // If a number value doesn't match the step interval 193 | if (validity.stepMismatch) return localSettings.messageStepMismatch; 194 | 195 | // If a number field is over the max 196 | if (validity.rangeOverflow) return localSettings.messageRangeOverflow.replace('{max}', field.getAttribute('max')); 197 | 198 | // If a number field is below the min 199 | if (validity.rangeUnderflow) return localSettings.messageRangeUnderflow.replace('{min}', field.getAttribute('min')); 200 | 201 | // If pattern doesn't match 202 | if (validity.patternMismatch) { 203 | 204 | // If pattern info is included, return custom error 205 | if (field.hasAttribute('title')) return field.getAttribute('title'); 206 | 207 | // Otherwise, generic error 208 | return localSettings.messagePatternMismatch; 209 | 210 | } 211 | 212 | // If all else fails, return a generic catchall error 213 | return localSettings.messageGeneric; 214 | 215 | }; 216 | 217 | /** 218 | * Show an error message on a field 219 | * @public 220 | * @param {Node} field The field to show an error message for 221 | * @param {String} error The error message to show 222 | * @param {Object} options User options 223 | */ 224 | validate.showError = function (field, error, options) { 225 | 226 | // Merge user options with existing settings or defaults 227 | var localSettings = extend(settings || defaults, options || {}); 228 | 229 | // Before show error callback 230 | localSettings.beforeShowError(field, error); 231 | 232 | // Add error class to field 233 | field.classList.add(localSettings.fieldClass); 234 | 235 | // If the field is a radio button and part of a group, error all and get the last item in the group 236 | if (field.type === 'radio' && field.name) { 237 | var group = document.getElementsByName(field.name); 238 | if (group.length > 0) { 239 | for (var i = 0; i < group.length; i++) { 240 | if (group[i].form !== field.form) continue; // Only check fields in current form 241 | group[i].classList.add(localSettings.fieldClass); 242 | } 243 | field = group[group.length - 1]; 244 | } 245 | } 246 | 247 | // Get field id or name 248 | var id = field.id || field.name; 249 | if (!id) return; 250 | 251 | // Check if error message field already exists 252 | // If not, create one 253 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id ); 254 | if (!message) { 255 | message = document.createElement('div'); 256 | message.className = localSettings.errorClass; 257 | message.id = 'error-for-' + id; 258 | 259 | // If the field is a radio button or checkbox, insert error after the label 260 | var label; 261 | if (field.type === 'radio' || field.type ==='checkbox') { 262 | label = field.form.querySelector('label[for="' + id + '"]') || getClosest(field, 'label'); 263 | if (label) { 264 | label.parentNode.insertBefore( message, label.nextSibling ); 265 | } 266 | } 267 | 268 | // Otherwise, insert it after the field 269 | if (!label) { 270 | field.parentNode.insertBefore( message, field.nextSibling ); 271 | } 272 | } 273 | 274 | // Add ARIA role to the field 275 | field.setAttribute('aria-describedby', 'error-for-' + id); 276 | 277 | // Update error message 278 | message.innerHTML = error; 279 | 280 | // Remove any existing styles hiding the error message 281 | message.style.display = ''; 282 | message.style.visibility = ''; 283 | 284 | // After show error callback 285 | localSettings.afterShowError(field, error); 286 | 287 | }; 288 | 289 | /** 290 | * Remove an error message from a field 291 | * @public 292 | * @param {Node} field The field to remove the error from 293 | * @param {Object} options User options 294 | */ 295 | validate.removeError = function (field, options) { 296 | 297 | // Merge user options with existing settings or defaults 298 | var localSettings = extend(settings || defaults, options || {}); 299 | 300 | // Before remove error callback 301 | localSettings.beforeRemoveError(field); 302 | 303 | // Remove ARIA role from the field 304 | field.removeAttribute('aria-describedby'); 305 | 306 | // Remove error class to field 307 | field.classList.remove(localSettings.fieldClass); 308 | 309 | // If the field is a radio button and part of a group, remove error from all and get the last item in the group 310 | if (field.type === 'radio' && field.name) { 311 | var group = document.getElementsByName(field.name); 312 | if (group.length > 0) { 313 | for (var i = 0; i < group.length; i++) { 314 | if (group[i].form !== field.form) continue; // Only check fields in current form 315 | group[i].classList.remove(localSettings.fieldClass); 316 | } 317 | field = group[group.length - 1]; 318 | } 319 | } 320 | 321 | // Get field id or name 322 | var id = field.id || field.name; 323 | if (!id) return; 324 | 325 | // Check if an error message is in the DOM 326 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id + ''); 327 | if (!message) return; 328 | 329 | // If so, hide it 330 | message.innerHTML = ''; 331 | message.style.display = 'none'; 332 | message.style.visibility = 'hidden'; 333 | 334 | // After remove error callback 335 | localSettings.afterRemoveError(field); 336 | 337 | }; 338 | 339 | /** 340 | * Add the `novalidate` attribute to all forms 341 | * @private 342 | * @param {Boolean} remove If true, remove the `novalidate` attribute 343 | */ 344 | var addNoValidate = function (remove) { 345 | var forms = document.querySelectorAll(settings.selector); 346 | for (var i = 0; i < forms.length; i++) { 347 | if (remove) { 348 | forms[i].removeAttribute('novalidate'); 349 | continue; 350 | } 351 | forms[i].setAttribute('novalidate', true); 352 | } 353 | }; 354 | 355 | /** 356 | * Check field validity when it loses focus 357 | * @private 358 | * @param {Event} event The blur event 359 | */ 360 | var blurHandler = function (event) { 361 | 362 | // Only run if the field is in a form to be validated 363 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 364 | 365 | // Validate the field 366 | var error = validate.hasError(event.target); 367 | 368 | // If there's an error, show it 369 | if (error) { 370 | validate.showError(event.target, error); 371 | return; 372 | } 373 | 374 | // Otherwise, remove any errors that exist 375 | validate.removeError(event.target); 376 | 377 | }; 378 | 379 | /** 380 | * Check radio and checkbox field validity when clicked 381 | * @private 382 | * @param {Event} event The click event 383 | */ 384 | var clickHandler = function (event) { 385 | 386 | // Only run if the field is in a form to be validated 387 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 388 | 389 | // Only run if the field is a checkbox or radio 390 | var type = event.target.getAttribute('type'); 391 | if (!(type === 'checkbox' || type === 'radio')) return; 392 | 393 | // Validate the field 394 | var error = validate.hasError(event.target); 395 | 396 | // If there's an error, show it 397 | if (error) { 398 | validate.showError(event.target, error); 399 | return; 400 | } 401 | 402 | // Otherwise, remove any errors that exist 403 | validate.removeError(event.target); 404 | 405 | }; 406 | 407 | /** 408 | * Check all fields on submit 409 | * @private 410 | * @param {Event} event The submit event 411 | */ 412 | var submitHandler = function (event) { 413 | 414 | // Only run on forms flagged for validation 415 | if (!event.target.matches(settings.selector)) return; 416 | 417 | // Get all of the form elements 418 | var fields = event.target.elements; 419 | 420 | // Validate each field 421 | // Store the first field with an error to a variable so we can bring it into focus later 422 | var hasErrors; 423 | for (var i = 0; i < fields.length; i++) { 424 | var error = validate.hasError(fields[i]); 425 | if (error) { 426 | validate.showError(fields[i], error); 427 | if (!hasErrors) { 428 | hasErrors = fields[i]; 429 | } 430 | } 431 | } 432 | 433 | // Prevent form from submitting if there are errors or submission is disabled 434 | if (hasErrors || settings.disableSubmit) { 435 | event.preventDefault(); 436 | } 437 | 438 | // If there are errrors, focus on first element with error 439 | if (hasErrors) { 440 | hasErrors.focus(); 441 | return; 442 | } 443 | 444 | // Otherwise, submit the form 445 | settings.onSubmit(event.target, fields); 446 | 447 | }; 448 | 449 | /** 450 | * Destroy the current initialization. 451 | * @public 452 | */ 453 | validate.destroy = function () { 454 | 455 | // If plugin isn't already initialized, stop 456 | if ( !settings ) return; 457 | 458 | // Remove event listeners 459 | document.removeEventListener('blur', blurHandler, true); 460 | document.removeEventListener('click', clickHandler, false); 461 | document.removeEventListener('submit', submitHandler, false); 462 | 463 | // Remove all errors 464 | var fields = document.querySelectorAll(settings.errorClass); 465 | for (var i = 0; i < fields.length; i++) { 466 | validate.removeError(fields[i]); 467 | } 468 | 469 | // Remove `novalidate` from forms 470 | addNoValidate(true); 471 | 472 | // Reset variables 473 | settings = null; 474 | 475 | }; 476 | 477 | /** 478 | * Initialize Validate 479 | * @public 480 | * @param {Object} options User settings 481 | */ 482 | validate.init = function (options) { 483 | 484 | // feature test 485 | if (!supports()) return; 486 | 487 | // Destroy any existing initializations 488 | validate.destroy(); 489 | 490 | // Merge user options with defaults 491 | settings = extend(defaults, options || {}); 492 | 493 | // Add the `novalidate` attribute to all forms 494 | addNoValidate(); 495 | 496 | // Event listeners 497 | document.addEventListener('blur', blurHandler, true); 498 | document.addEventListener('click', clickHandler, true); 499 | document.addEventListener('submit', submitHandler, false); 500 | 501 | }; 502 | 503 | 504 | // 505 | // Public APIs 506 | // 507 | 508 | return validate; 509 | 510 | }); -------------------------------------------------------------------------------- /static/js/validate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * validate v1.1.3: A lightweight form validation script that augments native HTML5 form validation elements and attributes. 3 | * (c) 2018 Chris Ferdinandi 4 | * MIT License 5 | * http://github.com/cferdinandi/validate 6 | */ 7 | 8 | (function (root, factory) { 9 | if ( typeof define === 'function' && define.amd ) { 10 | define([], factory(root)); 11 | } else if ( typeof exports === 'object' ) { 12 | module.exports = factory(root); 13 | } else { 14 | root.validate = factory(root); 15 | } 16 | })(typeof global !== 'undefined' ? global : this.window || this.global, (function (root) { 17 | 18 | 'use strict'; 19 | 20 | // 21 | // Variables 22 | // 23 | 24 | var validate = {}; // Object for public APIs 25 | var supports = 'querySelector' in document && 'addEventListener' in root; // Feature test 26 | var settings; 27 | 28 | // Default settings 29 | var defaults = { 30 | 31 | // Classes and Selectors 32 | selector: '[data-validate]', 33 | fieldClass: 'error', 34 | errorClass: 'error-message', 35 | 36 | // Messages 37 | messageValueMissing: 'Please fill out this field.', 38 | messageValueMissingSelect: 'Please select a value.', 39 | messageValueMissingSelectMulti: 'Please select at least one value.', 40 | messageTypeMismatchEmail: 'Please enter an email address.', 41 | messageTypeMismatchURL: 'Please enter a URL.', 42 | messageTooShort: 'Please lengthen this text to {minLength} characters or more. You are currently using {length} characters.', 43 | messageTooLong: 'Please shorten this text to no more than {maxLength} characters. You are currently using {length} characters.', 44 | messagePatternMismatch: 'Please match the requested format.', 45 | messageBadInput: 'Please enter a number.', 46 | messageStepMismatch: 'Please select a valid value.', 47 | messageRangeOverflow: 'Please select a value that is no more than {max}.', 48 | messageRangeUnderflow: 'Please select a value that is no less than {min}.', 49 | messageGeneric: 'The value you entered for this field is invalid.', 50 | 51 | // Form Submission 52 | disableSubmit: false, 53 | onSubmit: function () {}, 54 | 55 | // Callbacks 56 | beforeShowError: function () {}, 57 | afterShowError: function () {}, 58 | beforeRemoveError: function () {}, 59 | afterRemoveError: function () {} 60 | 61 | }; 62 | 63 | 64 | // 65 | // Methods 66 | // 67 | 68 | // Element.matches() polyfill 69 | if (!Element.prototype.matches) { 70 | Element.prototype.matches = 71 | Element.prototype.matchesSelector || 72 | Element.prototype.mozMatchesSelector || 73 | Element.prototype.msMatchesSelector || 74 | Element.prototype.oMatchesSelector || 75 | Element.prototype.webkitMatchesSelector || 76 | function(s) { 77 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 78 | i = matches.length; 79 | while (--i >= 0 && matches.item(i) !== this) {} 80 | return i > -1; 81 | }; 82 | } 83 | 84 | /** 85 | * Merge two or more objects. Returns a new object. 86 | * @private 87 | * @param {Boolean} deep If true, do a deep (or recursive) merge [optional] 88 | * @param {Object} objects The objects to merge together 89 | * @returns {Object} Merged values of defaults and options 90 | */ 91 | var extend = function () { 92 | 93 | // Variables 94 | var extended = {}; 95 | var deep = false; 96 | var i = 0; 97 | var length = arguments.length; 98 | 99 | // Check if a deep merge 100 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 101 | deep = arguments[0]; 102 | i++; 103 | } 104 | 105 | // Merge the object into the extended object 106 | var merge = function (obj) { 107 | for ( var prop in obj ) { 108 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 109 | // If deep merge and property is an object, merge properties 110 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 111 | extended[prop] = extend( true, extended[prop], obj[prop] ); 112 | } else { 113 | extended[prop] = obj[prop]; 114 | } 115 | } 116 | } 117 | }; 118 | 119 | // Loop through each object and conduct a merge 120 | for ( ; i < length; i++ ) { 121 | var obj = arguments[i]; 122 | merge(obj); 123 | } 124 | 125 | return extended; 126 | 127 | }; 128 | 129 | /** 130 | * Get the closest matching element up the DOM tree. 131 | * @private 132 | * @param {Element} elem Starting element 133 | * @param {String} selector Selector to match against 134 | * @return {Boolean|Element} Returns null if not match found 135 | */ 136 | var getClosest = function ( elem, selector ) { 137 | for ( ; elem && elem !== document; elem = elem.parentNode ) { 138 | if ( elem.matches( selector ) ) return elem; 139 | } 140 | return null; 141 | }; 142 | 143 | /** 144 | * Validate a form field 145 | * @public 146 | * @param {Node} field The field to validate 147 | * @param {Object} options User options 148 | * @return {String} The error message 149 | */ 150 | validate.hasError = function (field, options) { 151 | 152 | // Merge user options with existing settings or defaults 153 | var localSettings = extend(settings || defaults, options || {}); 154 | 155 | // Don't validate submits, buttons, file and reset inputs, and disabled fields 156 | if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return; 157 | 158 | // Get validity 159 | var validity = field.validity; 160 | 161 | // If valid, return null 162 | if (validity.valid) return; 163 | 164 | // If field is required and empty 165 | if (validity.valueMissing) { 166 | 167 | if (field.type === 'select-multiple') return localSettings.messageValueMissingSelectMulti; 168 | 169 | if (field.type === 'select-one') return localSettings.messageValueMissingSelect; 170 | 171 | return localSettings.messageValueMissing; 172 | } 173 | 174 | // If not the right type 175 | if (validity.typeMismatch) { 176 | 177 | // Email 178 | if (field.type === 'email') return localSettings.messageTypeMismatchEmail; 179 | 180 | // URL 181 | if (field.type === 'url') return localSettings.messageTypeMismatchURL; 182 | 183 | } 184 | 185 | // If too short 186 | if (validity.tooShort) return localSettings.messageTooShort.replace('{minLength}', field.getAttribute('minLength')).replace('{length}', field.value.length); 187 | 188 | // If too long 189 | if (validity.tooLong) return localSettings.messageTooLong.replace('{minLength}', field.getAttribute('maxLength')).replace('{length}', field.value.length); 190 | 191 | // If number input isn't a number 192 | if (validity.badInput) return localSettings.messageBadInput; 193 | 194 | // If a number value doesn't match the step interval 195 | if (validity.stepMismatch) return localSettings.messageStepMismatch; 196 | 197 | // If a number field is over the max 198 | if (validity.rangeOverflow) return localSettings.messageRangeOverflow.replace('{max}', field.getAttribute('max')); 199 | 200 | // If a number field is below the min 201 | if (validity.rangeUnderflow) return localSettings.messageRangeUnderflow.replace('{min}', field.getAttribute('min')); 202 | 203 | // If pattern doesn't match 204 | if (validity.patternMismatch) { 205 | 206 | // If pattern info is included, return custom error 207 | if (field.hasAttribute('title')) return field.getAttribute('title'); 208 | 209 | // Otherwise, generic error 210 | return localSettings.messagePatternMismatch; 211 | 212 | } 213 | 214 | // If all else fails, return a generic catchall error 215 | return localSettings.messageGeneric; 216 | 217 | }; 218 | 219 | /** 220 | * Show an error message on a field 221 | * @public 222 | * @param {Node} field The field to show an error message for 223 | * @param {String} error The error message to show 224 | * @param {Object} options User options 225 | */ 226 | validate.showError = function (field, error, options) { 227 | 228 | // Merge user options with existing settings or defaults 229 | var localSettings = extend(settings || defaults, options || {}); 230 | 231 | // Before show error callback 232 | localSettings.beforeShowError(field, error); 233 | 234 | // Add error class to field 235 | field.classList.add(localSettings.fieldClass); 236 | 237 | // If the field is a radio button and part of a group, error all and get the last item in the group 238 | if (field.type === 'radio' && field.name) { 239 | var group = document.getElementsByName(field.name); 240 | if (group.length > 0) { 241 | for (var i = 0; i < group.length; i++) { 242 | if (group[i].form !== field.form) continue; // Only check fields in current form 243 | group[i].classList.add(localSettings.fieldClass); 244 | } 245 | field = group[group.length - 1]; 246 | } 247 | } 248 | 249 | // Get field id or name 250 | var id = field.id || field.name; 251 | if (!id) return; 252 | 253 | // Check if error message field already exists 254 | // If not, create one 255 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id ); 256 | if (!message) { 257 | message = document.createElement('div'); 258 | message.className = localSettings.errorClass; 259 | message.id = 'error-for-' + id; 260 | 261 | // If the field is a radio button or checkbox, insert error after the label 262 | var label; 263 | if (field.type === 'radio' || field.type ==='checkbox') { 264 | label = field.form.querySelector('label[for="' + id + '"]') || getClosest(field, 'label'); 265 | if (label) { 266 | label.parentNode.insertBefore( message, label.nextSibling ); 267 | } 268 | } 269 | 270 | // Otherwise, insert it after the field 271 | if (!label) { 272 | field.parentNode.insertBefore( message, field.nextSibling ); 273 | } 274 | } 275 | 276 | // Add ARIA role to the field 277 | field.setAttribute('aria-describedby', 'error-for-' + id); 278 | 279 | // Update error message 280 | message.innerHTML = error; 281 | 282 | // Remove any existing styles hiding the error message 283 | message.style.display = ''; 284 | message.style.visibility = ''; 285 | 286 | // After show error callback 287 | localSettings.afterShowError(field, error); 288 | 289 | }; 290 | 291 | /** 292 | * Remove an error message from a field 293 | * @public 294 | * @param {Node} field The field to remove the error from 295 | * @param {Object} options User options 296 | */ 297 | validate.removeError = function (field, options) { 298 | 299 | // Merge user options with existing settings or defaults 300 | var localSettings = extend(settings || defaults, options || {}); 301 | 302 | // Before remove error callback 303 | localSettings.beforeRemoveError(field); 304 | 305 | // Remove ARIA role from the field 306 | field.removeAttribute('aria-describedby'); 307 | 308 | // Remove error class to field 309 | field.classList.remove(localSettings.fieldClass); 310 | 311 | // If the field is a radio button and part of a group, remove error from all and get the last item in the group 312 | if (field.type === 'radio' && field.name) { 313 | var group = document.getElementsByName(field.name); 314 | if (group.length > 0) { 315 | for (var i = 0; i < group.length; i++) { 316 | if (group[i].form !== field.form) continue; // Only check fields in current form 317 | group[i].classList.remove(localSettings.fieldClass); 318 | } 319 | field = group[group.length - 1]; 320 | } 321 | } 322 | 323 | // Get field id or name 324 | var id = field.id || field.name; 325 | if (!id) return; 326 | 327 | // Check if an error message is in the DOM 328 | var message = field.form.querySelector('.' + localSettings.errorClass + '#error-for-' + id + ''); 329 | if (!message) return; 330 | 331 | // If so, hide it 332 | message.innerHTML = ''; 333 | message.style.display = 'none'; 334 | message.style.visibility = 'hidden'; 335 | 336 | // After remove error callback 337 | localSettings.afterRemoveError(field); 338 | 339 | }; 340 | 341 | /** 342 | * Add the `novalidate` attribute to all forms 343 | * @private 344 | * @param {Boolean} remove If true, remove the `novalidate` attribute 345 | */ 346 | var addNoValidate = function (remove) { 347 | var forms = document.querySelectorAll(settings.selector); 348 | for (var i = 0; i < forms.length; i++) { 349 | if (remove) { 350 | forms[i].removeAttribute('novalidate'); 351 | continue; 352 | } 353 | forms[i].setAttribute('novalidate', true); 354 | } 355 | }; 356 | 357 | /** 358 | * Check field validity when it loses focus 359 | * @private 360 | * @param {Event} event The blur event 361 | */ 362 | var blurHandler = function (event) { 363 | 364 | // Only run if the field is in a form to be validated 365 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 366 | 367 | // Validate the field 368 | var error = validate.hasError(event.target); 369 | 370 | // If there's an error, show it 371 | if (error) { 372 | validate.showError(event.target, error); 373 | return; 374 | } 375 | 376 | // Otherwise, remove any errors that exist 377 | validate.removeError(event.target); 378 | 379 | }; 380 | 381 | /** 382 | * Check radio and checkbox field validity when clicked 383 | * @private 384 | * @param {Event} event The click event 385 | */ 386 | var clickHandler = function (event) { 387 | 388 | // Only run if the field is in a form to be validated 389 | if (!event.target.form || !event.target.form.matches(settings.selector)) return; 390 | 391 | // Only run if the field is a checkbox or radio 392 | var type = event.target.getAttribute('type'); 393 | if (!(type === 'checkbox' || type === 'radio')) return; 394 | 395 | // Validate the field 396 | var error = validate.hasError(event.target); 397 | 398 | // If there's an error, show it 399 | if (error) { 400 | validate.showError(event.target, error); 401 | return; 402 | } 403 | 404 | // Otherwise, remove any errors that exist 405 | validate.removeError(event.target); 406 | 407 | }; 408 | 409 | /** 410 | * Check all fields on submit 411 | * @private 412 | * @param {Event} event The submit event 413 | */ 414 | var submitHandler = function (event) { 415 | 416 | // Only run on forms flagged for validation 417 | if (!event.target.matches(settings.selector)) return; 418 | 419 | // Get all of the form elements 420 | var fields = event.target.elements; 421 | 422 | // Validate each field 423 | // Store the first field with an error to a variable so we can bring it into focus later 424 | var hasErrors; 425 | for (var i = 0; i < fields.length; i++) { 426 | var error = validate.hasError(fields[i]); 427 | if (error) { 428 | validate.showError(fields[i], error); 429 | if (!hasErrors) { 430 | hasErrors = fields[i]; 431 | } 432 | } 433 | } 434 | 435 | // Prevent form from submitting if there are errors or submission is disabled 436 | if (hasErrors || settings.disableSubmit) { 437 | event.preventDefault(); 438 | } 439 | 440 | // If there are errrors, focus on first element with error 441 | if (hasErrors) { 442 | hasErrors.focus(); 443 | return; 444 | } 445 | 446 | // Otherwise, submit the form 447 | settings.onSubmit(event.target, fields); 448 | 449 | }; 450 | 451 | /** 452 | * Destroy the current initialization. 453 | * @public 454 | */ 455 | validate.destroy = function () { 456 | 457 | // If plugin isn't already initialized, stop 458 | if ( !settings ) return; 459 | 460 | // Remove event listeners 461 | document.removeEventListener('blur', blurHandler, true); 462 | document.removeEventListener('click', clickHandler, false); 463 | document.removeEventListener('submit', submitHandler, false); 464 | 465 | // Remove all errors 466 | var fields = document.querySelectorAll(settings.errorClass); 467 | for (var i = 0; i < fields.length; i++) { 468 | validate.removeError(fields[i]); 469 | } 470 | 471 | // Remove `novalidate` from forms 472 | addNoValidate(true); 473 | 474 | // Reset variables 475 | settings = null; 476 | 477 | }; 478 | 479 | /** 480 | * Initialize Validate 481 | * @public 482 | * @param {Object} options User settings 483 | */ 484 | validate.init = function (options) { 485 | 486 | // feature test 487 | if (!supports) return; 488 | 489 | // Destroy any existing initializations 490 | validate.destroy(); 491 | 492 | // Merge user options with defaults 493 | settings = extend(defaults, options || {}); 494 | 495 | // Add the `novalidate` attribute to all forms 496 | addNoValidate(); 497 | 498 | // Event listeners 499 | document.addEventListener('blur', blurHandler, true); 500 | document.addEventListener('click', clickHandler, true); 501 | document.addEventListener('submit', submitHandler, false); 502 | 503 | }; 504 | 505 | 506 | // 507 | // Public APIs 508 | // 509 | 510 | return validate; 511 | 512 | })); -------------------------------------------------------------------------------- /static/js/validate.min.js: -------------------------------------------------------------------------------- 1 | /*! validate v1.1.3 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ 2 | !(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) { 59 | for (var i = 0; i < group.length; i++) { 60 | if (group[i].form === field.form && field.checked) { 61 | field = group[i]; 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | 68 | // Run validity checks 69 | var checkValidity = { 70 | badInput: (isNum && length > 0 && !/^[-+]?(?:\d+|\d*[.,]\d+)$/.test(field.value)), // value of a number field is not a number 71 | patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern 72 | 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 73 | 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 74 | 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 75 | 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 76 | 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 77 | 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 78 | 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 79 | }; 80 | 81 | // Run browser's own validation if available 82 | var fieldTagName = field.tagName.toLowerCase(); 83 | var browserValidity = fieldTagName in browserValidityFunctions ? browserValidityFunctions[fieldTagName].call(field) : {}; 84 | 85 | // Check if any errors 86 | for (var key in checkValidity) { 87 | if (checkValidity.hasOwnProperty(key)) { 88 | // If browser has detected an error, adopt it to our validity object 89 | if (key in browserValidity && browserValidity[key]) { 90 | checkValidity[key] = true; 91 | } 92 | 93 | // If there's an error, change valid value 94 | if (checkValidity[key]) { 95 | valid = false; 96 | } 97 | } 98 | } 99 | 100 | // Add valid property to validity object 101 | checkValidity.valid = valid; 102 | 103 | // Return object 104 | return checkValidity; 105 | 106 | }; 107 | 108 | // If the full set of ValidityState features aren't supported, polyfill 109 | if (!supported()) { 110 | Object.defineProperty(HTMLInputElement.prototype, 'validity', { 111 | get: function ValidityState() { 112 | return getValidityState(this); 113 | }, 114 | configurable: true, 115 | }); 116 | Object.defineProperty(HTMLButtonElement.prototype, 'validity', { 117 | get: function ValidityState() { 118 | return getValidityState(this); 119 | }, 120 | configurable: true, 121 | }); 122 | Object.defineProperty(HTMLSelectElement.prototype, 'validity', { 123 | get: function ValidityState() { 124 | return getValidityState(this); 125 | }, 126 | configurable: true, 127 | }); 128 | Object.defineProperty(HTMLTextAreaElement.prototype, 'validity', { 129 | get: function ValidityState() { 130 | return getValidityState(this); 131 | }, 132 | configurable: true, 133 | }); 134 | } 135 | 136 | })(window, document); -------------------------------------------------------------------------------- /static/js/validityState-polyfill.min.js: -------------------------------------------------------------------------------- 1 | /*! validate v1.1.3 | (c) 2018 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/validate */ 2 | !(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.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); --------------------------------------------------------------------------------