PageSourceSearch

https://members.ciea.org.uk/wp-content/themes/memberlite/js/memberlite.js?ver=7.1.2

js ciea.org.uk collected 2026-09-25 02:26:40 UTC 12,349 bytes, 367 lines download raw bytes

1/**
2 * Main JS for the Memberlite Theme
3 */
4document.addEventListener('DOMContentLoaded', function () {
5	initTabs();
6	initDesktopNav();
7	initMobileNav();
8	initStickyNav();
9	initStickyVariation();
10	initBackToTop();
11});
12
13// ─── Tabs ────────────────────────────────────────────────────────────────────
14
15function initTabs() {
16	document.querySelectorAll('.memberlite_tabbable .memberlite_tabs li a').forEach(function (tabLink) {
17		tabLink.addEventListener('click', onTabClick);
18	});
19
20	// Check if we should switch tab content on page load
21	const hashLink = document.querySelector('a[data-toggle="tab"][href="' + window.location.hash + '"]');
22	if (hashLink) {
23		hashLink.click();
24	}
25}
26
27function onTabClick(e) {
28	e.preventDefault();
29
30	const tab = this.getAttribute('href').replace(/#/, '');
31	const tabarea = this.closest('.memberlite_tabbable');
32
33	setActiveTab(tabarea, tab);
34	setActiveTabLink(tabarea, this);
35	updateUrlHash(tab);
36}
37
38function setActiveTab(tabarea, tab) {
39	tabarea.querySelectorAll('.memberlite_tab_pane').forEach(function (pane) {
40		pane.style.display = 'none';
41		pane.classList.remove('memberlite_active');
42	});
43
44	const activePane = document.getElementById(tab);
45	if (activePane) {
46		activePane.style.display = '';
47		activePane.classList.add('memberlite_active');
48	}
49}
50
51function setActiveTabLink(tabarea, activeLink) {
52	tabarea.querySelectorAll('.memberlite_tabs li').forEach(function (li) {
53		li.classList.remove('memberlite_active');
54	});
55	activeLink.closest('li').classList.add('memberlite_active');
56}
57
58function updateUrlHash(hash) {
59	if (history.pushState) {
60		history.pushState(null, null, '#' + hash);
61	} else {
62		location.hash = '#' + hash;
63	}
64}
65
66// ─── Desktop Navigation ──────────────────────────────────────────────────────
67
68function initDesktopNav() {
69	// Both of these navigations are in the header and can have submenus.
70	const widgetSiteNav = document.querySelector('.header-widget-area');
71	const memberNav = document.getElementById('member-navigation');
72	const mainSiteNav = document.getElementById('site-navigation');
73	// Memberlite Nav Menu Block
74	const blockSiteNav = document.querySelectorAll('.memberlite-nav-menu');
75
76	if (mainSiteNav) {
77		mainSiteNav.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem);
78		initDesktopNavClickOutside(mainSiteNav);
79	}
80
81	if (blockSiteNav) {
82		blockSiteNav.forEach((blockMenu) => {
83			blockMenu.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem);
84			initDesktopNavClickOutside(blockMenu);
85		});
86	}
87
88	if (memberNav) {
89		memberNav.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem);
90		initDesktopNavClickOutside(memberNav);
91	}
92
93	if (widgetSiteNav) {
94		widgetSiteNav.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem);
95		initDesktopNavClickOutside(widgetSiteNav);
96	}
97}
98
99/**
100 * Collapse a single menu item's submenu and reset its toggle button.
101 * Also collapses any open nested submenus inside it so they don't appear
102 * pre-expanded if this item is reopened later.
103 *
104 * @param {HTMLElement} li - A .menu-item-has-children element.
105 */
106function collapseDesktopNavItem(li) {
107	const btn = li.querySelector(':scope > button[aria-controls]');
108	if (btn) {
109		btn.setAttribute('aria-expanded', 'false');
110	}
111
112	li.querySelectorAll('.menu-item-has-children > button[aria-controls]').forEach(function (nestedBtn) {
113		nestedBtn.setAttribute('aria-expanded', 'false');
114	});
115}
116
117/**
118 * Collapse all open items within a container, with an optional item to skip
119 * (used to close siblings when opening a new item).
120 *
121 * @param {HTMLElement} container - The element to search within.
122 * @param {HTMLElement} [skip]    - A .menu-item-has-children li to leave open.
123 */
124function collapseAllDesktopNavItems(container, skip) {
125	container.querySelectorAll('.menu-item-has-children').forEach(function (li) {
126		if (li !== skip) {
127			collapseDesktopNavItem(li);
128		}
129	});
130}
131
132/**
133 * Attach all event listeners to a single .menu-item-has-children element.
134 *
135 * @param {HTMLElement} li - A .menu-item-has-children element.
136 */
137function initDesktopNavItem(li) {
138	const btn = li.querySelector(':scope > button[aria-controls]');
139	if (!btn) {
140		return;
141	}
142
143	// Hover: mouseenter/mouseleave keep aria-expanded in sync with the visual
144	// hover state so the button is never out of step with what the user sees.
145	// This lets aria-expanded serve as the single CSS hook for showing submenus.
146	li.addEventListener('mouseenter', function () {
147		if (li.parentElement) {
148			collapseAllDesktopNavItems(li.parentElement, li);
149		}
150		btn.setAttribute('aria-expanded', 'true');
151	});
152
153	li.addEventListener('mouseleave', function () {
154		btn.setAttribute('aria-expanded', 'false');
155	});
156
157	// Button click: toggles the submenu for keyboard and pointer users.
158	// Because mouseenter already sets aria-expanded="true" before a mouse user
159	// could click, clicking while hovered correctly closes rather than
160	// "reopening" an already-visible submenu.
161	btn.addEventListener('click', function () {
162		const isExpanded = btn.getAttribute('aria-expanded') === 'true';
163
164		if (!isExpanded) {
165			if (li.parentElement) {
166				collapseAllDesktopNavItems(li.parentElement, li);
167			}
168			btn.setAttribute('aria-expanded', 'true');
169		} else {
170			collapseDesktopNavItem(li);
171		}
172	});
173
174	// Escape key: close the innermost open submenu and return focus to its
175	// toggle button. stopPropagation gives one-level-at-a-time behaviour for
176	// three-level menus — a second Escape closes the next level up.
177	li.addEventListener('keydown', function (e) {
178		if (e.key !== 'Escape') {
179			return;
180		}
181
182		if (btn.getAttribute('aria-expanded') === 'true') {
183			e.stopPropagation();
184			collapseDesktopNavItem(li);
185			btn.focus();
186		}
187	});
188
189	// focusout: collapse when focus leaves the <li> entirely. focusout bubbles,
190	// so one listener covers all descendants. relatedTarget is where focus is
191	// moving to — if it's still inside this <li>, we do nothing.
192	li.addEventListener('focusout', function (e) {
193		if (!li.contains(e.relatedTarget)) {
194			collapseDesktopNavItem(li);
195		}
196	});
197}
198
199/**
200 * Collapse everything if the user clicks outside the nav entirely.
201 *
202 * @param {HTMLElement} nav
203 */
204function initDesktopNavClickOutside(nav) {
205	document.addEventListener('click', function (e) {
206		if (!nav.contains(e.target)) {
207			collapseAllDesktopNavItems(nav);
208		}
209	});
210}
211
212// ─── Mobile Navigation ───────────────────────────────────────────────────────
213
214function initMobileNav() {
215	const mobileNav = document.getElementById('mobile-navigation');
216	const mobilenavTrigger = document.getElementById('expand-mobile-nav');
217	const mobileNavClose = document.getElementById('close-mobile-nav');
218
219	if (!mobileNav || !mobilenavTrigger || !mobileNavClose) return;
220
221	mobilenavTrigger.addEventListener('click', function () {
222		openMobileNav(mobileNav, mobilenavTrigger, mobileNavClose);
223	});
224
225	mobileNavClose.addEventListener('click', function () {
226		closeMobileNav(mobileNav, mobilenavTrigger);
227	});
228
229	// Close the mobile menu when the Escape key is pressed
230	document.addEventListener('keydown', function (e) {
231		if (e.key === 'Escape' && mobileNav.classList.contains('open')) {
232			closeMobileNav(mobileNav, mobilenavTrigger);
233		}
234	});
235
236	// Close the mobile menu when an anchor link is clicked
237	mobileNav.addEventListener('click', function (e) {
238		const link = e.target.closest('a');
239		if (link && isAnchorLinkForCurrentPage(link)) {
240			closeMobileNav(mobileNav, mobilenavTrigger);
241		}
242	});
243}
244
245function openMobileNav(mobileNav, trigger, closeBtn) {
246	const content = document.getElementById('content');
247	const colophon = document.getElementById('colophon');
248
249	document.body.classList.add('mobile-nav-open');
250	if (content) content.setAttribute('inert', true);
251	if (colophon) colophon.setAttribute('inert', true);
252	mobileNav.removeAttribute('inert');
253	mobileNav.classList.add('open');
254	trigger.setAttribute('aria-expanded', 'true');
255	closeBtn.focus();
256}
257
258function closeMobileNav(mobileNav, trigger) {
259	const content = document.getElementById('content');
260	const colophon = document.getElementById('colophon');
261
262	document.body.classList.remove('mobile-nav-open');
263	if (content) content.removeAttribute('inert');
264	if (colophon) colophon.removeAttribute('inert');
265	mobileNav.classList.remove('open');
266	mobileNav.setAttribute('inert', true);
267	trigger.setAttribute('aria-expanded', 'false');
268	trigger.focus();
269}
270
271function isAnchorLinkForCurrentPage(link) {
272	return (
273		// Filter out links with no # at all, and bare # links (which have a hash of '')
274		link.hash !== '' &&
275		// Rule out links to other domains that happen to have a hash, e.g. https://example.com/page#section
276		(link.origin === window.location.origin) &&
277		// Rule out links to other pages on the same site, e.g. /another-page#section,
278		// which would cause a full navigation anyway and naturally close the menu
279		(link.pathname === window.location.pathname)
280	);
281}
282
283// ─── Sticky Navigation ───────────────────────────────────────────────────────
284
285function initStickyNav() {
286	const stickyWrapper = document.querySelector( '.site-navigation-sticky-wrapper' );
287	const stickyNav = document.getElementById( 'site-navigation' );
288
289	if ( ! stickyWrapper || ! stickyNav ) return;
290
291	const navHeight = stickyNav.offsetHeight;
292	const adminBar = document.getElementById( 'wpadminbar' );
293	const adminBarHeight = adminBar ? adminBar.offsetHeight : 0;
294
295	stickyWrapper.style.height = navHeight + 'px';
296	stickyNav.style.height = navHeight + 'px';
297
298	const stickyTop = stickyWrapper.getBoundingClientRect().top + window.scrollY - adminBarHeight;
299
300	window.addEventListener( 'scroll', function () {
301		updateStickyState( stickyNav, stickyTop, adminBarHeight );
302	}, { passive: true } );
303}
304
305function updateStickyState( stickyNav, stickyTop, adminBarHeight ) {
306	if ( window.scrollY > stickyTop ) {
307		stickyNav.classList.add( 'site-navigation-sticky' );
308		stickyNav.style.top = adminBarHeight + 'px';
309	} else {
310		stickyNav.classList.remove( 'site-navigation-sticky' );
311		stickyNav.style.top = '';
312	}
313}
314
315function initStickyVariation() {
316	if ( window.innerWidth < 768 ) return;
317
318	const stickyWrapper = document.querySelector( '.site-header-variation-sticky-wrapper' );
319	const stickyNav     = document.querySelector( '.site-header-variation' );
320
321	if ( ! stickyWrapper || ! stickyNav ) return;
322
323	const navHeight      = stickyNav.offsetHeight;
324	const adminBar       = document.getElementById( 'wpadminbar' );
325	const adminBarHeight = adminBar ? adminBar.offsetHeight : 0;
326
327	stickyWrapper.style.height = navHeight + 'px';
328	stickyNav.style.height     = navHeight + 'px';
329
330	const stickyTop = stickyWrapper.getBoundingClientRect().top + window.scrollY - adminBarHeight;
331
332	updateStickyState( stickyNav, stickyTop, adminBarHeight );
333
334	window.addEventListener( 'scroll', function () {
335		updateStickyState( stickyNav, stickyTop, adminBarHeight );
336	}, { passive: true } );
337}
338
339// ─── Back to Top ─────────────────────────────────────────────────────────────
340
341function initBackToTop() {
342	const btn = document.querySelector('.memberlite-back-to-top.floating');
343	if (!btn) return;
344
345	const SCROLL_THRESHOLD = 300;
346
347	function updateScrollVisibility() {
348		btn.classList.toggle('is-visible', window.scrollY > SCROLL_THRESHOLD);
349	}
350
351	// Hide the button when the site footer scrolls into view.
352	const footer = document.getElementById('colophon');
353	if (footer) {
354		new IntersectionObserver(function (entries) {
355			if (!entries.length) return;
356			btn.classList.toggle('is-footer-visible', entries[0].isIntersecting);
357		}).observe(footer);
358	}
359
360	window.addEventListener('scroll', updateScrollVisibility, { passive: true });
361	updateScrollVisibility();
362
363	btn.addEventListener('click', function (e) {
364		e.preventDefault();
365		window.scrollTo({ top: 0, behavior: 'smooth' });
366	});
367}

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.