PageSourceSearch

https://891khol.org/wp-content/plugins/khol-functionality/assets/js/nav-dropdown.js?ver=1788226782

js 891khol.org collected 2026-10-02 02:32:13 UTC 4,118 bytes, 146 lines download raw bytes

1/**
2 * Topics-bar dropdowns.
3 *
4 * CSS already opens a submenu on hover and on keyboard focus, so this script
5 * only covers what CSS cannot:
6 *
7 * - Escape closes an open menu and returns focus to its parent.
8 * - Touch devices have no hover, and the parent items are real links, so a first
9 *   tap would navigate away before the submenu could be seen. The first tap
10 *   opens; a second follows the link.
11 * - aria-expanded, kept in step with whatever opened the menu.
12 *
13 * Nothing here is required for the menu to work with a mouse or a keyboard --
14 * if this file fails to load the dropdowns still open.
15 */
16( function () {
17	'use strict';
18
19	var menu = document.querySelector( '.highlight-menu' );
20	if ( ! menu ) {
21		return;
22	}
23
24	var parents = [].slice.call( menu.querySelectorAll( '.menu-item-has-children' ) );
25	if ( ! parents.length ) {
26		return;
27	}
28
29	// Touch is detected from the event itself rather than sniffed up front, so a
30	// hybrid laptop behaves correctly with whichever input is actually used.
31	var openedByTouch = null;
32
33	function link( item ) {
34		return item.querySelector( 'a' );
35	}
36
37	function close( item ) {
38		item.classList.remove( 'is-open' );
39		var a = link( item );
40		if ( a ) {
41			a.setAttribute( 'aria-expanded', 'false' );
42		}
43		if ( openedByTouch === item ) {
44			openedByTouch = null;
45		}
46	}
47
48	// Escape has to beat CSS. The menu opens on :focus-within, and Escape is
49	// supposed to return focus to the parent item -- which would satisfy
50	// :focus-within and reopen it immediately. This flag suppresses the panel
51	// while focus deliberately rests on a dismissed parent, and is cleared as
52	// soon as the user moves on.
53	function undismiss( item ) {
54		item.classList.remove( 'is-dismissed' );
55	}
56
57	function closeAll( except ) {
58		parents.forEach( function ( item ) {
59			if ( item !== except ) {
60				close( item );
61			}
62		} );
63	}
64
65	parents.forEach( function ( item ) {
66		var a = link( item );
67		if ( ! a ) {
68			return;
69		}
70
71		a.setAttribute( 'aria-haspopup', 'true' );
72		a.setAttribute( 'aria-expanded', 'false' );
73
74		a.addEventListener( 'click', function ( event ) {
75			// Only intercept touch. A mouse user has already seen the menu on
76			// hover, and a keyboard user reaches the children by tabbing.
77			if ( ! event.pointerType || 'touch' !== event.pointerType ) {
78				return;
79			}
80
81			if ( openedByTouch === item ) {
82				return; // Second tap: let the link do its job.
83			}
84
85			event.preventDefault();
86			closeAll( item );
87			item.classList.add( 'is-open' );
88			a.setAttribute( 'aria-expanded', 'true' );
89			openedByTouch = item;
90		} );
91
92		// Mirror the hover state into aria so assistive tech is not told the
93		// menu is closed while it is plainly open.
94		item.addEventListener( 'mouseenter', function () {
95			undismiss( item );
96			a.setAttribute( 'aria-expanded', 'true' );
97		} );
98		item.addEventListener( 'mouseleave', function () {
99			// Not while the item still holds focus: the menu is genuinely open
100			// for a keyboard user, and saying otherwise would be a lie to AT.
101			if ( openedByTouch !== item && ! item.contains( document.activeElement ) ) {
102				a.setAttribute( 'aria-expanded', 'false' );
103			}
104		} );
105		item.addEventListener( 'focusin', function () {
106			a.setAttribute( 'aria-expanded', 'true' );
107		} );
108		item.addEventListener( 'focusout', function ( event ) {
109			if ( ! item.contains( event.relatedTarget ) ) {
110				close( item );
111				undismiss( item );
112			}
113		} );
114	} );
115
116	document.addEventListener( 'keydown', function ( event ) {
117		if ( 'Escape' !== event.key ) {
118			return;
119		}
120
121		parents.forEach( function ( item ) {
122			var holdsFocus = item.contains( document.activeElement );
123			if ( ! holdsFocus && ! item.classList.contains( 'is-open' ) ) {
124				return;
125			}
126
127			var a = link( item );
128			close( item );
129
130			if ( holdsFocus ) {
131				// Dismissed but still focused, so the panel must stay shut
132				// until focus moves.
133				item.classList.add( 'is-dismissed' );
134				if ( a ) {
135					a.focus();
136				}
137			}
138		} );
139	} );
140
141	document.addEventListener( 'click', function ( event ) {
142		if ( ! menu.contains( event.target ) ) {
143			closeAll( null );
144		}
145	} );
146}() );

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.