PageSourceSearch

https://southernpartners.org/wp-content/themes/southernpartners-th…/js/header-menu.js?ver=0.2.18.1785786945

js southernpartners.org collected 2026-09-28 07:41:17 UTC 10,959 bytes, 394 lines download raw bytes

1(() => {
2	const initMegaMenu = () => {
3		const menu = document.querySelector("[data-sp-mega-menu]");
4
5		if (!(menu instanceof HTMLElement) || menu.dataset.spMegaReady === "true") {
6			return;
7		}
8
9		menu.dataset.spMegaReady = "true";
10
11		const menuItems = Array.from(menu.querySelectorAll("[data-sp-mega-item]"));
12		const desktopMedia = window.matchMedia("(min-width: 981px)");
13		const drawer = menu.querySelector("[data-sp-mega-drawer]");
14		const mobilePanel = menu.querySelector(".sp-mega-menu__mobile-panel");
15		const mobileToggle = menu.querySelector("[data-sp-mega-toggle]");
16		const mobileBack = menu.querySelector("[data-sp-mega-back]");
17		const closeButtons = Array.from(menu.querySelectorAll("[data-sp-mega-close]"));
18		const mobileLevels = Array.from(menu.querySelectorAll("[data-sp-mega-level]"));
19		const mobileStack = ["root"];
20		let activeItem = null;
21		let closeTimer = 0;
22		let mobileCloseTimer = 0;
23
24		const clearMobileCloseTimer = () => {
25			if (mobileCloseTimer) {
26				window.clearTimeout(mobileCloseTimer);
27				mobileCloseTimer = 0;
28			}
29		};
30
31		const clearCloseTimer = () => {
32			if (closeTimer) {
33				window.clearTimeout(closeTimer);
34				closeTimer = 0;
35			}
36		};
37
38		const closeDesktopMenu = () => {
39			clearCloseTimer();
40
41			menuItems.forEach((item) => {
42				const trigger = item.querySelector("[data-sp-mega-trigger]");
43				const panel = item.querySelector("[data-sp-mega-panel]");
44
45				item.classList.remove("is-open");
46
47				if (trigger instanceof HTMLElement) {
48					trigger.setAttribute("aria-expanded", "false");
49				}
50
51				if (panel instanceof HTMLElement) {
52					panel.hidden = true;
53				}
54			});
55
56			activeItem = null;
57		};
58
59		const positionDesktopPanel = (item, panel) => {
60			if (!(item instanceof HTMLElement) || !(panel instanceof HTMLElement)) {
61				return;
62			}
63
64			const itemRect = item.getBoundingClientRect();
65			const viewportWidth = document.documentElement.clientWidth;
66			const safeMargin = 16;
67			const previousVisibility = panel.style.visibility;
68
69			panel.style.left = "0px";
70			panel.style.right = "auto";
71			panel.style.visibility = "hidden";
72
73			const panelRect = panel.getBoundingClientRect();
74			const preferredLeft = itemRect.left + itemRect.width / 2 - panelRect.width / 2;
75			const clampedLeft = Math.max(
76				safeMargin,
77				Math.min(preferredLeft, Math.max(safeMargin, viewportWidth - safeMargin - panelRect.width))
78			);
79			const relativeLeft = clampedLeft - itemRect.left;
80			const arrowOffset = Math.max(20, Math.min(panelRect.width - 20, itemRect.width / 2 - relativeLeft));
81
82			panel.style.left = `${relativeLeft}px`;
83			panel.style.setProperty("--sp-panel-arrow-offset", `${arrowOffset}px`);
84			panel.style.visibility = previousVisibility;
85		};
86
87		const openDesktopMenu = (item) => {
88			if (!desktopMedia.matches || !(item instanceof HTMLElement)) {
89				return;
90			}
91
92			const trigger = item.querySelector("[data-sp-mega-trigger]");
93			const panel = item.querySelector("[data-sp-mega-panel]");
94
95			if (!(trigger instanceof HTMLElement) || !(panel instanceof HTMLElement)) {
96				return;
97			}
98
99			clearCloseTimer();
100
101			if (activeItem && activeItem !== item) {
102				closeDesktopMenu();
103			}
104
105			item.classList.add("is-open");
106			trigger.setAttribute("aria-expanded", "true");
107			panel.hidden = false;
108			positionDesktopPanel(item, panel);
109			activeItem = item;
110		};
111
112		const scheduleDesktopClose = () => {
113			clearCloseTimer();
114			closeTimer = window.setTimeout(closeDesktopMenu, 120);
115		};
116
117		menuItems.forEach((item) => {
118			const trigger = item.querySelector("[data-sp-mega-trigger]");
119			const panel = item.querySelector("[data-sp-mega-panel]");
120
121			if (!(trigger instanceof HTMLElement) || !(panel instanceof HTMLElement)) {
122				return;
123			}
124
125			item.addEventListener("mouseenter", () => openDesktopMenu(item));
126			item.addEventListener("mouseleave", scheduleDesktopClose);
127			item.addEventListener("focusin", () => openDesktopMenu(item));
128			item.addEventListener("focusout", (event) => {
129				const nextTarget = event.relatedTarget;
130
131				if (!(nextTarget instanceof Node) || !item.contains(nextTarget)) {
132					scheduleDesktopClose();
133				}
134			});
135
136			trigger.addEventListener("click", (event) => {
137				if (!desktopMedia.matches) {
138					return;
139				}
140
141				event.preventDefault();
142
143				if (activeItem === item) {
144					closeDesktopMenu();
145					return;
146				}
147
148				openDesktopMenu(item);
149			});
150		});
151
152		menu.addEventListener("keydown", (event) => {
153			if (event.key === "Escape") {
154				closeDesktopMenu();
155			}
156		});
157
158		document.addEventListener("click", (event) => {
159			if (!(event.target instanceof Node) || menu.contains(event.target)) {
160				return;
161			}
162
163			closeDesktopMenu();
164		});
165
166		const getLevelById = (levelId) => menu.querySelector(`[data-sp-mega-level="${levelId}"]`);
167		const mobileFocusableSelector = [
168			'a[href]:not([tabindex="-1"])',
169			'button:not([disabled]):not([tabindex="-1"])',
170			'input:not([disabled]):not([tabindex="-1"])',
171			'select:not([disabled]):not([tabindex="-1"])',
172			'textarea:not([disabled]):not([tabindex="-1"])',
173			'[tabindex]:not([tabindex="-1"])'
174		].join(",");
175
176		const getMobileFocusableElements = () => {
177			if (!(mobilePanel instanceof HTMLElement)) {
178				return [];
179			}
180
181			return Array.from(mobilePanel.querySelectorAll(mobileFocusableSelector)).filter(
182				(element) => element instanceof HTMLElement && element.getClientRects().length > 0
183			);
184		};
185
186		const focusCurrentMobileLevel = () => {
187			const currentLevel = getLevelById(mobileStack[mobileStack.length - 1]);
188			const firstControl = currentLevel instanceof HTMLElement
189				? Array.from(currentLevel.querySelectorAll(mobileFocusableSelector)).find(
190					(element) => element instanceof HTMLElement && element.getClientRects().length > 0
191				)
192				: null;
193
194			if (firstControl instanceof HTMLElement) {
195				firstControl.focus({ preventScroll: true });
196			}
197		};
198
199		const syncMobileState = (direction = "forward") => {
200			if (!(drawer instanceof HTMLElement) || !(mobileToggle instanceof HTMLElement)) {
201				return;
202			}
203
204			const currentLevelId = mobileStack[mobileStack.length - 1];
205			const currentIndex = mobileStack.length - 1;
206
207			mobileLevels.forEach((level) => {
208				if (!(level instanceof HTMLElement)) {
209					return;
210				}
211
212				const levelId = level.dataset.spMegaLevel;
213				const levelIndex = mobileStack.indexOf(levelId);
214				const isActive = levelId === currentLevelId;
215
216				level.classList.toggle("is-active", isActive);
217				level.classList.remove("is-before", "is-after");
218
219				if (!isActive && levelIndex !== -1 && levelIndex < currentIndex) {
220					level.classList.add("is-before");
221				} else if (!isActive) {
222					level.classList.add("is-after");
223				}
224			});
225
226			if (mobileBack instanceof HTMLElement) {
227				mobileBack.hidden = mobileStack.length <= 1;
228			}
229
230			drawer.classList.toggle("is-sublevel", mobileStack.length > 1);
231			drawer.dataset.spMegaDirection = direction;
232		};
233
234		const openMobileDrawer = () => {
235			if (!(drawer instanceof HTMLElement) || !(mobileToggle instanceof HTMLElement)) {
236				return;
237			}
238
239			clearMobileCloseTimer();
240			drawer.hidden = false;
241			window.requestAnimationFrame(() => {
242				drawer.classList.add("is-open");
243
244				const initialFocus = mobilePanel instanceof HTMLElement
245					? mobilePanel.querySelector("[data-sp-mega-close]")
246					: null;
247
248				if (initialFocus instanceof HTMLElement) {
249					initialFocus.focus({ preventScroll: true });
250				}
251			});
252			mobileToggle.setAttribute("aria-expanded", "true");
253			document.body.classList.add("sp-mega-menu-open");
254			document.body.style.overflow = "hidden";
255			mobileStack.splice(0, mobileStack.length, "root");
256			syncMobileState("forward");
257		};
258
259		const closeMobileDrawer = (restoreFocus = true) => {
260			if (!(drawer instanceof HTMLElement) || !(mobileToggle instanceof HTMLElement)) {
261				return;
262			}
263
264			const wasOpen = !drawer.hidden;
265
266			clearMobileCloseTimer();
267			drawer.classList.remove("is-open");
268			mobileToggle.setAttribute("aria-expanded", "false");
269			document.body.classList.remove("sp-mega-menu-open");
270			document.body.style.overflow = "";
271			mobileStack.splice(0, mobileStack.length, "root");
272			syncMobileState("forward");
273			mobileCloseTimer = window.setTimeout(() => {
274				drawer.hidden = true;
275			}, 220);
276
277			if (restoreFocus && wasOpen) {
278				mobileToggle.focus({ preventScroll: true });
279			}
280		};
281
282		if (drawer instanceof HTMLElement) {
283			drawer.addEventListener("keydown", (event) => {
284				if (drawer.hidden) {
285					return;
286				}
287
288				if (event.key === "Escape") {
289					event.preventDefault();
290					event.stopPropagation();
291					closeMobileDrawer();
292					return;
293				}
294
295				if (event.key !== "Tab") {
296					return;
297				}
298
299				const focusableElements = getMobileFocusableElements();
300
301				if (focusableElements.length === 0) {
302					event.preventDefault();
303					return;
304				}
305
306				const firstElement = focusableElements[0];
307				const lastElement = focusableElements[focusableElements.length - 1];
308				const activeElement = document.activeElement;
309
310				if (event.shiftKey && activeElement === firstElement) {
311					event.preventDefault();
312					lastElement.focus();
313				} else if (!event.shiftKey && activeElement === lastElement) {
314					event.preventDefault();
315					firstElement.focus();
316				}
317			});
318		}
319
320		if (mobileToggle instanceof HTMLElement) {
321			mobileToggle.addEventListener("click", () => {
322				if (drawer instanceof HTMLElement && !drawer.hidden) {
323					closeMobileDrawer();
324					return;
325				}
326
327				openMobileDrawer();
328			});
329		}
330
331		closeButtons.forEach((button) => {
332			button.addEventListener("click", () => closeMobileDrawer());
333		});
334
335		if (mobileBack instanceof HTMLElement) {
336			mobileBack.addEventListener("click", () => {
337				if (mobileStack.length > 1) {
338					mobileStack.pop();
339					syncMobileState("backward");
340					window.requestAnimationFrame(focusCurrentMobileLevel);
341				}
342			});
343		}
344
345		menu.addEventListener("click", (event) => {
346			const nextButton = event.target instanceof Element ? event.target.closest("[data-sp-mega-next]") : null;
347
348			if (!(nextButton instanceof HTMLElement)) {
349				return;
350			}
351
352			const nextLevel = nextButton.dataset.spMegaNext;
353
354			if (!nextLevel || !getLevelById(nextLevel)) {
355				return;
356			}
357
358			event.preventDefault();
359			mobileStack.push(nextLevel);
360			syncMobileState("forward");
361			window.requestAnimationFrame(focusCurrentMobileLevel);
362		});
363
364		desktopMedia.addEventListener("change", (event) => {
365			if (event.matches) {
366				closeMobileDrawer(false);
367				return;
368			}
369
370			closeDesktopMenu();
371		});
372
373		window.addEventListener("resize", () => {
374			if (!(activeItem instanceof HTMLElement) || !desktopMedia.matches) {
375				return;
376			}
377
378			const panel = activeItem.querySelector("[data-sp-mega-panel]");
379
380			if (panel instanceof HTMLElement && !panel.hidden) {
381				positionDesktopPanel(activeItem, panel);
382			}
383		});
384
385		syncMobileState();
386	};
387
388	if (document.readyState === "loading") {
389		document.addEventListener("DOMContentLoaded", initMegaMenu, { once: true });
390		return;
391	}
392
393	initMegaMenu();
394})();

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.