PageSourceSearch

https://www.tarleton.edu/oiss/wp-content/themes/tarleton_child_the…/js/header-navigation.js?ver=1790626360

js tarleton.edu collected 2026-10-01 10:43:46 UTC 15,529 bytes, 623 lines download raw bytes

1(function ($, window, document) {
2	'use strict';
3
4	$(function () {
5		var $masthead = $('#masthead');
6
7		if (!$masthead.length) {
8			return;
9		}
10
11		var $followWrap = $masthead.parent('.followWrap');
12		var $navWrapper = $('#primary-nav-wrapper');
13		var $toggle = $('#nav-toggle');
14		var $nav = $('#site-navigation');
15
16		if (
17			!$followWrap.length ||
18			!$navWrapper.length ||
19			!$toggle.length ||
20			!$nav.length
21		) {
22			return;
23		}
24
25		var MOBILE_BREAKPOINT = 1140;
26		var BRANDING_MIN_WIDTH = 270;
27		var rafId = 0;
28
29		function getStableTitleWidth() {
30			var titleEl = $masthead.find('.site-text-wrapper .site-title').get(0);
31
32			if (!titleEl) {
33				return null;
34			}
35
36			var hadCollapseClass = $masthead.hasClass('header-needs-collapse');
37			var navWasHidden = $nav.is('[hidden]');
38			var navClass = $nav.attr('class') || '';
39			var toggleExpanded = $toggle.attr('aria-expanded');
40
41			$masthead.removeClass('header-needs-collapse');
42			$nav.removeAttr('hidden');
43			$nav.removeClass('is-open is-collapsed');
44
45			var width = titleEl.getBoundingClientRect().width;
46
47			if (hadCollapseClass) {
48				$masthead.addClass('header-needs-collapse');
49			}
50
51			$nav.attr('class', navClass);
52
53			if (navWasHidden) {
54				$nav.attr('hidden', 'hidden');
55			} else {
56				$nav.removeAttr('hidden');
57			}
58
59			if (typeof toggleExpanded !== 'undefined') {
60				$toggle.attr('aria-expanded', toggleExpanded);
61			}
62
63			return Math.ceil(width);
64		}
65
66		function shouldCollapseEarly() {
67			if (window.innerWidth <= MOBILE_BREAKPOINT) {
68				return true;
69			}
70
71			var titleWidth = getStableTitleWidth();
72
73			if (titleWidth === null) {
74				return false;
75			}
76
77			return titleWidth <= BRANDING_MIN_WIDTH;
78		}
79
80		function isCollapsedView() {
81			return shouldCollapseEarly();
82		}
83
84		function isDesktopView() {
85			return window.innerWidth >= 1141 && !isCollapsedView();
86		}
87
88		function resetDesktopSubmenuDirections() {
89			$nav.find('li.submenu-open-left').removeClass('submenu-open-left');
90		}
91
92		function positionDesktopSubmenu($item) {
93			if (!isDesktopView()) {
94				return;
95			}
96
97			var $submenu = $item.children('.sub-menu');
98
99			if (!$submenu.length) {
100				return;
101			}
102
103			$item.removeClass('submenu-open-left');
104
105			var submenuEl = $submenu.get(0);
106			if (!submenuEl) {
107				return;
108			}
109
110			var previousVisibility = submenuEl.style.visibility;
111			var previousOpacity = submenuEl.style.opacity;
112			var previousPointerEvents = submenuEl.style.pointerEvents;
113			var previousDisplay = submenuEl.style.display;
114
115			submenuEl.style.visibility = 'hidden';
116			submenuEl.style.opacity = '0';
117			submenuEl.style.pointerEvents = 'none';
118			submenuEl.style.display = 'block';
119
120			var itemRect = $item.get(0).getBoundingClientRect();
121			var submenuRect = submenuEl.getBoundingClientRect();
122			var viewportWidth = document.documentElement.clientWidth;
123			var gutter = 8;
124
125			var isTopLevel = $item.parent('.onepress-menu').length > 0;
126
127			if (isTopLevel) {
128				var projectedRight = itemRect.left + submenuRect.width;
129
130				if (projectedRight > viewportWidth - gutter) {
131					$item.addClass('submenu-open-left');
132				}
133			} else {
134				var projectedNestedRight = itemRect.right + submenuRect.width;
135				var projectedNestedLeft = itemRect.left - submenuRect.width;
136
137				if (
138					projectedNestedRight > viewportWidth - gutter &&
139					projectedNestedLeft >= gutter
140				) {
141					$item.addClass('submenu-open-left');
142				}
143			}
144
145			submenuEl.style.visibility = previousVisibility;
146			submenuEl.style.opacity = previousOpacity;
147			submenuEl.style.pointerEvents = previousPointerEvents;
148			submenuEl.style.display = previousDisplay;
149		}
150
151		function refreshDesktopSubmenuDirections() {
152			if (!isDesktopView()) {
153				resetDesktopSubmenuDirections();
154				return;
155			}
156
157			$nav.find('li.menu-item-has-children').each(function () {
158				positionDesktopSubmenu($(this));
159			});
160		}
161
162		function syncFollowWrapHeight() {
163			window.cancelAnimationFrame(rafId);
164
165			rafId = window.requestAnimationFrame(function () {
166				var mastheadEl = $masthead.get(0);
167				var wrapEl = $followWrap.get(0);
168
169				if (!mastheadEl || !wrapEl) {
170					return;
171				}
172
173				var height = Math.ceil(mastheadEl.getBoundingClientRect().height);
174
175				if (height > 0) {
176					wrapEl.style.height = height + 'px';
177					wrapEl.style.setProperty('--masthead-sticky-height', height + 'px');
178					document.documentElement.style.setProperty('--masthead-sticky-height', height + 'px');
179				}
180
181				syncNavViewportMetrics();
182			});
183		}
184
185		function getBrandbarHeight() {
186			var brandbar = document.getElementById('brandbar-tsu');
187
188			if (!brandbar) {
189				return 0;
190			}
191
192			return Math.ceil(brandbar.getBoundingClientRect().height);
193		}
194
195		function getVisibleViewportHeight() {
196			if (window.visualViewport && window.visualViewport.height) {
197				return Math.ceil(window.visualViewport.height);
198			}
199
200			return Math.ceil(window.innerHeight || document.documentElement.clientHeight || 0);
201		}
202
203		function syncNavViewportMetrics() {
204			window.requestAnimationFrame(function () {
205				var mastheadEl = $masthead.get(0);
206
207				if (!mastheadEl) {
208					return;
209				}
210
211				var root = document.documentElement;
212				var mastheadHeight = Math.ceil(mastheadEl.getBoundingClientRect().height);
213				var brandbarHeight = getBrandbarHeight();
214				var visibleViewportHeight = getVisibleViewportHeight();
215				var navGap = 16; // 1rem breathing room
216				var panelMaxHeight = Math.max(
217					160,
218					visibleViewportHeight - brandbarHeight - mastheadHeight - navGap
219				);
220
221				root.style.setProperty('--brandbar-height', brandbarHeight + 'px');
222				root.style.setProperty('--nav-visible-viewport-height', visibleViewportHeight + 'px');
223				root.style.setProperty('--nav-panel-max-height', panelMaxHeight + 'px');
224				root.style.setProperty('--masthead-sticky-height', mastheadHeight + 'px');
225
226				if ($followWrap.length) {
227					$followWrap.get(0).style.setProperty('--masthead-sticky-height', mastheadHeight + 'px');
228				}
229			});
230		}
231
232		function setToggleState(isOpen) {
233			$toggle.attr('aria-expanded', isOpen ? 'true' : 'false');
234			$toggle.attr(
235				'aria-label',
236				isOpen
237					? 'Close primary navigation menu'
238					: 'Open primary navigation menu'
239			);
240
241			$nav.toggleClass('is-open', isOpen);
242			$nav.toggleClass('is-collapsed', !isOpen);
243
244			if (isOpen) {
245				$nav.removeAttr('hidden');
246			} else {
247				$nav.attr('hidden', 'hidden');
248				closeAllSubmenus();
249			}
250
251			syncFollowWrapHeight();
252		}
253
254		function closeMenu(restoreFocus) {
255			if (!isCollapsedView()) {
256				return;
257			}
258
259			setToggleState(false);
260
261			if (restoreFocus) {
262				$toggle.trigger('focus');
263			}
264		}
265
266		function toggleMenu() {
267			if (!isCollapsedView()) {
268				return;
269			}
270
271			var isOpen = $toggle.attr('aria-expanded') === 'true';
272			setToggleState(!isOpen);
273		}
274
275		function syncForViewport() {
276			var collapsed = isCollapsedView();
277
278			$masthead.toggleClass('header-needs-collapse', collapsed);
279
280			if (collapsed) {
281				var isOpen = $toggle.attr('aria-expanded') === 'true';
282				setToggleState(isOpen);
283			} else {
284				$toggle.attr('aria-expanded', 'false');
285				$toggle.attr('aria-label', 'Open primary navigation menu');
286				$nav.removeClass('is-open is-collapsed');
287				$nav.removeAttr('hidden');
288				closeAllSubmenus(true);
289			}
290
291			setupSubmenuArrows();
292			syncFollowWrapHeight();
293			refreshDesktopSubmenuDirections();
294		}
295
296		function deferredSync() {
297			window.setTimeout(function () {
298				syncForViewport();
299				syncFollowWrapHeight();
300			}, 0);
301		}
302
303		function setupSubmenuArrows() {
304			$nav.find('.nav-toggle-subarrow').each(function (index) {
305				var $arrow = $(this);
306				var $item = $arrow.parent('li');
307				var $submenu = $item.children('.sub-menu');
308				var $link = $item.children('a').first();
309				var submenuId = $submenu.attr('id');
310				var labelText = $.trim($link.text());
311
312				if (!$item.length || !$submenu.length) {
313					return;
314				}
315
316				if (!submenuId) {
317					submenuId = 'submenu-' + index;
318					$submenu.attr('id', submenuId);
319				}
320
321				$arrow.attr({
322					tabindex: '0',
323					role: 'button',
324					'aria-expanded': $item.hasClass('nav-toggle-dropdown') ? 'true' : 'false',
325					'aria-controls': submenuId,
326					'aria-label': labelText
327						? 'Toggle ' + labelText + ' submenu'
328						: 'Toggle submenu'
329				});
330			});
331		}
332
333		function closeSubmenu($item) {
334			var $arrow = $item.children('.nav-toggle-subarrow');
335
336			$item.removeClass('nav-toggle-dropdown');
337			$arrow.attr('aria-expanded', 'false');
338
339			$item
340				.find('> .sub-menu li.nav-toggle-dropdown')
341				.removeClass('nav-toggle-dropdown');
342			$item
343				.find('> .sub-menu .nav-toggle-subarrow[aria-expanded="true"]')
344				.attr('aria-expanded', 'false');
345
346			syncFollowWrapHeight();
347		}
348
349		function openSubmenu($item) {
350			var $submenu = $item.children('.sub-menu');
351			var $arrow = $item.children('.nav-toggle-subarrow');
352
353			if (!$submenu.length) {
354				return;
355			}
356
357			$item
358				.siblings('.nav-toggle-dropdown')
359				.each(function () {
360					closeSubmenu($(this));
361				});
362
363			$item.addClass('nav-toggle-dropdown');
364			$arrow.attr('aria-expanded', 'true');
365
366			syncFollowWrapHeight();
367		}
368
369		function toggleSubmenu($item) {
370			if ($item.hasClass('nav-toggle-dropdown')) {
371				closeSubmenu($item);
372			} else {
373				openSubmenu($item);
374			}
375		}
376
377		function closeAllSubmenus(forceDesktopCleanup) {
378			$nav.find('li.nav-toggle-dropdown').removeClass('nav-toggle-dropdown');
379			$nav.find('.nav-toggle-subarrow').attr('aria-expanded', 'false');
380
381			if (forceDesktopCleanup) {
382				$nav.find('.sub-menu').removeAttr('style');
383			}
384
385			syncFollowWrapHeight();
386		}
387
388		$(window).on('load resize orientationchange', function () {
389			syncForViewport();
390			setupSubmenuArrows();
391			deferredSync();
392			syncNavViewportMetrics();
393		});
394
395		$(document).on('hero_ready header_view_changed', function () {
396			syncForViewport();
397			setupSubmenuArrows();
398			deferredSync();
399			syncNavViewportMetrics();
400		});
401
402		if ('MutationObserver' in window) {
403			var wrapObserver = new MutationObserver(function (mutations) {
404				for (var i = 0; i < mutations.length; i++) {
405					if (
406						mutations[i].type === 'attributes' &&
407						mutations[i].attributeName === 'style'
408					) {
409						deferredSync();
410						break;
411					}
412				}
413			});
414
415			wrapObserver.observe($followWrap.get(0), {
416				attributes: true,
417				attributeFilter: ['style']
418			});
419		}
420
421		if ('ResizeObserver' in window) {
422			var mastheadObserver = new ResizeObserver(function () {
423				syncForViewport();
424				syncFollowWrapHeight();
425				syncNavViewportMetrics();
426			});
427
428			mastheadObserver.observe($masthead.get(0));
429		}
430
431		if (window.visualViewport) {
432			window.visualViewport.addEventListener('resize', function () {
433				syncForViewport();
434				syncFollowWrapHeight();
435				syncNavViewportMetrics();
436			});
437
438			window.visualViewport.addEventListener('scroll', function () {
439				syncNavViewportMetrics();
440			});
441		}
442
443		/*
444		 * Hamburger toggle
445		 */
446		$toggle.on('click', function (event) {
447			event.preventDefault();
448			toggleMenu();
449		});
450
451		$toggle.on('keydown', function (event) {
452			if (!isCollapsedView()) {
453				return;
454			}
455
456			if (event.key === 'Enter' || event.key === ' ') {
457				event.preventDefault();
458				toggleMenu();
459			}
460		});
461
462		/*
463		 * Close whole collapsed nav when focus leaves the nav wrapper.
464		 */
465		$navWrapper.on('focusout', function () {
466			window.setTimeout(function () {
467				var activeElement = document.activeElement;
468
469				if (!$navWrapper.get(0).contains(activeElement)) {
470					closeAllSubmenus(false);
471
472					if (isCollapsedView()) {
473						closeMenu(false);
474					}
475				}
476			}, 0);
477		});
478
479		/*
480		 * ESC closes the whole collapsed nav, unless handled by a submenu arrow.
481		 */
482		$navWrapper.on('keydown', function (event) {
483			if (!isCollapsedView()) {
484				return;
485			}
486
487			if (
488				event.key === 'Escape' &&
489				!$(event.target).hasClass('nav-toggle-subarrow')
490			) {
491				event.preventDefault();
492				closeMenu(true);
493			}
494		});
495
496		$(document).on('click', function (event) {
497			var target = event.target;
498
499			if (!$navWrapper.get(0).contains(target)) {
500				closeAllSubmenus(false);
501
502				if (isCollapsedView()) {
503					closeMenu(false);
504				}
505			}
506		});
507
508		/*
509		 * Make submenu arrows reliably focusable before click so wrapper focusout
510		 * logic does not close the whole nav unexpectedly.
511		 */
512		$nav.on('mousedown', '.nav-toggle-subarrow', function (event) {
513			if (!isCollapsedView()) {
514				return;
515			}
516
517			event.preventDefault();
518			event.stopPropagation();
519
520			$(this).trigger('focus');
521		});
522
523		/*
524		 * Collapsed/mobile submenu toggle via subarrow click/tap.
525		 * Parent links remain links; only the arrow toggles submenu state.
526		 */
527		$nav.on('click', '.nav-toggle-subarrow', function (event) {
528			event.preventDefault();
529			event.stopPropagation();
530
531			var $arrow = $(this);
532			var $item = $arrow.parent('li');
533			var $submenu = $item.children('.sub-menu');
534
535			if (!$item.length || !$submenu.length) {
536				return;
537			}
538
539			toggleSubmenu($item);
540		});
541
542		/*
543		 * Keyboard support for collapsed/mobile submenu arrows.
544		 * - Enter / Space toggles the submenu
545		 * - Escape collapses the submenu and returns focus to the arrow
546		 */
547		$nav.on('keydown', '.nav-toggle-subarrow', function (event) {
548			var $arrow = $(this);
549			var $item = $arrow.parent('li');
550			var $submenu = $item.children('.sub-menu');
551
552			if (!$item.length || !$submenu.length) {
553				return;
554			}
555
556			if (event.key === 'Enter' || event.key === ' ') {
557				event.preventDefault();
558				event.stopPropagation();
559				toggleSubmenu($item);
560				return;
561			}
562
563			if (event.key === 'Escape') {
564				event.preventDefault();
565				event.stopPropagation();
566				closeSubmenu($item);
567				$arrow.trigger('focus');
568			}
569		});
570
571		/*
572		 * Shift+Tab from the first submenu focusable returns to the submenu arrow,
573		 * so keyboard users can collapse it with Enter/Space or Escape.
574		 */
575		$nav.on('keydown', '.sub-menu a, .sub-menu button, .sub-menu [tabindex]:not([tabindex="-1"])', function (event) {
576			if (!isCollapsedView()) {
577				return;
578			}
579
580			if (event.key !== 'Tab' || !event.shiftKey) {
581				return;
582			}
583
584			var $current = $(this);
585			var $submenu = $current.closest('.sub-menu');
586			var $parentItem = $submenu.parent('li');
587			var $arrow = $parentItem.children('.nav-toggle-subarrow');
588
589			if (!$arrow.length) {
590				return;
591			}
592
593			var $focusables = $submenu
594				.find('a, button, [tabindex]:not([tabindex="-1"])')
595				.filter(':visible');
596
597			if ($focusables.get(0) === this) {
598				event.preventDefault();
599				$arrow.trigger('focus');
600			}
601		});
602
603		$nav.on('mouseenter focusin', 'li.menu-item-has-children', function () {
604			if (!isDesktopView()) {
605				return;
606			}
607
608			positionDesktopSubmenu($(this));
609		});
610
611		$(window).on('load resize orientationchange', function () {
612			refreshDesktopSubmenuDirections();
613		});
614
615		$(document).on('hero_ready header_view_changed', function () {
616			refreshDesktopSubmenuDirections();
617		});
618
619		setupSubmenuArrows();
620		syncForViewport();
621		syncFollowWrapHeight();
622	});
623})(jQuery, window, document);

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.