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.