/** * Text Scroll Fade Animation * Animates text elements word by word on scroll */ // Register GSAP plugins gsap.registerPlugin(SplitText, ScrollTrigger); // Configuration const MAINCONFIG = { CLASSES: { open: "cc-open", }, selectors: { textScrollFade: "[r-text-scroll-fade]", parallaxItem: "[r-parallax-item]", filterPageCount: "#filter-page-count", currentPageElement: '[r-filter-page-count="current"]', totalPageElement: '[r-filter-page-count="total"]', backButton: "[r-cms-back-btn]", filterMobileButton: "[r-filter-mobile-btn]", filterMobileBackdrop: "[r-filter-mobile-backdrop]", filterMobileContent: "[r-filter-mobile-content]", filterTagsContainer: "#filter-tags", filterTagElement: '[fs-list-element="tag"]', mobFilterNr: "#mob-filter-nr", galleryBtn: "[r-gallery-btn]", galleryImgSource: '[r-gallery-img="source"]', galleryImgTarget: '[r-gallery-img="target"]', galleryTextSource: '[r-gallery-text="source"]', galleryTextTarget: '[r-gallery-text="target"]', dynamicIssueTabList: '[r-dynamic-issue="tab-list"]', dynamicIssueTab: '[r-dynamic-issue="tab"]', dynamicIssueTabText: '[r-dynamic-issue="tab-text"]', dynamicIssueNr: '[r-dynamic-issue="nr"]', dynamicIssueTitle: '[r-dynamic-issue="title"]', }, animation: { textScrollFade: { startOpacity: 0.2, endOpacity: 1, duration: 0.3, ease: "power2.out", stagger: 0.05, }, parallax: { yOffset: { 1: "5%", // Less movement for value "1" 2: "10%", // More movement for value "2" 3: "25%", 4: "-5%", 5: "-10%", 6: "-25%", }, ease: "none", }, }, breakpoints: { TABLET: 991, MOBILE_LANDSCAPE: 767, MOBILE_PORTRAIT: 479, }, }; /** * Initialize text scroll fade animations */ function initTextScrollFade() { const textElements = document.querySelectorAll( MAINCONFIG.selectors.textScrollFade, ); textElements.forEach((element) => { // Split text into words using GSAP SplitText const splitText = new SplitText(element, { type: "words" }); // Set initial opacity for all words gsap.set(splitText.words, { opacity: MAINCONFIG.animation.textScrollFade.startOpacity, }); // Create scroll-triggered scrub animation gsap.to(splitText.words, { opacity: MAINCONFIG.animation.textScrollFade.endOpacity, ease: MAINCONFIG.animation.textScrollFade.ease, duration: MAINCONFIG.animation.textScrollFade.duration, stagger: MAINCONFIG.animation.textScrollFade.stagger, scrollTrigger: { trigger: element, start: "top bottom", end: "bottom center", scrub: true, }, }); }); } /** * Initialize parallax animations */ function initParallax() { const mm = gsap.matchMedia(); const parallaxElements = document.querySelectorAll( MAINCONFIG.selectors.parallaxItem, ); parallaxElements.forEach((element) => { const intensity = element.getAttribute("r-parallax-item") || "1"; const disableParallax = element.getAttribute("r-disable-parallax"); // Determine the media query BEFORE mm.add let mediaQuery = "all"; if (disableParallax === "tablet") { mediaQuery = `(min-width: ${MAINCONFIG.breakpoints.TABLET + 1}px)`; } else if (disableParallax === "landscape") { mediaQuery = `(min-width: ${MAINCONFIG.breakpoints.MOBILE_LANDSCAPE + 1}px)`; } const yOffset = MAINCONFIG.animation.parallax.yOffset[intensity] ?? MAINCONFIG.animation.parallax.yOffset[1]; mm.add(mediaQuery, () => { const tween = gsap.to(element, { yPercent: parseFloat(yOffset), // convert "10%" → 10 ease: MAINCONFIG.animation.parallax.ease, scrollTrigger: { trigger: element, start: "top bottom", end: "bottom top", scrub: true, invalidateOnRefresh: true, }, }); // ✅ cleanup on media change return () => { tween.scrollTrigger?.kill(); tween.kill(); }; }); }); } /** * Initialize pagination counter synchronization */ function initFilterPagination() { const filterPageCount = document.querySelector( MAINCONFIG.selectors.filterPageCount, ); const currentPageElement = document.querySelector( MAINCONFIG.selectors.currentPageElement, ); const totalPageElement = document.querySelector( MAINCONFIG.selectors.totalPageElement, ); if (!filterPageCount || !currentPageElement || !totalPageElement) { return; } /** * Updates custom pagination elements from filter page count */ function updateCustomPagination() { const pageText = filterPageCount.textContent.trim(); const match = pageText.match(/(\d+)\s*\/\s*(\d+)/); if (match) { const currentPage = match[1]; const totalPages = match[2]; currentPageElement.textContent = currentPage; totalPageElement.textContent = totalPages; } } // Initial update updateCustomPagination(); // Watch for changes using MutationObserver const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "childList" || mutation.type === "characterData") { updateCustomPagination(); } }); }); // Start observing observer.observe(filterPageCount, { childList: true, subtree: true, characterData: true, }); } /** * Initialize dynamic back button functionality */ function initDynamicBackButton() { const backBtn = document.querySelector(MAINCONFIG.selectors.backButton); if (!backBtn) return; const ref = document.referrer; // Check if referrer is from the same domain const fromSameSite = ref && ref.includes(window.location.hostname); if (fromSameSite) { // Override link so it behaves like "back" backBtn.addEventListener("click", (e) => { e.preventDefault(); history.back(); }); } // else: leave href untouched } /** * Initialize filter tags count monitoring */ function initFilterTagsCount() { const mobFilterNr = document.querySelector(MAINCONFIG.selectors.mobFilterNr); if (!mobFilterNr) { return; } /** * Updates mobile filter number based on checked filters */ function updateFilterCount() { // Count all checked filter checkboxes const checkedFilters = document.querySelectorAll( "input[fs-list-field]:checked", ); const filterCount = checkedFilters.length; if (filterCount === 0) { mobFilterNr.style.display = "none"; mobFilterNr.textContent = ""; } else { mobFilterNr.style.display = "block"; mobFilterNr.textContent = filterCount.toString(); } } // Initial update updateFilterCount(); // Find all filter checkboxes and add event listeners const filterCheckboxes = document.querySelectorAll("input[fs-list-field]"); filterCheckboxes.forEach((checkbox) => { checkbox.addEventListener("change", function () { updateFilterCount(); }); }); } /** * Handles mobile filter toggle functionality */ function filterMobileToggle() { // Handle both button and backdrop clicks $(document).on( "click", `${MAINCONFIG.selectors.filterMobileButton}, ${MAINCONFIG.selectors.filterMobileBackdrop}`, function (e) { e.preventDefault(); e.stopPropagation(); const btn = $(MAINCONFIG.selectors.filterMobileButton); const sidebar = $(MAINCONFIG.selectors.filterMobileContent); btn.toggleClass(MAINCONFIG.CLASSES.open); sidebar.toggleClass(MAINCONFIG.CLASSES.open); }, ); } /** * Initialize dynamic issue tab functionality * Made by Copilot */ function initDynamicIssue() { const tabList = document.querySelector( MAINCONFIG.selectors.dynamicIssueTabList, ); const nrElement = document.querySelector(MAINCONFIG.selectors.dynamicIssueNr); const titleElement = document.querySelector( MAINCONFIG.selectors.dynamicIssueTitle, ); if (!tabList || !nrElement || !titleElement) { return; } const tabs = tabList.querySelectorAll(MAINCONFIG.selectors.dynamicIssueTab); if (!tabs.length) { return; } /** * Updates the issue number and title based on clicked tab * @param {HTMLElement} clickedTab - The tab element that was clicked * @param {number} tabIndex - The index of the clicked tab (0-based) */ function updateIssueContent(clickedTab, tabIndex) { // Get the tab text element from within the clicked tab const tabTextElement = clickedTab.querySelector( MAINCONFIG.selectors.dynamicIssueTabText, ); if (!tabTextElement) return; // Format the tab index as zero-padded number with brackets (01), (02), (03), etc. const formattedNumber = `(${String(tabIndex + 1).padStart(2, "0")})`; const tabText = tabTextElement.textContent; // Update the number element nrElement.textContent = formattedNumber; // Update the title element with the tab text content titleElement.textContent = tabText; } // Add click event listeners to all tabs tabs.forEach((tab, index) => { tab.addEventListener("click", function (e) { e.preventDefault(); updateIssueContent(this, index); }); }); // Initialize with first tab on page load if (tabs[0]) { updateIssueContent(tabs[0], 0); } } /** * Initialize gallery functionality */ function initGallery() { const galleryBtns = document.querySelectorAll( MAINCONFIG.selectors.galleryBtn, ); const targetImg = document.querySelector( MAINCONFIG.selectors.galleryImgTarget, ); const targetText = document.querySelector( MAINCONFIG.selectors.galleryTextTarget, ); if (!galleryBtns.length || !targetImg || !targetText) { return; } /** * Updates the target image source, text content and active state * @param {HTMLElement} activeBtn - The button to make active */ function updateGallery(activeBtn) { // Get source image from the active button const sourceImg = activeBtn.querySelector( MAINCONFIG.selectors.galleryImgSource, ); if (!sourceImg) return; // Update target image source targetImg.src = sourceImg.src; // Copy srcset if it exists for responsive images if (sourceImg.srcset) { targetImg.srcset = sourceImg.srcset; } // Copy sizes if it exists if (sourceImg.sizes) { targetImg.sizes = sourceImg.sizes; } // Update target text content with rich HTML const sourceText = activeBtn.querySelector( MAINCONFIG.selectors.galleryTextSource, ); if (sourceText && targetText) { // Copy the rich HTML content from source to target targetText.innerHTML = sourceText.innerHTML; } // Remove active class from all buttons galleryBtns.forEach((btn) => btn.classList.remove("cc-active")); // Add active class to current button activeBtn.classList.add("cc-active"); } // Set first button as active on page load if (galleryBtns[0]) { updateGallery(galleryBtns[0]); } // Add hover and focus event listeners to all gallery buttons galleryBtns.forEach((btn) => { btn.addEventListener("mouseenter", function () { updateGallery(this); }); btn.addEventListener("focus", function () { updateGallery(this); }); }); } // Initialize when DOM is ready document.addEventListener("DOMContentLoaded", () => { initTextScrollFade(); initParallax(); initFilterPagination(); initDynamicBackButton(); filterMobileToggle(); initFilterTagsCount(); initGallery(); initDynamicIssue(); });