1document.addEventListener('DOMContentLoaded', () => { 2 3 // âââ CSS vars: header bottom + logo left edge ââââââââââââââââââââââââââââ 4 /* --nav-bottom is measured from HEIGHTS, not from the header's position. 5 * 6 * It used to read header.getBoundingClientRect().bottom. The header is 7 * position: fixed, and on iOS a fixed element's rect is resolved against 8 * the VISUAL viewport â which moves while Safari's toolbars expand and 9 * collapse, and they are mid-animation during page load. So the first 10 * measurement came back short, .wp-site-blocks got too little padding and 11 * the top of the page sat under the header; then the re-measure after load 12 * found the settled value and everything jumped down to its right place 13 * about half a second in. 14 * 15 * A height cannot be perturbed that way. The header's own offsetHeight 16 * plus the admin bar's, when there is one, is the same number by 17 * construction and is stable from the first layout â no crop, and nothing 18 * to jump to afterwards. 19 * 20 * It also lines up exactly with the CSS fallbacks in mainnav-bar.css: 21 * 55 + 0, 55 + 32, 64 + 32, 64 + 46. */ 22 const setNavVars = () => { 23 const header = document.querySelector('header'); 24 const adminBar = document.getElementById('wpadminbar'); 25 const logo = document.querySelector('.mainnav-logo'); 26 27 if (header) { 28 /* TWO offsets, because there are two different questions and they 29 * only have the same answer when nobody is logged in. 30 * 31 * --nav-bottom: how far down the VIEWPORT the fixed chrome reaches 32 * â admin bar plus header. Anything position:fixed or sticky, and 33 * any scroll-margin-top, is measured against the viewport and needs 34 * this one. That is the dropdown, the backdrop, the mobile menu, 35 * the sticky filter bar and the Ofsted modal. 36 * 37 * --header-height: the header alone. Used only for 38 * .wp-site-blocks' padding, which sits INSIDE the document â and 39 * when the admin bar shows, WordPress has already pushed the whole 40 * document down by its height with 41 * `html { margin-top: 32px !important }`. Adding the bar again 42 * there counts it twice and opens a gap exactly one admin bar tall 43 * between the header and the top of the page. */ 44 const headerH = header.offsetHeight; 45 const barH = adminBar ? adminBar.offsetHeight : 0; 46 47 document.documentElement.style.setProperty('--nav-bottom', (headerH + barH) + 'px'); 48 document.documentElement.style.setProperty('--header-height', headerH + 'px'); 49 } 50 51 // Horizontal, so unaffected by the above â the visual viewport's 52 // vertical offset is the only part that misbehaves. 53 if (logo) document.documentElement.style.setProperty('--logo-left', logo.getBoundingClientRect().left + 'px'); 54 }; 55 setNavVars(); 56 57 /* Re-measured whenever the header's height could still change. 58 * 59 * --nav-bottom is now what holds page content clear of the fixed header 60 * (see .wp-site-blocks in mainnav-bar.css), so a stale value means the top 61 * of the page sits underneath it. DOMContentLoaded is the earliest useful 62 * moment but not the last one that matters: the logo is an <img> that may 63 * not have loaded, Font Awesome swaps its <i> tags for <svg> on a later 64 * tick, and the web font can arrive later still â any of which can change 65 * what the header measures. 66 * 67 * Three cheap catch-ups rather than a polling loop. Each is one rect read 68 * and they only fire when something has genuinely settled. */ 69 window.addEventListener('load', setNavVars); 70 71 if ( document.fonts && document.fonts.ready ) { 72 document.fonts.ready.then( setNavVars ); 73 } 74 75 // No scroll listener here deliberately â the header is a fixed-height 76 // sticky nav with no shrink-on-scroll behaviour, so its rect never 77 // actually changes from scrolling, only from resizing. A scroll listener 78 // doing 2x getBoundingClientRect() reads on every event forces a 79 // synchronous reflow every frame for a value that never changes â 80 // pure jank on tablets, no benefit. 81 window.addEventListener('resize', setNavVars, { passive: true }); 82 83 // Orientation change on a tablet resizes, but iOS can fire the resize 84 // before the new dimensions have settled â re-read once after. 85 window.addEventListener('orientationchange', function () { 86 setTimeout( setNavVars, 100 ); 87 }); 88 89 // âââ Desktop primary nav â only on pointer (non-touch) devices ââââââââââ 90 // iPadOS reports (hover: hover) and (pointer: fine) as true even on pure 91 // touch, and iPad landscape width (1180-1366px on most models) is above 92 // this 1089px breakpoint â so a width check alone lets this mouse-only 93 // hover/dropdown JS bind on touch devices that can never trigger 94 // mouseenter/mouseleave. A real touch check closes that gap. 95 const isTouchDevice = ('ontouchstart' in window) || navigator.maxTouchPoints > 0; 96 97 if (window.innerWidth >= 1089 && !isTouchDevice) { 98 99 const items = Array.from(document.querySelectorAll('.mainnav-primary-item')); 100 const backdrop = document.createElement('div'); 101 backdrop.className = 'nav-backdrop'; 102 document.body.appendChild(backdrop); 103 104 const dropdowns = items.map(i =>
104 i.querySelector('.mainnav-dropdown')).filter(Boolean); 105 106 // Temporarily show all tab panels so height measurements include them all 107 const allPanels = document.querySelectorAll('.mainnav-tab-panel'); 108 allPanels.forEach(p => p.style.display = 'block'); 109 110 dropdowns.forEach(dd => { 111 dd.style.visibility = 'hidden'; 112 dd.style.opacity = '0'; 113 dd.style.display = 'block'; 114 }); 115 const maxH = Math.max(...dropdowns.map(dd => dd.offsetHeight)); 116 dropdowns.forEach(dd => { 117 dd.style.minHeight = maxH + 'px'; 118 dd.style.display = ''; 119 dd.style.visibility = ''; 120 dd.style.opacity = ''; 121 }); 122 123 // Restore panels to their correct display state 124 allPanels.forEach(p => { p.style.display = ''; }); 125 126 let closeTimer = null; 127 let switchTimer = null; 128 129 function closeAll() { 130 items.forEach(item => { 131 item.classList.remove('is-open', 'is-animating'); 132 const btn = item.querySelector('.mainnav-primary-btn'); 133 const dd = item.querySelector('.mainnav-dropdown'); 134 if (btn) btn.setAttribute('aria-expanded', 'false'); 135 if (dd) dd.setAttribute('aria-hidden', 'true'); 136 }); 137 backdrop.classList.remove('active'); 138 } 139 140 const itemBar = document.querySelector('.mainnav-item-bar'); 141 142 function doSwitch(target) { 143 clearTimeout(switchTimer); 144 const wasOpen = items.some(i => i.classList.contains('is-open')); 145 146 if (wasOpen) { 147 itemBar.classList.add('is-switching'); 148 requestAnimationFrame(() => requestAnimationFrame(() => { 149 itemBar.classList.remove('is-switching'); 150 })); 151 } 152 153 items.forEach(item => { 154 const btn = item.querySelector('.mainnav-primary-btn'); 155 const dd = item.querySelector('.mainnav-dropdown'); 156 if (item === target) { 157 item.classList.add('is-open'); 158 if (!wasOpen) item.classList.add('is-animating'); 159 if (btn) btn.setAttribute('aria-expanded', 'true'); 160 if (dd) dd.setAttribute('aria-hidden', 'false'); 161 } else { 162 item.classList.remove('is-open', 'is-animating'); 163 if (btn) btn.setAttribute('aria-expanded', 'false'); 164 if (dd) dd.setAttribute('aria-hidden', 'true'); 165 } 166 }); 167 backdrop.classList.add('active'); 168 } 169 170 function openItem(target) { 171 clearTimeout(closeTimer); 172 clearTimeout(switchTimer); 173 const anyOpen = items.some(i => i.classList.contains('is-open')); 174 const alreadyOpen = target.classList.contains('is-open'); 175 if (anyOpen && !alreadyOpen) { 176 switchTimer = setTimeout(() => doSwitch(target), 100); 177 } else { 178 doSwitch(target); 179 } 180 } 181 182 function scheduleClose() { 183 clearTimeout(closeTimer); 184 clearTimeout(switchTimer); 185 closeTimer = setTimeout(closeAll, 150); 186 } 187 188 items.forEach(item => { 189 const dd = item.querySelector('.mainnav-dropdown'); 190 item.addEventListener('mouseenter', () => openItem(item)); 191 item.addEventListener('mouseleave', scheduleClose); 192 if (dd) { 193 dd.addEventListener('mouseenter', () => clearTimeout(closeTimer)); 194 dd.addEventListener('mouseleave', scheduleClose); 195 } 196 }); 197 198 backdrop.addEventListener('click', closeAll); 199 document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAll(); }); 200 201 // âââ Tab switching within tabbed dropdowns âââââââââââââââââââââââââââ 202 document.querySelectorAll('.mainnav-tab-bar').forEach(bar => { 203 bar.querySelectorAll('.mainnav-tab').forEach(tab => { 204 tab.addEventListener('click', e => { 205 e.stopPropagation(); 206 const targetId = tab.dataset.tab; 207 const dropdown = tab.closest('.mainnav-dropdown'); 208 209 bar.querySelectorAll('.mainnav-tab').forEach(t => t.classList.remove('is-active')); 210 tab.classList.add('is-active'); 211 212 dropdown.querySelectorAll('.mainnav-tab-panel').forEach(p => p.classList.remove('is-active')); 213 dropdown.querySelector('#' + targetId)?.classList.add('is-active'); 214 }); 215 }); 216 }); 217 } 218 219 // âââ Region pill switcher ââââââââââââââââââââââââââââââââââââââââââââââââ 220 const regionBtn = document.querySelector('.mainnav-region-btn'); 221 const regionDropdown = document.querySelector('.mainnav-region-dropdown'); 222 const regionLabel = document.querySelector('.mainnav-region-label'); 223 224 if (regionBtn && regionDropdown) { 225 // Measure at the widest option so the pill never shrinks when switching 226 const originalText = regionLabel.textContent; 227 regionLabel.textContent = 'International'; 228 regionBtn.style.minWidth = regionBtn.offsetWidth + 'px'; 229 regionLabel.textContent = originalText; 230 231 regionBtn.addEventListener('click', e => { 232 e.stopPropagation(); 233 const isOpen = regionDropdown.classList.toggle('is-open'); 234 regionBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 235 regionDropdown.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); 236 }); 237 238 regionDropdown.querySelectorAll('.mainnav-region-option').forEach(opt => { 239 opt.addEventListener('click', () => { 240 regionDropdown.querySelectorAll('.mainnav-region-option').forEach(o => { 241 o.classList.remove('mainnav-region-option--active'); 242 o.setAttribute('aria-selected', 'false'); 243 }); 244 opt.classList.add('mainnav-region-option--active'); 245 opt.setAttribute('aria-selected', 'true');
246 if (regionLabel) regionLabel.textContent = opt.textContent; 247 regionDropdown.classList.remove('is-open'); 248 regionBtn.setAttribute('aria-expanded', 'false'); 249 regionDropdown.setAttribute('aria-hidden', 'true'); 250 }); 251 }); 252 253 document.addEventListener('click', e => { 254 if (!regionBtn.closest('.mainnav-region').contains(e.target)) { 255 regionDropdown.classList.remove('is-open'); 256 regionBtn.setAttribute('aria-expanded', 'false'); 257 regionDropdown.setAttribute('aria-hidden', 'true'); 258 } 259 }); 260 } 261 262});
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.