PageSourceSearch

https://cornerstoneseducation.co.uk/wp-content/themes/cornerstones…/js/navigation/nav-front.js?ver=2.272.0

js cornerstoneseducation.co.uk collected 2026-09-26 10:12:13 UTC 11,016 bytes, 262 lines download raw bytes

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.