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.