// Configuration const CONFIG = { // Lenis smooth scroll settings lenis: { lerp: 0.1, wheelMultiplier: 0.7, gestureOrientation: "vertical", normalizeWheel: false, smoothTouch: false, }, // Animation settings animation: { duration: 0.8, staggerAmount: 0.2, lettersFadeDuration: 0.6, lettersFadeStagger: 0.8, navHoverDuration: 0.5, navHoverStagger: 0.03, navResetDuration: 0.4, modalDuration: 0.2, modalYOffset: "2em", }, // Breakpoints breakpoints: { tablet: 991, }, // Color arrays brandColors: [ "var(--color--blauw)", "var(--color--paars)", "var(--color--geel)", "var(--color--oranje)", "var(--color--magenta)", ], navBGColors: [ "var(--color--blauw)", "var(--color--paars)", "var(--color--oranje)", "var(--color--magenta)", ], // Color class mapping colorClassMap: { blue: "is--blue", orange: "is--orange", purple: "is--purple", yellow: "is--yellow", }, // Selectors selectors: { ajaxModal: { lightbox: "[tr-ajaxmodal-element='lightbox']", lightboxClose: "[tr-ajaxmodal-element='lightbox-close']", lightboxModal: "[tr-ajaxmodal-element='lightbox-modal']", cmsLink: "[tr-ajaxmodal-element='cms-link']", cmsPageContent: "[tr-ajaxmodal-element='cms-page-content']", }, animate: "[animate], [animateheadline], [stagger-link]", animateHeadline: "[animateheadline]", staggerLink: "[stagger-link]", staggerLinkText: "[stagger-link-text]", lettersFadeIn: "[letters-fade-in]", navMenuLink: ".nav-menu-link", navMenuButton: ".nav-menu-button", navMenuOverlay: ".nav-menu-overlay", navMenuLinkArrow: ".nav-menu-link-arrow", navMenuBottom: ".nav-menu-bottom", navMenuTabButton: ".nav-menu-tab-button", navMenuBottomBack: ".nav-menu-bottom-back", navMenuBottomOverlay: ".nav-menu-bottom-overlay", navMenuBottomTabHeader: ".nav-menu-bottom-tab-header", cmsFilterCheckbox: ".cms-filter-checkbox-wrap", cmsArticleModal: ".cms-article-modal", copyrightYear: ".copyright-year", skipLink: "#skip-link", mainContent: ".main-content", scrollDisable: "[scroll=disable]", scrollEnable: "[scroll=enable]", scrollBoth: "[scroll=both]", body: "body", }, // Timing timing: { navLinkDelay: 400, copyStatusDelay: 1200, }, }; // Utility functions const utils = { /** * Check if we're in Webflow editor */ isWebflowEditor() { return Webflow.env("editor") !== undefined; }, /** * Check if window width is above tablet breakpoint */ isDesktop() { return window.innerWidth > CONFIG.breakpoints.tablet; }, /** * Rotate array elements (move first to end) */ rotateArray(arr) { const first = arr.shift(); arr.push(first); return first; }, /** * Debounce function for performance */ debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, /** * Vanilla JS equivalent of jQuery's $(selector) */ $(selector) { return document.querySelector(selector); }, /** * Vanilla JS equivalent of jQuery's $(selector) for multiple elements */ $$(selector) { return document.querySelectorAll(selector); }, /** * Add event listener with multiple event types */ addEventListeners(element, events, handler) { events.split(' ').forEach(event => { element.addEventListener(event, handler); }); }, /** * Toggle class on element */ toggleClass(element, className) { element.classList.toggle(className); }, /** * Add class to element */ addClass(element, className) { element.classList.add(className); }, /** * Remove class from element */ removeClass(element, className) { element.classList.remove(className); }, /** * Check if element has class */ hasClass(element, className) { return element.classList.contains(className); }, /** * Set CSS property on element */ setStyle(element, property, value) { element.style[property] = value; }, /** * Get CSS property from element */ getStyle(element, property) { return window.getComputedStyle(element)[property]; }, /** * Set attribute on element */ setAttr(element, attribute, value) { element.setAttribute(attribute, value); }, /** * Get attribute from element */ getAttr(element, attribute) { return element.getAttribute(attribute); }, /** * Create element with attributes */ createElement(tag, attributes = {}) { const element = document.createElement(tag); Object.entries(attributes).forEach(([key, value]) => { if (key === 'textContent') { element.textContent = value; } else { utils.setAttr(element, key, value); } }); return element; }, /** * Parse HTML string and return element */ parseHTML(html) { const parser = new DOMParser(); return parser.parseFromString(html, 'text/html'); }, /** * Find element within another element */ find(element, selector) { return element.querySelector(selector); }, /** * Find all elements within another element */ findAll(element, selector) { return element.querySelectorAll(selector); }, /** * Check if element matches selector */ matches(element, selector) { return element.matches(selector); }, /** * Check if element is descendant of another element */ isDescendant(parent, child) { return parent.contains(child); }, }; // Lenis smooth scroll initialization class SmoothScroll { constructor() { if (utils.isWebflowEditor()) return; this.lenis = new Lenis(CONFIG.lenis); this.init(); } init() { const raf = (time) => { this.lenis.raf(time); requestAnimationFrame(raf); }; requestAnimationFrame(raf); this.bindEvents(); } bindEvents() { utils.$$("[data-lenis-start]").forEach(element => { element.addEventListener("click", () => this.lenis.start()); }); utils.$$("[data-lenis-stop]").forEach(element => { element.addEventListener("click", () => this.lenis.stop()); }); utils.$$("[data-lenis-toggle]").forEach(element => { element.addEventListener("click", (e) => { utils.toggleClass(e.currentTarget, "stop-scroll"); utils.hasClass(e.currentTarget, "stop-scroll") ? this.lenis.stop() : this.lenis.start(); }); }); } } // Copyright year update class CopyrightYear { static init() { Webflow.push(() => { const copyrightElement = utils.$(CONFIG.selectors.copyrightYear); if (copyrightElement) { copyrightElement.textContent = new Date().getFullYear(); } }); } } // Accessibility skip link class SkipLink { static init() { document.addEventListener("DOMContentLoaded", () => { const skipLink = utils.$(CONFIG.selectors.skipLink); if (!skipLink) return; utils.addEventListeners(skipLink, "click keydown", (e) => { if (e.type === "keydown" && e.which !== 13) return; e.preventDefault(); const target = utils.$(CONFIG.selectors.mainContent); if (target) { utils.setAttr(target, "tabindex", "-1"); target.focus(); } }); }); } } // Scroll control class ScrollControl { static init() { Webflow.push(() => { const body = document.body; utils.$$(CONFIG.selectors.scrollDisable).forEach(element => { element.addEventListener("click", () => { utils.setStyle(body, "overflow", "hidden"); }); }); utils.$$(CONFIG.selectors.scrollEnable).forEach(element => { element.addEventListener("click", () => { if (utils.getStyle(body, "overflow") !== "hidden") { window.scrollPosition = window.pageYOffset; } utils.setStyle(body, "overflow", ""); }); }); utils.$$(CONFIG.selectors.scrollBoth).forEach(element => { element.addEventListener("click", () => { utils.setStyle(body, "overflow", utils.getStyle(body, "overflow") !== "hidden" ? "hidden" : ""); }); }); }); } } // Navigation menu link delay class NavMenuLinks { static init() { utils.$$(CONFIG.selectors.navMenuLink).forEach(element => { element.addEventListener("click", function(e) { e.preventDefault(); setTimeout(() => { window.location = this.href; }, CONFIG.timing.navLinkDelay); }); }); } } // Random rotation on hover class RotateButtons { static init() { const rotateButtons = utils.$$(CONFIG.selectors.cmsFilterCheckbox); rotateButtons.forEach(button => { button.addEventListener("mouseover", () => { if (!utils.isDesktop()) return; const randomRotation = Math.random() * 24 - 12; utils.setStyle(button, "transform", `rotate(${randomRotation}deg)`); }); button.addEventListener("mouseout", () => { utils.setStyle(button, "transform", "rotate(0deg)"); }); }); } } // AJAX modal functionality class AjaxModal { constructor() { this.lightbox = utils.$(CONFIG.selectors.ajaxModal.lightbox); this.lightboxClose = utils.$(CONFIG.selectors.ajaxModal.lightboxClose); this.lightboxModal = utils.$(CONFIG.selectors.ajaxModal.lightboxModal); this.initialPageTitle = document.title; this.initialPageUrl = window.location.href; this.focusedLink = null; if (!this.lightbox || !this.lightboxClose || !this.lightboxModal) return; this.init(); } init() { this.setupAccessibility(); this.createTimeline(); this.bindEvents(); } setupAccessibility() { utils.setAttr(this.lightboxClose, "aria-label", "Close Modal"); } createTimeline() { this.timeline = gsap.timeline({ paused: true, onReverseComplete: () => { this.focusedLink?.focus(); this.updatePageInfo(this.initialPageTitle, this.initialPageUrl); }, onComplete: () => { this.lightboxClose.focus(); }, }); this.timeline .set("body", { overflow: "hidden" }) .set(this.lightbox, { display: "block", onComplete: () => this.lightboxModal.scrollTop = 0, }) .from(this.lightbox, { opacity: 0, duration: CONFIG.animation.modalDuration }) .from(this.lightboxModal, { y: CONFIG.animation.modalYOffset, duration: CONFIG.animation.modalDuration * 2 }, "<"); } bindEvents() { document.addEventListener("click", (e) => { // Check if the clicked element or any of its parents matches the selector let target = e.target; let isCmsLink = false; while (target && target !== document) { if (utils.matches(target, CONFIG.selectors.ajaxModal.cmsLink)) { isCmsLink = true; break; } target = target.parentElement; } if (isCmsLink) { this.handleCmsLinkClick(e, target); } }); this.lightboxClose.addEventListener("click", () => this.timeline.reverse()); document.addEventListener("keydown", (e) => { if (e.key === "Escape") this.timeline.reverse(); }); this.lightbox.addEventListener("click", (e) => { // Close modal when clicking on the lightbox background (not on modal content) if (e.target === this.lightbox) { this.timeline.reverse(); } }); } handleCmsLinkClick(e, linkElement) { this.focusedLink = linkElement; this.initialPageUrl = window.location.href; e.preventDefault(); const linkUrl = utils.getAttr(linkElement, "href"); fetch(linkUrl) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const cmsContent = doc.querySelector(CONFIG.selectors.ajaxModal.cmsPageContent); const cmsTitle = doc.querySelector("title")?.textContent || document.title; const cmsUrl = window.location.origin + linkUrl; this.updatePageInfo(cmsTitle, cmsUrl); this.lightboxModal.innerHTML = ""; if (cmsContent) { this.lightboxModal.appendChild(cmsContent.cloneNode(true)); } this.timeline.play(); this.keepFocusWithinLightbox(); this.lightboxReady(); }) .catch(error => { console.error("Error loading modal content:", error); }); } updatePageInfo(newTitle, newUrl) { document.title = newTitle; window.history.replaceState({}, "", newUrl); } keepFocusWithinLightbox() { const focusableElements = this.lightbox.querySelectorAll( "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])" ); const lastFocusableChild = Array.from(focusableElements) .filter(el => !el.disabled && !utils.getAttr(el, "aria-hidden")) .pop(); if (lastFocusableChild) { lastFocusableChild.addEventListener("focusout", () => { this.lightboxClose.focus(); }); } } lightboxReady() { Webflow.ready(); Webflow.require("ix2").init(); ShareArticle.init(); } } // Dynamic background color for AJAX modals class DynamicBackgroundColor { static init() { document.addEventListener("click", (e) => { // Check if the clicked element or any of its parents matches the selector let target = e.target; let isCmsLink = false; while (target && target !== document) { if (utils.matches(target, CONFIG.selectors.ajaxModal.cmsLink)) { isCmsLink = true; break; } target = target.parentElement; } if (!isCmsLink) return; const colorValue = utils.getAttr(target, "color"); const colorClass = CONFIG.colorClassMap[colorValue]; if (!colorClass) return; const modal = utils.$(CONFIG.selectors.cmsArticleModal); if (!modal) return; Object.values(CONFIG.colorClassMap).forEach(cls => { utils.removeClass(modal, cls); }); utils.addClass(modal, colorClass); }); } } // Highlight color switching class HighlightColor { static init() { window.addEventListener("mousedown", () => { const highlightColor = utils.rotateArray(CONFIG.brandColors); document.documentElement.style.setProperty("--highlight-color", highlightColor); }); } } // Menu background color switching class MenuBackgroundColor { static init() { utils.$$(CONFIG.selectors.navMenuButton).forEach(element => { element.addEventListener("click", function(e) { const clicks = this.dataset.clicks === "true"; if (clicks) { if (e.which === 27) { // Handle escape key if needed return; } } else { const navColorChange = utils.rotateArray(CONFIG.navBGColors); const overlay = utils.$(CONFIG.selectors.navMenuOverlay); const arrows = utils.$$(CONFIG.selectors.navMenuLinkArrow); if (overlay) utils.setStyle(overlay, "background", navColorChange); arrows.forEach(arrow => { utils.setStyle(arrow, "color", navColorChange); }); } this.dataset.clicks = !clicks; // Reset Bottom Nav const backButton = utils.$(CONFIG.selectors.navMenuBottomBack); if (backButton) backButton.click(); }); }); } } // GSAP text animations class TextAnimations { static init() { window.addEventListener("DOMContentLoaded", () => { this.splitText(); this.setupAnimations(); this.setupNavHoverEffects(); this.preventFlash(); }); } static splitText() { new SplitType(CONFIG.selectors.animate, { types: "lines, words, chars", tagName: "span", }); } static createScrollTrigger(triggerElement, timeline) { ScrollTrigger.create({ trigger: triggerElement, start: "top bottom", onLeaveBack: () => { timeline.progress(0); timeline.pause(); }, }); ScrollTrigger.create({ trigger: triggerElement, start: "top 90%", onEnter: () => timeline.play(), }); } static setupAnimations() { utils.$$(CONFIG.selectors.animate).forEach(element => { const timeline = gsap.timeline({ paused: true }); timeline.from(utils.findAll(element, ".word"), { yPercent: 100, duration: CONFIG.animation.duration, ease: "power1.out", stagger: { amount: CONFIG.animation.staggerAmount }, }); TextAnimations.createScrollTrigger(element, timeline); }); utils.$$(CONFIG.selectors.animateHeadline).forEach(element => { const timeline = gsap.timeline({ paused: true }); timeline.from(utils.findAll(element, ".char"), { yPercent: 100, duration: CONFIG.animation.duration, ease: "power1.out", stagger: { amount: CONFIG.animation.staggerAmount }, }); TextAnimations.createScrollTrigger(element, timeline); }); utils.$$(CONFIG.selectors.lettersFadeIn).forEach(element => { const timeline = gsap.timeline({ paused: true }); timeline.from(utils.findAll(element, ".char"), { opacity: 0, duration: CONFIG.animation.lettersFadeDuration, ease: "power1.out", stagger: { amount: CONFIG.animation.lettersFadeStagger }, }); TextAnimations.createScrollTrigger(element, timeline); }); } static setupNavHoverEffects() { const staggerLinks = utils.$$(CONFIG.selectors.staggerLink); staggerLinks.forEach(link => { const letters = utils.findAll(link, `${CONFIG.selectors.staggerLinkText} .char`); link.addEventListener("mouseenter", () => { gsap.to(letters, { yPercent: -100, duration: CONFIG.animation.navHoverDuration, ease: "power4.inOut", stagger: { each: CONFIG.animation.navHoverStagger, from: "start" }, overwrite: true, }); }); link.addEventListener("mouseleave", () => { gsap.to(letters, { yPercent: 0, duration: CONFIG.animation.navResetDuration, ease: "power4.inOut", stagger: { each: CONFIG.animation.navHoverStagger, from: "start" }, }); }); }); } static preventFlash() { gsap.set(`${CONFIG.selectors.animate}, ${CONFIG.selectors.animateHeadline}, ${CONFIG.selectors.lettersFadeIn}`, { opacity: 1 }); } } // Navigation bottom menu class NavBottomMenu { static init() { utils.$$(CONFIG.selectors.navMenuTabButton).forEach(element => { element.addEventListener("click", () => { const header = utils.$(CONFIG.selectors.navMenuBottomTabHeader); const bottom = utils.$(CONFIG.selectors.navMenuBottom); if (header) utils.addClass(header, "is-open"); if (bottom) utils.setStyle(bottom, "overflow", "scroll"); }); }); utils.$$(`${CONFIG.selectors.navMenuBottomBack}, ${CONFIG.selectors.navMenuBottomOverlay}`).forEach(element => { element.addEventListener("click", () => { this.scrollToTop(); const header = utils.$(CONFIG.selectors.navMenuBottomTabHeader); const buttons = utils.$$(CONFIG.selectors.navMenuTabButton); const bottom = utils.$(CONFIG.selectors.navMenuBottom); if (header) utils.removeClass(header, "is-open"); buttons.forEach(btn => { utils.removeClass(btn, "w--current"); utils.removeClass(btn, "w--tab-active"); }); if (bottom) utils.setStyle(bottom, "overflow", "hidden"); }); }); const scrollToTopBtn = utils.$("#scroll-to-top"); if (scrollToTopBtn) { scrollToTopBtn.addEventListener("click", () => this.scrollToTop()); } } static scrollToTop() { const bottom = utils.$(CONFIG.selectors.navMenuBottom); if (bottom) { gsap.to(bottom, { scrollTop: 0 }); } } } // Filter count management class FilterCount { static init() { const checkboxes = utils.$$(CONFIG.selectors.cmsFilterCheckbox); checkboxes.forEach(checkbox => { checkbox.addEventListener("click", utils.debounce(this.updateFilterCounts, 100)); }); this.updateFilterCounts(); } static updateFilterCounts() { const filterCheckboxes = utils.$$(CONFIG.selectors.cmsFilterCheckbox); filterCheckboxes.forEach(filterCheckbox => { const parent = filterCheckbox.parentElement; if (!parent) return; const activeCheckboxes = utils.findAll(parent, `${CONFIG.selectors.cmsFilterCheckbox}.is-active`); const count = activeCheckboxes.length; const filterCountElement = utils.find(parent, ".filter-count"); if (filterCountElement) { filterCountElement.textContent = count; } }); } } // Share article functionality class ShareArticle { static init() { const title = document.title; const url = window.location.href; this.setupSocialLinks(title, url); this.setupCopyButton(); } static setupSocialLinks(title, url) { const socialLinks = { "[data-share-facebook]": `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`, "[data-share-twitter]": `https://twitter.com/share?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`, "[data-share-linkedin]": `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`, "[data-share-whatsapp]": `https://api.whatsapp.com/send?text=${encodeURIComponent(title + "\n" + url)}`, }; Object.entries(socialLinks).forEach(([selector, href]) => { const element = utils.$(selector); if (element) { utils.setAttr(element, "href", href); utils.setAttr(element, "target", "_blank"); } }); } static setupCopyButton() { const copyButton = document.getElementById("copy-to-clipboard"); if (!copyButton) return; copyButton.addEventListener("click", () => { navigator.clipboard.writeText(window.location.href).then(() => { const copyBtnText = document.getElementById("copy-status"); const icon = utils.find(copyButton.parentElement, ".social-share-icon"); const closeIcon = utils.find(copyButton.parentElement, ".social-share-icon-close"); if (icon && closeIcon && copyBtnText) { copyBtnText.textContent = "Copied!"; utils.setStyle(icon, "display", "none"); utils.setStyle(closeIcon, "display", "block"); setTimeout(() => { copyBtnText.textContent = "Copy Link"; utils.setStyle(icon, "display", "block"); utils.setStyle(closeIcon, "display", "none"); }, CONFIG.timing.copyStatusDelay); } }); }); } } // Main initialization class App { static init() { // Initialize all components new SmoothScroll(); CopyrightYear.init(); SkipLink.init(); ScrollControl.init(); NavMenuLinks.init(); RotateButtons.init(); new AjaxModal(); DynamicBackgroundColor.init(); HighlightColor.init(); MenuBackgroundColor.init(); TextAnimations.init(); NavBottomMenu.init(); FilterCount.init(); ShareArticle.init(); } } // Initialize when DOM is ready document.addEventListener("DOMContentLoaded", () => { App.init(); });