PageSourceSearch

https://ultimategutterguard.com/wp-content/themes/Blank-Theme-r/js/mega-menu.js?ver=8.1.6

js ultimategutterguard.com collected 2026-10-04 18:08:55 UTC 12,522 bytes, 334 lines download raw bytes

1/**
2 * Mega Menu Frontend JavaScript
3 * Handles mobile toggle, hover intent, and accessibility enhancements
4 */
5(function($) {
6    'use strict';
7
8    var MegaMenu = {
9
10        settings: {
11            breakpoint: 991,
12            hoverDelay: 100,
13            animationDuration: 300
14        },
15
16        init: function() {
17            // Merge with localized settings
18            if (typeof btMegaMenu !== 'undefined') {
19                $.extend(this.settings, btMegaMenu);
20            }
21
22            this.bindEvents();
23            this.setupAccessibility();
24        },
25
26        bindEvents: function() {
27            var self = this;
28
29            // Click handler - mobile toggle / desktop pin
30            $(document).on('click', '.has-mega-menu > a', function(e) {
31                if (self.isMobile()) {
32                    e.preventDefault();
33                    e.stopPropagation();
34                    self.toggleMobile($(this).parent());
35                } else {
36                    e.preventDefault();
37                    var $item = $(this).parent();
38
39                    if ($item.hasClass('mega-menu-pinned')) {
40                        // Unpin
41                        $item.removeClass('mega-menu-pinned mega-menu-active');
42                        self.updateAria($item, false);
43                    } else {
44                        // Unpin any other pinned menu
45                        $('.has-mega-menu.mega-menu-pinned').not($item).each(function() {
46                            $(this).removeClass('mega-menu-pinned mega-menu-active');
47                            self.updateAria($(this), false);
48                        });
49
50                        // Pin this one
51                        self.positionPanel($item);
52                        $item.addClass('mega-menu-pinned mega-menu-active');
53                        self.updateAria($item, true);
54                    }
55                }
56            });
57
58            // Desktop hover intent (prevent accidental triggers)
59            $(document).on('mouseenter', '.has-mega-menu', function() {
60                if (!self.isMobile()) {
61                    var $item = $(this);
62                    clearTimeout($item.data('hoverTimer'));
63
64                    // Unpin other items when hovering a new one
65                    $('.has-mega-menu.mega-menu-pinned').not($item).each(function() {
66                        $(this).removeClass('mega-menu-pinned mega-menu-active');
67                        self.updateAria($(this), false);
68                    });
69
70                    var timer = setTimeout(function() {
71                        // Set contained width if needed
72                        self.positionPanel($item);
73                        $item.addClass('mega-menu-active');
74                        self.updateAria($item, true);
75                    }, self.settings.hoverDelay);
76
77                    $item.data('hoverTimer', timer);
78                }
79            });
80
81            $(document).on('mouseleave', '.has-mega-menu', function() {
82                if (!self.isMobile()) {
83                    var $item = $(this);
84                    clearTimeout($item.data('hoverTimer'));
85
86                    // Don't close if pinned via click
87                    if ($item.hasClass('mega-menu-pinned')) return;
88
89                    // Blur focused link to prevent CSS :focus rule from keeping
90                    // panel visible with pointer-events:none (ghost panel bug)
91                    $item.find('> a:focus').blur();
92
93                    $item.removeClass('mega-menu-active');
94                    self.updateAria($item, false);
95                }
96            });
97
98            // Close on escape key
99            $(document).on('keydown', function(e) {
100                if (e.key === 'Escape' || e.keyCode === 27) {
101                    self.closeAll();
102                }
103            });
104
105            // Close on click outside
106            $(document).on('click', function(e) {
107                if (!$(e.target).closest('.has-mega-menu').length) {
108                    self.closeAll();
109                }
110            });
111
112            // Handle window resize
113            var resizeTimer;
114            $(window).on('resize', function() {
115                clearTimeout(resizeTimer);
116                resizeTimer = setTimeout(function() {
117                    self.handleResize();
118                }, 250);
119            });
120        },
121
122        /**
123         * Setup accessibility features
124         */
125        setupAccessibility: function() {
126            // Add ARIA attributes to mega menu items
127            $('.has-mega-menu').each(function() {
128                var $item = $(this);
129                var $link = $item.find('> a');
130                var $panel = $item.find('.mega-menu-panel');
131
132                $link.attr({
133                    'aria-haspopup': 'true',
134                    'aria-expanded': 'false'
135                });
136
137                $panel.attr({
138                    'role': 'menu',
139                    'aria-hidden': 'true'
140                });
141            });
142
143            // Keyboard navigation - open on Enter/Space when focu
143sed
144            $(document).on('keydown', '.has-mega-menu > a', function(e) {
145                if (e.key === 'Enter' || e.key === ' ' || e.keyCode === 13 || e.keyCode === 32) {
146                    e.preventDefault();
147                    var $item = $(this).parent();
148
149                    if (MegaMenu.isMobile()) {
150                        MegaMenu.toggleMobile($item);
151                    } else {
152                        if ($item.hasClass('mega-menu-active')) {
153                            $item.removeClass('mega-menu-active mega-menu-pinned');
154                            MegaMenu.updateAria($item, false);
155                        } else {
156                            // Close other open mega menus
157                            $('.has-mega-menu.mega-menu-active').each(function() {
158                                $(this).removeClass('mega-menu-active mega-menu-pinned');
159                                MegaMenu.updateAria($(this), false);
160                            });
161
162                            $item.addClass('mega-menu-active');
163                            MegaMenu.updateAria($item, true);
164
165                            // Focus first link in panel
166                            setTimeout(function() {
167                                $item.find('.mega-menu-panel a').first().focus();
168                            }, 100);
169                        }
170                    }
171                }
172            });
173
174            // Arrow key navigation within mega menu panel
175            $(document).on('keydown', '.mega-menu-panel', function(e) {
176                var $panel = $(this);
177                var $links = $panel.find('a:visible');
178                var currentIndex = $links.index(document.activeElement);
179
180                switch (e.key || e.keyCode) {
181                    case 'ArrowDown':
182                    case 40:
183                        e.preventDefault();
184                        if (currentIndex < $links.length - 1) {
185                            $links.eq(currentIndex + 1).focus();
186                        }
187                        break;
188
189                    case 'ArrowUp':
190                    case 38:
191                        e.preventDefault();
192                        if (currentIndex > 0) {
193                            $links.eq(currentIndex - 1).focus();
194                        } else {
195                            // Move focus back to trigger link
196                            $panel.closest('.has-mega-menu').find('> a').focus();
197                        }
198                        break;
199
200                    case 'Tab':
201                    case 9:
202                        // Close menu when tabbing out of last link
203                        if (!e.shiftKey && currentIndex === $links.length - 1) {
204                            var $item = $panel.closest('.has-mega-menu');
205                            $item.removeClass('mega-menu-active mega-menu-pinned mega-menu-mobile-open');
206                            MegaMenu.updateAria($item, false);
207                        }
208                        break;
209                }
210            });
211        },
212
213        /**
214         * Toggle mobile mega menu
215         */
216        toggleMobile: function($item) {
217            var isOpen = $item.hasClass('mega-menu-mobile-open');
218            var $panel = $item.find('.mega-menu-panel');
219
220            // Close other open mega menus
221            $('.has-mega-menu.mega-menu-mobile-open').not($item).each(function() {
222                $(this).removeClass('mega-menu-mobile-open');
223                $(this).find('.mega-menu-panel').slideUp(this.settings ? this.settings.animationDuration : 300);
224                MegaMenu.updateAria($(this), false);
225            });
226
227            // Toggle current item
228            if (isOpen) {
229                $item.removeClass('mega-menu-mobile-open');
230                $panel.slideUp(this.settings.animationDuration);
231                this.updateAria($item, false);
232            } else {
233                $item.addClass('mega-menu-mobile-open');
234                $panel.slideDown(this.settings.animationDuration);
235                this.updateAria($item, true);
236            }
237        },
238
239        /**
240         * Close all mega menus
241         */
242        closeAll: function() {
243            $('.has-mega-menu').each(function() {
244                var $item = $(this);
245                $item.removeClass('mega-menu-active mega-menu-pinned mega-menu-mobile-open');
246                MegaMenu.updateAria($item, false);
247
248                if (MegaMenu.isMobile()) {
249                    $item.find('.mega-menu-panel').slideUp(MegaMenu.settings.animationDuration);
250                }
251            });
252        },
253
254        /**
255         * Update ARIA attributes
256         */
257        updateAria: function($item, isOpen) {
258            var $link = $item.find('> a');
259            var $panel = $item.find('.mega-menu-panel');
260
261            $link.attr('aria-expanded', isOpen ? 'true' : 'false');
262            $panel.attr('aria-hidden', isOpen ? 'false' : 'true');
263        },
264
265        /**
266         * Position the mega menu panel
267         * Panel uses position:absolute relative to #cssmenu/#cssmenu2
268         * We calculate left offset and width so the panel aligns to viewport (full) or .container (contained)
269         */
270        positionPanel: function($item) {
271            var $panel = $item.find('.mega-menu-panel');
272            var $menuContainer = $item.closest('#cssmenu, #cssmenu2');
273
274            if (!$menuContainer.length) return;
275
276            var menuRect = $menuContainer[0].getBoundingClientRect();
277
278            if ($item.hasClass('mega-menu-width-full')) {
279                // Full width: span entire viewport
280                $panel.css({
281                    left: -menuRect.left + 'px',
282                    width: $(window).width() + 'px'
283                });
284            } else if ($item.hasClass('mega-menu-width-contained')) {
285                // Contained: match .container width and position
286                var $container = $('.container').first();
287                if ($container.length) {
288                    var containerRect = $container[0].getBoundingClientRect();
289                    $panel.css({
290                        left: (containerRect.left - menuRect.left) + 'px',
291                        width: containerRect.width + 'px'
292                    });
293                }
294            }
295            // AUTO width: no repositioning needed, stays relative to menu item
296        },
297
298        /**
299         * Handle window resize
300         */
301        handleResize: function() {
302            if (!this.isMobile()) {
303                // Reset mobile state when switching to desktop
304                $('.has-mega-menu').removeClass('mega-menu-mobile-open mega-menu-pinned');
305                $('.mega-menu-panel').css('display', '');
306
307                // Update ARIA for all
308                $('.has-mega-menu').each(function() {
309                    MegaMenu.updateAria($(this), false);
310                });
311            }
312        },
313
314        /**
315         * Check if currently in mobile view
316         */
317        isMobile: function() {
318            return $(window).width() <= this.settings.breakpoint;
319        }
320    };
321
322    // Initialize when DOM is ready
323    $(document).ready(function() {
324        MegaMenu.init();
325    });
326
327    // Also init after AJAX content loads (for dynamic menus)
328    $(document).ajaxComplete(function() {
329        setTimeout(function() {
330            MegaMenu.setupAccessibility();
331        }, 100);
332    });
333
334})(jQuery);

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.