PageSourceSearch

https://charlestontransportation.com/wp-content/themes/charleston-tst/assets/js/main.js?ver=1.0.0

js charlestontransportation.com collected 2026-10-06 04:53:57 UTC 7,558 bytes, 209 lines download raw bytes

1/**
2 * Main JavaScript functionality
3 * - Smooth scroll for anchor links with header offset
4 */
5document.addEventListener('DOMContentLoaded', function() {
6    
7    /**
8     * Get the current header height dynamically
9     * Accounts for different header states and screen sizes
10     */
11    function getHeaderOffset() {
12        const header = document.getElementById('site-header');
13        if (!header) return 0;
14        
15        // Get the actual height of the header
16        const headerHeight = header.offsetHeight;
17        
18        return headerHeight;
19    }
20    
21    /**
22     * Smooth scroll to target element with header offset
23     * @param {Element} target - The target element to scroll to
24     * @param {number} duration - Animation duration in milliseconds
25     */
26    function smoothScrollTo(target, duration = 800) {
27        const header = document.getElementById('site-header');
28        const hero = document.querySelector('.hero-section') || document.querySelector('.tst-hero-section');
29        const heroHeight = hero ? hero.offsetHeight : 100;
30        const stickyThreshold = heroHeight - 10; // From header.js
31        
32        const targetPosition = target.getBoundingClientRect().top + window.pageYOffset;
33        const startPosition = window.pageYOffset;
34        
35        // Calculate final scroll position with header offset
36        let finalScrollPosition = targetPosition - getHeaderOffset();
37        
38        // If target element is after the hero, ensure we scroll past the sticky threshold
39        // This guarantees the sticky header will appear
40        if (targetPosition > heroHeight && finalScrollPosition <= stickyThreshold) {
41            finalScrollPosition = stickyThreshold + 1;
42        }
43        
44        const distance = finalScrollPosition - startPosition;
45        let startTime = null;
46        
47        // Easing function for smooth animation (easeInOutCubic)
48        function easeInOutCubic(t) {
49            return t < 0.5 
50                ? 4 * t * t * t 
51                : 1 - Math.pow(-2 * t + 2, 3) / 2;
52        }
53        
54        function animation(currentTime) {
55            if (startTime === null) startTime = currentTime;
56            const timeElapsed = currentTime - startTime;
57            const progress = Math.min(timeElapsed / duration, 1);
58            const ease = easeInOutCubic(progress);
59            
60            window.scrollTo(0, startPosition + distance * ease);
61            
62            if (timeElapsed < duration) {
63                requestAnimationFrame(animation);
64            }
65        }
66        
67        requestAnimationFrame(animation);
68    }
69    
70    /**
71     * Handle anchor link clicks
72     */
73    function initAnchorLinks() {
74        // Select all anchor links (links starting with #)
75        const anchorLinks = document.querySelectorAll('a[href^="#"]');
76        
77        anchorLinks.forEach(link => {
78            link.addEventListener('click', function(e) {
79                const href = this.getAttribute('href');
80                
81                // Skip if it's just "#" or empty
82                if (!href || href === '#') return;
83                
84                // Find the target element
85                const targetId = href.substring(1);
86                const targetElement = document.getElementById(targetId);
87                
88                // If target exists, prevent default and smooth scroll
89                if (targetElement) {
90                    e.preventDefault();
91                    
92                    // Close mobile menu if open
93                    const menuToggle = document.querySelector('.menu-toggle');
94                    const primaryMenu = document.getElementById('primary-menu');
95                    if (menuToggle && primaryMenu && menuToggle.getAttribute('aria-expanded') === 'true') {
96                        menuToggle.click();
97                    }
98                    
99                    // Perform smooth scroll
100                    smoothScrollTo(targetElement);
101                    
102                    // Update URL hash without jumping
103                    if (history.pushState) {
104                        history.pushState(null, null, href);
105                    } else {
106                        // Fallback for older browsers
107                        window.location.hash = href;
108                    }
109                }
110            });
111        });
112    }
113    
114    /**
115     * Open accordion item if it's in the URL hash
116     */
117    function openAccordionFromHash() {
118        const hash = window.location.hash;
119        if (!hash) return;
120        
121        const targetId = hash.substring(1);
122        const targetElement = document.getElementById(targetId);
123        
124        if (!targetElement) return;
125        
126        // Check if the target is an accordion collapse element
127        if (targetElement.classList.contains('accordion-collapse')) {
128            const accordionItem = targetElement.closest('.accordion-item');
129            
130            // Listen for the accordion to finish opening
131            targetElement.addEventListener('shown.bs.collapse', function onShown() {
132                // Remove the event listener after it fires once
133                targetElement.removeEventListener('shown.bs.collapse', onShown);
134                
135                // Scroll to the accordion item after it's fully expanded
136                if (accordionItem) {
137                    smoothScrollTo(accordionItem);
138                }
139            });
140            
141            // Find the Bootstrap collapse instance or create one
142            const bsCollapse = bootstrap.Collapse.getOrCreateInstance(targetElement);
143            
144            // Open the accordion item
145            bsCollapse.show();
146        } else {
147            // Regular scroll for non-accordion elements
148            setTimeout(() => {
149                smoothScrollTo(targetElement);
150            }, 100);
151        }
152    }
153    
154    /**
155     * Handle initial page load with hash
156     */
157    function handleInitialHash() {
158        const hash = window.location.hash;
159        if (hash) {
160            // Prevent default browser scroll to hash
161            if (history.scrollRestoration) {
162                history.scrollRestoration = 'manual';
163            }
164            
165            const targetId = hash.substring(1);
166            const targetElement = document.getElementById(targetId);
167            
168            if (targetElement) {
169                // Scroll to top first to prevent browser's default scroll
170                window.scrollTo(0, 0);
171                
172                // Wait a bit for page to fully load, then open accordion
173                setTimeout(() => {
174                    openAccordionFromHash();
175                }, 300);
176            }
177        }
178    }
179    
180    /**
181     * Handle hash changes (when clicking links or using browser back/forward)
182     */
183    window.addEventListener('hashchange', function(e) {
184        e.preventDefault();
185        openAccordionFromHash();
186    });
187    
188    // Initialize anchor link functionality
189    initAnchorLinks();
190    
191    // Handle initial hash on page load
192    handleInitialHash();
193    
194    // Re-initialize if new content is added dynamically
195    // (useful for AJAX-loaded content)
196    const observer = new MutationObserver(function(mutations) {
197        mutations.forEach(function(mutation) {
198            if (mutation.addedNodes.length) {
199                initAnchorLinks();
200            }
201        });
202    });
203    
204    // Observe the body for changes
205    observer.observe(document.body, {
206        childList: true,
207        subtree: true
208    });
209});

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.