/*Disable all dev mods for complex components if the user forgot to disable them in the admin area*/ document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".dev-edite-mode").forEach((el) => { if (el.classList.contains("is-on")) { el.classList.remove("is-on"); } }); }); /*Start link click cms code */ document.addEventListener("DOMContentLoaded", function () { const clickableBlocks = document.querySelectorAll( ".card-white-blog, .grid-blog-content" ); clickableBlocks.forEach((block) => { block.addEventListener("click", function (event) { if (event.target.closest("a")) return; const link = block.querySelector(".is-cms-link"); if (link && link.href) { window.location.href = link.href; } }); }); }); /*End link click cms code */ /*Start tab code */ /* ========================================================================== 7. Interactive Time Tabs with Video & Lottie Crossfade ========================================================================== */ (function () { // Initialize all interactive grid wrappers under the given root function initInteractiveGrids(root = document) { const wrappers = root.querySelectorAll( ".interactive-grid_wrapper:not(.__inited)" ); if ( !wrappers.length || typeof gsap === "undefined" || typeof ScrollTrigger === "undefined" ) { return; } gsap.registerPlugin(ScrollTrigger); wrappers.forEach((wrapper) => { wrapper.classList.add("__inited"); setupInteractiveGrid(wrapper); }); } // Set up a single interactive grid wrapper function setupInteractiveGrid(wrapper) { const duration = parseFloat(wrapper.dataset.stepDuration) || 6; const autoMediaHeight = wrapper.dataset.mediaWrapperAuto === "true"; const fadeDuration = parseFloat(wrapper.dataset.fadeDuration) || 0.5; const animateInterContent = wrapper.dataset.animeInterContent === "true"; const tabEls = Array.from(wrapper.querySelectorAll(".interactive-tab")); if (!tabEls.length) return; const tabs = tabEls.map((el) => ({ container: el, hidden: el.querySelector(".interactive-tab_content_hidden"), visible: el.querySelector(".interactive-tab_content_visible"), content: el.querySelector(".interactive-tab_content"), bullet: el.querySelector(".bullets_active"), mediaWrapper: el.querySelector(".interactive-tab_media_wrap"), progressBar: el.querySelector(".interactive-progress"), progressWrapper: el.querySelector(".interactive-progress_wrap"), lottieEl: el.querySelector(".lottie-element"), contentInteractiveMedia: el.querySelector( ".content-in-interactive-media" ), })); let heightCache = []; let activeIndex = 0; let playTimer = null; let progressTween = null; let isAutoPlay = true; let isInViewport = false; let resizeDebounce = null; // Calculate and cache heights of hidden tab content function measureHeights() { heightCache = tabs.map((tab) => { const width = tab.hidden.getBoundingClientRect().width; gsap.set(tab.hidden, { height: "auto", width: `${width}px`, opacity: 1, position: "absolute", visibility: "hidden", }); const h = tab.hidden.scrollHeight; gsap.set(tab.hidden, { clearProps: "height,width,opacity,position,visibility", }); return h; }); } measureHeights(); // Re-measure on window resize window.addEventListener("resize", () => { clearTimeout(resizeDebounce); resizeDebounce = setTimeout(() => { measureHeights(); const active = tabs[activeIndex]; gsap.set(active.hidden, { height: "auto", opacity: 1 }); if (window.innerWidth < 991) { if (autoMediaHeight) { gsap.set(active.mediaWrapper, { height: "auto", opacity: 1 }); } else { gsap.set(active.mediaWrapper, { height: "80vw", overflow: "hidden", opacity: 1, }); } } else { gsap.set(active.mediaWrapper, { clearProps: "height", opacity: 1 }); } }, 100); }); function clearPlayTimer() { clearTimeout(playTimer); playTimer = null; } function clearProgressTween() { if (progressTween) progressTween.kill(); progressTween = null; } function startProgress() { if (!isAutoPlay || !isInViewport) return; const { progressBar, progressWrapper } = tabs[activeIndex]; clearPlayTimer(); clearProgressTween(); gsap.set(progressWrapper, { opacity: 1 }); gsap.set(progressBar, { opacity: 1, width: 0 }); progressTween = gsap.fromTo( progressBar, { width: 0 }, { width: "100%", ease: "none", duration } ); playTimer = setTimeout( () => activateTab((activeIndex + 1) % tabs.length, false), duration * 1000 ); } function stopProgress() { clearPlayTimer(); clearProgressTween(); } // Set up scroll-triggered autoplay const scrollStart = wrapper.dataset.scrollStart || "top 100%"; const scrollEnd = wrapper.dataset.scrollEnd || "bottom 0%"; ScrollTrigger.create({ trigger: wrapper, start: scrollStart, end: scrollEnd, onEnter: () => { isInViewport = true; startProgress(); }, onEnterBack: () => { isInViewport = true; startProgress(); }, onLeave: () => { isInViewport = false; stopProgress(); }, onLeaveBack: () => { isInViewport = false; stopProgress(); }, }); // Reset all tabs to their initial state function resetTabs() { tabs.forEach((tab) => { tab.container.classList.remove("is-interactive-active"); gsap.set(tab.hidden, { clearProps: "height,opacity,width" }); gsap.set(tab.mediaWrapper, { clearProps: "height,opacity" }); gsap.set(tab.progressWrapper, { clearProps: "opacity" }); gsap.set(tab.progressBar, { clearProps: "width,opacity" }); gsap.set(tab.content, { clearProps: "opacity" }); gsap.set(tab.bullet, { clearProps: "opacity" }); if (animateInterContent && tab.contentInteractiveMedia) { gsap.set(tab.contentInteractiveMedia, { clearProps: "opacity,y,zIndex,position", }); } }); } // Activate a specific tab by index function activateTab(index, userClicked) { const mobileUpStop = wrapper.dataset.mobileUpStop === "true"; activeIndex = index; resetTabs(); clearPlayTimer(); clearProgressTween(); const tab = tabs[index]; tab.container.classList.add("is-interactive-active"); gsap.to(tab.content, { opacity: 1, duration: fadeDuration, ease: "power2.out", }); gsap.to(tab.bullet, { opacity: 1, duration: fadeDuration, ease: "power2.out", }); gsap.to(tab.visible, { opacity: 1, duration: fadeDuration, ease: "power2.out", }); gsap.fromTo( tab.hidden, { height: 0, opacity: 0 }, { height: `${heightCache[index]}px`, opacity: 1, duration: 0.5, ease: "power2.out", onComplete: () => gsap.set(tab.hidden, { height: "auto" }), } ); if (window.innerWidth < 991) { if (autoMediaHeight) { const mw = tab.mediaWrapper; const w = mw.getBoundingClientRect().width; gsap.set(mw, { height: "auto", width: `${w}px`, position: "absolute", visibility: "hidden", }); const targetH = mw.scrollHeight; gsap.set(mw, { clearProps: "width,position,visibility", height: 0, overflow: "hidden", opacity: 1, }); gsap.to(mw, { height: `${targetH}px`, duration: 0.5, ease: "power2.out", onComplete: () => gsap.set(mw, { height: "auto" }), }); } else { gsap.to(tab.mediaWrapper, { height: "68vw", overflow: "hidden", opacity: 1, duration: 0.5, ease: "power2.out", }); } } else { gsap.to(tab.mediaWrapper, { opacity: 1, duration: 0.5, ease: "power2.out", }); } if ( animateInterContent && window.innerWidth >= 991 && tab.contentInteractiveMedia ) { tab.contentInteractiveMedia.style.position = "relative"; tab.contentInteractiveMedia.style.zIndex = "10"; gsap.fromTo( tab.contentInteractiveMedia, { y: 40, opacity: 0 }, { y: 0, opacity: 1, duration: 0.5, ease: "power2.out" } ); } if (userClicked) { isAutoPlay = false; gsap.set(tab.progressWrapper, { opacity: 1 }); gsap.set(tab.progressBar, { opacity: 1, width: "100%" }); } else { startProgress(); } if (userClicked && window.innerWidth < 991 && !mobileUpStop) { const header = document.querySelector("header") || document.querySelector(".navbar_component"); const headerH = header ? header.getBoundingClientRect().height : 0; const extraOff = 30; const topY = tab.container.getBoundingClientRect().top + window.pageYOffset; window.scrollTo({ top: topY - headerH - extraOff, behavior: "smooth" }); } if (tab.lottieEl?.__lottieAnim) { tab.lottieEl.__lottieAnim.goToAndPlay(0, true); } } // Attach click handlers to each tab tabs.forEach((tab, i) => { const clickArea = tab.container.querySelector( ".interactive-tab_content_wrap" ); if (clickArea) { clickArea.addEventListener("click", () => { if (activeIndex === i) return; activateTab(i, true); }); } }); // Show the first tab by default activateTab(0, false); } // Initialize on page load document.addEventListener("DOMContentLoaded", () => { initInteractiveGrids(); }); // Re-init and re-measure heights when dynamically loading new grids via .menu_tab click document.addEventListener("click", (e) => { if (e.target.closest(".menu_tab")) { setTimeout(() => { initInteractiveGrids(); if (typeof ScrollTrigger !== "undefined") { ScrollTrigger.refresh(); } // trigger a resize event so measureHeights runs for new wrappers window.dispatchEvent(new Event("resize")); }, 50); } }); })(); /*End tab code */ /*Start scroll code */ const boxes = document.querySelectorAll( ".box-wrapper-scroll, .scrolling_component" ); boxes.forEach((box) => { gsap.to(box.querySelector(".progress-line-scroll"), { height: "100%", ease: "none", scrollTrigger: { trigger: box, start: "top 50%", end: "bottom 50%", scrub: 0.7, }, }); }); /*End scroll code */ /*Start sticky code */ const applyCardTransforms = () => { if (window.innerWidth < 768) return; const cards = document.querySelectorAll(".layout_card"); const viewportHeight = window.innerHeight; cards.forEach((card, index) => { const rect = card.getBoundingClientRect(); const cardMiddle = rect.top + rect.height / 2; const distanceRatio = Math.min( Math.max( 1 - Math.abs(cardMiddle - viewportHeight / 2) / (viewportHeight / 2), 0 ), 1 ); const scale = 0.9 + distanceRatio * 0.1; const offsetY = index * 12; card.style.transform = `translateY(${offsetY}px) scale(${scale})`; }); }; /*End sticky code */ /*Start slider code */ (() => { // Guard: run only if Swiper is loaded and the slider exists on the page const hasSwiper = typeof window.Swiper !== "undefined"; const sliderEl = document.querySelector("#sticky-slider"); if (!hasSwiper || !sliderEl) return; let testSwiper = null; let isInitRunning = false; // re-entrancy guard for init/destroy // Generic debounce helper function debounce(fn, delay = 150) { let t; return function debounced(...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), delay); }; } // Helper: get controls relative to the closest wrapper (fallback to document) const getControls = () => { const block = sliderEl.closest(".block-wrapper") || document; return { prevArrow: block.querySelector("#blog-arrow-slider-prev") || null, nextArrow: block.querySelector("#blog-arrow-slider-next") || null, scrollbarEl: block.querySelector(".swiper-scrollbar") || null, paginationEl: block.querySelector(".swiper-pagination") || null, }; }; // Init/Destroy Swiper depending on viewport width function initSwiperIfNeeded() { // Prevent overlapping init/destroy calls during rapid resize if (isInitRunning) return; isInitRunning = true; try { const screenWidth = window.innerWidth; if (screenWidth < 768 && testSwiper === null) { const { prevArrow, nextArrow, scrollbarEl, paginationEl } = getControls(); testSwiper = new Swiper(sliderEl, { slidesPerView: 1, spaceBetween: 12, grabCursor: true, a11y: true, loop: false, initialSlide: 0, breakpoints: { 0: { slidesPerView: 1 }, 480: { slidesPerView: 1 }, 640: { slidesPerView: 1 }, }, // Attach modules only if elements exist navigation: prevArrow && nextArrow ? { prevEl: prevArrow, nextEl: nextArrow } : undefined, scrollbar: scrollbarEl ? { el: scrollbarEl, hide: false } : undefined, pagination: paginationEl ? { el: paginationEl, type: "progressbar" } : undefined, }); } else if (screenWidth >= 768 && testSwiper !== null) { // Cleanup on wider screens to avoid duplicate instances testSwiper.destroy(true, true); testSwiper = null; } } finally { isInitRunning = false; } } // Debounced version for resize events const debouncedInitSwiperIfNeeded = debounce(initSwiperIfNeeded, 200); // Safe listeners: only when the slider is present window.addEventListener("load", initSwiperIfNeeded, { passive: true }); window.addEventListener("resize", debouncedInitSwiperIfNeeded); // Optional: bind card transforms only if the function exists if (typeof window.applyCardTransforms === "function") { document.addEventListener("scroll", window.applyCardTransforms, { passive: true, }); window.addEventListener("load", window.applyCardTransforms, { passive: true, }); // Debounce only the resize-driven transforms (scroll remains responsive) const debouncedCardTransforms = debounce(window.applyCardTransforms, 150); window.addEventListener("resize", debouncedCardTransforms); } })(); /*End colapse Faq code */ /*Start marquee code */ function initMarquees(selector, speed) { const marquees = document.querySelectorAll(selector); if (!marquees.length) return; marquees.forEach((parent) => { const original = parent.innerHTML; // duplicate content twice for seamless loop parent.insertAdjacentHTML("beforeend", original); parent.insertAdjacentHTML("beforeend", original); let offset = 0; let paused = false; // uncomment if pause-on-hover is desired /* parent.addEventListener("mouseenter", () => { paused = true; }); parent.addEventListener("mouseleave", () => { paused = false; }); */ setInterval(() => { if (paused) return; const first = parent.firstElementChild; first.style.marginLeft = `-${offset}px`; if (offset > first.clientWidth) offset = 0; else offset += speed; }, 16); }); } document.addEventListener("DOMContentLoaded", () => { initMarquees(".marquee", 0.9); }); /*End marquee code */ /* ========================================================================== 12. Slider for Related resources START ========================================================================== */ document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".blog-swiper-wrap").forEach((sliderEl) => { const block = sliderEl.closest(".block-wrapper") || document; const prevArrow = block.querySelector("#blog-arrow-slider-prev"); const nextArrow = block.querySelector("#blog-arrow-slider-next"); const swiper = new Swiper(sliderEl, { slidesPerView: 1, spaceBetween: 20, effect: "fade", fadeEffect: { crossFade: true }, speed: 600, // Navigation only if arrows exist (prevents console errors) navigation: prevArrow && nextArrow ? { prevEl: prevArrow, nextEl: nextArrow } : undefined, breakpoints: { 992: { slidesPerView: 1, spaceBetween: 20 }, 768: { slidesPerView: 1, spaceBetween: 8 }, 0: { slidesPerView: 1, spaceBetween: 8 }, }, on: { // Ensure ARIA roles are corrected right after init and when slides change afterInit(sw) { fixA11yRoles(sw.el); }, slidesLengthChange(sw) { fixA11yRoles(sw.el); }, }, }); // Make arrow active/inactive state safe if arrows are present function updateArrowState() { if (!prevArrow || !nextArrow) return; prevArrow.classList.toggle("is-on", !swiper.isBeginning); nextArrow.classList.toggle("is-on", !swiper.isEnd); } updateArrowState(); swiper.on("slideChange", updateArrowState); swiper.on("breakpoint", updateArrowState); // --- A11y fix for Webflow CMS lists + Swiper --- function fixA11yRoles(root) { // Keep wrapper as list (Webflow sets this on .w-dyn-items / .swiper-wrapper) const wrapper = root.querySelector(".swiper-wrapper"); if (wrapper) wrapper.setAttribute("role", "list"); // Slides must be list items under a list container root.querySelectorAll(".swiper-slide").forEach((slide) => { slide.setAttribute("role", "listitem"); slide.removeAttribute("aria-roledescription"); // avoid conflicting semantics }); } }); }); /*Slider for Related resources END */ /* ========================================================================== 9. Simple Custom Tabs ========================================================================== */ document.querySelectorAll(".tab-wrapper").forEach((wrapper) => { const tabs = wrapper.querySelectorAll( ".menu_tab, .switch_tab, .tab-img_switch" ); const panels = wrapper.querySelectorAll(".content_tab"); tabs.forEach((tab, idx) => { tab.addEventListener("click", () => { tabs.forEach((t) => t.classList.remove("is-active")); tab.classList.add("is-active"); panels.forEach((p) => p.classList.remove("is-active", "visible-anime")); const target = panels[idx]; if (!target) return; target.classList.add("is-active"); // force reflow for CSS animation void target.offsetWidth; target.classList.add("visible-anime"); }); }); // optionally activate the first tab if (tabs.length) tabs[0].click(); }); /* ========================================================================== 10. Mobile Sliders Initialization & Destruction ========================================================================== */ document.addEventListener("DOMContentLoaded", () => { const BREAKPOINT = 768; const instances = new Map(); function initSliders() { document.querySelectorAll(".menu-tabs-slider").forEach((el) => { if (!instances.has(el)) { let space = parseInt(el.dataset.sliderSpace, 8); if (isNaN(space)) space = 8; const swiper = new Swiper(el, { slidesPerView: 2, spaceBetween: space, }); instances.set(el, swiper); } }); document.querySelectorAll(".winter-slider").forEach((el) => { if (!instances.has(el)) { const swiper = new Swiper(el, { slidesPerView: 2.1, spaceBetween: 8, loop: true, pagination: { el: ".swiper-bullet-wrapper.is-slider-winter", clickable: true, bulletClass: "swiper-bullet-winter", bulletActiveClass: "is_active_winter", }, }); instances.set(el, swiper); } }); document.querySelectorAll(".brand-slider").forEach((el) => { if (!instances.has(el)) { const swiper = new Swiper(el, { slidesPerView: 1.2, spaceBetween: 8, loop: true, pagination: { el: ".swiper-bullet-wrapper.is-slider-brand", clickable: true, bulletClass: "swiper-bullet-brand", bulletActiveClass: "is_active_brand", }, }); instances.set(el, swiper); } }); } function destroySliders() { instances.forEach((swiper, el) => { swiper.destroy(true, true); instances.delete(el); }); } function checkSliders() { window.innerWidth <= BREAKPOINT ? initSliders() : destroySliders(); } checkSliders(); window.addEventListener("resize", checkSliders); }); /* ========================================================================== 11. Video src dynamic for Lightbox START ========================================================================== */ (function () { window.Webflow = window.Webflow || []; window.Webflow.push(function () { initDynamicLightboxes(document); document.addEventListener("fs-cmsload", function () { initDynamicLightboxes(document); }); }); function initDynamicLightboxes(root) { const lightboxes = root.querySelectorAll(".dynamic-src"); lightboxes.forEach((lb) => { const urlEl = lb.querySelector(".video-data-url"); if (!urlEl) return; const rawUrl = (urlEl.textContent || "").trim(); if (!isUsableHttpsUrl(rawUrl)) return; // ← validate before doing anything const item = buildLightboxItem(rawUrl); if (!item) return; lb.setAttribute("href", rawUrl); let jsonScript = lb.querySelector("script.w-json"); if (!jsonScript) { jsonScript = document.createElement("script"); jsonScript.type = "application/json"; jsonScript.className = "w-json"; lb.appendChild(jsonScript); } const payload = { items: [item], group: "" }; jsonScript.textContent = JSON.stringify(payload); // ensure Webflow rebuilds lightbox payload lb.removeAttribute("data-wf-lightbox"); }); try { if (window.Webflow && Webflow.require) { const mod = Webflow.require("lightbox"); if (mod && typeof mod.ready === "function") mod.ready(); } } catch (_) {} } // === URL validation helper === function isUsableHttpsUrl(s) { // Must start with https:// if (!/^https:\/\//i.test(s)) return false; // Skip obvious placeholders in different languages const placeholderRe = /(put\s+your\s+link\s+here|your\s+link|paste\s+link|insert\s+link|встав(те|ити)?.*посиланн|сюди\s*лінк|сюди\s*посилання)/i; if (placeholderRe.test(s)) return false; // Must be a valid URL (no spaces, no javascript:, etc.) try { const u = new URL(s); // Basic sanity checks if (!u.hostname || /\s/.test(s)) return false; if (u.protocol !== "https:") return false; } catch { return false; } return true; } // === Builders & parsers === function buildLightboxItem(url) { // YouTube const yt = parseYouTube(url); if (yt) { const embed = `https://www.youtube.com/embed/${yt.id}?autoplay=1&rel=0&showinfo=0`; const html = iframeHtml(embed, 940, 528); const thumb = `https://i.ytimg.com/vi/${yt.id}/hqdefault.jpg`; return { type: "video", originalUrl: url, url, html, thumbnailUrl: thumb, width: 940, height: 528, }; } // Vimeo const vm = parseVimeo(url); if (vm) { const embed = `https://player.vimeo.com/video/${vm.id}?autoplay=1&title=0&byline=0&portrait=0`; const html = iframeHtml(embed, 940, 528); return { type: "video", originalUrl: url, url, html, width: 940, height: 528, }; } // Direct video files if (/\.(mp4|webm|ogg)(\?.*)?$/i.test(url)) { const html = ``; return { type: "video", originalUrl: url, url, html, width: 940, height: 528, }; } // Fallback: try to iframe any other https URL const html = iframeHtml(url, 940, 528); return { type: "video", originalUrl: url, url, html, width: 940, height: 528, }; } function parseYouTube(url) { const m = url.match( /(?:youtube\.com\/.*[?&]v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{6,})/i ); return m ? { id: m[1] } : null; } function parseVimeo(url) { const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/i); return m ? { id: m[1] } : null; } function iframeHtml(src, w, h) { const s = escapeHtml(src); return ``; } function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } })(); /* Video src dynamic for Lightbox END */ /* ========================================================================== 1. Lottie Playback on Visibility & Hover Control (з fallback на .lottie-data-url) ========================================================================== */ (function () { if (!window.bodymovin) { console.warn("[Lottie] bodymovin not found."); return; } const origLoad = bodymovin.loadAnimation; bodymovin.loadAnimation = function (config) { const anim = origLoad(config); if (config.container) config.container.__lottieAnim = anim; return anim; }; })(); function getLottiePath(element) { // 1) proportion for .lottie-data-url const inlineEl = element.querySelector(".lottie-data-url"); const inlineUrl = inlineEl && inlineEl.textContent ? inlineEl.textContent.trim() : ""; if (inlineUrl) { inlineEl.style.display = "none"; return inlineUrl; } const attrUrl = (element.getAttribute("data-lottie-src") || "").trim(); return attrUrl; } function initLottie(element) { const path = getLottiePath(element); if (!path) { console.warn("[Lottie] no source:", element); return; } const playOnHover = element.hasAttribute("data-play-hover"); const loopLottie = element.hasAttribute("data-lottie-loop"); const rendererType = element.getAttribute("data-lottie-renderer") || "svg"; const animationConfig = { container: element, renderer: rendererType, path: path, rendererSettings: { preserveAspectRatio: "xMidYMid slice", }, loop: !playOnHover && loopLottie, autoplay: !playOnHover, }; const anim = bodymovin.loadAnimation(animationConfig); if (playOnHover) { const parentWrapper = element.closest(".lottie-wrapper-hover") || element; anim.setDirection(1); parentWrapper.addEventListener("mouseenter", () => { anim.setDirection(1); anim.play(); }); parentWrapper.addEventListener("mouseleave", () => { anim.setDirection(-1); anim.play(); }); } } document.addEventListener("DOMContentLoaded", function () { const observer = new IntersectionObserver( (entries, obs) => { entries.forEach((entry) => { if (entry.isIntersecting) { if (!entry.target.__lottieAnim) { initLottie(entry.target); } obs.unobserve(entry.target); } }); }, { rootMargin: "0px 0px 200px 0px", threshold: 0.1 } ); const candidates = new Set(); document .querySelectorAll(".lottie-element") .forEach((el) => candidates.add(el)); document .querySelectorAll("[data-lottie-src]") .forEach((el) => candidates.add(el)); const lottieElements = Array.from(candidates); lottieElements.forEach((el) => { el.style.position = "relative"; el.style.width = "100%"; el.style.height = "100%"; el.style.overflow = "hidden"; if (el.hasAttribute("data-no-wait")) { if (!el.__lottieAnim) initLottie(el); } else { observer.observe(el); } }); }); /* Play lottie when visible and control hover END */ /* ========================================================================== 7. Filters accordion custom START ========================================================================== */ $(function () { function initFiltersAccordion() { var $groups = $(".filters_filter-group"); var $headings = $groups.find(".filters_filter-group-heading"); $headings.off(".accordion"); if ($(window).width() < 991) { $groups.removeClass("is-active").find(".flex-filtres-left").hide(); $headings.on("click.accordion", function () { var $group = $(this).closest(".filters_filter-group"); var $content = $group.find(".flex-filtres-left"); if ($group.hasClass("is-active")) { $content.slideUp(200); $group.removeClass("is-active"); } else { $groups .filter(".is-active") .removeClass("is-active") .find(".flex-filtres-left") .slideUp(200); $group.addClass("is-active"); $content.slideDown(200); } }); } else { $groups.each(function () { var $g = $(this); var $c = $g.find(".flex-filtres-left"); if ($g.hasClass("is-active")) { $c.show(); } else { $c.hide(); } }); $headings.on("click.accordion", function () { var $group = $(this).closest(".filters_filter-group"); var $content = $group.find(".flex-filtres-left"); if ($group.hasClass("is-active")) { $group.removeClass("is-active"); $content.slideUp(200); } else { $group.addClass("is-active"); $content.slideDown(200); } }); } } initFiltersAccordion(); var resizeTimer; $(window).on("resize", function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(initFiltersAccordion, 100); }); }); /* Filters accordion custom END */ /*Filters open close on tablet */ $(function () { function initFilterToggle() { var $wrapper = $(".filters_lists-wrapper"); var $openBtn = $("[data-filters-open]"); var $closeBtn = $("[data-filters-close]"); if (!$wrapper.length) return; $openBtn.off("click.filterToggle"); $closeBtn.off("click.filterToggle"); if ($(window).width() < 991) { $wrapper.hide().removeClass("is-active"); $openBtn.on("click.filterToggle", function () { if ($wrapper.hasClass("is-active")) { $wrapper.removeClass("is-active").slideUp(200); } else { $wrapper.addClass("is-active").slideDown(200); } }); $closeBtn.on("click.filterToggle", function () { if ($wrapper.hasClass("is-active")) { $wrapper.removeClass("is-active").slideUp(200); } }); } else { $wrapper.show().removeClass("is-active"); } } initFilterToggle(); var resizeTimer; $(window).on("resize", function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(initFilterToggle, 100); }); }); /*Filters accordion custom END */ /* 18. Auto-Open First FAQ Item on Load document.addEventListener("DOMContentLoaded", () => { const lists = document.querySelectorAll(".faq_list"); if (!lists.length) return; lists.forEach((list) => { const firstQ = list.querySelector(".faq_question, .faq-question"); if (firstQ) firstQ.click(); }); }); */ /* Pagination hide state Start */ (function () { // Single target: control ONLY this element const PAGINATION_SELECTOR = ".pagination"; const COUNT_SELECTOR = ".w-page-count"; let countObserver = null; let outerObserver = null; // Normalize text like "1 / 1" (handle NBSP and multiple spaces) function normalize(text) { return (text || "") .replace(/\u00A0/g, " ") // non-breaking space -> normal space .replace(/\s+/g, " ") // collapse multiple spaces .trim(); } // Read "1 / 1" style or aria-label="Page 1 of 1" and return total pages function readTotalPages(paginationEl) { const countEl = paginationEl.querySelector(COUNT_SELECTOR); if (!countEl) return null; // Prefer visible text content const text = normalize(countEl.textContent); const m = text.match(/^(\d+)\s*\/\s*(\d+)$/); if (m) { const total = parseInt(m[2], 10); if (!Number.isNaN(total)) return total; } // Fallback to aria-label e.g. "Page 1 of 1" const aria = countEl.getAttribute("aria-label") || ""; const ariaMatch = aria.match(/of\s+(\d+)/i); if (ariaMatch) { const total = parseInt(ariaMatch[1], 10); if (!Number.isNaN(total)) return total; } return null; } // Show/hide ONLY the .pagination element function updateVisibility(paginationEl) { const total = readTotalPages(paginationEl); // Be conservative if we can't parse -> show if (total === null) { paginationEl.removeAttribute("data-hidden"); return; } if (total <= 1) { paginationEl.setAttribute("data-hidden", "true"); } else { paginationEl.removeAttribute("data-hidden"); } } // Observe text changes inside .w-page-count function observeCount(paginationEl) { const countEl = paginationEl.querySelector(COUNT_SELECTOR); if (!countEl) return; // Initial check updateVisibility(paginationEl); // Reconnect observer if (countObserver) countObserver.disconnect(); countObserver = new MutationObserver(() => { // Defer to next tick to let DOM settle setTimeout(() => updateVisibility(paginationEl), 0); }); countObserver.observe(countEl, { subtree: true, childList: true, characterData: true, }); } // Attach to current .pagination (if re-rendered, we'll re-attach) function attach() { const paginationEl = document.querySelector(PAGINATION_SELECTOR); if (!paginationEl) return; observeCount(paginationEl); } // Run on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", attach, { once: true }); } else { attach(); } // Watch for .pagination node being replaced/re-rendered and re-attach if (outerObserver) outerObserver.disconnect(); outerObserver = new MutationObserver(() => { const paginationEl = document.querySelector(PAGINATION_SELECTOR); if (!paginationEl) return; observeCount(paginationEl); }); outerObserver.observe(document.body, { childList: true, subtree: true }); // Optional: if using Finsweet filters/loads, re-attach after their events const reattach = () => setTimeout(attach, 0); window.addEventListener("fs-cmsfilter-update", reattach); window.addEventListener("fs-cmsfilter-reset", reattach); window.addEventListener("fs-cmsfilter-change", reattach); window.addEventListener("fs-cmsload", reattach); })(); /* pagination hide state End */ /* ========================================================================== 11. Scrolling Component with Sticky Media & Lottie Reset ========================================================================== */ document.addEventListener("DOMContentLoaded", () => { const mm = gsap.matchMedia(); mm.add("(min-width: 992px)", () => { document.querySelectorAll(".scrolling_component").forEach((comp) => { comp.querySelectorAll(".scrolling_content-and-media").forEach((sec) => { ScrollTrigger.create({ trigger: sec, start: "top 50%", end: "bottom 50%", onEnter: () => activateSection(sec), onEnterBack: () => activateSection(sec), }); }); }); function activateSection(sec) { const parent = sec.closest(".scrolling_component"); parent .querySelectorAll(".scrolling_content-and-media") .forEach((s) => s.classList.remove("is-active-scrolling")); sec.classList.add("is-active-scrolling"); const lottie = sec.querySelector(".lottie-element"); if (lottie?.__lottieAnim) { lottie.__lottieAnim.goToAndPlay(0, true); } } return () => ScrollTrigger.getAll().forEach((t) => t.kill()); }); }); /*new scroll block component START */ // Collect content/media pairs const pairs = []; gsap.utils.toArray(".scrolling_content-and-media-new").forEach((wrapper) => { const content = wrapper.querySelector(".scrolling_content-box-new"); const media = wrapper.querySelector(".scrolling_media_wrap-new"); if (content && media) pairs.push({ wrapper, content, media }); }); // Fallback: content + next sibling as media if (pairs.length === 0) { gsap.utils.toArray(".scrolling_content-box-new").forEach((box) => { const next = box.nextElementSibling; if (next && next.matches(".scrolling_media_wrap-new")) { const wrapper = box.closest(".scrolling_content-and-media-new") || box.parentElement; pairs.push({ wrapper, content: box, media: next }); } }); } // Scroll segment proportions (of the whole path) const D1 = 20, D2 = 40, D3 = 30; // 0→1, plateau 1→1, 1→0 const TOTAL = D1 + D2 + D3; // Active zone bounds for media (aligned with D2 plateau) // For 50/30/20: "top 50%" → "top 20%" const ACTIVE_START = `top 50%`; // "top 50%" const ACTIVE_END = `top -11%`; // "top 20%" // Helper: vertical lift offset for media (can be set on media or content via data-media-offset="60px"/"3rem") const getMediaOffset = (contentEl, mediaEl) => mediaEl.getAttribute("data-media-offset") || contentEl.getAttribute("data-media-offset") || "3rem"; // Initial states pairs.forEach(({ content, media }) => { const offset = getMediaOffset(content, media); gsap.set(content, { opacity: 0 }); gsap.set(media, { opacity: 0, y: offset }); }); // Build triggers for a desktop pair function setupPairDesktop({ content, media }) { const offset = getMediaOffset(content, media); // Single scrubbed timeline for content const tl = gsap.timeline({ defaults: { ease: "none", overwrite: "auto" }, scrollTrigger: { trigger: content, start: "top 50%", // entering end: "top 0%", // leaving above scrub: true, invalidateOnRefresh: true, // markers: true }, }); // 0 → 1 tl.fromTo(content, { opacity: 0 }, { opacity: 1, duration: D1 }); // Plateau 1 → 1, then fade out tl.to(content, { opacity: 1, duration: D2 }).to(content, { opacity: 0, duration: D3, }); // Media: appear + rise from below within the active zone, then hide ScrollTrigger.create({ trigger: content, start: ACTIVE_START, end: ACTIVE_END, invalidateOnRefresh: true, // markers: true, onEnter: () => gsap.fromTo( media, { opacity: 0, y: offset }, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out", overwrite: "auto", } ), onEnterBack: () => gsap.fromTo( media, { opacity: 0, y: offset }, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out", overwrite: "auto", } ), onLeave: () => gsap.to(media, { opacity: 0, y: offset, duration: 0.3, ease: "power2.out", overwrite: "auto", }), onLeaveBack: () => gsap.to(media, { opacity: 0, y: offset, duration: 0.3, ease: "power2.out", overwrite: "auto", }), }); } // Build triggers for a mobile/tablet pair (< 992px): // simple one-time reveal of BOTH content and media together (from bottom + opacity) function setupPairMobile({ wrapper, content, media }) { const offset = getMediaOffset(content, media); // Ensure initial states (in case of resize/matchMedia re-run) gsap.set([content, media], { opacity: 0 }); gsap.set(media, { y: offset }); gsap .timeline({ defaults: { ease: "power2.out", overwrite: "auto" }, scrollTrigger: { trigger: wrapper || content, start: "top 50%", // one-time play, no scrub, no reverse on scroll up toggleActions: "play none none none", once: true, invalidateOnRefresh: true, // markers: true, }, }) // Reveal both at the same time .fromTo( [content, media], { opacity: 0, y: (i, el) => (el === media ? offset : "3rem") }, { opacity: 1, y: 0, duration: 0.8, stagger: 0 } // simultaneous ); } // Responsive via matchMedia const mm = gsap.matchMedia(); // Desktop (≥ 992px): keep fade-out after plateau (unchanged behavior) mm.add("(min-width: 992px)", () => { pairs.forEach((pair) => setupPairDesktop(pair)); }); // Mobile/Tablet (< 992px): one-time reveal of the whole pair from bottom with opacity mm.add("(max-width: 991px)", () => { pairs.forEach((pair) => setupPairMobile(pair)); }); // Recalculate positions after load/resize window.addEventListener("load", () => ScrollTrigger.refresh()); /*new scroll block component END */ const boxesnew = document.querySelectorAll(".scrolling_component-new"); boxesnew.forEach((box) => { gsap.to(box.querySelector(".progress-line-scroll"), { height: "100%", ease: "none", scrollTrigger: { trigger: box, start: "top 50%", end: "bottom 50%", scrub: true, }, }); }); /*End scroll code */