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.