PageSourceSearch

https://www.bryantstratton.edu/wp-content/plugins/bsc-toolbox/bsc-forms/js/validation.js?ver=1784902730

js bryantstratton.edu collected 2026-10-02 10:36:08 UTC 68,850 bytes, 1,582 lines download raw bytes

1( function () {
2    'use strict';
3    
4    // Load disposable email domains asynchronously
5    var disposableDomainsLoaded = false;
6    var disposableDomainsPromise = null;
7    
8    function loadDisposableDomains() {
9        if (disposableDomainsPromise) {
10            return disposableDomainsPromise;
11        }
12        
13        disposableDomainsPromise = fetch('/wp-content/plugins/bsc-toolbox/bsc-forms/data/disposable-domains.json')
14            .then(function(response) {
15                if (!response.ok) {
16                    throw new Error('Failed to load disposable domains');
17                }
18                return response.json();
19            })
20            .then(function(domains) {
21                window.disposableDomains = new Set(domains);
22                disposableDomainsLoaded = true;
23                // console.log('BSC Forms: Loaded', domains.length, 'disposable email domains');
24                return window.disposableDomains;
25            })
26            .catch(function(error) {
27                // console.warn('BSC Forms: Could not load disposable domains list:', error);
28                window.disposableDomains = new Set(); // Fallback to empty set
29                disposableDomainsLoaded = true;
30                return window.disposableDomains;
31            });
32            
33        return disposableDomainsPromise;
34    }
35    
36    // Load invalid ZIP codes asynchronously
37    var invalidZipCodesLoaded = false;
38    var invalidZipCodesPromise = null;
39    
40    function loadInvalidZipCodes() {
41        if (invalidZipCodesPromise) {
42            return invalidZipCodesPromise;
43        }
44        
45        invalidZipCodesPromise = fetch('/wp-content/plugins/bsc-toolbox/bsc-forms/data/invalid-zip-codes.json')
46            .then(function(response) {
47                if (!response.ok) {
48                    throw new Error('Failed to load invalid ZIP codes');
49                }
50                return response.json();
51            })
52            .then(function(zipCodes) {
53                window.invalidZipCodes = new Set(zipCodes);
54                invalidZipCodesLoaded = true;
55                // console.log('BSC Forms: Loaded', zipCodes.length, 'invalid ZIP codes');
56                return window.invalidZipCodes;
57            })
58            .catch(function(error) {
59                // console.warn('BSC Forms: Could not load invalid ZIP codes list:', error);
60                window.invalidZipCodes = new Set(); // Fallback to empty set
61                invalidZipCodesLoaded = true;
62                return window.invalidZipCodes;
63            });
64            
65        return invalidZipCodesPromise;
66    }
67
68    // Top-level so validateField/validateSelectField can read them even when DOMContentLoaded ran before script load
69    var isInitializing = true;
70    var BSC_SUBMIT_ONLY = true;
71
72    // Check if there are any forms with bsc-validation class FIRST
73    // Only initialize if forms are present
74    document.addEventListener( 'DOMContentLoaded', function () {
75        var hasValidationForms = document.querySelectorAll('.bsc-validation').length > 0;
76        if (!hasValidationForms) {
77            return;
78        }
79                        
80        // Global variable to check if validation script is loaded
81        window.BSC_VALIDATION_LOADED = true;
82
83        // Preload disposable domains and invalid ZIP codes in background
84        loadDisposableDomains();
85        loadInvalidZipCodes();
86
87// Function to initialize submit buttons as validation-disabled by default (only for lead-forms)
88function initializeSubmitButtonsAsDisabled() {
89    var submitButtons = document.querySelectorAll('.lead-forms .gform_footer button[type="submit"], .lead-forms form button[type="submit"], .lead-forms input[type="submit"], .lead-forms .gform_footer input[type="submit"]');
90    
91    submitButtons.forEach(function(button) {
92        // Keep button enabled but visually disabled - this allows clicks to work
93        button.disabled = false;
94        button.classList.add('disabled');
95        button.classList.remove('enabled');
96        // Add a data attribute to track validation state
97        button.setAttribute('data-validation-disabled', 'true');
98    });
99}
100
101// Function to ensure submit buttons are disabled - called multiple times for reliability (only for lead-forms)
102function ensureSubmitButtonsDisabled() {
103    var submitButtons = document.querySelectorAll('.lead-forms .gform_footer button[type="submit"], .lead-forms form button[type="submit"], .lead-forms input[type="submit"], .lead-forms .gform_footer input[type="submit"]');
104    
105    submitButtons.forEach(function(button) {
106        if (!button.classList.contains('disabled')) {
107            button.disabled = false; // Keep button clickable
108            button.classList.add('disabled');
109            button.classList.remove('enabled');
110            button.setAttribute('data-validation-disabled', 'true');
111        }
112    });
113}
114
115// Initialize submit buttons as disabled immediately when script loads
116initializeSubmitButtonsAsDisabled();
117
118// Also disable on DOMContentLoaded
119ensureSubmitButtonsDisabled();
120
121// Shared name validation: period only after Jr, Sr, Esq, Mr, Mrs; allow ñ, é
122function bscNamePeriodAllowed( value ) {
123    if ( value.indexOf( '.' ) !== -1 ) {
124        var withoutSuffix = value.replace( /\s+(Jr|Sr|Esq|Dr|Mr|Mrs)\.\s*$/gi, '' );
125        if ( withoutSuffix.indexOf( '.' ) !== -1 ) {
126            return 'Period is only allowed after Jr, Sr, Esq, Mr, or Mrs. Dr.';
127        }
128    }
129    return null;
130}
131
132// Configuration for input field validation
133var zipCodeValidationConfig = {
134    maxLength: 5,
135    pattern: /^\d{5}$/,
136    message: 'Please enter a valid 5-digit ZIP code.',
137    requiredMessage: 'Please enter a valid 5-digit ZIP code.',
138    parentSelector: '.gfield',
139    customValidation: function(value) {
140        // Check against invalid ZIP codes (async)
141        if (window.invalidZipCodes && window.invalidZipCodes.has(value)) {
142            return 'Please enter a valid zip code.';
143        }
144        return null; // No error
145    }
146};
147
148// Zip field input names across forms (Form 8/39/51/52 and form 1); used when container has no .gf-zipcode class
149var zipInputNames = ['input_1_2', 'input_8_6', 'input_39_6', 'input_51_6', 'input_52_6', 'input_71_6', 'input_4', 'input_5', 'input_6', 'input_8_5', 'input_39_5', 'input_71_5'];
150var validationConfig = {
151    '.gf-zipcode': zipCodeValidationConfig,
152    '.zipcode': zipCodeValidationConfig,
153    '.zip-code': zipCodeValidationConfig,
154    '.gf-firstname': {
155        maxLength: 50,
156        pattern: /^[A-Za-z' \-ñé.]+$/,
157        message: 'Only letters (A-Z), apostrophes, hyphens, spaces, and period after Jr/Sr/Esq/Mr/Mrs are allowed.',
158        requiredMessage: 'Please enter your first name.',
159        parentSelector: '.gfield',
160        customValidation: function(value) {
161            var periodErr = bscNamePeriodAllowed( value );
162            if ( periodErr ) {
163                return periodErr;
164            }
165            // Prevent names that start or end with spaces
166            if (value.startsWith(' ') || value.endsWith(' ')) {
167                return 'Names cannot start or end with spaces.';
168            }
169            // Prevent names that start or end with hyphens
170            if (value.startsWith('-') || value.endsWith('-')) {
171                return 'Names cannot start or end with hyphens.';
172            }
173            // Prevent consecutive hyphens
174            if (value.includes('--')) {
175                return 'Names cannot contain consecutive hyphens.';
176            }
177            // Limit total hyphens to 3
178            var hyphenCount = (value.match(/-/g) || []).length;
179            if (hyphenCount > 3) {
180                return 'Names cannot contain more than 3 hyphens.';
181            }
182            // Limit total apostrophes to 3
183            var apostropheCount = (value.match(/'/g) || []).length;
184            if (apostropheCount > 3) {
185                return 'Names cannot contain more than 2 apostrophes.';
186            }
187            // Prevent consecutive apostrophes
188            if (value.includes("''")) {
189                return 'Names cannot contain consecutive apostrophes.';
190            }
191            // Allow single space separator but require text on both sides
192            var spaceCount = (value.match(/ /g) || []).length;
193            if (spaceCount > 1) {
194                return 'Names can contain one space.';
195            }
196            if (spaceCount === 1) {
197                var parts = value.split(' ');
198                if (parts[0].length === 0 || parts[1].length === 0) {
199                    return 'Space separator must have text on both sides.';
200                }
201            }
202            // Disallow test names
203            var lowerValue = value.toLowerCase();
204            if (lowerValue === 'test' || lowerValue === 'testing' || lowerValue === 'tester') {
205                return 'This name is not allowed.';
206            }
207            return null; // No error
208        }
209    },
210    '.name_first': {
211        maxLength: 50,
212        pattern: /^[A-Za-z' \-ñé.]+$/,
213        message: 'Only letters (A-Z), apostrophes, hyphens, spaces, and period after Jr/Sr/Esq/Mr/Mrs are allowed.',
214        requiredMessage: 'Please enter your first name.',
215        parentSelector: '.gfield',
216        customValidation: function(value) {
217            var periodErr = bscNamePeriodAllowed( value );
218            if ( periodErr ) {
219                return periodErr;
220            }
221            if (value.startsWith(' ') || value.endsWith(' ')) {
222                return 'Names cannot start or end with spaces.';
223            }
224            if (value.startsWith('-') || value.endsWith('-')) {
225                return 'Names cannot start or end with hyphens.';
226            }
227            if (value.includes('--')) {
228                return 'Names cannot contain consecutive hyphens.';
229            }
230            var hyphenCount = (value.match(/-/g) || []).length;
231            if (hyphenCount > 3) {
232                return 'Names cannot contain more than 3 hyphens.';
233            }
234            var apostropheCount = (value.match(/'/g) || []).length;
235            if (apostropheCount > 3) {
236                return 'Names cannot contain more than 2 apostrophes.';
237            }
238            if (value.includes("''")) {
239                return 'Names cannot contain consecutive apostrophes.';
240            }
241            var spaceCount = (value.match(/ /g) || []).length;
242            if (spaceCount > 1) {
243                return 'Names can contain one space.';
244            }
245            if (spaceCount === 1) {
246                var parts = value.split(' ');
247                if (parts[0].length === 0 || parts[1].length === 0) {
248                    return 'Space separator must have text on both sides.';
249                }
250            }
251            var lowerValue = value.toLowerCase();
252            if (lowerValue === 'test' || lowerValue === 'testing' || lowerValue === 'tester') {
253                return 'This name is not allowed.';
254            }
255            return null;
256        }
257    },
258    '.gf-lastname': {
259        maxLength: 50,
260        pattern: /^[A-Za-z' \-ñé.]+$/,
261        message: 'Only letters (A-Z), apostrophes, hyphens, spaces, and period after Jr/Sr/Esq/Mr/Mrs are allowed.',
262        requiredMessage: 'Please enter your last name.',
263        parentSelector: '.gfield',
264        customValidation: function(value) {
265            var periodErr = bscNamePeriodAllowed( value );
266            if ( periodErr ) {
267                return periodErr;
268            }
269            // Prevent names that start or end with spaces
270            if (value.startsWith(' ') || value.endsWith(' ')) {
271                return 'Names cannot start or end with spaces.';
272            }
273            // Prevent names that start or end with hyphens
274            if (value.startsWith('-') || value.endsWith('-')) {
275                return 'Names cannot start or end with hyphens.';
276            }
277            // Prevent consecutive hyphens
278            if (value.includes('--')) {
279                return 'Names cannot contain consecutive hyphens.';
280            }
281            // Limit total hyphens to 3
282            var hyphenCount = (value.match(/-/g) || []).length;
283            if (hyphenCount > 3) {
284                return 'Names cannot contain more than 3 hyphens.';
285            }
286            // Limit total apostrophes to 3
287            var apostropheCount = (value.match(/'/g) || []).length;
288            if (apostropheCount > 3) {
289                return 'Names cannot contain more than 2 apostrophes.';
290            }
291            // Prevent consecutive apostrophes
292            if (value.includes("''")) {
293                return 'Names cannot contain consecutive apostrophes.';
294            }
295            // Allow single space separator but require text on both sides
296            var spaceCount = (value.match(/ /g) || []).length;
297            if (spaceCount > 1) {
298                return 'Names can contain one space.';
299            }
300            if (spaceCount === 1) {
301                var parts = value.split(' ');
302                if (parts[0].length === 0 || parts[1].length === 0) {
303                    return 'Space separator must have text on both sides.';
304                }
305            }
306            // Disallow test names
307            var lowerValue = value.toLowerCase();
308            if (lowerValue === 'test' || lowerValue === 'testing' || lowerValue === 'tester') {
309                return 'This name is not allowed.';
310            }
311            return null; // No error
312        }
313    },
314    '.name_last': {
315        maxLength: 50,
316        pattern: /^[A-Za-z' \-ñé.]+$/,
317        message: 'Only letters (A-Z), apostrophes, hyphens, spaces, and period after Jr/Sr/Esq/Mr/Mrs are allowed.',
318        requiredMessage: 'Please enter your last name.',
319        parentSelector: '.gfield',
320        customValidation: function(value) {
321            var periodErr = bscNamePeriodAllowed( value );
322            if ( periodErr ) {
323                return periodErr;
324            }
325            if (value.startsWith(' ') || value.endsWith(' ')) {
326                return 'Names cannot start or end with spaces.';
327            }
328            if (value.startsWith('-') || value.endsWith('-')) {
329                return 'Names cannot start or end with hyphens.';
330            }
331            if (value.includes('--')) {
332                return 'Names cannot contain consecutive hyphens.';
333            }
334            var hyphenCount = (value.match(/-/g) || []).length;
335            if (hyphenCount > 3) {
336                return 'Names cannot contain more than 3 hyphens.';
337            }
338            var apostropheCount = (value.match(/'/g) || []).length;
339            if (apostropheCount > 3) {
340                return 'Names cannot contain more than 2 apostrophes.';
341            }
342            if (value.includes("''")) {
343                return 'Names cannot contain consecutive apostrophes.';
344            }
345            var spaceCount = (value.match(/ /g) || []).length;
346            if (spaceCount > 1) {
347                return 'Names can contain one space.';
348            }
349            if (spaceCount === 1) {
350                var parts = value.split(' ');
351                if (parts[0].length === 0 || parts[1].length === 0) {
352                    return 'Space separator must have text on both sides.';
353                }
354            }
355            var lowerValue = value.toLowerCase();
356            if (lowerValue === 'test' || lowerValue === 'testing' || lowerValue === 'tester') {
357                return 'This name is not allowed.';
358            }
359            return null;
360        }
361    },
362    '.gf-email': {
363        maxLength: 254,
364        pattern: /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/,
365        message: 'Please enter a valid email address.',
366        requiredMessage: 'Please enter a valid email address.',
367        parentSelector: '.gfield',
368        customValidation: function(value, field) {
369            var atPosition = value.indexOf('@');
370            if (atPosition !== -1) {
371                var localPart = value.substring(0, atPosition);
372                var domainPart = value.substring(atPosition + 1);
373                
374                // Check for consecutive periods in local part
375                if (localPart.includes('..')) {
376                    return 'Email address cannot contain consecutive periods.';
377                }
378                
379                // Check for maximum of 3 periods before the @ symbol
380                var periodCount = (localPart.match(/\./g) || []).length;
381                if (periodCount > 3) {
382                    return 'Email address cannot contain more than 3 periods before the @ symbol.';
383                }
384                
385                // Check for consecutive periods in domain part
386                if (domainPart.includes('..')) {
387                    return 'Email address cannot contain consecutive periods.';
388                }
389                
390                // Check for maximum of 3 periods after the @ symbol
391                var domainPeriodCount = (domainPart.match(/\./g) || []).length;
392                if (domainPeriodCount > 3) {
393                    return 'Email address cannot contain more than 3 periods after the @ symbol.';
394                }
395                
396                // Check for consecutive hyphens in domain part
397                if (domainPart.includes('--')) {
398                    return 'Email address cannot contain consecutive hyphens.';
399                }
400                
401                // Check for maximum of 3 hyphens after the @ symbol
402                var domainHyphenCount = (domainPart.match(/-/g) || []).length;
403                if (domainHyphenCount > 3) {
404                    return 'Email address cannot contain more than 3 hyphens after the @ symbol.';
405                }
406            }
407            
408            // Check for misspelled email domains
409            var lowerValue = value.toLowerCase();
410            var misspelledDomains = [
411                'aol.cm', 'aol.con', 'aple.com', 'g-mail.com', 'g.mail.com', 'gemail.com', 'gimail.com', 'gmaik.com', 'gmail.con', 'gmaile.com', 'gmaill.com', 'gmaiol.com', 'gmal.com', 'gmale.com', 'gmall.com', 'gmaol.com', 'gmaul.com', 'gmeil.com', 'gmial.com', 'gmil.com', 'gmmail.com', 'gmsil.com', 'gnail.com', 'goglemail.com', 'googel.com', 'googl.com', 'googlmail.com', 'googlr.com', 'gxm.com', 'gxm.net', 'hormail.com', 'hotmail.con', 'hotmaill.com', 'hotmal.com', 'hotmale.com', 'hotmial.com', 'hotmil.com', 'hotnail.com', 'iclaud.com', 'icloud.con', 'icoud.com', 'live.con', 'outlok.com', 'outlook.con', 'yahho.com', 'yaho.co', 'yaho.com', 'yahoo.cmo', 'yahu.com', 'yhoo.com', 'ymail.con', 'yshoo.com'
412            ];
413            
414            // Extract domain from email
415            var atPosition = lowerValue.indexOf('@');
416            if (atPosition !== -1) {
417                var domain = lowerValue.substring(atPosition + 1);
418                if (misspelledDomains.indexOf(domain) !== -1) {
419                    return 'Please enter a valid email address.';
420                }
421                
422                // Check against disposable email domains (async); skip on Request Info Test (39, 71)
423                var isTestForm = false;
424                if ( field && field.closest ) {
425                    var formEl = field.closest( 'form' );
426                    var formId = formEl ? ( formEl.id || '' ) : '';
427                    isTestForm = formId === 'gform_39' || formId === 'gform_71';
428                }
429                if ( ! isTestForm && window.disposableDomains && window.disposableDomains.has( domain ) ) {
430                    return 'Please enter a valid email address.';
431                }
432            }
433            
434            // Disallow test email addresses
435            if (lowerValue === 'test.com' || lowerValue === 'test.edu' || lowerValue === 'testing.com' || lowerValue === 'testing.edu') {
436                return 'This email address is not allowed.';
437            }
438            return null; // No error
439        }
440    },
441    '.gf-phone': {
442        maxLength: 20,
443        pattern: /^[\d\s\-\(\)\.\+]+$/,
444        message: 'Please enter a complete phone number.',
445        requiredMessage: 'Phone number is required.',
446        parentSelector: '.gfield',
447        customValidation: function(value) {
448            var digits = value.replace(/\D/g, '');
449            if ( digits.length === 11 && digits.charAt(0) === '1' ) {
450                digits = digits.slice(1);
451            }
452            if (digits.length !== 10) {
453                return value.trim().length ? 'Please enter a complete phone number.' : null;
454            }
455            
456            // Check if all 10 digits are the same
457            var firstDigit = digits[0];
458            var allSame = true;
459            for (var i = 1; i < digits.length; i++) {
460                if (digits[i] !== firstDigit) {
461                    allSame = false;
462                    break;
463                }
464            }
465            if (allSame) {
466                return 'Please enter a valid phone number.';
467            }
468            
469            // Last 7 digits cannot all be the same (e.g. 217-222-2222, 708-444-4444)
470            var last7 = digits.slice(3, 10);
471            var last7Same = true;
472            for (var j = 1; j < last7.length; j++) {
473                if (last7[j] !== last7[0]) {
474                    last7Same = false;
475                    break;
476                }
477            }
478            if (last7Same) {
479                return 'Please enter a valid phone number.';
480            }
481            
482            // Helper: is a sequence of digits consecutive forward (with 0-9 wrap)?
483            function isSequentialForward(str) {
484                for (var k = 0; k < str.length - 1; k++) {
485                    var a = parseInt(str[k], 10);
486                    var b = parseInt(str[k + 1], 10);
487                    if ((a + 1) % 10 !== b) {
488                        return false;
489                    }
490                }
491                return true;
492            }
493            // Helper: is a sequence of digits consecutive backward (with 0-9 wrap)?
494            function isSequentialBackward(str) {
495                for (var k = 0; k < str.length - 1; k++) {
496                    var a = parseInt(str[k], 10);
497                    var b = parseInt(str[k + 1], 10);
498                    if ((a - 1 + 10) % 10 !== b) {
499                        return false;
500                    }
501                }
502                return true;
503            }
504            
505            // Last 7 digits cannot be sequential (e.g. 217-234-5678, 708-654-3210, 817-345-6789)
506            if (isSequentialForward(last7) || isSequentialBackward(last7)) {
507                return 'Please enter a valid phone number.';
508            }
509            
510            // Entire 10 digits cannot be sequential with wrap (e.g. 234-567-8901, 876-543-2109)
511            if (isSequentialForward(digits) || isSequentialBackward(digits)) {
512                return 'Please enter a valid phone number.';
513            }
514            
515            return null; // No error
516        }
517    }
518};
519
520// Configuration for select field validation
521var selectValidationConfig = {
522    '.gf-program-of-interest': {
523        requiredMessage: 'Please select a program.',
524        parentSelector: '.gfield'
525    },
526    '.degree-of-interest': {
527        requiredMessage: 'Please select a degree of interest.',
528        parentSelector: '.gfield'
529    },
530    '.gf-degree-of-interest': {
531        requiredMessage: 'Please select a degree of interest.',
532        parentSelector: '.gfield'
533    },
534    '.gf-hs-grad-status': {
535        requiredMessage: 'Please select a graduation status.',
536        parentSelector: '.gfield'
537    },
538    '.gf-military-affiliation': {
539        requiredMessage: 'Please indicate your military affiliation.',
540        parentSelector: '.gfield'
541    },
542    // NOTE: .gf-campus-online removed from general validation system
543    // Campus field uses custom validation logic in forms-campus.php
544    '.gf-select': {
545        requiredMessage: 'Please make a selection.',
546        parentSelector: '.gfield'
547    }
548};
549
550// Attach validation listeners to inputs and selects. Called on gform_post_render and after DOMContentLoaded
551// so listeners are attached when the form exists (GF may render after DOMContentLoaded).
552function attachValidationListeners() {
553    var scope = document.querySelector( '.lead-forms' ) || document;
554    if ( ! scope ) {
555        return;
556    }
557
558    // Input fields: only attach if not already attached
559    Object.keys( validationConfig ).forEach( function ( selector ) {
560        var fields = scope.querySelectorAll( selector + ' input, input' + selector );
561        fields.forEach( function ( field ) {
562            if ( field.getAttribute( 'data-validation-listener' ) === 'true' ) {
563                return;
564            }
565            field.setAttribute( 'data-validation-listener', 'true' );
566            var config = validationConfig[ selector ];
567            var isNameField = field.closest( '.gf-firstname' ) || field.closest( '.gf-lastname' ) || field.closest( '.name_first' ) || field.closest( '.name_last' );
568
569            if ( isNameField ) {
570                field.addEventListener( 'blur', function() {
571                    this.setAttribute('data-touched', 'true');
572                    validateNameFields();
573                });
574            } else {
575                field.addEventListener( 'blur', function () {
576                    this.setAttribute('data-touched', 'true');
577                    validateField( this, config );
578                });
579            }
580        });
581    });
582
583    // Select fields: attach by config then fallback. Use _bscSelectListenersAttached so we can re-attach after campus script clones (Degree, etc.).
584    function attachSelectFieldListeners( root ) {
585        if ( ! root ) {
586            return;
587        }
588        Object.keys( selectValidationConfig ).forEach( function ( selector ) {
589            var containers = root.querySelectorAll( selector );
590            containers.forEach( function ( container ) {
591                var selectField = container.querySelector( 'select' );
592                if ( ! selectField || selectField._bscSelectListenersAttached ) {
593                    return;
594                }
595                selectField._bscSelectListenersAttached = true;
596                selectField.setAttribute( 'data-validation-listener', 'true' );
597                var config = selectValidationConfig[ selector ];
598                selectField.addEventListener( 'change', function () {
599                    this.setAttribute( 'data-touched', 'true' );
600                    if ( ! this.hasAttribute( 'data-validation-deferred' ) ) {
601                        validateSelectField( this, config );
602                    }
603                    if ( typeof window.BSC_SyncSelectValidation === 'function' ) {
604                        window.BSC_SyncSelectValidation();
605                    }
606                } );
607                selectField.addEventListener( 'blur', function () {
608                    this.setAttribute( 'data-touched', 'true' );
609                    if ( ! this.hasAttribute( 'data-validation-deferred' ) ) {
610                        validateSelectField( this, config );
611                    }
612                    if ( typeof window.BSC_SyncSelectValidation === 'function' ) {
613                        window.BSC_SyncSelectValidation();
614                    }
615                } );
616            } );
617        } );
618        var genericSelectConfig = selectValidationConfig['.gf-select'];
619        if ( genericSelectConfig ) {
620            var fallbackSelects = root.querySelectorAll( '.gfield select' );
621            fallbackSelects.forEach( function ( selectField ) {
622                if ( selectField._bscSelectListenersAttached ) {
623                    return;
624                }
625                if ( selectField.name === 'input_3' || selectField.name === 'input_8_3' || selectField.name === 'input_39_3' || selectField.name === 'input_51_3' || selectField.name === 'input_52_3' || selectField.name === 'input_71_3' ) {
626                    return;
627                }
628                selectField._bscSelectListenersAttached = true;
629                selectField.setAttribute( 'data-validation-listener', 'true' );
630                selectField.addEventListener( 'change', function () {
631                    this.setAttribute( 'data-touched', 'true' );
632                    if ( ! this.hasAttribute( 'data-validation-deferred' ) ) {
633                        validateSelectField( this, genericSelectConfig );
634                    }
635                    if ( typeof window.BSC_SyncSelectValidation === 'function' ) {
636                        window.BSC_SyncSelectValidation();
637                    }
638                } );
639                selectField.addEventListener( 'blur', function () {
640                    this.setAttribute( 'data-touched', 'true' );
641                    if ( ! this.hasAttribute( 'data-validation-deferred' ) ) {
642                        validateSelectField( this, genericSelectConfig );
643                    }
644                    if ( typeof window.BSC_SyncSelectValidation === 'function' ) {
645                        window.BSC_SyncSelectValidation();
646                    }
647                } );
648            } );
649        }
650    }
651    attachSelectFieldListeners( scope );
652    setTimeout( function () {
653        var root = document.querySelector( '.lead-forms' ) || document;
654        attachSelectFieldListeners( root );
655        if ( typeof window.BSC_SyncSelectValidation === 'function' ) {
656            window.BSC_SyncSelectValidation();
657        }
658    }, 150 );
659    setTimeout( function () {
660        var root = document.querySelector( '.lead-forms' ) || document;
661        attachSelectFieldListeners( root );
662        if ( typeof window.BSC_SyncSelectValidation === 'function' ) {
663            window.BSC_SyncSelectValidation();
664        }
665    }, 450 );
666
667    // Zip fields by input name: digits-only on input; validation only on blur (same UX as other fields)
668    function attachZipFieldListeners() {
669        var root = document.querySelector( '.lead-forms' ) || document;
670        if ( ! root ) {
671            return;
672        }
673        zipInputNames.forEach( function ( name ) {
674            var inputs = root.querySelectorAll( 'input[name="' + name + '"]' );
675            inputs.forEach( function ( field ) {
676                if ( field._bscZipListenersAttached ) {
677                    return;
678                }
679                field._bscZipListenersAttached = true;
680                field.setAttribute( 'data-validation-listener', 'true' );
681                field.setAttribute( 'inputmode', 'numeric' );
682                field.addEventListener( 'input', function () {
683                    var val = this.value.replace( /\D/g, '' );
684                    if ( val.length > 5 ) {
685                        val = val.slice( 0, 5 );
686                    }
687                    this.value = val;
688                } );
689                field.addEventListener( 'keypress', function ( e ) {
690                    var allowedKeys = [ 8, 9, 27, 13, 46 ];
691                    var isDigit = ( e.which >= 48 && e.which <= 57 );
692                    if ( ! isDigit && allowedKeys.indexOf( e.which ) === -1 ) {
693                        e.preventDefault();
694                    }
695                } );
696                field.addEventListener( 'blur', function () {
697                    this.setAttribute( 'data-touched', 'true' );
698                    validateField( this, zipCodeValidationConfig );
699                } );
700            } );
701        } );
702    }
703    attachZipFieldListeners();
704    setTimeout( attachZipFieldListeners, 150 );
705    setTimeout( attachZipFieldListeners, 450 );
706
707    // Sync: apply is-valid to any select that already has a valid value (covers programmatic selection / re-render)
708    function syncSelectValidationState() {
709        var root = document.querySelector( '.lead-forms' ) || document;
710        var selects = root.querySelectorAll( '.gfield select' );
711        selects.forEach( function ( selectField ) {
712            if ( selectField.name === 'input_3' || selectField.name === 'input_8_3' || selectField.name === 'input_39_3' || selectField.name === 'input_51_3' || selectField.name === 'input_52_3' || selectField.name === 'input_71_3' ) {
713                return;
714            }
715            var opt = selectField.options[ selectField.selectedIndex ];
716            var hasValid = opt && opt.value !== '' && ! opt.disabled;
717            if ( ! hasValid ) {
718                return;
719            }
720            var container = selectField.closest( '.gfield' );
721            if ( ! container || container.classList.contains( 'is-valid' ) ) {
722                return;
723            }
724            var config = null;
725            Object.keys( selectValidationConfig ).forEach( function ( selector ) {
726                var cls = selector.replace( '.', '' );
727                if ( container.classList.contains( cls ) ) {
728                    config = selectValidationConfig[ selector ];
729                }
730            } );
731            if ( ! config ) {
732                config = selectValidationConfig['.gf-select'];
733            }
734            if ( config ) {
735                selectField.setAttribute( 'data-touched', 'true' );
736                validateSelectField( selectField, config, true );
737            }
738        } );
739        if ( typeof updateSubmitButtonState === 'function' ) {
740            updateSubmitButtonState();
741        }
742    }
743    syncSelectValidationState();
744    window.BSC_SyncSelectValidation = syncSelectValidationState;
745    // Delayed sync to catch programmatic or async selection (e.g. when change does not fire)
746    setTimeout( syncSelectValidationState, 300 );
747    setTimeout( syncSelectValidationState, 800 );
748    var syncInterval = setInterval( syncSelectValidationState, 400 );
749    setTimeout( function () { clearInterval( syncInterval ); }, 2500 );
750
751    // Allow validation to run after listeners are attached (e.g. when form rendered via gform_post_render)
752    isInitializing = false;
753}
754
755// Helper function to get field container
756function getFieldContainer( field, selector ) {
757    return field.closest( selector );
758}
759
760// Helper function to set validation state
761function setValidationState( field, container, isValid, message, forceValidation ) {
762    if ( ! container ) {
763        return;
764    }
765    
766    // PRECISE FIX: Add is-empty class for empty fields and block validation states
767    var fieldValue = field.value ? field.value.trim() : '';
768    var hasBeenTouched = field.hasAttribute('data-touched');
769    var isEmpty = !fieldValue;
770    
771    // Add or remove is-empty class based on field content
772    if (isEmpty) {
773        container.classList.add('is-empty');
774    } else {
775        container.classList.remove('is-empty');
776    }
777    
778    if (!forceValidation && isEmpty && !hasBeenTouched) {
779        return;
780    }
781
782    // Remove existing validation states
783    container.classList.remove( 'is-valid', 'is-invalid' );
784    
785    // Remove existing feedback icons
786    var feedbackIcon = container.querySelector( '.validation-feedback' );
787    if ( feedbackIcon ) {
788        feedbackIcon.remove();
789    }
790
791    // Check if field has custom-message class
792    var hasCustomMessage = container.classList.contains('custom-message');
793    var existingCustomMessage = null;
794    
795    // If field has custom-message class, preserve the existing Gravity Forms validation message
796    if ( hasCustomMessage ) {
797        var existingMessage = container.querySelector( '.gfield_validation_message' );
798        if ( existingMessage ) {
799            existingCustomMessage = existingMessage.textContent.trim();
800        }
801    }
802
803    // Remove existing validation messages
804    var validationMessage = container.querySelector( '.gfield_validation_message' );
805    if ( validationMessage ) {
806        validationMessage.remove();
807    }
808
809    // Show validation state if field has been touched OR if forceValidation is true
810    var hasBeenTouched = field.hasAttribute('data-touched');
811    var shouldShowValidation = hasBeenTouched || forceValidation;
812    
813    if ( shouldShowValidation ) {
814        container.classList.add( isValid ? 'is-valid' : 'is-invalid' );
815        
816        // Add feedback icon
817        var newFeedbackIcon = document.createElement( 'span' );
818        newFeedbackIcon.className = 'validation-feedback';
819        var inputContainer = container.querySelector('.ginput_container');
820        if (inputContainer) {
821            inputContainer.appendChild( newFeedbackIcon );
822        }
823
824        // Show error message if field is invalid
825        if ( ! isValid ) {
826            var messageElement = document.createElement( 'div' );
827            messageElement.className = 'gfield_validation_message';
828            
829            // Use custom message if available, otherwise use provided message
830            if ( hasCustomMessage && existingCustomMessage ) {
831                messageElement.textContent = existingCustomMessage;
832            } else if ( message ) {
833                messageElement.textContent = message;
834            }
835            
836            if (inputContainer) {
837                inputContainer.parentNode.insertBefore( messageElement, inputContainer.nextSibling );
838            }
839        }
840        
841        // Update submit button state after validation (debounced)
842        if (window.submitButtonUpdateTimeout) {
843            clearTimeout(window.submitButtonUpdateTimeout);
844        }
845        window.submitButtonUpdateTimeout = setTimeout(updateSubmitButtonState, 50);
846    }
847}
848
849// Validate input fields
850function validateField( field, config, forceValidation ) {
851    // Check if field has been touched
852    var hasBeenTouched = field.hasAttribute('data-touched');
853    
854    // In submit-only mode, ignore non-forced validations UNLESS field has been touched
855    if (typeof BSC_SUBMIT_ONLY !== 'undefined' && BSC_SUBMIT_ONLY && !forceValidation && !hasBeenTouched) {
856        return true;
857    }
858    // Don't validate during initial setup
859    if (isInitializing && !forceValidation) {
860        return false;
861    }
862    
863    var value = field.value.trim();
864    var isValid = true;
865    var errorMessage = config.message;
866    
867    // For force validation (submit button click), treat empty as invalid regardless of explicit required flag
868    if (forceValidation && !value) {
869        isValid = false;
870        errorMessage = config.requiredMessage || 'This field is required.';
871    } else if ( value ) {
872        if ( config.maxLength && value.length > config.maxLength ) {
873            isValid = false;
874        }
875        if ( config.pattern && ! config.pattern.test( value ) ) {
876            isValid = false;
877        }
878        // Check custom validation if it exists
879        if ( config.customValidation && isValid ) {
880            var customError = config.customValidation( value, field );
881            if ( customError ) {
882                isValid = false;
883                errorMessage = customError;
884            }
885        }
886    } else {
887        // Field is empty
888        // Check if field is required by looking at multiple indicators
889        var isRequired = field.required || 
890                       field.hasAttribute('required') || 
891                       field.getAttribute('aria-required') === 'true' ||
892                       field.closest('.gfield').classList.contains('gfield_contains_required') ||
893                       field.closest('.gfield').querySelector('.gfield_label .gfield_required') !== null;
894        
895        if ( isRequired ) {
896            isValid = false;
897            errorMessage = config.requiredMessage || 'This field is required.';
898        } else {
899            // For optional empty fields, don't show validation state unless field has been touched
900            var hasBeenTouched = field.hasAttribute('data-touched');
901            if (!hasBeenTouched && !forceValidation) {
902                // Don't show any validation state for untouched optional empty fields
903                return true;
904            }
905            isValid = true; // Optional field can be empty
906        }
907    }
908
909    // Only call setValidationState if we want to show validation
910    var hasBeenTouched = field.hasAttribute('data-touched');
911    if (hasBeenTouched || forceValidation || isValid === false) {
912        setValidationState( field, getFieldContainer( field, config.parentSelector ), isValid, errorMessage, forceValidation );
913    }
914    return isValid;
915}
916
917// Validate select fields
918function validateSelectField( field, config, forceValidation ) {
919    // Check if field has been touched
920    var hasBeenTouched = field.hasAttribute('data-touched');
921    
922    // In submit-only mode, ignore non-forced validations UNLESS field has been touched
923    if (typeof BSC_SUBMIT_ONLY !== 'undefined' && BSC_SUBMIT_ONLY && !forceValidation && !hasBeenTouched) {
924        return true;
925    }
926    // Don't validate during initial setup
927    if (isInitializing && !forceValidation) {
928        return false;
929    }
930    
931    // Check if field has deferred validation attribute
932    if (field.hasAttribute('data-validation-deferred') && !forceValidation) {
933        return false;
934    }
935    
936    var value = field.value;
937    var isValid = true;
938    var errorMessage = config.requiredMessage || 'This field is required.';
939
940    // Check if field has a valid value selected
941    var selectedOption = field.options[field.selectedIndex];
942    var hasValidSelection = selectedOption && 
943                          selectedOption.value !== '' && 
944                          !selectedOption.disabled;
945    
946    if ( ! hasValidSelection ) {
947        isValid = false;
948    } else {
949        isValid = true;
950    }
951
952    // Only call setValidationState if we want to show validation
953    if (hasBeenTouched || forceValidation || isValid === false) {
954        setValidationState( field, getFieldContainer( field, config.parentSelector ), isValid, errorMessage, forceValidation );
955    }
956    return isValid;
957}
958
959// Validate name fields (including cross-validation). Runs on blur and on submit.
960function validateNameFields( force ) {
961    // Gravity Forms uses .name_first / .name_last; some themes use .gf-firstname / .gf-lastname
962    var firstNameField = document.querySelector( '.name_first input' ) || document.querySelector( '.gf-firstname input' );
963    var lastNameField  = document.querySelector( '.name_last input' ) || document.querySelector( '.gf-lastname input' );
964
965    if ( ! firstNameField || ! lastNameField ) {
966        return;
967    }
968
969    var firstValue = firstNameField.value.trim();
970    var lastValue = lastNameField.value.trim();
971    var firstNameConfig = validationConfig[ '.name_first' ] || validationConfig[ '.gf-firstname' ];
972    var lastNameConfig = validationConfig[ '.name_last' ] || validationConfig[ '.gf-lastname' ];
973
974    // Step 1: Perform individual validation
975    var isFirstNameValid = validateField(firstNameField, firstNameConfig);
976    var isLastNameValid = validateField(lastNameField, lastNameConfig);
977
978    // Step 2: Check if they are identical (error unless in allowed-exceptions list)
979    var identicalNameExceptions = [
980        'lee lee', 'kim kim', 'kelly kelly', 'jordan jordan', 'thomas thomas', 'jackson jackson',
981        'james james', 'allen allen', 'kelley kelley', 'lisa lisa', 'yang yang', 'david david',
982        'li li', 'lang lang', 'fang fang', 'eiki eiki', 'andrew andrew'
983    ];
984    var combinedKey = ( firstValue + ' ' + lastValue ).toLowerCase();
985    var isIdentical = firstValue && lastValue && firstValue.toLowerCase() === lastValue.toLowerCase();
986    var isAllowedIdentical = isIdentical && identicalNameExceptions.indexOf( combinedKey ) !== -1;
987    if ( isIdentical && ! isAllowedIdentical ) {
988        var identicalMessage = 'First and last name cannot be identical.';
989        setValidationState( firstNameField, getFieldContainer( firstNameField, firstNameConfig.parentSelector ), false, identicalMessage, false );
990        setValidationState( lastNameField, getFieldContainer( lastNameField, lastNameConfig.parentSelector ), false, identicalMessage, false );
991    } else if ( ! isIdentical || isAllowedIdentical ) {
992        // If names are not identical, show individual validation states
993        // Only show validation states if fields have been touched or have values
994        if (isFirstNameValid && (firstNameField.hasAttribute('data-touched') || firstValue)) {
995            setValidationState( firstNameField, getFieldContainer( firstNameField, firstNameConfig.parentSelector ), true, firstNameConfig.message, false );
996        }
997        if (isLastNameValid && (lastNameField.hasAttribute('data-touched') || lastValue)) {
998            setValidationState( lastNameField, getFieldContainer( lastNameField, lastNameConfig.parentSelector ), true, lastNameConfig.message, false );
999        }
1000    }
1001}
1002
1003// Phone number formatting
1004jQuery(document).ready(function($) {
1005    $('.gf-phone input, input.gf-phone').on('input', function(e) {
1006        var input = $(this);
1007        var value = input.val().replace(/\D/g, '');
1008        if ( value.length === 11 && value.charAt(0) === '1' ) {
1009            value = value.slice(1);
1010        }
1011        if (value.length > 10) {
1012            value = value.substr(0, 10);
1013        }
1014        
1015        // Format the number as (xxx) xxx-xxxx
1016        if (value.length > 0) {
1017            if (value.length <= 3) {
1018                value = '(' + value;
1019            } else if (value.length <= 6) {
1020                value = '(' + value.substr(0, 3) + ') ' + value.substr(3);
1021            } else {
1022                value = '(' + value.substr(0, 3) + ') ' + value.substr(3, 3) + '-' + value.substr(6);
1023            }
1024        }
1025        
1026        input.val(value);
1027    });
1028
1029    // Prevent non-numeric key presses
1030    $('.gf-phone input, input.gf-phone').on('keydown', function(e) {
1031        // Allow: backspace, delete, tab, escape, enter and arrow keys
1032        if ($.inArray(e.keyCode, [46, 8, 9, 27, 13, 37, 39]) !== -1 ||
1033            // Allow: Ctrl+A, Ctrl+C, Ctrl+V
1034            (e.keyCode === 65 && e.ctrlKey === true) ||
1035            (e.keyCode === 67 && e.ctrlKey === true) ||
1036            (e.keyCode === 86 && e.ctrlKey === true)) {
1037            return;
1038        }
1039        // Stop if not a number
1040        if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) &&
1041            (e.keyCode < 96 || e.keyCode > 105)) {
1042            e.preventDefault();
1043        }
1044    });
1045});
1046
1047// ZIP code input restrictions - centralized validation
1048jQuery(document).ready(function($) {
1049    // Add numeric inputmode to ZIP code fields for mobile keyboards
1050    function addNumericInputMode() {
1051        $('.gf-zipcode input, input.gf-zipcode').each(function() {
1052            $(this).attr('inputmode', 'numeric');
1053        });
1054    }
1055    
1056    // Run immediately
1057    addNumericInputMode();
1058    
1059    // ZIP code input formatting and validation
1060    $('.gf-zipcode input, input.gf-zipcode').on('input', function(e) {
1061        let input = $(this);
1062        let value = input.val();
1063        
1064        // Remove any non-digit characters
1065        value = value.replace(/\D/g, '');
1066        
1067        // Limit to 5 digits maximum
1068        if (value.length > 5) {
1069            value = value.slice(0, 5);
1070        }
1071        
1072        input.val(value);
1073    });
1074
1075    // Prevent non-digit key presses for ZIP code fields
1076    $('.gf-zipcode input, input.gf-zipcode').on('keypress', function(e) {
1077        // Only allow digits (0-9), backspace, delete, tab, escape, enter
1078        var allowedKeys = [8, 9, 27, 13, 46]; // backspace, tab, escape, enter, delete
1079        var isDigit = (e.which >= 48 && e.which <= 57); // 0-9
1080        
1081        if (!isDigit && allowedKeys.indexOf(e.which) === -1) {
1082            e.preventDefault();
1083            return false;
1084        }
1085    });
1086});
1087
1088// Force validation on all fields to show validation messages
1089function forceValidationOnAllFields() {
1090    // Validate all configured input fields directly via config selectors
1091    Object.keys(validationConfig).forEach(function(selector) {
1092        var config = validationConfig[selector];
1093        var inputs = document.querySelectorAll(selector + ' input, input' + selector);
1094        inputs.forEach(function(input) {
1095            var type = (input.getAttribute('type') || '').toLowerCase();
1096            if (type === 'hidden' || type === 'submit' || type === 'button' || type === 'image') {
1097                return;
1098            }
1099            // Mark as touched and force-validate
1100            input.setAttribute('data-touched', 'true');
1101            var container = getFieldContainer(input, config.parentSelector || '.gfield');
1102            if (container) {
1103                // Cache visibility for later checks
1104                var cs = window.getComputedStyle(container);
1105                container.setAttribute('data-visible', (cs.display !== 'none' && cs.visibility !== 'hidden') ? 'true' : 'false');
1106            }
1107            validateField(input, config, true);
1108        });
1109    });
1110
1111    // Zip fields by name (when container has no .gf-zipcode class)
1112    var zipScope = document.querySelector( '.lead-forms' ) || document;
1113    if ( zipScope ) {
1114        zipInputNames.forEach( function ( name ) {
1115            var inputs = zipScope.querySelectorAll( 'input[name="' + name + '"]' );
1116            inputs.forEach( function ( input ) {
1117                input.setAttribute( 'data-touched', 'true' );
1118                var container = getFieldContainer( input, '.gfield' );
1119                if ( container ) {
1120                    var cs = window.getComputedStyle( container );
1121                    container.setAttribute( 'data-visible', ( cs.display !== 'none' && cs.visibility !== 'hidden' ) ? 'true' : 'false' );
1122                }
1123                validateField( input, zipCodeValidationConfig, true );
1124            } );
1125        } );
1126    }
1127
1128    // Name fields: run cross-validation (identical check) and set validation state
1129    validateNameFields(true);
1130
1131    // Validate all configured select fields directly via config selectors
1132    Object.keys(selectValidationConfig).forEach(function(selector) {
1133        var config = selectValidationConfig[selector];
1134        var containers = document.querySelectorAll(selector);
1135        containers.forEach(function(container) {
1136            var select = container.querySelector('select');
1137            if (!select) { return; }
1138            select.setAttribute('data-touched', 'true');
1139
1140            // Campus custom handling
1141            if ( select.name === 'input_3' || select.name === 'input_8_3' || select.name === 'input_39_3' || select.name === 'input_51_3' || select.name === 'input_52_3' || select.name === 'input_71_3' ) {
1142                select.setAttribute('data-user-clicked', 'true');
1143                select.removeAttribute('data-validation-deferred');
1144                if (typeof validateCampusFieldWithPrerequisites === 'function') {
1145                    try { validateCampusFieldWithPrerequisites( true, select ); } catch (err) {}
1146                }
1147            }
1148
1149            // Cache visibility for later checks
1150            var cs = window.getComputedStyle(container);
1151            container.setAttribute('data-visible', (cs.display !== 'none' && cs.visibility !== 'hidden') ? 'true' : 'false');
1152
1153            validateSelectField(select, config, true);
1154        });
1155    });
1156
1157    // Fallback: validate any .lead-forms select not matched by config (e.g. no CSS class on gfield)
1158    var genericSelectConfig = selectValidationConfig['.gf-select'];
1159    if ( genericSelectConfig ) {
1160        var fallbackContainers = document.querySelectorAll( '.lead-forms .gfield' );
1161        fallbackContainers.forEach( function ( container ) {
1162            var select = container.querySelector( 'select' );
1163            if ( ! select ) { return; }
1164            if ( select.name === 'input_3' || select.name === 'input_8_3' || select.name === 'input_39_3' || select.name === 'input_51_3' || select.name === 'input_52_3' || select.name === 'input_71_3' ) {
1165                return; // Campus has custom validation
1166            }
1167            if ( container.classList.contains( 'is-valid' ) ) { return; }
1168            select.setAttribute( 'data-touched', 'true' );
1169            var cs = window.getComputedStyle( container );
1170            container.setAttribute( 'data-visible', ( cs.display !== 'none' && cs.visibility !== 'hidden' ) ? 'true' : 'false' );
1171            validateSelectField( select, genericSelectConfig, true );
1172        } );
1173    }
1174
1175    // Update submit button state after forcing validation (debounced)
1176    if (window.submitButtonUpdateTimeout) {
1177        clearTimeout(window.submitButtonUpdateTimeout);
1178    }
1179    window.submitButtonUpdateTimeout = setTimeout(updateSubmitButtonState, 50);
1180}
1181
1182// Check if all fields are valid for form submission (only for lead-forms)
1183function areAllFieldsValid() {
1184    var allFields = document.querySelectorAll('.lead-forms .gfield');
1185    var allValid = true;
1186    
1187    allFields.forEach(function(field, index) {
1188        // Only check fields that are visible and have actual form inputs
1189        // Use cached visibility check to avoid repeated getComputedStyle calls
1190        var isVisible = field.getAttribute('data-visible') === 'true';
1191        var hasInputs = field.querySelector('input, select, textarea') !== null;
1192        var hasLabel = field.querySelector('label') !== null;
1193        
1194        // Skip fields that are hidden, have no inputs, or no labels (likely not part of the form)
1195        if (!isVisible || !hasInputs || !hasLabel) {
1196            return;
1197        }
1198        
1199        // CRITICAL: Skip campus field completely - it has custom validation
1200        var campusField = field.querySelector(
1201            'select[name="input_3"],select[name="input_8_3"],select[name="input_39_3"],select[name="input_51_3"],select[name="input_52_3"],select[name="input_71_3"]'
1202        );
1203        if (campusField) {
1204            return; // Skip campus field entirely
1205        }
1206        
1207        var hasValid = field.classList.contains('is-valid');
1208        var fieldLabel = field.querySelector('label');
1209        var fieldName = fieldLabel ? fieldLabel.textContent.trim() : 'Unknown field';
1210        
1211        
1212        if (!hasValid) {
1213            allValid = false;
1214        }
1215    });
1216    
1217    return allValid;
1218}
1219
1220// Function to update submit button state (only for lead-forms)
1221function updateSubmitButtonState() {
1222    var submitButtons = document.querySelectorAll('.lead-forms .gform_footer button[type="submit"], .lead-forms form button[type="submit"], .lead-forms input[type="submit"], .lead-forms .gform_footer input[type="submit"]');
1223    var isValid = areAllFieldsValid();
1224    
1225    submitButtons.forEach(function(button, index) {
1226        if (isValid) {
1227            button.disabled = false;
1228            button.classList.remove('disabled');
1229            button.classList.add('enabled');
1230        } else {
1231            // Keep button enabled but visually disabled - this allows clicks to work
1232            button.disabled = false;
1233            button.classList.add('disabled');
1234            button.classList.remove('enabled');
1235            // Add a data attribute to track validation state
1236            button.setAttribute('data-validation-disabled', 'true');
1237        }
1238    });
1239}
1240
1241// Handle form submission - prevent if not all fields are valid (only for lead-forms)
1242if (typeof jQuery !== 'undefined') {
1243    // Handle Gravity Forms
1244    jQuery(document).on('gform_post_render', function() {
1245        var form = jQuery('.lead-forms .gform_wrapper form');
1246        if (form.length) {
1247            form.off('submit.validation').on('submit.validation', function(e) {
1248                // Force validation first using config-driven selectors
1249                forceValidationOnAllFields();
1250                if (!areAllFieldsValid()) {
1251                    e.preventDefault();
1252                    e.stopPropagation();
1253                    return false;
1254                }
1255
1256                // Normalize phone inputs to digits-only right before submission (does not affect validation)
1257                try {
1258                    jQuery('.gf-phone input, input.gf-phone').each(function() {
1259                        var $input = jQuery(this);
1260                        var digits = ($input.val() || '').replace(/\D/g, '').slice(0, 10);
1261                        if (digits.length === 10) {
1262                            $input.val(digits);
1263                        }
1264                    });
1265                } catch (err) {
1266                    // no-op
1267                }
1268            });
1269        }
1270        
1271        // Add direct click handlers for submit buttons (even when disabled)
1272        var submitButtons = jQuery('.lead-forms .gform_footer button[type="submit"], .lead-forms form button[type="submit"], .lead-forms input[type="submit"], .lead-forms .gform_footer input[type="submit"]');
1273        
1274        // Add multiple event types to ensure we catch the interaction
1275        submitButtons.off('click.validation mousedown.validation').on('click.validation mousedown.validation', function(e) {
1276            // Force validation first using config-driven selectors
1277            forceValidationOnAllFields();
1278            if (!areAllFieldsValid()) {
1279                e.preventDefault();
1280                e.stopPropagation();
1281                return false;
1282            }
1283        });
1284        
1285        // Also try adding a direct event listener as backup
1286        submitButtons.each(function() {
1287            var button = this;
1288            
1289            // Remove existing listeners first
1290            button.removeEventListener('click', handleSubmitClick);
1291            button.removeEventListener('mousedown', handleSubmitClick);
1292            
1293            // Add new listeners
1294            button.addEventListener('click', handleSubmitClick);
1295            button.addEventListener('mousedown', handleSubmitClick);
1296        });
1297        
1298        function handleSubmitClick(e) {
1299            // Force validation first using config-driven selectors
1300            forceValidationOnAllFields();
1301            if (!areAllFieldsValid()) {
1302                e.preventDefault();
1303                e.stopPropagation();
1304                return false;
1305            }
1306        }
1307        
1308        // Attach validation listeners when form is rendered (GF may render after DOMContentLoaded)
1309        if ( typeof attachValidationListeners === 'function' ) {
1310            attachValidationListeners();
1311        }
1312        // Initialize submit buttons as disabled and update state after form render
1313        initializeSubmitButtonsAsDisabled();
1314        updateSubmitButtonState();
1315    });
1316    
1317    // Handle regular forms (non-Gravity Forms)
1318    jQuery(document).on('DOMContentLoaded', function() {
1319        jQuery('.lead-forms form').off('submit.validation').on('submit.validation', function(e) {
1320            // Skip if this is a Gravity Form (handled above)
1321            if (jQuery(this).closest('.gform_wrapper').length) {
1322                return;
1323            }
1324            
1325            // Force validation first using config-driven selectors
1326            forceValidationOnAllFields();
1327            if (!areAllFieldsValid()) {
1328                e.preventDefault();
1329                e.stopPropagation();
1330                return false;
1331            }
1332
1333            // Normalize phone inputs to digits-only right before submission (does not affect validation)
1334            try {
1335                jQuery('.gf-phone input, input.gf-phone').each(function() {
1336                    var $input = jQuery(this);
1337                    var digits = ($input.val() || '').replace(/\D/g, '').slice(0, 10);
1338                    if (digits.length === 10) {
1339                        $input.val(digits);
1340                    }
1341                });
1342            } catch (err) {
1343                // no-op
1344            }
1345        });
1346        
1347        // Add direct click handlers for submit buttons (even when disabled)
1348        var submitButtons = jQuery('.lead-forms .gform_footer button[type="submit"], .lead-forms form button[type="submit"], .lead-forms input[type="submit"], .lead-forms .gform_footer input[type="submit"]');
1349        
1350        // Add multiple event types to ensure we catch the interaction
1351        submitButtons.off('click.validation mousedown.validation').on('click.validation mousedown.validation', function(e) {
1352            // Force validation first using config-driven selectors
1353            forceValidationOnAllFields();
1354            if (!areAllFieldsValid()) {
1355                e.preventDefault();
1356                e.stopPropagation();
1357                return false;
1358            }
1359        });
1360        
1361        // Also try adding a direct event listener as backup
1362        submitButtons.each(function() {
1363            var button = this;
1364            
1365            // Remove existing listeners first
1366            button.removeEventListener('click', handleSubmitClickRegular);
1367            button.removeEventListener('mousedown', handleSubmitClickRegular);
1368            
1369            // Add new listeners
1370            button.addEventListener('click', handleSubmitClickRegular);
1371            button.addEventListener('mousedown', handleSubmitClickRegular);
1372        });
1373        
1374        function handleSubmitClickRegular(e) {
1375            // Force validation first using config-driven selectors
1376            forceValidationOnAllFields();
1377            if (!areAllFieldsValid()) {
1378                e.preventDefault();
1379                e.stopPropagation();
1380                return false;
1381            }
1382        }
1383    });
1384}
1385
1386// Hook into Gravity Forms button click handler
1387function hookIntoGravityFormsSubmission() {
1388    if (typeof window.gform !== 'undefined' && typeof window.gform.submission !== 'undefined') {
1389        
1390        // Store the original handleButtonClick function
1391        var originalHandleButtonClick = window.gform.submission.handleButtonClick;
1392        
1393        // Override the handleButtonClick function
1394        window.gform.submission.handleButtonClick = function(button) {
1395            
1396            // Check if button is in validation-disabled state
1397            if (button.getAttribute('data-validation-disabled') === 'true') {
1398                // Force validation on all fields to show validation messages
1399                forceValidationOnAllFields();
1400                return false; // Prevent form submission
1401            }
1402            
1403            // Call the original function if validation passes
1404            return originalHandleButtonClick.call(this, button);
1405        };
1406        
1407        return true; // Successfully hooked
1408    }
1409    return false; // Not ready yet
1410}
1411
1412// Try to hook immediately
1413if (!hookIntoGravityFormsSubmission()) {
1414    
1415    // Retry every 100ms for up to 5 seconds
1416    var retryCount = 0;
1417    var maxRetries = 50;
1418    var retryInterval = setInterval(function() {
1419        retryCount++;
1420        if (hookIntoGravityFormsSubmission()) {
1421            clearInterval(retryInterval);
1422        } else if (retryCount >= maxRetries) {
1423            clearInterval(retryInterval);
1424        }
1425    }, 100);
1426}
1427
1428// Ensure submit button is clickable for validation feedback
1429setTimeout(function() {
1430    var submitButtons = [
1431        document.querySelector('#gform_submit_button_8'),
1432        document.querySelector('#gform_submit_button_39'),
1433        document.querySelector('#gform_submit_button_51'),
1434        document.querySelector('#gform_submit_button_52')
1435    ];
1436    submitButtons.forEach(function(submitButton) {
1437        if (submitButton) {
1438            submitButton.disabled = false;
1439            submitButton.classList.remove('disabled');
1440            submitButton.setAttribute('data-validation-disabled', 'true');
1441        }
1442    });
1443}, 3000);
1444
1445// Add event listeners to update submit button state when fields change (only for lead-forms)
1446function addSubmitButtonUpdateListeners() {
1447    // Listen for validation state changes
1448    var observer = new MutationObserver(function(mutations) {
1449        mutations.forEach(function(mutation) {
1450            if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
1451                updateSubmitButtonState();
1452            }
1453        });
1454    });
1455
1456    // Observe all gfield elements within lead-forms for class changes
1457    var fields = document.querySelectorAll('.lead-forms .gfield');
1458    fields.forEach(function(field) {
1459        observer.observe(field, { attributes: true, attributeFilter: ['class'] });
1460    });
1461}
1462
1463// Function to run initial validation on all fields (only for lead-forms)
1464function runInitialValidation() {
1465    // Clear any existing validation states
1466    var allFields = document.querySelectorAll('.lead-forms .gfield');
1467    
1468    allFields.forEach(function(field, index) {
1469        // AGGRESSIVE FIX: Clear ALL validation states and don't apply any to empty fields
1470        field.classList.remove('is-valid', 'is-invalid');
1471        
1472        // Remove any existing validation feedback icons
1473        var feedbackIcon = field.querySelector('.validation-feedback');
1474        if (feedbackIcon) {
1475            feedbackIcon.remove();
1476        }
1477        
1478        // Remove any existing validation messages
1479        var validationMessage = field.querySelector('.gfield_validation_message');
1480        if (validationMessage) {
1481            validationMessage.remove();
1482        }
1483        
1484        // AGGRESSIVE FIX: Don't run any validation on empty fields
1485        var inputField = field.querySelector('input');
1486        var selectField = field.querySelector('select');
1487        
1488        if (inputField && !inputField.value.trim()) {
1489            return;
1490        }
1491        
1492        if (selectField && (!selectField.value || selectField.value === '')) {
1493            return;
1494        }
1495        
1496        // Check if field has a valid pre-selected value and pre-validate it
1497        var selectField = field.querySelector('select');
1498        if (selectField) {
1499            // Skip validation if field has deferred validation attribute OR custom validation
1500            if (selectField.hasAttribute('data-validation-deferred') || selectField.hasAttribute('data-custom-validation')) {
1501                return;
1502            }
1503            
1504            // For select fields, check if they have a valid pre-selected value
1505            var selectedOption = selectField.options[selectField.selectedIndex];
1506            var hasValidSelection = selectedOption && 
1507                                  selectedOption.value !== '' && 
1508                                  !selectedOption.disabled;
1509            
1510            if (hasValidSelection) {
1511                // Find the corresponding config for this select field
1512                var fieldClass = null;
1513                Object.keys(selectValidationConfig).forEach(function(selector) {
1514                    var className = selector.replace('.', '');
1515                    if (field.classList.contains(className)) {
1516                        fieldClass = selector;
1517                    }
1518                });
1519                
1520                if (fieldClass) {
1521                    var config = selectValidationConfig[fieldClass];
1522                    // Pre-validate the field with forceValidation = true
1523                    validateSelectField(selectField, config, true);
1524                }
1525            }
1526        }
1527        
1528        // Check if field has input elements with pre-filled values
1529        var inputField = field.querySelector('input');
1530        if (inputField && inputField.value.trim()) {
1531            // Skip validation if field has deferred validation attribute
1532            if (inputField.hasAttribute('data-validation-deferred')) {
1533                return;
1534            }
1535            
1536            // Find the corresponding config for this input field
1537            var fieldClass = null;
1538            Object.keys(validationConfig).forEach(function(selector) {
1539                var className = selector.replace('.', '');
1540                if (field.classList.contains(className)) {
1541                    fieldClass = selector;
1542                }
1543            });
1544            
1545            if (fieldClass) {
1546                var config = validationConfig[fieldClass];
1547                // Pre-validate the field with forceValidation = true
1548                validateField(inputField, config, true);
1549            }
1550        }
1551        // Note: Empty input fields are intentionally not validated here
1552        // They will be validated when the user interacts with them
1553    });
1554}
1555
1556// Initialize submit buttons as disabled immediately
1557initializeSubmitButtonsAsDisabled();
1558
1559// Add numeric inputmode to ZIP code fields for mobile keyboards
1560function addNumericInputMode() {
1561    var inputs = document.querySelectorAll('.gf-zipcode input, input.gf-zipcode');
1562    inputs.forEach(function(input) {
1563        input.setAttribute('inputmode', 'numeric');
1564    });
1565}
1566
1567// Initialize submit button state and listeners; attach validation when form is present
1568setTimeout(function() {
1569    if ( typeof attachValidationListeners === 'function' ) {
1570        attachValidationListeners();
1571    }
1572    runInitialValidation();
1573    updateSubmitButtonState();
1574    addSubmitButtonUpdateListeners();
1575    addNumericInputMode(); // Add numeric keyboard for ZIP code fields
1576
1577    // Allow validation to run after initial setup
1578    isInitializing = false;
1579}, 100);
1580
1581}); // End of DOMContentLoaded event listener
1582})();

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.