PageSourceSearch

https://treefortmusicfest.com/wp-content/themes/tmf27/assets/js/navigation.js?ver=1789148835.1

js treefortmusicfest.com collected 2026-09-24 10:15:01 UTC 9,151 bytes, 276 lines download raw bytes

1/**
2 * Site navigation: full-screen overlay toggle + submenu flyouts.
3 *
4 * Markup: header.php. Styles: assets/css/header.css.
5 * Vanilla JS, no dependencies. Progressive enhancement: without JS the overlay
6 * stays hidden via the [hidden] attribute and the bar still renders.
7 */
8( function () {
9    'use strict';
10
11    var toggle = document.querySelector( '.tmf-nav-toggle' );
12    var overlay = document.getElementById( 'tmf-nav-overlay' );
13
14    if ( ! toggle || ! overlay ) {
15        return;
16    }
17
18    var closeBtn = overlay.querySelector( '.tmf-nav-close' );
19    var lastFocused = null;
20
21    function clearSubmenuLayout( subMenu ) {
22        subMenu.style.paddingTop = '';
23        subMenu.style.height = '';
24    }
25
26    // Column B's position, height, and padding are now fully CSS-driven (fixed to
27    // the top of the screen, 100vh tall — see header.css). This just strips any
28    // stale inline styles a previous version may have left so the CSS wins.
29    function layoutSubmenu( li ) {
30        var subMenu = li.querySelector( ':scope > .sub-menu' );
31        if ( subMenu ) {
32            clearSubmenuLayout( subMenu );
33        }
34    }
35
36    function focusable() {
37        return Array.prototype.slice.call(
38            overlay.querySelectorAll(
39                'a[href], button:not([disabled]), input, [tabindex]:not([tabindex="-1"])'
40            )
41        ).filter( function ( el ) {
42            return el.offsetParent !== null;
43        } );
44    }
45
46    function openNav() {
47        lastFocused = document.activeElement;
48        overlay.hidden = false;
49        // Force reflow so the opacity transition runs after display change.
50        void overlay.offsetWidth;
51        overlay.classList.add( 'is-open' );
52        toggle.setAttribute( 'aria-expanded', 'true' );
53        document.body.style.overflow = 'hidden';
54        ( closeBtn || overlay ).focus();
55    }
56
57    function closeNav() {
58        overlay.classList.remove( 'is-open' );
59        toggle.setAttribute( 'aria-expanded', 'false' );
60        document.body.style.overflow = '';
61        closeOpenSubmenus();
62
63        var onEnd = function () {
64            overlay.hidden = true;
65            overlay.removeEventListener( 'transitionend', onEnd );
66        };
67        overlay.addEventListener( 'transitionend', onEnd );
68
69        if ( lastFocused ) {
70            lastFocused.focus();
71        }
72    }
73
74    function closeOpenSubmenus() {
75        var open = overlay.querySelectorAll( '.menu-item-has-children.is-open' );
76        Array.prototype.forEach.call( open, function ( li ) {
77            li.classList.remove( 'is-open' );
78            var a = li.querySelector( ':scope > a' );
79            if ( a ) {
80                a.setAttribute( 'aria-expanded', 'false' );
81            }
82            var subMenu = li.querySelector( ':scope > .sub-menu' );
83            if ( subMenu ) {
84                clearSubmenuLayout( subMenu );
85            }
86        } );
87    }
88
89    // Open / close.
90    toggle.addEventListener( 'click', openNav );
91    if ( closeBtn ) {
92        closeBtn.addEventListener( 'click', closeNav );
93    }
94
95    // Click the scrim (the dimmed area outside the 480px panel) to close. The
96    // panel and its contents are descendants, so e.target === overlay is true
97    // ONLY for the backdrop itself — clicks inside the menu don't bubble a match.
98    overlay.addEventListener( 'click', function ( e ) {
99        if ( e.target === overlay ) {
100            closeNav();
101        }
102    } );
103
104    // ESC closes; Tab is trapped inside the overlay while open.
105    document.addEventListener( 'keydown', function ( e ) {
106        if ( overlay.hidden ) {
107            return;
108        }
109        if ( e.key === 'Escape' ) {
110            closeNav();
111            return;
112        }
113        if ( e.key === 'Tab' ) {
114            var items = focusable();
115            if ( ! items.length ) {
116                return;
117            }
118            var first = items[ 0 ];
119            var last = items[ items.length - 1 ];
120            if ( e.shiftKey && document.activeElement === first ) {
121                e.preventDefault();
122                last.focus();
123            } else if ( ! e.shiftKey && document.activeElement === last ) {
124                e.preventDefault();
125                first.focus();
126            }
127        }
128    } );
129
130    // Submenu (column B): open on HOVER on desktop, on TAP on touch. One at a
131    // time. The parent link still navigates on click at desktop.
132    function openSubmenu( li ) {
133        if ( li.classList.contains( 'is-open' ) ) {
134            return;
135        }
136        closeOpenSubmenus();
137        li.classList.add( 'is-open' );
138        var a = li.querySelector( ':scope > a' );
139        if ( a ) {
140            a.setAttribute( 'aria-expanded', 'true' );
141        }
142        layoutSubmenu( li );
143    }
144
145    function closeSubmenu( li ) {
146        li.classList.remove( 'is-open' );
147        var a = li.querySelector( ':scope > a' );
148        if ( a ) {
149            a.setAttribute( 'aria-expanded', 'false' );
150        }
151        var subMenu = li.querySelector( ':scope > .sub-menu' );
152        if ( subMenu ) {
153            clearSubmenuLayout( subMenu );
154        }
155    }
156
157    var canHover = false;
158    try {
159        canHover = window.matchMedia( '(hover: hover) and (pointer: fine)' ).matches;
160    } catch ( e ) {}
161
162    var parentItems = overlay.querySelectorAll( '.menu-item-has-children' );
163    Array.prototype.forEach.call( parentItems, function ( li ) {
164        var link = li.querySelector( ':scope > a' );
165
166        if ( link ) {
167            link.setAttribute( 'aria-expanded', 'false' );
168
169            // A parent with children is a PURE TRIGGER — it never navigates on
170            // either device (the real overview link, e.g. "All Forts", lives as
171            // the first submenu item). Desktop click opens; touch tap toggles.
172            link.addEventListener( 'click', function ( e ) {
173                e.preventDefault();
174                if ( canHover ) {
175                    openSubmenu( li );
176                } else if ( li.classList.contains( 'is-open' ) ) {
177                    closeSubmenu( li );
178                } else {
179                    openSubmenu( li );
180                }
181            } );
182        }
183
184        if ( canHover ) {
185            // Desktop: also open/close on hover; moving into column B (a DOM
186            // descendant) keeps it open. Keyboard: open on focus in, close when
187            // focus leaves the whole item.
188            li.addEventListener( 'mouseenter', function () { openSubmenu( li ); } );
189            li.addEventListener( 'mouseleave', function () { closeSubmenu( li ); } );
190            li.addEventListener( 'focusin', function () { openSubmenu( li ); } );
191            li.addEventListener( 'focusout', function ( e ) {
192                if ( ! li.contains( e.relatedTarget ) ) {
193                    closeSubmenu( li );
194                }
195            } );
196        }
197    } );
198
199    // Re-fit the open flyout when the viewport changes (resize or crossing the
200    // desktop breakpoint), since its height and alignment are pixel-based.
201    window.addEventListener( 'resize', function () {
202        var openLi = overlay.querySelector(
203            '.menu-item-has-children.is-open'
204        );
205        if ( openLi ) {
206            layoutSubmenu( openLi );
207        }
208    } );
209}() );
210
211/**
212 * Land Acknowledgement modal (global).
213 *
214 * Any link inside a .land-acknowledgement element opens the modal emitted by
215 * footer.php. Handling is DELEGATED off document, so the trigger works wherever
216 * the client drops it (Elementor widget, menu item, footer) — including markup
217 * added after load. Close via the ×, the backdrop, or Escape.
218 *
219 * Deliberately its own IIFE: the navigation block above returns early when the
220 * nav toggle is absent, which would otherwise skip this.
221 */
222( function () {
223    'use strict';
224
225    var modal = document.getElementById( 'tmf-land-modal' );
226    if ( ! modal ) {
227        return;
228    }
229
230    var lastFocused = null;
231
232    function openModal() {
233        lastFocused = document.activeElement;
234        modal.hidden = false;
235        document.body.classList.add( 'tmf-modal-open' );
236
237        var close = modal.querySelector( '.tmf-land-modal__close' );
238        if ( close ) {
239            close.focus();
240        }
241    }
242
243    function closeModal() {
244        modal.hidden = true;
245        document.body.classList.remove( 'tmf-modal-open' );
246
247        if ( lastFocused ) {
248            lastFocused.focus();
249        }
250    }
251
252    document.addEventListener( 'click', function ( e ) {
253        // Trigger: any link inside .land-acknowledgement.
254        if ( e.target.closest( '.land-acknowledgement a' ) ) {
255            e.preventDefault();
256            openModal();
257            return;
258        }
259
260        if ( e.target.closest( '.tmf-land-modal__close' ) ) {
261            closeModal();
262            return;
263        }
264
265        // Backdrop click (the overlay itself, not the dialog).
266        if ( e.target === modal ) {
267            closeModal();
268        }
269    } );
270
271    document.addEventListener( 'keydown', function ( e ) {
272        if ( ( e.key === 'Escape' || e.key === 'Esc' ) && ! modal.hidden ) {
273            closeModal();
274        }
275    } );
276}() );

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.