PageSourceSearch

https://lfv-bgld.at/media/astroid/js/megamenu.js?e5cfa0

js lfv-bgld.at collected 2026-10-02 19:46:59 UTC 21,921 bytes, 646 lines download raw bytes

1/**
2 * @package   Astroid Framework
3 * @author    Astroid Framework Team https://astroidframe.work
4 * @copyright Copyright (C) 2026 AstroidFrame.work.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3 or Later
6 */
7class AstroidMegaMenuPro {
8
9    constructor(navbar, options = {}) {
10
11        this.navbar = navbar;
12
13        this.settings = Object.assign({
14            megamenuSelector: navbar.dataset.megamenuClass || '.has-megamenu',
15            dropdownSelector: '.nav-item-caret',
16            contentSelector: navbar.dataset.megamenuContentClass || '.megamenu-container',
17            submenuSelector: navbar.dataset.megamenuSubmenuClass || '.nav-submenu',
18            trigger: navbar.dataset.astroidTrigger || 'hover', // 'hover' or 'click'
19            duration: (navbar.dataset.transitionSpeed/1000) || 0.6,
20            ease: navbar.dataset.easing || 'expo.out',
21            backdrop: navbar.dataset.megamenuBackdrop === 'true' || false,
22            spacing: navbar.dataset.megamenuSpacing === 'true' || false,
23            headerSelector: '.astroid-header',
24            rtl: document.body.classList.contains('rtl'),
25            effect: navbar.dataset.megamenuAnimation || 'slide-scale', // slide-scale | fade | zoom | slide | drop | flip | scaleY | none
26            stagger: navbar.dataset.megamenuStagger === 'true' || false,
27        }, options);
28        // Detect interaction capability instead of just touch devices
29        this.canHover = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
30        this.init();
31    }
32
33    init() {
34        this.items = this.navbar.querySelectorAll(
35            `${this.settings.megamenuSelector}`
36        );
37
38        this.createBackdrop();
39        this.injectARIA();
40        this.bindEvents();
41        this.observeMutations();
42        this.handleResize();
43    }
44
45    /* =============================
46       BACKDROP
47    ============================= */
48
49    createBackdrop() {
50        if (!this.settings.backdrop) return;
51
52        this.backdrop = document.createElement('div');
53        this.backdrop.className = 'astroid-menu-backdrop';
54
55        gsap.set(this.backdrop, {
56            position: 'fixed',
57            inset: 0,
58            backdropFilter: 'blur(6px)',
59            background: 'rgba(0,0,0,0.25)',
60            opacity: 0,
61            pointerEvents: 'none',
62            zIndex: 90
63        });
64        document.body.appendChild(this.backdrop);
65    }
66
67    showBackdrop() {
68        if (!this.backdrop) return;
69        gsap.to(this.backdrop, {
70            opacity: 1,
71            duration: 0.3
72        });
73        this.backdrop.style.pointerEvents = 'auto';
74    }
75
76    hideBackdrop() {
77        if (!this.backdrop) return;
78        gsap.to(this.backdrop, {
79            opacity: 0,
80            duration: 0.3
81        });
82        this.backdrop.style.pointerEvents = 'none';
83    }
84
85    /* =============================
86       EVENT BINDING
87    ============================= */
88
89    bindEvents() {
90        this.items.forEach(item => {
91            const trigger = item.querySelector('.as-menu-item');
92            if (!trigger) return;
93            const content = this.getContent(item);
94            if (!content) return;
95
96            if (this.settings.trigger === 'click') {
97                trigger.addEventListener('click', e => {
98                    e.preventDefault();
99                    this.toggle(item);
100                });
101            } else if (!this.canHover) {
102                const arrow = item.querySelector('.fa-chevron-down.nav-item-caret');
103                if (!arrow) {
104                    // Create caret element and append it if .nav-title exists
105                    const navTitle = trigger.querySelector('.nav-title');
106                    if (navTitle) {
107                        item.classList.remove('no-dropdown-icon');
108                        const i = document.createElement('i');
109                        i.classList.add('nav-item-caret', 'fas', 'fa-chevron-down');
110                        navTitle.appendChild(i);
111                        i.addEventListener('click', e => {
112                            e.preventDefault();
113                            this.toggle(item);
114                        });
115                    }
116                } else {
117                    arrow.addEventListener('click', e => {
118                        e.preventDefault();
119                        this.toggle(item);
120                    });
121                }
122            } else {
123                // Hover-capable devices
124                item.addEventListener('mouseenter', () => this.open(item));
125                item.addEventListener('mouseleave', () => this.close(item));
126            }
127            this.handleSubmenus(item);
128            this.keyboardSupport(trigger, item);
129        });
130
131        document.addEventListener('click', e => {
132            if (!this.navbar.contains(e.target)) this.closeAll();
133        });
134    }
135
136    /* =============================
137       MAIN OPEN
138    ============================= */
139
140    toggle(item) {
141        item.classList.contains('open')
142            ? this.close(item)
143            : this.open(item);
144    }
145    open(item) {
146
147        this.closeSiblings(item);
148
149        const content = this.getContent(item);
150        if (!content) return;
151        // If closing animation is running, kill it
152        gsap.killTweensOf(content);
153
154        const subs = item.querySelectorAll('.nav-item-submenu');
155        if (subs) subs.forEach(sub => this.closeSub(sub));
156
157        item.classList.add('open');
158
159        // Kill previous animation if exists
160        if (content._tl) content._tl.kill();
161
162        // Always force visible before animating (fix fast hover issue)
163        content.style.display = 'block';
164        content.style.pointerEvents = 'auto';
165        content.style.visibility = 'hidden';
166        this.positionContent(item, content);
167
168        gsap.set(content, { autoAlpha: 1 });
169
170        const effect = this.settings.effect;
171        content._tl = gsap.timeline();
172
173        if (effect === 'slide-scale') {
174            content._tl.fromTo(content,
175                { autoAlpha: 0, y: 25, scale: 0.97 },
176                { autoAlpha: 1, y: 0, scale: 1, duration: this.settings.duration, ease: this.settings.ease }
177            );
178        }
179        else if (effect === 'fade') {
180            content._tl.fromTo(content,
181                { autoAlpha: 0 },
182                { autoAlpha: 1, duration: this.settings.duration, ease: this.settings.ease }
183            );
184        }
185        else if (effect === 'zoom') {
186            content._tl.fromTo(content,
187                { autoAlpha: 0, scale: 0.95 },
188                { autoAlpha: 1, scale: 1, duration: this.settings.duration, ease: this.settings.ease }
189            );
190        }
191        else if (effect === 'slide') {
192            content._tl.fromTo(content,
193                { autoAlpha: 0, y: 20 },
194                { autoAlpha: 1, y: 0, duration: this.settings.duration, ease: this.settings.ease }
195            );
196        }
197        else if (effect === 'drop') {
198            content._tl.fromTo(content,
199                { autoAlpha: 0, y: -20 },
200                { autoAlpha: 1, y: 0, duration: this.settings.duration * 0.9, ease: this.settings.ease }
201            );
202        }
203        else if (effect === 'flip') {
204            content._tl.fromTo(content,
205                { autoAlpha: 0, rotateX: -15, transformPerspective: 800, transformOrigin: 'top center' },
206                { autoAlpha: 1, rotateX: 0, duration: this.settings.duration, ease: this.settings.ease }
207            );
208        }
209        else if (effect === 'scaleY') {
210            content._tl.fromTo(content,
211                { autoAlpha: 0, scaleY: 0.9, transformOrigin: 'top center' },
212                { autoAlpha: 1, scaleY: 1, duration: this.settings.duration, ease: this.settings.ease }
213            );
214        }
215        else if (effect === 'blur-in') {
216            content._tl.fromTo(content,
217                { autoAlpha: 0, filter: 'blur(10px)', y: 10 },
218                { autoAlpha: 1, filter: 'blur(0px)', y: 0, duration: this.settings.duration, ease: this.settings.ease }
219            );
220        }
221        else { // none (default)
222            gsap.set(content, { autoAlpha: 1, y: 0, scale: 1 });
223        }
224
225        if (effect !== 'none' && this.settings.stagger) {
226            this.staggerItems(content, item);
227        }
228
229        this.showBackdrop();
230        this.rotateArrow(item, true);
231    }
232
233    close(item) {
234
235        const content = this.getContent(item);
236        if (!content) return;
237
238        if (content._tl) content._tl.kill();
239
240        // Kill any running tweens on content
241        gsap.killTweensOf(content);
242
243        item.classList.remove('open');
244
245        const effect = this.settings.effect;
246
247        let vars = {
248            autoAlpha: 0,
249            duration: this.settings.duration * 0.6,
250            ease: 'power2.inOut',
251            overwrite: 'auto',
252            onComplete: () => {
253                if (!item.classList.contains('open')) {
254                    content.style.display = 'none';
255                    content.style.pointerEvents = 'none';
256                }
257            }
258        };
259
260        if (effect === 'slide') vars.y = 15;
261        else if (effect === 'zoom') vars.scale = 0.96;
262        else if (effect === 'drop') vars.y = -15;
263        else if (effect === 'flip') vars.rotateX = -10;
264        else if (effect === 'scaleY') vars.scaleY = 0.9;
265        else if (effect === 'blur-in') vars.filter = 'blur(8px)';
266        else if (effect === 'slide-scale') {
267            vars.y = 15;
268            vars.scale = 0.98;
269        }
270        else if (effect === 'fade') vars.scale = 1;
271        else {
272            vars.scale = 1;
273        }
274
275        gsap.to(content, vars);
276
277        this.rotateArrow(item, false);
278
279        this.hideBackdrop();
280    }
281
282    closeAll() {
283        this.items.forEach(item => this.close(item));
284    }
285
286    closeSiblings(current) {
287        this.items.forEach(item => {
288            if (item !== current) this.close(item);
289        });
290    }
291
292    getContent(item) {
293        return item.querySelector(this.settings.contentSelector) ||
294            item.querySelector(this.settings.submenuSelector);
295    }
296
297    /* =============================
298       SUBMENUS
299    ============================= */
300
301    handleSubmenus(parent) {
302
303        const subs = parent.querySelectorAll('.nav-item-submenu');
304        const parentContent = this.getContent(parent);
305
306        subs.forEach(sub => {
307            const link = sub.querySelector('.as-menu-item');
308
309            const submenu = sub.querySelector(this.settings.submenuSelector);
310            if (!submenu || !link) return;
311
312            if (this.settings.trigger === 'click') {
313                link.addEventListener('click', e => {
314                    e.preventDefault();
315                    sub.classList.contains('open')
316                        ? this.closeSub(sub)
317                        : this.openSub(sub, parentContent);
318                })
319            } else if (!this.canHover) {
320                const arrow = sub.querySelector('.nav-item-caret');
321                arrow.addEventListener('click', e => {
322                    e.preventDefault();
323                    sub.classList.contains('open')
324                        ? this.closeSub(sub)
325                        : this.openSub(sub, parentContent);
326                });
327            } else {
328                sub.addEventListener('mouseenter', () => this.openSub(sub, parentContent));
329                sub.addEventListener('mouseleave', () => this.closeSub(sub));
330            }
331
332
333            link.addEventListener('keydown', e => {
334                if (e.key === 'Enter') {
335                    e.preventDefault();
336                    sub.classList.contains('open')
337                        ? this.closeSub(sub)
338                        : this.openSub(sub, parentContent);
339                }
340
341                if (e.key === 'Escape') {
342                    this.closeSub(sub);
343                    link.focus();
344                }
345            });
346        });
347    }
348
349    openSub(sub, parentContent) {
350
351        const submenu = sub.querySelector(':scope > '+this.settings.submenuSelector);
352        if (!submenu) return;
353
354        // reference the parent's stagger timeline directly
355        const waitMs = 150; // extra wait time in milliseconds
356
357        const proceed = () => {
358            if (submenu._tl) submenu._tl.kill();
359            sub.classList.add('open');
360            submenu.style.display = 'block';
361            submenu.style.pointerEvents = 'auto';
362            this.smartSubPosition(sub, submenu);
363
364            submenu._tl = gsap.timeline();
365
366            submenu._tl.fromTo(submenu,
367                { autoAlpha: 0, x: this.settings.rtl ? -15 : 15 },
368                { autoAlpha: 1, x: 0, duration: 0.35, ease: 'power2.out' }
369            );
370        };
371
372        if (parentContent._staggerTl && typeof parentContent._staggerTl.totalProgress === 'function' && parentContent._staggerTl.totalProgress() < 1) {
373            // If we're already waiting, don't attach another handler
374            if (parentContent._waitingForStagger) return;
375            parentContent._waitingForStagger = true;
376
377            const prevOnComplete = typeof parentContent._staggerTl.eventCallback === 'function' ? parentContent._staggerTl.eventCallback('onComplete') : null;
378
379            parentContent._staggerTl.eventCallback('onComplete', function() {
380                // call previous onComplete if present
381                if (typeof prevOnComplete === 'function') {
382                    try { prevOnComplete.call(this); } catch (e) {}
383                }
384
385                setTimeout(() => {
386                    parentContent._waitingForStagger = false;
387                    // ensure the submenu/parent still exists in DOM
388                    if (!document.contains(sub)) return;
389
390                    // Re-check that the stagger timeline is actually finished.
391                    // It's possible another stagger was created or restarted; if it's not complete, bail.
392                    if (parentContent._staggerTl && typeof parentContent._staggerTl.totalProgress === 'function' && parentContent._staggerTl.totalProgress() < 1) {
393                        return;
394                    }
395
396                    // Use the nearest trigger to this sub for hover detection
397                    const subTrigger = sub.querySelector(':scope > .as-menu-item')
398                        || sub.querySelector('.as-menu-item')
399                        || (sub.closest('.nav-item') && sub.closest('.nav-item').querySelector('.as-menu-item'));
400
401                    const isHovered = sub.matches(':hover') || (subTrigger && subTrigger.matches(':hover'));
402                    if (!isHovered) return;
403
404                    proceed();
405                }, waitMs);
406            });
407
408            return;
409        }
410
411        proceed();
412    }
413
414    closeSub(sub) {
415
416        const submenu = sub.querySelector(this.settings.submenuSelector);
417        if (!submenu) return;
418
419        if (submenu._tl) submenu._tl.kill();
420
421        gsap.to(submenu, {
422            autoAlpha: 0,
423            x: this.settings.rtl ? -10 : 10,
424            duration: 0.25,
425            ease: 'power2.in',
426            onComplete: () => {
427                submenu.style.display = 'none';
428                submenu.style.pointerEvents = 'none';
429                sub.classList.remove('open');
430            }
431        });
432    }
433
434    /* =============================
435       SMART POSITIONING
436    ============================= */
437
438    positionContent(item, content) {
439
440        const positionType = item.dataset.position;
441
442        // Reset first
443        content.style.left = '';
444        content.style.right = '';
445
446        const header = this.navbar.closest(this.settings.headerSelector) || this.navbar;
447        const headerRect = header.getBoundingClientRect();
448        const itemRect = item.getBoundingClientRect();
449        const mark = item.querySelector('.submenu-dropdown-mask');
450
451        // Set top based on available space below or above the item
452        mark.style.height = ( headerRect.bottom - itemRect.bottom + 10 ) + 'px';
453        if (this.settings.spacing) {
454            content.style.top = 'calc(100% + '+( headerRect.bottom - itemRect.bottom )+'px)';
455        }
456
457        // ===== FULL WIDTH (container width & centered) =====
458        if (positionType === 'full') {
459            content.style.width = headerRect.width + 'px';
460            content.style.left = (headerRect.left - itemRect.left) + 'px';
461            content.style.right = 'auto';
462            return;
463        }
464
465        // ===== EDGE (100vw & stick to viewport edges) =====
466        if (positionType === 'edge') {
467
468            content.style.width = window.innerWidth + 'px';
469
470            // Align to viewport left
471            content.style.left = (0 - itemRect.left) + 'px';
472            content.style.right = 'auto';
473
474            return;
475        }
476
477        // ===== DEFAULT SMART POSITION =====
478
479        const rect = content.getBoundingClientRect();
480        const overflowRight = rect.right > window.innerWidth;
481        const overflowLeft = rect.left < 0;
482
483        if (overflowRight) {
484            content.style.left = 'auto';
485            content.style.right = '0';
486        }
487
488        if (overflowLeft) {
489            content.style.left = '0';
490            content.style.right = 'auto';
491        }
492    }
493
494    smartSubPosition(parent, submenu) {
495        const parentRect = parent.getBoundingClientRect();
496        const rect = submenu.getBoundingClientRect();
497        const overflowRight = parentRect.right + rect.width > window.innerWidth;
498
499        if (overflowRight) {
500            submenu.style.left = 'auto';
501            submenu.style.right = '100%';
502        } else {
503            submenu.style.left = '100%';
504            submenu.style.right = 'auto';
505        }
506    }
507
508    /* =============================
509       ANIMATION HELPERS
510    ============================= */
511
512    staggerItems(container, parentItem) {
513
514        const el = container instanceof Element ? container : null;
515        if (!el) return;
516
517        let items;
518        if (parentItem.classList.contains('nav-item-megamenu')) {
519            items = el.querySelectorAll('li.nav-item-submenu.nav-item-level-2, li.nav-item-submenu.nav-item-level-3');
520        } else {
521            items = el.querySelectorAll('li.nav-item-submenu.nav-item-level-2');
522        }
523        if (!items.length) return;
524
525        // Kill previous stagger timeline if exists
526        if (el._staggerTl) {
527            el._staggerTl.kill();
528            el._staggerTl = null;
529        }
530
531        // Clear inline styles to avoid broken states
532        gsap.set(items, { clearProps: 'all' });
533
534        el._staggerTl = gsap.timeline({
535            defaults: {
536                ease: 'power2.out',
537                overwrite: 'auto'
538            }
539        });
540
541        el._staggerTl.from(items, {
542            opacity: 0,
543            y: 20,
544            stagger: 0.05,
545            duration: 0.4,
546            onComplete: () => {
547                // Clear inline styles after animation to prevent broken states
548                gsap.set(items, { clearProps: 'all' });
549            }
550        });
551    }
552
553    rotateArrow(item, open) {
554
555        const arrow = item.querySelector('.fa-chevron-down.nav-item-caret');
556        if (!arrow) return;
557
558        gsap.to(arrow, {
559            rotate: open ? 180 : 0,
560            duration: 0.3,
561            ease: 'power2.out'
562        });
563    }
564
565    /* =============================
566       ACCESSIBILITY
567    ============================= */
568
569    injectARIA() {
570        this.items.forEach(item => {
571            const trigger = item.querySelector(':scope > .as-menu-item');
572            const content = this.getContent(item);
573
574            if (!trigger || !content) return;
575
576            trigger.setAttribute('aria-haspopup', 'true');
577            trigger.setAttribute('aria-expanded', 'false');
578
579            // Ensure the content has an id so we can set aria-controls
580            if (!content.id) {
581                content.id = 'megamenu-content-' + Math.random().toString(36).substr(2, 9);
582            }
583            trigger.setAttribute('aria-controls', content.id);
584
585            // Also inject ARIA for submenu items inside this item
586            const subs = item.querySelectorAll('.nav-item-submenu');
587            subs.forEach(sub => {
588                const subTrigger = sub.querySelector(':scope > .as-menu-item');
589                const subMenu = sub.querySelector(':scope > .nav-submenu, :scope > .nav-submenu-static');
590
591                if (!subTrigger || !subMenu) return;
592                subTrigger.setAttribute('aria-haspopup', 'true');
593                subTrigger.setAttribute('aria-expanded', 'false');
594
595                if (!subMenu.id) {
596                    subMenu.id = 'megamenu-sub-' + Math.random().toString(36).substr(2, 9);
597                }
598                subTrigger.setAttribute('aria-controls', subMenu.id);
599            });
600        });
601    }
602
603    keyboardSupport(trigger, item) {
604
605        trigger.addEventListener('keydown', e => {
606
607            if (e.key === 'Enter') {
608                e.preventDefault();
609                this.toggle(item);
610            }
611
612            if (e.key === 'Escape') {
613                this.close(item);
614                trigger.focus();
615            }
616        });
617    }
618
619
620    /* =============================
621       OBSERVERS
622    ============================= */
623
624    observeMutations() {
625
626        const observer = new MutationObserver(() => {
627            this.items = this.navbar.querySelectorAll(
628                `${this.settings.megamenuSelector}`
629            );
630        });
631
632        observer.observe(this.navbar, { childList: true, subtree: true });
633    }
634
635    handleResize() {
636        window.addEventListener('resize', () => {
637            this.closeAll();
638        });
639    }
640}
641
642document.addEventListener('DOMContentLoaded', () => {
643    document.querySelectorAll('[data-megamenu]').forEach(nav => {
644        new AstroidMegaMenuPro(nav, {});
645    });
646});

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.