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.