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