PageSourceSearch

https://arizer.ca/wp-content/themes/arizer/js/touch-keyboard-navigation.js?ver=1.1

js arizer.ca collected 2026-09-30 16:50:37 UTC 9,470 bytes, 359 lines download raw bytes

1/**
2 * Touch & Keyboard navigation.
3 *
4 * Contains handlers for touch devices and keyboard navigation.
5 */
6
7(function() {
8
9	/**
10	 * Debounce
11	 *
12	 * @param {Function} func
13	 * @param {number} wait
14	 * @param {boolean} immediate
15	 */
16	function debounce(func, wait, immediate) {
17		'use strict';
18
19		var timeout;
20		wait      = (typeof wait !== 'undefined') ? wait : 20;
21		immediate = (typeof immediate !== 'undefined') ? immediate : true;
22
23		return function() {
24
25			var context = this, args = arguments;
26			var later = function() {
27				timeout = null;
28
29				if (!immediate) {
30					func.apply(context, args);
31				}
32			};
33
34			var callNow = immediate && !timeout;
35
36			clearTimeout(timeout);
37			timeout = setTimeout(later, wait);
38
39			if (callNow) {
40				func.apply(context, args);
41			}
42		};
43	}
44
45	/**
46	 * Add class
47	 *
48	 * @param {Object} el
49	 * @param {string} cls
50	 */
51	function addClass(el, cls) {
52		if ( ! el.className.match( '(?:^|\\s)' + cls + '(?!\\S)') ) {
53			el.className += ' ' + cls;
54		}
55	}
56
57	/**
58	 * Delete class
59	 *
60	 * @param {Object} el
61	 * @param {string} cls
62	 */
63	function deleteClass(el, cls) {
64		el.className = el.className.replace( new RegExp( '(?:^|\\s)' + cls + '(?!\\S)' ),'' );
65	}
66
67	/**
68	 * Has class?
69	 *
70	 * @param {Object} el
71	 * @param {string} cls
72	 *
73	 * @returns {boolean} Has class
74	 */
75	function hasClass(el, cls) {
76
77		if ( el.className.match( '(?:^|\\s)' + cls + '(?!\\S)' ) ) {
78			return true;
79		}
80	}
81
82	/**
83	 * Toggle Aria Expanded state for screenreaders
84	 *
85	 * @param {Object} ariaItem
86	 */
87	function toggleAriaExpandedState( ariaItem ) {
88		'use strict';
89
90		var ariaState = ariaItem.getAttribute('aria-expanded');
91
92		if ( ariaState === 'true' ) {
93			ariaState = 'false';
94		} else {
95			ariaState = 'true';
96		}
97
98		ariaItem.setAttribute('aria-expanded', ariaState);
99	}
100
101	/**
102	 * Open sub-menu
103	 *
104	 * @param {Object} currentSubMenu
105	 */
106	function openSubMenu( currentSubMenu ) {
107		'use strict';
108
109		// Update classes
110		// classList.add is not supported in IE11
111		currentSubMenu.parentElement.className += ' off-canvas';
112		currentSubMenu.parentElement.lastElementChild.className += ' expanded-true';
113
114		// Update aria-expanded state
115		toggleAriaExpandedState( currentSubMenu );
116	}
117
118	/**
119	 * Close sub-menu
120	 *
121	 * @param {Object} currentSubMenu
122	 */
123	function closeSubMenu( currentSubMenu ) {
124		'use strict';
125
126		var menuItem     = getCurrentParent( currentSubMenu, '.menu-item' ); // this.parentNode
127		var menuItemAria = menuItem.querySelector('a[aria-expanded]');
128		var subMenu      = currentSubMenu.closest('.sub-menu');
129
130		// If this is in a sub-sub-menu, go back to parent sub-menu
131		if ( getCurrentParent( currentSubMenu, 'ul' ).classList.contains( 'sub-menu' ) ) {
132
133			// Update classes
134			// classList.remove is not supported in IE11
135			menuItem.className = menuItem.className.replace( 'off-canvas', '' );
136			subMenu.className  = subMenu.className.replace( 'expanded-true', '' );
137
138			// Update aria-expanded and :focus states
139			toggleAriaExpandedState( menuItemAria );
140
141		// Or else close all sub-menus
142		} else {
143
144			// Update classes
145			// classList.remove is not supported in IE11
146			menuItem.className = menuItem.className.replace( 'off-canvas', '' );
147			menuItem.lastElementChild.className = menuItem.lastElementChild.className.replace( 'expanded-true', '' );
148
149			// Update aria-expanded and :focus states
150			toggleAriaExpandedState( menuItemAria );
151		}
152	}
153
154	/**
155	 * Find first ancestor of an element by selector
156	 *
157	 * @param {Object} child
158	 * @param {String} selector
159	 * @param {String} stopSelector
160	 */
161	function getCurrentParent( child, selector, stopSelector ) {
162
163		var currentParent = null;
164
165		while ( child ) {
166
167			if ( child.matches(selector) ) {
168
169				currentParent = child;
170				break;
171
172			} else if ( stopSelector && child.matches(stopSelector) ) {
173
174				break;
175			}
176
177			child = child.parentElement;
178		}
179
180		return currentParent;
181	}
182
183	/**
184	 * Remove all off-canvas states
185	 */
186	function removeAllFocusStates() {
187		'use strict';
188
189		var siteBranding            = document.getElementsByClassName( 'site-branding' )[0];
190
191		if ( ! siteBranding ) {
192			return;
193		}
194
195		var getFocusedElements      = siteBranding.querySelectorAll(':hover, :focus, :focus-within');
196		var getFocusedClassElements = siteBranding.querySelectorAll('.is-focused');
197		var i;
198		var o;
199
200		for ( i = 0; i < getFocusedElements.length; i++) {
201			getFocusedElements[i].blur();
202		}
203
204		for ( o = 0; o < getFocusedClassElements.length; o++) {
205			deleteClass( getFocusedClassElements[o], 'is-focused' );
206		}
207	}
208
209	/**
210	 * Matches polyfill for IE11
211	 */
212	if (!Element.prototype.matches) {
213		Element.prototype.matches = Element.prototype.msMatchesSelector;
214	}
215
216	/**
217	 * Toggle `focus` class to allow sub-menu access on touch screens.
218	 */
219	function toggleSubmenuDisplay() {
220
221		document.addEventListener('touchstart', function(event) {
222
223			if ( event.target.matches('a') ) {
224
225				var url = event.target.getAttribute( 'href' ) ? event.target.getAttribute( 'href' ) : '';
226
227				// Open submenu if url is #
228				if ( '#' === url && event.target.nextSibling.matches('.submenu-expand') ) {
229					openSubMenu( event.target );
230				}
231			}
232
233			// Check if .submenu-expand is touched
234			if ( event.target.matches('.submenu-expand') ) {
235				openSubMenu(event.target);
236
237			// Check if child of .submenu-expand is touched
238			} else if ( null != getCurrentParent( event.target, '.submenu-expand' ) &&
239								getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) ) {
240				openSubMenu( getCurrentParent( event.target, '.submenu-expand' ) );
241
242			// Check if .menu-item-link-return is touched
243			} else if ( event.target.matches('.menu-item-link-return') ) {
244				closeSubMenu( event.target );
245
246			// Check if child of .menu-item-link-return is touched
247			} else if ( null != getCurrentParent( event.target, '.menu-item-link-return' ) && getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) {
248				closeSubMenu( event.target );
249			}
250
251			// Prevent default mouse/focus events
252			removeAllFocusStates();
253
254		}, false);
255
256		document.addEventListener('touchend', function(event) {
257
258			var mainNav = getCurrentParent( event.target, '.main-navigation' );
259
260			if ( null != mainNav && hasClass( mainNav, '.main-navigation' ) ) {
261				// Prevent default mouse events
262				event.preventDefault();
263
264			} else if (
265				event.target.matches('.submenu-expand') ||
266				null != getCurrentParent( event.target, '.submenu-expand' ) &&
267				getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) ||
268				event.target.matches('.menu-item-link-return') ||
269				null != getCurrentParent( event.target, '.menu-item-link-return' ) &&
270				getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) {
271					// Prevent default mouse events
272					event.preventDefault();
273			}
274
275			// Prevent default mouse/focus events
276			removeAllFocusStates();
277
278		}, false);
279
280		document.addEventListener('focus', function(event) {
281
282			if ( event.target.matches('.main-navigation > div > ul > li a') ) {
283
284				// Remove Focused elements in sibling div
285				var currentDiv        = getCurrentParent( event.target, 'div', '.main-navigation' );
286				var currentDivSibling = currentDiv.previousElementSibling === null ? currentDiv.nextElementSibling : currentDiv.previousElementSibling;
287				var focusedElement    = currentDivSibling.querySelector( '.is-focused' );
288				var focusedClass      = 'is-focused';
289				var prevLi            = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).previousElementSibling;
290				var nextLi            = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).nextElementSibling;
291
292				if ( null !== focusedElement && null !== hasClass( focusedElement, focusedClass ) ) {
293					deleteClass( focusedElement, focusedClass );
294				}
295
296				// Add .is-focused class to top-level li
297				if ( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ) ) {
298					addClass( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ), focusedClass );
299				}
300
301				// Check for previous li
302				if ( prevLi && hasClass( prevLi, focusedClass ) ) {
303					deleteClass( prevLi, focusedClass );
304				}
305
306				// Check for next li
307				if ( nextLi && hasClass( nextLi, focusedClass ) ) {
308					deleteClass( nextLi, focusedClass );
309				}
310			}
311
312		}, true);
313
314		document.addEventListener('click', function(event) {
315
316			// Remove all focused menu states when clicking outside site branding
317			if ( event.target !== document.getElementsByClassName( 'site-branding' )[0] ) {
318				removeAllFocusStates();
319			} else {
320				// nothing
321			}
322
323		}, false);
324	}
325
326	/**
327	 * Run our sub-menu function as soon as the document is `ready`
328	 */
329	document.addEventListener( 'DOMContentLoaded', function() {
330		toggleSubmenuDisplay();
331	});
332
333	/**
334	 * Run our sub-menu function on selective refresh in the customizer
335	 */
336	document.addEventListener( 'customize-preview-menu-refreshed', function( e, params ) {
337		if ( 'menu-1' === params.wpNavMenuArgs.theme_location ) {
338			toggleSubmenuDisplay();
339		}
340	});
341
342	/**
343	 * Run our sub-menu function every time the window resizes
344	 */
345	var isResizing = false;
346	window.addEventListener( 'resize', function() {
347		isResizing = true;
348		debounce( function() {
349			if ( isResizing ) {
350				return;
351			}
352
353			toggleSubmenuDisplay();
354			isResizing = false;
355
356		}, 150 );
357	} );
358
359})();

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.