PageSourceSearch

https://okappi.be/wp-content/themes/custom/js/animation/header-overlay.js?ver=7.1.3

js okappi.be collected 2026-10-09 21:47:52 UTC 14,290 bytes, 414 lines download raw bytes

1document.addEventListener("DOMContentLoaded", function (event) {
2    
3    function openHeaderOverlay(menuAPI) {
4        const html = document.querySelector('html');
5        const body = document.querySelector('body');
6        const button = document.querySelector('#menu-button');
7        const overlay = document.querySelector('#headerOverlay');
8        const header = document.querySelector('header.site--header');
9
10        if (!button) return;
11
12        const closedText = button.querySelector('.text.closed');
13        const openText = button.querySelector('.text.open');
14        const iconLines = button.querySelectorAll('.icon-container span');
15
16        const topPosition = window.innerWidth >= 1600 ? "18px" : "10px";
17
18        const tl = gsap.timeline({
19            paused: true
20        });
21
22        tl.to(closedText, {
23            yPercent: -100,
24            y: -10,
25            duration: 0.8,
26            ease: "power4.inOut"
27        }, 0);
28
29        tl.to(openText, {
30            yPercent: 0,
31            y: 0,
32            duration: 0.8,
33            ease: "power4.inOut"
34        }, 0);
35
36        tl.to(iconLines, {
37            top: topPosition,
38            duration: 0.6,
39            ease: "power4.inOut"
40        }, 0);
41
42
43        tl.to(iconLines[0], {
44            rotation: 45,
45            duration: 0.6,
46            ease: "power4.inOut"
47        }, 0.4);
48
49        tl.to(iconLines[1], {
50            rotation: 45,
51            duration: 0.6,
52            ease: "power4.inOut"
53        }, 0.4);
54
55        tl.to(iconLines[2], {
56            rotation: -45,
57            duration: 0.6,
58            ease: "power4.inOut"
59        }, 0.4);
60
61        tl.to(iconLines[3], {
62            rotation: -45,
63            duration: 0.6,
64            ease: "power4.inOut"
65        }, 0.4);
66
67
68        button.addEventListener('click', function(e) {
69            e.preventDefault();
70
71            const isOpen = button.classList.toggle('open');
72
73            if (overlay) {
74                overlay.classList.toggle('open', isOpen);
75            }
76
77            if (header) {
78                header.classList.toggle('open', isOpen);
79            }
80
81            /*
82            if(html) {
83                html.classList.toggle('noscroll', isOpen);
84            }
85            */
86
87            // noscroll toggles once the open/close reveal has actually finished, not the
88            // instant the button is clicked — the scrollbar appearing/disappearing shifts
89            // the layout, and doing that while the menu itself is still mid-animation was
90            // an extra visible jolt on top of it.
91            if (isOpen) {
92                tl.play();
93                menuAPI?.animateMenuIn(() => {
94                    if (body) body.classList.add('noscroll');
95                });
96            } else {
97                menuAPI?.animateMenuOut(() => {
98                    if (body) body.classList.remove('noscroll');
99                });
100                tl.reverse();
101            }
102        });
103    }
104
105    function initMenuAnimations() {
106        const overlayMenu = document.querySelector("#headerOverlay");
107        const menu = document.querySelector("#headerOverlay .header-overlay-inner");
108        if (!menu) return;
109
110        const topLevel = menu.querySelector(".level--1");
111        const secondLevel = menu.querySelector(".level--2");
112        const secondLevelItems = secondLevel?.querySelectorAll(".menu-item-content");
113        const menuImages = document.querySelector(".navigation-images");
114        const imageItems = menuImages?.querySelectorAll(".image-container");
115
116        const colors = {
117            original: getComputedStyle(document.getElementById("headerOverlay")).backgroundColor,
118            current: null,
119        };
120
121        let itemIndex = null;
122
123        function setupMenuItemColors() {
124            topLevel.querySelectorAll(".menu-item").forEach((item) => {
125                if (!item.dataset.color) return;
126                addHoverEvents(item);
127            });
128        }
129
130        function addHoverEvents(item) {
131            item.addEventListener("mouseenter", () => handleMenuItemHover(item));
132            item.addEventListener("focus", () => handleMenuItemHover(item));
133            // No mouseleave/blur handler — the active item (and its second-level content/
134            // image) should stay put once hovered/focused, rather than reverting the instant
135            // the cursor leaves it. It's only ever replaced by whichever item takes over next.
136
137            item.addEventListener("keydown", (e) => {
138                if (e.key !== "Tab" || e.shiftKey) return;
139
140                const index = parseInt(item.dataset.menuIndex, 10);
141
142                if (isNaN(index)) return;
143
144                const target = secondLevel.querySelector(
145                    `.menu-item-content[data-index="${index}"]`
146                );
147
148                if (!target) return;
149
150                const firstLink = target.querySelector("a");
151
152                if (!firstLink) return;
153
154                e.preventDefault();
155
156                handleMenuItemHover(item);
157
158                // Wait until the content is visible, then focus its first link
159                requestAnimationFrame(() => {
160                    firstLink.focus();
161                });
162            });
163
164            item.addEventListener("click", (e) => {
165                if (item.getAttribute("href") === "#") e.preventDefault();
166                // mouseenter/focus alone don't reliably fire on a touch tap, and a
167                // dropdown item is a <button> with nowhere to navigate to on click, so
168                // this is the only way its second-level content ever opens on mobile.
169                handleMenuItemHover(item);
170            });
171        }
172
173        function setupSecondLevelFocus() {
174            secondLevel.querySelectorAll(".menu-item-content").forEach((content) => {
175                const links = content.querySelectorAll("a");
176
177                if (!links.length) return;
178
179                const lastLink = links[links.length - 1];
180
181                lastLink.addEventListener("keydown", (e) => {
182                    if (e.key !== "Tab" || e.shiftKey) return;
183
184                    e.preventDefault();
185
186                    const index = parseInt(content.dataset.index, 10);
187
188                    if (isNaN(index)) return;
189
190                    const items = topLevel.querySelectorAll(".menu-item");
191                    const nextItem = items[index + 1];
192
193                    if (nextItem) {
194                        nextItem.focus();
195                    }
196                });
197            });
198        }
199
200        function handleMenuItemHover(item) {
201            const hoverColor = item.dataset.color;
202            const index = parseInt(item.dataset.menuIndex, 10);
203
204            if (isNaN(index) || itemIndex === index) return;
205            itemIndex = index;
206
207            updateImageContent(index);
208            updateSecondLevelContent(index);
209
210            if (hoverColor && colors.current !== hoverColor) {
211                gsap.to(overlayMenu, { backgroundColor: hoverColor, duration: 0.15, ease: "power2.out" });
212                colors.current = hoverColor;
213            }
214
215            topLevel.querySelectorAll(".menu-item").forEach((i) => i.classList.remove("menu-item-active"));
216            item.classList.add("menu-item-active");
217        }
218
219        function resetMenuBackground() {
220            gsap.to(overlayMenu, { backgroundColor: colors.original, duration: 0.2, ease: "power2.out" });
221            colors.current = null;
222            itemIndex = null;
223            topLevel.querySelectorAll(".menu-item").forEach((i) => i.classList.remove("menu-item-active"));
224        }
225
226        function updateSecondLevelContent(index) {
227            if (!secondLevelItems) return;
228
229            secondLevelItems.forEach((el) => {
230                gsap.set(el, { display: "none", opacity: 0 });
231                el.classList.remove("active");
232            });
233
234            const target = secondLevel.querySelector(`.menu-item-content[data-index="${index}"]`);
235            if (!target) return;
236
237            const links = target.querySelectorAll(".menu-item-content-links a");
238            const buttons = target.querySelectorAll(".menu-item-content-buttons a");
239            const subtitles = target.querySelectorAll(".menu-label");
240            const text = target.querySelector(".custom-text");
241
242            gsap.set(target, { display: "block", opacity: 1 });
243            target.classList.add("active");
244
245            gsap.set(links, { y: 15, scale: 0.95, opacity: 0 });
246            gsap.set(buttons, { y: 15, scale: 0.95, opacity: 0 });
247            gsap.set(subtitles, { y: 15, scale: 0.95 });
248            gsap.set(text, { y: 15, scale: 0.95 });
249
250            gsap.to(links, {
251                y: 0,
252                scale: 1,
253                opacity: 1,
254                duration: 0.4,
255                ease: "power2.out",
256                stagger: 0.1,
257                onComplete: () => gsap.set(links, { clearProps: "all" }),
258            });
259
260            gsap.to(buttons, {
261                y: 0,
262                scale: 1,
263                opacity: 1,
264                duration: 0.4,
265                ease: "power2.out",
266                stagger: 0.1,
267                onComplete: () => gsap.set(buttons, { clearProps: "all" }),
268            });
269
270            gsap.to(subtitles, {
271                y: 0,
272                scale: 1,
273                duration: 0.4,
274                ease: "power2.out",
275                stagger: 0.1,
276                onComplete: () => gsap.set(subtitles, { clearProps: "all" }),
277            });
278
279            gsap.to(text, {
280                y: 0,
281                scale: 1,
282                duration: 0.4,
283                ease: "power2.out",
284                stagger: 0.1,
285                onComplete: () => gsap.set(text, { clearProps: "all" }),
286            });
287        }
288
289        function updateImageContent(index) {
290            if (!imageItems) return;
291
292            imageItems.forEach((img) => {
293                if (parseInt(img.dataset.index, 10) !== index) {
294                    gsap.set(img, { display: "none", opacity: 0 });
295                    img.classList.remove("active");
296                }
297            });
298
299            const target = menuImages.querySelector(`.image-container[data-index="${index}"]`);
300            if (!target) return;
301            gsap.set(target, { display: "block", opacity: 0 });
302            target.classList.add("active");
303            gsap.to(target, { opacity: 1, duration: 0.3, ease: "power2.in" });
304        }
305
306        function animateMenuIn(onComplete) {
307            const items = topLevel.querySelectorAll(".menu-item");
308            const subtitle = topLevel.querySelector(".menu-label");
309            const delay = 0.6;
310
311            gsap.set(items, { y: 30, x: 50, opacity: 0 });
312            gsap.set(subtitle, { y: 30, x: 50, opacity: 0 });
313
314            // Wrapped in a timeline (rather than two standalone tweens) so onComplete fires
315            // exactly once, after the slower of the two — attaching it directly to a staggered
316            // tween's own vars fires it once per staggered item, not once overall.
317            const tl = gsap.timeline({ onComplete });
318
319            tl.to(items, {
320                y: 0, x: 0, opacity: 1, duration: 0.3, ease: "power2.out",
321                stagger: { amount: 0.25, from: "start" },
322            }, delay);
323
324            tl.to(subtitle, { y: 0, x: 0, opacity: 0.6, duration: 0.4, ease: "power2.out" }, delay);
325
326            // First item active by default whenever the menu opens, matching the
327            // hover-driven state (image/second-level content/background) rather than
328            // opening with nothing selected.
329            if (items.length) handleMenuItemHover(items[0]);
330        }
331
332        function animateMenuOut(onComplete) {
333            const items = topLevel.querySelectorAll(".menu-item");
334            const subtitle = topLevel.querySelector(".menu-label");
335
336            const tl = gsap.timeline({ onComplete });
337
338            tl.to(items, {
339                x: 50, opacity: 0, duration: 0.3, ease: "power2.in",
340                stagger: { amount: 0.15, from: "end" },
341            }, 0);
342
343            tl.to(subtitle, { x: 50, opacity: 0, duration: 0.3, ease: "power2.in" }, 0);
344
345            resetMenuBackground();
346        }
347
348        setupMenuItemColors();
349        setupSecondLevelFocus();
350
351        return { resetMenuBackground, animateMenuIn, animateMenuOut };
352    }
353
354    function openMobileSubMenu() {
355        document.querySelectorAll('#headerOverlay .main-link + button').forEach(button => {
356            const submenu = button.nextElementSibling;
357
358            gsap.set(submenu, {
359                height: 0,
360                opacity: 0,
361                overflow: 'hidden',
362                display: 'none'
363            });
364
365            let isOpen = false;
366
367            button.addEventListener('click', (e) => {
368                e.preventDefault();
369                button.classList.toggle('open');
370
371                if (isOpen) {
372                    gsap.to(submenu, {
373                        height: 0,
374                        opacity: 0,
375                        duration: 0.3,
376                        ease: 'power2.inOut',
377                        onComplete: () => {
378                            gsap.set(submenu, { display: 'none' });
379                        }
380                    });
381                } else {
382                    gsap.set(submenu, {
383                        display: 'block',
384                        height: 'auto'
385                    });
386
387                    const height = submenu.offsetHeight;
388
389                    gsap.set(submenu, {
390                        height: 0,
391                        opacity: 0
392                    });
393
394                    gsap.to(submenu, {
395                        height,
396                        opacity: 1,
397                        duration: 0.3,
398                        ease: 'power2.inOut',
399                        onComplete: () => {
400                            gsap.set(submenu, { height: 'auto' });
401                        }
402                    });
403                }
404
405                isOpen = !isOpen;
406                button.setAttribute('aria-expanded', isOpen);
407            });
408        });
409    }
410
411    const menuAPI = initMenuAnimations();
412    openHeaderOverlay(menuAPI);
413    openMobileSubMenu();
414});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.