1// 2// YJA Mobile Navigation 3// 4 5// Toggle open / close 6function yjaMobileNavigation(yjaMobileNavConfig) { 7 8 let yjaMenuToggler = [] || ""; 9 let yjaMenuMainToggler = "mmenu-open-panel"; 10 const yjaMenuWrapper = document.querySelector('.yja-mobile-navigation'); 11 const yjaMenuButtonClose = document.getElementById("yjaMenuButtonClose"); 12 const yjaMenuButtonLast = document.getElementById('yjaMenuBottomButton'); 13 const toggleChildMenuButtons = document.querySelectorAll('button.yja-mobile-navigation__button--toggler'); 14 15 16 let config = { 17 navItemId: 'mainMenubar', 18 togglerElement: '', 19 yjaMenuMainToggler: 'mmenu-open-panel', 20 elementsToHide: [] || "", 21 excludedElements: ['#skip-to-content','#footer'], 22 focusableElements: ['a[href]','area[href]','button','details','input','iframe','select','textarea','[contentEditable=""]','[contentEditable="true"]','[contentEditable="TRUE"]','[tabindex]:not([tabindex^="-"])'] //':not([disabled])' 23 } 24 25 document.addEventListener('DOMContentLoaded', () => { 26 config = { ...config, ...yjaMobileNavConfig }; 27 yjaMenuMainToggler = document.getElementById(config.yjaMenuMainToggler); 28 29 if (config.togglerElement === '') config.togglerElement = `#${config.yjaMenuMainToggler}`; 30 31 32 33 if ( config.togglerElement !== '' && yjaMenuWrapper !== null) { 34 if (config.togglerElement.startsWith('.')) yjaMenuToggler = [...document.querySelectorAll(config.togglerElement)]; 35 else yjaMenuToggler = document.querySelector(config.togglerElement); 36 37 38 // Open mobile menu 39 if (Array.isArray(yjaMenuToggler)) { 40 yjaMenuToggler.forEach(toggler => { 41 toggler.addEventListener("click", (evt) => { 42 evt.preventDefault() 43 openMobileNavigation(toggler, yjaMenuWrapper, config) 44 }, false); 45 }) 46 } else { 47 yjaMenuToggler.addEventListener("click", (evt) => { 48 evt.preventDefault() 49 openMobileNavigation(yjaMenuToggler, yjaMenuWrapper, config) 50 }, false); 51 52 } 53 54 55 56 if (yjaMenuButtonClose !== null) { 57 // Close mobile menu by clock 58 yjaMenuButtonClose.addEventListener("click", () => { 59 // Hide other DOM elements from screen readers 60 modifyDomElements(config,true); 61 closeMobileMenu(yjaMenuWrapper, yjaMenuToggler); 62 63 64 }, false); 65 66 // If user press shift + TAB, jump to last focusable element on navigation 67 yjaMenuButtonClose.addEventListener("keydown", (evt) => { 68 const focusable = focusables(yjaMenuWrapper); 69 if (evt.shiftKey && evt.key === 'Tab') { 70 if (document.activeElement === focusable[0]) { 71 // Find first nav level links nav links/buttons without LI elements 72 const mainLinks = [...focusable].filter(item => { 73 if (item.closest('li') !== null) { 74 if (item.closest('li').classList.contains('level-1')) { 75 return item; 76 } 77 } else { 78 return item; 79 } 80 }); 81 82 if (mainLinks.length > 0) { 83 // Move to last element 84 mainLinks[mainLinks.length - 1].focus(); 85 } 86 } 87 } 88 }, false); 89 90 } 91 92 // Toggle child menu visibility 93 if (toggleChildMenuButtons.length > 0) { 94 toggleChildMenuButtons.forEach(button => { 95 button.addEventListener('click', () => { 96 97 // find closest div 98 const parent = button.closest('div'); 99 // find closest LI 100 const li = parent.closest('li'); 101 // find next UL that contains child elements 102 const ul = parent.nextElementSibling; 103 104 // if UL exists toggle its visibility 105 if (ul !== null) { 106 107 button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === 'false' ? 'true' : 'false'); 108 if (button.getAttribute('aria-expanded') === 'false') { 109 li.classList.remove('yja-mobile-navigation--open'); 110 } else { 111 li.classList.add('yja-mobile-navigation--open'); 112 } 113 } 114 }); 115 }); 116 } 117 118 119 120 // For screen readers and <TAB> navigation 121 if (yjaMenuButtonLast !== null) { 122 // If user press TAB, move focus to first element on navigation. 123 yjaMenuButtonLast.addEventListener("keydown", (evt) => { 124 const focusable = focusables(yjaMenuWrapper); 125 if (evt.key === "Tab") { 126 evt.preventDefault() 127 yjaMenuButtonClose.focus(); 128 } 129 }, false); 130 131 // Close action 132 yjaMenuButtonLast.addEventListener('click', () => { 133 modifyDomElements(config,true); 134 closeMobileMenu(yjaMenuWrapper, yjaMenuToggler); 135 }, false); 136 } 137 138 139 // Close by clicking outside navigation panel 140 yjaMenuWrapper.addEventListener('click', (evt) => { 141 const panel = yjaMenuWrapper.querySelector('.yja-mobile-navigation__content'); 142 const inside = panel.contains(evt.target); 143 if (!inside) { 144 modifyDomElements(config,true); 145 closeMobileMenu(yjaMenuWrapper, yjaMenuToggler); 146 } 147 }, false); 148 149 // Close by ESC key 150 yjaMenuWrapper.addEventListener('keydown', (evt) => { 151 if (evt.key === "Escape") { 152 modifyDomElements(config,true); 153 if (Array.isArray(yjaMenuToggler)) { 154 yjaMenuToggler.forEach(toggler => { 155 closeMobileMenu(yjaMenuWrapper, toggler); 156 }) 157 } else { 158 closeMobileMenu(yjaMenuWrapper, yjaMenuToggler); 159 } 160 } 161 }, false); 162 163 } 164 },false) 165} 166 167function openMobileNavigation(toggler, menuWrapper,config) { 168 const body = document.querySelector('body'); 169 // Hide other DOM elements from screen readers 170 modifyDomElements(config); 171 toggler.setAttribute('aria-expanded','true'); 172 menuWrapper.classList.add('yja-mobile-navigation--open'); 173 menuWrapper.classList.remove('yja-mobile-navigation--slide-out'); 174 body.classList.add('mobile-navigation-visible') 175 176 // Add timeout to enable animations 177 setTimeout(() => { 178 menuWrapper.classList.add('yja-mobile-navigation--slide-in'); 179 }, 100); 180 181 // Move focus on navigation element 182 setTimeout(() => { 183 const FOCUSABLE_ELEMENTS = ['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA'];
184 let focusablesElements = focusables(menuWrapper); 185 186 // Check if toggler has data-focus-target set 187 // if has focus on element or its first focusable child, that was defined in data-focus-target attribute 188 if (toggler?.dataset?.focusTarget) { 189 const focusTarget = document.getElementById(toggler?.dataset?.focusTarget); 190 if (!focusTarget) return; 191 if (focusTarget.hasAttribute('tabIndex') && focusTarget.getAttribute('tabIndex') === '-1') return; 192 193 if (FOCUSABLE_ELEMENTS?.includes(focusTarget?.nodeName)) { 194 focusablesElements = focusTarget; 195 } else { 196 focusablesElements = getActiveLink(focusTarget); 197 } 198 199 // Set focus on correct element 200 if (focusablesElements instanceof Array) focusablesElements[0].focus(); 201 else focusablesElements.focus(); 202 203 } else { 204 focusablesElements[0].focus(); 205 } 206 207 },500); 208} 209 210function getActiveLink(focusTarget) { 211 let item; 212 if (focusTarget.classList.contains('nav')) { 213 let currentPage = focusTarget.querySelector('[aria-current="page"]'); 214 215 if (!currentPage) item = focusables(focusTarget) 216 else { 217 if (currentPage.nodeName === 'LI') item = focusables(currentPage); 218 if (currentPage.nodeName === 'A') item = currentPage; 219 } 220 } else { 221 item = focusables(focusTarget) 222 } 223 224 return item; 225} 226 227function focusables(wrapper) { 228 const focusable = [...wrapper.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')]; 229 return focusable; 230} 231 232const closeMobileMenu = (yjaMenuWrapper,yjaMenuMainToggler) => { 233 const body = document.querySelector('body'); 234 235 yjaMenuWrapper.classList.remove('yja-mobile-navigation--slide-in'); 236 yjaMenuWrapper.classList.add('yja-mobile-navigation--slide-out'); 237 238 if (yjaMenuMainToggler.length > 1) yjaMenuMainToggler.forEach(toggle => toggle.setAttribute('aria-expanded', 'false')); 239 else yjaMenuMainToggler.setAttribute('aria-expanded', 'false'); 240 241 body.classList.remove('mobile-navigation-visible'); 242 243 // Add timeout to enable animations 244 setTimeout(() => { 245 yjaMenuWrapper.classList.remove('yja-mobile-navigation--open'); 246 247 if (yjaMenuMainToggler.length > 1) yjaMenuMainToggler.forEach(toggle => toggle.setAttribute('aria-expanded', 'false')); 248 else yjaMenuMainToggler.setAttribute('aria-expanded', 'false'); 249 250 // Move focus out of navigation element 251 if (yjaMenuMainToggler.length > 1) yjaMenuMainToggler[0].focus(); 252 else yjaMenuMainToggler.focus() 253 }, 500); 254} 255 256function getDisableElements(elements) { 257 if (!Array.isArray(elements)) return; 258 let domElements = [] 259 elements.forEach(item =>{ 260 let type = item.charAt(0) 261 switch(type) { 262 case '.': 263 let items = [...document.querySelectorAll(item)] 264 if (document.querySelectorAll(item)) items.forEach(item => domElements = [...domElements, item]) 265 break; 266 case '#': 267 if (document.querySelector(item)) domElements = [...domElements, document.querySelector(item)] 268 break; 269 default: 270 return 271 } 272 }) 273 return domElements; 274} 275 276function modifyDomElements(config,state) { 277 if (Array.isArray(config.elementsToHide) && config.elementsToHide.length > 0) { 278 config.elementsToHide.forEach(item => config.excludedElements = [...config.excludedElements, item]) 279 } 280 if (typeof config.elementsToHide === 'string' && config.elementsToHide !== '') config.excludedElements = [...config.excludedElements, config.elementsToHide] 281 282 const elements = getDisableElements(config.excludedElements); 283 setVisibilityOfDomElements(elements,state,config) 284} 285 286function setVisibilityOfDomElements(items, show,config) { 287 let hide = show ? 'false' : 'true'; 288 if (items.length > 0) { 289 let focusable = [] 290 items.forEach(elem => { 291 let arr = elem.querySelectorAll(config.focusableElements) 292 arr.forEach(a => focusable = [...focusable, a]); 293 elem.setAttribute('aria-hidden',hide); 294 295 if (hide === 'true') elem.setAttribute('tabIndex', '-1'); 296 else elem.removeAttribute('tabIndex'); 297 }) 298 299 if (focusable.length > 0) { 300 if (hide === 'true') focusable.forEach(e => e.setAttribute('tabIndex', '-1')); 301 else focusable.forEach(e => e.removeAttribute('tabIndex')); 302 } 303 } 304}
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.