1// Video Landing Script 2(function () { 3 const vidLandVideo = document.querySelector(".vid-land-video"); 4 const vidLandFallbackImg = document.querySelector(".vid-land-fallback-img"); 5 const vidLandContainer = document.querySelector(".video-container"); 6 7 if (!vidLandVideo || !vidLandFallbackImg || !vidLandContainer) return; 8 9 let started = false; // a real play() attempt is in flight or succeeded 10 let warmedOnce = false; // the initial load-time "play 3s then pause" has run 11 12 function showFallbackImage() { 13 vidLandVideo.style.display = "none"; 14 vidLandFallbackImg.style.display = "block"; 15 } 16 17 function showVideo() { 18 vidLandFallbackImg.style.display = "none"; 19 vidLandVideo.style.display = "block"; 20 } 21 22 function startVideo() { 23 if (started) return; 24 started = true; 25 26 const playPromise = vidLandVideo.play(); 27 if (playPromise !== undefined) { 28 playPromise.catch((error) => { 29 console.log("Video play failed:", error); 30 showFallbackImage(); 31 started = false; // allow a retry (e.g. on user interaction) 32 }); 33 } 34 } 35 36 // Warm up immediately on page load, regardless of scroll position: play 37 // for ~3s then pause. This swaps the fallback image out for the video's 38 // real frame right away, so by the time someone scrolls down to it â 39 // even quickly â there is no visible fallback->video jump. 40 function warmUp() { 41 if (warmedOnce) return; 42 warmedOnce = true; 43 startVideo(); 44 setTimeout(() => { 45 if (!vidLandVideo.paused) vidLandVideo.pause(); 46 }, 3000); 47 } 48 49 // Resume (and keep looping, via the existing `loop` attribute) once the 50 // section actually enters the viewport. 51 function resumeInView() { 52 if (!started) { 53 startVideo(); 54 return; 55 } 56 if (vidLandVideo.paused) { 57 vidLandVideo.play().catch(() => {}); 58 } 59 } 60 61 function initVideo() { 62 const canPlayMP4 = vidLandVideo.canPlayType("video/mp4"); 63 const canPlayWebM = vidLandVideo.canPlayType("video/webm"); 64 65 if (canPlayWebM === "" && canPlayMP4 === "") { 66 showFallbackImage(); 67 return; 68 } 69 70 // Add error handler
71 vidLandVideo.addEventListener("error", showFallbackImage); 72 73 // The fallback image is shown by default (see CSS); only reveal the 74 // video once it is actually confirmed to be playing, so there is never 75 // a moment where neither is visible (blank space) or both are stacked. 76 vidLandVideo.addEventListener("playing", showVideo); 77 78 warmUp(); 79 80 if ("IntersectionObserver" in window) { 81 const observer = new IntersectionObserver( 82 (entries) => { 83 entries.forEach((entry) => { 84 if (entry.isIntersecting) { 85 resumeInView(); 86 observer.disconnect(); 87 } 88 }); 89 }, 90 { threshold: 0.25 } 91 ); 92 observer.observe(vidLandContainer); 93 } 94 95 // Fallback triggers in case the load-time warm-up was blocked for some reason. 96 vidLandContainer.addEventListener("mouseenter", startVideo, { once: true }); 97 vidLandContainer.addEventListener("click", startVideo, { once: true }); 98 vidLandContainer.addEventListener("touchstart", startVideo, { 99 once: true, 100 passive: true, 101 }); 102 } 103 104 window.addEventListener("DOMContentLoaded", initVideo); 105})(); 106 107// Bro Animation Script 108(function () { 109 document.addEventListener("DOMContentLoaded", function () { 110 // Initialize Bro Animation with viewport detection 111 let broAnimation = null; 112 let broAnimationObserver = null; 113 114 function initBroAnimation() { 115 const container = document.getElementById("bro-animation"); 116 if (!container) { 117 console.error("Bro animation container not found!"); 118 return; 119 } 120 121 // Create the Lottie animation 122 if (typeof lottie !== "undefined") { 123 broAnimation = lottie.loadAnimation({ 124 container: container, 125 renderer: "svg", 126 loop: true, 127 autoplay: false, // Don't autoplay until in viewport 128 path: "../jsons/bro.json", 129 }); 130 131 // Set up after animation is loaded 132 broAnimation.addEventListener("DOMLoaded", function () { 133 console.log("Bro Lottie animation loaded successfully"); 134 setupBroViewportDetection(); 135 }); 136 137 // Debugging Helpers 138 broAnimation.addEventListener("error", function (err) { 139 console.error("Error loading Bro animation:", err); 140 }); 141 } 142 } 143 144 // Set up viewport detection for bro animation 145 function setupBroViewportDetection() { 146 const broSection = document.querySelector(".bro-section"); 147 if (!broSection) { 148 console.error("Bro section not found!"); 149 return; 150 } 151 152 // Create Intersection Observer 153 broAnimationObserver = new IntersectionObserver( 154 (entries) => { 155 entries.forEach((entry) => { 156 if (entry.isIntersecting) { 157 // Section is in viewport, start animation 158 if (broAnimation && !broAnimation.isPlaying) { 159 broAnimation.play(); 160 console.log("Bro animation started - section in viewport"); 161 } 162 } else { 163 // Section is out of viewport, pause animation 164 if (broAnimation && broAnimation.isPlaying) { 165 broAnimation.pause(); 166 console.log("Bro animation paused - section out of viewport"); 167 } 168 } 169 }); 170 }, 171 { 172 threshold: 0.3, // Start when 30% of the section is visible 173 rootMargin: "0px 0px -50px 0px", // Start slightly before the section is fully visible 174 } 175 ); 176 177 // Start observing the bro section 178 broAnimationObserver.observe(broSection); 179 } 180 181 // Initialize bro animation 182 initBroAnimation(); 183 184 // Initialize Scanning Animation 185 const scanningContainer = document.getElementById( 186 "scanning-landing-container" 187 ); 188 if (scanningContainer && typeof lottie !== "undefined") { 189 const scanningAnimation = lottie.loadAnimation({ 190 container: scanningContainer, 191 renderer: "svg", 192 loop: true, 193 autoplay: true, 194 path: "jsons/moniland.json", 195 }); 196 197 scanningAnimation.addEventListener("error", function (err) { 198 console.error("Error loading Scanning animation:", err); 199 }); 200 } 201 }); 202})(); 203 204// Solutions tablist content swap 205(function () { 206 document.addEventListener("DOMContentLoaded", () => { 207 const tablist = document.querySelector(".solutions-tablist"); 208 const tabs = tablist 209 ? Array.from(tablist.querySelectorAll(".solutions-tab")) 210 : []; 211 const dropdownNative = document.querySelector(".solutions-dropdown-native"); 212 const dropdownCombobox = document.getElementById("solutionsDropdownCombobox"); 213 const dropdownList = document.getElementById("solutionsDropdownList"); 214 const dropdownOptions = dropdownList 215 ? Array.from(dropdownList.querySelectorAll(".solutions-dropdown-custom-option")) 216 : []; 217 if (!tablist || tabs.length === 0) return; 218 219 const updateContent = (key) => { 220 if (!key) return; 221 222 const allContentElements = document.querySelectorAll("[data-tab-content]"); 223
224 allContentElements.forEach((element) => { 225 const elementTab = element.getAttribute("data-tab-content"); 226 if (elementTab === key) { 227 element.style.display = ""; 228 } else { 229 element.style.display = "none"; 230 } 231 }); 232 }; 233 234 let isInitialLoad = true; 235 let currentTabIndex = 0; 236 237 const setActiveTab = (tab) => { 238 const panel = document.querySelector(".accessibility-solutions-panel"); 239 const newTabIndex = tabs.indexOf(tab); 240 const slideDirection = 241 newTabIndex > currentTabIndex ? "slide-right" : "slide-left"; 242 243 tabs.forEach((btn) => { 244 const isSelected = btn === tab; 245 btn.setAttribute("aria-selected", isSelected.toString()); 246 btn.tabIndex = isSelected ? 0 : -1; 247 btn.classList.toggle("is-active", isSelected); 248 }); 249 250 if (dropdownNative && tab.dataset.tabKey) { 251 dropdownNative.value = tab.dataset.tabKey; 252 const changeEvent = new Event("change", { bubbles: true }); 253 dropdownNative.dispatchEvent(changeEvent); 254 } 255 256 if (tablist) { 257 const activeTab = tab; 258 const tablistRect = tablist.getBoundingClientRect(); 259 const activeTabRect = activeTab.getBoundingClientRect(); 260 261 const left = activeTabRect.left - tablistRect.left; 262 const width = activeTabRect.width; 263 264 tablist.style.setProperty("--slider-left", `${left}px`); 265 tablist.style.setProperty("--slider-width", `${width}px`); 266 } 267 268 if (panel && !isInitialLoad) { 269 panel.classList.remove("sliding-in", "slide-left", "slide-right"); 270 panel.classList.add("transitioning", slideDirection); 271 setTimeout(() => { 272 updateContent(tab.dataset.tabKey); 273 panel.classList.remove("transitioning"); 274 panel.classList.add("sliding-in", slideDirection); 275 setTimeout(() => { 276 panel.classList.remove("sliding-in", "slide-left", "slide-right"); 277 }, 400); 278 }, 400); 279 } else { 280 updateContent(tab.dataset.tabKey); 281 isInitialLoad = false; 282 } 283 284 currentTabIndex = newTabIndex; 285 }; 286
287 tabs.forEach((tab, index) => { 288 const defaultKey = index === 0 ? "owners" : "partners"; 289 if (!tab.dataset.tabKey) { 290 tab.dataset.tabKey = defaultKey; 291 } 292 tab.addEventListener("click", () => setActiveTab(tab)); 293 tab.addEventListener("keydown", (event) => { 294 if (event.key === "Enter" || event.key === " ") { 295 event.preventDefault(); 296 setActiveTab(tab); 297 } 298 if (event.key === "ArrowRight" || event.key === "ArrowLeft") { 299 event.preventDefault(); 300 const direction = event.key === "ArrowRight" ? 1 : -1; 301 const nextIndex = 302 (tabs.indexOf(tab) + direction + tabs.length) % tabs.length; 303 setActiveTab(tabs[nextIndex]); 304 tabs[nextIndex].focus(); 305 } 306 }); 307 }); 308 309 const initiallySelected = 310 tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || 311 tabs[0]; 312 currentTabIndex = tabs.indexOf(initiallySelected); 313 setActiveTab(initiallySelected); 314 315 if (dropdownCombobox && dropdownList && dropdownOptions.length > 0 && dropdownNative) { 316 let activeDropdownOption = dropdownOptions.find(function (opt) { 317 return opt.getAttribute("aria-selected") === "true"; 318 }) || dropdownOptions[0] || null; 319 320 function setActiveDropdownOption(option, scrollIntoView) { 321 if (!option) return; 322 activeDropdownOption = option; 323 dropdownOptions.forEach(function (opt) { 324 opt.classList.remove("solutions-dropdown-custom-option--active"); 325 }); 326 option.classList.add("solutions-dropdown-custom-option--active"); 327 if (dropdownCombobox) { 328 dropdownCombobox.setAttribute("aria-activedescendant", option.id); 329 } 330 if (scrollIntoView) { 331 option.scrollIntoView({ block: "nearest" }); 332 } 333 } 334 335 function getSelectedDropdownOption() { 336 return dropdownOptions.find(function (opt) { 337 return opt.getAttribute("aria-selected") === "true"; 338 }) || dropdownOptions[0]; 339 } 340 341 function openDropdown() { 342 if (dropdownCombobox.getAttribute("aria-expanded") === "true") { 343 return; 344 } 345 dropdownCombobox.setAttribute("aria-expanded", "true"); 346 dropdownList.hidden = false; 347 setActiveDropdownOption(getSelectedDropdownOption(), true); 348 } 349 350 function closeDropdown() { 351 if (dropdownCombobox.getAttribute("aria-expanded") === "false") { 352 return; 353 } 354 dropdownCombobox.setAttribute("aria-expanded", "false"); 355 dropdownList.hidden = true; 356 setActiveDropdownOption(getSelectedDropdownOption(), false); 357 dropdownCombobox.focus(); 358 } 359 360 function toggleDropdown(forceState) { 361 const isExpanded = dropdownCombobox.getAttribute("aria-expanded") === "true"; 362 const shouldOpen = typeof forceState === "boolean" ? forceState : !isExpanded; 363 if (shouldOpen) { 364 openDropdown(); 365 } else { 366 closeDropdown(); 367 } 368 } 369 370 function moveActiveDropdownOption(step) { 371 if (!dropdownOptions.length) return; 372 const optionsArray = dropdownOptions; 373 const currentIndex = optionsArray.indexOf(activeDropdownOption); 374 let nextIndex = currentIndex + step; 375 if (nextIndex < 0) { 376 nextIndex = 0; 377 } else if (nextIndex >= optionsArray.length) { 378 nextIndex = optionsArray.length - 1; 379 } 380 setActiveDropdownOption(optionsArray[nextIndex], true); 381 } 382 383 function selectDropdownOption(option) { 384 if (!option) return; 385 const value = option.getAttribute("data-value"); 386 const selectOption = Array.from(dropdownNative.options).find(function (opt) { 387 return opt.value === value; 388 }); 389 if (selectOption) { 390 dropdownNative.selectedIndex = Array.from(dropdownNative.options).indexOf(selectOption); 391 const changeEvent = new Event("change", { bubbles: true });
392 dropdownNative.dispatchEvent(changeEvent); 393 } 394 dropdownOptions.forEach(function (opt) { 395 opt.setAttribute("aria-selected", opt === option ? "true" : "false"); 396 }); 397 updateDropdownDisplay(option); 398 closeDropdown(); 399 } 400 401 function selectActiveDropdownOption() { 402 if (activeDropdownOption) { 403 selectDropdownOption(activeDropdownOption); 404 } 405 } 406 407 function updateDropdownDisplay(option) { 408 const selectedSpan = dropdownCombobox.querySelector(".solutions-dropdown-custom-selected"); 409 if (!selectedSpan) return; 410 const text = option ? option.querySelector(".solutions-dropdown-custom-text").textContent : "Select option"; 411 selectedSpan.innerHTML = '<span class="solutions-dropdown-custom-text">' + text + "</span>"; 412 if (option) { 413 setActiveDropdownOption(option, false); 414 } 415 } 416 417 function syncDropdownToNative() { 418 const selectedIndex = dropdownNative.selectedIndex; 419 const selectedOption = dropdownNative.options[selectedIndex]; 420 const customOption = Array.from(dropdownOptions).find(function (opt) { 421 return opt.getAttribute("data-value") === selectedOption.value; 422 }); 423 if (customOption) { 424 dropdownOptions.forEach(function (opt) { 425 opt.setAttribute("aria-selected", "false"); 426 }); 427 customOption.setAttribute("aria-selected", "true"); 428 updateDropdownDisplay(customOption); 429 } 430 } 431 432 dropdownCombobox.addEventListener("click", function (e) { 433 const button = e.target.closest(".solutions-dropdown-custom-button"); 434 if (!button) { 435 return; 436 } 437 e.stopPropagation(); 438 toggleDropdown(); 439 }); 440 441 document.addEventListener("click", function (e) { 442 if (!dropdownCombobox.contains(e.target) && !dropdownList.contains(e.target)) { 443 closeDropdown(); 444 } 445 }); 446 447 dropdownOptions.forEach(function (option) { 448 option.addEventListener("mousedown", function (e) { 449 e.preventDefault(); 450 }); 451 option.addEventListener("click", function () { 452 selectDropdownOption(option); 453 }); 454 }); 455 456 dropdownCombobox.addEventListener("keydown", function (e) { 457 const isExpanded = dropdownCombobox.getAttribute("aria-expanded") === "true"; 458 if (e.key === "Enter" || e.key === " ") { 459 e.preventDefault(); 460 if (isExpanded) { 461 selectActiveDropdownOption(); 462 } else { 463 openDropdown(); 464 } 465 return; 466 } 467 if (e.key === "Escape") { 468 if (isExpanded) { 469 e.preventDefault(); 470 e.stopPropagation(); 471 closeDropdown(); 472 } 473 return; 474 } 475 if (e.key === "ArrowDown") { 476 e.preventDefault(); 477 if (!isExpanded) { 478 openDropdown(); 479 } 480 moveActiveDropdownOption(1); 481 return; 482 } 483 if (e.key === "ArrowUp") { 484 e.preventDefault(); 485 if (!isExpanded) { 486 openDropdown(); 487 } 488 moveActiveDropdownOption(-1); 489 return; 490 } 491 if (e.key === "Home") { 492 e.preventDefault(); 493 openDropdown();
494 setActiveDropdownOption(dropdownOptions[0], true); 495 return; 496 } 497 if (e.key === "End") { 498 e.preventDefault(); 499 openDropdown(); 500 setActiveDropdownOption(dropdownOptions[dropdownOptions.length - 1], true); 501 return; 502 } 503 if (e.key === "Tab") { 504 if (isExpanded) { 505 closeDropdown(); 506 } 507 } 508 }); 509 510 dropdownNative.addEventListener("change", function () { 511 syncDropdownToNative(); 512 const selectedValue = dropdownNative.value; 513 const tabToSelect = tabs.find((tab) => tab.dataset.tabKey === selectedValue); 514 if (tabToSelect) { 515 setActiveTab(tabToSelect); 516 } 517 }); 518 519 syncDropdownToNative(); 520 } 521 522 window.addEventListener("resize", () => { 523 const activeTab = tabs.find( 524 (tab) => tab.getAttribute("aria-selected") === "true" 525 ); 526 if (activeTab && tablist) { 527 const tablistRect = tablist.getBoundingClientRect(); 528 const activeTabRect = activeTab.getBoundingClientRect(); 529 530 const left = activeTabRect.left - tablistRect.left; 531 const width = activeTabRect.width; 532 533 tablist.style.setProperty("--slider-left", `${left}px`); 534 tablist.style.setProperty("--slider-width", `${width}px`); 535 } 536 }); 537 }); 538})(); 539 540// AIZA Animation Script 541(function () { 542 document.addEventListener("DOMContentLoaded", function () { 543 // Initialize variables 544 let aizaAnimation = null; 545 let animationTimeout = null; 546 let hasPlayedOnce = false; // Track if initial 11-second play has happened 547 let isPlaying = false; 548 let initialPlayInProgress = false; // Flag to track if initial 11-second play is in progress 549 const animationDuration = 11000;
549 // 11 seconds in milliseconds 550 551 // Initialize the Lottie animation 552 function initAnimation() { 553 // Make sure the container exists 554 const container = document.getElementById("aiza-lottie-container"); 555 if (!container) { 556 console.error("Animation container not found!"); 557 return; 558 } 559 560 if (typeof lottie === "undefined") { 561 console.error("Lottie library not loaded!"); 562 return; 563 } 564 565 // Create the Lottie animation 566 aizaAnimation = lottie.loadAnimation({ 567 container: container, 568 renderer: "svg", 569 loop: true, 570 autoplay: false, // Don't autoplay until in viewport 571 path: "jsons/aiza.json", 572 rendererSettings: { 573 preserveAspectRatio: "xMidYMid slice", 574 className: "aiza-lottie-svg", 575 }, 576 }); 577 578 // Set up after animation is loaded 579 aizaAnimation.addEventListener("DOMLoaded", function () { 580 console.log("Lottie animation loaded successfully"); 581 setupListeners(); 582 checkIfInViewport(); // Check initial position 583 }); 584 } 585 586 // Set up event listeners for hover and scroll 587 function setupListeners() { 588 const container = document.getElementById("aiza-lottie-container"); 589 590 // Hover events 591 container.addEventListener("mouseenter", function () { 592 console.log("Mouse enter"); 593 594 // Only respond to hover after initial play has completed and animation is not in initial play 595 if ( 596 aizaAnimation && 597 !isPlaying && 598 hasPlayedOnce && 599 !initialPlayInProgress 600 ) { 601 console.log("Playing animation on hover"); 602 playAnimationGradually(); 603 } 604 }); 605 606 container.addEventListener("mouseleave", function () { 607 console.log("Mouse leave"); 608 609 // Only respond to unhover after initial play has completed and animation is not in initial play 610 if ( 611 aizaAnimation && 612 isPlaying && 613 hasPlayedOnce && 614 !initialPlayInProgress 615 ) { 616 console.log("Pausing animation on unhover"); 617 pauseAnimationGradually(); 618 } 619 }); 620 621 // Set up Intersection Observer 622 setupIntersectionObserver(); 623 } 624 625 // Play animation with gradual speed increase 626 function playAnimationGradually() { 627 if (!aizaAnimation) return; 628 629 // Start at low speed and gradually increase 630 aizaAnimation.setSpeed(0.1); 631 aizaAnimation.play(); 632 isPlaying = true; 633 634 // Gradually increase speed 635 let speed = 0.1; 636 const interval = setInterval(function () { 637 speed += 0.1; 638 aizaAnimation.setSpeed(speed); 639 640 if (speed >= 1) { 641 clearInterval(interval); 642 aizaAnimation.setSpeed(1); 643 } 644 }, 50); 645 } 646 647 // Pause animation with gradual slowdown 648 function pauseAnimationGradually() { 649 if (!aizaAnimation) return; 650 651 // Gradually decrease speed 652 let speed = 1; 653 const interval = setInterval(function () { 654 speed -= 0.1; 655 aizaAnimation.setSpeed(speed); 656 657 if (speed <= 0.1) { 658 clearInterval(interval); 659 aizaAnimation.pause(); 660 isPlaying = false; 661 } 662 }, 50); 663 } 664 665 // Check if element is in viewport 666 function checkIfInViewport() { 667 const container = document.getElementById("aiza-lottie-container"); 668 if (!container) return; 669 670 const rect = container.getBoundingClientRect(); 671 const isInViewport = 672 rect.top >= 0 && 673 rect.left >= 0 && 674 rect.bottom <= 675 (window.innerHeight || document.documentElement.clientHeight) && 676 rect.right <= 677 (window.innerWidth || document.documentElement.clientWidth); 678 679 if (isInViewport && !hasPlayedOnce && !initialPlayInProgress) { 680 console.log("Animation in viewport - starting initial play"); 681 playInitialAnimation(); 682 } 683 } 684
685 // Play animation for initial 11 seconds when first in viewport 686 function playInitialAnimation() { 687 if (!aizaAnimation) return; 688 689 // Set flag that initial play is in progress 690 initialPlayInProgress = true; 691 692 // Play the animation 693 playAnimationGradually(); 694 695 // Set timeout to stop after 11 seconds 696 clearTimeout(animationTimeout); 697 animationTimeout = setTimeout(function () { 698 console.log("Initial 11 seconds complete - pausing"); 699 pauseAnimationGradually(); 700 701 // Mark that initial play has completed and is no longer in progress 702 hasPlayedOnce = true; 703 initialPlayInProgress = false; 704 }, animationDuration); 705 } 706 707 // Use Intersection Observer for better performance 708 function setupIntersectionObserver() { 709 if ("IntersectionObserver" in window) { 710 const options = { 711 root: null, 712 rootMargin: "0px", 713 threshold: 0.5, // 50% of element must be visible 714 }; 715 716 const observer = new IntersectionObserver((entries) => { 717 entries.forEach((entry) => { 718 if ( 719 entry.isIntersecting && 720 !hasPlayedOnce && 721 !initialPlayInProgress 722 ) { 723 console.log("Observer detected animation in viewport"); 724 playInitialAnimation(); 725 } 726 }); 727 }, options); 728 729 const container = document.getElementById("aiza-lottie-container"); 730 if (container) { 731 observer.observe(container); 732 } 733 } else { 734 // Fallback to scroll event 735 window.addEventListener("scroll", debounce(checkIfInViewport, 100)); 736 window.addEventListener("resize", debounce(checkIfInViewport, 100)); 737 } 738 } 739 740 // Debounce function to limit how often a function is called 741 function debounce(func, wait) { 742 let timeout; 743 return function () { 744 const context = this; 745 const args = arguments; 746 clearTimeout(timeout); 747 timeout = setTimeout(function () { 748 func.apply(context, args); 749 }, wait); 750 }; 751 } 752 753 // Initialize animation 754 initAnimation(); 755 }); 756})(); 757 758// Black Card Scan Script 759(function () { 760 document.addEventListener("DOMContentLoaded", function () { 761 const input = document.querySelector(".black-card-scan .sca-input"); 762 const button = document.querySelector(".black-card-scan .sca-button"); 763 const helperContainer = document.querySelector(".helper-text-container"); 764 765 if (!input || !button) return; 766 767 // Add animation class to button after a short delay 768 setTimeout(() => { 769 button.classList.add("animate"); 770 }, 500); 771 772 function isValidUrl(domain) { 773 const originalDomain = domain; 774 const hasWww = originalDomain.match(/^(https?:\/\/)?www\./); 775 domain = domain.replace(/^(https?:\/\/)?(www\.)?/, ""); 776 const urlWithProtocol = hasWww 777 ? "https://www." + domain 778 : "https://" + domain; 779 try { 780 const url = new URL(urlWithProtocol); 781 const cleanUrl = 782 url.protocol + "//" + url.hostname + (url.port ? ":" + url.port : ""); 783 return { valid: true, url: cleanUrl }; 784 } catch (error) { 785 return { valid: false, error: "Invalid domain format." }; 786 } 787 } 788 789 function showError(message) { 790 if (input) input.style.borderColor = "#ff3b30"; 791 if (helperContainer) { 792 helperContainer.style.visibility = "visible"; 793 helperContainer.innerText = message 794 ? message 795 : "Valid formats: example.com, subdomain.example.com"; 796 } 797 } 798 799 function clearError() { 800 if (input) input.style.borderColor = ""; 801 if (helperContainer) helperContainer.style.visibility = "hidden"; 802 } 803 804 function handleDomainInput() { 805 const domain = input.value.trim(); 806 if (!domain) { 807 showError(); 808 return; 809 } 810 const { valid, error, url } = isValidUrl(domain); 811 if (valid) { 812 clearError(); 813 const currentWhitelist = new URLSearchParams( 814 window.location.search 815 ).get("whitelist"); 816 const scanPageExt = 817 document.body.getAttribute("data-scan-page-ext") || ""; 818 window.location.href = `/accessibility-checker/scan${scanPageExt}?domain=${encodeURIComponent(url)}&tnv_source=${encodeURIComponent(window.location.pathname)}${currentWhitelist ? `&whitelist=${encodeURIComponent(currentWhitelist)}` : ""}`; 819 } else { 820 showError(error); 821 } 822 } 823 824 input.addEventListener("input", clearError); 825 button.addEventListener("click", handleDomainInput); 826 input.addEventListener("keypress", (e) => { 827 if (e.key === "Enter") { 828 handleDomainInput(); 829 } 830 });
831 window.addEventListener("resize", function () { 832 if (helperContainer && helperContainer.style.visibility === "visible") { 833 showError(helperContainer.innerText); 834 } 835 }); 836 }); 837})(); 838 839// Scan CTA Script 840(function () { 841 // Trigger animation on page load 842 window.addEventListener("load", function () { 843 const container = document.querySelector(".scancta-input-container"); 844 if (container) { 845 setTimeout(() => { 846 container.classList.add("scancta-animated"); 847 }, 800); 848 } 849 }); 850 851 // Smart domain validation function 852 function scanctaIsValidDomain(domain) { 853 domain = domain.replace(/^https?:\/\//, "").replace(/^www\./, ""); 854 855 // Extract just the domain part (before any path) 856 domain = domain.split("/")[0]; 857 858 // Remove trailing slash (now redundant but kept for safety) 859 domain = domain.replace(/\/$/, ""); 860 861 const domainRegex = 862 /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/; 863 return domainRegex.test(domain); 864 } 865 866 function scanctaShowError(message) { 867 const errorDiv = document.getElementById("scanctaErrorMessage"); 868 const input = document.querySelector(".scancta-input-field"); 869 if (errorDiv) errorDiv.textContent = message; 870 if (errorDiv) errorDiv.classList.add("show"); 871 if (input) input.classList.add("error"); 872 setTimeout(() => { 873 scanctaHideError(); 874 }, 5000); 875 } 876 877 function scanctaHideError() { 878 const errorDiv = document.getElementById("scanctaErrorMessage"); 879 const input = document.querySelector(".scancta-input-field"); 880 if (errorDiv) errorDiv.classList.remove("show"); 881 if (input) input.classList.remove("error"); 882 } 883 884 function scanctaStartScan() { 885 const button = document.querySelector(".scancta-button"); 886 const input = document.querySelector(".scancta-input-field"); 887 const container = document.querySelector(".scancta-input-container"); 888 const url = input ? input.value.trim() : ""; 889 890 scanctaHideError(); 891 892 if (!url) { 893 scanctaShowError("Please enter a website URL"); 894 if (input) input.focus(); 895 return; 896 } 897 898 if (!scanctaIsValidDomain(url)) { 899 scanctaShowError( 900 "Please enter a valid domain (e.g., example.com, sub.example.co.uk)" 901 ); 902 if (input) input.focus(); 903 return; 904 } 905 906 // Clear input field before redirecting to prevent browser back button cache issues 907 if (input) { 908 input.value = ""; 909 input.classList.remove("has-value"); 910 } 911 if (container) container.classList.add("scancta-slide-back"); 912 if (button) { 913 button.classList.add("loading"); 914 button.textContent = "Scanning"; 915 } 916 917 setTimeout(() => { 918 // Clean the domain: remove protocol, www, trailing slashes, params, hash, and file extensions 919 let domain = url 920 .replace(/^https?:\/\//, "") // Remove protocol 921 .replace(/^www\./, "") // Remove www 922 .split("?")[0] // Remove URL parameters 923 .split("#")[0] // Remove hash fragments 924 .replace(/\/+$/, "") // Remove trailing slashes (MOVED BEFORE file extension removal) 925 .replace(/\.(html|php|htm|asp|aspx|jsp|xml|json)$/i, "") // Remove file extensions 926 .replace(/\/\/+/g, "/"); // Replace multiple slashes with single slash 927 928 const currentWhitelist = new URLSearchParams(window.location.search).get( 929 "whitelist" 930 ); 931 const scanPageExt = 932 document.body.getAttribute("data-scan-page-ext") || ""; 933 let scanUrl = `/accessibility-checker/scan${scanPageExt}?domain=${encodeURIComponent("https://" + domain)}&tnv_source=${encodeURIComponent(window.location.pathname)}`; 934 935 if (currentWhitelist) { 936 scanUrl += `&whitelist=${currentWhitelist}`; 937 } 938 939 window.location.href = scanUrl; 940 }, 2000); 941 } 942 943 // Admin scan function for screen recording 944 function adminScanctaStartScan() { 945 const button = document.querySelector(".scancta-button"); 946 const input = document.querySelector(".scancta-input-field"); 947 const container = document.querySelector(".scancta-input-container"); 948 const url = input ? input.value.trim() : ""; 949 950 scanctaHideError(); 951 952 if (!url) { 953 scanctaShowError("Please enter a website URL"); 954 if (input) input.focus(); 955 return; 956 } 957 958 if (!scanctaIsValidDomain(url)) { 959 scanctaShowError( 960 "Please enter a valid domain (e.g., example.com, sub.example.co.uk)" 961 ); 962 if (input) input.focus(); 963 return; 964 } 965 966 // Clear input field before redirecting to prevent browser back button cache issues 967 if (input) { 968 input.value = ""; 969 input.classList.remove("has-value"); 970 } 971 if (container) container.classList.add("scancta-slide-back"); 972 if (button) { 973 button.classList.add("loading"); 974 button.textContent = "Scanning"; 975 } 976 977 setTimeout(() => { 978 // Clean the domain: remove protocol, www, trailing slashes, params, hash, and file extensions 979 let domain = url 980 .replace(/^https?:\/\//, "") // Remove protocol 981 .replace(/^www\./, "") // Remove www 982 .split("?")[0] // Remove URL parameters 983 .split("#")[0] // Remove hash fragments 984 .replace(/\/+$/, "") // Remove trailing slashes (MOVED BEFORE file extension removal) 985 .replace(/\.(html|php|htm|asp|aspx|jsp|xml|json)$/i, "") // Remove file extensions 986 .replace(/\/\/+/g, "/"); // Replace multiple slashes with single slash 987 988 const currentWhitelist = new URLSearchParams(window.location.search).get( 989 "whitelist" 990 ); 991 const scanPageExt = 992 document.body.getAttribute("data-scan-page-ext") || ""; 993 let scanUrl = `/accessibility-checker/scan${scanPageExt}?domain=${encodeURIComponent("https://" + domain)}&tnv_source=${encodeURIComponent(window.location.pathname)}&tnvp=true&tnv-bh=true&adminScan=true`; 994 995 if (currentWhitelist) { 996 scanUrl += `&whitelist=${currentWhitelist}`; 997 } 998 999 window.location.href = scanUrl; 1000 }, 2000); 1001 } 1002 1003 // Make adminScanctaStartScan available globally for onclick handlers 1004 window.adminScanctaStartScan = adminScanctaStartScan; 1005 1006 // Form submission handler 1007 document.addEventListener("DOMContentLoaded", function () { 1008 const form = document.querySelector(".scancta-wrapper"); 1009 if (form) { 1010 form.addEventListener("submit", function (e) { 1011 e.preventDefault(); 1012 scanctaStartScan(); 1013 }); 1014 } 1015 1016 // Admin mode toggle functionality 1017 let adminMode = false; 1018 1019 // Keyboard shortcut to toggle admin mode (Ctrl+Shift+A) 1020 document.addEventListener("keydown", function (e) { 1021 if (e.ctrlKey && e.shiftKey && e.key === "A") { 1022 e.preventDefault(); 1023 adminMode = !adminMode; 1024 toggleAdminButtons(); 1025 console.log("Admin mode:", adminMode ? "enabled" : "disabled"); 1026 } 1027 });
1028 1029 // Function to toggle admin buttons visibility 1030 function toggleAdminButtons() { 1031 const adminButtons = document.querySelectorAll(".admin-scan-btn"); 1032 const originalButtons = document.querySelectorAll( 1033 ".scancta-button:not(.admin-scan-btn):not(.footer-cta-section .scancta-button)" 1034 ); 1035 1036 adminButtons.forEach((button) => { 1037 button.style.display = adminMode ? "block" : "none"; 1038 }); 1039 1040 originalButtons.forEach((button) => { 1041 if (!button.closest(".footer-cta-section")) { 1042 button.style.display = adminMode ? "none" : "block"; 1043 } 1044 }); 1045 } 1046 1047 // Input handlers 1048 const scanctaInput = document.querySelector(".scancta-input-field"); 1049 if (scanctaInput) { 1050 // Add placeholder attribute for proper CSS behavior 1051 scanctaInput.setAttribute("placeholder", " "); 1052 1053 // Handle input changes to manage the has-value class 1054 scanctaInput.addEventListener("input", function () { 1055 scanctaHideError(); 1056 if (this.value.trim()) { 1057 this.classList.add("has-value"); 1058 } else { 1059 this.classList.remove("has-value"); 1060 } 1061 }); 1062 1063 // Handle focus styles 1064 scanctaInput.addEventListener("focus", function (e) { 1065 if (e.target.matches(":focus-visible")) { 1066 this.style.boxShadow = 1067 "0 8px 25px rgba(0, 0, 0, 0.15), 0 0 0 3px rgba(139, 92, 246, 0.3)"; 1068 } 1069 }); 1070 1071 scanctaInput.addEventListener("blur", function () { 1072 // Reset box shadow but maintain border if has value 1073 if (this.classList.contains("has-value")) { 1074 this.style.boxShadow = "0 8px 25px rgba(0, 0, 0, 0.15)"; 1075 } else { 1076 this.style.boxShadow = "0 8px 25px rgba(0, 0, 0, 0.15)"; 1077 } 1078 // Keep has-value class if input has content 1079 if (this.value.trim()) { 1080 this.classList.add("has-value"); 1081 } else { 1082 this.classList.remove("has-value"); 1083 } 1084 }); 1085 } 1086 }); 1087})(); 1088 1089// Features Slideshow Script 1090(function () { 1091 let slideIndex = 1; 1092 let slideInterval; 1093 let progressTimeout = null; 1094 let isPlaying = true; 1095 let isTransitioning = false; 1096 const SLIDE_DURATION = 20000; 1097 1098 function showSlide(n) { 1099 if (isTransitioning) return; 1100 isTransitioning = true; 1101 1102 const slides = document.querySelectorAll(".slide"); 1103 if (n > slides.length) slideIndex = 1; 1104 else if (n < 1) slideIndex = slides.length; 1105 else slideIndex = n; 1106 1107 // Find current active slide 1108 const currentActiveSlide = document.querySelector(".slide.active"); 1109 const newSlide = slides[slideIndex - 1]; 1110 1111 // If same slide, just return 1112 if (currentActiveSlide === newSlide) { 1113 isTransitioning = false; 1114 return; 1115 } 1116 1117 // Reset all progress bars first 1118 slides.forEach((slide) => { 1119 const bar = slide.querySelector(".slideshow-progress-bar"); 1120 if (bar) { 1121 bar.querySelectorAll(".progress-segment").forEach((seg) => { 1122 seg.classList.remove("active", "completed", "loading"); 1123 }); 1124 } 1125 }); 1126 1127 // If this is the initial load (no active slide), handle it differently 1128 if (!currentActiveSlide) { 1129 // Initial slide setup 1130 newSlide.style.display = "flex"; 1131 setTimeout(() => { 1132 // Hide all other slides 1133 slides.forEach((slide) => { 1134 if (slide !== newSlide) { 1135 slide.style.display = "none"; 1136 slide.classList.remove("active"); 1137 } 1138 }); 1139 1140 // Activate the first slide 1141 newSlide.classList.add("active"); 1142 updateProgressBar(slideIndex); 1143 // Only start progress bar animation if playing 1144 if (isPlaying) { 1145 animateProgressBar(slideIndex); 1146 } 1147 1148 setTimeout(() => { 1149 isTransitioning = false; 1150 }, 100); 1151 }, 50); 1152 return; 1153 } 1154 1155 // Fade out current slide first 1156 currentActiveSlide.classList.remove("active"); 1157 1158 // Prepare new slide (make it visible but transparent) 1159 newSlide.style.display = "flex"; 1160 1161 // Small delay to ensure display change is processed 1162 setTimeout(() => { 1163 // Hide all other slides
1164 slides.forEach((slide) => { 1165 if (slide !== newSlide) { 1166 slide.style.display = "none"; 1167 slide.classList.remove("active"); 1168 } 1169 }); 1170 1171 // Fade in new slide 1172 setTimeout(() => { 1173 newSlide.classList.add("active"); 1174 // Update and animate progress bar for this slide 1175 updateProgressBar(slideIndex); 1176 // Only start progress bar animation if playing 1177 if (isPlaying) { 1178 animateProgressBar(slideIndex); 1179 } 1180 1181 // Allow new transitions after animation completes 1182 setTimeout(() => { 1183 isTransitioning = false; 1184 }, 800); // Match the CSS transition duration 1185 }, 50); 1186 }, 50); 1187 } 1188 1189 function updateProgressBar(slideIdx) { 1190 // Only update the progress bar in the active slide 1191 const slides = document.querySelectorAll(".slide"); 1192 const bar = slides[slideIdx - 1].querySelector(".slideshow-progress-bar"); 1193 if (!bar) return; 1194 bar.querySelectorAll(".progress-segment").forEach((segment, idx) => { 1195 segment.classList.remove("active", "completed", "loading"); 1196 if (idx + 1 === slideIdx) { 1197 segment.classList.add("active"); 1198 } else if (idx + 1 < slideIdx) { 1199 segment.classList.add("completed"); 1200 } 1201 }); 1202 } 1203 1204 function animateProgressBar(slideIdx) { 1205 clearTimeout(progressTimeout); 1206 // Only animate if playing 1207 if (!isPlaying) return; 1208 1209 // Only animate the progress bar in the active slide 1210 const slides = document.querySelectorAll(".slide"); 1211 const bar = slides[slideIdx - 1].querySelector(".slideshow-progress-bar"); 1212 if (!bar) return; 1213 const segments = bar.querySelectorAll(".progress-segment"); 1214 segments.forEach((segment, idx) => { 1215 segment.classList.remove("loading"); 1216 if (idx + 1 === slideIdx) { 1217 // Force a reflow to ensure the animation starts 1218 // This prevents the browser from batching the remove and add operations 1219 void segment.offsetWidth; 1220 1221 // Animate the active segment 1222 setTimeout(() => { 1223 // Double-check if still playing before starting animation 1224 if (isPlaying) { 1225 segment.classList.add("loading"); 1226 // Listen for animation end to auto-advance 1227 segment.addEventListener("animationend", handleProgressComplete, { 1228 once: true, 1229 }); 1230 } 1231 }, 50); 1232 } 1233 }); 1234 } 1235 1236 function handleProgressComplete() { 1237 if (isPlaying) { 1238 // Small delay before transitioning to next slide 1239 setTimeout(() => { 1240 nextSlide(); 1241 }, 200); 1242 } 1243 } 1244 1245 // Function to handle arrow clicks 1246 function changeSlide(direction) { 1247 if (direction === 1) { 1248 nextSlide(); 1249 } else if (direction === -1) { 1250 previousSlide(); 1251 } 1252 } 1253 1254 function nextSlide() { 1255 showSlide(slideIndex + 1); 1256 resetInterval(); 1257 } 1258 1259 function previousSlide() { 1260 showSlide(slideIndex - 1); 1261 resetInterval(); 1262 } 1263 1264 function resetInterval() { 1265 clearInterval(slideInterval); 1266 clearTimeout(progressTimeout); 1267 if (isPlaying) { 1268 animateProgressBar(slideIndex); 1269 } 1270 } 1271 1272 function togglePlayPause() { 1273 const playIcons = document.querySelectorAll(".play-icon"); 1274 const pauseIcons = document.querySelectorAll(".pause-icon"); 1275 isPlaying = !isPlaying; 1276 1277 if (isPlaying) { 1278 // Resume playing 1279 playIcons.forEach((icon) => (icon.style.display = "none")); 1280 pauseIcons.forEach((icon) => (icon.style.display = "block")); 1281 // Restart progress bar animation from the beginning 1282 updateProgressBar(slideIndex); 1283 animateProgressBar(slideIndex); 1284 } else { 1285 // Pause playing 1286 playIcons.forEach((icon) => (icon.style.display = "block")); 1287 pauseIcons.forEach((icon) => (icon.style.display = "none")); 1288 clearTimeout(progressTimeout); 1289 1290 // Stop progress bar animation by removing loading class but keep active state 1291 const slides = document.querySelectorAll(".slide"); 1292 const bar = slides[slideIndex - 1].querySelector( 1293 ".slideshow-progress-bar" 1294 ); 1295 if (bar) { 1296 const activeSegment = bar.querySelector(".progress-segment.active"); 1297 if (activeSegment) { 1298 activeSegment.classList.remove("loading");
1299 // Remove any pending animation event listeners 1300 activeSegment.removeEventListener( 1301 "animationend", 1302 handleProgressComplete 1303 ); 1304 // Ensure the active segment shows its color when paused 1305 // The CSS rule .progress-segment.active:not(.loading)::before will handle this 1306 } 1307 } 1308 } 1309 } 1310 1311 // Make functions available globally for onclick handlers 1312 window.changeSlide = changeSlide; 1313 window.togglePlayPause = togglePlayPause; 1314 1315 // Progress bar click/keyboard navigation 1316 function setupProgressBarNavigation() { 1317 document.querySelectorAll(".slide").forEach((slide, sidx) => { 1318 const bar = slide.querySelector(".slideshow-progress-bar"); 1319 if (!bar) return; 1320 bar.querySelectorAll(".progress-segment").forEach((segment) => { 1321 segment.addEventListener("click", () => { 1322 const slideNum = parseInt(segment.dataset.slide, 10); 1323 showSlide(slideNum); 1324 resetInterval(); 1325 }); 1326 segment.addEventListener("keydown", (e) => { 1327 if (e.key === "Enter" || e.key === " ") { 1328 e.preventDefault(); 1329 const slideNum = parseInt(segment.dataset.slide, 10); 1330 showSlide(slideNum); 1331 resetInterval(); 1332 } 1333 }); 1334 }); 1335 }); 1336 } 1337 1338 document.addEventListener("DOMContentLoaded", function () { 1339 // Intersection Observer for viewport detection 1340 const slideshowSection = document.querySelector(".features-slideshow"); 1341 let slideshowStarted = false; 1342 let wasPlayingBeforePause = false; 1343 1344 if (!slideshowSection) return; 1345 1346 const observer = new IntersectionObserver( 1347 (entries) => { 1348 entries.forEach((entry) => { 1349 if (entry.isIntersecting) { 1350 // Slideshow is in viewport 1351 if (!slideshowStarted) { 1352 slideshowStarted = true; 1353 // Add fade-in class 1354 slideshowSection.classList.add("in-viewport"); 1355 1356 // Start slideshow after fade-in completes 1357 setTimeout(() => { 1358 showSlide(1); 1359 // Ensure autoplay starts for the first slide only if playing 1360 if (isPlaying) { 1361 const firstSlide = document.querySelector(".slide.active"); 1362 if (firstSlide) { 1363 updateProgressBar(1); 1364 animateProgressBar(1); 1365 } 1366 } 1367 }, 200); // Wait 300ms for fade-in to start 1368 } else { 1369 // Slideshow was already started, resume if it was playing before 1370 if (wasPlayingBeforePause) { 1371 isPlaying = true; 1372 wasPlayingBeforePause = false; 1373 // Resume progress bar animation for current slide 1374 const currentSlide = document.querySelector(".slide.active"); 1375 if (currentSlide) { 1376 animateProgressBar(slideIndex); 1377 } 1378 } 1379 } 1380 } else { 1381 // Slideshow is out of viewport 1382 if (slideshowStarted) { 1383 // Pause the slideshow 1384 if (isPlaying) { 1385 wasPlayingBeforePause = true; 1386 isPlaying = false; 1387 // Stop progress bar animation 1388 clearTimeout(progressTimeout); 1389 const currentSlide = document.querySelector(".slide.active"); 1390 if (currentSlide) { 1391 const bar = currentSlide.querySelector( 1392 ".slideshow-progress-bar" 1393 ); 1394 if (bar) { 1395 bar.querySelectorAll(".progress-segment").forEach((seg) => { 1396 seg.classList.remove("loading"); 1397 }); 1398 } 1399 } 1400 } 1401 } 1402 } 1403 }); 1404 }, 1405 { 1406 threshold: 0.3, // Trigger when 30% of the element is visible 1407 rootMargin: "0px 0px 100px 0px", // Trigger a bit before the element is fully visible 1408 } 1409 ); 1410 1411 observer.observe(slideshowSection); 1412 1413 setupProgressBarNavigation(); 1414 // Keyboard navigation 1415 document.addEventListener("keydown", (e) => { 1416 if (e.key === "ArrowLeft") previousSlide(); 1417 if (e.key === "ArrowRight") nextSlide(); 1418 });
1419 // Touch support for mobile 1420 let touchStartX = 0; 1421 let touchEndX = 0; 1422 const slideshowContainer = document.querySelector(".slideshow-container"); 1423 if (slideshowContainer) { 1424 slideshowContainer.addEventListener("touchstart", (e) => { 1425 touchStartX = e.changedTouches[0].screenX; 1426 }); 1427 slideshowContainer.addEventListener("touchend", (e) => { 1428 touchEndX = e.changedTouches[0].screenX; 1429 handleSwipe(); 1430 }); 1431 } 1432 function handleSwipe() { 1433 if (touchEndX < touchStartX - 50) nextSlide(); 1434 if (touchEndX > touchStartX + 50) previousSlide(); 1435 } 1436 }); 1437})(); 1438 1439// Home Guidelines Tabs Script 1440(function () { 1441 document.addEventListener("DOMContentLoaded", function () { 1442 const tabButtons = document.querySelectorAll(".home-guidelines-tab"); 1443 const tabsWrapper = document.querySelector(".home-guidelines-tabs-wrapper"); 1444 const guidelinesPanel = tabsWrapper 1445 ? tabsWrapper.closest(".home-guidelines-panel") 1446 : null; 1447 const tabsToggle = document.querySelector(".home-guidelines-tabs-toggle"); 1448 const tabsCombobox = document.getElementById("homeguidelinestabscombobox"); 1449 const tabsToggleLabel = document.querySelector( 1450 ".home-guidelines-tabs-toggle-label" 1451 ); 1452 const tabsToggleFlag = document.querySelector( 1453 ".home-guidelines-tabs-toggle-flag" 1454 ); 1455 const detailsRegion = document.querySelector(".home-guidelines-details"); 1456 const mapImageEl = document.querySelector(".home-guidelines-map-image"); 1457 const overlayImageEl = document.querySelector(".home-guidelines-map-overlay"); 1458 const defaultMapImage = "/images/world-map.png"; 1459 const contentSections = document.querySelectorAll( 1460 ".home-guidelines-content-section" 1461 ); 1462 const overlayStyleProperties = [ 1463 "top", 1464 "right", 1465 "bottom", 1466 "left", 1467 "width", 1468 "height", 1469 "max-width", 1470 "max-height", 1471 "transform", 1472 ]; 1473 1474 let overlayFadeTimeoutId = null; 1475 const OVERLAY_FADE_DURATION_MS = 400; 1476 1477 // Map data for images and overlays only (not text content) 1478 const mapData = { 1479 wcag: { 1480 name: "WCAG 2.2", 1481 mapImage: "/images/world-map.png", 1482 }, 1483 ada: { 1484 name: "ADA Titles II & III", 1485 mapImage: "/images/world-map-ADA.png", 1486 overlayImage: "/images/ADA.png", 1487 overlayStyles: { 1488 top: "40%", 1489 left: "41%", 1490 width: "52%", 1491 transform: "translate(-50%, -50%)", 1492 }, 1493 }, 1494 section508: { 1495 name: "Section 508", 1496 mapImage: "/images/world-map-ADA.png", 1497 overlayImage: "/images/section508.png", 1498 overlayStyles: { 1499 top: "40%", 1500 left: "41%", 1501 width: "52%", 1502 transform: "translate(-50%, -50%)", 1503 }, 1504 }, 1505 aoda: { 1506 name: "AODA", 1507 mapImage: "/images/world-map-AODA.png", 1508 overlayImage: "/images/AODA.png", 1509 overlayStyles: { 1510 top: "42%",
1511 left: "49%", 1512 width: "52%", 1513 transform: "translate(-50%, -50%)", 1514 }, 1515 }, 1516 eaa: { 1517 name: "European Accessibility Act", 1518 mapImage: "/images/world-map-EAA.png", 1519 overlayImage: "/images/EAA.png", 1520 overlayStyles: { 1521 top: "38%", 1522 left: "43%", 1523 width: "52%", 1524 transform: "translate(-50%, -50%)", 1525 }, 1526 }, 1527 bitv: { 1528 name: "BITV 2.0", 1529 mapImage: "/images/world-map-BITV.png", 1530 overlayImage: "/images/BITV.png", 1531 overlayStyles: { 1532 top: "40%", 1533 left: "49%", 1534 width: "52%", 1535 transform: "translate(-50%, -50%)", 1536 }, 1537 }, 1538 agid: { 1539 name: "AGID Guidelines", 1540 mapImage: "/images/world-map-AGID.png", 1541 overlayImage: "/images/AGID.png", 1542 overlayStyles: { 1543 top: "46%", 1544 left: "55%", 1545 width: "52%", 1546 transform: "translate(-50%, -50%)", 1547 }, 1548 }, 1549 }; 1550 1551 function clearOverlayAnimations() { 1552 if (overlayFadeTimeoutId !== null) { 1553 window.clearTimeout(overlayFadeTimeoutId); 1554 overlayFadeTimeoutId = null; 1555 } 1556 } 1557 1558 function applyOverlayStyles(styles) { 1559 if (!overlayImageEl) { 1560 return; 1561 } 1562 1563 overlayStyleProperties.forEach(function (prop) { 1564 if (prop === "transform") { 1565 overlayImageEl.style.removeProperty("--overlay-transform"); 1566 overlayImageEl.style.removeProperty("transform"); 1567 } else { 1568 overlayImageEl.style.removeProperty(prop); 1569 } 1570 }); 1571 1572 if (!styles) { 1573 return; 1574 } 1575 1576 Object.entries(styles).forEach(function ([prop, value]) { 1577 if ( 1578 overlayStyleProperties.includes(prop) && 1579 typeof value === "string" && 1580 value.trim() !== "" 1581 ) { 1582 if (prop === "transform") { 1583 overlayImageEl.style.setProperty( 1584 "--overlay-transform", 1585 value.trim() 1586 ); 1587 } else { 1588 overlayImageEl.style.setProperty(prop, value.trim()); 1589 } 1590 } 1591 }); 1592 } 1593 1594 if (!tabButtons.length || !contentSections.length) { 1595 return; 1596 } 1597 1598 const order = Array.prototype.slice.call(tabButtons); 1599 const DEFAULT_DROPDOWN_BREAKPOINT = 820; 1600 const dropdownBreakpointAttr = tabsWrapper 1601 ? parseInt(tabsWrapper.getAttribute("data-dropdown-breakpoint"), 10) 1602 : NaN; 1603 const dropdownBreakpoint = Number.isNaN(dropdownBreakpointAttr) 1604 ? DEFAULT_DROPDOWN_BREAKPOINT 1605 : dropdownBreakpointAttr; 1606 const dropdownMediaQuery = 1607 typeof window.matchMedia === "function" 1608 ? window.matchMedia(`(max-width: ${dropdownBreakpoint}px)`) 1609 : null; 1610 1611 function isDropdownModeEnabled() { 1612 return ( 1613 Boolean(tabsWrapper) && 1614 tabsWrapper.getAttribute("data-tabs-mode") === "dropdown" 1615 ); 1616 } 1617 1618 const tabsList = document.querySelector(".home-guidelines-tabs-list"); 1619 1620 function updateRolesForMode(dropdownEnabled) { 1621 if (!tabsList || !tabButtons.length) { 1622 return; 1623 } 1624 1625 if (dropdownEnabled) { 1626 // Combobox mode: update roles to combobox/listbox/option 1627 if (tabsToggle) { 1628 tabsToggle.setAttribute("role", "combobox"); 1629 tabsToggle.setAttribute("aria-haspopup", "listbox"); 1630 } 1631 if (tabsList) { 1632 tabsList.setAttribute("role", "listbox"); 1633 // Hide list initially in dropdown mode 1634 if (tabsWrapper.getAttribute("data-expanded") !== "true") { 1635 tabsList.setAttribute("hidden", "true"); 1636 } 1637 } 1638 tabButtons.forEach(function (button) { 1639 button.setAttribute("role", "option"); 1640 // Keep original tab ID but use option ID for aria-activedescendant 1641 const optionId = button.getAttribute("data-option-id"); 1642 if (optionId && button.id !== optionId) { 1643 // Store original ID if not already stored 1644 if (!button.getAttribute("data-original-id")) { 1645 button.setAttribute("data-original-id", button.id); 1646 } 1647 // Use option ID for combobox mode 1648 button.id = optionId; 1649 } 1650 }); 1651 } else { 1652 // Tabs mode: update roles to tablist/tab 1653 // The combobox role is on tabsCombobox, no need to remove it from tabsToggle 1654 if (tabsList) { 1655 tabsList.removeAttribute("role"); 1656 tabsList.removeAttribute("hidden"); 1657 } 1658 tabButtons.forEach(function (button) { 1659 button.setAttribute("role", "tab"); 1660 // Restore original tab ID 1661 const originalId = button.getAttribute("data-original-id"); 1662 if (originalId) { 1663 button.id = originalId;
1664 button.removeAttribute("data-original-id"); 1665 } 1666 }); 1667 } 1668 } 1669 1670 function setTabsMode(dropdownEnabled) { 1671 if (!tabsWrapper) { 1672 return; 1673 } 1674 1675 tabsWrapper.setAttribute( 1676 "data-tabs-mode", 1677 dropdownEnabled ? "dropdown" : "tabs" 1678 ); 1679 tabsWrapper.setAttribute("data-expanded", "false"); 1680 1681 if (guidelinesPanel) { 1682 guidelinesPanel.setAttribute( 1683 "data-tabs-mode", 1684 dropdownEnabled ? "dropdown" : "tabs" 1685 ); 1686 } 1687 1688 if (tabsToggle) { 1689 if (tabsCombobox) { 1690 tabsCombobox.setAttribute("aria-expanded", "false"); 1691 } 1692 1693 if (dropdownEnabled) { 1694 tabsToggle.hidden = false; 1695 tabsToggle.removeAttribute("aria-hidden"); 1696 tabsToggle.removeAttribute("tabindex"); 1697 } else { 1698 tabsToggle.hidden = true; 1699 tabsToggle.setAttribute("aria-hidden", "true"); 1700 tabsToggle.setAttribute("tabindex", "-1"); 1701 } 1702 } 1703 1704 updateRolesForMode(dropdownEnabled); 1705 } 1706 1707 if (dropdownMediaQuery) { 1708 setTabsMode(dropdownMediaQuery.matches); 1709 1710 const handleTabsModeChange = function (event) { 1711 setTabsMode(event.matches); 1712 }; 1713 1714 if (typeof dropdownMediaQuery.addEventListener === "function") { 1715 dropdownMediaQuery.addEventListener("change", handleTabsModeChange); 1716 } else if (typeof dropdownMediaQuery.addListener === "function") { 1717 dropdownMediaQuery.addListener(handleTabsModeChange); 1718 } 1719 } else { 1720 setTabsMode(false); 1721 } 1722 1723 function getNextIndex(currentIndex, offset) { 1724 let nextIndex = currentIndex + offset; 1725 if (nextIndex < 0) { 1726 nextIndex = order.length - 1; 1727 } else if (nextIndex >= order.length) { 1728 nextIndex = 0; 1729 } 1730 return nextIndex; 1731 } 1732 1733 function setDropdownLabel(targetButton) { 1734 if (!tabsToggleLabel || !targetButton) { 1735 return; 1736 } 1737 1738 const labelEl = targetButton.querySelector(".home-guidelines-tab-label"); 1739 if (labelEl) { 1740 tabsToggleLabel.textContent = labelEl.textContent.trim(); 1741 } 1742 1743 if (!tabsToggleFlag) { 1744 return; 1745 } 1746 1747 tabsToggleFlag.innerHTML = ""; 1748 1749 const flagEl = targetButton.querySelector(".home-guidelines-tab-flag"); 1750 const iconEl = targetButton.querySelector(".home-guidelines-tab-icon"); 1751 1752 if (flagEl) { 1753 const flagClone = flagEl.cloneNode(true); 1754 tabsToggleFlag.appendChild(flagClone); 1755 } else if (iconEl) { 1756 const iconClone = iconEl.cloneNode(true); 1757 tabsToggleFlag.appendChild(iconClone); 1758 } 1759 } 1760 1761 function closeTabsDropdown() { 1762 if (!tabsWrapper || !tabsToggle || !isDropdownModeEnabled()) { 1763 return; 1764 } 1765 1766 tabsWrapper.setAttribute("data-expanded", "false"); 1767 if (tabsCombobox) { 1768 tabsCombobox.setAttribute("aria-expanded", "false"); 1769 } 1770 if (tabsList) { 1771 tabsList.setAttribute("hidden", "true"); 1772 } 1773 1774 // Reset tabindex when dropdown closes - only active option should be tabbable 1775 order.forEach(function (button) { 1776 const isActive = button.classList.contains("is-active"); 1777 button.setAttribute("tabindex", isActive ? "0" : "-1"); 1778 }); 1779 } 1780 1781 function setActiveTab(target, shouldFocus) { 1782 if (!target) { 1783 return; 1784 } 1785 1786 const key = target.getAttribute("data-guideline"); 1787 const mapInfo = mapData[key]; 1788 1789 if (!mapInfo) { 1790 return; 1791 } 1792 1793 order.forEach(function (button) { 1794 const isActive = button === target; 1795 button.classList.toggle("is-active", isActive); 1796 button.setAttribute("aria-selected", isActive ? "true" : "false"); 1797 button.setAttribute("tabindex", isActive ? "0" : "-1"); 1798 }); 1799 1800 // Update aria-activedescendant for combobox mode 1801 if (isDropdownModeEnabled() && tabsCombobox) { 1802 const optionId = target.getAttribute("data-option-id") || target.id; 1803 if (optionId) { 1804 tabsCombobox.setAttribute("aria-activedescendant", optionId); 1805 } 1806 } 1807 1808 if (shouldFocus) {
1809 target.focus(); 1810 } 1811 1812 // Show/hide content sections instead of injecting text 1813 contentSections.forEach(function (section) { 1814 const sectionKey = section.getAttribute("data-guideline-content"); 1815 if (sectionKey === key) { 1816 section.removeAttribute("hidden"); 1817 1818 // Update aria-label on details region 1819 if (detailsRegion) { 1820 detailsRegion.setAttribute( 1821 "aria-label", 1822 mapInfo.name + " overview and updates" 1823 ); 1824 } 1825 1826 // Update info row aria-label 1827 const infoRow = section.querySelector(".home-guidelines-info-row"); 1828 if (infoRow) { 1829 infoRow.setAttribute( 1830 "aria-label", 1831 mapInfo.name + " overview and latest update" 1832 ); 1833 } 1834 } else { 1835 section.setAttribute("hidden", "true"); 1836 } 1837 }); 1838 1839 // Update map image 1840 if (mapImageEl) { 1841 const nextSrc = mapInfo.mapImage || defaultMapImage; 1842 if (mapImageEl.getAttribute("src") !== nextSrc) { 1843 mapImageEl.setAttribute("src", nextSrc); 1844 } 1845 mapImageEl.setAttribute("data-guideline-map", key); 1846 } 1847 1848 // Handle overlay image 1849 if (overlayImageEl) { 1850 const overlaySrc = mapInfo.overlayImage || ""; 1851 const shouldShowOverlay = Boolean(overlaySrc); 1852 1853 if (shouldShowOverlay) { 1854 const fadeInOverlay = function () { 1855 const needsNewSrc = 1856 overlayImageEl.getAttribute("src") !== overlaySrc; 1857 1858 const startFadeIn = function () { 1859 requestAnimationFrame(function () { 1860 overlayImageEl.classList.add("is-visible"); 1861 }); 1862 }; 1863 1864 if (needsNewSrc) { 1865 const handleLoad = function () { 1866 overlayImageEl.removeEventListener("load", handleLoad); 1867 startFadeIn(); 1868 }; 1869 1870 overlayImageEl.addEventListener("load", handleLoad); 1871 overlayImageEl.setAttribute("src", overlaySrc); 1872 1873 if (overlayImageEl.complete && overlayImageEl.naturalWidth > 0) { 1874 overlayImageEl.removeEventListener("load", handleLoad); 1875 startFadeIn(); 1876 } 1877 } else { 1878 startFadeIn(); 1879 } 1880 }; 1881 1882 const prepareOverlay = function () { 1883 applyOverlayStyles(mapInfo.overlayStyles || null); 1884 overlayImageEl.removeAttribute("hidden"); 1885 overlayImageEl.setAttribute("data-guideline-overlay", key); 1886 fadeInOverlay(); 1887 }; 1888 1889 clearOverlayAnimations(); 1890 overlayImageEl.classList.remove("is-visible"); 1891 1892 if (overlayImageEl.getAttribute("data-guideline-overlay")) { 1893 overlayFadeTimeoutId = window.setTimeout(function () { 1894 overlayFadeTimeoutId = null; 1895 prepareOverlay(); 1896 }, OVERLAY_FADE_DURATION_MS); 1897 } else { 1898 prepareOverlay(); 1899 } 1900 } else { 1901 clearOverlayAnimations(); 1902 overlayImageEl.classList.remove("is-visible"); 1903 overlayImageEl.setAttribute("src", ""); 1904 applyOverlayStyles(null); 1905 overlayImageEl.setAttribute("data-guideline-overlay", ""); 1906 overlayImageEl.setAttribute("hidden", "true"); 1907 } 1908 } 1909 1910 setDropdownLabel(target); 1911 closeTabsDropdown(); 1912 } 1913 1914 let activeOptionIndex = 0; 1915 1916 function setActiveOptionIndex(index, scrollIntoView) { 1917 if (index < 0 || index >= order.length) { 1918 return; 1919 } 1920 activeOptionIndex = index; 1921 const activeOption = order[activeOptionIndex]; 1922 order.forEach(function (button) { 1923 button.classList.remove("home-guidelines-tab--active-option"); 1924 }); 1925 activeOption.classList.add("home-guidelines-tab--active-option"); 1926 if (tabsCombobox && isDropdownModeEnabled()) { 1927 const optionId = activeOption.getAttribute("data-option-id") || activeOption.id; 1928 if (optionId) { 1929 tabsCombobox.setAttribute("aria-activedescendant", optionId); 1930 } 1931 } 1932 if (scrollIntoView && activeOption) { 1933 activeOption.scrollIntoView({ block: "nearest" }); 1934 } 1935 } 1936 1937 function moveActiveOption(step) { 1938 if (!order.length) { 1939 return; 1940 } 1941 let nextIndex = activeOptionIndex + step; 1942 if (nextIndex < 0) { 1943 nextIndex = 0; 1944 } else if (nextIndex >= order.length) { 1945 nextIndex = order.length - 1; 1946 }
1947 setActiveOptionIndex(nextIndex, true); 1948 } 1949 1950 order.forEach(function (button, index) { 1951 button.addEventListener("click", function (event) { 1952 event.preventDefault(); 1953 setActiveTab(button, true); 1954 }); 1955 1956 // Update active option when focused via Tab 1957 button.addEventListener("focus", function () { 1958 if (isDropdownModeEnabled() && tabsWrapper && tabsWrapper.getAttribute("data-expanded") === "true") { 1959 activeOptionIndex = index; 1960 setActiveOptionIndex(index, false); 1961 } 1962 }); 1963 1964 button.addEventListener("keydown", function (event) { 1965 const key = event.key; 1966 const isDropdown = isDropdownModeEnabled(); 1967 const isExpanded = tabsWrapper && tabsWrapper.getAttribute("data-expanded") === "true"; 1968 1969 if (isDropdown && isExpanded) { 1970 // Combobox mode keyboard navigation (when dropdown is open) 1971 if (key === "ArrowDown" || key === "Down") { 1972 event.preventDefault(); 1973 moveActiveOption(1); 1974 order[activeOptionIndex].focus(); 1975 } else if (key === "ArrowUp" || key === "Up") { 1976 event.preventDefault(); 1977 moveActiveOption(-1); 1978 order[activeOptionIndex].focus(); 1979 } else if (key === "Home") { 1980 event.preventDefault();
1981 setActiveOptionIndex(0, true); 1982 order[0].focus(); 1983 } else if (key === "End") { 1984 event.preventDefault(); 1985 setActiveOptionIndex(order.length - 1, true); 1986 order[order.length - 1].focus(); 1987 } else if (key === "Enter" || key === " ") { 1988 event.preventDefault(); 1989 setActiveTab(button, true); 1990 } else if (key === "Escape" || key === "Esc") { 1991 event.preventDefault(); 1992 closeTabsDropdown(); 1993 if (tabsToggle) { 1994 if (tabsCombobox) { 1995 tabsCombobox.focus(); 1996 } 1997 } 1998 } 1999 // Tab key should work naturally - don't prevent default 2000 } else if (isDropdown && !isExpanded) { 2001 // If dropdown is closed but we're in dropdown mode, allow opening with Enter/Space 2002 if (key === "Enter" || key === " ") { 2003 event.preventDefault(); 2004 openTabsDropdown(); 2005 } 2006 } else { 2007 // Tabs mode keyboard navigation 2008 if (key === "ArrowRight" || key === "Right") { 2009 event.preventDefault(); 2010 const next = getNextIndex(index, 1);
2011 setActiveTab(order[next], true); 2012 } else if (key === "ArrowLeft" || key === "Left") { 2013 event.preventDefault(); 2014 const prev = getNextIndex(index, -1); 2015 setActiveTab(order[prev], true); 2016 } else if (key === "Home") { 2017 event.preventDefault(); 2018 setActiveTab(order[0], true); 2019 } else if (key === "End") { 2020 event.preventDefault(); 2021 setActiveTab(order[order.length - 1], true); 2022 } else if (key === " " || key === "Spacebar" || key === "Enter") { 2023 event.preventDefault(); 2024 setActiveTab(button, true); 2025 } 2026 } 2027 }); 2028 }); 2029 2030 const defaultActive = 2031 document.querySelector(".home-guidelines-tab.is-active") || order[0]; 2032 setActiveTab(defaultActive, false); 2033 2034 // Initialize active option index 2035 const defaultIndex = order.indexOf(defaultActive); 2036 if (defaultIndex >= 0) { 2037 activeOptionIndex = defaultIndex; 2038 if (isDropdownModeEnabled()) { 2039 setActiveOptionIndex(defaultIndex, false); 2040 } 2041 } 2042 2043 function openTabsDropdown() { 2044 if (!tabsWrapper || !tabsToggle || !isDropdownModeEnabled()) { 2045 return; 2046 } 2047 tabsWrapper.setAttribute("data-expanded", "true"); 2048 if (tabsCombobox) { 2049 tabsCombobox.setAttribute("aria-expanded", "true"); 2050 } 2051 if (tabsList) { 2052 tabsList.removeAttribute("hidden"); 2053 } 2054 2055 // Make all options tabbable when dropdown opens 2056 order.forEach(function (button) { 2057 button.setAttribute("tabindex", "0"); 2058 }); 2059 2060 const activeTab = document.querySelector(".home-guidelines-tab.is-active") || order[0]; 2061 if (activeTab) { 2062 const activeIndex = order.indexOf(activeTab); 2063 if (activeIndex >= 0) { 2064 activeOptionIndex = activeIndex;
2065 setActiveOptionIndex(activeIndex, true); 2066 activeTab.focus(); 2067 } 2068 } 2069 } 2070 2071 if (tabsToggle && tabsWrapper) { 2072 tabsToggle.addEventListener("click", function () { 2073 if (!isDropdownModeEnabled()) { 2074 return; 2075 } 2076 2077 const expanded = tabsCombobox ? tabsCombobox.getAttribute("aria-expanded") === "true" : false; 2078 if (expanded) { 2079 closeTabsDropdown(); 2080 } else { 2081 openTabsDropdown(); 2082 } 2083 }); 2084 2085 if (tabsCombobox) { 2086 tabsCombobox.addEventListener("keydown", function (event) { 2087 if (!isDropdownModeEnabled()) { 2088 return; 2089 } 2090 2091 const key = event.key; 2092 const isExpanded = tabsCombobox.getAttribute("aria-expanded") === "true"; 2093 2094 if (key === "Enter" || key === " ") { 2095 event.preventDefault(); 2096 if (isExpanded) { 2097 setActiveTab(order[activeOptionIndex], true); 2098 } else { 2099 openTabsDropdown(); 2100 } 2101 return; 2102 } 2103 2104 if (key === "Escape" || key === "Esc") { 2105 if (isExpanded) { 2106 event.preventDefault(); 2107 closeTabsDropdown(); 2108 if (tabsCombobox) { 2109 tabsCombobox.focus(); 2110 } 2111 } 2112 return; 2113 } 2114 2115 if (key === "ArrowDown" || key === "Down") { 2116 event.preventDefault(); 2117 if (!isExpanded) { 2118 openTabsDropdown(); 2119 } 2120 moveActiveOption(1); 2121 return; 2122 } 2123 2124 if (key === "ArrowUp" || key === "Up") { 2125 event.preventDefault(); 2126 if (!isExpanded) { 2127 openTabsDropdown(); 2128 } 2129 moveActiveOption(-1); 2130 return; 2131 } 2132 2133 if (key === "Home") { 2134 event.preventDefault(); 2135 if (!isExpanded) { 2136 openTabsDropdown(); 2137 }
2138 setActiveOptionIndex(0, true); 2139 return; 2140 } 2141 2142 if (key === "End") { 2143 event.preventDefault(); 2144 if (!isExpanded) { 2145 openTabsDropdown(); 2146 } 2147 setActiveOptionIndex(order.length - 1, true); 2148 return; 2149 } 2150 2151 // Tab key should work naturally - don't prevent default 2152 // When tabbing away from toggle, dropdown will close via blur/click handlers 2153 }); 2154 } 2155 2156 document.addEventListener("click", function (event) { 2157 if (!isDropdownModeEnabled() || !tabsWrapper.contains(event.target)) { 2158 closeTabsDropdown(); 2159 } 2160 }); 2161 2162 // Close dropdown when focus leaves the component 2163 tabsWrapper.addEventListener("focusout", function (event) { 2164 if (!isDropdownModeEnabled()) { 2165 return; 2166 } 2167 // Check if focus is moving to another element within the component 2168 setTimeout(function () { 2169 if (!tabsWrapper.contains(document.activeElement)) { 2170 closeTabsDropdown(); 2171 } 2172 }, 0); 2173 }); 2174 } 2175 2176 document.addEventListener("keydown", function (event) { 2177 if (!isDropdownModeEnabled()) { 2178 return; 2179 } 2180 2181 const solutionsDropdownCombobox = document.getElementById("solutionsDropdownCombobox"); 2182 if (solutionsDropdownCombobox && (solutionsDropdownCombobox.contains(event.target) || solutionsDropdownCombobox === event.target)) { 2183 return; 2184 } 2185 2186 if (event.key === "Escape" || event.key === "Esc") { 2187 closeTabsDropdown(); 2188 if (tabsCombobox) { 2189 tabsCombobox.focus(); 2190 } 2191 } 2192 }); 2193 }); 2194})(); 2195 2196// Accessibility image carousel 2197(function () { 2198 document.addEventListener("DOMContentLoaded", () => { 2199 const carousel = document.querySelector(".image-carousel"); 2200 if (!carousel) { 2201 return; 2202 } 2203 2204 const viewport = carousel.querySelector(".carousel-viewport"); 2205 const prevButton = carousel.querySelector('.carousel-nav[data-direction="prev"]'); 2206 const nextButton = carousel.querySelector('.carousel-nav[data-direction="next"]'); 2207 const cards = Array.from(carousel.querySelectorAll(".carousel-card")); 2208 let currentIndex = 0; 2209 let isAnimating = false; 2210 let animationTimeout; 2211 2212 const getVisibleCount = () => { 2213 if (window.innerWidth < 640) { 2214 return 1; 2215 } 2216 if (window.innerWidth < 1024) { 2217 return 2; 2218 } 2219 return 3; 2220 }; 2221 2222 const getCardMetrics = () => { 2223 if (!cards.length) return { cardWidth: 0, gap: 16 }; 2224 const gapVariable = getComputedStyle(document.documentElement).getPropertyValue("--carousel-gap"); 2225 const gap = Number.parseInt(gapVariable, 10) || 16; 2226 const cardWidth = cards[0].getBoundingClientRect().width; 2227 return { cardWidth, gap }; 2228 }; 2229 2230 const updateButtons = () => { 2231 if (!viewport || !prevButton || !nextButton) { 2232 return; 2233 } 2234 const visible = getVisibleCount(); 2235 const maxIndex = Math.max(0, cards.length - visible); 2236 prevButton.disabled = currentIndex <= 0; 2237 nextButton.disabled = currentIndex >= maxIndex; 2238 }; 2239 2240 const scrollCarousel = (direction) => { 2241 const { cardWidth, gap } = getCardMetrics(); 2242 if (!cardWidth || !viewport) { 2243 return; 2244 } 2245 const visible = getVisibleCount(); 2246 const maxIndex = Math.max(0, cards.length - visible); 2247 currentIndex = Math.min(Math.max(0, currentIndex + direction), maxIndex); 2248 isAnimating = true; 2249 clearTimeout(animationTimeout); 2250 const targetLeft = currentIndex * (cardWidth + gap); 2251 viewport.scrollTo({ left: targetLeft, behavior: "smooth" }); 2252 updateButtons(); 2253 animationTimeout = setTimeout(() => { 2254 isAnimating = false; 2255 }, 320); 2256 }; 2257 2258 prevButton?.addEventListener("click", () => scrollCarousel(-1)); 2259 nextButton?.addEventListener("click", () => scrollCarousel(1)); 2260 2261 // Touch/swipe support for mobile - let native scroll-snap handle snapping 2262 let scrollEndTimeout; 2263 let lastScrollLeft = viewport?.scrollLeft || 0; 2264 2265 // Detect when scrolling ends to sync the currentIndex 2266 const handleScrollEnd = () => { 2267 if (!viewport) return; 2268 const { cardWidth, gap } = getCardMetrics(); 2269 if (!cardWidth) return; 2270 2271 // Calculate the closest card index based on scroll position 2272 const newIndex = Math.round(viewport.scrollLeft / (cardWidth + gap)); 2273 const visible = getVisibleCount(); 2274 const maxIndex = Math.max(0, cards.length - visible); 2275 const clampedIndex = Math.min(Math.max(0, newIndex), maxIndex); 2276 2277 // Only update if index changed 2278 if (currentIndex !== clampedIndex) { 2279 currentIndex = clampedIndex; 2280 updateButtons(); 2281 } 2282 }; 2283 2284 viewport?.addEventListener("scroll", () => { 2285 // Clear existing timeout 2286 clearTimeout(scrollEndTimeout); 2287 2288 // Set a new timeout to detect when scrolling has stopped 2289 scrollEndTimeout = setTimeout(() => { 2290 handleScrollEnd(); 2291 }, 150); 2292 2293 // During scroll, update buttons based on current position 2294 if (!isAnimating) { 2295 const { cardWidth, gap } = getCardMetrics(); 2296 if (cardWidth) { 2297 const scrollIndex = Math.round(viewport.scrollLeft / (cardWidth + gap)); 2298 const visible = getVisibleCount(); 2299 const maxIndex = Math.max(0, cards.length - visible); 2300 const clampedIndex = Math.min(Math.max(0, scrollIndex), maxIndex); 2301 2302 // Update buttons state 2303 if (prevButton && nextButton) { 2304 prevButton.disabled = clampedIndex <= 0; 2305 nextButton.disabled = clampedIndex >= maxIndex; 2306 } 2307 } 2308 } 2309 });
2310 2311 window.addEventListener("resize", () => { 2312 // Recompute position on resize to stay aligned 2313 const { cardWidth, gap } = getCardMetrics(); 2314 const visible = getVisibleCount(); 2315 const maxIndex = Math.max(0, cards.length - visible); 2316 currentIndex = Math.min(currentIndex, maxIndex); 2317 const targetLeft = currentIndex * (cardWidth + gap); 2318 viewport?.scrollTo({ left: targetLeft }); 2319 updateButtons(); 2320 }); 2321 2322 updateButtons(); 2323 }); 2324})(); 2325 2326// Reviews carousel - Auto-scroll with columns (continuous infinite scroll) 2327(function() { 2328 document.addEventListener("DOMContentLoaded", () => { 2329 const carousel = document.querySelector(".reviews-carousel-section"); 2330 if (!carousel) { 2331 return; 2332 } 2333 2334 const viewport = carousel.querySelector(".reviews-carousel-viewport"); 2335 const track = carousel.querySelector(".reviews-carousel-track"); 2336 const columns = Array.from(carousel.querySelectorAll(".reviews-carousel-column")); 2337 2338 if (!viewport || !track || !columns.length) { 2339 return; 2340 } 2341 2342 // Collect all review cards from all columns 2343 const allCards = []; 2344 columns.forEach((column) => { 2345 const cards = Array.from(column.querySelectorAll(".reviews-carousel-card")); 2346 cards.forEach((card) => { 2347 const textEl = card.querySelector(".reviews-card-text"); 2348 const nameEl = card.querySelector(".reviews-card-name"); 2349 const text = textEl ? textEl.textContent.trim() : ""; 2350 const name = nameEl ? nameEl.textContent.trim() : ""; 2351 const key = text + "||" + name; 2352 allCards.push({ node: card, key }); 2353 }); 2354 }); 2355 2356 if (allCards.length > 1) { 2357 // Fisher-Yates shuffle algorithm 2358 const fisherYatesShuffle = (array) => { 2359 for (let i = array.length - 1; i > 0; i -= 1) { 2360 const j = Math.floor(Math.random() * (i + 1)); 2361 const temp = array[i]; 2362 array[i] = array[j]; 2363 array[j] = temp; 2364 } 2365 }; 2366 2367 // Create order where no identical reviews are adjacent 2368 const createNonAdjacentOrder = () => { 2369 const attempts = 50; 2370 const working = allCards.slice(); 2371 for (let attempt = 0; attempt < attempts; attempt += 1) { 2372 fisherYatesShuffle(working); 2373 let hasAdjacentDuplicate = false; 2374 for (let i = 1; i < working.length; i += 1) { 2375 if (working[i].key === working[i - 1].key) { 2376 hasAdjacentDuplicate = true; 2377 break; 2378 } 2379 } 2380 if (!hasAdjacentDuplicate) { 2381 return working.slice(); 2382 } 2383 } 2384 return working.slice(); 2385 }; 2386 2387 const shuffled = createNonAdjacentOrder(); 2388 2389 // Clear all existing columns 2390 columns.forEach((column) => { 2391 while (column.firstChild) { 2392 column.removeChild(column.firstChild); 2393 } 2394 }); 2395 2396 // Distribute reviews in pattern: 3, 2, 3, 2, 3, 2... 2397 // This pattern repeats: column with 3 reviews, then column with 2 reviews 2398 const pattern = [3, 2]; 2399 let currentColumnIndex = 0; 2400 let reviewsInCurrentColumn = 0; 2401 let patternIndex = 0; 2402 2403 // Function to get or create a column at the given index 2404 const getOrCreateColumn = (index) => { 2405 if (index < columns.length) { 2406 return columns[index]; 2407 } 2408 // Create a new column if needed 2409 const newColumn = document.createElement("div"); 2410 newColumn.className = "reviews-carousel-column"; 2411 track.appendChild(newColumn); 2412 columns.push(newColumn); 2413 return newColumn; 2414 }; 2415 2416 shuffled.forEach((item) => { 2417 // Get or create the current column 2418 const currentColumn = getOrCreateColumn(currentColumnIndex); 2419 2420 // Add review to current column 2421 currentColumn.appendChild(item.node); 2422 reviewsInCurrentColumn += 1; 2423 2424 // Check if we've reached the target count for current column 2425 const targetCount = pattern[patternIndex]; 2426 if (reviewsInCurrentColumn >= targetCount) { 2427 // Move to next column and next pattern value 2428 reviewsInCurrentColumn = 0; 2429 currentColumnIndex += 1;
2430 patternIndex = (patternIndex + 1) % pattern.length; 2431 } 2432 }); 2433 } 2434 2435 // Update columns array after shuffle (in case new columns were created) 2436 const finalColumns = Array.from(carousel.querySelectorAll(".reviews-carousel-column")); 2437 2438 let isPaused = false; 2439 let isDragging = false; 2440 let animationFrameId; 2441 const scrollSpeed = 0.2; // Scroll speed in pixels per frame (slow, smooth movement) 2442 let scrollAccumulator = 0; // Accumulate fractional pixels for smooth scrolling 2443 2444 let cachedColumnWidth = 0; 2445 let cachedOriginalSetWidth = 0; 2446 const gap = 24; 2447 const originalColumnCount = finalColumns.length; 2448 2449 // Clone all columns and append them to create seamless infinite scroll 2450 finalColumns.forEach((column) => { 2451 const clone = column.cloneNode(true); 2452 clone.setAttribute("aria-hidden", "true"); 2453 track.appendChild(clone); 2454 }); 2455 2456 // Drag state 2457 let dragStartX = 0; 2458 let dragStartScroll = 0; 2459 let lastDragX = 0; 2460 let hasDragged = false; 2461 const dragThreshold = 5; // Minimum pixels to move before considering it a drag 2462 2463 const updateCachedWidths = () => { 2464 if (!finalColumns.length) { 2465 cachedColumnWidth = 0; 2466 cachedOriginalSetWidth = 0; 2467 return; 2468 } 2469 const columnWidth = finalColumns[0].getBoundingClientRect().width; 2470 if (columnWidth > 0) { 2471 cachedColumnWidth = columnWidth + gap; 2472 cachedOriginalSetWidth = originalColumnCount * cachedColumnWidth; 2473 } 2474 }; 2475 2476 const scrollToNext = () => { 2477 if (!viewport) return; 2478 2479 if (isPaused || isDragging) { 2480 animationFrameId = requestAnimationFrame(scrollToNext); 2481 return; 2482 } 2483 2484 if (!cachedOriginalSetWidth || cachedOriginalSetWidth <= 0) { 2485 updateCachedWidths(); 2486 if (!cachedOriginalSetWidth || cachedOriginalSetWidth <= 0) { 2487 animationFrameId = requestAnimationFrame(scrollToNext); 2488 return; 2489 } 2490 } 2491 2492 // Accumulate scroll for smooth sub-pixel movement 2493 scrollAccumulator += scrollSpeed; 2494 const scrollDelta = Math.floor(scrollAccumulator); 2495 scrollAccumulator -= scrollDelta; 2496 2497 if (scrollDelta > 0) { 2498 const currentScroll = viewport.scrollLeft; 2499 const newScroll = currentScroll + scrollDelta; 2500 2501 // Reset to beginning when we have scrolled past the original set 2502 if (newScroll >= cachedOriginalSetWidth) { 2503 viewport.scrollLeft = newScroll - cachedOriginalSetWidth; 2504 } else { 2505 viewport.scrollLeft = newScroll; 2506 } 2507 } 2508 2509 animationFrameId = requestAnimationFrame(scrollToNext); 2510 }; 2511 2512 const startAutoScroll = () => { 2513 if (animationFrameId) { 2514 cancelAnimationFrame(animationFrameId); 2515 } 2516 animationFrameId = requestAnimationFrame(scrollToNext); 2517 }; 2518 2519 const stopAutoScroll = () => { 2520 if (animationFrameId) { 2521 cancelAnimationFrame(animationFrameId); 2522 animationFrameId = null; 2523 } 2524 }; 2525 2526 const pauseAutoScroll = () => { 2527 isPaused = true; 2528 }; 2529 2530 const resumeAutoScroll = () => { 2531 isPaused = false; 2532 // Ensure animation continues even if it was stopped 2533 if (!animationFrameId) { 2534 startAutoScroll(); 2535 } 2536 }; 2537 2538 // Only pause on focus for accessibility (keyboard navigation) 2539 viewport.addEventListener("focusin", pauseAutoScroll); 2540 viewport.addEventListener("focusout", resumeAutoScroll); 2541 2542 // Drag functionality 2543 const handleDragStart = (clientX) => { 2544 dragStartX = clientX; 2545 dragStartScroll = viewport.scrollLeft; 2546 lastDragX = clientX; 2547 hasDragged = false; 2548 }; 2549 2550 const handleDragMove = (clientX) => { 2551 if (isDragging === false && dragStartX !== 0) { 2552 const moveDistance = Math.abs(clientX - dragStartX); 2553 if (moveDistance > dragThreshold) { 2554 isDragging = true; 2555 hasDragged = true; 2556 viewport.classList.add("grabbing"); 2557 } else { 2558 return; // Not enough movement to start dragging 2559 } 2560 } 2561 2562 if (!isDragging) return; 2563 2564 const deltaX = lastDragX - clientX; // Inverted for natural drag direction 2565 let newScroll = viewport.scrollLeft + deltaX; 2566 2567 // Handle infinite scroll reset 2568 if (cachedOriginalSetWidth > 0) { 2569 if (newScroll < 0) { 2570 newScroll = cachedOriginalSetWidth + newScroll; 2571 } else if (newScroll >= cachedOriginalSetWidth) { 2572 newScroll = newScroll - cachedOriginalSetWidth; 2573 } 2574 } 2575 2576 viewport.scrollLeft = newScroll; 2577 lastDragX = clientX; 2578 }; 2579 2580 const handleDragEnd = () => { 2581 if (isDragging) { 2582 isDragging = false; 2583 viewport.classList.remove("grabbing"); 2584 } 2585 2586 // Reset drag state 2587 dragStartX = 0; 2588 dragStartScroll = 0; 2589 lastDragX = 0; 2590 hasDragged = false; 2591 2592 // Always resume auto-scroll after release 2593 isPaused = false; 2594 if (!animationFrameId) { 2595 startAutoScroll(); 2596 } 2597 }; 2598 2599 // Mouse events 2600 viewport.addEventListener("mousedown", (e) => { 2601 handleDragStart(e.clientX); 2602 });
2603 2604 document.addEventListener("mousemove", (e) => { 2605 if (dragStartX !== 0) { 2606 if (isDragging) { 2607 e.preventDefault(); 2608 } 2609 handleDragMove(e.clientX); 2610 } 2611 }); 2612 2613 document.addEventListener("mouseup", () => { 2614 handleDragEnd(); 2615 }); 2616 2617 // Touch events 2618 viewport.addEventListener("touchstart", (e) => { 2619 if (e.touches.length === 1) { 2620 handleDragStart(e.touches[0].clientX); 2621 } 2622 }, { passive: true }); 2623 2624 document.addEventListener("touchmove", (e) => { 2625 if (isDragging && e.touches.length === 1) { 2626 e.preventDefault(); 2627 handleDragMove(e.touches[0].clientX); 2628 } 2629 }, { passive: false }); 2630 2631 document.addEventListener("touchend", () => { 2632 handleDragEnd(); 2633 }); 2634 2635 const initializeCarousel = () => { 2636 updateCachedWidths(); 2637 if (cachedOriginalSetWidth > 0 && viewport.scrollWidth > viewport.clientWidth) { 2638 startAutoScroll(); 2639 } else { 2640 requestAnimationFrame(initializeCarousel); 2641 } 2642 }; 2643 2644 // Wait a bit longer for layout to be ready 2645 setTimeout(initializeCarousel, 200); 2646 2647 window.addEventListener("resize", () => { 2648 updateCachedWidths(); 2649 if (cachedOriginalSetWidth) { 2650 const currentScroll = viewport.scrollLeft; 2651 if (currentScroll >= cachedOriginalSetWidth) { 2652 viewport.scrollLeft = currentScroll - cachedOriginalSetWidth; 2653 } 2654 } 2655 }); 2656 2657 // Arrow navigation for keyboard users 2658 const prevButton = carousel.querySelector('.reviews-carousel-nav[data-direction="prev"]'); 2659 const nextButton = carousel.querySelector('.reviews-carousel-nav[data-direction="next"]'); 2660 2661 const scrollByColumn = (direction) => { 2662 if (!cachedColumnWidth) { 2663 updateCachedWidths(); 2664 } 2665 if (!cachedColumnWidth) return; 2666 2667 const scrollAmount = cachedColumnWidth; 2668 let newScroll = viewport.scrollLeft + (direction === "next" ? scrollAmount : -scrollAmount); 2669 2670 // Handle infinite scroll wrapping 2671 if (cachedOriginalSetWidth > 0) { 2672 if (newScroll < 0) { 2673 newScroll = cachedOriginalSetWidth + newScroll; 2674 } else if (newScroll >= cachedOriginalSetWidth) { 2675 newScroll = newScroll - cachedOriginalSetWidth; 2676 } 2677 } 2678 2679 viewport.scrollTo({ 2680 left: newScroll, 2681 behavior: "smooth" 2682 }); 2683 }; 2684 2685 if (prevButton) { 2686 prevButton.addEventListener("click", () => { 2687 pauseAutoScroll(); 2688 scrollByColumn("prev"); 2689 // Resume after a delay to let users see the result 2690 setTimeout(resumeAutoScroll, 3000); 2691 }); 2692 } 2693 2694 if (nextButton) { 2695 nextButton.addEventListener("click", () => { 2696 pauseAutoScroll(); 2697 scrollByColumn("next"); 2698 // Resume after a delay to let users see the result 2699 setTimeout(resumeAutoScroll, 3000); 2700 }); 2701 } 2702 }); 2703})(); 2704 2705// Solutions panel background video: slow down the loop 2706(function () { 2707 document.addEventListener("DOMContentLoaded", () => { 2708 const RATE = 0.3; 2709 document.querySelectorAll(".avz .bgvid").forEach((bgvid) => { 2710 const setRate = () => { 2711 try { 2712 bgvid.playbackRate = RATE; 2713 } catch (e) {} 2714 }; 2715 setRate(); 2716 ["loadedmetadata", "canplay", "play"].forEach((ev) => 2717 bgvid.addEventListener(ev, setRate) 2718 ); 2719 }); 2720 }); 2721})();
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.