1/* 2 * HC Off-canvas Nav 3 * =================== 4 * Version: 6.1.5 5 * Author: Some Web Media 6 * Author URL: https://github.com/somewebmedia/ 7 * Plugin URL: https://github.com/somewebmedia/hc-offcanvas-nav 8 * Description: JavaScript library for creating off-canvas multi-level navigations 9 * License: MIT 10 */ 11 12'use strict'; 13 14(function(global, factory) { 15 if (typeof module === 'object' && typeof module.exports === 'object') { 16 if (global.document) { 17 module.exports = factory(global); 18 } 19 else { 20 throw new Error('HC Off-canvas Nav requires a browser to run.'); 21 } 22 } 23 else if (typeof define === 'function' && define.amd) { 24 define('hcOffcanvasNav', [], factory(global)); 25 } 26 else { 27 factory(global); 28 } 29})(typeof window !== 'undefined' ? window : this, (window) => { 30 const document = window.document; 31 const html = document.getElementsByTagName('html')[0]; 32 33 let navCount = 0; 34 const navOpenClass = 'nav-open'; 35 36 const hcOffcanvasNav = function HcOffcanvasNav(elem, options) { 37 options = options || {}; 38 39 // use querySelector if string is passed 40 if (typeof elem === 'string') { 41 elem = elem.charAt(0) === '#' && elem.indexOf(' ') === -1 ? document.querySelector(elem) : document.querySelectorAll(elem); 42 } 43 44 // check if element exist 45 if (!elem) return false; 46 47 const Helpers = hcOffcanvasNav.Helpers; 48 49 const defaults = { 50 width: 280, 51 height: 'auto', 52 disableAt: false, 53 pushContent: null, 54 swipeGestures: true, 55 expanded: false, 56 position: 'left', // left, right, top, bottom 57 levelOpen: 'overlap', // overlap, expand, none/false 58 levelSpacing: 40, 59 levelTitles: true, 60 closeOpenLevels: true, 61 closeActiveLevel: false, 62 navTitle: null, 63 navClass: '', 64 disableBody: true, 65 closeOnClick: true, 66 closeOnEsc: true, 67 customToggle: null, 68 activeToggleClass: null, 69 bodyInsert: 'prepend', // prepend/append 70 keepClasses: true, 71 removeOriginalNav: false, 72 rtl: false, 73 insertClose: true, 74 insertBack: true, 75 levelTitleAsBack: true, 76 labelClose: '', 77 labelBack: 'Back' 78 }; 79 80 // show deprecated messages 81 if (typeof options.maxWidth !== 'undefined') { 82 Helpers.deprecated('maxWidth', 'disableAt', 'option'); 83 options.disableAt = options.maxWidth; 84 } 85 86 let Settings = Object.assign({}, defaults, options); 87 88 Settings.ariaLabels = Object.assign({}, { 89 open: 'Open Menu', 90 close: 'Close Menu', 91 submenu: 'Submenu' 92 }, options.ariaLabels); 93 94 let UpdatedSettings = []; 95 96 const checkForUpdate = (options) => { 97 if (!UpdatedSettings.length) { 98 return false; 99 } 100 101 let hasUpdated = false; 102 103 if (typeof options === 'string') { 104 options = [options]; 105 } 106 107 let l = options.length; 108 for (let i = 0; i < l; i++) { 109 if (UpdatedSettings.indexOf(options[i]) !== -1) { 110 hasUpdated = true; 111 } 112 } 113 114 return hasUpdated; 115 }; 116 117 const Plugin = function($originalNav) { 118 if (!$originalNav.querySelector('ul') && $originalNav.tagName !== 'UL') { 119 console.error('%c! HC Offcanvas Nav:' + `%c Navigation must contain <ul> element.`, 'color: #fa253b', 'color: default'); 120 return; 121 } 122 123 // count our nav 124 navCount++; 125 126 const navUniqId = `hc-nav-${navCount}`; 127 const Styles = Helpers.printStyle(`hc-offcanvas-${navCount}-style`); 128 const keydownEventName = 'keydown.hcOffcanvasNav'; 129 const activeToggleClass = Settings.activeToggleClass || 'toggle-open'; 130 131 // this is our new nav element 132 const $nav = Helpers.createElement('nav', {id: navUniqId}); 133 const $nav_container = Helpers.createElement('div', {class: 'nav-container'}); 134 135 $nav.addEventListener('click', Helpers.stopPropagation);
136 $nav.appendChild($nav_container); 137 138 let $toggle = null; 139 let $toggle_open = null; 140 let $push_content = null; 141 142 let Model = {}; 143 144 let _open = false; // is nav currently open 145 let _initExpanded = false; // should nav be opened on init 146 let _nextActiveLevel = null; // level that should be open next 147 let _top = 0; // to remember scroll position 148 let _containerWidth = 0; 149 let _containerHeight = 0; 150 let _transitionProperty; 151 let _transitionDuration; 152 let _transitionFunction; 153 let _closeLevelsTimeout = null; 154 let _indexes = {}; // object with level indexes 155 const _openLevels = []; // array with current open levels ids 156 let _keyboard = false; 157 const _focusEls = []; // array to store keyboard accessed items 158 let _xStart = null; // the X touch point 159 let _yStart = null; // the Y touch point 160 let _touchMoved = false; 161 let _touchNavTriggered = false; 162 163 if (!Settings.customToggle) { 164 // our toggle 165 $toggle = [Helpers.createElement('a', { 166 href: '#' 167 }, Helpers.createElement('span'))]; 168 169 $originalNav.insertAdjacentElement('afterend', $toggle[0]); 170 } 171 else { 172 // user toggle 173 $toggle = Helpers.getElements(Settings.customToggle); 174 } 175 176 if ($toggle && $toggle.length) { 177 $toggle.forEach(($t) => { 178 $t.addEventListener('click', toggleNav($t)); 179 $t.classList.add('hc-nav-trigger', navUniqId); 180 181 // ARIA 182 $t.setAttribute('role', 'button'); 183 $t.setAttribute('aria-label', (Settings.ariaLabels || {}).open); 184 $t.setAttribute('aria-controls', navUniqId); 185 $t.setAttribute('aria-expanded', false); 186 187 // make nav opening keyboard accessible 188 $t.addEventListener('keydown', (e) => { 189 if (e.key === 'Enter' || e.keyCode === 13) { 190 // trap focus inside nav 191 setTimeout(() => { 192 trapFocus(0, 0); 193 }, 0); 194 } 195 }); 196 }); 197 } 198 199 /* ARIA Keyboard Focus */ 200 201 const trapFocus = (n, l, i) => { 202 if (typeof l !== 'number' || (typeof n !== 'number' && !_focusEls.length)) { 203 return; 204 } 205 206 const focusableSelector = '[tabindex="0"], a[role="menuitem"], a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select'; 207 208 let $focusable = Array.prototype.filter.call($nav_container.querySelectorAll('.nav-wrapper'), (el) => { 209 return el.getAttribute('data-level') == l && (typeof i !== 'number' || (typeof i === 'number' && el.getAttribute('data-index') == i)); 210 })[0]; 211 212 $focusable = Helpers.children($focusable, '.nav-content')[0]; 213 214 const $navControls = Helpers.children($focusable, '.nav-close, .nav-back'); 215 216 $focusable = Helpers.children($focusable, 'ul'); 217 $focusable = Helpers.children($focusable, 'li'); 218 $focusable = Helpers.children($focusable, ':not(.nav-wrapper)'); 219 $focusable = [...$navControls, ...$focusable]; 220 $focusable = Array.prototype.map.call($focusable, (el) => { 221 return Array.prototype.slice.call(el.querySelectorAll(focusableSelector)); 222 }).flat(); 223 $focusable = Array.prototype.filter.call($focusable, (el) => el.getAttribute('tabindex') !== '-1'); 224 225 if (!$focusable) { 226 return; 227 } 228 229 // so we can keep the outline 230 $nav.classList.add('user-is-tabbing'); 231 232 const $first = $focusable[0]; 233 const $last = $focusable[$focusable.length - 1]; 234 235 if (typeof n === 'number') { 236 // put focus on item with desired index 237 $focusable[n].focus(); 238 } 239 else { 240 // focus last focusable element 241 _focusEls[_focusEls.length - 1].focus(); 242 // remove last element from focusable array 243 _focusEls.pop(); 244 } 245 246 // remove previous keydown event 247 document.removeEventListener(keydownEventName); 248 249 document.addEventListener(keydownEventName, (e) => { 250 if (!(e.key === 'Tab' || e.keyCode === 9)) { 251 return; 252 } 253 254 /* shift + tab */ 255 if (e.shiftKey) { 256 if (document.activeElement === $first) { 257 e.preventDefault(); 258 $last.focus(); 259 } 260 } 261 /* tab */ 262 else { 263 if (document.activeElement === $last) { 264 e.preventDefault(); 265 $first.focus(); 266 } 267 } 268 }); 269 }; 270 271 const untrapFocus = () => { 272 document.removeEventListener(keydownEventName); 273 274 if ($toggle_open) { 275 setTimeout(() => { 276 $toggle_open.focus(); 277 }, _transitionDuration); 278 } 279 }; 280 281 /* Build methods */ 282 283 const calcNav = () => { 284 // remove transition from the nav container so we can update the nav without flickering
285 $nav_container.style.transition = 'none'; 286 287 // nav must be block so we can get rendered width 288 $nav.style.display = 'block'; 289 290 const computedWidth = Helpers.formatSizeVal(_containerWidth = $nav_container.offsetWidth); 291 const computedHeight = Helpers.formatSizeVal(_containerHeight = $nav_container.offsetHeight); 292 293 // fix 100% transform glitching 294 Styles.add( 295 `.hc-offcanvas-nav.${navUniqId}.nav-position-left .nav-container`, 296 `transform: translate3d(-${computedWidth}, 0, 0)` 297 ); 298 Styles.add( 299 `.hc-offcanvas-nav.${navUniqId}.nav-position-right .nav-container`, 300 `transform: translate3d(${computedWidth}, 0, 0)` 301 ); 302 Styles.add( 303 `.hc-offcanvas-nav.${navUniqId}.nav-position-top .nav-container`, 304 `transform: translate3d(0, -${computedHeight}, 0)` 305 ); 306 Styles.add( 307 `.hc-offcanvas-nav.${navUniqId}.nav-position-bottom .nav-container`, 308 `transform: translate3d(0, ${computedHeight}, 0)` 309 ); 310 311 Styles.insert(); 312 313 // clear our temp inline styles 314 $nav.style.display = ''; 315 $nav_container.style.transition = ''; 316 317 _transitionProperty = window.getComputedStyle($nav_container).transitionProperty; 318 _transitionDuration = Helpers.toMs(window.getComputedStyle($nav_container).transitionDuration); 319 _transitionFunction = window.getComputedStyle($nav_container).transitionTimingFunction; 320 321 if (Settings.pushContent && $push_content && _transitionProperty) { 322 Styles.add(Helpers.getElementCssTag($push_content), `transition: ${_transitionProperty} ${_transitionDuration}ms ${_transitionFunction}`); 323 } 324 325 Styles.insert(); 326 }; 327 328 // init function 329 const initNav = (reinit) => { 330 const toggleDisplay = $toggle ? window.getComputedStyle($toggle[0]).display : false; 331 const mediaquery = Settings.disableAt ? `max-width: ${Settings.disableAt - 1}px` : false; 332 const width = Helpers.formatSizeVal(Settings.width); 333 const height = Helpers.formatSizeVal(Settings.height); 334 const spacing = Helpers.formatSizeVal(Settings.levelSpacing); 335 336 if (Helpers.isNumeric(width) || width.indexOf('px') !== -1) { 337 _containerWidth = parseInt(width); 338 } 339 340 if (Helpers.isNumeric(height) || height.indexOf('px') !== -1) { 341 _containerHeight = parseInt(height); 342 } 343 344 if (checkForUpdate(['disableAt', 'position'])) { 345 // clear media queries from previous run 346 Styles.reset(); 347 } 348 349 // create styles 350 Styles.add(`.hc-offcanvas-nav.${navUniqId}`, 'display: block', mediaquery); 351 352 // hide original 353 Styles.add(`.hc-nav-original.${navUniqId}`, 'display: none', mediaquery); 354 355 // trigger 356 if (toggleDisplay) { 357 Styles.add(`.hc-nav-trigger.${navUniqId}`, `display: ${toggleDisplay && toggleDisplay !== 'none' ? toggleDisplay : 'block'}`, mediaquery); 358 } 359 360 if (['left', 'right'].indexOf(Settings.position) !== -1) { 361 // container width 362 Styles.add(`.hc-offcanvas-nav.${navUniqId} .nav-container`, `width: ${width}`); 363 } 364 else { 365 // container height 366 Styles.add(`.hc-offcanvas-nav.${navUniqId} .nav-container`, `height: ${height}`); 367 } 368 369 // container transform 370 Styles.add( 371 `.hc-offcanvas-nav.${navUniqId}.nav-position-left .nav-container`, 372 `transform: translate3d(-${width}, 0, 0);` 373 ); 374 Styles.add( 375 `.hc-offcanvas-nav.${navUniqId}.nav-position-right .nav-container`, 376 `transform: translate3d(${width}, 0, 0);` 377 ); 378 Styles.add( 379 `.hc-offcanvas-nav.${navUniqId}.nav-position-top .nav-container`, 380 `transform: translate3d(0, -${height}, 0);` 381 ); 382 Styles.add( 383 `.hc-offcanvas-nav.${navUniqId}.nav-position-bottom .nav-container`, 384 `transform: translate3d(0, ${height}, 0);` 385 ); 386 387 // wrappers 388 Styles.add( 389 `.hc-offcanvas-nav.${navUniqId}.nav-levels-overlap.nav-position-left li.level-open > .nav-wrapper`, 390 `transform: translate3d(-${spacing}, 0, 0)`, mediaquery 391 ); 392 Styles.add( 393 `.hc-offcanvas-nav.${navUniqId}.nav-levels-overlap.nav-position-right li.level-open > .nav-wrapper`, 394 `transform: translate3d(${spacing}, 0, 0)`, mediaquery 395 ); 396 Styles.add( 397 `.hc-offcanvas-nav.${navUniqId}.nav-levels-overlap.nav-position-top li.level-open > .nav-wrapper`, 398 `transform: translate3d(0, -${spacing}, 0)`, mediaquery 399 ); 400 Styles.add( 401 `.hc-offcanvas-nav.${navUniqId}.nav-levels-overlap.nav-position-bottom li.level-open > .nav-wrapper`, 402 `transform: translate3d(0, ${spacing}, 0)`, mediaquery 403 ); 404 405 Styles.insert(); 406 407 // get page content 408 if (!reinit || (reinit && checkForUpdate('pushContent'))) { 409 $push_content = Settings.pushContent ? Helpers.getElements(Settings.pushContent)[0] : null; 410 } 411 412 // remove transition from the nav container so we can update the nav without flickering
413 $nav_container.style.transition = 'none'; 414 415 const wasOpen = $nav.classList.contains(navOpenClass); 416 417 const navClasses = [ 418 'hc-offcanvas-nav', 419 Settings.navClass || '', 420 navUniqId, 421 'nav-levels-' + (Settings.levelOpen || 'none'), 422 `nav-position-${Settings.position}`, 423 Settings.disableBody ? 'disable-body' : '', 424 Helpers.isIos ? 'is-ios' : '', 425 Helpers.isTouchDevice ? 'touch-device' : '', 426 wasOpen ? navOpenClass : '', 427 Settings.rtl ? 'rtl' : '', 428 Settings.insertClose === true && !Settings.labelClose ? 'nav-close-button-empty' : '' 429 ].join(' ').trim().replace(/ +/g, ' '); 430 431 $nav.removeEventListener('click'); 432 $nav.className = navClasses; 433 $nav.setAttribute('aria-hidden', true); 434 435 // set css variable so we can use it in themes if needed 436 document.documentElement.style.setProperty('--nav-level-spacing', Settings.levelSpacing + 'px'); 437 438 // close menu on body click (nav::after) 439 if (Settings.disableBody) { 440 $nav.addEventListener('click', closeNav); 441 } 442 443 if (reinit) { 444 calcNav(); 445 } 446 else { 447 // timed out so we can get computed data 448 setTimeout(calcNav, 0); 449 } 450 }; 451 452 // create nav model function 453 const createModel = () => { 454 455 // get first level menus 456 const $first_level = () => { 457 return $originalNav.tagName === 'UL' 458 ? [$originalNav] 459 : Array.prototype.filter.call($originalNav.children, (child) => child.tagName === 'UL' || child instanceof HTMLHeadingElement); 460 }; 461 462 // call 463 Model = getModel($first_level(), null); 464 465 function getModel($menu, id) { 466 const level = []; 467 468 Array.prototype.forEach.call($menu, ($ul) => { 469 if ($ul.tagName !== 'UL' && !($ul instanceof HTMLHeadingElement)) { 470 return; 471 } 472 473 const nav = { 474 tagName: $ul.tagName, 475 id: id, 476 htmlClass: $ul.getAttribute('class') || null, 477 items: [] 478 }; 479 480 if ($ul instanceof HTMLHeadingElement) { 481 // this is a heading 482 nav.content = Helpers.clone($ul, false, true); 483 } 484 else { 485 // this submenu should be open next 486 if ($ul.getAttribute('data-nav-active') !== null) { 487 _nextActiveLevel = id; 488 // remove data attribute 489 $ul.removeAttribute('data-nav-active'); 490 } 491 492 Array.prototype.forEach.call($ul.children, ($li) => { 493 const customContent = $li.getAttribute('data-nav-custom-content') !== null; 494 let $content = customContent ? $li.childNodes : Array.prototype.filter.call($li.children, (child) => child.tagName !== 'UL' && !child.querySelector('ul')).concat($li.children.length ? [] : [$li.firstChild]); 495 const $nested_navs = customContent ? [] : Array.prototype.slice.call($li.querySelectorAll('ul')); 496 const $subnav = !$nested_navs.length ? [] : [].concat(Array.prototype.filter.call($nested_navs[0].parentNode.children, (child) => child.tagName === 'UL' || child instanceof HTMLHeadingElement)); 497 498 let uniqid = null; 499 500 // if no content check for text 501 if (!$content.length) { 502 let text = ''; 503 504 for (let i = 0; i < $li.childNodes.length; i++) { 505 if ($li.childNodes[i].nodeType === Node.TEXT_NODE) { 506 text += $li.childNodes[i].textContent.trim(); 507 } 508 } 509 510 $content = [document.createTextNode(text)]; 511 } 512 513 // save unique identifier for remembering open sub menus 514 if ($subnav.length) { 515 if (!Helpers.data($li, 'hc-uniqid')) { 516 uniqid = Math.random().toString(36).substr(2); 517 Helpers.data($li, 'hc-uniqid', uniqid); 518 } 519 else { 520 uniqid = Helpers.data($li, 'hc-uniqid'); 521 } 522 } 523 524 // submenu of this list element should be open next 525 if ($li.getAttribute('data-nav-active') !== null) { 526 _nextActiveLevel = uniqid; 527 // remove data attribute 528 $li.removeAttribute('data-nav-active'); 529 } 530 531 // add elements to this level 532 nav.items.push({ 533 id: uniqid, 534 htmlClass: $li.getAttribute('class') || '', 535 content: $content, 536 custom: customContent, 537 subnav: $subnav.length ? getModel($subnav, uniqid) : [], 538 highlight: $li.getAttribute('data-nav-highlight') !== null 539 }); 540 }); 541 } 542 543 level.push(nav); 544 }); 545 546 return level; 547 } 548 }; 549 550 // create nav DOM function 551 const createNavDom = (reinit) => { 552 if (reinit) { 553 // empty the container 554 while ($nav_container.firstChild) $nav_container.removeChild($nav_
554container.firstChild); 555 // reset indexes 556 _indexes = {}; 557 } 558 559 // call 560 createDom(Model, $nav_container, 0, Settings.navTitle); 561 562 function createDom(menu, $container, level, title, backIndex, backTitle) { 563 const $wrapper = Helpers.createElement('div', { 564 class: `nav-wrapper nav-wrapper-${level}`, 565 'data-level': level, 566 'data-index': backIndex || 0 567 }); 568 const $content = Helpers.createElement('div', {class: 'nav-content'}); 569 570 $wrapper.addEventListener('click', Helpers.stopPropagation); 571 $wrapper.appendChild($content); 572 $container.appendChild($wrapper); 573 574 // titles 575 if (title && (level === 0 || (level > 0 && Settings.levelOpen === 'overlap'))) { 576 const _title = typeof title === 'string' 577 ? title 578 : Helpers.clone(window.jQuery && title instanceof window.jQuery && title.length ? title[0] : title, true, true); 579 580 $content.insertBefore(Helpers.createElement('h2', { 581 id: level === 0 ? `${navUniqId}-nav-title` : null, 582 class: level === 0 ? 'nav-title' : 'level-title' 583 }, _title), $content.firstChild); 584 585 // Nav ARIA title 586 if (level === 0 && typeof title === 'string') { 587 $nav.setAttribute('aria-labelledby', `${navUniqId}-nav-title`); 588 } 589 } 590 591 let menu_count = -1; 592 593 menu.forEach((nav, i_nav) => { 594 if (nav.tagName !== 'UL') { 595 $content.appendChild(nav.content); 596 return; 597 } 598 else { 599 menu_count++; 600 } 601 602 const $menu = Helpers.createElement('ul', { 603 id: nav.id 604 ? menu.length > 1 605 ? `menu-${nav.id}-${menu_count}` // sibling menus 606 : `menu-${nav.id}` 607 : null, 608 role: 'menu', 609 'aria-level': level + 1 610 }); 611 612 $content.appendChild($menu); 613 614 // keep original menu classes 615 if (Settings.keepClasses && nav.htmlClass) { 616 $menu.classList.add.apply($menu.classList, nav.htmlClass.split(' ')); 617 } 618 619 nav.items.forEach((item, i_item) => { 620 const $item_content = item.content; 621 622 // item has custom content 623 if (item.custom) { 624 625 const $custom_item = Helpers.createElement('li', {class: 'nav-item nav-item-custom'}, 626 Helpers.createElement('div', {class: 'nav-custom-content'}, Array.prototype.map.call($item_content, (el) => { 627 return Helpers.clone(el, true, true); 628 })) 629 ); 630 631 // keep original menu item classes 632 if (Settings.keepClasses && item.htmlClass) { 633 $custom_item.classList.add.apply($custom_item.classList, item.htmlClass.split(' ')); 634 } 635 636 // insert item 637 $menu.appendChild($custom_item); 638 639 // stop here 640 return; 641 } 642 643 const $original_link = Array.prototype.filter.call($item_content, (child) => { 644 return child.tagName === 'A' || (child.nodeType !== Node.TEXT_NODE && child.querySelector('a')); 645 })[0]; 646 647 let $item_link; 648 649 if ($original_link) { 650 $item_link = Helpers.clone($original_link, false, true); 651 $item_link.classList.add('nav-item-link'); 652 } 653 else { 654 $item_link = Helpers.createElement(item.subnav.length ? 'a' : 'span', { 655 class: 'nav-item-link' 656 }, Array.prototype.map.call($item_content, (el) => { 657 return Helpers.clone(el, true, true); 658 })); 659 } 660 661 if ($item_link.tagName === 'A') { 662 $item_link.setAttribute('tabindex', '0'); 663 $item_link.setAttribute('role', 'menuitem'); 664 665 if (!$item_link.getAttribute('href')) { 666 $item_link.setAttribute('href', '#'); 667 } 668 } 669 670 if ($original_link) { 671 $item_link.addEventListener('click', (e) => { 672 e.stopPropagation(); 673 674 // trigger original click event 675 if (Helpers.hasListener($original_link, 'click')) { 676 $original_link.click(); 677 } 678 }); 679 } 680 681 if ($item_link.getAttribute('href') === '#') { 682 // prevent page jumping 683 $item_link.addEventListener('click', Helpers.preventDefault); 684 } 685 686 // close nav on item click 687 if (Settings.closeOnClick) { 688 if (!areLevelsOpenable()) { 689 // every item should close the nav except disabled 690 if ( 691 $item_link.tagName === 'A' && 692 $item_link.dataset.navClose !== 'false' && 693 ($item_link.getAttribute('disabled') === null || $item_link.getAttribute('disabled') === 'false') 694 ) { 695 $item_link.addEventListener('click', closeNav); 696 } 697 } 698 else { 699 // only items without submenus 700 // or with submenus but with valid links 701 if ( 702 $item_link.tagName === 'A' && 703 $item_link.dataset.navClose !== 'false' && 704 ($item_link.getAttribute('disabled') === null || $item_link.getAttribute('disabled') === 'false') && 705 (!item.subnav.length || ($item_link.getAttribute('href') && $item_link.getAttribute('href').charAt(0) !== '#')) 706 ) { 707 $item_link.addEventListener('click', closeNav); 708 } 709 } 710 } 711 712 // our nav item 713 const $item = Helpers.createElement('li', { 714 class: 'nav-item' 715 }); 716 717 $item.appendChild($item_link); 718 $menu.appendChild($item); 719 720 // keep original menu item classes
721 if (Settings.keepClasses && item.htmlClass) { 722 $item.classList.add.apply($item.classList, item.htmlClass.split(' ')); 723 } 724 725 // is nav item highlighted? 726 if (item.highlight) { 727 $item.classList.add('nav-highlight'); 728 } 729 730 // wrap item link 731 Helpers.wrap($item_link, Helpers.createElement('div', {class: 'nav-item-wrapper'})); 732 733 // do subnav 734 if (item.subnav.length) { 735 const nextLevel = level + 1; 736 const uniqid = item.id; 737 let nav_title = ''; 738 739 // create new level 740 if (!_indexes[nextLevel]) { 741 _indexes[nextLevel] = 0; 742 } 743 744 // li parent class 745 $item.classList.add('nav-parent'); 746 747 if (!areLevelsOpenable()) { 748 $item_link.setAttribute('aria-expanded', true); 749 } 750 // if we can open levels 751 else { 752 const index = _indexes[nextLevel]; 753 754 const $checkbox = Helpers.createElement('input', { 755 type: 'checkbox', 756 id: `${navUniqId}-${nextLevel}-${index}`, 757 class: 'hc-chk', 758 tabindex: -1, 759 'data-level': nextLevel, 760 'data-index': index, 761 value: uniqid 762 }); 763 764 $checkbox.addEventListener('click', Helpers.stopPropagation); 765 $checkbox.addEventListener('change', checkboxChange); 766 $item.insertBefore($checkbox, $item.firstChild); 767 768 const attachToLink = ($el) => { 769 $el.addEventListener('click', (e) => { 770 e.stopPropagation(); 771 // trigger checkbox to toggle level 772 $checkbox.setAttribute('checked', $checkbox.getAttribute('checked') === 'true' ? false : true); 773 774 if ('createEvent' in document) { 775 const evt = document.createEvent('HTMLEvents'); 776 evt.initEvent('change', false, true); 777 $checkbox.dispatchEvent(evt); 778 } 779 }); 780 781 $el.addEventListener('keydown', function(e) { 782 if (e.key === 'Enter' || e.keyCode === 13) { 783 // remember we are accessing via keyboard 784 _keyboard = true; 785 _focusEls.push(this); 786 } 787 }); 788 789 // ARIA 790 $el.setAttribute('aria-controls', item.subnav.length > 1 791 ? item.subnav.filter(s => s.tagName === 'UL').map((s, i) => `menu-${s.id}-${i}`).join(' ') // sibling menus 792 : `menu-${uniqid}` 793 ); 794 $el.setAttribute('aria-haspopup', Settings.levelOpen === 'overlap'); 795 $el.setAttribute('aria-expanded', false); 796 } 797 798 // nav is updated, we should keep this level open 799 if (_openLevels.indexOf(uniqid) !== -1) { 800 $wrapper.classList.add('sub-level-open'); 801 $wrapper.addEventListener('click', () => closeLevel(nextLevel, index)); // close on self click 802 $item.classList.add('level-open'); 803 $checkbox.setAttribute('checked', true); 804 } 805 806 // subnav title 807 nav_title = Settings.levelTitles === true ? $item_content[0].textContent.trim() : ''; 808 809 // item has no actual link 810 if (!$item_link.getAttribute('href') || $item_link.getAttribute('href') === '#') { 811 $item_link.appendChild(Helpers.createElement('span', {class: 'nav-next'}, Helpers.createElement('span'))); 812 attachToLink($item_link); 813 } 814 // item has valid link, create our next link 815 else { 816 const $a_next = Helpers.createElement('a', { 817 href: '#', 818 class: 'nav-next', 819 'aria-label': `${(Settings.ariaLabels || {}).submenu}: ${nav_title}`, 820 role: 'menuitem', 821 tabindex: 0 822 }, Helpers.createElement('span')); 823 824 $a_next.addEventListener('click', Helpers.preventClick()); 825 attachToLink($a_next); 826 827 if (Settings.rtl) { 828 $item_link.parentNode.appendChild($a_next); 829 } 830 else { 831 $item_link.parentNode.insertBefore($a_next, $item_link.nextSibling); 832 } 833 } 834 } 835 836 _indexes[nextLevel]++; 837 838 createDom(item.subnav, $item, nextLevel, nav_title, _indexes[nextLevel]-1, typeof title === 'string' ? title : ''); 839 } 840 }); 841 }); 842 843 // insert back links 844 if (level && typeof backIndex !== 'undefined') { 845 if (Settings.insertBack !== false && Settings.levelOpen === 'overlap') { 846 const $children_menus = Helpers.children($content, 'ul');
847 const backLabel = (Settings.levelTitleAsBack ? (backTitle || Settings.labelBack) : Settings.labelBack) || ''; 848 const $back_a = Helpers.createElement('a', {href: '#', class: 'nav-back-button', role: 'menuitem', tabindex: 0}, [ 849 backLabel, 850 Helpers.createElement('span') 851 ]); 852 853 if (Settings.insertBack === true || Settings.insertBack === 0) { 854 const $back = Helpers.createElement('div', {class: 'nav-back'}, $back_a); 855 $content.insertBefore($back, Helpers.children($content, ':not(.level-title)')[0]); 856 } 857 else { 858 const $back = Helpers.createElement('li', {class: 'nav-item nav-back'}, $back_a); 859 Helpers.insertAt($back, Settings.insertBack === true ? 0 : Settings.insertBack, $children_menus); 860 } 861 862 const closeThisLevel = () => closeLevel(level, backIndex); 863 864 Helpers.wrap($back_a, Helpers.createElement('div', {class: 'nav-item-wrapper'})); 865 $back_a.addEventListener('click', Helpers.preventClick(closeThisLevel)); 866 $back_a.addEventListener('keydown', (e) => { 867 if (e.key === 'Enter' || e.keyCode === 13) { 868 // remember we are accessing via keyboard 869 _keyboard = true; 870 } 871 }); 872 } 873 } 874 875 // insert close button 876 if (level === 0 && Settings.insertClose !== false) { 877 const $close_a = Helpers.createElement('a', { 878 href: '#', 879 class: 'nav-close-button' + (Settings.labelClose ? ' has-label' : ''), 880 role: 'menuitem', 881 tabindex: 0, 882 'aria-label': !Settings.labelClose ? (Settings.ariaLabels || {}).close : '' 883 }, 884 [Settings.labelClose || '', Helpers.createElement('span')] 885 ); 886 887 $close_a.addEventListener('click', Helpers.preventClick(closeNav)); 888 $close_a.addEventListener('keydown', (e) => { 889 if (e.key === 'Enter' || e.keyCode === 13) { 890 untrapFocus(); 891 } 892 }); 893 894 if (title && Settings.insertClose === true) { 895 // after nav title 896 $content.insertBefore(Helpers.createElement('div', { 897 class: 'nav-close' 898 }, $close_a), $content.children[1]); 899 } 900 else if (Settings.insertClose === true) { 901 // before nav content 902 $content.insertBefore(Helpers.createElement('div', { 903 class: 'nav-close' 904 }, $close_a), $content.firstChild); 905 } 906 else { 907 // as menu item 908 const $nav_ul = Helpers.children($content, 'ul'); 909 const $close = Helpers.createElement('li', {class: 'nav-item nav-close'}, $close_a); 910 911 Helpers.wrap($close_a, Helpers.createElement('div', {class: 'nav-item-wrapper'})); 912 Helpers.insertAt($close, Settings.insertClose, $nav_ul); 913 } 914 } 915 } 916 }; 917 918 /* Touch swipe gestures */ 919 920 const touchStart = (target) => { 921 return (e) => { 922 if (Settings.position !== 'left' && Settings.position !== 'right') { 923 return; 924 } 925 926 _xStart = e.touches[0].clientX; 927 _yStart = e.touches[0].clientY; 928 929 // temporary attach touch listeners 930 if (target === 'doc') { 931 if (!_touchNavTriggered) { 932 document.addEventListener('touchmove', touchMove_open, Helpers.supportsPassive); 933 document.addEventListener('touchend', touchEnd_open, Helpers.supportsPassive); 934 } 935 } 936 else { 937 _touchNavTriggered = true; 938 $nav_container.addEventListener('touchmove', touchMove_close, Helpers.supportsPassive); 939 $nav_container.addEventListener('touchend', touchEnd_close, Helpers.supportsPassive); 940 } 941 }; 942 }; 943 944 const touchCaptureNav = (transNav, transContent) => { 945 window.addEventListener('touchmove', Helpers.preventDefault, Helpers.supportsPassive); // disable page scroll 946 $nav.style.visibility = 'visible'; 947 $nav_container.style[Helpers.browserPrefix('transition')] = 'none'; 948 Helpers.setTransform($nav_container, transNav, Settings.position); 949 950 if ($push_content) { 951 $push_content.style[Helpers.browserPrefix('transition')] = 'none'; 952 Helpers.setTransform($push_content, transContent, Settings.position); 953 } 954 }; 955 956 const touchReleaseNav = (action, timeoutVsb = true, transNav = false, transContent = false) => { 957 window.removeEventListener('touchmove', Helpers.preventDefault, Helpers.supportsPassive) // enable page scroll 958 $nav_container.style[Helpers.browserPrefix('transition')] = ''; 959 Helpers.setTransform($nav_container, transNav, Settings.position); 960 961 if ($push_content) { 962 $push_content.style[Helpers.browserPrefix('transition')] = ''; 963 Helpers.setTransform($push_content, transContent, Settings.position); 964 } 965 966 if (action === 'open') { 967 openNav(); 968 } 969 else { 970 closeNav(); 971 972 if (timeoutVsb) { 973 setTimeout(() => { 974 $nav.style.visibility = ''; 975 }, _transitionDuration); 976 } 977 else { 978 $nav.style.visibility = ''; 979 } 980 } 981 }; 982 983 const touchMove_open = (e) => { 984 let xDiff = 0 - (_xStart - e.touches[0].clientX); 985 const levelSpacing = Settings.levelOpen === 'overlap' ? activeLevel() * Settings.levelSpacing : 0; 986 const swipeWidth = _containerWidth + levelSpacing; 987 const maxStart = 50; // from the edge of the screen 988 989 if (Settings.position === 'left') { 990 xDiff = Math.min(Math.max(xDiff, 0), swipeWidth); 991 } 992 else { 993 xDiff = Math.abs(Math.min(Math.max(xDiff, -swipeWidth), 0)); 994 } 995 996 if ( 997 (Settings.position === 'left' && _xStart < maxStart) || // swipe right -> 998 (Settings.position === 'right' && _xStart > document.body.clientWidth - maxStart) // swipe left <- 999 ) { 1000 _touchMoved = true; 1001 touchCaptureNav(0 - (_containerWidth - xDiff), Math.abs(xDiff)); 1002 } 1003 }; 1004 1005 const touchEnd_open = (e) => { 1006 // remove touch listeners from document 1007 document.removeEventListener('touchmove', touchMove_open); 1008 document.removeEventListener('touchend', touchEnd_open); 1009 1010 if (!_touchMoved) { 1011 return; 1012 } 1013 1014 const lastTouch = e.changedTouches[e.changedTouches.length-1]; 1015 let xDiff = 0 - (_xStart - lastTouch.clientX); 1016 const levelSpacing = Settings.levelOpen === 'overlap' ? activeLevel() * Settings.levelSpacing : 0; 1017 const swipeWidth = _containerWidth + levelSpacing; 1018 const diffTrashold = 70; // swipe distance required 1019 1020 if (Settings.position === 'left') { 1021 xDiff = Math.min(Math.max(xDiff, 0), swipeWidth); 1022 } 1023 else { 1024 xDiff = Math.abs(Math.min(Math.max(xDiff, -swipeWidth), 0)); 1025 } 1026 1027 if (!xDiff) { 1028 touchReleaseNav('close', false); 1029 } 1030 else if (xDiff > diffTrashold) { 1031 touchReleaseNav('open'); 1032 } 1033 else { 1034 touchReleaseNav('close'); 1035 } 1036 1037 // reset touch 1038 _xStart = null; 1039 _yStart = null; 1040 _touchMoved = false;
1041 }; 1042 1043 const touchMove_close = (e) => { 1044 let xDiff = 0 - (_xStart - e.touches[0].clientX); 1045 let yDiff = 0 - (_yStart - e.touches[0].clientY); 1046 1047 if (Math.abs(xDiff) < Math.abs(yDiff)) { 1048 return; 1049 } 1050 1051 const levelSpacing = Settings.levelOpen === 'overlap' ? activeLevel() * Settings.levelSpacing : 0; 1052 const swipeWidth = _containerWidth + levelSpacing; 1053 1054 if (Settings.position === 'left') { 1055 xDiff = Math.min(Math.max(xDiff, -swipeWidth), 0); 1056 } 1057 else { 1058 xDiff = Math.min(Math.max(xDiff, 0), swipeWidth); 1059 } 1060 1061 if ( 1062 (Settings.position === 'left' && xDiff < 0) || // swipe right -> 1063 (Settings.position === 'right' && xDiff > 0) // swipe left <- 1064 ) { 1065 _touchMoved = true; 1066 touchCaptureNav(-Math.abs(xDiff) + levelSpacing, swipeWidth - Math.abs(xDiff)); 1067 } 1068 }; 1069 1070 const touchEnd_close = (e) => { 1071 // remove touch listeners from nav 1072 $nav_container.removeEventListener('touchmove', touchMove_close); 1073 $nav_container.removeEventListener('touchend', touchEnd_close); 1074 _touchNavTriggered = false; 1075 1076 if (!_touchMoved) { 1077 return; 1078 } 1079 1080 const lastTouch = e.changedTouches[e.changedTouches.length-1]; 1081 let xDiff = 0 - (_xStart - lastTouch.clientX); 1082 const levelSpacing = Settings.levelOpen === 'overlap' ? activeLevel() * Settings.levelSpacing : 0; 1083 const swipeWidth = _containerWidth + levelSpacing; 1084 const diffTrashold = 50; 1085 1086 if (Settings.position === 'left') { 1087 xDiff = Math.abs(Math.min(Math.max(xDiff, -swipeWidth), 0)); 1088 } 1089 else { 1090 xDiff = Math.abs(Math.min(Math.max(xDiff, 0), swipeWidth)); 1091 } 1092 1093 if (xDiff === swipeWidth) { 1094 touchReleaseNav('close', false); 1095 } 1096 else if (xDiff > diffTrashold) { 1097 touchReleaseNav('close'); 1098 } 1099 else { 1100 touchReleaseNav('open', true, levelSpacing, swipeWidth); 1101 } 1102 1103 // reset touch 1104 _xStart = null; 1105 _yStart = null; 1106 _touchMoved = false; 1107 }; 1108 1109 /* Setup our nav */ 1110 1111 // init nav 1112 initNav(); 1113 1114 // init our Model 1115 createModel(); 1116 1117 // create view from model 1118 createNavDom(); 1119 1120 // remove original nav 1121 if (Settings.removeOriginalNav === true) { 1122 $originalNav.parentNode.removeChild($originalNav); 1123 } 1124 else { 1125 // add classes to original menu so we know it's connected to our copy 1126 $originalNav.classList.add('hc-nav-original', navUniqId); 1127 } 1128 1129 // insert nav to DOM 1130 if (Settings.bodyInsert === 'prepend') { 1131 document.body.insertBefore($nav, document.body.firstChild); 1132 } 1133 else if (Settings.bodyInsert === 'append') { 1134 document.body.appendChild($nav); 1135 } 1136 1137 // opened nav right away 1138 if (Settings.expanded === true) { 1139 _initExpanded = true; // set flag 1140 openNav(); 1141 } 1142 1143 if (Settings.swipeGestures) { 1144 // close touch event on nav swipe 1145 // trigger before document touch 1146 $nav_container.addEventListener('touchstart', touchStart('nav'), Helpers.supportsPassive); 1147 // open touch event on document swipe 1148 document.addEventListener('touchstart', touchStart('doc'), Helpers.supportsPassive); 1149 } 1150 1151 // close levels on escape 1152 if (Settings.closeOnEsc) { 1153 document.addEventListener('keydown', checkEsc); 1154 } 1155 1156 // re-calculate our nav if window is resized 1157 const debounceResize = Helpers.debounce(calcNav, 500); 1158 window.addEventListener('resize', debounceResize, Helpers.supportsPassive); 1159 1160 /* Private methods */ 1161 1162 function checkEsc(e) { 1163 if (isOpen() && (e.key === 'Escape' || e.keyCode === 27)) { 1164 const level = activeLevel(); 1165 1166 if (level === 0) { 1167 closeNav(); 1168 untrapFocus(); 1169 } 1170 else { 1171 closeLevel(level, activeIndex()); 1172 trapFocus(null, level-1); 1173 } 1174 } 1175 }; 1176 1177 function checkboxChange() { 1178 const l = Number(this.dataset.level); 1179 const i = Number(this.dataset.index); 1180 1181 if (this.getAttribute('checked') === 'true') { 1182 openLevel(l, i); 1183 } 1184 else { 1185 closeLevel(l, i); 1186 } 1187 } 1188 1189 function closeToggles($t) { 1190 $t.classList.remove(activeToggleClass); 1191 $t.setAttribute('aria-expanded', false); 1192 }; 1193 1194 function areLevelsOpenable() { 1195 return Settings.levelOpen !== false && Settings.levelOpen !== 'none'; 1196 } 1197 1198 function isOpen() { 1199 return _open; 1200 } 1201 1202 function activeLevel() { 1203 return _openLevels.length 1204 ? Number( 1205 Array.prototype.filter.call($nav_container.querySelectorAll('.hc-chk'), (el) => { 1206 return el.value == _openLevels[_openLevels.length - 1]; 1207 })[0].dataset.level 1208 ) 1209 : 0; 1210 } 1211 1212 function activeIndex() { 1213 return _openLevels.length 1214 ? Number( 1215 Array.prototype.filter.call($nav_container.querySelectorAll('.hc-chk'), (el) => { 1216 return el.value == _openLevels[_openLevels.length - 1]; 1217 })[0].dataset.index 1218 ) 1219 : 0; 1220 } 1221 1222 function openNav(l, i) { 1223 // check if already open 1224 if (isOpen() && typeof i === 'undefined') { 1225 return; 1226 } 1227 1228 // open main nav 1229 _openNav(); 1230 1231 if (!areLevelsOpenable()) { 1232 return; 1233 } 1234 1235 let $checkbox; 1236 1237 if ((typeof l === 'number' || Helpers.isNumeric(l)) && (typeof i === 'number' || Helpers.isNumeric(i))) { 1238 1239 $checkbox = document.querySelector(`#${navUniqId}-${l}-${i}`); 1240 1241 if (!$checkbox) { 1242 console.warn(`HC Offcanvas Nav: level ${l} doesn't have index ${i}`); 1243 return; 1244 } 1245 } 1246 else if (_nextActiveLevel) { 1247 // get level to open from [data-nav-active] 1248 $checkbox = Array.prototype.filter.call($nav_container.querySelectorAll('.hc-chk'), (el) => { 1249 return el.value == _nextActiveLevel; 1250 })[0]; 1251 1252 // reset flag 1253 if (Settings.closeActiveLevel || !Settings.closeOpenLevels) { 1254 _nextActiveLevel = null; 1255 } 1256 } 1257 else if (Settings.closeOpenLevels === false) { 1258 // get last checked level 1259 $checkbox = Array.prototype.filter.call($nav_container.querySelectorAll('.hc-chk'), (el) => { 1260 return el.getAttribute('checked') === 'true'; 1261 }); 1262 $checkbox = $checkbox[$checkbox.length - 1]; 1263 } 1264 1265 // open sub levels as well 1266 if ($checkbox) { 1267 let levels = []; 1268 l = Number($checkbox.dataset.level); 1269 i = Number($checkbox.dataset.index); 1270 1271 if (l > 1) { 1272 const $parents = []; 1273 1274 for (; $checkbox && $checkbox !== document; $checkbox = $checkbox.parentNode) { 1275 if ($checkbox.matches('.nav-wrapper')) { 1276 $parents.push($checkbox); 1277 } 1278 continue; 1279 } 1280 1281 // get parent levels to open 1282 for (let i = 0; i < $parents.length; i++) { 1283 const $this = $parents[i]; 1284 const level = Number($this.dataset.level); 1285 1286 if (level > 0) { 1287 levels.push({ 1288 level: level, 1289 index: Number($this.dataset.index) 1290 }); 1291 } 1292 }; 1293 1294 levels = levels.reverse(); 1295 } 1296 1297 levels.push({ 1298 level: l, 1299 index: i 1300 }); 1301 1302 for (let n = 0; n < levels.length; n++) { 1303 // open each level without transition 1304 openLevel(levels[n].level, levels[n].index, false); 1305 } 1306 } 1307 } 1308 1309 function _openNav() { 1310 // check if already open 1311 if (isOpen()) { 1312 return; 1313 } 1314 1315 _open = true; 1316 1317 $nav.style.visibility = 'visible'; 1318 $nav.setAttribute('aria-hidden', false); 1319 $nav.classList.add(navOpenClass); 1320 1321 if ($toggle) { 1322 $toggle.forEach(closeToggles); 1323 1324 if ($toggle_open) { 1325 $toggle_open.classList.add(activeToggleClass); 1326 $toggle_open.setAttribute('aria-expanded', true); 1327 } 1328 } 1329 1330 if (Settings.levelOpen === 'expand' && _closeLevelsTimeout) { 1331 clearTimeout(_closeLevelsTimeout); 1332 } 1333 1334 if (Settings.disableBody) { 1335 // remember scroll position 1336 _top = window.pageYOffset || html.scrollTop || document.documentElement.scrollTop || document.body.scrollTop; 1337 1338 if (document.documentElement.scrollHeight > document.documentElement.clientHeight) { 1339 // has scroll bar 1340 html.classList.add('hc-nav-yscroll'); 1341 } 1342 1343 document.body.classList.add('hc-nav-open'); 1344 1345 if (_top) { 1346 // leave page in place 1347 document.body.style.top = `${-_top}
1347px`; 1348 } 1349 } 1350 1351 if ($push_content) { 1352 const transformVal = Helpers.getAxis(Settings.position) === 'x' ? _containerWidth : _containerHeight; 1353 Helpers.setTransform($push_content, transformVal, Settings.position); 1354 } 1355 1356 if (_initExpanded) { 1357 // reset flag 1358 _initExpanded = false; 1359 // don't trigger open event if nav is initially expanded 1360 return; 1361 } 1362 1363 // trigger "toggle" event 1364 if ($nav._eventListeners.toggle) { 1365 $nav._eventListeners.toggle.forEach((ev) => { 1366 ev.fn(Helpers.customEventObject('toggle', $nav, $nav, { 1367 action: 'open' 1368 }), Object.assign({}, Settings)); 1369 }); 1370 } 1371 1372 setTimeout(() => { 1373 // trigger "open" event 1374 if ($nav._eventListeners.open) { 1375 $nav._eventListeners.open.forEach((ev) => { 1376 ev.fn(Helpers.customEventObject('open', $nav, $nav), Object.assign({}, Settings)); 1377 }); 1378 } 1379 }, _transitionDuration); 1380 } 1381 1382 function closeNav() { 1383 // check if already closed 1384 if (!isOpen()) { 1385 return; 1386 } 1387 1388 _open = false; 1389 1390 if ($push_content) { 1391 Helpers.setTransform($push_content, false); 1392 } 1393 1394 $nav.classList.remove(navOpenClass); 1395 $nav.classList.remove('user-is-tabbing'); 1396 $nav.setAttribute('aria-hidden', true); 1397 $nav_container.removeAttribute('style'); 1398 1399 if ($toggle) { 1400 $toggle.forEach(closeToggles); 1401 } 1402 1403 if (Settings.levelOpen === 'expand' && ['top', 'bottom'].indexOf(Settings.position) !== -1) { 1404 // close all levels before closing the nav because the nav height changed 1405 closeLevel(0); 1406 } 1407 else if (areLevelsOpenable()) { 1408 // close all levels when nav closes 1409 _closeLevelsTimeout = setTimeout(() => { 1410 // keep timeout so we can prevent it if nav opens again before it's closed 1411 closeLevel(0); 1412 }, Settings.levelOpen === 'expand' ? _transitionDuration : 0); 1413 } 1414 1415 if (Settings.disableBody) { 1416 document.body.classList.remove('hc-nav-open'); 1417 html.classList.remove('hc-nav-yscroll'); 1418 1419 if (_top) { 1420 document.body.style.top = ''; 1421 document.body.scrollTop = _top; 1422 html.scrollTop = _top; 1423 1424 // for some reason we need timeout if position is bottom 1425 if (Settings.position === 'bottom') { 1426 const t = _top; 1427 setTimeout(() => { 1428 // reset page position 1429 document.body.scrollTop = t; 1430 html.scrollTop = t; 1431 }, 0); 1432 } 1433 1434 // reset top 1435 _top = 0; 1436 } 1437 } 1438 1439 // trigger "toggle" event 1440 if ($nav._eventListeners.toggle) { 1441 $nav._eventListeners.toggle.forEach((ev) => { 1442 ev.fn(Helpers.customEventObject('toggle', $nav, $nav, { 1443 action: 'close' 1444 }), Object.assign({}, Settings)); 1445 }); 1446 } 1447 1448 setTimeout(() => { 1449 $nav.style.visibility = ''; 1450 1451 // trigger "close" event 1452 if ($nav._eventListeners.close) { 1453 $nav._eventListeners.close.forEach((ev) => { 1454 ev.fn(Helpers.customEventObject('close', $nav, $nav), Object.assign({}, Settings)); 1455 }); 1456 } 1457 1458 // only trigger this "close" event once and then remove it 1459 if ($nav._eventListeners['close.once']) { 1460 $nav._eventListeners['close.once'].forEach((ev) => { 1461 ev.fn(Helpers.customEventObject('close.once', $nav, $nav), Object.assign({}, Settings)); 1462 }); 1463 } 1464 $nav.removeEventListener('close.once'); 1465 }, _transitionDuration); 1466 } 1467 1468 function toggleNav($t) { 1469 return (e) => { 1470 if (e) { 1471 e.preventDefault(); 1472 e.stopPropagation(); 1473 } 1474 1475 if ($t) { 1476 // remember which toggle button was pressed 1477 $toggle_open = $t; 1478 } 1479 1480 if (_open) closeNav(); 1481 else openNav(); 1482 } 1483 } 1484 1485 function openLevel(l, i, transition = true) { 1486 const $checkbox = document.querySelector(`#${navUniqId}-${l}-${i}`); 1487 const uniqid = $checkbox.value; 1488 const $li = $checkbox.parentNode; 1489 const $wrap = $li.closest('.nav-wrapper'); 1490 const $sub_wrap = Helpers.children($li, '.nav-wrapper')[0]; 1491 1492 if (transition === false) { 1493 // disable level transition
1494 $sub_wrap.style.transition = 'none'; 1495 } 1496 1497 $checkbox.setAttribute('checked', true); // ensure it is checked 1498 $wrap.classList.add('sub-level-open'); 1499 $li.classList.add('level-open'); 1500 $li.querySelectorAll('[aria-controls]')[0].setAttribute('aria-expanded', true); 1501 1502 if (transition === false) { 1503 setTimeout(() => { 1504 // re-enable level transition after nav open 1505 $sub_wrap.style.transition = ''; 1506 }, _transitionDuration); 1507 } 1508 1509 // remember what is open 1510 if (_openLevels.indexOf(uniqid) === -1) { 1511 _openLevels.push(uniqid); 1512 } 1513 1514 if (Settings.levelOpen === 'overlap') { 1515 // close on self click 1516 $wrap.addEventListener('click', () => closeLevel(l, i)); 1517 // expand the nav 1518 Helpers.setTransform($nav_container, l * Settings.levelSpacing, Settings.position); 1519 1520 // push content 1521 if ($push_content) { 1522 const transformVal = Helpers.getAxis(Settings.position) === 'x' ? _containerWidth : _containerHeight; 1523 Helpers.setTransform($push_content, transformVal + l * Settings.levelSpacing, Settings.position); 1524 } 1525 } 1526 1527 // trigger level open event 1528 if ($nav._eventListeners['open.level']) { 1529 $nav._eventListeners['open.level'].forEach((ev) => { 1530 ev.fn(Helpers.customEventObject('open.level', $nav, $sub_wrap, { 1531 currentLevel: l, 1532 currentIndex: i 1533 }), Object.assign({}, Settings)); 1534 }); 1535 } 1536 1537 if (_keyboard) { 1538 // trap focus inside level when keyboard accessing 1539 trapFocus(0, l, i); 1540 // reset keyboard flag 1541 _keyboard = false; 1542 } 1543 } 1544 1545 const _closeLevel = (l, i, transform) => { 1546 const $checkbox = document.querySelector(`#${navUniqId}-${l}-${i}`); 1547 1548 if (!$checkbox) return; 1549 1550 const uniqid = $checkbox.value; 1551 const $li = $checkbox.parentNode; 1552 const $wrap = $li.closest('.nav-wrapper'); 1553 1554 $checkbox.setAttribute('checked', false); // ensure it is unchecked 1555 $wrap.classList.remove('sub-level-open'); 1556 $li.classList.remove('level-open'); 1557 $li.querySelectorAll('[aria-controls]')[0].setAttribute('aria-expanded', false); 1558 1559 // this is not open anymore 1560 if (_openLevels.indexOf(uniqid) !== -1) { 1561 _openLevels.splice(_openLevels.indexOf(uniqid), 1); 1562 } 1563 1564 if (transform && Settings.levelOpen === 'overlap') { 1565 //level closed, remove wrapper click 1566 $wrap.removeEventListener('click'); 1567 $wrap.addEventListener('click', Helpers.stopPropagation); 1568 // collapse the nav 1569 Helpers.setTransform($nav_container, (l - 1) * Settings.levelSpacing, Settings.position); 1570 1571 // push back content 1572 if ($push_content) { 1573 const transformVal = Helpers.getAxis(Settings.position) === 'x' ? _containerWidth : _containerHeight; 1574 Helpers.setTransform($push_content, transformVal + (l - 1) * Settings.levelSpacing, Settings.position); 1575 } 1576 } 1577 }; 1578 1579 function closeLevel(l, i) { 1580 for (let level = l; level <= Object.keys(_indexes).length; level++) { 1581 if (level === l && typeof i !== 'undefined') { 1582 // close specified level with index 1583 _closeLevel(l, i, true); 1584 } 1585 else { 1586 if (l === 0 && !Settings.closeOpenLevels) { 1587 // do nothing 1588 } else { 1589 // close all sub sub levels 1590 for (let index = 0; index < _indexes[level]; index++) { 1591 _closeLevel(level, index, level === l); 1592 } 1593 } 1594 } 1595 } 1596 1597 // trigger level open event 1598 if (l > 0 && $nav._eventListeners['close.level']) { 1599 const $wrap = document.querySelector(`#${navUniqId}-${l}-${i}`).closest('.nav-wrapper'); 1600 1601 $nav._eventListeners['close.level'].forEach((ev) => { 1602 ev.fn(Helpers.customEventObject('close.level', $nav, $wrap, { 1603 currentLevel: l - 1, 1604 currentIndex: activeIndex() 1605 }), Object.assign({}, Settings)); 1606 }); 1607 } 1608 1609 if (_keyboard) { 1610 // trap focus back one level when keyboard accessing 1611 trapFocus(null, l-1); 1612 // reset keyboard flag 1613 _keyboard = false;
1614 } 1615 } 1616 1617 /* Public methods */ 1618 1619 $nav.on = (type, cb) => { 1620 $nav.addEventListener(type, cb); 1621 }; 1622 1623 $nav.off = (type, cb) => { 1624 $nav.removeEventListener(type, cb); 1625 }; 1626 1627 $nav.getSettings = () => Object.assign({}, Settings); 1628 1629 $nav.isOpen = isOpen; 1630 1631 $nav.open = openNav; 1632 1633 $nav.close = closeNav; 1634 1635 $nav.toggle = toggleNav(null); 1636 1637 $nav.update = (options, updateDom) => { 1638 // clear updated array 1639 UpdatedSettings = []; 1640 1641 // update our settings 1642 if (typeof options === 'object') { 1643 // only get what's been actually updated 1644 for (let prop in options) { 1645 if (Settings[prop] !== options[prop]) { 1646 UpdatedSettings.push(prop); 1647 } 1648 } 1649 1650 Settings = Object.assign({}, Settings, options); 1651 } 1652 1653 if (options === true || updateDom === true) { 1654 // can't update Model if original nav is removed 1655 if (Settings.removeOriginalNav) { 1656 console.warn('%c! HC Offcanvas Nav:' + `%c Can't update because original navigation has been removed. Disable \`removeOriginalNav\` option.`, 'color: #fa253b', 'color: default'); 1657 return; 1658 } 1659 1660 // hard update, reinit Model and DOM 1661 initNav(true); 1662 createModel(); 1663 createNavDom(true); 1664 } 1665 else { 1666 // soft update just reinit DOM from existing Model 1667 initNav(true); 1668 createNavDom(true); 1669 } 1670 }; 1671 1672 return $nav; // return our nav element 1673 }; 1674 1675 if (Array.isArray(elem) || elem instanceof NodeList) { 1676 const navs = []; 1677 // call the plugin on each element 1678 for (let i = 0; i < elem.length; i++) { 1679 navs.push(Plugin(elem[i])); 1680 } 1681 1682 return navs.length > 1 ? navs : navs[0]; 1683 } 1684 else { 1685 return Plugin(elem); 1686 } 1687 }; 1688 1689 // jQuery Plugin 1690 if (typeof window.jQuery !== 'undefined') { 1691 const $ = window.jQuery; 1692 const namespace = 'hcOffcanvasNav'; 1693 1694 $.fn.extend({ 1695 hcOffcanvasNav: function(args) { 1696 // check if selected element exist 1697 if (!this.length) return this; 1698 1699 return this.each(function() { 1700 let instance = $.data(this, namespace); 1701 1702 if (instance) { 1703 // already created, just update 1704 instance.update(args); 1705 } 1706 else { 1707 // create new instance 1708 instance = new hcOffcanvasNav(this, args); 1709 $.data(this, namespace, instance); 1710 } 1711 }); 1712 } 1713 }); 1714 } 1715 1716 // browser global 1717 window.hcOffcanvasNav = window.hcOffcanvasNav || hcOffcanvasNav; 1718 1719 return hcOffcanvasNav; 1720});
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.