1class UserAgentComponent { 2 container = $("#divuseragent"); 3 txtMessageContainer = $("#divuseragentmessage"); 4 txtMessage = $("#spanuseragentmessage"); 5 txtCounter = $("#divPopUpLeadFormCounter"); 6 dialogContainer = $("#modal-useragent"); 7 contentContainer= $("#divAgentDialogContent"); 8 leadQuestionContainer = null; 9 zipCodeContainer = null; 10 11 dialogRef = null; 12 dialogInitialized = false; // Track if jQuery UI dialog has been initialized 13 static jqueryUILoading = false; // Track if jQuery UI is currently loading 14 static jqueryUILoaded = false; // Track if jQuery UI has been loaded 15 mode = 1; // 1 = minized , 2 = opened dialog 16 isOpen = false; 17 isSimpleForm = false; // Track if this is a simple form vs wizard 18 19 // Draggable state 20 isDragging = false; 21 justFinishedDragging = false; 22 avatarDraggable = false; // Admin setting to allow avatar dragging on mobile 23 24 //lead data 25 leadText = ""; 26 leadId = 0; 27 zipCode = ""; 28 email = ""; 29 phone = ""; 30 name = ""; 31 notes = ""; 32 firstAnswer =""; 33 firstQuestion = ""; 34 optInService = false; 35 optInServiceText = ""; 36 optInMarketing = false; 37 leadformVariantId = null; // ID of the form variant, null for default form 38 isSubmitting = false; // Prevent double-submission 39 40 // Address step data 41 address = ""; 42 addressCity = ""; 43 addressState = ""; 44 addressZip = ""; 45 enableAddressStep = false; 46 autocomplete = null; // Google Places Autocomplete instance 47 48 49 autoOpen = false; 50 autoOpenDelay = 0; 51 autoOpenSessionMode = true; 52 autoOpenMouseMode = false; 53 showPopupThankYou = false; 54 autoOpenRef = null; 55 sessionOpenedKey = "popup.Opened"; 56 constructor(delay, message, showAnimation , showPopupThankYou = false) { 57 58 this.delay = delay; 59 this.message = message; 60 this.showAnimation = showAnimation; 61 this.setMessage(this.message); 62 this.showPopupThankYou = showPopupThankYou; 63 64 // Dialog initialization is deferred until jQuery UI is loaded (lazy-load for performance) 65 // Event handlers are set up now but dialog() is called later in initDialog() 66 this.setupDialogEventHandlers(); 67 68 this.container.click( (event) => { 69 // Don't open dialog if clicking the close button 70 if ($(event.target).closest('.user-agent-bubble-close').length) { 71 return; 72 } 73 // Don't open dialog if we just finished dragging 74 if (this.justFinishedDragging) { 75 this.justFinishedDragging = false; 76 return; 77 } 78 this.switchOpen(); 79 }); 80 81 // Close button for the message bubble 82 $("#btnCloseBubbleMessage").click( (event) => { 83 event.stopPropagation(); 84 85 // Get avatar position before closing message 86 const avatarBefore = this.$handle ? this.$handle[0].getBoundingClientRect() : null; 87 88 this.setMessage(""); 89 90 // Remember closure for 1 hour 91 localStorage.setItem('messageBubbleClosed', Date.now().toString()); 92 93 // Compensate for avatar shift caused by closing message 94 if (avatarBefore && this.$handle) { 95 const avatarAfter = this.$handle[0].getBoundingClientRect(); 96 const shiftX = avatarAfter.left - avatarBefore.left; 97 const shiftY = avatarAfter.top - avatarBefore.top; 98 if (shiftX !== 0) { 99 const currentLeft = parseInt(this.container.css('left')) || 0; 100 this.container.css('left', (currentLeft - shiftX) + 'px'); 101 } 102 if (shiftY !== 0) { 103 const currentTop = parseInt(this.container.css('top')) || 0; 104 this.container.css('top', (currentTop - shiftY) + 'px'); 105 } 106 } 107 }); 108 109 110 $.when(this.getLeadPage(), this.getInitialSettings()).then((leadPageResult, initialSettingsResult) => { 111 // Process initial settings FIRST to set isSimpleForm flag before processing lead page 112 this.processInitialSettingsResult(initialSettingsResult); 113 this.processLeadPageResult(leadPageResult); 114 this.setAutoOpen(); 115 116 // Load jQuery UI and initialize draggable 117 UserAgentComponent.loadJQueryUI().then(() => { 118 this.initDraggable(); 119 }); 120 }); 121 122 } 123 124 /** 125 * Set up event handlers for dialog content (works before dialog is initialized) 126 */ 127 setupDialogEventHandlers() { 128 this.dialogContainer.on( "click" , "button[data-categoryid]" , (event)=> 129 { 130 this.leadId = event.target.getAttribute("data-categoryid"); 131 this.leadText = $.trim($(event.target).text()); 132 133 this.leadQuestionContainer.hide(); 134 135 if (this.enableAddressStep) { 136 // Show address step instead of zip 137 $("#divAgentAddress").show(); 138 // Load Google Maps API and init autocomplete (lazy) 139 const apiKey = $("#hdnGoogleMapsApiKey").val(); 140 if (apiKey && !this.autocomplete) { 141 UserAgentComponent.loadGoogleMapsAPI(apiKey).then(() => { 142 this.initAddressAutocomplete(); 143 setTimeout(() => { $("#txtAgentAddress").focus(); }, 1); 144 }).catch(() => { 145 // If Maps fails to load, fall back to zip step 146 $("#divAgentAddress").hide(); 147 this.zipCodeContainer.show(); 148 setTimeout(() => { $("#txtAgentZip").focus(); }, 1); 149 }); 150 } else { 151 setTimeout(() => { $("#txtAgentAddress").focus(); }, 1); 152 } 153 } else { 154 this.zipCodeContainer.show(); 155 setTimeout(() => { 156 $("#txtAgentZip").focus(); 157 } , 1); 158 } 159 }); 160 this.dialogContainer.on( "click" , ".first-answers" , (event)=>{ 161 this.firstAnswer = $.trim($(event.target).text()); 162 this.firstQuestion = $("#hdnFirstQuestion").val(); 163 // Store the index of the answer option 164 this.firstAnswerIndex = $(event.target).data("answer-index"); 165 166 $("#divAgentFirstQuestion").hide(); 167 this.showLeadQuestion(); 168 169 }); 170 this.dialogContainer.on( "click" , "#btnAddressNext" , this.addressNext.bind(this)); 171 this.dialogContainer.on( "click" , "#btnZipCodeNext" , this.zipCodeNext.bind(this)); 172 this.dialogContainer.on( "keyup" , "#txtAgentZip" , (event) => { this.zipCodekeyUp(event); } ) 173 this.dialogContainer.on( "click" , "#btnNameNext" , this.nameNext.bind(this)); 174 this.dialogContainer.on( "keyup" , "#txtAgentName" , (event) => { this.namekeyUp(event); } ) 175 this.dialogContainer.on( "click" , "#btnNotesNext" , this.notesNext.bind(this)); 176 this.dialogContainer.on( "click" , "#btnEmailNext" , this.contactNext.bind(this)); 177 178 this.dialogContainer.on( "click" , "#chkOptInService" , (event) => { 179 if (event.target.checked) 180 $("#divOption").show(); 181 else 182 $("#divOption").hide(); 183 } ); 184 } 185 186 /** 187 * Preload jQuery UI during browser idle time (non-blocking) 188 */ 189 static preloadJQueryUI() { 190 if (UserAgentComponent.jqueryUILoaded || UserAgentComponent.jqueryUILoading) { 191 return; 192 } 193 194 const doPreload = () => { 195 UserAgentComponent.loadJQueryUI().catch(() => { 196 // Silently fail on preload - will retry on click 197 }); 198 }; 199 200 // Use requestIdleCallback if available, otherwise setTimeout 201 if (typeof requestIdleCallback !== 'undefined') { 202 requestIdleCallback(doPreload, { timeout: 3000 }); 203 } else { 204 setTimeout(doPreload, 1000); 205 } 206 } 207 208 /** 209 * Initialize draggable functionality - lazy loads the draggable module only when enabled 210 */ 211 initDraggable() { 212 // If dragging is disabled, clear any saved positions and skip loading the module 213 if (!this.avatarDraggable) { 214 localStorage.removeItem('bubblePosition'); 215 return; 216 } 217 218 // Dynamically import the draggable module only when needed 219 import('/js/pop.up.lead.form.draggable.js') 220 .then(module => { 221 module.initDraggable(this); 222 }) 223 .catch(error => { 224 console.error('Failed to load draggable module:', error); 225 }); 226 } 227 228 /** 229 * Load jQuery UI CSS and JS dynamically 230 * @returns {Promise} Resolves when jQuery UI is ready 231 */ 232 static loadJQueryUI() { 233 return new Promise((resolve, reject) => { 234 // Already loaded 235 if (UserAgentComponent.jqueryUILoaded || (typeof $.ui !== 'undefined' && typeof $.fn.dialog !== 'undefined')) { 236 UserAgentComponent.jqueryUILoaded = true; 237 resolve(); 238 return; 239 } 240 241 // Currently loading - wait for it 242 if (UserAgentComponent.jqueryUILoading) { 243 const checkLoaded = setInterval(() => { 244 if (UserAgentComponent.jqueryUILoaded) { 245 clearInterval(checkLoaded); 246 resolve(); 247 } 248 }, 50); 249 return; 250 } 251 252 UserAgentComponent.jqueryUILoading = true; 253 254 // Load CSS first 255 const cssLink = document.createElement('link'); 256 cssLink.rel = 'stylesheet'; 257 cssLink.href = '/css/jquery-ui.min.css'; 258 document.head.appendChild(cssLink); 259 260 // Load JS 261 const script = document.createElement('script'); 262 script.src = '/js/jquery-ui.min.js'; 263 script.onload = () => { 264 UserAgentComponent.jqueryUILoaded = true; 265 UserAgentComponent.jqueryUILoading = false;
266 resolve(); 267 }; 268 script.onerror = () => { 269 UserAgentComponent.jqueryUILoading = false; 270 reject(new Error('Failed to load jQuery UI')); 271 }; 272 document.body.appendChild(script); 273 }); 274 } 275 276 /** 277 * Load Google Maps Places API dynamically 278 * @param {string} apiKey 279 * @returns {Promise} 280 */ 281 static loadGoogleMapsAPI(apiKey) { 282 return new Promise((resolve, reject) => { 283 if (typeof google !== 'undefined' && typeof google.maps !== 'undefined' && typeof google.maps.places !== 'undefined') { 284 resolve(); 285 return; 286 } 287 // Define auth failure handler before loading (if not already defined by inline form) 288 if (!window.gm_authFailure) { 289 window.gm_authFailure = function() { 290 window.googleMapsAuthFailed = true; 291 }; 292 } 293 const script = document.createElement('script'); 294 script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`; 295 script.onload = () => resolve(); 296 script.onerror = () => reject(new Error('Failed to load Google Maps API')); 297 document.body.appendChild(script); 298 }); 299 } 300 301 /** 302 * Initialize Google Places Autocomplete on the address input 303 */ 304 initAddressAutocomplete() { 305 const input = document.getElementById('txtAgentAddress'); 306 if (!input || typeof google === 'undefined' || window.googleMapsAuthFailed) { 307 // Google Maps not available â field works as plain text input 308 if (input) { 309 input.placeholder = 'Enter the property address'; 310 } 311 return; 312 } 313 314 // Protect the input BEFORE Google's Autocomplete touches it. 315 // MutationObserver undoes Google's auth-failure behavior (disabled, error class, error icon) 316 // instantly, and capturing keyboard listeners block Google's keystroke-swallowing handlers. 317 const originalPlaceholder = input.placeholder || 'Enter the property address'; 318 const observer = new MutationObserver(() => { 319 let needsFocus = false; 320 if (input.disabled) { 321 input.disabled = false; 322 needsFocus = true; // disabled steals focus â restore it 323 window.googleMapsAuthFailed = true; 324 } 325 if (input.classList.contains('gm-err-autocomplete')) { 326 input.classList.remove('gm-err-autocomplete'); 327 input.style.backgroundImage = 'none'; 328 window.googleMapsAuthFailed = true; 329 } 330 if (input.placeholder === 'Oops! Something went wrong.') { 331 input.placeholder = originalPlaceholder; 332 } 333 if (needsFocus) input.focus(); 334 }); 335 observer.observe(input, { attributes: true, attributeFilter: ['disabled', 'class', 'style', 'placeholder'] }); 336 ['keydown', 'keypress', 'keyup'].forEach(evtType => { 337 input.addEventListener(evtType, e => { 338 if (window.googleMapsAuthFailed) e.stopImmediatePropagation(); 339 }, true); 340 }); 341 342 this.autocomplete = new google.maps.places.Autocomplete(input, { 343 types: ['address'], 344 componentRestrictions: { country: ['us'] }, 345 fields: ['address_components', 'formatted_address'] 346 }); 347 348 this.autocomplete.addListener('place_changed', () => { 349 const place = this.autocomplete.getPlace(); 350 if (!place || !place.address_components) return; 351 this.parseAddressComponents(place.address_components); 352 }); 353 } 354 355 /** 356 * Parse address components from Google Places result 357 * @param {Array} components 358 */ 359 parseAddressComponents(components) { 360 let streetNumber = ''; 361 let route = ''; 362 let city = ''; 363 let state = ''; 364 let zip = ''; 365 366 for (const component of components) { 367 const types = component.types; 368 if (types.includes('street_number')) { 369 streetNumber = component.long_name; 370 } else if (types.includes('route')) { 371 route = component.long_name; 372 } else if (types.includes('locality')) { 373 city = component.long_name; 374 } else if (types.includes('sublocality_level_1') && !city) { 375 city = component.long_name; 376 } else if (types.includes('administrative_area_level_1')) { 377 state = component.short_name; 378 } else if (types.includes('postal_code')) { 379 zip = component.long_name; 380 } 381 } 382 383 const street = (streetNumber + ' ' + route).trim(); 384 $('#hdnAddressStreet').val(street); 385 $('#hdnAddressCity').val(city); 386 $('#hdnAddressState').val(state); 387 $('#hdnAddressZip').val(zip); 388 } 389 390 /** 391 * Handle address step Continue button 392 */ 393 addressNext(event) { 394 event.preventDefault(); 395 event.stopPropagation(); 396 397 const city = $.trim($('#hdnAddressCity').val()); 398 const state = $.trim($('#hdnAddressState').val()); 399 const street = $.trim($('#hdnAddressStreet').val()); 400 const addressText = $.trim($('#txtAgentAddress').val()); 401 402 if (!addressText) { 403 $('#divAgentAddressError').text('Please enter the property address.').show(); 404 return false;
405 } 406 407 // If Google Maps auth failed, accept free-text and skip service area check 408 if (window.googleMapsAuthFailed) { 409 this.address = addressText; 410 this.addressCity = ''; 411 this.addressState = ''; 412 this.addressZip = ''; 413 this.zipCode = '00000'; 414 $('#divAgentAddress').hide(); 415 $('#divAgentName').show(); 416 setTimeout(() => { $('#txtAgentName').focus(); }, 1); 417 return false; 418 } 419 420 if (!city || !state) { 421 $('#divAgentAddressError').text('Please select a complete address from the suggestions.').show(); 422 return false; 423 } 424 425 // Save address data and proceed â accept all addresses 426 this.address = street; 427 this.addressCity = city; 428 this.addressState = state; 429 this.addressZip = $.trim($('#hdnAddressZip').val()); 430 this.zipCode = this.addressZip || '00000'; 431 432 $('#divAgentAddress').hide(); 433 $('#divAgentName').show(); 434 setTimeout(() => { $('#txtAgentName').focus(); }, 1); 435 436 return false; 437 } 438 439 /** 440 * Initialize jQuery UI dialog (called after jQuery UI is loaded) 441 */ 442 initDialog() { 443 if (this.dialogInitialized) return; 444 445 const showPopupThankYou = this.showPopupThankYou; 446 this.dialogRef = this.dialogContainer.dialog({ 447 autoOpen: false, 448 width: 'auto', 449 modal: true, 450 close: function() { 451 this.reshowAgent(); 452 this.mode = 1; 453 }.bind(this), 454 open: function() { 455 this.mode = 2; 456 this.leadQuestionContainer = $("#divAgentLeadQuestion"); 457 this.zipCodeContainer = $("#divAgentZipCode"); 458 $('.ui-widget-overlay').on('click', function() { 459 this.dialogContainer.dialog('close'); 460 }.bind(this)); 461 this.showFirstStep(); 462 if (showPopupThankYou) { 463 $("#divAgentFirstQuestion").hide(); 464 this.leadQuestionContainer.hide(); 465 $("#divAgentThankYou").show(); 466 $("#divAgentFooter").hide(); 467 } 468 }.bind(this) 469 }); 470 471 this.dialogInitialized = true; 472 } 473 474 openDialog() { 475 let title = $("#hdnUserAgentTitle").val(); 476 sessionStorage.setItem(this.sessionOpenedKey, "true"); 477 this.dialogContainer.dialog("option", "title", title); 478 this.dialogRef.dialog("open"); 479 } 480 481 show() { 482 if (this.showAnimation) { 483 this.container.delay(this.delay).css("display", "flex").fadeIn(200); 484 485 } else { 486 this.container.delay(this.delay).css("display", "flex").fadeIn(200); 487 } 488 } 489 490 491 reshowAgent() { 492 // Clear message while container is still hidden 493 this.setMessage(""); 494 495 // If we have a saved avatar position, restore it BEFORE showing container 496 if (this.avatarPositionBeforeDialog && this.$handle) { 497 // Show container invisibly to measure position 498 this.container.css({ 499 "display": "flex", 500 "visibility": "hidden" 501 }); 502 503 // Calculate and apply compensation for both horizontal and vertical shifts 504 const avatarAfter = this.$handle[0].getBoundingClientRect(); 505 const shiftX = avatarAfter.left - this.avatarPositionBeforeDialog.left; 506 const shiftY = avatarAfter.top - this.avatarPositionBeforeDialog.top; 507 508 if (shiftX !== 0) { 509 const currentLeft = parseInt(this.container.css('left')) || 0; 510 this.container.css('left', (currentLeft - shiftX) + 'px'); 511 } 512 if (shiftY !== 0) { 513 const currentTop = parseInt(this.container.css('top')) || 0; 514 this.container.css('top', (currentTop - shiftY) + 'px'); 515 } 516 517 // Now make visible - avatar is already at correct position 518 this.container.css("visibility", "visible"); 519 this.avatarPositionBeforeDialog = null; 520 } else { 521 this.container.css("display", "flex"); 522 } 523 524 $("#divAgentEmail").hide(); 525 $("#divAgentNotes").hide(); 526 $("#divAgentThankYou").hide(); 527 $("#divAgentFooter").show(); 528 $("#divAgentName").hide(); 529 $("#divAgentZipCode").hide(); 530 $("#divAgentAddress").hide();
531 this.showFirstStep(); 532 } 533 534 showFirstStep() { 535 if ($("#hdnEnableFirstQuestion").val() == 1) { 536 $("#divAgentFirstQuestion").show(); 537 this.leadQuestionContainer.hide(); 538 } else { 539 $("#divAgentFirstQuestion").hide(); 540 this.showLeadQuestion(); 541 } 542 } 543 showLeadQuestion() { 544 this.leadQuestionContainer.show(); 545 setTimeout(() => { 546 this.leadQuestionContainer.find("button[data-categoryid]").first().focus(); 547 } , 1); 548 } 549 550 setMessage(message) { 551 this.message = message; 552 if (this.message) { 553 // Check if message bubble was closed within the last hour 554 const closedTime = localStorage.getItem('messageBubbleClosed'); 555 const ONE_HOUR_MS = 60 * 60 * 1000; 556 if (closedTime && (Date.now() - parseInt(closedTime, 10)) < ONE_HOUR_MS) { 557 // Don't show message if closed within 1 hour 558 this.txtMessageContainer.css("display", "none"); 559 this.txtCounter.css("display", "none"); 560 return; 561 } 562 563 this.txtMessage.text(this.message); 564 565 this.txtMessageContainer.css("display", "flex"); 566 this.txtCounter.css("display", "block"); 567 } else { 568 this.txtMessageContainer.css("display", "none"); 569 this.txtCounter.css("display", "none"); 570 } 571 } 572 getInitialSettings() { 573 // Check if leadformVariantId is in URL (for preview mode) 574 const leadformVariantId = UserAgentComponent.getQueryParameter("leadformVariantId"); 575 const ajaxData = { 576 pageUrl: window.location.href 577 }; 578 579 // Add leadformVariantId if present in URL 580 if (leadformVariantId) { 581 ajaxData.leadformVariantId = leadformVariantId; 582 } 583 584 return $.ajax({ 585 url: "/popUpLeadForm.php?action=setting", 586 type: "POST", 587 dataType: "json", 588 data: ajaxData 589 } 590 ); 591 } 592 593 processInitialSettingsResult(result) { 594 if ( result && result[1] === "success" ) { 595 const data = result[0]; 596 if (data && data.resultCode > 0) { 597 // Check if this is a simple form type 598 if (data.formType === 'simple') { 599 // For simple forms, still show avatar but mark as simple 600 this.isSimpleForm = true; 601 } 602 603 // Store the leadform variant ID (null for default form, integer for variants) 604 this.leadformVariantId = data.leadformVariantId || null; 605 // Store avatar draggable setting 606 this.avatarDraggable = data.avatarDraggable === 1; 607 608 // Show avatar/agent UI for both wizard and simple forms 609 this.setMessage(data.message); 610 if ( data.imageSrc > '') 611 $('#imgAgentAvatar').prop ("src" , data.imageSrc ); 612 this.show(); 613 } 614 } 615 } 616 getLeadPage () { 617 // Check if leadformVariantId is in URL (for preview mode) 618 const leadformVariantId = UserAgentComponent.getQueryParameter("leadformVariantId"); 619 const ajaxData = { 620 pageUrl: window.location.href 621 }; 622 623 // Add leadformVariantId if present in URL 624 if (leadformVariantId) { 625 ajaxData.leadformVariantId = leadformVariantId; 626 } 627 628 return $.ajax({ 629 url: "/popUpLeadForm.php?action=category", 630 type: "POST", 631 dataType: "html", 632 data: ajaxData 633 } 634 ); 635 } 636 637 processLeadPageResult(result) { 638 if ( result && result[1] === "success" ) { 639 const data = result[0]; 640 if (data) { 641 // Both simple and wizard forms go into the dialog 642 // Use vanilla JS to avoid jQuery script parsing issues with simple form 643 if (this.isSimpleForm) { 644 this.contentContainer[0].innerHTML = data; 645 // Manually execute scripts for simple form 646 const scripts = this.contentContainer[0].getElementsByTagName('script'); 647 for (let i = 0; i < scripts.length; i++) { 648 eval(scripts[i].innerHTML); 649 } 650 651 // If in preview mode (showPopupThankYou), show thank you content for simple forms 652 if (this.showPopupThankYou) { 653 const hideFormShowThankYou = () => { 654 const formContainer = document.querySelector('.simple-lead-form'); 655 const thankYouContent = document.getElementById('thankYouContent'); 656 if (formContainer && thankYouContent) { 657 // Hide all direct children EXCEPT thankYouContent 658 Array.from(formContainer.children).forEac
658h(child => { 659 if (child.id !== 'thankYouContent') { 660 child.style.display = 'none'; 661 } 662 }); 663 // Show and center the thank you content 664 thankYouContent.style.display = 'block'; 665 thankYouContent.style.textAlign = 'center'; 666 // Also ensure the container itself is visible 667 formContainer.style.display = 'block'; 668 } 669 }; 670 671 // Try immediately and also after delays to handle any timing issues 672 hideFormShowThankYou(); 673 setTimeout(hideFormShowThankYou, 100); 674 setTimeout(hideFormShowThankYou, 500); 675 } 676 } else { 677 // Standard wizard - put content in dialog 678 this.contentContainer.html(data); 679 // Read address step setting from hidden input 680 this.enableAddressStep = ($("#hdnEnableAddressStep").val() == 1); 681 } 682 } 683 } 684 685 } 686 687 setAutoOpen() { 688 this.autoOpen = ($("#hdnAutoOpen").val() == 1); 689 this.autoOpenDelay = parseInt($("#hdnAutoOpenDelay").val()); 690 this.autoOpenSessionMode = ($("#hdnAutoOpenOncePreSession").val() == 1); 691 this.autoOpenMouseMode = ($("#hdnOpenOnMouseOut").val() == 1); 692 let alreadyOpened = false; 693 const sessionKey = "autoOpened"; 694 if ( this.autoOpenSessionMode) 695 { 696 let sessionValue = sessionStorage.getItem(sessionKey); 697 if (sessionValue === "true") 698 alreadyOpened = true; 699 else 700 sessionStorage.setItem(sessionKey, "true"); 701 } 702 703 704 if ( this.mode === 1 && this.autoOpen && this.autoOpenDelay > 2 && !alreadyOpened) 705 { 706 this.autoOpenRef = setTimeout(() => { 707 this.switchOpen(); 708 }, this.autoOpenDelay*1000); 709 } 710 711 if ( this.autoOpenMouseMode) 712 { 713 $(document).on("mouseleave", () => { 714 const openedBefore = sessionStorage.getItem(this.sessionOpenedKey); 715 if ( this.mode === 1 && openedBefore !== "true") 716 { 717 this.switchOpen(); 718 } 719 }); 720 } 721 722 723 } 724 /** 725 * Validate zip code or ca postal code 726 * @param zipCode 727 * @returns {boolean} 728 */ 729 validateZipCode(zipCode) { 730 const zipCodePattern = /^\d{5}(?:[-\s]\d{4})?$/; 731 // noinspection RegExpAnonymousGroup 732 const caRegex = new RegExp(/^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ]( )?\d[ABCEGHJKLMNPRSTVWXYZ]\d$/i); 733 return zipCodePattern.test(zipCode) || caRegex.test(zipCode); 734 } 735 736 zipCodekeyUp(event) { 737 if (event.key === "Enter") { 738 this.zipCodeNext(event); 739 } 740 } 741 zipCodeNext ( event ) { 742 event.preventDefault(); 743 event.stopPropagation(); 744 let zip = $.trim($("#txtAgentZip").val()); 745 746 if (zip.length == 0) { 747 $("#txtAgentZip").addClass("error"); 748 $("#divAgentZipError").text("zip code required."); 749 $("#divAgentZipError").show (); 750 751 return false; 752 } 753 if ( this.validateZipCode ( zip ) ) 754 { 755 this.zipCode = zip; 756 $("#divAgentZipError").hide() ; 757 $("#divAgentZipError").text(""); 758 $("#txtAgentZip").removeClass( "error" ); 759 $("#divAgentZipCode").hide(); 760 $("#divAgentName").show(); 761 setTimeout(() => { 762 $("#txtAgentName").focus(); 763 } , 1); 764 } 765 else 766 { 767 768 if ($("#divAgentZipError").data("country") && $("#divAgentZipError").data("country").toUpperCase() === "CA") 769 $("#divAgentZipError").text("Please enter a valid postal code."); 770 else 771 $("#divAgentZipError").text("Please enter a valid zip code."); 772 $("#divAgentZipError").show (); 773 $("#txtAgentZip").addClass("error"); 774 } 775 return false;
776 } 777 778 namekeyUp(event) { 779 if (event.key === "Enter") { 780 this.nameNext(event); 781 } 782 } 783 784 nameNext ( event ) { 785 event.preventDefault(); 786 event.stopPropagation(); 787 let name = $.trim($("#txtAgentName").val()); 788 789 if (name.length == 0) { 790 $("#txtAgentName").addClass("error"); 791 $("#divAgentNameError").text("Name required."); 792 $("#divAgentNameError").show (); 793 } 794 else { 795 this.name = name; 796 $("#divAgentNameError").hide(); 797 $("#divAgentNameError").text(""); 798 $("#txtAgentName").removeClass("error"); 799 $("#divAgentName").hide(); 800 $("#divAgentNotes").show(); 801 setTimeout(() => { 802 $("#txtAgentNote").focus(); 803 } , 1); 804 805 } 806 return false; 807 } 808 notesNext ( event ) { 809 event.preventDefault(); 810 event.stopPropagation(); 811 let notes = $.trim($("#txtAgentNote").val()); 812 813 if (notes.length == 0) { 814 $("#txtAgentNote").addClass("error"); 815 $("#divAgentNoteError").text("Detail required."); 816 $("#divAgentNoteError").show(); 817 } 818 else { 819 this.notes = notes; 820 $("#divAgentNoteError").hide(); 821 $("#divAgentNoteError").text(""); 822 $("#divAgentNoteError").removeClass("error"); 823 $("#divAgentNotes").hide(); 824 $("#divAgentEmail").show(); 825 setTimeout(() => { 826 $("#txtAgentEmail").focus(); 827 } , 1); 828 829 } 830 return false; 831 } 832 /* 833 * Contact next step 834 */ 835 contactNext ( event ) { 836 event.preventDefault(); 837 event.stopPropagation(); 838 839 // Prevent double-submission 840 if (this.isSubmitting) { 841 return false; 842 } 843 844 let hasError = false; 845 // noinspection RegExpAnonymousGroup 846 const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 847 // noinspection RegExpAnonymousGroup 848 const phoneRegExp = /^(1\s?)?(\d{3}|\(\d{3}\))[\s\-]?\d{3}[\s\-]?\d{4}$/ 849 let email = $.trim($("#txtAgentEmail").val()); 850 let phone = $.trim($("#txtAgentPhone").val()); 851 852 853 if (email.length == 0 || emailRegExp.test(email) == false){ 854 $("#txtAgentEmail").addClass("error"); 855 if (email.length == 0) 856 $("#divAgentEmailError").text("Email required."); 857 else 858 $("#divAgentEmailError").text("Invalid email address."); 859 $("#divAgentEmailError").show(); 860 hasError = true; 861 } 862 else 863 { 864 $("#divAgentEmailError").hide(); 865 $("#divAgentEmailError").text(""); 866 $("#txtAgentEmail").removeClass("error"); 867 } 868 if (phone.length == 0 || phoneRegExp.test(phone) == false) { 869 $("#txtAgentPhone").addClass("error"); 870 if (phone.length == 0) 871 $("#divAgentPhoneError").text("Phone required."); 872 else 873 $("#divAgentPhoneError").text("Invalid phone number."); 874 $("#divAgentPhoneError").show(); 875 hasError = true; 876 } 877 else 878 { 879 $("#divAgentPhoneError").hide(); 880 $("#divAgentPhoneError").text(""); 881 $("#txtAgentPhone").removeClass("error"); 882 } 883 if (!hasError ){ 884 this.email = email; 885 this.phone = phone; 886 this.optInService = $("#chkOptInService").prop("checked"); 887 this.optInServiceText = $.trim($("#hdnOptInServiceText").val()); 888 this.optInMarketing = $("#chkOptInMarketing").prop("checked") || false; 889 this.optInMarketingText = $.trim($("#hdnOptInMarketingText").val()); 890 // disclosureText/Version resolved server-side (not sent from client to avoid WAF blocking HTML in POST) 891 892 // Mark as submitting to prevent double-clicks 893 this.isSubmitting = true; 894 895 // Save lead and handle backend validation 896 this.saveLead( 897 // Success callback 898 () => { 899 $("#divAgentEmail").hide(); 900 $("#divAgentPhone").hide(); 901 902 const redirect = $("#hdnRedirect").val(); 903 const redirectUrl = $("#hdnRedirectUrl").val(); 904 905 if (redirect == 1 && redirectUrl.length > 0) 906 { 907 const params = new URLSearchParams(); 908 if (this.name) params.set('full_name', this.name); 909 if (this.email) params.set('email', this.email); 910 if (this.phone) params.set('phone', this.phone); 911 if (this.address) params.set('address', this.address); 912 if (this.addressCity) params.set('city', this.addressCity); 913 if (this.addressState) params.set('state', this.addressState); 914 if (this.addressZip) params.set('postal_code', this.addressZip); 915 const sep = redirectUrl.includes('?') ? '&' : '?'; 916 const finalUrl = params.toString() ? redirectUrl + sep + params.toString() : redirectUrl; 917 setTimeout(() => { 918 window.location.href = finalUrl; 919 }, 1); 920 921 } 922 else { 923 $("#divAgentEmail").hide(); 924 $("#divAgentThankYou").show(); 925 $("#divAgentFooter").hide(); 926 927 setTimeout(() => { 928 $("#divAgentThankYou").find("button.blue-box-large").first().focus(); 929 930 }, 1); 931 } 932 }, 933 // Error callback - show backend validation error 934 (errorMessage) => { 935 // Reset submission flag on error 936 this.isSubmitting = false;
937 938 // Check if error is about email 939 if (errorMessage.toLowerCase().includes('email') || 940 errorMessage.toLowerCase().includes('domain') || 941 errorMessage.toLowerCase().includes('@')) { 942 $("#txtAgentEmail").addClass("error"); 943 $("#divAgentEmailError").text(errorMessage); 944 $("#divAgentEmailError").show(); 945 } else { 946 // Show general error 947 $("#divAgentEmailError").text(errorMessage); 948 $("#divAgentEmailError").show(); 949 } 950 } 951 ); 952 } 953 return false; 954 } 955 /* 956 * Save lead data 957 */ 958 saveLead(onSuccess, onError) { 959 // const token = $("#hdnToken").val(); 960 // const requstId = $("#hdnRequestId").val(); 961 const question = $("#hdnFirstQuestion").val(); 962 $.ajax({ 963 url: "/popUpLeadForm.php?action=save", 964 type: "POST", 965 data: { 966 // "requestId": requstId, 967 // "token": token, 968 "categoryId": this.leadId, 969 "category" : this.leadText, 970 "zip": this.zipCode, 971 "name": this.name, 972 "notes": this.notes, 973 "email": this.email, 974 "phone": this.phone, 975 "firstAnswer": this.firstAnswer, 976 "firstQuestion": this.firstQuestion, 977 "firstAnswerIndex": this.firstAnswerIndex, // Add the index 978 "optInService": this.optInService, 979 "optInServiceText": this.optInServiceText, 980 "optInMarketing": this.optInMarketing, 981 "optInMarketingText": this.optInMarketingText || "", 982 "address": this.address, 983 "addressCity": this.addressCity, 984 "addressState": this.addressState, 985 "addressZip": this.addressZip, 986 "pageUrl": window.location.href, 987 "userAgent": navigator.userAgent, 988 "leadformVariantId": this.leadformVariantId, // ID of form variant (null for default) 989 // Evo A/B attribution: reads hidden fields from the inline form in the page DOM 990 // (rendered by EvoAbSplit::resolveVariantContent), not from the popup template 991 "evo_variant_id": $('input[name="evo_variant_id"]').val() || '', 992 "evo_version": $('input[name="evo_version"]').val() || '', 993 "evo_page_path": $('input[name="evo_page_path"]').val() || '' 994 }, 995 996 dataType: "json" 997 }).done((data) => { 998 if (data && data.success === false) { 999 // Backend validation failed 1000 if (onError) { 1001 onError(data.error || 'Invalid form data'); 1002 } 1003 } else { 1004 // Success 1005 if (onSuccess) { 1006 onSuccess(); 1007 } 1008 } 1009 }).fail((xhr, status, error) => { 1010 // AJAX request failed 1011 if (onError) { 1012 onError('Unable to submit form. Please try again.'); 1013 } 1014 }); 1015 1016 } 1017 1018 /** 1019 * Get query parameter from url 1020 * @param param 1021 * @returns {string} 1022 */ 1023 static getQueryParameter(param ) { 1024 const url = new URL(window.location.href); 1025 const params = new URLSearchParams(url.search); 1026 return params.get(param); 1027 } 1028 1029 switchOpen () { 1030 // Save avatar position before hiding (for position restoration on close) 1031 this.avatarPositionBeforeDialog = this.$handle ? this.$handle[0].getBoundingClientRect() : null; 1032 1033 this.container.hide(); 1034 clearTimeout(this.autoOpenRef); 1035 1036 // Lazy-load jQuery UI if not already loaded, then open dialog 1037 UserAgentComponent.loadJQueryUI() 1038 .then(() => { 1039 this.initDialog(); 1040 this.openDialog(); 1041 }) 1042 .catch((error) => { 1043 console.error('Failed to load jQuery UI:', error); 1044 // Show container again on error so user can retry 1045 this.container.css("display", "flex"); 1046 }); 1047 } 1048 1049} 1050 1051// Wait for jQuery to be available (handles deferred jQuery loading) 1052(function initPopupForm() { 1053 if (typeof jQuery === 'undefined') { 1054 setTimeout(initPopupForm, 50); 1055 return; 1056 } 1057 1058 jQuery(document).ready(function () { 1059 //userAgentComponent.setMessage(""); //no notification 1060 if (typeof window.userAgentComponent === 'undefined') 1061 { 1062 let delay = 1000; 1063 let showPopupThankYou = (UserAgentComponent.getQueryParameter("showPopupThankYou") == 1); 1064 if (showPopupThankYou)
1065 delay = 0; 1066 window.userAgentComponent = new UserAgentComponent( delay, "", true , showPopupThankYou); 1067 1068 if (showPopupThankYou) 1069 { 1070 setTimeout( () => { 1071 window.userAgentComponent.switchOpen(); 1072 } , 1073 delay+1000); 1074 } 1075 1076 } 1077 1078 // DISABLED: Badge now hidden via CSS with attribution text in form 1079 // addRecaptchaBadgeCloseButton(); 1080 }); 1081})(); 1082 1083/** 1084 * Add a close button near the reCAPTCHA badge on mobile 1085 * Remembers if user closed badge for 1 hour 1086 */ 1087function addRecaptchaBadgeCloseButton() { 1088 const isMobile = window.innerWidth <= 767; 1089 if (!isMobile) return; 1090 1091 const STORAGE_KEY = 'recaptchaBadgeClosed'; 1092 const ONE_HOUR_MS = 60 * 60 * 1000; 1093 1094 // Check if badge was closed within the last hour 1095 const closedTime = localStorage.getItem(STORAGE_KEY); 1096 const shouldHide = closedTime && (Date.now() - parseInt(closedTime, 10)) < ONE_HOUR_MS; 1097 1098 // Wait for reCAPTCHA badge to be loaded 1099 const checkForBadge = setInterval(() => { 1100 const badge = document.querySelector('.grecaptcha-badge'); 1101 if (badge && !document.querySelector('.recaptcha-close-btn')) { 1102 clearInterval(checkForBadge); 1103 1104 // If previously closed within 1 hour, keep it hidden 1105 if (shouldHide) { 1106 badge.style.display = 'none'; 1107 return; 1108 } 1109 1110 // Create close button as a separate fixed element (not inside badge) 1111 const closeBtn = document.createElement('button'); 1112 closeBtn.className = 'recaptcha-close-btn'; 1113 closeBtn.innerHTML = '×'; 1114 closeBtn.setAttribute('aria-label', 'Close reCAPTCHA badge'); 1115 1116 closeBtn.addEventListener('click', (e) => { 1117 e.preventDefault(); 1118 e.stopPropagation(); 1119 badge.style.display = 'none'; 1120 closeBtn.style.display = 'none'; 1121 // Remember closure for 1 hour 1122 localStorage.setItem(STORAGE_KEY, Date.now().toString()); 1123 }); 1124 1125 // Append to body, positioned via CSS 1126 document.body.appendChild(closeBtn); 1127 } 1128 }, 500); 1129 1130 // Stop checking after 10 seconds 1131 setTimeout(() => clearInterval(checkForBadge), 10000); 1132}
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.