/*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 */