PageSourceSearch

https://www.aoascc.org/includes/js/form-validator.js?v=1.01

js aoascc.org collected 2026-09-24 22:32:21 UTC 9,035 bytes, 345 lines download raw bytes

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.