1(function ($, Drupal, once) { 2 "use strict" 3 4 Drupal.behaviors.custom = { 5 attach: function (context, settings) { 6 7 // ===== Mega Menu: state detection + background scroll lock + dynamic height ===== 8 const navRoot = document.getElementById('tbm-main') || document.querySelector('#tbm-main'); 9 const mobileMenuQuery = window.matchMedia('(max-width: 1140px)'); 10 11 // Helper: detect if mega menu is open (top-level item open or mobile overlay shown) 12 const isMenuOpen = () => { 13 if (!navRoot) return false; 14 return ( 15 !!navRoot.querySelector('.tbm-item.level-1.open') || 16 navRoot.classList.contains('tbm--mobile-show') 17 ); 18 }; 19 20 // Background scroll lock (allow scroll only inside the open submenu/panels) 21 const scrollLock = (() => { 22 let locked = false; 23 let y = 0; 24 25 const allowTarget = (t) => { 26 if (!navRoot) return false; 27 // Mobile overlay scroll container. 28 const mobileCollapse = navRoot.querySelector(':scope > .tbm-collapse'); 29 if (navRoot.classList.contains('tbm--mobile-show') && mobileCollapse && mobileCollapse.contains(t)) return true; 30 // Open top submenu 31 const topOpen = navRoot.querySelector('.tbm-item.level-1.open > .tbm-submenu'); 32 if (topOpen && topOpen.contains(t)) return true; 33 // Inline accordion containers and panels 34 const panels = navRoot.querySelectorAll('.tbm-inline-accordion, .tbm-inline-accordion .accordion-collapse'); 35 for (const p of panels) if (p.contains(t)) return true; 36 return false; 37 }; 38 39 const onBlock = (e) => { 40 if (!allowTarget(e.target)) { 41 e.preventDefault(); 42 } 43 }; 44 45 return { 46 lock() { 47 if (locked) return; 48 locked = true; 49 y = window.scrollY || document.documentElement.scrollTop || 0; 50 document.documentElement.classList.add('noscroll'); 51 document.body.classList.add('noscroll'); 52 // iOS/Safari friendly: fix body to prevent jump 53 document.body.style.position = 'fixed'; 54 document.body.style.top = `-${y}px`; 55 document.body.style.width = '100%'; 56 document.addEventListener('wheel', onBlock, { passive: false }); 57 document.addEventListener('touchmove', onBlock, { passive: false }); 58 }, 59 unlock() { 60 if (!locked) return; 61 locked = false; 62 document.documentElement.classList.remove('noscroll'); 63 document.body.classList.remove('noscroll'); 64 document.body.style.position = ''; 65 document.body.style.top = ''; 66 document.body.style.width = ''; 67 document.removeEventListener('wheel', onBlock, { passive: false }); 68 document.removeEventListener('touchmove', onBlock, { passive: false }); 69 window.scrollTo(0, y); 70 } 71 }; 72 })(); 73 74 const setMobileCollapseHeight = () => { 75 if (!navRoot) return; 76 77 const collapse = navRoot.querySelector(':scope > .tbm-collapse'); 78 if (!collapse) return; 79 80 if (!navRoot.classList.contains('tbm--mobile-show') || !mobileMenuQuery.matches) { 81 collapse.style.removeProperty('min-height'); 82 collapse.style.removeProperty('max-height'); 83 collapse.style.removeProperty('overflow-y'); 84 collapse.style.removeProperty('-webkit-overflow-scrolling'); 85 return; 86 } 87 88 const collapseTop = collapse.getBoundingClientRect().top; 89 const available = Math.max(0, window.innerHeight - collapseTop); 90 91 collapse.style.minHeight = available + 'px'; 92 collapse.style.maxHeight = available + 'px'; 93 collapse.style.overflowY = 'auto'; 94 collapse.style.webkitOverflowScrolling = 'touch'; 95 }; 96 97 // Compute available viewport height from the top of the OPEN top submenu ("cage").
98 // Only size the cage. Inner accordions/panels inherit and can grow naturally. 99 const setScrollableMaxHeight = () => { 100 if (!navRoot) return; 101 102 const cage = navRoot.querySelector('.tbm-item.level-1.open > .tbm-submenu'); 103 if (!cage) return; 104 105 // Proactively clear any stale inline styles that may have been set previously 106 cage.querySelectorAll('.tbm-inline-accordion, .tbm-inline-accordion .accordion-collapse').forEach(el => { 107 el.style.maxHeight = ''; 108 el.style.overflowY = ''; 109 el.style.webkitOverflowScrolling = ''; 110 }); 111 112 const cageTop = cage.getBoundingClientRect().top; 113 const available = Math.max(0, window.innerHeight - cageTop); 114 115 cage.style.maxHeight = available + 'px'; 116 cage.style.overflowY = 'auto'; 117 cage.style.webkitOverflowScrolling = 'touch'; 118 119 // As a safety, ensure no inline accordion/panel keeps a stale max-height 120 // that could collapse it. Clear on all under the open menu in a microtask 121 // to win over any other async writers. 122 setTimeout(() => { 123 const scope = cage.closest('#tbm-main') || navRoot; 124 if (!scope) return; 125 scope.querySelectorAll('.tbm-inline-accordion, .tbm-inline-accordion .accordion-collapse').forEach(el => { 126 el.style.removeProperty('max-height'); 127 el.style.removeProperty('overflow-y'); 128 el.style.removeProperty('-webkit-overflow-scrolling'); 129 }); 130 }, 0); 131 }; 132 133 // Observe class changes to toggle lock and refresh heights 134 if (navRoot) { 135 const onState = () => { 136 const open = isMenuOpen(); 137 if (open) { 138 scrollLock.lock(); 139 setMobileCollapseHeight(); 140 setScrollableMaxHeight(); 141 } else { 142 setMobileCollapseHeight(); 143 scrollLock.unlock(); 144 } 145 }; 146 147 // Ensure we only attach one observer 148 once('tbm-observer', navRoot).forEach(function () { 149 const mo = new MutationObserver(onState); 150 mo.observe(navRoot, { attributes: true, subtree: true, attributeFilter: ['class'] }); 151 // Initial sync 152 onState(); 153 154 // Recompute on resize/orientation and on Bootstrap collapse events 155 window.addEventListener('resize', () => { 156 if (isMenuOpen()) { 157 setMobileCollapseHeight(); 158 setScrollableMaxHeight(); 159 } 160 }); 161 window.addEventListener('orientationchange', () => { 162 if (isMenuOpen()) { 163 setMobileCollapseHeight(); 164 setScrollableMaxHeight(); 165 } 166 }); 167 document.addEventListener('shown.bs.collapse', (e) => { 168 if (navRoot.contains(e.target) && isMenuOpen()) { 169 setMobileCollapseHeight(); 170 setScrollableMaxHeight(); 171 } 172 }); 173 document.addEventListener('hidden.bs.collapse', (e) => { 174 if (navRoot.contains(e.target) && isMenuOpen()) { 175 setMobileCollapseHeight(); 176 setScrollableMaxHeight(); 177 } 178 }); 179 }); 180 181 once('tbm-mobile-back-area', navRoot).forEach(function () { 182 $(navRoot).on('click', '.tbm-item.level-1.open > .tbm-link-container', function (e) { 183 if (!mobileMenuQuery.matches || e.target !== this) return; 184 185 const toggle = this.querySelector(':scope > .tbm-submenu-toggle'); 186 if (!toggle) return; 187 188 e.preventDefault(); 189 toggle.click(); 190 }); 191 }); 192 } 193 194 // Helper: clear any fixed height on the cage and inner accordions, then reapply viewport-bounded max-height to the cage. 195 const topSubmenu = () => $('.tbm-item.level-1.open > .tbm-submenu'); 196 const refreshCage = () => { 197 const $top = topSubmenu(); 198 if (!$top.length) return; 199 // Clear cage inline height 200 $top.css('height', ''); 201 const cageEl = $top.get(0); 202 if (cageEl) { 203 // Clear any previous inline max-height/overflow set on inner accordions/panels 204 cageEl.querySelectorAll('.tbm-inline-accordion, .tbm-inline-accordion .accordion-collapse').forEach(el => { 205 el.style.maxHeight = ''; 206 el.style.overflowY = ''; 207 el.style.webkitOverflowScrolling = ''; 208 }); 209 } 210 setScrollableMaxHeight(); 211 }; 212 213 // When inline accordions open/close, recompute the cage height after transition. 214 once('accordion-events', document).forEach(function () { 215 $(document).on('shown.bs.collapse hidden.bs.collapse', '.tbm-inline-accordion .accordion-collapse', function (e) { 216 if (navRoot && navRoot.contains(e.target) && isMenuOpen()) { 217 refreshCage(); 218 } 219 }); 220 }); 221 } 222 } 223})(jQuery, Drupal, once)
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.