1/* 2import '../scss/styles.scss'; 3import 'bootstrap/dist/js/bootstrap.bundle.min.js'; 4 5// Import mmenu CSS and JS 6import 'mmenu-js/dist/mmenu.css'; 7import Mmenu from 'mmenu-js'; 8*/ 9 10//console.log('ð app.js loaded - file executing'); 11 12// Simple console log to check if imports work 13//console.log('ð Starting main.js'); 14//console.log('ð¦ Mmenu imported:', typeof Mmenu, Mmenu); 15 16 17//navigation 18document.addEventListener('DOMContentLoaded', function() { 19 const navbar = document.querySelector('.navbar'); 20 const navItems = document.querySelectorAll('.nav-item.dropdown-hover'); 21 const navLinks = document.querySelectorAll('.navbar .nav-link'); // Only target nav links within the navbar 22 const megaMenus = document.querySelectorAll('.mega-menu'); 23 24 let hoverTimeout; 25 let isHoveringNav = false; 26 let isHoveringMega = false; 27 let isFocusWithinNav = false; 28 let currentActiveNavIndex = -1; 29 30 // Hide all mega menus initially 31 megaMenus.forEach(menu => { 32 menu.style.opacity = '0'; 33 menu.style.visibility = 'hidden'; 34 menu.style.transform = 'translateX(-50%) translateY(-10px)'; 35 menu.style.transition = 'all 0.15s ease'; 36 }); 37 38 function showMegaMenu(menuKey) { 39 clearTimeout(hoverTimeout); 40 megaMenus.forEach(menu => { 41 if (menu.dataset.menu === menuKey) { 42 menu.style.opacity = '1'; 43 menu.style.visibility = 'visible'; 44 menu.style.transform = 'translateX(-50%) translateY(0)'; 45 } else { 46 menu.style.opacity = '0'; 47 menu.style.visibility = 'hidden'; 48 menu.style.transform = 'translateX(-50%) translateY(-10px)'; 49 } 50 }); 51 document.querySelector('.site-header')?.classList.add('menu-active'); 52 } 53 54 function hideMegaMenus() { 55 hoverTimeout = setTimeout(() => { 56 if (!isHoveringNav && !isHoveringMega && !isFocusWithinNav) { 57 megaMenus.forEach(menu => { 58 menu.style.opacity = '0'; 59 menu.style.visibility = 'hidden'; 60 menu.style.transform = 'translateX(-50%) translateY(-10px)'; 61 }); 62 // Only remove active class from navbar nav links, not all nav links on the page 63 navLinks.forEach(link => link.classList.remove('active')); 64 document.querySelector('.site-header')?.classList.remove('menu-active'); 65 currentActiveNavIndex = -1; 66 } 67 }, 150); 68 } 69 70 navItems.forEach((navItem, index) => { 71 const navLink = navItem.querySelector('.nav-link'); 72 const menuKey = navItem.dataset.menu; 73 74 navItem.addEventListener('mouseenter', function() { 75 isHoveringNav = true; 76 showMegaMenu(menuKey); 77 // Only remove active class from navbar nav links, not all nav links on the page 78 navLinks.forEach(link => link.classList.remove('active')); 79 if (navLink) { 80 navLink.classList.add('active'); 81 currentActiveNavIndex = index; 82 } 83 }); 84 85 navItem.addEventListener('mouseleave', function() { 86 isHoveringNav = false; 87 hideMegaMenus(); 88 }); 89 }); 90 91 megaMenus.forEach(menu => { 92 menu.addEventListener('mouseenter', function() { 93 isHoveringMega = true; 94 clearTimeout(hoverTimeout); 95 }); 96 menu.addEventListener('mouseleave', function() { 97 isHoveringMega = false; 98 hideMegaMenus(); 99 }); 100 }); 101 102 // Keyboard navigation support (optional, similar to your current logic) 103 navLinks.forEach((navLink, index) => { 104 navLink.addEventListener('focus', function() { 105 isFocusWithinNav = true; 106 isHoveringNav = true; 107 const menuKey = navLink.closest('.nav-item').dataset.menu; 108 showMegaMenu(menuKey); 109 // Only remove active class from navbar nav links, not all nav links on the page 110 navLinks.forEach(link => link.classList.remove('active')); 111 this.classList.add('active'); 112 currentActiveNavIndex = index; 113 }); 114 navLink.addEventListener('blur', function() { 115 setTimeout(() => { 116 isFocusWithinNav = false; 117 hideMegaMenus(); 118 }, 50); 119 }); 120 }); 121}); 122 123//hide navigation on scroll 124/* 125document.addEventListener('DOMContentLoaded', function() { 126 const siteHeader = document.querySelector('.site-header'); 127 let lastScrollY = window.scrollY; 128 129 function handleScroll() { 130 // Only on desktop 131 if (window.innerWidth < 992) { 132 siteHeader.classList.remove('hidden', 'scrolled'); 133 return; 134 } 135 136 const currentScrollY = window.scrollY; 137 138 // Add scrolled class for shadow 139 if (currentScrollY > 10) { 140 siteHeader.classList.add('scrolled'); 141 } else { 142 siteHeader.classList.remove('scrolled'); 143 } 144 145 // Hide/show logic 146 if (currentScrollY > 100 && currentScrollY > lastScrollY) { 147 // Scrolling down and past 100px 148 siteHeader.classList.add('hidden'); 149 } else if (currentScrollY < lastScrollY) { 150 // Scrolling up 151 siteHeader.classList.remove('hidden'); 152 } 153 154 lastScrollY = currentScrollY; 155 } 156 157 window.addEventListener('scroll', handleScroll, { passive: true });
158 window.addEventListener('resize', handleScroll); 159}); 160*/ 161 162// Video Background Handler 163document.addEventListener('DOMContentLoaded', function() { 164 const heroVideo = document.querySelector('.home-header__bg-video'); 165 166 if (heroVideo) { 167 // Handle video loading and fallback 168 heroVideo.addEventListener('loadeddata', function() { 169 //console.log('â Hero video loaded successfully'); 170 this.style.opacity = '1'; 171 }); 172 173 heroVideo.addEventListener('error', function() { 174 //console.log('â Hero video failed to load, showing fallback image'); 175 // Hide video and show fallback image 176 this.style.display = 'none'; 177 const fallbackImage = this.querySelector('img'); 178 if (fallbackImage) { 179 fallbackImage.style.display = 'block'; 180 } 181 }); 182 183 // Desktop only: Auto-play + IntersectionObserver for performance 184 const isDesktop = window.innerWidth >= 992; 185 if (isDesktop) { 186 // Pause video when not in viewport (performance optimization) 187 const observer = new IntersectionObserver((entries) => { 188 entries.forEach(entry => { 189 if (entry.isIntersecting) { 190 heroVideo.play(); 191 } else { 192 heroVideo.pause(); 193 } 194 }); 195 }, { threshold: 0.25 }); 196 197 observer.observe(heroVideo); 198 199 // Ensure video plays on load (some browsers block autoplay) 200 const playVideo = () => { 201 const playPromise = heroVideo.play(); 202 if (playPromise !== undefined) { 203 playPromise.catch(error => { 204 //console.log('Video autoplay prevented:', error);
205 // Video autoplay was prevented, but that's okay 206 }); 207 } 208 }; 209 210 // Try to play after a short delay 211 setTimeout(playVideo, 100); 212 } else { 213 // Mobile: Keep video paused for performance - user controls with pause button 214 heroVideo.pause(); 215 //console.log('â Mobile detected - video paused, user can play with pause button'); 216 } 217 } 218}); 219 220// Video pause/play control button 221document.addEventListener('DOMContentLoaded', function() { 222 const videoControl = document.querySelector('.home-header__video-control'); 223 const video = document.querySelector('.home-header__bg-video'); 224 225 if (videoControl && video) { 226 // Initial state based on viewport 227 const isDesktop = window.innerWidth >= 992; 228 let isPlaying = isDesktop; // true on desktop (auto-playing), false on mobile (paused) 229 230 // Set correct icon on load 231 if (isDesktop) { 232 // Desktop: video is auto-playing, show pause icon 233 videoControl.querySelector('i').classList.remove('fa-circle-play'); 234 videoControl.querySelector('i').classList.add('fa-circle-pause'); 235 } else { 236 // Mobile: video is paused, show play icon 237 videoControl.querySelector('i').classList.remove('fa-circle-pause'); 238 videoControl.querySelector('i').classList.add('fa-circle-play'); 239 } 240 241 videoControl.addEventListener('click', function(e) { 242 e.preventDefault(); 243 244 if (isPlaying) { 245 // Pause the video 246 video.pause(); 247 videoControl.querySelector('i').classList.remove('fa-circle-pause'); 248 videoControl.querySelector('i').classList.add('fa-circle-play'); 249 isPlaying = false; 250 //console.log('â Video paused'); 251 } else { 252 // Play the video 253 video.play(); 254 videoControl.querySelector('i').classList.remove('fa-circle-play'); 255 videoControl.querySelector('i').classList.add('fa-circle-pause'); 256 isPlaying = true; 257 //console.log('â Video playing'); 258 } 259 }); 260 261 // Update button when video ends (auto-loop) 262 video.addEventListener('ended', function() { 263 videoControl.querySelector('i').classList.remove('fa-circle-play'); 264 videoControl.querySelector('i').classList.add('fa-circle-pause'); 265 isPlaying = true; 266 }); 267 } 268}); 269 270//hide top-bar only on scroll 271document.addEventListener('DOMContentLoaded', function() { 272 const siteHeader = document.querySelector('.site-header'); 273 const topBar = document.querySelector('.top-bar'); 274 let lastScrollY = window.scrollY; 275 276 function handleScroll() { 277 // Only on desktop 278 if (window.innerWidth < 992) { 279 siteHeader.classList.remove('scrolled', 'top-bar-hidden'); 280 topBar.classList.remove('hidden'); 281 return; 282 } 283 284 const currentScrollY = window.scrollY; 285 286 // Add scrolled class for any additional styling 287 if (currentScrollY > 10) { 288 siteHeader.classList.add('scrolled'); 289 } else { 290 siteHeader.classList.remove('scrolled'); 291 } 292 293 // Hide/show top-bar logic 294 if (currentScrollY > 100 && currentScrollY > lastScrollY) { 295 // Scrolling down and past 100px - hide top-bar only 296 topBar.classList.add('hidden'); 297 siteHeader.classList.add('top-bar-hidden'); 298 } else if (currentScrollY < lastScrollY) { 299 // Scrolling up - show top-bar 300 topBar.classList.remove('hidden'); 301 siteHeader.classList.remove('top-bar-hidden'); 302 } 303 304 lastScrollY = currentScrollY; 305 } 306 307 window.addEventListener('scroll', handleScroll, { passive: true }); 308 window.addEventListener('resize', handleScroll); 309}); 310 311// Accessibility: Focus trap and tab navigation for mega menu 312(function() { 313 const navItems = document.querySelectorAll('.nav-item.dropdown-hover'); 314 const megaMenus = document.querySelectorAll('.mega-menu'); 315 const navLinks = document.querySelectorAll('.navbar .nav-link'); 316 317 function getFocusableElements(menu) { 318 return menu.querySelectorAll('a, button, input, [tabindex]:not
318([tabindex="-1"])'); 319 } 320 321 function openMegaMenuByIndex(index) { 322 if (navItems[index]) { 323 navItems[index].querySelector('.nav-link').focus(); 324 navItems[index].dispatchEvent(new Event('mouseenter')); 325 } 326 } 327 328 function closeAllMegaMenus() { 329 megaMenus.forEach(menu => { 330 menu.style.opacity = '0'; 331 menu.style.visibility = 'hidden'; 332 menu.style.transform = 'translateX(-50%) translateY(-10px)'; 333 }); 334 navLinks.forEach(link => link.classList.remove('active')); 335 document.querySelector('.site-header')?.classList.remove('menu-active'); 336 } 337 338 function getOpenMegaMenu() { 339 return Array.from(megaMenus).find(menu => menu.style.opacity === '1'); 340 } 341 342 // ESC key handler - global listener for when mega menu is open 343 document.addEventListener('keydown', function(e) { 344 if (e.key === 'Escape') { 345 const openMenu = getOpenMegaMenu(); 346 if (openMenu) { 347 // Find which nav item owns this menu 348 const menuKey = openMenu.dataset.menu; 349 const ownerNavItem = Array.from(navItems).find(item => item.dataset.menu === menuKey); 350 closeAllMegaMenus(); 351 if (ownerNavItem) { 352 ownerNavItem.querySelector('.nav-link').focus(); 353 } 354 } 355 } 356 }); 357 358 navItems.forEach((navItem, navIndex) => { 359 const menuKey = navItem.dataset.menu; 360 const megaMenu = document.querySelector(`.mega-menu[data-menu="${menuKey}"]`); 361 const navLink = navItem.querySelector('.nav-link'); 362 363 // Tab from nav link into mega menu 364 navLink.addEventListener('keydown', function(e) { 365 if (e.key === 'Tab' && !e.shiftKey && megaMenu && megaMenu.style.opacity === '1') { 366 // Focus first focusable in mega menu 367 const focusables = getFocusableElements(megaMenu); 368 if (focusables.length) { 369 e.preventDefault(); 370 focusables[0].focus(); 371 } 372 } else if (e.key === 'Tab' && e.shiftKey && megaMenu && megaMenu.style.opacity === '1') { 373 // Shift+Tab backwards from nav link - close menu 374 closeAllMegaMenus(); 375 } 376 }); 377 378 if (megaMenu) { 379 megaMenu.addEventListener('keydown', function(e) { 380 const focusables = Array.from(getFocusableElements(megaMenu)); 381 const first = focusables[0]; 382 const last = focusables[focusables.length - 1]; 383 const active = document.activeElement; 384 385 if (e.key === 'Tab') { 386 if (!e.shiftKey && active === last) { 387 // Tab from last item - move to next nav item 388 e.preventDefault(); 389 if (navIndex < navItems.length - 1) { 390 closeAllMegaMenus(); 391 openMegaMenuByIndex(navIndex + 1); 392 } else { 393 // Last nav item - exit to page content 394 closeAllMegaMenus(); 395 setTimeout(() => { 396 let next = document.querySelector('.site-header').nextElementSibling; 397 if (next) { 398 let nextFocusable = next.querySelector('a, button, input, [tabindex]:not([tabindex="-1"])'); 399 if (nextFocusable) nextFocusable.focus(); 400 } 401 }, 10); 402 } 403 } else if (e.shiftKey && active === first) { 404 // Shift+Tab from first item - close menu and go back to nav link 405 e.preventDefault(); 406 closeAllMegaMenus(); 407 navLink.focus(); 408 } 409 } 410 }); 411 } 412 }); 413})(); 414 415function buildCustomFooterHTML() { 416 // Check if data exists 417 if (typeof mmFooterData === 'undefined') { 418 console.error('WordPress footer data not loaded'); 419 return ''; 420 } 421 422 let html = '<div class="mm-custom-footer">'; 423 424 // Top row - Dynamic buttons from ACF repeater 425 html += '<div class="mm-footer-top-actions">'; 426 427 if (mmFooterData.mega_menu_links && mmFooterData.mega_menu_links.length > 0) { 428 mmFooterData.mega_menu_links.forEach(function(link) { 429 html += `<a href="${link.url}" class="mm-footer-btn mm-footer-btn--icon" target="${link.target}">`; 430 html += `<span>${link.title}</span>`; 431 html += '</a>'; 432 }); 433 } 434 435 html += '</div>'; 436 437 // Second row - Give CTA (using ACF link field) 438 html += '<div class="mm-footer-give-bg">'; 439 html += '<div class="mm-footer-give-section">'; 440 html += `<a href="${mmFooterData.give.url}" class="mm-footer-cta" target="${mmFooterData.give.target}"> 441 <div class="cta-content"> 442 <h6 class="cta-title">${mmFooterData.give.title}</h6> 443 <p class="cta-description">${mmFooterData.give.description}</p> 444 </div> 445 </a>`; 446 html += '</div>'; 447 html += '</div>'; 448 449 // Third row - "top" menu items from WordPress 450 html += '<div class="mm-footer-links-section">'; 451 452 if (mmFooterData.menu_items && mmFooterData.menu_items.length > 0) {
453 mmFooterData.menu_items.forEach(function(item) { 454 html += `<a href="${item.url}" class="mm-footer-link" target="${item.target}"> 455 <span>${item.title}</span> 456 <i class="fas fa-arrow-right"></i> 457 </a>`; 458 }); 459 } 460 461 html += '</div>'; 462 html += '</div>'; 463 464 return html; 465} 466 467 468// Mobile Menu - Updated configuration for full width 469document.addEventListener('DOMContentLoaded', function() { 470 471 //console.log('ð± Initializing mobile menu...'); 472 473 const mobileToggle = document.getElementById('mobile-menu-toggle'); 474 const mobileMenu = document.getElementById('mobile-menu'); 475 476 /* 477 console.log('ð Elements found:', { 478 toggle: !!mobileToggle, 479 menu: !!mobileMenu 480 }); 481 */ 482 483 if (mobileToggle && mobileMenu) { 484 try { 485 const menu = new Mmenu('#mobile-menu', { 486 // Extensions for positioning and theme 487 offCanvas: { 488 position: 'top' // Mmenu v9 uses this instead of 'position-top' extension:contentReference[oaicite:5]{index=5} 489 }, 490 491 // Navbar configuration 492 navbar: { 493 title: ' ', // Default title 494 }, 495 496 // Navigation bars 497 navbars: [ 498 // First top navbar - just for search 499 { 500 position: 'top', 501 content: [ 502 '<div class="mm-custom-search">' + 503 '<form class="mm-search-form" action="/" method="GET">' + 504 '<div class="mm-search-input-wrapper">' + 505 '<label for="mm-search-input" class="sr-only">Search Muhlenberg</label>' + 506 '<input type="text" id="mm-search-input" name="s" placeholder="Search Muhlenberg" class="mm-search-input" autocomplete="off">' + 507 '<button type="submit" class="mm-search-btn" aria-label="Search">' + 508 '<i class="fa-light fa-magnifying-glass"></i>' + 509 '</button>' + 510 '</div>' + 511 '</form>' + 512 '</div>' 513 ] 514 }, 515 ], 516 517 // Search field 518 /* 519 searchfield: { 520 add: true, 521 search: true, 522 submit: true, 523 placeholder: 'Search Muhlenberg' 524 }, 525 */ 526 527 }, 528 /** Configuration object (integration & accessibility) **/ 529 { 530 offCanvas: { 531 // Define the "page" element selector that the menu will push/overlay 532 page: { selector: '#main-content' }, 533 // Screen reader texts for accessibility 534 screenReader: { 535 closeMenu: 'Close menu', 536 closeSubmenu: 'Close submenu' 537 } 538 } 539 // No separate screenReader key here; itâs nested under offCanvas in v9 config 540 } 541 ); 542 543 544 545 //console.log('â mmenu initialized successfully'); 546 547 // Add custom footer to all panels after initialization 548 setTimeout(() => { 549 const customFooterHTML = buildCustomFooterHTML(); 550 /*const customFooterHTML = 551 '<div class="mm-custom-footer">' + 552 // Top row - 3 buttons side by side 553 '<div class="mm-footer-top-actions">' + 554 '<a href="/visit" class="mm-footer-btn mm-footer-btn--icon">' + 555 '<i class="fas fa-map-marker-alt"></i>' + 556 '<span>Visit</span>' + 557 '</a>' + 558 '<a href="/request-info" class="mm-footer-btn mm-footer-btn--icon">' + 559 '<i class="fas fa-info-circle"></i>' + 560 '<span>Request Info</span>' + 561 '</a>' + 562 '<a href="/apply" class="mm-footer-btn mm-footer-btn--icon">' + 563 '<i class="fas fa-clipboard"></i>' + 564 '<span>Apply</span>' + 565 '</a>' + 566 '</div>' + 567 568 // Second row - Give CTA 569 '<div class="mm-footer-give-bg">' + 570 '<div class="mm-footer-give-section">' + 571 '<a href="/give" class="mm-footer-cta">' + 572 '<div class="cta-content">' + 573 '<h6 class="cta-title"><i class="fas fa-hand-holding-heart"></i> Give</h6>' + 574 '<p class="cta-description">Support Muhlenberg students and programs with your gift.</p>' + 575 '</div>' + 576 '</a>' + 577 '</div>' + 578 '</div>' + 579 580 // Third row - Black background section with arrows 581 '<div class="mm-footer-links-section">' + 582 '<a href="/bergconnect" class="mm-footer-link">' + 583 '<span>BergConnect</span>' + 584 '<i class="fas fa-arrow-right"></i>' + 585 '</a>' + 586 '<a href="/alumni-donors" class="mm-footer-link">' + 587 '<span>Alumni & Donors</span>' + 588 '<i class="fas fa-arrow-right"></i>' + 589 '</a>' + 590 '<a href="/magazine" class="mm-footer-link">' + 591 '<span>Magazine</span>' + 592 '<i class="fas fa-arrow-right"></i>' + 593 '</a>' + 594 '</div>' + 595 '</div>';*/ 596 597 // Find all panels and append the footer 598 const panels = document.querySelectorAll('.mm-panel');
599 panels.forEach(panel => { 600 const ulElement = panel.querySelector('ul'); 601 if (ulElement) { 602 // Insert the footer HTML after the ul element 603 ulElement.insertAdjacentHTML('afterend', customFooterHTML); 604 } 605 }); 606 607 //console.log('â Custom footer added to', panels.length, 'panels'); 608 }, 100); // Small delay to ensure panels are fully created 609 610 // Track menu state 611 let isMenuOpen = false; 612 613 // Function to update toggle button icon 614 function updateToggleIcon(isOpen) { 615 const icon = mobileToggle.querySelector('i'); 616 if (icon) { 617 if (isOpen) { 618 // Change to X/close icon 619 icon.className = 'fas fa-times'; // or 'fas fa-close' if you prefer 620 } else { 621 // Change back to hamburger 622 icon.className = 'fas fa-bars'; 623 } 624 } 625 } 626 627 // Toggle button click handler 628 mobileToggle.addEventListener('click', function(e) { 629 //console.log('ð Toggle clicked, menu state:', isMenuOpen); 630 e.preventDefault(); 631 e.stopPropagation(); 632 633 if (isMenuOpen) { 634 // Menu is open, close it 635 menu.API.close(); 636 isMenuOpen = false; 637 updateToggleIcon(false); 638 } else { 639 // Menu is closed, open it 640 menu.API.open(); 641 isMenuOpen = true; 642 updateToggleIcon(true); 643 } 644 }); 645 646 // Simple resize checker - add this at the very end of your file 647 window.addEventListener('resize', function() { 648 649 const searchForm = document.getElementById('topBarSearchForm'); 650 const searchToggle = document.getElementById('topBarSearchToggle'); 651 652 // Check if we're at desktop size (992px or larger) 653 if (window.innerWidth >= 992) { 654 655 // Hide the mobile menu if it exists 656 if (mobileMenu) { 657 menu.API.close(); 658 isMenuOpen = false; 659 updateToggleIcon(false); 660 } 661 662 if (searchForm && searchForm.classList.contains('active')) { 663 searchForm.classList.remove('active'); 664 searchToggle.setAttribute('aria-expanded', 'false'); 665 } 666 667 // Reset the toggle button icon if it exists 668 if (mobileToggle) { 669 const icon = mobileToggle.querySelector('i'); 670 if (icon) { 671 icon.className = 'fas fa-bars'; 672 } 673 } 674 // Remove any mmenu classes from body 675 document.body.classList.remove('mm-menu-opened', 'mm-wrapper--opened'); 676 } 677 }); 678 679 // Event listeners 680 menu.API.bind('opened', function() { 681 //console.log('ð± Menu opened'); 682 document.body.classList.add('mm-menu-opened'); 683 }); 684 685 menu.API.bind('closed', function() { 686 //console.log('ð± Menu closed'); 687 document.body.classList.remove('mm-menu-opened'); 688 }); 689 690 } catch (error) { 691 //console.error('â Error initializing mmenu:', error); 692 } 693 } else { 694 //console.log('â Required elements not found'); 695 } 696 697 698}); 699 700//close announcements 701document.addEventListener('click', function(e) { 702 // Check if clicked element is the close button or within the close button 703 const closeButton = e.target.closest('.alert-dismissible .btn-close'); 704 705 if (closeButton) { 706 //console.log('â Announcement close button clicked'); 707 708 // Find the announcement container 709 const announcementBar = closeButton.closest('.announcement-bar'); 710 const announcementContainer = closeButton.closest('#announcementContainer'); 711 712 // Remove has-bar class and CSS variable from graphic-header 713 const graphicHeader = document.querySelector('.graphic-header'); 714 if (graphicHeader) { 715 graphicHeader.classList.remove('has-bar'); 716 graphicHeader.style.removeProperty('--announcement-height'); 717 //console.log('â Removed has-bar class and height variable'); 718 } 719 720 // Hide the announcement container 721 if (announcementContainer) { 722 announcementContainer.style.display = 'none'; 723 } 724 } 725}); 726 727// Add has-bar class to graphic-header when announcement bar is present 728document.addEventListener('DOMContentLoaded', function() { 729 const announcementContainer = document.getElementById('announcementContainer'); 730 const graphicHeader = document.querySelector('.graphic-header'); 731 732 if (announcementContainer && graphicHeader) { 733 // Check if announcement is visible 734 const isVisible = announcementContainer.style.display !== 'none' && 735 announcementContainer.offsetHeight > 0; 736 737 if (isVisible) { 738 // Measure the actual height of the announcement bar 739 const announcementHeight = announcementContainer.offsetHeight; 740 741 // Set CSS variable for dynamic height adjustment 742 graphicHeader.style.setProperty('--announcement-height', `${announcementHeight}px`); 743 graphicHeader.classList.add('has-bar'); 744 745 //console.log(`â Announcement bar detected - height: ${announcementHeight}px`); 746 } 747 } 748 749 // Update height on window resize (in case content reflows) 750 window.addEventListener('resize', function() { 751 if (announcementContainer && graphicHeader && graphicHeader.classList.contains('has-bar')) { 752 const announcementHeight = announcementContainer.offsetHeight; 753 graphicHeader.style.setProperty('--announcement-height', `${announcementHeight}px`); 754 } 755 }); 756}); 757 758 759 760//new listeners 761document.addEventListener('click', function(e) { 762 if (e.target.classList.contains('btn-close')) { 763 const pill = e.target.closest('.filter-type-pill'); 764 if (pill) pill.remove(); 765 } 766}); 767 768 769document.addEventListener('DOMContentLoaded', function() { 770 document.querySelectorAll('select[data-multi-select]').forEach(function(el) { 771 new MultiSelect(el, { 772 placeholder: el.getAttribute('data-placeholder') || 'Select...', 773 search: el.getAttribute('data-search') === 'true', 774 selectAll: el.getAttribute('data-select-all') !== 'false', 775 listAll: el.getAttribute('data-list-all') === 'true' 776 });
777 }); 778}); 779 780 781 782 783// Dynamic header height calculation & unified anchor / accordion scroll offset 784document.addEventListener('DOMContentLoaded', function () { 785 const BUFFER = 10; // 10px clean breathing room below sticky navbar 786 787 /** 788 * Calculate total fixed/sticky navigation offset dynamically. 789 * Measures the visible sticky navbar height (since top-bar slides offscreen on scroll). 790 */ 791 function getHeaderOffset() { 792 const navbar = document.querySelector('.site-header .navbar') || document.querySelector('.navbar'); 793 const adminBar = document.getElementById('wpadminbar'); 794 const announcementContainer = document.getElementById('announcementContainer'); 795 796 let headerHeight = 0; 797 if (navbar && navbar.offsetHeight > 0) { 798 headerHeight = navbar.offsetHeight; 799 } else { 800 headerHeight = window.innerWidth >= 992 ? 90 : 80; 801 } 802 803 let adminBarHeight = 0; 804 if (adminBar && adminBar.offsetHeight > 0) { 805 adminBarHeight = adminBar.offsetHeight; 806 } 807 808 let announcementHeight = 0; 809 if (announcementContainer && announcementContainer.offsetHeight > 0 && window.getComputedStyle(announcementContainer).display !== 'none') { 810 const style = window.getComputedStyle(announcementContainer); 811 if (style.position === 'fixed' || style.position === 'sticky') { 812 announcementHeight = announcementContainer.offsetHeight; 813 } 814 } 815 816 return headerHeight + adminBarHeight + announcementHeight + BUFFER; 817 } 818 819 /** 820 * Synchronize CSS variables for native browser scroll-padding & scroll-margin 821 */ 822 function syncScrollOffsetCSS() { 823 const totalOffset = getHeaderOffset(); 824 document.documentElement.style.setProperty('--scroll-offset', `${totalOffset}px`); 825 826 const navbar = document.querySelector('.site-header .navbar') || document.querySelector('.navbar'); 827 if (navbar && navbar.offsetHeight > 0) { 828 document.documentElement.style.setProperty('--site-header-height', `${navbar.offsetHeight}px`); 829 } else { 830 document.documentElement.style.setProperty('--site-header-height', `${window.innerWidth >= 992 ? 90 : 80}px`); 831 } 832 833 const adminBar = document.getElementById('wpadminbar'); 834 if (adminBar) { 835 document.documentElement.style.setProperty('--wp-admin-bar-height', `${adminBar.offsetHeight}px`); 836 } 837 } 838 839 syncScrollOffsetCSS(); 840 window.addEventListener('resize', syncScrollOffsetCSS, { passive: true }); 841 window.addEventListener('load', syncScrollOffsetCSS); 842 843 /** 844 * Robust element finder supporting IDs with spaces, special characters, and name attributes 845 */ 846 function getElementByHashOrId(idString) { 847 if (!idString) return null; 848 const cleanId = idString.replace(/^#/, ''); 849 if (!cleanId) return null; 850 851 // 1. Direct getElementById (fast & exact, supports spaces like 'plant operation policies') 852 let el = document.getElementById(cleanId); 853 if (el) return el; 854 855 // Try decoded 856 try { 857 const decoded = decodeURIComponent(cleanId); 858 el = document.getElementById(decoded); 859 if (el) return el; 860 } catch (e) {} 861 862 // 2. Query selector with CSS.escape 863 try { 864 el = document.querySelector(`[id="${cleanId}"]`) || 865 document.querySelector(`[name="${cleanId}"]`) || 866 document.querySelector(`[data-id="${cleanId}"]`); 867 if (el) return el; 868 } catch (e) {} 869 870 return null; 871 } 872 873 /** 874 * Checks if target or any ancestor/child is an accordion/tab/collapse, and opens it. 875 * Supports: 876 * 1. Bootstrap 5 Generic Accordions (e.g. accordion_generic, button[data-bs-toggle="collapse"], .collapse) 877 * 2. Elementor Nested Accordions (<details class="e-n-accordion-item">) 878 * 3. Legacy Elementor Accordions / Toggles (.elementor-tab-title) 879 * 4. The Plus Addons Accordions / Tabs 880 * 5. Bootstrap & Elementor Tabs 881 */ 882 function checkAndOpenAccordion(target) { 883 if (!target) return false; 884 let opened = false; 885 886 // --- 1. Bootstrap 5 Collapse & Generic Accordion Widgets --- 887 // A. Target contains closed accordion button(s) (e.g. #hrl is on the widget container) 888 const innerBsButtons = target.querySelectorAll?.('.accordion-button.collapsed, [data-bs-toggle="collapse"].collapsed, button[data-bs-toggle="collapse"][aria-expanded="false"]'); 889 if (innerBsButtons && innerBsButtons.length > 0) { 890 innerBsButtons.forEach(btn => { 891 btn.click(); 892 opened = true; 893 }); 894 } 895 896 // B. Target contains closed collapse elements 897 const innerCollapses = target.querySelectorAll?.('.collapse:not(.show), .accordion-collapse:not(.show)'); 898 if (innerCollapses && innerCollapses.length > 0) {
899 innerCollapses.forEach(col => { 900 if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) { 901 bootstrap.Collapse.getOrCreateInstance(col, { toggle: false }).show(); 902 opened = true; 903 } else { 904 col.classList.add('show'); 905 opened = true; 906 } 907 }); 908 } 909 910 // C. Target is itself an accordion button that is collapsed 911 if (target.matches?.('.accordion-button.collapsed, [data-bs-toggle="collapse"].collapsed, button[data-bs-toggle="collapse"][aria-expanded="false"]')) { 912 target.click(); 913 opened = true; 914 } 915 916 // D. Target is inside a closed collapse element 917 const parentCollapse = target.closest?.('.collapse:not(.show), .accordion-collapse:not(.show)'); 918 if (parentCollapse) { 919 if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) { 920 bootstrap.Collapse.getOrCreateInstance(parentCollapse, { toggle: false }).show(); 921 opened = true; 922 } else { 923 parentCollapse.classList.add('show'); 924 opened = true; 925 } 926 const controllingBtn = document.querySelector(`[data-bs-target="#${parentCollapse.id}"], [href="#${parentCollapse.id}"]`); 927 if (controllingBtn) { 928 controllingBtn.classList.remove('collapsed'); 929 controllingBtn.setAttribute('aria-expanded', 'true'); 930 } 931 } 932 933 // E. Target has data-accordion-target 934 const accordionTargetAttr = target.getAttribute?.('data-accordion-target'); 935 if (accordionTargetAttr) { 936 const collapseEl = document.getElementById('collapse-' + accordionTargetAttr) || document.getElementById(accordionTargetAttr); 937 if (collapseEl && !collapseEl.classList.contains('show')) { 938 if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) { 939 bootstrap.Collapse.getOrCreateInstance(collapseEl, { toggle: false }).show(); 940 opened = true; 941 } 942 } 943 } 944 945 // --- 2. Elementor Nested Accordions (details / summary) --- 946 const details = target.closest?.('details') || (target.tagName === 'DETAILS' ? target : null); 947 if (details) { 948 if (!details.open) { 949 details.open = true; 950 const summary = details.querySelector('summary'); 951 if (summary) { 952 summary.setAttribute('aria-expanded', 'true'); 953 } 954 opened = true; 955 } 956 } 957 958 const innerDetails = target.querySelectorAll?.('details:not([open])'); 959 if (innerDetails && innerDetails.length > 0) { 960 innerDetails.forEach(d => { 961 d.open = true; 962 const summary = d.querySelector('summary'); 963 if (summary) summary.setAttribute('aria-expanded', 'true'); 964 }); 965 opened = true; 966 } 967 968 // --- 3. Legacy Elementor & The Plus Addons Accordion/Toggles/Tabs --- 969 const triggerClasses = [ 970 '.elementor-tab-title', 971 '.plus-accordion-header', 972 '.elementor-tab-mobile-title', 973 '.elementor-tab-desktop-title', 974 '.plus-tab-header' 975 ]; 976 977 for (const cls of triggerClasses) { 978 const innerTriggers = target.querySelectorAll?.(`${cls}:not(.elementor-active):not(.active)`); 979 if (innerTriggers && innerTriggers.length > 0) { 980 innerTriggers.forEach(trig => { 981 trig.click(); 982 opened = true; 983 }); 984 } 985 } 986 987 let trigger = null; 988 for (const cls of triggerClasses) { 989 trigger = target.closest?.(cls); 990 if (trigger) break; 991 } 992 993 if (!trigger) { 994 const containerClasses = [ 995 '.elementor-accordion-item', 996 '.elementor-toggle-item', 997 '.theplus-accordion-item' 998 ]; 999 let container = null; 1000 for (const cls of containerClasses) { 1001 container = target.closest?.(cls); 1002 if (container) break; 1003 } 1004 if (container) { 1005 for (const cls of triggerClasses) { 1006 trigger = container.querySelector(cls); 1007 if (trigger) break; 1008 } 1009 } 1010 } 1011 1012 if (!trigger) { 1013 const contentClasses = [ 1014 '.elementor-tab-content', 1015 '.plus-accordion-content', 1016 '.plus-tab-content' 1017 ]; 1018 let contentArea = null; 1019 for (const cls of contentClasses) { 1020 contentArea = target.closest?.(cls); 1021 if (contentArea) break; 1022 } 1023 if (contentArea) { 1024 let sibling = contentArea.previousElementSibling; 1025 while (sibling) { 1026 for (const cls of triggerClasses) { 1027 if (sibling.matches(cls)) { 1028 trigger = sibling; 1029 break; 1030 } 1031 } 1032 if (trigger) break; 1033 sibling = sibling.previousElementSibling; 1034 } 1035 } 1036 } 1037 1038 if (trigger) { 1039 const isActive = trigger.classList.contains('elementor-active') || trigger.classList.contains('active'); 1040 if (!isActive) { 1041 trigger.click(); 1042 opened = true; 1043 } 1044 } 1045 1046 // --- 4. Tab Navigation (Bootstrap / Elementor Tabs) --- 1047 const tabPane = target.closest?.('.tab-pane'); 1048 if (tabPane && !tabPane.classList.contains('active')) { 1049 const tabId = tabPane.id; 1050 if (tabId) { 1051 const tabTrigger = document.querySelector(`[data-bs-target="#${tabId}"], [href="#${tabId}"]`); 1052 if (tabTrigger) { 1053 tabTrigger.click(); 1054 opened = true; 1055 } 1056 } 1057 } 1058 1059 return opened; 1060 } 1061 1062 /** 1063 * Helper to extract anchor target element on current page 1064 */ 1065 function getSamePageAnchorTarget(link) { 1066 if (!link || !link.getAttribute) return null; 1067 const href = link.getAttribute('href'); 1068 if (!href || href === '#' || href === '#!' || href.startsWith('javascript:')) return null; 1069 1070 let targetId = ''; 1071 if (href.startsWith('#')) { 1072 targetId = href.slice(1); 1073 } else {
1074 try { 1075 const url = new URL(link.href, window.location.href); 1076 const isSamePath = ( 1077 url.origin === window.location.origin && 1078 url.pathname.replace(/\/+$/, '') === window.location.pathname.replace(/\/+$/, '') && 1079 url.search === window.location.search 1080 ); 1081 if (isSamePath && url.hash && url.hash.length > 1) { 1082 targetId = url.hash.slice(1); 1083 } 1084 } catch (e) {} 1085 } 1086 1087 if (!targetId) return null; 1088 const element = getElementByHashOrId(targetId); 1089 return { 1090 id: targetId, 1091 element: element 1092 }; 1093 } 1094 1095 let isScrolling = false; 1096 let lastProcessedHash = ''; 1097 1098 /** 1099 * Smoothly scrolls to target factoring in sticky navigation and accordion animations 1100 */ 1101 function scrollToTarget(target, targetId) { 1102 if (!target) return; 1103 1104 // Open any accordions/tabs containing or associated with target 1105 const wasAccordionOpened = checkAndOpenAccordion(target); 1106 1107 function executeScroll(behavior = 'smooth') { 1108 const offset = getHeaderOffset(); 1109 const elementPosition = target.getBoundingClientRect().top + window.scrollY; 1110 const targetScrollTop = Math.max(0, elementPosition - offset); 1111 1112 isScrolling = true; 1113 window.scrollTo({ 1114 top: targetScrollTop, 1115 behavior: behavior 1116 }); 1117 1118 if (targetId) { 1119 lastProcessedHash = '#' + targetId; 1120 if (window.location.hash !== '#' + targetId) { 1121 history.replaceState(null, null, '#' + targetId); 1122 } 1123 } 1124 1125 setTimeout(() => { 1126 isScrolling = false; 1127 }, 600); 1128 } 1129 1130 if (wasAccordionOpened) { 1131 // If accordion opened, allow slight delay for reflow before computing position 1132 setTimeout(() => { 1133 executeScroll('smooth'); 1134 }, 50); 1135 1136 // Post-expansion correction passes (accounts for CSS accordion expansion timing) 1137 setTimeout(() => { 1138 const offset = getHeaderOffset(); 1139 const currentTop = target.getBoundingClientRect().top; 1140 if (Math.abs(currentTop - offset) > 15) { 1141 executeScroll('smooth'); 1142 } 1143 }, 250); 1144 1145 setTimeout(() => { 1146 const offset = getHeaderOffset(); 1147 const currentTop = target.getBoundingClientRect().top; 1148 if (Math.abs(currentTop - offset) > 15) { 1149 executeScroll('smooth'); 1150 } 1151 }, 450); 1152 } else { 1153 executeScroll('smooth'); 1154 } 1155 } 1156 1157 /** 1158 * Click handler for all same-page anchor links 1159 */ 1160 document.body.addEventListener('click', function (e) { 1161 const tabToggle = e.target.closest('[data-bs-toggle="tab"], [data-bs-toggle="pill"], .splide__arrow, .splide__pagination__page'); 1162 if (tabToggle) return; 1163 1164 const anchor = e.target.closest('a'); 1165 if (!anchor) return; 1166 1167 const anchorInfo = getSamePageAnchorTarget(anchor); 1168 if (anchorInfo && anchorInfo.element) { 1169 e.preventDefault(); 1170 scrollToTarget(anchorInfo.element, anchorInfo.id); 1171 } 1172 }); 1173 1174 /** 1175 * Handles URL hash on initial page load or hashchange 1176 */ 1177 function processHashScroll(isInitialLoad = false) { 1178 const hash = window.location.hash; 1179 if (hash && hash.length > 1 && (isInitialLoad || hash !== lastProcessedHash)) { 1180 const decodedId = decodeURIComponent(hash.slice(1)); 1181 const target = getElementByHashOrId(decodedId) || getElementByHashOrId(hash.slice(1)); 1182 1183 if (target) { 1184 lastProcessedHash = hash; 1185 const delay = isInitialLoad ? 250 : 50; 1186 setTimeout(function () { 1187 scrollToTarget(target, decodedId); 1188 }, delay); 1189 } 1190 } 1191 } 1192 1193 // Handle on DOMContentLoaded (with small timeout), window load, and hashchange 1194 if (window.location.hash) { 1195 setTimeout(function() { 1196 processHashScroll(true); 1197 }, 100); 1198 } 1199 1200 window.addEventListener('load', function () { 1201 processHashScroll(true); 1202 }); 1203 1204 window.addEventListener('hashchange', function () { 1205 processHashScroll(false); 1206 }); 1207}); 1208 1209 1210//gsap animations 1211document.addEventListener('DOMContentLoaded', function () { 1212 if (window.gsap && window.ScrollTrigger) { 1213 const gsap = window.gsap; 1214 const ScrollTrigger = window.ScrollTrigger; 1215 gsap.registerPlugin(ScrollTrigger); 1216 1217 // Disable auto-refresh on resize to prevent recalculation issues 1218 ScrollTrigger.config({ 1219 autoRefreshEvents: "visibilitychange,DOMContentLoaded,load" 1220 });
1221 1222 const cards = gsap.utils.toArray(".sj-stack-container .sj-stack-card"); 1223 1224 // Force normalize card heights before GSAP calculates 1225 if (cards.length > 0) { 1226 // Wait for images and content to load 1227 window.addEventListener('load', () => { 1228 cards.forEach((card) => { 1229 // Reset any inline height from WordPress/Elementor 1230 card.style.height = 'auto'; 1231 card.style.minHeight = '531px'; 1232 }); 1233 1234 // Refresh ScrollTrigger after height normalization 1235 ScrollTrigger.refresh(); 1236 }); 1237 } 1238 1239 cards.forEach((card, index) => { 1240 gsap.to(card, { 1241 scrollTrigger: { 1242 trigger: card, 1243 start: () => `top bottom-=100`, 1244 end: () => `top top+=40`, 1245 scrub: true, 1246 invalidateOnRefresh: true 1247 }, 1248 ease: "none", 1249 scale: () => 1 - (cards.length - index) * 0.025 1250 }); 1251 1252 ScrollTrigger.create({ 1253 trigger: card, 1254 start: "center center-=200", 1255 pin: true, 1256 pinSpacing: false, 1257 endTrigger: ".stop-point", 1258 end: "top center+=500", 1259 id: 'pin', 1260 invalidateOnRefresh: true, 1261 pinType: "transform" 1262 }); 1263 }); 1264 } else { 1265 console.error('GSAP or ScrollTrigger not loaded!'); 1266 } 1267}); 1268 1269 1270// Search focus blur effect 1271/* 1272document.addEventListener('DOMContentLoaded', function() { 1273 const searchInput = document.querySelector('.swp-input--search'); 1274 const homeHeader = document.querySelector('.home-header'); 1275 1276 if (searchInput && homeHeader) { 1277 // Add blur class on focus 1278 searchInput.addEventListener('focus', function() { 1279 homeHeader.classList.add('search-focused'); 1280 //console.log('â Search focused - blur activated'); 1281 }); 1282 1283 // Remove blur class on blur 1284 searchInput.addEventListener('blur', function() { 1285 homeHeader.classList.remove('search-focused'); 1286 //console.log('â Search blurred - blur deactivated'); 1287 }); 1288 } 1289}); 1290*/ 1291// Search focus blur effect (Architectural Update for Async GCSE) 1292document.addEventListener('DOMContentLoaded', function() { 1293 const homeHeader = document.querySelector('.home-header'); 1294 1295 if (homeHeader) { 1296 // We use 'focusin' and 'focusout' because they bubble up the DOM. 1297 // This allows us to catch focus events on the Google Search input 1298 // even though it is injected asynchronously after page load. 1299 1300 homeHeader.addEventListener('focusin', function(e) { 1301 // Check if the element receiving focus is the Google Search input 1302 if (e.target && e.target.classList.contains('gsc-input')) { 1303 homeHeader.classList.add('search-focused'); 1304 // console.log('â Google Search focused - blur activated'); 1305 } 1306 }); 1307 1308 homeHeader.addEventListener('focusout', function(e) { 1309 // Check if the element losing focus is the Google Search input 1310 if (e.target && e.target.classList.contains('gsc-input')) { 1311 homeHeader.classList.remove('search-focused'); 1312 // console.log('â Google Search blurred - blur deactivated'); 1313 } 1314 }); 1315 } 1316}); 1317 1318function checkZoom() { 1319 const zoom = ((window.outerWidth - 10) / window.innerWidth) * 100; 1320 const header = document.querySelector('.site-header'); 1321 1322 if (zoom >= 400) { 1323 header.classList.add('zoom-override'); 1324 } else { 1325 header.classList.remove('zoom-override'); 1326 } 1327} 1328 1329window.addEventListener('resize', checkZoom); 1330checkZoom(); // Run on load 1331 1332 1333//smart quotes - recursive processing for all text nodes 1334function processAllText(element = document.body) { 1335 element.childNodes.forEach(node => { 1336 if (node.nodeType === Node.TEXT_NODE) { 1337 // Process all text content 1338 node.textContent = node.textContent 1339 .replace(/\'(\d)/g, '\u2019$1') // Apostrophe before digits (FIRST to catch all cases) 1340 .replace(/(\W|^)"(\S)/g, '$1\u201C$2') // Opening double quote 1341 .replace(/(\S)"(\W|$)/g, '$1\u201D$2') // Closing double quote 1342 .replace(/(\W|^)\'(?!\d)(\S)/g, '$1\u2018$2') // Opening single quote (but NOT before digits) 1343 .replace(/([a-z])\'([a-z])/gi, '$1\u2019$2') // Apostrophes in contractions (We're, don't, etc) 1344 .replace(/(\S)\'(\W|$)/g, '$1\u2019$2'); // Closing single quote 1345 } else if (node.nodeType === Node.ELEMENT_NODE) { 1346 // Skip script, style, and noscript tags to avoid breaking code 1347 if (!['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.nodeName)) { 1348 processAllText(node); // Recursively process all child elements 1349 } 1350 } 1351 }); 1352} 1353 1354function processSmartQuotes() { 1355 // Process entire document body - catches all text nodes regardless of element type 1356 processAllText(document.body); 1357} 1358 1359// Process on initial page load 1360document.addEventListener('DOMContentLoaded', function() { 1361 processSmartQuotes(); 1362 1363 // Skip to Main Content link handler
1364 const skipLink = document.querySelector('.skip-link'); 1365 const mainContent = document.getElementById('main-content'); 1366 1367 if (skipLink && mainContent) { 1368 skipLink.addEventListener('click', function(e) { 1369 e.preventDefault(); 1370 // Find first focusable element within main-content 1371 const focusableElements = mainContent.querySelectorAll( 1372 'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])' 1373 ); 1374 const firstFocusable = focusableElements[0]; 1375 1376 if (firstFocusable) { 1377 // Focus the first focusable element 1378 firstFocusable.focus(); 1379 } else { 1380 // Fallback: focus main-content container 1381 mainContent.setAttribute('tabindex', '-1'); 1382 mainContent.focus(); 1383 } 1384 1385 // Scroll to main content 1386 mainContent.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1387 }); 1388 } 1389}); 1390 1391// Process accordion content when it's shown (Bootstrap 5) 1392document.addEventListener('shown.bs.collapse', function(event) { 1393 // Process the newly opened accordion content 1394 processAllText(event.target); 1395}); 1396 1397 1398 1399// Fix Swiper "stuck" issue - Ver 1.0 1400// Issue: Swiper carousel arrows work on first click but become unresponsive on clicks 2+ 1401// Diagnosis: Check if internal state flags are stuck OR if click events aren't reaching carousel 1402// Solution: Track clicks, monitor state, attach arrow click listeners, and proactively reset state 1403 1404// ===== PAGE CAROUSEL: Initialize on page load ===== 1405document.addEventListener('DOMContentLoaded', () => { 1406 // Delay to allow Elementor to finish initializing Swiper instances 1407 setTimeout(() => { 1408 initSwiperMonitoring(); 1409 }, 2000); // 2 second delay for Elementor initialization 1410}); 1411 1412// Also try on window load (catches late-loading carousels) 1413window.addEventListener('load', () => { 1414 setTimeout(() => { 1415 initSwiperMonitoring(); 1416 }, 500); 1417}); 1418 1419// ===== LIGHTBOX CAROUSEL: Initialize when lightbox opens ===== 1420// Listen for Elementor lightbox events 1421 1422// Direct click listener on lightbox trigger buttons 1423document.addEventListener('click', (event) => { 1424 const lightboxTrigger = event.target.closest('[data-elementor-open-lightbox="yes"]'); 1425 if (lightboxTrigger) { 1426 // Wait for lightbox to appear in DOM 1427 setTimeout(() => { 1428 const lightboxModal = document.querySelector('.elementor-lightbox'); 1429 const lightboxSwiper = lightboxModal?.querySelector('.swiper'); 1430 1431 if (lightboxSwiper) { 1432 initSwiperMonitoring([lightboxSwiper]); 1433 } 1434 }, 200); 1435 } 1436}, true); // Use capture phase to catch clicks early 1437 1438document.addEventListener('elementor/lightbox/show', (event) => { 1439 // Small delay to ensure lightbox DOM is fully rendered 1440 setTimeout(() => { 1441 const lightboxModal = document.querySelector('.elementor-lightbox'); 1442 const lightboxSwiper = lightboxModal?.querySelector('.swiper'); 1443 1444 if (lightboxSwiper) { 1445 // Pass just the lightbox swiper to the same monitoring function 1446 initSwiperMonitoring([lightboxSwiper]); 1447 } 1448 }, 100); 1449}); 1450 1451// Also listen for alternative event names in case Elementor uses something different 1452document.addEventListener('elementor-lightbox-open', (event) => { 1453 setTimeout(() => { 1454 const lightboxModal = document.querySelector('.elementor-lightbox'); 1455 const lightboxSwiper = lightboxModal?.querySelector('.swiper'); 1456 if (lightboxSwiper) { 1457 initSwiperMonitoring([lightboxSwiper]); 1458 } 1459 }, 100); 1460}); 1461 1462// Monitor DOM for lightbox appearance as fallback 1463const lightboxObserver = new MutationObserver(() => { 1464 const lightboxModal = document.querySelector('.elementor-lightbox.elementor-lightbox--open'); 1465 if (lightboxModal && !lightboxModal.dataset.swiperMonitored) { 1466 const lightboxSwiper = lightboxModal.querySelector('.swiper'); 1467 if (lightboxSwiper) { 1468 lightboxModal.dataset.swiperMonitored = 'true'; 1469 initSwiperMonitoring([lightboxSwiper]); 1470 } 1471 } 1472}); 1473 1474lightboxObserver.observe(document.body, { 1475 childList: true, 1476 subtree: true, 1477 attributes: true, 1478 attributeFilter: ['class'] 1479}); 1480 1481function initSwiperMonitoring(elements) { 1482 // If elements array passed, use those. Otherwise, scan for all .swiper elements 1483 const swiperElements = elements || document.querySelectorAll('.swiper'); 1484
1485 swiperElements.forEach((swiperEl, carouselIndex) => { 1486 // Prevent duplicate initialization on the same swiper element 1487 if (swiperEl?.dataset?.swiperMonitoringInitialized === 'true') { 1488 return; 1489 } 1490 1491 // Try to get Swiper instance (could be stored as .swiper or accessed differently) 1492 let swiper = swiperEl?.swiper; 1493 1494 // If not found, try looking for it in the parent (Elementor might wrap it differently) 1495 if (!swiper && swiperEl.parentElement?.swiper) { 1496 swiper = swiperEl.parentElement.swiper; 1497 } 1498 1499 // Also check if it's on the element itself as a different property 1500 if (!swiper && swiperEl.swiper !== undefined) { 1501 swiper = swiperEl.swiper; 1502 } 1503 1504 // Skip if we can't find swiper instance 1505 if (!swiper) { 1506 return; 1507 } 1508 1509 let clickCount = 0; 1510 let stuckDetectionCount = 0; 1511 1512 // ===== ARROW CLICK TRACKING ===== 1513 // Look for both standard Swiper arrows AND Elementor-specific arrows 1514 const arrows = swiperEl.querySelectorAll( 1515 '.swiper-button-next, .swiper-button-prev, .elementor-swiper-button-next, .elementor-swiper-button-prev' 1516 ); 1517 1518 arrows.forEach(arrow => { 1519 arrow.addEventListener('click', (e) => { 1520 clickCount++; 1521 }); 1522 }); 1523 1524 // ===== EVENT LISTENERS ===== 1525 // Reset state when transitions complete 1526 swiper.on('slideChangeTransitionEnd', () => { 1527 resetSwiperState(swiper, carouselIndex); 1528 }); 1529 1530 swiper.on('slideChange', () => { 1531 resetSwiperState(swiper, carouselIndex); 1532 }); 1533 1534 // Update Elementor lightbox captions when slides change 1535 const isLightboxSwiper = !!swiperEl.closest('.elementor-lightbox'); 1536 if (isLightboxSwiper) { 1537 const syncCaption = () => updateLightboxCaption(swiper); 1538 swiper.on('slideChangeTransitionEnd', syncCaption); 1539 swiper.on('slideChange', syncCaption); 1540 setTimeout(syncCaption, 0); 1541 } 1542 1543 // ===== PROACTIVE STATE MONITOR ===== 1544 // Check every 500ms if carousel appears stuck (animating flag locked) 1545 const existingIntervalId = swiperEl.dataset.swiperMonitoringIntervalId; 1546 if (existingIntervalId) { 1547 clearInterval(Number(existingIntervalId)); 1548 } 1549 1550 const intervalId = setInterval(() => { 1551 if (swiper.animating) { 1552 setTimeout(() => { 1553 if (swiper.animating) { 1554 // State appears stuck - flag hasn't changed in 400ms 1555 resetSwiperState(swiper, carouselIndex); 1556 } 1557 }, 400); 1558 } 1559 }, 500); 1560 1561 swiperEl.dataset.swiperMonitoringIntervalId = String(intervalId); 1562 swiperEl.dataset.swiperMonitoringInitialized = 'true'; 1563 1564 // Update params for safety 1565 swiper.params.preventInteractionOnTransition = false; 1566 swiper.params.simulateTouch = false; 1567 swiper.params.observer = true; 1568 swiper.params.observeParents = true; 1569 }); 1570} 1571 1572// Helper function to reset all stuck state flags 1573function resetSwiperState(swiper, carouselIndex) { 1574 const beforeState = { 1575 animating: swiper.animating, 1576 allowSlideNext: swiper.allowSlideNext, 1577 allowSlidePrev: swiper.allowSlidePrev, 1578 allowTouchMove: swiper.allowTouchMove 1579 }; 1580 1581 // Clear animation flag 1582 swiper.animating = false; 1583 1584 // Reset transition styles that may be lingering 1585 if (swiper.wrapperEl) { 1586 swiper.wrapperEl.style.transitionDuration = '0ms'; 1587 swiper.wrapperEl.style.transform = swiper.wrapperEl.style.transform; 1588 } 1589 1590 // Re-enable all interaction flags 1591 swiper.allowSlideNext = true; 1592 swiper.allowSlidePrev = true; 1593 swiper.allowTouchMove = true; 1594 1595 // Trigger lazy loading for current and adjacent slides 1596 if (swiper.lazy) { 1597 try { 1598 swiper.lazy.load(); 1599 } catch (e) { 1600 } 1601 } else { 1602 // Try manual approach if .lazy doesn't exist 1603 const images = swiper.el.querySelectorAll('img.swiper-lazy'); 1604 images.forEach(img => { 1605 if (img.dataset.src && !img.src) { 1606 img.src = img.dataset.src; 1607 } 1608 }); 1609 } 1610 1611 // Reconcile internal state
1612 swiper.update(); 1613} 1614 1615function updateLightboxCaption(swiper) { 1616 const lightboxModal = swiper?.el?.closest('.elementor-lightbox'); 1617 if (!lightboxModal) return; 1618 1619 const footer = lightboxModal.querySelector('.elementor-slideshow__footer'); 1620 const titleEl = footer?.querySelector('.elementor-slideshow__title'); 1621 const descEl = footer?.querySelector('.elementor-slideshow__description'); 1622 if (!titleEl && !descEl) return; 1623 1624 let activeSlide = swiper.slides?.[swiper.activeIndex]; 1625 const realIndex = typeof swiper.realIndex === 'number' ? swiper.realIndex : null; 1626 if (realIndex !== null && swiper.slides?.length) { 1627 const realSlide = Array.from(swiper.slides).find(slide => { 1628 const slideIndex = slide.getAttribute('data-swiper-slide-index'); 1629 return slideIndex !== null && Number(slideIndex) === realIndex; 1630 }); 1631 if (realSlide) activeSlide = realSlide; 1632 } 1633 1634 if (!activeSlide) return; 1635 1636 let title = activeSlide.getAttribute('data-title') || activeSlide.dataset?.title || ''; 1637 let description = activeSlide.getAttribute('data-description') || activeSlide.dataset?.description || ''; 1638 1639 const mediaEl = activeSlide.querySelector('[data-caption], [data-title], [data-description], img, video, iframe'); 1640 if (mediaEl) { 1641 title = title || mediaEl.getAttribute('data-title') || mediaEl.getAttribute('title') || mediaEl.getAttribute('alt') || ''; 1642 description = description || mediaEl.getAttribute('data-description') || mediaEl.getAttribute('data-caption') || ''; 1643 } 1644 1645 if (titleEl) titleEl.textContent = title; 1646 if (descEl) descEl.textContent = description; 1647} 1648 1649 1650// ===== Search Toggle ===== 1651document.addEventListener('DOMContentLoaded', () => { 1652 const searchToggle = document.getElementById('topBarSearchToggle'); 1653 const searchForm = document.getElementById('topBarSearchForm'); 1654 const navbarMain = document.getElementById('navbarMain'); // Target the main nav wrapper 1655 1656 if (searchToggle && searchForm) { 1657 // 1. Toggle Open and Focus 1658 searchToggle.addEventListener('click', (e) => { 1659 e.preventDefault(); 1660 1661 // Check if it's already open 1662 if (searchForm.classList.contains('active')) { 1663 // CLOSE STATE 1664 searchForm.classList.remove('active'); 1665 searchToggle.setAttribute('aria-expanded', 'false'); 1666 if (navbarMain) navbarMain.classList.remove('no-bg'); 1667 } else { 1668 // OPEN STATE 1669 searchForm.classList.add('active'); 1670 searchToggle.setAttribute('aria-expanded', 'true'); 1671 if (navbarMain) navbarMain.classList.add('no-bg'); 1672 1673 // Dynamically find Google's injected input field and focus it 1674 setTimeout(() => { 1675 const googleInput = searchForm.querySelector('input.gsc-input'); 1676 if (googleInput) { 1677 googleInput.focus(); 1678 } 1679 }, 100); 1680 } 1681 }); 1682 1683 // 2. The "Click Outside" Closer 1684 document.addEventListener('mousedown', (e) => { 1685 // Check if the click was NOT on the form or the toggle button 1686 const isClickInside = searchForm.contains(e.target) || searchToggle.contains(e.target); 1687 1688 if (!isClickInside && searchForm.classList.contains('active')) { 1689 searchForm.classList.remove('active'); 1690 searchToggle.setAttribute('aria-expanded', 'false'); 1691 if (navbarMain) navbarMain.classList.remove('no-bg'); 1692 } 1693 }); 1694 1695 // 3. Accessibility: Close on ESC key 1696 // ADDED 'true' FOR CAPTURE PHASE TO OVERRIDE GOOGLE'S SCRIPT 1697 document.addEventListener('keydown', (e) => { 1698 // If ESC is pressed AND the search menu is currently open 1699 if (e.key === 'Escape' && searchForm.classList.contains('active')) { 1700 searchForm.classList.remove('active'); 1701 searchToggle.setAttribute('aria-expanded', 'false'); 1702 if (navbarMain) navbarMain.classList.remove('no-bg'); 1703 searchToggle.focus(); // Return focus to the magnifying glass 1704 } 1705 }, true); 1706 } 1707});�
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.