1/* 2* 3* EXPOSURE FORM VALIDATOR CLASS 4* Documentation: http://www.wscode.v4/form-validator/ 5* 6*/ 7 8class WS_Form_Validator { 9 10 constructor( args ) { 11 this.selector = args['selector']; 12 this.pre_validate = args['pre_validate']; 13 this.post_validate = args['post_validate']; 14 this.initialize(); 15 } 16 17 18 // INITIALIZE 19 initialize() { 20 21 this.form = document.querySelector( this.selector ); 22 if ( !this.form ) { 23 return; 24 } 25 26 this.form.setAttribute( 'novalidate', true ); 27 this.form.addEventListener( 'submit', (e) => { 28 29 // Pre-validation callback: 30 if ( typeof this.pre_validate === 'function' ) { 31 this.pre_validate(e); 32 } 33 34 // Prevent form submission if there are validation errors 35 if ( false == this.validate_form() ) { 36 e.preventDefault(); // Prevent form submit 37 e.stopPropagation(); 38 e.stopImmediatePropagation(); // Prevents other listeners of the same event from being called 39 return; 40 } 41 42 // Post-validation callback: 43 if ( typeof this.post_validate === 'function' ) { 44 e.preventDefault(); // Prevent form submit 45 e.stopPropagation(); 46 e.stopImmediatePropagation(); // Prevents other listeners of the same event from being called 47 this.post_validate(e); 48 } 49 50 }, true ); 51 } 52 53 54 55 // VALIDATE FORMS 56 validate_form() { 57 58 this.remove_errors(); 59 60 let is_valid = true; 61 let inputs = this.form.querySelectorAll( 'input:not(:is([type="hidden"]), [type="radio"], [type="checkbox"].required, [type="submit"]), fieldset, select, textarea, .g-recaptcha' ); 62 63 let errors = []; 64 65 inputs.forEach( input => { 66 67 if ( !this.is_visible( input ) ) { 68 return; 69 } 70 71 let error = ''; 72 73 if ( 'fieldset' == input.type ) { 74 error = this.validate_fieldset( input ); 75 } else { 76 error = this.validate( input ); 77 } 78 79 // Add errors, if any, to an array 80 if ( error ) { 81 errors.push( { 'input': input, 'error': error }); 82 is_valid = false; 83 return; 84 } 85 86 }); // forEach 87 88 89 // If there ARE errors, loop through and append them 90 if ( !is_valid ) { 91 errors.forEach( error => { 92 this.append_error( error['input'], error['error'] ); 93 }); 94 this.set_error_focus( errors ); 95 return false; 96 } 97 98 // If there are NO errors: 99 return true; 100 101 } 102 103 104 105 // DEFAULT VALIDATION (not blank if required, valid input otherwise) 106 validate( el ) { 107 108 if ( el.dataset.match && el.value.length > 0 ) { 109 return this.compare_values( el ); 110 } 111 112 if ( 'cc-number' == el.dataset.type && el.value.length > 0 && false == this.luhn_check( el ) ) { 113 return el.dataset.error ? el.dataset.error : 'Please enter a valid credit card number'; 114 } 115 116 if ( 'cc-exp' == el.dataset.type && el.value.length > 0 ) { 117 return this.validate_expiration_date( el ); 118 } 119 120 if ( el.classList.contains( 'g-recaptcha' ) && typeof grecaptcha === 'object' && typeof grecaptcha.getResponse === 'function' && grecaptcha.getResponse().length == 0 ) { 121 if ( !el.id ) { el.id = 'recaptcha'; } 122 return 'Please confirm you are not a robot'; 123 } 124 125 if (!el.validity.valid && 'email' == el.type) { 126 return `Provide a valid email address`; 127 } 128 129 if ( el.validity.valid ) { 130 return; 131 } 132 133 if ( el.dataset.error ) { 134 return el.dataset.error; 135 } 136 137 let name = this.get_display_name( el ); 138 139 if (!el.classList.contains('g-recaptcha')) { 140 141 switch (true) { 142 case el.validity.tooLong: 143 return `The ${name} field is too long`; 144 break; 145 146 case el.validity.tooShort: 147 return `The ${name} field is too short`; 148 break; 149 150 case el.validity.rangeOverflow: 151 case el.validity.rangeUnderflow: 152 return `The ${name} field is out of range`; 153 break; 154 155 case el.validity.valueMissing: 156 return `The ${name} field cannot be blank`; 157 break; 158 159 case el.validity.customError: 160 return el.validationMessage; 161 break; 162 163 default: 164 return `The ${name}
164 field is invalid`; 165 break; 166 } 167 } 168 169 } 170 171 172 173 // VALIDATE FIELDSETS (at least one checkbox/radio button within must be checked) 174 validate_fieldset( el ) { 175 if ( !this.is_required( el ) ) { 176 return; 177 } 178 179 if ( 0 == el.querySelectorAll( 'input:checked' ).length ) { 180 let name = this.get_display_name( el ); 181 return el.dataset.error ? el.dataset.error : `Choose an option from the ${name} field`; 182 } 183 } 184 185 186 187 // CHECK IF ELEMENT IS VISIBLE 188 is_visible( el ) { 189 return !!( el.offsetWidth || el.offsetHeight || el.getClientRects().length ); 190 } 191 192 193 194 // CHECK IF ELEMENT IS REQUIRED 195 is_required( el ) { 196 if ( el.hasAttribute( 'required' ) || 'true' == el.getAttribute( 'aria-required' ) || 'true' == el.dataset.required ) { 197 return true; 198 } 199 } 200 201 202 203 // COMPARE ELEMENT VALUES TO MAKE SURE THEY'RE EQUAL (i.e. confirm password fields) 204 compare_values( el ) { 205 let compare = document.getElementById( el.dataset.match.replace( /^#/, '' ) ); 206 if ( el.value != compare.value ) { 207 return `The ${this.get_display_name( el )} value must match the ${this.get_display_name( compare )} value`; 208 } 209 return ''; 210 } 211 212 213 214 215 // VALIDATE CREDIT CARD NUMBER FIELDS (i.e. fields with data-type="cc-number") 216 luhn_check( el ) { 217 let number = el.value; 218 if ( 0 == number.length || isNaN( number ) ) { 219 return false; 220 } 221 number = number.replace(/\D/g, ''); 222 let length = number.length; 223 let parity = length % 2; 224 let total = 0; 225 for ( var i = 0; i < length; i++) { 226 let digit = number.charAt( i ); 227 if ( i % 2 == parity ) { 228 digit = digit * 2; 229 if ( digit > 9 ) { 230 digit = digit - 9; 231 } 232 } 233 total = total + parseInt( digit ); 234 } 235 if ( total % 10 != 0 ) { 236 return false; 237 } 238 return true; 239 } 240 241 242 243 // VALIDATE CREDICT CARD EXPIRATION DATES (i.e. fields with data-type="cc-exp") 244 validate_expiration_date( el ) { 245 let value = el.value; 246 let pattern = /^((0[1-9])|(1[0-2]))\/(\d{4})$/; 247 if ( !pattern.test( value ) ) { 248 return 'Expiration date should be in MM/YYYY format'; 249 } 250 let d2 = new Date(); 251 let myDate = new Date(parseInt(value.split('/')[1]), parseInt(value.split('/')[0]), 1); 252 let milli_d1 = myDate.getTime(); 253 let milli_d2 = d2.getTime(); 254 let difference = milli_d1 - milli_d2; 255 if ( difference < 0 ) { 256 return 'This credit card has expired'; 257 } 258 return ''; 259 } 260 261 262 263 // APPEND ERROR MESSAGES TO INVALID INPUTS 264 append_error( el, text ) { 265 let id = el.id + '-error'; 266 let item = document.getElementById( id ); 267 let html = document.createElement( 'div' ); 268 269 html.id = id; 270 html.innerHTML = this.sanitize( text ); 271 html.classList.add( 'form-error' ); 272 273 el.setAttribute( 'aria-describedby', el.id + '-error' ); 274 275 // If defined, insert the error into a custom container element 276 if ( el.dataset.container && document.querySelector( el.dataset.container ) ) { 277 document.querySelector( el.dataset.container ).appendChild( html ) ; 278 return; 279 } 280 281 // If element is a fieldset, insert the error before the first label 282 if ( 'fieldset' == el.type ) { 283 el = el.querySelector( 'label' ); 284 } 285 286 el.before( html ); 287 } 288 289 290 291 // REMOVE ALL ERRORS BEFORE VALIDATION 292 remove_errors() { 293 document.querySelectorAll( '.form-error' ).forEach( error => { 294 let input = document.getElementById( error.id.replace( /-error$/, '' ) ); 295 input?.removeAttribute( 'aria-describedby' ); // Remove 'aria-describedby' attribute from the input 296 error.remove(); // Remove the error 297 }); 298 } 299 300 301 302 // SET FOCUS ON FIRST INPUT WITH A FORM ERROR 303 set_error_focus( errors ) { 304 305 // Get the first element with an error 306 let input = errors[0]['input']; 307 if ( !input ) { 308 return; 309 } 310 311 // Set role="alert" on the error associated with this input 312 let error = document.getElementById( input.id + '-error' ); 313 if ( error ) { 314 error.role = 'alert'; 315 } 316 317 // If the element is a fieldset, select the first checkbox/radio button input within 318 if ( 'fieldset' == input.type ) { 319 input = input.querySelector( 'input' ); 320 } 321 322 // Focus on the input 323 input.focus(); 324 } 325 326 327 328 // SANITIZE ERROR TEXT 329 sanitize( text ) { 330 const temp = document.createElement( 'div' ); 331 temp.innerHTML = text; 332 const sanitized = temp.textContent; 333 return sanitized; 334 } 335 336 337 338 // RETURN ELEMENT'S DISPLAY NAME (element's ID or the 'data-display' attribute value, if defined) 339 get_display_name( el ) { 340 let name = el.dataset.display ? el.dataset.display : el.id.replaceAll( '-', ' ' ); 341 return name; 342 } 343 344 345}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.