PageSourceSearch

https://www.laughingstock.ca/assets/20th/script/script.js

js laughingstock.ca collected 2026-09-26 14:01:50 UTC 21,162 bytes, 569 lines download raw bytes

1/**
2 * LFNG Landing Page - JavaScript Functionality
3 * Mobile-First Responsive Design
4 */
5
6(function() {
7    'use strict';
8
9    // DOM Elements
10    const navToggle = document.querySelector('.nav__toggle');
11    const navMenu = document.querySelector('.nav__menu');
12    const navLinks = document.querySelectorAll('.nav__link');
13    const navDropdownItems = document.querySelectorAll('.nav__item--dropdown');
14    const mobileOverlay = document.querySelector('.nav__mobile-overlay');
15    const mobileMenuItems = document.querySelectorAll('.nav__mobile-item--expandable');
16    const newsletterForm = document.querySelector('.footer__newsletter-form');
17    const newsletterInput = document.querySelector('.footer__newsletter-input');
18    const newsletterSignupForm = document.querySelector('#newsletterForm');
19
20    // Mobile Navigation Toggle
21    function initMobileNav() {
22        console.log('Initializing mobile nav...');
23        console.log('navToggle:', navToggle);
24        console.log('mobileOverlay:', mobileOverlay);
25        
26        if (!navToggle || !mobileOverlay) {
27            console.log('Missing elements - navToggle or mobileOverlay not found');
28            return;
29        }
30
31        // Open mobile menu
32        navToggle.addEventListener('click', function() {
33            const isExpanded = navToggle.getAttribute('aria-expanded') === 'true';
34            
35            // Toggle aria-expanded
36            navToggle.setAttribute('aria-expanded', !isExpanded);
37            
38            // Toggle mobile overlay
39            mobileOverlay.classList.toggle('nav__mobile-overlay--open');
40            navToggle.classList.toggle('nav__toggle--open');
41            
42            // Prevent body scroll when menu is open
43            document.body.classList.toggle('nav-open', !isExpanded);
44        });
45
46        // Close mobile menu
47        // Removed mobile close button - using hamburger/cross toggle instead
48
49        // Close menu when clicking outside
50        document.addEventListener('click', function(e) {
51            if (!navToggle.contains(e.target) && !mobileOverlay.contains(e.target)) {
52                mobileOverlay.classList.remove('nav__mobile-overlay--open');
53                navToggle.classList.remove('nav__toggle--open');
54                navToggle.setAttribute('aria-expanded', 'false');
55                document.body.classList.remove('nav-open');
56            }
57        });
58
59        // Handle escape key
60        document.addEventListener('keydown', function(e) {
61            if (e.key === 'Escape' && mobileOverlay.classList.contains('nav__mobile-overlay--open')) {
62                mobileOverlay.classList.remove('nav__mobile-overlay--open');
63                navToggle.classList.remove('nav__toggle--open');
64                navToggle.setAttribute('aria-expanded', 'false');
65                document.body.classList.remove('nav-open');
66            }
67        });
68    }
69
70    // Mobile Menu Expandable Items
71    function initMobileMenuItems() {
72        mobileMenuItems.forEach(item => {
73            const header = item.querySelector('.nav__mobile-item-header');
74            const toggle = item.querySelector('.nav__mobile-item-toggle');
75            const submenu = item.querySelector('.nav__mobile-submenu');
76            
77            if (!header || !toggle || !submenu) return;
78
79            // Set initial state - Contact should be expanded by default
80            if (item.classList.contains('nav__mobile-item--expanded')) {
81                toggle.textContent = '−';
82            } else {
83                toggle.textContent = '+';
84            }
85
86            header.addEventListener('click', function() {
87                const isExpanded = item.classList.contains('nav__mobile-item--expanded');
88                
89                if (isExpanded) {
90                    // Collapse
91                    item.classList.remove('nav__mobile-item--expanded');
92                    toggle.textContent = '+';
93                } else {
94                    // Expand
95                    item.classList.add('nav__mobile-item--expanded');
96                    toggle.textContent = '−';
97                }
98            });
99        });
100    }
101
102    // Mobile Footer Accordion Functionality
103    function initMobileFooterAccordion() {
104        const footerMobileItems = document.querySelectorAll('.footer__mobile-item');
105        
106        footerMobileItems.forEach(item => {
107            const header = item.querySelector('.footer__mobile-item-header');
108            const toggle = item.querySelector('.footer__mobile-item-toggle');
109            const content = item.querySelector('.footer__mobile-item-content');
110            
111            if (!header || !toggle || !content) return;
112
113            // Set initial state - WHERE TO BUY should be expanded by default
114            if (item.classList.contains('footer__mobile-item--expanded')) {
115                toggle.textContent = '−';
116            } else {
117                toggle.textContent = '+';
118            }
119
120            header.addEventListener('click', function() {
121                const isExpanded = item.classList.contains('footer__mobile-item--expanded');
122                
123                if (isExpanded) {
124                    // Collapse
125                    item.classList.remove('footer__mobile-item--expanded');
126                    toggle.textContent = '+';
127                } else {
128                    // Expand
129                    item.classList.add('footer__mobile-item--expanded');
130                    toggle.textContent = '−';
131                }
132            });
133        });
134    }
135
136    // Dropdown Menu Functionality
137    function initDropdownMenus() {
138        navDropdownItems.forEach(item => {
139            const dropdown = item.querySelector('.nav__dropdown');
140            const link = item.querySelector('.nav__link');
141            
142            if (!dropdown || !link) return;
143
144            // Show dropdown on mouse enter
145            item.addEventListener('mouseenter', function() {
146                dropdown.style.opacity = '1';
147                dropdown.style.visibility = 'visible';
148                dropdown.style.transform = 'translateY(0)';
149            });
150
151            // Hide dropdown on mouse leave
152            item.addEventListener('mouseleave', function() {
153                dropdown.style.opacity = '0';
154                dropdown.style.visibility = 'hidden';
155                dropdown.style.transform = 'translateY(-10px)';
156            });
157
158            // Handle keyboard navigation
159            link.addEventListener('keydown', function(e) {
160                if (e.key === 'Enter' || e.key === ' ') {
161                    e.preventDefault();
162                    const isVisible = dropdown.style.visibility === 'visible';
163                    
164                    if (isVisible) {
165                        dropdown.style.opacity = '0';
166                        dropdown.style.visibility = 'hidden';
167                        dropdown.style.transform = 'translateY(-10px)';
168                    } else {
169                        dropdown.style.opacity = '1';
170                        dropdown.style.visibility = 'visible';
171                        dropdown.style.transform = 'translateY(0)';
172                    }
173                }
174            });
175
176            // Handle dropdown links
177            const dropdownLinks = dropdown.querySelectorAll('.nav__dropdown-link');
178            dropdownLinks.forEach(dropdownLink => {
179                dropdownLink.addEventListener('click', function() {
180                    // Close dropdown after link click
181                    dropdown.style.opacity = '0';
182                    dropdown.style.visibility = 'hidden';
183                    dropdown.style.transform = 'translateY(-10px)';
184                });
185            });
186        });
187
188        // Close all dropdowns when clicking outside
189        document.addEventListener('click', function(e) {
190            if (!e.target.closest('.nav__item--dropdown')) {
191                navDropdownItems.forEach(item => {
192                    const dropdown = item.querySelector('.nav__dropdown');
193                    if (dropdown) {
194                        dropdown.style.opacity = '0';
195                        dropdown.style.visibility = 'hidden';
196                        dropdown.style.transform = 'translateY(-10px)';
197                    }
198                });
199            }
200        });
201    }
202
203    // Smooth Scrolling for Navigation Links
204    function initSmoothScrolling() {
205        navLinks.forEach(link => {
206            link.addEventListener('click', function(e) {
207                const href = this.getAttribute('href');
208                
209                // Only handle internal links
210                if (href.startsWith('#')) {
211                    e.preventDefault();
212                    const targetId = href.substring(1);
213                    const targetElement = document.getElementById(targetId);
214                    
215                    if (targetElement) {
216                        const headerHeight = document.querySelector('.header').offsetHeight;
217                        const targetPosition = targetElement.offsetTop - headerHeight;
218                        
219                        window.scrollTo({
220                            top: targetPosition,
221                            behavior: 'smooth'
222                        });
223                    }
224                }
225            });
226        });
227    }
228
229    // Newsletter Form Handling
230    function initNewsletterForm() {
231        if (!newsletterForm || !newsletterInput) return;
232
233        newsletterForm.addEventListener('submit', function(e) {
234            e.preventDefault();
235            
236            const email = newsletterInput.value.trim();
237            
238            if (!email) {
239                showFormMessage('Please enter your email address.', 'error');
240                return;
241            }
242            
243            if (!isValidEmail(email)) {
244                showFormMessage('Please enter a valid email address.', 'error');
245                return;
246            }
247            
248            // Simulate form submission
249            showFormMessage('Thank you for subscribing!', 'success');
250            newsletterInput.value = '';
251        });
252    }
253
254    // Newsletter Signup Form Handling
255    function initNewsletterSignupForm() {
256        if (!newsletterSignupForm) return;
257
258        newsletterSignupForm.addEventListener('submit', function(e) {
259            e.preventDefault();
260            
261            const formData = new FormData(this);
262            const firstName = formData.get('firstName').trim();
263            const lastName = formData.get('lastName').trim();
264            const province = formData.get('province');
265            const email = formData.get('email').trim();
266            const mailingList = formData.get('mailingList');
267            
268            // Validation
269            if (!firstName) {
270                showNewsletterMessage('Please enter your first name.', 'error');
271                return;
272            }
273            
274            if (!lastName) {
275                showNewsletterMessage('Please enter your last name.', 'error');
276                return;
277            }
278            
279            if (!province) {
280                showNewsletterMessage('Please select your province.', 'error');
281                return;
282            }
283            
284            if (!email) {
285                showNewsletterMessage('Please enter your email address.', 'error');
286                return;
287            }
288            
289            if (!isValidEmail(email)) {
290                showNewsletterMessage('Please enter a valid email address.', 'error');
291                return;
292            }
293            
294            // Simulate form submission
295            showNewsletterMessage('Thank you for signing up for our newsletter!', 'success');
296            this.reset();
297        });
298    }
299
300    // Newsletter Signup Message Display
301    function showNewsletterMessage(message, type) {
302        const formContainer = document.querySelector('.newsletter-signup__form-container');
303        if (!formContainer) return;
304
305        // Remove existing message
306        const existingMessage = formContainer.querySelector('.newsletter-message');
307        if (existingMessage) {
308            existingMessage.remove();
309        }
310        
311        // Create new message
312        const messageElement = document.createElement('div');
313        messageElement.className = `newsletter-message newsletter-message--${type}`;
314        messageElement.textContent = message;
315        
316        // Insert message after form
317        const form = formContainer.querySelector('.newsletter-signup__form');
318        formContainer.insertBefore(messageElement, form.nextSibling);
319        
320        // Remove message after 5 seconds
321        setTimeout(() => {
322            if (messageElement.parentNode) {
323                messageElement.remove();
324            }
325        }, 5000);
326    }
327
328    // Email Validation
329    function isValidEmail(email) {
330        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
331        return emailRegex.test(email);
332    }
333
334    // Form Message Display
335    function showFormMessage(message, type) {
336        // Remove existing message
337        const existingMessage = newsletterForm.querySelector('.form-message');
338        if (existingMessage) {
339            existingMessage.remove();
340        }
341        
342        // Create new message
343        const messageElement = document.createElement('div');
344        messageElement.className = `form-message form-message--${type}`;
345        messageElement.textContent = message;
346        
347        // Insert message after form
348        newsletterForm.parentNode.insertBefore(messageElement, newsletterForm.nextSibling);
349        
350        // Remove message after 5 seconds
351        setTimeout(() => {
352            if (messageElement.parentNode) {
353                messageElement.remove();
354            }
355        }, 5000);
356    }
357
358    // Scroll Animations with Intersection Observer
359    function initScrollAnimations() {
360        // Check if Intersection Observer is supported
361        if (!('IntersectionObserver' in window)) {
362            // Fallback: show all elements immediately
363            document.querySelectorAll('.scroll-animate').forEach(el => {
364                el.classList.add('animate-in');
365            });
366            return;
367        }
368
369        // Create intersection observer
370        const observerOptions = {
371            threshold: 0.1,
372            rootMargin: '0px 0px -50px 0px'
373        };
374
375        const observer = new IntersectionObserver((entries) => {
376            entries.forEach(entry => {
377                if (entry.isIntersecting) {
378                    entry.target.classList.add('animate-in');
379                    
380                    // For stagger animations, animate children
381                    const staggerChildren = entry.target.querySelectorAll('.scroll-animate--stagger');
382                    if (staggerChildren.length > 0) {
383                        staggerChildren.forEach((child, index) => {
384                            setTimeout(() => {
385                                child.classList.add('animate-in');
386                            }, index * 100);
387                        });
388                    }
389                    
390                    // Unobserve after animation to improve performance
391                    observer.unobserve(entry.target);
392                }
393            });
394        }, observerOptions);
395
396        // Observe all elements with scroll animation classes
397        const animatedElements = document.querySelectorAll(`
398            .scroll-animate,
399            .scroll-animate--fade-in,
400            .scroll-animate--slide-left,
401            .scroll-animate--slide-right,
402            .scroll-animate--scale,
403            .scroll-animate--rotate,
404            .scroll-animate--hero-title,
405            .scroll-animate--hero-subtitle,
406            .scroll-animate--hero-button
407        `);
408
409        animatedElements.forEach(el => {
410            observer.observe(el);
411        });
412
413        // Parallax effect for background images
414        function initParallaxEffect() {
415            const parallaxElements = document.querySelectorAll('.scroll-animate--parallax');
416            
417            if (parallaxElements.length === 0) return;
418
419            function updateParallax() {
420                const scrolled = window.pageYOffset;
421                
422                parallaxElements.forEach(el => {
423                    const speed = el.dataset.speed || 0.5;
424                    const yPos = -(scrolled * speed);
425                    el.style.transform = `translateY(${yPos}px)`;
426                });
427            }
428
429            // Throttle scroll events for better performance
430            let ticking = false;
431            function requestTick() {
432                if (!ticking) {
433                    requestAnimationFrame(updateParallax);
434                    ticking = true;
435                }
436            }
437
438            function handleScroll() {
439                requestTick();
440                ticking = false;
441            }
442
443            window.addEventListener('scroll', handleScroll, { passive: true });
444        }
445
446        initParallaxEffect();
447    }
448
449    // Lazy Loading for Images
450    function initLazyLoading() {
451        const images = document.querySelectorAll('img[data-src]');
452        
453        if ('IntersectionObserver' in window) {
454            const imageObserver = new IntersectionObserver((entries, observer) => {
455                entries.forEach(entry => {
456                    if (entry.isIntersecting) {
457                        const img = entry.target;
458                        img.src = img.dataset.src;
459                        img.classList.remove('lazy');
460                        img.classList.add('loaded');
461                        observer.unobserve(img);
462                    }
463                });
464            });
465            
466            images.forEach(img => imageObserver.observe(img));
467        } else {
468            // Fallback for browsers without IntersectionObserver
469            images.forEach(img => {
470                img.src = img.dataset.src;
471                img.classList.remove('lazy');
472                img.classList.add('loaded');
473            });
474        }
475    }
476
477    // Button Click Effects
478    function initButtonEffects() {
479        const buttons = document.querySelectorAll('.btn');
480        
481        buttons.forEach(button => {
482            button.addEventListener('click', function(e) {
483                // Create ripple effect
484                const ripple = document.createElement('span');
485                const rect = this.getBoundingClientRect();
486                const size = Math.max(rect.width, rect.height);
487                const x = e.clientX - rect.left - size / 2;
488                const y = e.clientY - rect.top - size / 2;
489                
490                ripple.style.width = ripple.style.height = size + 'px';
491                ripple.style.left = x + 'px';
492                ripple.style.top = y + 'px';
493                ripple.classList.add('ripple');
494                
495                this.appendChild(ripple);
496                
497                // Remove ripple after animation
498                setTimeout(() => {
499                    ripple.remove();
500                }, 600);
501            });
502        });
503    }
504
505    // Scroll to Top Button
506    function initScrollToTop() {
507        // Create scroll to top button
508        const scrollToTopBtn = document.createElement('button');
509        scrollToTopBtn.className = 'scroll-to-top';
510        scrollToTopBtn.innerHTML = '↑';
511        scrollToTopBtn.setAttribute('aria-label', 'Scroll to top');
512        document.body.appendChild(scrollToTopBtn);
513        
514        // Show/hide button based on scroll position
515        window.addEventListener('scroll', function() {
516            if (window.scrollY > 300) {
517                scrollToTopBtn.classList.add('scroll-to-top--visible');
518            } else {
519                scrollToTopBtn.classList.remove('scroll-to-top--visible');
520            }
521        });
522        
523        // Scroll to top when clicked
524        scrollToTopBtn.addEventListener('click', function() {
525            window.scrollTo({
526                top: 0,
527                behavior: 'smooth'
528            });
529        });
530    }
531
532    // Performance Optimization: Debounce Function
533    function debounce(func, wait) {
534        let timeout;
535        return function executedFunction(...args) {
536            const later = () => {
537                clearTimeout(timeout);
538                func(...args);
539            };
540            clearTimeout(timeout);
541            timeout = setTimeout(later, wait);
542        };
543    }
544
545    // Initialize all functionality when DOM is loaded
546    function init() {
547        initMobileNav();
548        initMobileMenuItems();
549        initMobileFooterAccordion();
550        initDropdownMenus();
551        initSmoothScrolling();
552        initNewsletterForm();
553        initNewsletterSignupForm();
554        initScrollAnimations();
555        initLazyLoading();
556        initButtonEffects();
557        initScrollToTop();
558        
559        console.log('LFNG Landing Page initialized successfully');
560    }
561
562    // Run initialization when DOM is ready
563    if (document.readyState === 'loading') {
564        document.addEventListener('DOMContentLoaded', init);
565    } else {
566        init();
567    }
568
569})();

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.