1/** 2 * Live Breathe Scotland - Main JavaScript 3 * Core functionality used across all pages 4 * 5 * @version 1.0.1 6 * - Fixed forced reflow issue on resize event 7 */ 8 9(function() { 10 'use strict'; 11 12 /* ============================================ 13 MOBILE MENU 14 ============================================ */ 15 function initMobileMenu() { 16 const mobileMenuToggle = document.querySelector('.mobile-menu-toggle'); 17 const mobileMenu = document.querySelector('.mobile-menu'); 18 const mobileMenuOverlay = document.querySelector('.mobile-menu-overlay'); 19 const mobileMenuClose = document.querySelector('.mobile-menu-close'); 20 const body = document.body; 21 22 if (!mobileMenuToggle || !mobileMenu) return; 23 24 function openMenu() { 25 mobileMenu.classList.add('active'); 26 mobileMenuOverlay.classList.add('active'); 27 mobileMenuToggle.classList.add('active'); 28 mobileMenuToggle.setAttribute('aria-expanded', 'true'); 29 body.classList.add('menu-open'); 30 } 31 32 function closeMenu() { 33 mobileMenu.classList.remove('active'); 34 mobileMenuOverlay.classList.remove('active'); 35 mobileMenuToggle.classList.remove('active'); 36 mobileMenuToggle.setAttribute('aria-expanded', 'false'); 37 body.classList.remove('menu-open'); 38 } 39 40 // Toggle button 41 mobileMenuToggle.addEventListener('click', function() { 42 if (mobileMenu.classList.contains('active')) { 43 closeMenu(); 44 } else { 45 openMenu(); 46 } 47 }); 48 49 // Close button 50 if (mobileMenuClose) { 51 mobileMenuClose.addEventListener('click', closeMenu); 52 } 53 54 // Overlay click 55 if (mobileMenuOverlay) { 56 mobileMenuOverlay.addEventListener('click', closeMenu); 57 } 58 59 // Escape key 60 document.addEventListener('keydown', function(e) { 61 if (e.key === 'Escape' && mobileMenu.classList.contains('active')) { 62 closeMenu(); 63 } 64 }); 65 66 // Mobile dropdown toggles - clicking toggle button 67 const dropdownToggles = document.querySelectorAll('.mobile-dropdown-toggle'); 68 dropdownToggles.forEach(function(toggle) { 69 toggle.addEventListener('click', function(e) { 70 e.preventDefault(); 71 e.stopPropagation(); 72 const parent = this.closest('.mobile-has-dropdown'); 73 parent.classList.toggle('open'); 74 }); 75 }); 76 77 // Mobile dropdown - clicking label or entire header 78 const dropdownHeaders = document.querySelectorAll('.mobile-dropdown-header'); 79 dropdownHeaders.forEach(function(header) { 80 header.addEventListener('click', function(e) { 81 if (!e.target.closest('.mobile-dropdown-toggle')) { 82 e.preventDefault(); 83 const parent = this.closest('.mobile-has-dropdown'); 84 parent.classList.toggle('open'); 85 } 86 }); 87 }); 88 89 // Close menu when clicking non-dropdown links 90 const mobileMenuLinks = document.querySelectorAll('.mobile-menu-links > li:not(.mobile-has-dropdown) a, .mobile-dropdown-menu a'); 91 mobileMenuLinks.forEach(function(link) { 92 link.addEventListener('click', closeMenu); 93 }); 94 95 // Close menu on window resize above mobile breakpoint 96 // Use debounced resize handler with rAF to avoid forced reflows 97 let resizeTimeout; 98 let resizeTicking = false; 99 100 function handleResize() { 101 // Read layout property once at the start of the frame 102 const windowWidth = window.innerWidth; 103 const isMenuActive = mobileMenu.classList.contains('active'); 104 105 // Only write to DOM if conditions are met 106 if (windowWidth > 1390 && isMenuActive) { 107 closeMenu(); 108 } 109 resizeTicking = false; 110 } 111 112 window.addEventListener('resize', function() { 113 // Debounce: clear previous timeout 114 clearTimeout(resizeTimeout); 115 116 // Throttle with rAF to batch reads/writes 117 if (!resizeTicking) { 118 resizeTimeout = setTimeout(function() { 119 requestAnimationFrame(handleResize); 120 resizeTicking = true; 121 }, 100); 122 } 123 }, { passive: true }); 124 } 125 126 /* ============================================ 127 HOMEPAGE NAV SCROLL EFFECT 128 ============================================ */ 129 function initNavScrollEffect() { 130 const nav = document.getElementById('nav'); 131 if (!nav || !nav.classList.contains('nav-homepage')) return; 132 133 let ticking = false;
134 135 function updateNav() { 136 if (window.scrollY > 50) { 137 nav.classList.add('scrolled'); 138 } else { 139 nav.classList.remove('scrolled'); 140 } 141 ticking = false; 142 } 143 144 window.addEventListener('scroll', function() { 145 if (!ticking) { 146 requestAnimationFrame(updateNav); 147 ticking = true; 148 } 149 }, { passive: true }); 150 151 // Run once on load 152 updateNav(); 153 } 154 155 /* ============================================ 156 INITIALISE ON DOM READY 157 ============================================ */ 158 if (document.readyState === 'loading') { 159 document.addEventListener('DOMContentLoaded', function() { 160 initMobileMenu(); 161 initNavScrollEffect(); 162 }); 163 } else { 164 initMobileMenu(); 165 initNavScrollEffect(); 166 } 167 168})();
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.