1/** 2 * Header 2026 â Improved dropdown UX 3 * 4 * Overrides the hover/scroll behaviour from application.js with: 5 * - Intent-based hover delay (prevents flicker) 6 * - No scroll-close (so the menu stays open while browsing) 7 * - Click-outside-to-close 8 * - ESC key closes dropdowns 9 * - Mobile: only one submenu open at a time 10 * - Backdrop overlay when dropdown is open 11 */ 12(function () { 13 'use strict'; 14 15 document.addEventListener('DOMContentLoaded', function () { 16 17 var OPEN_DELAY = 80; // ms before opening on hover 18 var CLOSE_DELAY = 250; // ms before closing on leave (generous for mouse travel) 19 20 var dropdownItems = document.querySelectorAll('.nav-primary > .nav-item--has-children'); 21 22 // ââ Track which item should be open ââ 23 // application.js removes .show on scroll via $(window).on("scroll", ...) 24 // Instead of racing to unbind it, we track intent and restore after scroll. 25 var activeItem = null; 26 27 function restoreAfterScroll() { 28 if (activeItem && getViewportWidth() >= 1024) { 29 // application.js scroll handler runs synchronously and removes .show 30 // We re-apply on the next frame 31 requestAnimationFrame(function () { 32 if (activeItem) { 33 activeItem.classList.add('show'); 34 updateBackdrop(); 35 } 36 }); 37 } 38 } 39 40 window.addEventListener('scroll', restoreAfterScroll, { passive: true }); 41 42 // ââ Unbind application.js hover handlers ââ 43 // Wait a tick so application.js $(document).ready() has fired first 44 setTimeout(function () { 45 if (window.jQuery) { 46 jQuery('.nav-primary .nav-item--has-children').off('mouseenter mouseleave'); 47 jQuery('.nav-primary .nav-item--has-children .arrow').off('click'); 48 } 49 }, 0); 50 51 // ââ Utility ââ 52 function getViewportWidth() { 53 return window.innerWidth || document.documentElement.clientWidth; 54 } 55 56 // ââ Backdrop overlay ââ 57 var backdrop = document.createElement('div'); 58 backdrop.className = 'nav-backdrop'; 59 document.body.appendChild(backdrop); 60 61 function updateBackdrop() { 62 var anyOpen = Array.prototype.some.call(dropdownItems, function (item) { 63 return item.classList.contains('show'); 64 }); 65 backdrop.classList.toggle('is-visible', anyOpen); 66 } 67 68 function closeAll() { 69 activeItem = null; 70 dropdownItems.forEach(function (item) { 71 item.classList.remove('show'); 72 }); 73 updateBackdrop(); 74 } 75 76 function closeOthers(current) { 77 dropdownItems.forEach(function (item) { 78 if (item !== current) { 79 item.classList.remove('show'); 80 } 81 }); 82 } 83 84 // ââ Desktop: intent-based hover ââ 85 dropdownItems.forEach(function (item) { 86 var openTimer = null; 87 var closeTimer = null; 88 89 item.addEventListener('mouseenter', function () { 90 if (getViewportWidth() < 1024) return; 91 92 clearTimeout(closeTimer); 93 closeTimer = null; 94 95 openTimer = setTimeout(function () { 96 closeOthers(item); 97 activeItem = item; 98 item.classList.add('show'); 99 updateBackdrop(); 100 }, OPEN_DELAY); 101 }); 102 103 item.addEventListener('mouseleave', function () { 104 if (getViewportWidth() < 1024) return; 105 106 clearTimeout(openTimer); 107 openTimer = null; 108 109 closeTimer = setTimeout(function () { 110 activeItem = null; 111 item.classList.remove('show'); 112 updateBackdrop(); 113 }, CLOSE_DELAY); 114 }); 115 }); 116 117 // ââ Desktop: click outside to close ââ 118 document.addEventListener('click', function (e) { 119 if (getViewportWidth() < 1024) return; 120 121 var isInsideNav = e.target.closest('.nav-item--has-children'); 122 if (!isInsideNav) { 123 closeAll(); 124 } 125 }); 126 127 // ââ Keyboard: ESC to close ââ 128 document.addEventListener('keydown', function (e) { 129 if (e.key === 'Escape' || e.keyCode === 27) { 130 closeAll(); 131 } 132 }); 133 134 // ââ Mobile: tap parent link or arrow to toggle dropdown ââ 135 document.addEventListener('click', function (e) { 136 if (getViewportWidth() >= 1024) return; 137 138 var parentItem = e.target.closest('.nav-item--has-children'); 139 if (!parentItem) return; 140 141 var link = parentItem.querySelector(':scope > a'); 142 if (!link || !link.contains(e.target)) return; 143 144 e.preventDefault(); 145 146 closeOthers(parentItem); 147 var isOpen = parentItem.classList.toggle('show'); 148 activeItem = isOpen ? parentItem : null; 149 updateBackdrop(); 150 }); 151 }); 152})();
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.