1/** 2 * @Author: Roni Laukkarinen 3 * @Date: 2021-04-23 13:10:51 4 * @Last Modified by: Roni Laukkarinen 5 * @Last Modified time: 2022-01-03 15:27:02 6 */ 7// TODO: Refactor file 8/* eslint-disable default-case, eqeqeq, no-restricted-globals, no-undef, no-var, vars-on-top, max-len, prefer-destructuring, no-redeclare, no-plusplus, no-use-before-define, no-unused-vars, block-scoped-var, func-names */ 9/* 10An accessible menu for WordPress 11 12https://github.com/theme-smith/accessible-nav-wp 13Kirsten Smith ([email protected]) 14Licensed GPL v.2 (http://www.gnu.org/licenses/gpl-2.0.html) 15 16This work derived from: 17https://github.com/WordPress/twentysixteen (GPL v.2) 18https://github.com/wpaccessibility/a11ythemepatterns/tree/master/menu-keyboard-arrow-nav (GPL v.2) 19*/ 20 21/*! 22 * Check if an element is out of the viewport 23 * (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com 24 * @param {Node} elem The element to check 25 * @return {Object} A set of booleans for each side of the element 26 * @source https://gomakethings.com/how-to-check-if-any-part-of-an-element-is-out-of-the-viewport-with-vanilla-js/ 27 */ 28var isOutOfViewport = function (elem) { 29 // Get element's bounding 30 var bounding = elem.getBoundingClientRect(); 31 32 // Check if it's out of the viewport on each side 33 var out = {}; 34 out.top = bounding.top < 0; 35 out.left = bounding.left < 0; 36 out.bottom = bounding.bottom > (window.innerHeight || document.documentElement.clientHeight); 37 out.right = bounding.right > (window.innerWidth || document.documentElement.clientWidth); 38 out.any = out.top || out.left || out.bottom || out.right; 39 40 return out; 41}; 42 43// Navigation.js start 44(function ($) { 45 // Responsive nav width 46 var responsivenav = 1051; 47 var html; 48 var body; 49 var container; 50 var button; 51 var menu; 52 var menuWrapper; 53 var links; 54 var subMenus; 55 var i; 56 var len; 57 var focusableElements; 58 var firstFocusableElement; 59 var lastFocusableElement; 60 61 // Hover intent 62 const menuItems = document.querySelectorAll('.menu-item'); 63 // const hoverIntentTimeout = 1000; 64 65 menuItems.forEach((li) => { 66 // Find sub menus 67 var subMenusUnderMenuItem = li.querySelectorAll('.sub-menu'); 68 69 // Loop through sub menus 70 subMenusUnderMenuItem.forEach((subMenu) => { 71 // First let's check if submenu exists 72 if (typeof subMenusUnderMenuItem !== 'undefined') { 73 // Check if the sub menu is out of viewport or not 74 var isOut = isOutOfViewport(subMenu); 75 76 // At least one side of the element is out of viewport 77 if (isOut.any) { 78 subMenu.classList.add('is-out-of-viewport'); 79 } 80 } 81 }); 82 83 li.addEventListener('mouseover', function () { 84 this.classList.add('hover-intent'); 85 this.parentNode.classList.add('hover-intent'); 86 87 document.addEventListener('keydown', (keydownMouseoverEvent) => { 88 if (keydownMouseoverEvent.key === 'Escape') { 89 li.classList.remove('hover-intent'); 90 li.parentNode.classList.remove('hover-intent'); 91 li.parentNode.parentNode.classList.remove('hover-intent'); 92 } 93 }); 94 }); 95 96 li.addEventListener('mouseleave', function () { 97 this.classList.remove('hover-intent'); 98 this.parentNode.classList.remove('hover-intent'); 99 }); 100 }); 101 102 // Define menu items 103 var menuContainer = $('.site-header'); 104 var menuToggle = menuContainer.find('.menu-toggle'); 105 106 var siteHeaderMenu = menuContainer.find('.nav-primary'); 107 var siteNavigation = menuContainer.find('#primary-menu'); 108 109 // Close focused dropdowns when pressing esc 110 $('.menu-item a, .dropdown button').on('keyup', function (e) { 111 // Checking are menu items open or not 112 const isSubMenuDropdownOpen = $(this).parent().parent().parent() 113 .find('.sub-menu') 114 .prev('.dropdown-toggle') 115 .attr('aria-expanded'); 116 const isMainMenuDropdownOpen = $(this).closest('.menu-item').find('.dropdown-toggle').attr('aria-expanded'); 117 const areWeInDropdown = $(this).parent().parent().hasClass('sub-menu'); 118 119 if (isSubMenuDropdownOpen === 'true' || isMainMenuDropdownOpen === 'true') { 120 if ($('.dropdown').find(':focus').length !== 0) { 121 // Close menu using Esc key but only if dropdown is open 122 if (e.code === 'Escape') { 123 // Close the dropdown menu 124 var thisDropdown = $(this).parent().parent().parent(); 125 126 var screenReaderSpan = thisDropdown.find('.screen-reader-text'); 127 var dropdownToggle = thisDropdown.find('.dropdown-toggle'); 128 thisDropdown.find('.sub-menu').removeClass('toggled-on'); 129 thisDropdown.find('.dropdown-toggle').removeClass('toggled-on'); 130 thisDropdown.find('.dropdown').removeClass('toggled-on'); 131 dropdownToggle.attr('aria-expanded', 'false'); 132 // jscs:enable 133 screenReaderSpan.text(dr_screenReaderText.expand); 134 // Move focus back to previous dropdown select 135 // But only if we are not already in the toggle of the first dropdown menu 136 if (areWeInDropdown === true) { 137 thisDropdown.find('.dropdown-toggle:first').trigger('focus'); 138 } 139 } 140 } 141 } 142 143 if (window.innerWidth > responsivenav) { 144 // Close previous dropdown if we are on main level 145 var prevDropdown = $(this).parent().prev(); 146 147 var screenReaderSpanPrev = prevDropdown.find('.screen-reader-text'); 148 var dropdownTogglePrev = prevDropdown.find('.dropdown-toggle'); 149 prevDropdown.find('.sub-menu').removeClass('toggled-on'); 150 prevDropdown.find('.dropdown-toggle').removeClass('toggled-on'); 151 prevDropdown.find('.dropdown').removeClass('toggled-on'); 152 dropdownTogglePrev.attr('aria-expanded', 'false'); 153 screenReaderSpanPrev.text(dr_screenReaderText.expand); 154 155 // Close next dropdown if we are on main level 156 var nextDropdown = $(this).parent().next(); 157 158 var screenReaderSpanNext = nextDropdown.find('.screen-reader-text'); 159 var dropdownToggleNext = nextDropdown.find('.dropdown-toggle'); 160 nextDropdown.find('.sub-menu').removeClass('toggled-on'); 161 nextDropdown.find('.dropdown-toggle').removeClass('toggled-on'); 162 nextDropdown.find('.dropdown').removeClass('toggled-on'); 163 dropdownToggleNext.attr('aria-expanded', 'false'); 164 screenReaderSpanNext.text(dr_screenReaderText.expand); 165 } 166 }); 167 168 // Adds aria attribute 169 siteHeaderMenu.find('.menu-item-has-children').attr('aria-haspopup', 'true'); 170 171 // Add default dropdown-toggle label 172 $('.submenu-expand').each(function () { 173 $(this).attr('aria-label', `${dr_screenReaderText.expand_for} ${$(this).prev().text()}`); 174 }); 175 176 // Toggles the sub-menu when dropdown toggle button accessed 177 siteHeaderMenu.find('.submenu-expand').on('click', function (e) { 178 var dropdownMenu = $(this).nextAll('.sub-menu'); 179 180 $(this).prev().toggleClass('toggled-on'); 181 $(this).toggleClass('toggled-on'); 182 dropdownMenu.toggleClass('toggled-on'); 183 184 // jscs:disable 185 $(this).attr( 186 'aria-expanded', 187 $(this).attr('aria-expanded') === 'false' ? 'true' : 'false', 188 ); 189 // jscs:enable 190 // Change screen reader open/close labels 191 192 $(this).attr( 193 'aria-label', 194 $(this).attr('aria-label') === `${dr_screenReaderText.collapse_for} ${$(this).prev().text()}` 195 ? `${dr_screenReaderText.expand_for} ${$(this).prev().text()}` 196 : `${dr_screenReaderText.collapse_for} ${$(this).prev().text()}`, 197 ); 198 }); 199 200 // Adds a class to sub-menus for styling 201 $('.sub-menu .menu-item-has-children') 202 .parent('.sub-menu') 203 .addClass('has-sub-menu'); 204 205 // Keyboard navigation 206 $('.menu-item a, button.submenu-expand').on('keydown', function (e) { 207 if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].indexOf(e.code) == -1) { 208 return; 209 } 210 211 switch (e.code) { 212 case 'ArrowLeft': // Left key 213 e.stopPropagation(); 214 215 if ($(this).hasClass('dropdown-toggle')) { 216 $(this).prev('a').trigger('focus'); 217 } else if ( 218 $(this).parent().prev().children('button.dropdown-toggle').length 219 ) { 220 $(this).parent().prev().children('button.dropdown-toggle') 221 .trigger('focus'); 222 } else { 223 $(this).parent().prev().children('a') 224 .trigger('focus'); 225 } 226 227 if ($(this).is('ul ul ul.sub-menu.toggled-on li:first-child a')) { 228 $(this) 229 .parents('ul.sub-menu.toggled-on li') 230 .children('button.dropdown-toggle') 231 .trigger('focus'); 232 } 233 234 break; 235 236 case 'ArrowRight': // Right key 237 e.stopPropagation(); 238 239 if ($(this).next('button.dropdown-toggle').length) { 240 $(this).next('button.dropdown-toggle').trigger('focus'); 241 } else if ($(this).parent().next().find('input').length) { 242 $(this).parent().next().find('input') 243 .trigger('focus'); 244 } else { 245 $(this).parent().next().children('a') 246 .trigger('focus'); 247 } 248 249 if ($(this).is('ul.sub-menu .dropdown-toggle.toggled-on')) { 250 $(this).parent().find('ul.sub-menu li:first-child a').trigger('focus'); 251 } 252 253 break; 254 255 case 'ArrowDown': // Down key 256 e.stopPropagation(); 257 258 if ($(this).next().length) { 259 $(this).next().find('li:first-child a').first() 260 .trigger('focus'); 261 } else if ($(this).parent().next().find('input').length) { 262 $(this).parent().next().find('input') 263 .trigger('focus'); 264 } else { 265 $(this).parent().next().children('a') 266 .trigger('focus'); 267 } 268 269 if ( 270 $(this).is('ul.sub-menu a') 271 && $(this).next('button.dropdown-toggle').length 272 ) { 273 $(this).parent().next().children('a') 274 .trigger('focus'); 275 } 276 277 if ( 278 $(this).is('ul.sub-menu .dropdown-toggle') 279 && $(this).parent().next().children('.dropdown-toggle').length 280 ) { 281 $(this).parent().next().children('.dropdown-toggle') 282 .trigger('focus'); 283 } 284 285 break; 286 287 case 'ArrowUp': // Up key 288 e.stopPropagation(); 289 290 if ($(this).parent().prev().length) { 291 $(this).parent().prev().children('a') 292 .trigger('focus'); 293 } else { 294 $(this) 295 .parents('ul') 296 .first() 297 .prev('.dropdown-toggle.toggled-on') 298 .trigger('focus'); 299 } 300 301 if ( 302 $(this).is('ul.sub-menu .dropdown-toggle') 303 && $(this).parent().prev().children('.dropdown-toggle').length 304 ) { 305 $(this).parent().prev().children('.dropdown-toggle') 306 .trigger('focus'); 307 } 308 309 break; 310 } 311 }); 312 313 container = document.getElementById('nav'); 314 if (!container) { 315 return; 316 } 317 318 button = document.querySelector('.menu-toggle'); 319 if (typeof button === 'undefined') { 320 return; 321 } 322 323 // Set vars. 324 html = document.getElementsByTagName('html')[0]; 325 body = document.getElementsByTagName('body')[0]; 326 menu = container.getElementsByTagName('ul')[0]; 327 menuWrapper = document.querySelector('.nav-menu'); 328 329 function mobileNav() { 330 var mobileNavInstance; 331 332 // Toggles the menu button 333 if (!menuToggle.length) { 334 return; 335 } 336
337 // Do not set aria-expanded false on desktop 338 if (window.innerWidth < responsivenav) { 339 menuToggle.add(siteNavigation).attr('aria-expanded', 'false'); 340 } 341 342 menuToggle.on('click', function () { 343 // $(this).add(menuWrapper).toggleClass('toggled-on'); 344 // Change screen reader expanded state 345 $(this).attr( 346 'aria-expanded', 347 $(this).attr('aria-expanded') === 'false' ? 'true' : 'false', 348 ); 349 350 // Change screen reader open/close labels 351 $('#nav-toggle-label').text( 352 // eslint-disable-next-line no-undef 353 $('#nav-toggle-label').text() === dr_screenReaderText.expand_toggle 354 ? dr_screenReaderText.collapse_toggle 355 : dr_screenReaderText.expand_toggle, 356 ); 357 358 $(this).attr( 359 'aria-label', 360 $(this).attr('aria-label') === dr_screenReaderText.expand_toggle 361 ? dr_screenReaderText.collapse_toggle 362 : dr_screenReaderText.expand_toggle, 363 ); 364 365 // jscs:disable 366 $(this) 367 .add(siteNavigation) 368 .attr( 369 'aria-expanded', 370 $(this).add(siteNavigation).attr('aria-expanded') === 'false' 371 ? 'true' 372 : 'false', 373 ); 374 // jscs:enable 375 }); 376 377 // Hide menu toggle button if menu is empty and return early. 378 if (typeof menu === 'undefined') { 379 button.style.display = 'none'; 380 return; 381 } 382 383 // Do not set aria-expanded false on desktop 384 if (window.innerWidth < responsivenav) { 385 menu.setAttribute('aria-expanded', 'false'); 386 } 387
388 // if (menu.className.indexOf('nav-menu') === -1) { 389 // menu.className += ' nav-menu'; 390 // } 391 392 // Focus trap for mobile navigation 393 if (window.innerWidth < responsivenav) { 394 firstFocusableElement = null; 395 lastFocusableElement = null; 396 397 // Select nav items 398 var navElements = container.querySelectorAll([ 399 '.nav-primary a[href]', 400 '.nav-primary button', 401 ]); 402 403 // Listen for key events on nav elements and the toggle button 404 // to trigger focus trap 405 for (var ii = 0; ii < navElements.length; ii++) { 406 navElements[ii].addEventListener('keydown', focusTrap); 407 } 408 } 409 410 // Function to hide the HubSpot chat widget 411 function hideChatWidget() { 412 const chatWidget = document.querySelector('#hubspot-messages-iframe-container'); 413 if (chatWidget) { 414 chatWidget.style.display = 'none'; 415 } 416 } 417 418 // Function to show the HubSpot chat widget 419 function showChatWidget() { 420 const chatWidget = document.querySelector('#hubspot-messages-iframe-container'); 421 if (chatWidget) { 422 chatWidget.style.display = ''; 423 } 424 } 425 426 // Click event handler for menu toggle button 427 button.onclick = function () { 428 if (container.className.indexOf('is-active') !== -1) { 429 closeMenu(); // Close menu. 430 showChatWidget(); // Show chat widget when menu is closed. 431 } else { 432 html.className += ' disable-scroll'; 433 body.className += ' js-nav-active'; 434 container.className += ' is-active'; 435 button.className += ' is-active'; 436 button.setAttribute('aria-expanded', 'true'); 437 menu.setAttribute('aria-expanded', 'true'); 438 439 // Add focus trap when menu open 440 button.addEventListener('keydown', focusTrap, false); 441 442 hideChatWidget(); // Hide chat widget when menu is open. 443 } 444 }; 445 446 // Close menu using Esc key. 447 document.addEventListener('keyup', (event) => { 448 if (event.code == 'Escape' || event.code == 'Esc') { 449 if (container.className.indexOf('is-active') !== -1) { 450 closeMenu(); // Close menu. 451 } 452 } 453 }); 454 455 // Close menu clicking menu wrapper area. 456 menuWrapper.onclick = function (e) { 457 if ( 458 e.target == menuWrapper 459 && container.className.indexOf('is-active') !== -1 460 ) { 461 closeMenu(); // Close menu. 462 } 463 }; 464 } 465 if (window.innerWidth < responsivenav) { 466 mobileNav(); // fire right away for mobile devices 467 } 468 469 // Close menu function. 470 function closeMenu() { 471 button.removeEventListener('keydown', focusTrap, false); 472 html.className = html.className.replace(' disable-scroll', ''); 473 body.className = body.className.replace(' js-nav-active', ''); 474 container.className = container.className.replace(' is-active', ''); 475 button.className = button.className.replace(' is-active', ''); 476 button.setAttribute('aria-expanded', 'false'); 477 menu.setAttribute('aria-expanded', 'false'); 478 $('#nav-toggle-label').text(dr_screenReaderText.expand_toggle); 479 480 // Return focus to nav-toggle 481 button.focus(); 482 } 483 484 // Get all the link elements within the menu. 485 links = menu.getElementsByTagName('a'); 486 subMenus = menu.getElementsByTagName('ul'); 487 488 // Each time a menu link is focused or blurred, toggle focus. 489 for (i = 0, len = links.length; i < len; i++) { 490 links[i].addEventListener('focus', toggleFocus, true); 491 links[i].addEventListener('blur', toggleFocus, true); 492 } 493 494 /** 495 * Sets or removes .focus class on an element. 496 */ 497 function toggleFocus() { 498 var self = this; 499 500 // Move up through the ancestors of the current link until we hit .nav-menu.
501 while (self.className.indexOf('nav-menu') === -1) { 502 // On li elements toggle the class .focus. 503 if (self.tagName.toLowerCase() === 'li') { 504 if (self.className.indexOf('focus') !== -1) { 505 self.className = self.className.replace(' focus', ''); 506 } else { 507 self.className += ' focus'; 508 } 509 } 510 511 self = self.parentElement; 512 } 513 } 514 515 function focusTrap(e) { 516 // Set focusable elements inside main navigation. 517 focusableElements = [...container.querySelectorAll( 518 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])', 519 )].filter((el) => !el.hasAttribute('disabled')).filter((el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length)); 520 521 firstFocusableElement = focusableElements[0]; 522 lastFocusableElement = focusableElements[focusableElements.length - 1]; 523 524 // Redirect last Tab to first focusable element. 525 if (lastFocusableElement === e.target && e.code === 'Tab' && !e.shiftKey) { 526 button.focus(); // Set focus on first element - that's actually close menu button. 527 } 528 529 // Redirect first Shift+Tab to toggle button element. 530 if (firstFocusableElement === e.target && e.code === 'Tab' && e.shiftKey) { 531 button.focus(); // Set focus on last element. 532 } 533 534 // Redirect Shift+Tab from the toggle button to last focusable element. 535 if (button === e.target && e.code === 'Tab' && e.shiftKey) { 536 lastFocusableElement.focus(); // Set focus on last element. 537 } 538 } 539 540 $(window).on('resize', () => { 541 if (window.innerWidth > responsivenav && body.className.indexOf('js-nav-active') !== -1) { 542 closeMenu(); // Close menu. 543 } else if (window.innerWidth < responsivenav && typeof window.mobileNavInstance == 'undefined') { 544 mobileNav(); 545 } 546 }); 547}(jQuery));
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.