1/** 2 * Elemental Menu - off-canvas mobile behavior. 3 * 4 * Open/close the off-canvas panel, drill into submenus (each level swipes in 5 * from the right) and step back out. No dependencies. Each widget instance is 6 * initialized once (guarded), and re-initialized in the Elementor editor. 7 */ 8( function () { 9 'use strict'; 10 11 var PANEL = '.elemental-menu__panel.elemental-menu__container'; 12 var SUBMENU = '.elemental-menu__panel-body .elemental-menu__panel'; 13 14 function initMenu( root ) { 15 if ( ! root || root.dataset.elementalMenuInit === '1' ) { 16 return; 17 } 18 19 var panel = root.querySelector( PANEL ); 20 if ( ! panel ) { 21 return; 22 } 23 24 root.dataset.elementalMenuInit = '1'; 25 26 // Custom breakpoint: the tablet/mobile presets collapse to the off-canvas 27 // menu via static CSS media queries, but a user-defined width (Layout > 28 // Breakpoint > Custom) can't be a static media query. When present, match 29 // it here and toggle `.is-collapsed`, which triggers the same mobile mode. 30 const breakpoint = root.getAttribute( 'data-elemental-breakpoint' ); 31 if ( breakpoint ) { 32 const breakpointPx = parseInt( breakpoint, 10 ); 33 if ( breakpointPx > 0 && window.matchMedia ) { 34 const mql = window.matchMedia( '(max-width: ' + breakpointPx + 'px)' ); 35 const applyBreakpoint = function () { 36 root.classList.toggle( 'is-collapsed', mql.matches ); 37 }; 38 applyBreakpoint(); 39 // addEventListener is the modern API; addListener is the fallback 40 // for older Safari where MediaQueryList predates EventTarget. 41 if ( mql.addEventListener ) { 42 mql.addEventListener( 'change', applyBreakpoint ); 43 } else if ( mql.addListener ) { 44 mql.addListener( applyBreakpoint ); 45 } 46 } 47 } 48 49 // Scroll state: when enabled (data-scroll-state), toggle `.is-scrolled` on 50 // the root as soon as the page is scrolled down at all. Purely a styling 51 // hook (the Scroll State controls recolor the desktop bar); no positioning. 52 if ( '1' === root.dataset.scrollState ) { 53 var ticking = false; 54 var syncScrolled = function () { 55 ticking = false; 56 var scrolled = ( window.pageYOffset || document.documentElement.scrollTop || 0 ) > 0; 57 root.classList.toggle( 'is-scrolled', scrolled ); 58 }; 59 var onScroll = function () { 60 if ( ! ticking ) { 61 ticking = true; 62 window.requestAnimationFrame( syncScrolled ); 63 } 64 }; 65 window.addEventListener( 'scroll', onScroll, { passive: true } ); 66 // Set the initial state (e.g. the page loaded already scrolled). 67 syncScrolled(); 68 } 69 70 var backdrop = root.querySelector( '.elemental-menu__backdrop' ); 71 72 function resetLevels() { 73 panel.querySelectorAll( SUBMENU + '.is-active' ).forEach( function ( submenu ) { 74 submenu.classList.remove( 'is-active' ); 75 } ); 76 } 77 78 function openPanel() { 79 panel.classList.add( 'is-open' ); 80 panel.setAttribute( 'aria-hidden', 'false' ); 81 document.body.classList.add( 'elemental-menu-open' ); 82 83 root.querySelectorAll( '.elemental-menu__open-button' ).forEach( function ( btn ) { 84 btn.setAttribute( 'aria-expanded', 'true' ); 85 } ); 86 87 if ( backdrop ) { 88 backdrop.hidden = false; 89 // Next frame so the opacity transition runs. 90 window.requestAnimationFrame( function () { 91 backdrop.classList.add( 'is-visible' ); 92 } ); 93 } 94 } 95 96 function closePanel() { 97 resetLevels(); 98 panel.classList.remove( 'is-open' ); 99 panel.setAttribute( 'aria-hidden', 'true' ); 100 document.body.classList.remove( 'elemental-menu-open' ); 101 102 root.querySelectorAll( '.elemental-menu__open-button' ).forEach( function ( btn ) { 103 btn.setAttribute( 'aria-expanded', 'false' ); 104 } ); 105 106 if ( backdrop ) { 107 backdrop.classList.remove( 'is-visible' ); 108 backdrop.hidden = true; 109 } 110 } 111 112 root.querySelectorAll( '.elemental-menu__open-button' ).forEach( function ( btn ) { 113 btn.setAttribute( 'aria-expanded', 'false' ); 114 btn.addEventListener( 'click', function ( e ) { 115 e.preventDefault(); 116 openPanel(); 117 } ); 118 } ); 119 120 panel.querySelectorAll( '.elemental-menu__close-button' ).forEach( function ( btn ) { 121 btn.addEventListener( 'click', function ( e ) { 122 e.preventDefault(); 123 closePanel(); 124 } ); 125 } ); 126 127 if ( backdrop ) { 128 backdrop.addEventListener( 'click', closePanel ); 129 } 130 131 // Drill into a submenu, or step back out. 132 panel.addEventListener( 'click', function ( e ) { 133 var back = e.target.closest( '.elemental-menu__back' ); 134 if ( back && panel.contains( back ) ) { 135 e.preventDefault(); 136 // Step back one level: deactivate the deepest active submenu. Nested 137 // levels follow their parents in document order, so the last active 138 // one is the deepest. 139 var actives = panel.querySelectorAll( SUBMENU + '.is-active' ); 140 if ( actives.length ) { 141 actives[ actives.length - 1 ].classList.remove( 'is-active' ); 142 } 143 return; 144 } 145 146 var link = e.target.closest( 'a.elemental-menu__link, a.elemental-menu__sublink' ); 147 if ( link && panel.contains( link ) ) { 148 var li = link.closest( 'li' ); 149 var submenu = li ? li.querySelector( ':scope > .elemental-menu__panel' ) : null; 150 if ( submenu ) { 151 // Parent item: drill in instead of navigating. 152 e.preventDefault(); 153 submenu.classList.add( 'is-active' ); 154 } 155 } 156 } ); 157 158 // Escape closes the panel. 159 document.addEventListener( 'keydown', function ( e ) { 160 if ( 'Escape' === e.key && panel.classList.contains( 'is-open' ) ) { 161 closePanel(); 162 } 163 } ); 164 165 // If the viewport grows past the breakpoint while the panel is open, the 166 // panel is hidden by the media query but the backdrop + scroll-lock would 167 // persist - force a clean close so the page stays interactive. 168 window.addEventListener( 'resize', function () { 169 if ( panel.classList.contains( 'is-open' ) && 'none' === window.getComputedStyle( panel ).display ) { 170 closePanel(); 171 } 172 } ); 173 } 174 175 function initAll( context ) { 176 ( context || document ).querySelectorAll( '.elemental-menu' ).forEach( initMenu ); 177 } 178 179 if ( 'loading' === document.readyState ) { 180 document.addEventListener( 'DOMContentLoaded', function () { 181 initAll(); 182 } ); 183 } else { 184 initAll(); 185 } 186 187 // Elementor editor: (re)initialize when the widget is rendered in the preview. 188 window.addEventListener( 'elementor/frontend/init', function () { 189 if ( window.elementorFrontend && window.elementorFrontend.hooks ) { 190 window.elementorFrontend.hooks.addAction( 191 'frontend/element_ready/elemental-elemental-menu.default',
192 function ( $scope ) { 193 var el = $scope && $scope[0] ? $scope[0] : $scope; 194 if ( ! el || ! el.querySelector ) { 195 return; 196 } 197 initMenu( el.querySelector( '.elemental-menu' ) || el ); 198 } 199 ); 200 } 201 } ); 202} )();
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.