PageSourceSearch

https://mts-siding-roofing.com/js/pop.up.lead.form.99calls.js?v=2026.03.09a

js mts-siding-roofing.com collected 2026-10-04 16:22:29 UTC 43,041 bytes, 1,132 lines download raw bytes

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 = '&times;';
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.