PageSourceSearch

https://www.livebreathescotland.com/js/main.js

js livebreathescotland.com collected 2026-10-02 07:19:05 UTC 5,835 bytes, 168 lines download raw bytes

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.