PageSourceSearch

https://www.newlondonadulted.org/includes/js/form-validator.js

js newlondonadulted.org collected 2026-09-26 10:15:56 UTC 8,183 bytes, 328 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.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.