PageSourceSearch

https://centerforfaithandgiving.org/wp-content/themes/CenterForFaithAndGiving/assets/javascript/ac-menu.js

js centerforfaithandgiving.org collected 2026-09-26 04:54:27 UTC 6,567 bytes, 325 lines download raw bytes

1(function () {
2	'use strict';
3
4	function initMainNav() {
5		const nav = document.getElementById('main-nav');
6
7		if (!nav) {
8			return;
9		}
10
11		if (nav.dataset.navigationInitialized === 'true') {
12			return;
13		}
14
15		nav.dataset.navigationInitialized = 'true';
16
17		const mobileBreakpoint = 640;
18
19		/*
20		 * Main mobile navigation toggle.
21		 */
22		const navToggle = document.createElement('button');
23
24		navToggle.type = 'button';
25		navToggle.className = 'main-nav-toggle';
26		navToggle.setAttribute('aria-expanded', 'false');
27		navToggle.setAttribute('aria-controls', 'main-nav');
28
29		navToggle.innerHTML = `
30			<span class="main-nav-toggle__label">Menu</span>
31			<span class="main-nav-toggle__icon" aria-hidden="true"></span>
32		`;
33
34		nav.parentNode.insertBefore(navToggle, nav);
35
36
37		/**
38		 * Get the direct label element for a menu item.
39		 *
40		 * This may be either:
41		 * - a real link
42		 * - a non-link placeholder label
43		 */
44		function getItemLabel(item) {
45			return item.querySelector(
46				':scope > a, :scope > .menu-item-label'
47			);
48		}
49
50
51		/**
52		 * Reset viewport correction on a submenu.
53		 */
54		function resetSubmenuPosition(submenu) {
55			if (!submenu) {
56				return;
57			}
58
59			submenu.style.removeProperty('--submenu-shift');
60		}
61
62
63		/**
64		 * Keep a desktop dropdown inside the viewport.
65		 */
66		function positionSubmenu(item) {
67			if (window.innerWidth <= mobileBreakpoint) {
68				return;
69			}
70
71			const submenu = item.querySelector(':scope > .sub-menu');
72
73			if (!submenu) {
74				return;
75			}
76
77			resetSubmenuPosition(submenu);
78
79			const viewportPadding = 16;
80			const rect = submenu.getBoundingClientRect();
81
82			let shift = 0;
83
84			if (rect.right > window.innerWidth - viewportPadding) {
85				shift -= rect.right - (window.innerWidth - viewportPadding);
86			}
87
88			if (rect.left + shift < viewportPadding) {
89				shift += viewportPadding - (rect.left + shift);
90			}
91
92			if (shift !== 0) {
93				submenu.style.setProperty(
94					'--submenu-shift',
95					`${shift}px`
96				);
97			}
98		}
99
100
101		/**
102		 * Close a single submenu.
103		 */
104		function closeSubmenu(item, returnFocus = false) {
105			const button = item.querySelector(
106				':scope > .submenu-toggle'
107			);
108
109			const label = getItemLabel(item);
110			const submenu = item.querySelector(':scope > .sub-menu');
111
112			item.classList.remove('submenu-open');
113
114			resetSubmenuPosition(submenu);
115
116			if (!button) {
117				return;
118			}
119
120			button.setAttribute('aria-expanded', 'false');
121
122			if (label) {
123				button.setAttribute(
124					'aria-label',
125					`Open submenu for ${label.textContent.trim()}`
126				);
127			}
128
129			if (returnFocus) {
130				button.focus();
131			}
132		}
133
134
135		/**
136		 * Close sibling submenus at the same menu level.
137		 */
138		function closeSiblingSubmenus(currentItem) {
139			const parent = currentItem.parentElement;
140
141			if (!parent) {
142				return;
143			}
144
145			parent
146				.querySelectorAll(':scope > .submenu-open')
147				.forEach(function (item) {
148					if (item === currentItem) {
149						return;
150					}
151
152					closeSubmenu(item);
153				});
154		}
155
156
157		/**
158		 * Close the mobile navigation.
159		 */
160		function closeMainNav(returnFocus = false) {
161			nav.classList.remove('main-nav-open');
162			navToggle.setAttribute('aria-expanded', 'false');
163
164			if (returnFocus) {
165				navToggle.focus();
166			}
167		}
168
169
170		/*
171		 * Mobile navigation toggle.
172		 */
173		navToggle.addEventListener('click', function () {
174			const isOpen = nav.classList.toggle('main-nav-open');
175
176			navToggle.setAttribute(
177				'aria-expanded',
178				isOpen ? 'true' : 'false'
179			);
180		});
181
182
183		/*
184		 * Add submenu toggle buttons.
185		 */
186		const parentItems = nav.querySelectorAll(
187			'.menu-item-has-children'
188		);
189
190		parentItems.forEach(function (item, index) {
191			const label = getItemLabel(item);
192			const submenu = item.querySelector(':scope > .sub-menu');
193
194			if (!label || !submenu) {
195				return;
196			}
197
198			const itemId = item.id
199				? item.id.replace('menu-item-', '')
200				: index + 1;
201
202			const submenuId = `nav-submenu-${itemId}`;
203			const labelText = label.textContent.trim();
204
205			submenu.id = submenuId;
206
207			const button = document.createElement('button');
208
209			button.type = 'button';
210			button.className = 'submenu-toggle';
211
212			button.setAttribute('aria-expanded', 'false');
213			button.setAttribute('aria-controls', submenuId);
214			button.setAttribute(
215				'aria-label',
216				`Open submenu for ${labelText}`
217			);
218
219			button.innerHTML =
220				'<span aria-hidden="true"></span>';
221
222			label.insertAdjacentElement('afterend', button);
223
224			button.addEventListener('click', function () {
225				const isCurrentlyOpen =
226					item.classList.contains('submenu-open');
227
228				if (!isCurrentlyOpen) {
229					closeSiblingSubmenus(item);
230				}
231
232				const isOpen =
233					item.classList.toggle('submenu-open');
234
235				button.setAttribute(
236					'aria-expanded',
237					isOpen ? 'true' : 'false'
238				);
239
240				button.setAttribute(
241					'aria-label',
242					`${isOpen ? 'Close' : 'Open'} submenu for ${labelText}`
243				);
244
245				if (isOpen) {
246					requestAnimationFrame(function () {
247						positionSubmenu(item);
248					});
249				} else {
250					resetSubmenuPosition(submenu);
251				}
252			});
253		});
254
255
256		/*
257		 * Escape closes the nearest open submenu.
258		 */
259		nav.addEventListener('keydown', function (event) {
260			if (event.key !== 'Escape') {
261				return;
262			}
263
264			const openItem =
265				event.target.closest('.submenu-open');
266
267			if (openItem && nav.contains(openItem)) {
268				event.preventDefault();
269
270				closeSubmenu(openItem, true);
271
272				return;
273			}
274
275			if (nav.classList.contains('main-nav-open')) {
276				event.preventDefault();
277
278				closeMainNav(true);
279			}
280		});
281
282
283		/*
284		 * Clicking outside the navigation closes open menus.
285		 */
286		document.addEventListener('click', function (event) {
287			if (
288				nav.contains(event.target) ||
289				navToggle.contains(event.target)
290			) {
291				return;
292			}
293
294			nav
295				.querySelectorAll('.submenu-open')
296				.forEach(function (item) {
297					closeSubmenu(item);
298				});
299		});
300
301
302		/*
303		 * Recalculate open dropdown positioning after resize.
304		 */
305		window.addEventListener('resize', function () {
306			nav
307				.querySelectorAll(
308					'.menu > .submenu-open'
309				)
310				.forEach(function (item) {
311					positionSubmenu(item);
312				});
313		});
314	}
315
316
317	if (document.readyState === 'loading') {
318		document.addEventListener(
319			'DOMContentLoaded',
320			initMainNav
321		);
322	} else {
323		initMainNav();
324	}
325})();

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.