PageSourceSearch

https://sunnygaragedoors.com/wp-content/themes/sunny-garage-doors/assets/js/main.js?ver=1.9.0

js sunnygaragedoors.com collected 2026-09-29 19:36:03 UTC 10,573 bytes, 307 lines download raw bytes

1/**
2 * Sunny Garage Doors - Main JavaScript
3 */
4(function() {
5    'use strict';
6
7    /**
8     * SVG Sprite Loader
9     * Fetches the SVG sprite and injects it into the DOM so <use href="...#icon"> works
10     */
11    function loadSVGSprite() {
12        if (!window.sunnyData || !window.sunnyData.svgUrl) return;
13
14        fetch(window.sunnyData.svgUrl)
15            .then(function(response) { return response.text(); })
16            .then(function(svgText) {
17                var div = document.createElement('div');
18                div.style.display = 'none';
19                div.innerHTML = svgText;
20                document.body.insertBefore(div, document.body.firstChild);
21
22                // Make all <use> elements visible now that sprite is loaded
23                document.querySelectorAll('svg use').forEach(function(use) {
24                    use.style.opacity = '1';
25                });
26            })
27            .catch(function() {});
28    }
29
30    /**
31     * Mobile Navigation Toggle
32     */
33    function initMobileNav() {
34        var html = document.documentElement;
35        var header = document.querySelector('.hdr');
36        if (!header) return;
37
38        // Open menu button
39        var menuBtn = header.querySelector('.mnu-btn');
40        if (menuBtn) {
41            menuBtn.addEventListener('click', function() {
42                html.classList.add('mnu-opn');
43            });
44        }
45
46        // Close menu button
47        var closeBtns = header.querySelectorAll('.mnu-nv [data-role="btn"]');
48        closeBtns.forEach(function(btn) {
49            btn.addEventListener('click', function() {
50                html.classList.remove('mnu-opn');
51            });
52        });
53
54        // Mobile submenu toggles
55        header.querySelectorAll('.mbl-nv .el-tab').forEach(function(tab) {
56            tab.addEventListener('click', function() {
57                var panel = this.nextElementSibling;
58                if (panel && panel.classList.contains('el-panel')) {
59                    panel.classList.add('active');
60                }
61            });
62        });
63
64        // Back buttons in submenus
65        header.querySelectorAll('.el-sec-tab').forEach(function(backBtn) {
66            backBtn.addEventListener('click', function() {
67                var panel = this.closest('.el-panel');
68                if (panel) {
69                    panel.classList.remove('active');
70                }
71            });
72        });
73
74        // Close menu on overlay click
75        document.addEventListener('click', function(e) {
76            if (html.classList.contains('mnu-opn') && !e.target.closest('.hdr .nv') && !e.target.closest('.mnu-btn')) {
77                html.classList.remove('mnu-opn');
78            }
79        });
80    }
81
82    /**
83     * Desktop Flyout Navigation
84     */
85    function initDesktopNav() {
86        var navItems = document.querySelectorAll('.hdr .nv > ul > li');
87        navItems.forEach(function(item) {
88            var flyout = item.querySelector('.fly-nv');
89            if (!flyout) return;
90
91            item.addEventListener('mouseenter', function() {
92                this.classList.add('active');
93            });
94            item.addEventListener('mouseleave', function() {
95                this.classList.remove('active');
96            });
97        });
98    }
99
100    /**
101     * Scroll Animations (IntersectionObserver)
102     * Adds 'anm' class to sections with data-onvisible="anm" when they enter viewport
103     */
104    function initScrollAnimations() {
105        var sections = document.querySelectorAll('[data-onvisible="anm"]');
106        if (!sections.length) return;
107
108        if (!('IntersectionObserver' in window)) {
109            // Fallback: show everything
110            sections.forEach(function(section) {
111                section.classList.add('anm');
112                section.classList.add('vsbl');
113            });
114            return;
115        }
116
117        var observer = new IntersectionObserver(function(entries) {
118            entries.forEach(function(entry) {
119                if (entry.isIntersecting) {
120                    entry.target.classList.add('anm');
121                    entry.target.classList.add('vsbl');
122                    observer.unobserve(entry.target);
123                }
124            });
125        }, {
126            threshold: 0.1,
127            rootMargin: '0px 0px -50px 0px'
128        });
129
130        sections.forEach(function(section) {
131            observer.observe(section);
132        });
133    }
134
135    /**
136     * Back to Top Button
137     */
138    function initBackToTop() {
139        var btn = document.querySelector('.btn-tp');
140        if (!btn) return;
141
142        window.addEventListener('scroll', function() {
143            if (window.scrollY > 500) {
144                document.documentElement.classList.add('header-hide');
145            } else {
146                document.documentElement.classList.remove('header-hide');
147            }
148        });
149
150        btn.addEventListener('click', function(e) {
151            e.preventDefault();
152            window.scrollTo({ top: 0, behavior: 'smooth' });
153        });
154    }
155
156    /**
157     * Service Scroller (horizontal scroll on mobile)
158     */
159    function initScrollers() {
160        document.querySelectorAll('[data-role="scroller"]').forEach(function(scroller) {
161            var container = scroller.querySelector('[data-role="container"]');
162            var list = scroller.querySelector('[data-role="list"]');
163            var prevBtn = scroller.querySelector('[data-action="Prev"]');
164            var nextBtn = scroller.querySelector('[data-action="Next"]');
165
166            if (!list || !prevBtn || !nextBtn) return;
167
168            var scrollAmount = 300;
169
170            prevBtn.addEventListener('click', function() {
171                list.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
172            });
173
174            nextBtn.addEventListener('click', function() {
175                list.scrollBy({ left: scrollAmount, behavior: 'smooth' });
176            });
177        });
178    }
179
180    /**
181     * Accordion/Tab Panels
182     */
183    function initTabs() {
184        document.querySelectorAll('.el-tab-box .el-tab.tb-arw').forEach(function(tab) {
185            tab.addEventListener('click', function() {
186                var panel = this.nextElementSibling;
187                if (panel && panel.classList.contains('el-panel')) {
188                    this.classList.toggle('active');
189                    panel.classList.toggle('active');
190
191                    if (panel.classList.contains('sld')) {
192                        if (panel.classList.contains('active')) {
193                            panel.style.height = panel.scrollHeight + 'px';
194                        } else {
195                            panel.style.height = '0';
196                        }
197                    }
198                }
199            });
200        });
201    }
202
203    /**
204     * Picture element srcset fix
205     * The source site uses src instead of srcset on <source> elements
206     */
207    function fixPictureSources() {
208        document.querySelectorAll('picture source[src]:not([srcset])').forEach(function(source) {
209            source.setAttribute('srcset', source.getAttribute('src'));
210        });
211    }
212
213    /**
214     * Sticky Appointment CTA (homepage, tablet + below).
215     * Reveals the floating CTA once the user scrolls past the hero button.
216     */
217    function initStickyCTA() {
218        var sticky = document.getElementById('StickyAppointmentCTA');
219        if (!sticky) return;
220
221        var threshold = 400; // px scrolled before the sticky button appears
222        var ticking = false;
223
224        function update() {
225            if (window.scrollY > threshold) {
226                document.body.classList.add('sticky-cta-visible');
227                sticky.setAttribute('aria-hidden', 'false');
228            } else {
229                document.body.classList.remove('sticky-cta-visible');
230                sticky.setAttribute('aria-hidden', 'true');
231            }
232            ticking = false;
233        }
234
235        window.addEventListener('scroll', function() {
236            if (!ticking) {
237                window.requestAnimationFrame(update);
238                ticking = true;
239            }
240        }, { passive: true });
241
242        update();
243    }
244
245    /**
246     * Toggle the Sameday chat button based on the ServiceTitan scheduler.
247     * When the ST web-scheduler dialog is open, add `st-widget-open` to <body>
248     * (CSS then hides #sameday-chat-button); remove it when the dialog closes.
249     */
250    function initServiceTitanChatToggle() {
251        function stOpen() {
252            var els = document.querySelectorAll(
253                'iframe[src*="servicetitan"], iframe[src*="webscheduler"], [id*="stwidget"], [class*="stwidget"], [id*="st-web-scheduler"]'
254            );
255            for (var i = 0; i < els.length; i++) {
256                var e = els[i];
257                // ST preloads the scheduler iframe at full size but
258                // visibility:hidden while closed — so require it to be
259                // actually visible, not just present and sized.
260                if (e.offsetWidth > 100 && e.offsetHeight > 100) {
261                    var visible;
262                    if (typeof e.checkVisibility === 'function') {
263                        visible = e.checkVisibility({ visibilityProperty: true, opacityProperty: true });
264                    } else {
265                        var cs = window.getComputedStyle(e);
266                        visible = e.offsetParent !== null && cs.visibility !== 'hidden' &&
267                            cs.display !== 'none' && parseFloat(cs.opacity) > 0.01;
268                    }
269                    if (visible) {
270                        return true;
271                    }
272                }
273            }
274            return false;
275        }
276        function update() {
277            document.body.classList.toggle('st-widget-open', stOpen());
278        }
279        var observer = new MutationObserver(update);
280        observer.observe(document.body, {
281            childList: true,
282            subtree: true,
283            attributes: true,
284            attributeFilter: ['style', 'class', 'hidden']
285        });
286        // Safety net in case the widget mutates in a way the observer misses.
287        setInterval(update, 500);
288        update();
289    }
290
291    /**
292     * Initialize everything on DOM ready
293     */
294    document.addEventListener('DOMContentLoaded', function() {
295        loadSVGSprite();
296        initMobileNav();
297        initDesktopNav();
298        initScrollAnimations();
299        initBackToTop();
300        initScrollers();
301        initTabs();
302        initStickyCTA();
303        fixPictureSources();
304        initServiceTitanChatToggle();
305    });
306
307})();

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.