1// Utility function 2function Util () {}; 3 4/* 5 class manipulation functions 6*/ 7Util.hasClass = function(el, className) { 8 return el.classList.contains(className); 9}; 10 11Util.addClass = function(el, className) { 12 var classList = className.split(' '); 13 el.classList.add(classList[0]); 14 if (classList.length > 1) Util.addClass(el, classList.slice(1).join(' ')); 15}; 16 17Util.removeClass = function(el, className) { 18 var classList = className.split(' '); 19 el.classList.remove(classList[0]); 20 if (classList.length > 1) Util.removeClass(el, classList.slice(1).join(' ')); 21}; 22 23Util.toggleClass = function(el, className, bool) { 24 if(bool) Util.addClass(el, className); 25 else Util.removeClass(el, className); 26}; 27 28Util.setAttributes = function(el, attrs) { 29 for(var key in attrs) { 30 el.setAttribute(key, attrs[key]); 31 } 32}; 33 34/* 35 DOM manipulation 36*/ 37Util.getChildrenByClassName = function(el, className) { 38 var children = el.children, 39 childrenByClass = []; 40 for (var i = 0; i < children.length; i++) { 41 if (Util.hasClass(children[i], className)) childrenByClass.push(children[i]); 42 } 43 return childrenByClass; 44}; 45 46Util.is = function(elem, selector) { 47 if(selector.nodeType){ 48 return elem === selector; 49 } 50 51 var qa = (typeof(selector) === 'string' ? document.querySelectorAll(selector) : selector), 52 length = qa.length, 53 returnArr = []; 54 55 while(length--){ 56 if(qa[length] === elem){ 57 return true; 58 } 59 } 60 61 return false; 62}; 63 64/* 65 Animate height of an element 66*/ 67Util.setHeight = function(start, to, element, duration, cb, timeFunction) { 68 var change = to - start, 69 currentTime = null; 70 71 var animateHeight = function(timestamp){ 72 if (!currentTime) currentTime = timestamp; 73 var progress = timestamp - currentTime; 74 if(progress > duration) progress = duration; 75 var val = parseInt((progress/duration)*change + start); 76 if(timeFunction) { 77 val = Math[timeFunction](progress, start, to - start, duration); 78 } 79 element.style.height = val+"px"; 80 if(progress < duration) { 81 window.requestAnimationFrame(animateHeight); 82 } else { 83 if(cb) cb(); 84 } 85 }; 86 87 //set the height of the element before starting animation -> fix bug on Safari 88 element.style.height = start+"px"; 89 window.requestAnimationFrame(animateHeight); 90}; 91 92/* 93 Smooth Scroll 94*/ 95 96Util.scrollTo = function(final, duration, cb, scrollEl) { 97 var element = scrollEl || window; 98 var start = element.scrollTop || document.documentElement.scrollTop, 99 currentTime = null; 100 101 if(!scrollEl) start = window.scrollY || document.documentElement.scrollTop; 102 103 var animateScroll = function(timestamp){ 104 if (!currentTime) currentTime = timestamp; 105 var progress = timestamp - currentTime; 106 if(progress > duration) progress = duration; 107 var val = Math.easeInOutQuad(progress, start, final-start, duration); 108 element.scrollTo(0, val); 109 if(progress < duration) { 110 window.requestAnimationFrame(animateScroll); 111 } else { 112 cb && cb(); 113 } 114 }; 115 116 window.requestAnimationFrame(animateScroll); 117}; 118 119/* 120 Focus utility classes 121*/ 122 123//Move focus to an element 124Util.moveFocus = function (element) { 125 if( !element ) element = document.getElementsByTagName("body")[0]; 126 element.focus(); 127 if (document.activeElement !== element) { 128 element.setAttribute('tabindex','-1'); 129 element.focus(); 130 } 131}; 132 133/* 134 Misc 135*/ 136 137Util.getIndexInArray = function(array, el) { 138 return Array.prototype.indexOf.call(array, el); 139}; 140 141Util.cssSupports = function(property, value) { 142 if('CSS' in window) { 143 return CSS.supports(property, value); 144 } else { 145 var jsProperty = property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase();}); 146 return jsProperty in document.body.style; 147 } 148}; 149 150// merge a set of user options into plugin defaults 151// https://gomakethings.com/vanilla-javascript-version-of-jquery-extend/ 152Util.extend = function() { 153 // Variables 154 var extended = {}; 155 var deep = false; 156 var i = 0; 157 var length = arguments.length; 158 159 // Check if a deep merge 160 if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 161 deep = arguments[0]; 162 i++; 163 } 164 165 // Merge the object into the extended object 166 var merge = function (obj) { 167 for ( var prop in obj ) { 168 if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 169 // If deep merge and property is an object, merge properties 170 if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 171 extended[prop] = extend( true, extended[prop], obj[prop] ); 172 } else { 173 extended[prop] = obj[prop]; 174 } 175 } 176 } 177 }; 178 179 // Loop through each object and conduct a merge 180 for ( ; i < length; i++ ) { 181 var obj = arguments[i]; 182 merge(obj); 183 } 184 185 return extended; 186}; 187 188// Check if Reduced Motion is enabled 189Util.osHasReducedMotion = function() { 190 if(!window.matchMedia) return false;
191 var matchMediaObj = window.matchMedia('(prefers-reduced-motion: reduce)'); 192 if(matchMediaObj) return matchMediaObj.matches; 193 return false; // return false if not supported 194}; 195 196/* 197 Polyfills 198*/ 199//Closest() method 200if (!Element.prototype.matches) { 201 Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 202} 203 204if (!Element.prototype.closest) { 205 Element.prototype.closest = function(s) { 206 var el = this; 207 if (!document.documentElement.contains(el)) return null; 208 do { 209 if (el.matches(s)) return el; 210 el = el.parentElement || el.parentNode; 211 } while (el !== null && el.nodeType === 1); 212 return null; 213 }; 214} 215 216//Custom Event() constructor 217if ( typeof window.CustomEvent !== "function" ) { 218 219 function CustomEvent ( event, params ) { 220 params = params || { bubbles: false, cancelable: false, detail: undefined }; 221 var evt = document.createEvent( 'CustomEvent' ); 222 evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); 223 return evt; 224 } 225 226 CustomEvent.prototype = window.Event.prototype; 227 228 window.CustomEvent = CustomEvent; 229} 230 231/* 232 Animation curves 233*/ 234Math.easeInOutQuad = function (t, b, c, d) { 235 t /= d/2; 236 if (t < 1) return c/2*t*t + b; 237 t--; 238 return -c/2 * (t*(t-2) - 1) + b; 239}; 240 241Math.easeInQuart = function (t, b, c, d) { 242 t /= d; 243 return c*t*t*t*t + b; 244}; 245 246Math.easeOutQuart = function (t, b, c, d) { 247 t /= d; 248 t--; 249 return -c * (t*t*t*t - 1) + b; 250}; 251 252Math.easeInOutQuart = function (t, b, c, d) { 253 t /= d/2; 254 if (t < 1) return c/2*t*t*t*t + b; 255 t -= 2; 256 return -c/2 * (t*t*t*t - 2) + b; 257}; 258 259Math.easeOutElastic = function (t, b, c, d) { 260 var s=1.70158;var p=d*0.7;var a=c; 261 if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; 262 if (a < Math.abs(c)) { a=c; var s=p/4; } 263 else var s = p/(2*Math.PI) * Math.asin (c/a); 264 return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; 265}; 266 267 268/* JS Utility Classes */ 269 270// make focus ring visible only for keyboard navigation (i.e., tab key) 271(function() { 272 var focusTab = document.getElementsByClassName('js-tab-focus'), 273 shouldInit = false, 274 outlineStyle = false, 275 eventDetected = false; 276 277 function detectClick() { 278 if(focusTab.length > 0) { 279 resetFocusStyle(false); 280 window.addEventListener('keydown', detectTab); 281 } 282 window.removeEventListener('mousedown', detectClick); 283 outlineStyle = false; 284 eventDetected = true; 285 }; 286 287 function detectTab(event) { 288 if(event.keyCode !== 9) return; 289 resetFocusStyle(true); 290 window.removeEventListener('keydown', detectTab); 291 window.addEventListener('mousedown', detectClick); 292 outlineStyle = true; 293 }; 294 295 function resetFocusStyle(bool) { 296 var outlineStyle = bool ? '' : 'none'; 297 for(var i = 0; i < focusTab.length; i++) { 298 focusTab[i].style.setProperty('outline', outlineStyle); 299 } 300 }; 301 302 function initFocusTabs() { 303 if(shouldInit) { 304 if(eventDetected) resetFocusStyle(outlineStyle); 305 return; 306 } 307 shouldInit = focusTab.length > 0; 308 window.addEventListener('mousedown', detectClick); 309 }; 310 311 initFocusTabs(); 312 window.addEventListener('initFocusTabs', initFocusTabs); 313}()); 314 315function resetFocusTabsStyle() { 316 window.dispatchEvent(new CustomEvent('initFocusTabs')); 317}; 318// File#: _1_diagonal-movement 319// Usage: codyhouse.co/license 320/* 321 Modified version of the jQuery-menu-aim plugin 322 https://github.com/kamens/jQuery-menu-aim 323 - Replaced jQuery with Vanilla JS 324 - Minor changes 325*/ 326(function () { 327 var menuAim = function (opts) { 328 init(opts); 329 }; 330 331 window.menuAim = menuAim; 332 333 function init(opts) { 334 var activeRow = null, 335 mouseLocs = [], 336 lastDelayLoc = null, 337 timeoutId = null, 338 options = Util.extend({ 339 menu: '', 340 rows: false, //if false, get direct children - otherwise pass nodes list 341 submenuSelector: "*", 342 submenuDirection: "right", 343 tolerance: 75, // bigger = more forgivey when entering submenu 344 enter: function () { }, 345 exit: function () { }, 346 activate: function () { }, 347 deactivate: function () { }, 348 exitMenu: function () { } 349 }, opts), 350 menu = options.menu; 351 352 var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track 353 DELAY = 300; // ms delay when user appears to be entering submenu 354 355 /** 356 * Keep track of the last few locations of the mouse. 357 */ 358 var mouseMoveFallback = function (event) { 359 (!window.requestAnimationFrame) ? mousemoveDocument(event) : window.requestAnimationFrame(function () { mousemoveDocument(event); }); 360 }; 361 362 var mousemoveDocument = function (e) { 363 mouseLocs.push({ x: e.pageX, y: e.pageY }); 364 365 if (mouseLocs.length > MOUSE_LOCS_TRACKED) { 366 mouseLocs.shift(); 367 } 368 }; 369 370 /** 371 * Cancel possible row activations when leaving the menu entirely 372 */ 373 var mouseleaveMenu = function () { 374 if (timeoutId) { 375 clearTimeout(timeoutId); 376 } 377 378 // If exitMenu is supplied and returns true, deactivate the 379 // currently active row on menu exit. 380 if (options.exitMenu(this)) { 381 if (activeRow) { 382 options.deactivate(activeRow); 383 } 384 385 activeRow = null; 386 } 387 }; 388 389 /** 390 * Trigger a possible row activation whenever entering a new row. 391 */ 392 var mouseenterRow = function () { 393 if (timeoutId) { 394 // Cancel any previous activation delays 395 clearTimeout(timeoutId); 396 } 397 398 options.enter(this); 399 possiblyActivate(this); 400 }, 401 mouseleaveRow = function () { 402 options.exit(this); 403 }; 404 405 /* 406 * Immediately activate a row if the user clicks on it. 407 */ 408 var clickRow = function () { 409 activate(this); 410 }; 411 412 /** 413 * Activate a menu row. 414 */ 415 var activate = function (row) { 416 if (row == activeRow) { 417 return; 418 } 419 420 if (activeRow) { 421 options.deactivate(activeRow); 422 } 423 424 options.activate(row); 425 activeRow = row; 426 }; 427 428 /** 429 * Possibly activate a menu row. If mouse movement indicates that we 430 * shouldn't activate yet because user may be trying to enter 431 * a submenu's content, then delay and check again later. 432 */ 433 var possiblyActivate = function (row) {
434 var delay = activationDelay(); 435 436 if (delay) { 437 timeoutId = setTimeout(function () { 438 possiblyActivate(row); 439 }, delay); 440 } else { 441 activate(row); 442 } 443 }; 444 445 /** 446 * Return the amount of time that should be used as a delay before the 447 * currently hovered row is activated. 448 * 449 * Returns 0 if the activation should happen immediately. Otherwise, 450 * returns the number of milliseconds that should be delayed before 451 * checking again to see if the row should be activated. 452 */ 453 var activationDelay = function () { 454 if (!activeRow || !Util.is(activeRow, options.submenuSelector)) { 455 // If there is no other submenu row already active, then 456 // go ahead and activate immediately. 457 return 0; 458 } 459 460 function getOffset(element) { 461 var rect = element.getBoundingClientRect(); 462 return { top: rect.top + window.pageYOffset, left: rect.left + window.pageXOffset }; 463 }; 464 465 var offset = getOffset(menu), 466 upperLeft = { 467 x: offset.left, 468 y: offset.top - options.tolerance 469 }, 470 upperRight = { 471 x: offset.left + menu.offsetWidth, 472 y: upperLeft.y 473 }, 474 lowerLeft = { 475 x: offset.left, 476 y: offset.top + menu.offsetHeight + options.tolerance 477 }, 478 lowerRight = { 479 x: offset.left + menu.offsetWidth, 480 y: lowerLeft.y 481 }, 482 loc = mouseLocs[mouseLocs.length - 1], 483 prevLoc = mouseLocs[0]; 484 485 if (!loc) { 486 return 0; 487 } 488 489 if (!prevLoc) { 490 prevLoc = loc; 491 } 492 493 if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { 494 // If the previous mouse location was outside of the entire 495 // menu's bounds, immediately activate. 496 return 0; 497 } 498 499 if (lastDelayLoc && loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) { 500 // If the mouse hasn't moved since the last time we checked 501 // for activation status, immediately activate. 502 return 0; 503 } 504 505 // Detect if the user is moving towards the currently activated 506 // submenu. 507 // 508 // If the mouse is heading relatively clearly towards 509 // the submenu's content, we should wait and give the user more 510 // time before activating a new row. If the mouse is heading 511 // elsewhere, we can immediately activate a new row. 512 // 513 // We detect this by calculating the slope formed between the 514 // current mouse location and the upper/lower right points of 515 // the menu. We do the same for the previous mouse location. 516 // If the current mouse location's slopes are 517 // increasing/decreasing appropriately compared to the 518 // previous's, we know the user is moving toward the submenu. 519 // 520 // Note that since the y-axis increases as the cursor moves 521 // down the screen, we are looking for the slope between the 522 // cursor and the upper right corner to decrease over time, not 523 // increase (somewhat counterintuitively). 524 function slope(a, b) { 525 return (b.y - a.y) / (b.x - a.x); 526 }; 527 528 var decreasingCorner = upperRight, 529 increasingCorner = lowerRight; 530 531 // Our expectations for decreasing or increasing slope values 532 // depends on which direction the submenu opens relative to the 533 // main menu. By default, if the menu opens on the right, we 534 // expect the slope between the cursor and the upper right 535 // corner to decrease over time, as explained above. If the 536 // submenu opens in a different direction, we change our slope 537 // expectations. 538 if (options.submenuDirection == "left") { 539 decreasingCorner = lowerLeft; 540 increasingCorner = upperLeft; 541 } else if (options.submenuDirection == "below") { 542 decreasingCorner = lowerRight; 543 increasingCorner = lowerLeft; 544 } else if (options.submenuDirection == "above") { 545 decreasingCorner = upperLeft; 546 increasingCorner = upperRight; 547 } 548 549 var decreasingSlope = slope(loc, decreasingCorner), 550 increasingSlope = slope(loc, increasingCorner), 551 prevDecreasingSlope = slope(prevLoc, decreasingCorner), 552 prevIncreasingSlope = slope(prevLoc, increasingCorner); 553 554 if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) { 555 // Mouse is moving from previous location towards the 556 // currently activated submenu. Delay before activating a 557 // new menu row, because user may be moving into submenu. 558 lastDelayLoc = loc; 559 return DELAY; 560 } 561 562 lastDelayLoc = null; 563 return 0; 564 }; 565 566 var reset = function (triggerDeactivate) { 567 if (timeoutId) { 568 clearTimeout(timeoutId); 569 } 570 571 if (activeRow && triggerDeactivate) { 572 options.deactivate(activeRow); 573 } 574 575 activeRow = null; 576 }; 577 578 var destroyInstance = function () { 579 menu.removeEventListener('mouseleave', mouseleaveMenu); 580 document.removeEventListener('mousemove', mouseMoveFallback); 581 if (rows.length > 0) { 582 for (var i = 0; i < rows.length; i++) { 583 rows[i].removeEventListener('mouseenter', mouseenterRow); 584 rows[i].removeEventListener('mouseleave', mouseleaveRow); 585 rows[i].removeEventListener('click', clickRow); 586 } 587 } 588 589 }; 590 591 /** 592 * Hook up initial menu events 593 */ 594 menu.addEventListener('mouseleave', mouseleaveMenu); 595 var rows = (options.rows) ? options.rows : menu.children; 596 if (rows.length > 0) { 597 for (var i = 0; i < rows.length; i++) { 598 (function (i) { 599 rows[i].addEventListener('mouseenter', mouseenterRow); 600 rows[i].addEventListener('mouseleave', mouseleaveRow); 601 rows[i].addEventListener('click', clickRow); 602 })(i); 603 } 604 } 605 606 document.addEventListener('mousemove', mouseMoveFallback); 607 608 /* Reset/destroy menu */ 609 menu.addEventListener('reset', function (event) { 610 reset(event.detail); 611 }); 612 menu.addEventListener('destroy', destroyInstance); 613 }; 614}()); 615 616 617// File#: _1_tabs 618// Usage: codyhouse.co/license 619(function () { 620 var Tab = function (element) { 621 this.element = element; 622 this.tabList = this.element.getElementsByClassName('js-tabs__controls')[0]; 623 this.listItems = this.tabList.getElementsByTagName('li'); 624 this.triggers = this.tabList.getElementsByTagName('a'); 625 this.panelsList = this.element.getElementsByClassName('js-tabs__panels')[0]; 626 this.panels = Util.getChildrenByClassName(this.panelsList, 'js-tabs__panel'); 627 this.hideClass = 'is-hidden'; 628 this.customShowClass = this.element.getAttribute('data-show-panel-class') ? this.element.getAttribute('data-show-panel-class') : false;
629 this.layout = this.element.getAttribute('data-tabs-layout') ? this.element.getAttribute('data-tabs-layout') : 'horizontal'; 630 // deep linking options 631 this.deepLinkOn = this.element.getAttribute('data-deep-link') == 'on'; 632 // init tabs 633 this.initTab(); 634 }; 635 636 Tab.prototype.initTab = function () { 637 //set initial aria attributes 638 this.tabList.setAttribute('role', 'tablist'); 639 for (var i = 0; i < this.triggers.length; i++) { 640 var bool = (i == 0), 641 panelId = this.panels[i].getAttribute('id'); 642 this.listItems[i].setAttribute('role', 'presentation'); 643 Util.setAttributes(this.triggers[i], { 'role': 'tab', 'aria-selected': bool, 'aria-controls': panelId, 'id': 'tab-' + panelId }); 644 Util.addClass(this.triggers[i], 'js-tabs__trigger'); 645 Util.setAttributes(this.panels[i], { 'role': 'tabpanel', 'aria-labelledby': 'tab-' + panelId }); 646 Util.toggleClass(this.panels[i], this.hideClass, !bool); 647 648 if (!bool) this.triggers[i].setAttribute('tabindex', '-1'); 649 } 650 651 //listen for Tab events 652 this.initTabEvents(); 653 654 // check deep linking option 655 this.initDeepLink(); 656 }; 657 658 Tab.prototype.initTabEvents = function () { 659 var self = this; 660 //click on a new tab -> select content 661 this.tabList.addEventListener('click', function (event) { 662 if (event.target.closest('.js-tabs__trigger')) self.triggerTab(event.target.closest('.js-tabs__trigger'), event); 663 }); 664 //arrow keys to navigate through tabs 665 this.tabList.addEventListener('keydown', function (event) { 666 ; 667 if (!event.target.closest('.js-tabs__trigger')) return; 668 if (tabNavigateNext(event, self.layout)) { 669 event.preventDefault(); 670 self.selectNewTab('next'); 671 } else if (tabNavigatePrev(event, self.layout)) { 672 event.preventDefault(); 673 self.selectNewTab('prev'); 674 } 675 }); 676 }; 677 678 Tab.prototype.selectNewTab = function (direction) { 679 var selectedTab = this.tabList.querySelector('[aria-selected="true"]'), 680 index = Util.getIndexInArray(this.triggers, selectedTab); 681 index = (direction == 'next') ? index + 1 : index - 1; 682 //make sure index is in the correct interval 683 //-> from last element go to first using the right arrow, from first element go to last using the left arrow 684 if (index < 0) index = this.listItems.length - 1; 685 if (index >= this.listItems.length) index = 0; 686 this.triggerTab(this.triggers[index]); 687 this.triggers[index].focus(); 688 }; 689 690 Tab.prototype.triggerTab = function (tabTrigger, event) { 691 var self = this; 692 event && event.preventDefault(); 693 var index = Util.getIndexInArray(this.triggers, tabTrigger); 694 //no need to do anything if tab was already selected 695 if (this.triggers[index].getAttribute('aria-selected') == 'true') return; 696 697 for (var i = 0; i < this.triggers.length; i++) { 698 var bool = (i == index); 699 Util.toggleClass(this.panels[i], this.hideClass, !bool); 700 if (this.customShowClass) Util.toggleClass(this.panels[i], this.customShowClass, bool); 701 this.triggers[i].setAttribute('aria-selected', bool); 702 bool ? this.triggers[i].setAttribute('tabindex', '0') : this.triggers[i].setAttribute('tabindex', '-1'); 703 } 704 705 // update url if deepLink is on 706 if (this.deepLinkOn) { 707 history.replaceState(null, '', '#' + tabTrigger.getAttribute('aria-controls')); 708 } 709 }; 710 711 Tab.prototype.initDeepLink = function () { 712 if (!this.deepLinkOn) return; 713 var hash = window.location.hash.substr(1); 714 var self = this; 715 if (!hash || hash == '') return; 716 for (var i = 0; i < this.panels.length; i++) { 717 if (this.panels[i].getAttribute('id') == hash) { 718 this.triggerTab(this.triggers[i], false); 719 setTimeout(function () { self.panels[i].scrollIntoView(true); }); 720 break; 721 } 722 }; 723 }; 724 725 function tabNavigateNext(event, layout) { 726 if (layout == 'horizontal' && (event.keyCode && event.keyCode == 39 || event.key && event.key == 'ArrowRight')) { return true; } 727 else if (layout == 'vertical' && (event.keyCode && event.keyCode == 40 || event.key && event.key == 'ArrowDown')) { return true; } 728 else { return false; } 729 }; 730 731 function tabNavigatePrev(event, layout) { 732 if (layout == 'horizontal' && (event.keyCode && event.keyCode == 37 || event.key && event.key == 'ArrowLeft')) { return true; } 733 else if (layout == 'vertical' && (event.keyCode && event.keyCode == 38 || event.key && event.key == 'ArrowUp')) { return true; } 734 else { return false; } 735 }; 736 737 window.Tab = Tab; 738 739 //initialize the Tab objects 740 var tabs = document.getElementsByClassName('js-tabs'); 741 if (tabs.length > 0) { 742 for (var i = 0; i < tabs.length; i++) { 743 (function (i) { new Tab(tabs[i]); })(i); 744 } 745 } 746}()); 747// File#: _2_dropdown 748// Usage: codyhouse.co/license 749(function () { 750 var Dropdown = function (element) { 751 this.element = element; 752 this.trigger = this.element.getElementsByClassName('js-dropdown__trigger')[0]; 753 this.dropdown = this.element.getElementsByClassName('js-dropdown__menu')[0]; 754 this.triggerFocus = false;
755 this.dropdownFocus = false; 756 this.hideInterval = false; 757 // sublevels 758 this.dropdownSubElements = this.element.getElementsByClassName('js-dropdown__sub-wrapper'); 759 this.prevFocus = false; // store element that was in focus before focus changed 760 this.addDropdownEvents(); 761 }; 762 763 Dropdown.prototype.addDropdownEvents = function () { 764 //place dropdown 765 var self = this; 766 this.placeElement(); 767 this.element.addEventListener('placeDropdown', function (event) { 768 self.placeElement(); 769 }); 770 // init dropdown 771 this.initElementEvents(this.trigger, this.triggerFocus); // this is used to trigger the primary dropdown 772 this.initElementEvents(this.dropdown, this.dropdownFocus); // this is used to trigger the primary dropdown 773 // init sublevels 774 this.initSublevels(); // if there are additional sublevels -> bind hover/focus events 775 }; 776 777 Dropdown.prototype.placeElement = function () { 778 // remove inline style first 779 this.dropdown.removeAttribute('style'); 780 // check dropdown position 781 var triggerPosition = this.trigger.getBoundingClientRect(), 782 isRight = (window.innerWidth < triggerPosition.left + parseInt(getComputedStyle(this.dropdown).getPropertyValue('width'))); 783 784 var xPosition = isRight ? 'right: 0px; left: auto;' : 'left: 0px; right: auto;'; 785 this.dropdown.setAttribute('style', xPosition); 786 }; 787 788 Dropdown.prototype.initElementEvents = function (element, bool) { 789 var self = this; 790 element.addEventListener('mouseenter', function () { 791 bool = true; 792 self.showDropdown(); 793 }); 794 element.addEventListener('focus', function () { 795 self.showDropdown(); 796 }); 797 element.addEventListener('mouseleave', function () { 798 bool = false;
799 self.hideDropdown(); 800 }); 801 element.addEventListener('focusout', function () { 802 self.hideDropdown(); 803 }); 804 }; 805 806 Dropdown.prototype.showDropdown = function () { 807 if (this.hideInterval) clearInterval(this.hideInterval); 808 // remove style attribute 809 this.dropdown.removeAttribute('style'); 810 this.placeElement(); 811 this.showLevel(this.dropdown, true); 812 }; 813 814 Dropdown.prototype.hideDropdown = function () { 815 var self = this; 816 if (this.hideInterval) clearInterval(this.hideInterval); 817 this.hideInterval = setTimeout(function () { 818 var dropDownFocus = document.activeElement.closest('.js-dropdown'), 819 inFocus = dropDownFocus && (dropDownFocus == self.element); 820 // if not in focus and not hover -> hide 821 if (!self.triggerFocus && !self.dropdownFocus && !inFocus) { 822 self.hideLevel(self.dropdown, true); 823 // make sure to hide sub/dropdown 824 self.hideSubLevels(); 825 self.prevFocus = false; 826 } 827 }, 300); 828 }; 829 830 Dropdown.prototype.initSublevels = function () { 831 var self = this; 832 var dropdownMenu = this.element.getElementsByClassName('js-dropdown__menu'); 833 for (var i = 0; i < dropdownMenu.length; i++) { 834 var listItems = dropdownMenu[i].children; 835 // bind hover 836 new menuAim({ 837 menu: dropdownMenu[i], 838 activate: function (row) { 839 var subList = row.getElementsByClassName('js-dropdown__menu')[0]; 840 if (!subList) return; 841 Util.addClass(row.querySelector('a'), 'dropdown__item--hover'); 842 self.showLevel(subList); 843 }, 844 deactivate: function (row) { 845 var subList = row.getElementsByClassName('dropdown__menu')[0]; 846 if (!subList) return; 847 Util.removeClass(row.querySelector('a'), 'dropdown__item--hover'); 848 self.hideLevel(subList); 849 }, 850 submenuSelector: '.js-dropdown__sub-wrapper', 851 }); 852 } 853 // store focus element before change in focus 854 this.element.addEventListener('keydown', function (event) { 855 if (event.keyCode && event.keyCode == 9 || event.key && event.key == 'Tab') { 856 self.prevFocus = document.activeElement; 857 } 858 }); 859 // make sure that sublevel are visible when their items are in focus 860 this.element.addEventListener('keyup', function (event) { 861 if (event.keyCode && event.keyCode == 9 || event.key && event.key == 'Tab') { 862 // focus has been moved -> make sure the proper classes are added to subnavigation 863 var focusElement = document.activeElement, 864 focusElementParent = focusElement.closest('.js-dropdown__menu'), 865 focusElementSibling = focusElement.nextElementSibling; 866 867 // if item in focus is inside submenu -> make sure it is visible 868 if (focusElementParent && !Util.hasClass(focusElementParent, 'dropdown__menu--is-visible')) { 869 self.showLevel(focusElementParent); 870 } 871 // if item in focus triggers a submenu -> make sure it is visible 872 if (focusElementSibling && !Util.hasClass(focusElementSibling, 'dropdown__menu--is-visible')) { 873 self.showLevel(focusElementSibling); 874 } 875 876 // check previous element in focus -> hide sublevel if required 877 if (!self.prevFocus) return; 878 var prevFocusElementParent = self.prevFocus.closest('.js-dropdown__menu'), 879 prevFocusElementSibling = self.prevFocus.nextElementSibling; 880 881 if (!prevFocusElementParent) return; 882 883 // element in focus and element prev in focus are siblings 884 if (focusElementParent && focusElementParent == prevFocusElementParent) { 885 if (prevFocusElementSibling) self.hideLevel(prevFocusElementSibling); 886 return; 887 } 888 889 // element in focus is inside submenu triggered by element prev in focus 890 if (prevFocusElementSibling && focusElementParent && focusElementParent == prevFocusElementSibling) return; 891 892 // shift tab -> element in focus triggers the submenu of the element prev in focus 893 if (focusElementSibling && prevFocusElementParent && focusElementSibling == prevFocusElementParent) return; 894 895 var focusElementParentParent = focusElementParent.parentNode.closest('.js-dropdown__menu'); 896 897 // shift tab -> element in focus is inside the dropdown triggered by a siblings of the element prev in focus 898 if (focusElementParentParent && focusElementParentParent == prevFocusElementParent) { 899 if (prevFocusElementSibling) self.hideLevel(prevFocusElementSibling); 900 return; 901 } 902 903 if (prevFocusElementParent && Util.hasClass(prevFocusElementParent, 'dropdown__menu--is-visible')) { 904 self.hideLevel(prevFocusElementParent); 905 } 906 } 907 }); 908 }; 909 910 Dropdown.prototype.hideSubLevels = function () { 911 var visibleSubLevels = this.dropdown.getElementsByClassName('dropdown__menu--is-visible'); 912 if (visibleSubLevels.length == 0) return; 913 while (visibleSubLevels[0]) { 914 this.hideLevel(visibleSubLevels[0]); 915 } 916 var hoveredItems = this.dropdown.getElementsByClassName('dropdown__item--hover'); 917 while (hoveredItems[0]) { 918 Util.removeClass(hoveredItems[0], 'dropdown__item--hover'); 919 } 920 }; 921 922 Dropdown.prototype.showLevel = function (level, bool) { 923 if (bool == undefined) { 924 //check if the sublevel needs to be open to the left 925 Util.removeClass(level, 'dropdown__menu--left'); 926 var boundingRect = level.getBoundingClientRect(); 927 if (window.innerWidth - boundingRect.right < 5 && boundingRect.left + window.scrollX > 2 * boundingRect.width) Util.addClass(level, 'dropdown__menu--left'); 928 } 929 Util.addClass(level, 'dropdown__menu--is-visible'); 930 Util.removeClass(level, 'dropdown__menu--is-hidden'); 931 }; 932 933 Dropdown.prototype.hideLevel = function (level, bool) { 934 if (!Util.hasClass(level, 'dropdown__menu--is-visible')) return; 935 Util.removeClass(level, 'dropdown__menu--is-visible'); 936 Util.addClass(level, 'dropdown__menu--is-hidden'); 937 938 level.addEventListener('transitionend', function cb(event) { 939 if (event.propertyName != 'opacity') return; 940 level.removeEventListener('transitionend', cb); 941 if (Util.hasClass(level, 'dropdown__menu--is-hidden')) Util.removeClass(level, 'dropdown__menu--is-hidden dropdown__menu--left'); 942 if (bool && !Util.hasClass(level, 'dropdown__menu--is-visible')) level.setAttribute('style', 'width: 0px; overflow: hidden;'); 943 }); 944 }; 945 946 window.Dropdown = Dropdown; 947 948 var dropdown = document.getElementsByClassName('js-dropdown'); 949 if (dropdown.length > 0) { // init Dropdown objects 950 for (var i = 0; i < dropdown.length; i++) { 951 (function (i) { new Dropdown(dropdown[i]); })(i); 952 } 953 } 954}()); 955// File#: _3_mega-site-navigation 956// Usage: codyhouse.co/license 957(function () { 958 var MegaNav = function (element) { 959 this.element = element; 960 this.search = this.element.getElementsByClassName('js-mega-nav__search'); 961 this.searchActiveController = false;
962 this.menu = this.element.getElementsByClassName('js-mega-nav__nav'); 963 this.menuItems = this.menu[0].getElementsByClassName('js-mega-nav__item'); 964 this.menuActiveController = false; 965 this.itemExpClass = 'mega-nav__item--expanded'; 966 this.classIconBtn = 'mega-nav__icon-btn--state-b'; 967 this.classSearchVisible = 'mega-nav__search--is-visible'; 968 this.classNavVisible = 'mega-nav__nav--is-visible'; 969 this.classMobileLayout = 'mega-nav--mobile'; 970 this.classDesktopLayout = 'mega-nav--desktop'; 971 this.layout = 'mobile'; 972 // store dropdown elements (if present) 973 this.dropdown = this.element.getElementsByClassName('js-dropdown'); 974 // expanded class - added to header when subnav is open 975 this.expandedClass = 'mega-nav--expanded'; 976 // check if subnav should open on hover 977 this.hover = this.element.getAttribute('data-hover') && this.element.getAttribute('data-hover') == 'on'; 978 initMegaNav(this); 979 }; 980 981 function initMegaNav(megaNav) { 982 setMegaNavLayout(megaNav); // switch between mobile/desktop layout 983 initSearch(megaNav); // controll search navigation 984 initMenu(megaNav); // control main menu nav - mobile only 985 initSubNav(megaNav); // toggle sub navigation visibility 986 987 megaNav.element.addEventListener('update-menu-layout', function (event) { 988 setMegaNavLayout(megaNav); // window resize - update layout 989 }); 990 }; 991 992 function setMegaNavLayout(megaNav) { 993 var layout = getComputedStyle(megaNav.element, ':before').getPropertyValue('content').replace(/\'|"/g, ''); 994 if (layout == megaNav.layout) return; 995 megaNav.layout = layout; 996 Util.toggleClass(megaNav.element, megaNav.classDesktopLayout, megaNav.layout == 'desktop'); 997 Util.toggleClass(megaNav.element, megaNav.classMobileLayout, megaNav.layout != 'desktop'); 998 if (megaNav.layout == 'desktop') { 999 closeSubNav(megaNav, false); 1000 // if the mega navigation has dropdown elements -> make sure they are in the right position (viewport awareness) 1001 triggerDropdownPosition(megaNav); 1002 } 1003 closeSearch(megaNav, false); 1004 resetMegaNavOffset(megaNav); // reset header offset top value 1005 resetNavAppearance(megaNav); // reset nav expanded appearance 1006 }; 1007 1008 function resetMegaNavOffset(megaNav) { 1009 document.documentElement.style.setProperty('--mega-nav-offset-y', megaNav.element.getBoundingClientRect().top + 'px'); 1010 }; 1011 1012 function closeNavigation(megaNav) { // triggered by Esc key press 1013 // close search 1014 closeSearch(megaNav); 1015 // close nav 1016 if (Util.hasClass(megaNav.menu[0], megaNav.classNavVisible)) { 1017 toggleMenu(megaNav, megaNav.menu[0], 'menuActiveController', megaNav.classNavVisible, megaNav.menuActiveController, true); 1018 } 1019 //close subnav 1020 closeSubNav(megaNav, false); 1021 resetNavAppearance(megaNav); // reset nav expanded appearance 1022 }; 1023 1024 function closeFocusNavigation(megaNav) { // triggered by Tab key pressed 1025 // close search when focus is lost 1026 if (Util.hasClass(megaNav.search[0], megaNav.classSearchVisible) && !document.activeElement.closest('.js-mega-nav__search')) { 1027 toggleMenu(megaNav, megaNav.search[0], 'searchActiveController', megaNav.classSearchVisible, megaNav.searchActiveController, true); 1028 } 1029 // close nav when focus is lost 1030 if (Util.hasClass(megaNav.menu[0], megaNav.classNavVisible) && !document.activeElement.closest('.js-mega-nav__nav')) { 1031 toggleMenu(megaNav, megaNav.menu[0], 'menuActiveController', megaNav.classNavVisible, megaNav.menuActiveController, true); 1032 } 1033 // close subnav when focus is lost 1034 for (var i = 0; i < megaNav.menuItems.length; i++) { 1035 if (!Util.hasClass(megaNav.menuItems[i], megaNav.itemExpClass)) continue; 1036 var parentItem = document.activeElement.closest('.js-mega-nav__item'); 1037 if (parentItem && parentItem == megaNav.menuItems[i]) continue; 1038 closeSingleSubnav(megaNav, i); 1039 } 1040 resetNavAppearance(megaNav); // reset nav expanded appearance 1041 }; 1042 1043 function closeSearch(megaNav, bool) { 1044 if (megaNav.search.length < 1) return; 1045 if (Util.hasClass(megaNav.search[0], megaNav.classSearchVisible)) { 1046 toggleMenu(megaNav, megaNav.search[0], 'searchActiveController', megaNav.classSearchVisible, megaNav.searchActiveController, bool); 1047 } 1048 }; 1049 1050 function initSearch(megaNav) { 1051 if (megaNav.search.length == 0) return; 1052 // toggle search 1053 megaNav.searchToggles = document.querySelectorAll('[aria-controls="' + megaNav.search[0].getAttribute('id') + '"]'); 1054 for (var i = 0; i < megaNav.searchToggles.length; i++) { 1055 (function (i) { 1056 megaNav.searchToggles[i].addEventListener('click', function (event) { 1057 // toggle search 1058 toggleMenu(megaNav, megaNav.search[0], 'searchActiveController', megaNav.classSearchVisible, megaNav.searchToggles[i], true); 1059 // close nav if it was open 1060 if (Util.hasClass(megaNav.menu[0], megaNav.classNavVisible)) { 1061 toggleMenu(megaNav, megaNav.menu[0], 'menuActiveController', megaNav.classNavVisible, megaNav.menuActiveController, false); 1062 } 1063 // close subnavigation if open 1064 closeSubNav(megaNav, false); 1065 resetNavAppearance(megaNav); // reset nav expanded appearance 1066 }); 1067 })(i); 1068 } 1069 }; 1070 1071 function initMenu(megaNav) { 1072 if (megaNav.menu.length == 0) return; 1073 // toggle nav 1074 megaNav.menuToggles = document.querySelectorAll('[aria-controls="' + megaNav.menu[0].getAttribute('id') + '"]'); 1075 for (var i = 0; i < megaNav.menuToggles.length; i++) { 1076 (function (i) { 1077 megaNav.menuToggles[i].addEventListener('click', function (event) { 1078 // toggle nav 1079 toggleMenu(megaNav, megaNav.menu[0], 'menuActiveController', megaNav.classNavVisible, megaNav.menuToggles[i], true); 1080 // close search if it was open 1081 if (Util.hasClass(megaNav.search[0], megaNav.classSearchVisible)) { 1082 toggleMenu(megaNav, megaNav.search[0], 'searchActiveController', megaNav.classSearchVisible, megaNav.searchActiveController, false); 1083 } 1084 resetNavAppearance(megaNav); // reset nav expanded appearance 1085 }); 1086 })(i); 1087 } 1088 }; 1089 1090 function toggleMenu(megaNav, element, controller, visibleClass, toggle, moveFocus) { 1091 var menuIsVisible = Util.hasClass(element, visibleClass); 1092 Util.toggleClass(element, visibleClass, !menuIsVisible); 1093 Util.toggleClass(toggle, megaNav.classIconBtn, !menuIsVisible); 1094 menuIsVisible ? toggle.removeAttribute('aria-expanded') : toggle.setAttribute('aria-expanded', 'true'); 1095 if (menuIsVisible) { 1096 if (toggle && moveFocus) toggle.focus(); 1097 megaNav[controller] = false;
1098 } else { 1099 if (toggle) megaNav[controller] = toggle; 1100 getFirstFocusable(element).focus(); // move focus to first focusable element 1101 } 1102 }; 1103 1104 function getFirstFocusable(element) { 1105 var focusableEle = element.querySelectorAll('[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable], audio[controls], video[controls], summary'), 1106 firstFocusable = false; 1107 for (var i = 0; i < focusableEle.length; i++) { 1108 if (focusableEle[i].offsetWidth || focusableEle[i].offsetHeight || focusableEle[i].getClientRects().length) { 1109 firstFocusable = focusableEle[i]; 1110 break; 1111 } 1112 } 1113 return firstFocusable; 1114 }; 1115 1116 function initSubNav(megaNav) { 1117 // toggle subnavigation visibility 1118 megaNav.element.addEventListener('click', function (event) { 1119 toggleSubNav(megaNav, event, 'click'); 1120 }); 1121 1122 if (megaNav.hover) { // data-hover="on" => use mouse events 1123 megaNav.element.addEventListener('mouseover', function (event) { 1124 if (megaNav.layout != 'desktop') return; 1125 toggleSubNav(megaNav, event, 'mouseover') 1126 }); 1127 1128 megaNav.element.addEventListener('mouseout', function (event) { 1129 if (megaNav.layout != 'desktop') return; 1130 var mainItem = event.target.closest('.js-mega-nav__item'); 1131 if (!mainItem) return; 1132 var triggerBtn = mainItem.getElementsByClassName('js-mega-nav__control'); 1133 if (triggerBtn.length < 1) return; 1134 var itemExpanded = Util.hasClass(mainItem, megaNav.itemExpClass); 1135 if (!itemExpanded) return; 1136 var mainItemHover = event.relatedTarget; 1137 if (mainItemHover && mainItem.contains(mainItemHover)) return; 1138 1139 Util.toggleClass(mainItem, megaNav.itemExpClass, !itemExpanded); 1140 itemExpanded ? triggerBtn[0].removeAttribute('aria-expanded') : triggerBtn[0].setAttribute('aria-expanded', 'true'); 1141 }); 1142 } 1143 }; 1144 1145 function toggleSubNav(megaNav, event, eventType) { 1146 var triggerBtn = event.target.closest('.js-mega-nav__control'); 1147 if (!triggerBtn) return; 1148 var mainItem = triggerBtn.closest('.js-mega-nav__item'); 1149 if (!mainItem) return; 1150 var itemExpanded = Util.hasClass(mainItem, megaNav.itemExpClass); 1151 if (megaNav.hover && itemExpanded && megaNav.layout == 'desktop' && eventType != 'click') return; 1152 Util.toggleClass(mainItem, megaNav.itemExpClass, !itemExpanded); 1153 itemExpanded ? triggerBtn.removeAttribute('aria-expanded') : triggerBtn.setAttribute('aria-expanded', 'true'); 1154 if (megaNav.layout == 'desktop' && !itemExpanded) closeSubNav(megaNav, mainItem); 1155 // close search if open 1156 closeSearch(megaNav, false); 1157 resetNavAppearance(megaNav); // reset nav expanded appearance 1158 }; 1159 1160 function closeSubNav(megaNav, selectedItem) { 1161 // close subnav when a new sub nav element is open 1162 if (megaNav.menuItems.length == 0) return; 1163 for (var i = 0; i < megaNav.menuItems.length; i++) { 1164 if (megaNav.menuItems[i] != selectedItem) closeSingleSubnav(megaNav, i); 1165 } 1166 }; 1167 1168 function closeSingleSubnav(megaNav, index) { 1169 Util.removeClass(megaNav.menuItems[index], megaNav.itemExpClass); 1170 var triggerBtn = megaNav.menuItems[index].getElementsByClassName('js-mega-nav__control'); 1171 if (triggerBtn.length > 0) triggerBtn[0].removeAttribute('aria-expanded'); 1172 }; 1173 1174 function triggerDropdownPosition(megaNav) { 1175 // emit custom event to properly place dropdown elements - viewport awarness 1176 if (megaNav.dropdown.length == 0) return; 1177 for (var i = 0; i < megaNav.dropdown.length; i++) { 1178 megaNav.dropdown[i].dispatchEvent(new CustomEvent('placeDropdown')); 1179 } 1180 }; 1181 1182 function resetNavAppearance(megaNav) { 1183 ((megaNav.element.getElementsByClassName(megaNav.itemExpClass).length > 0 && megaNav.layout == 'desktop') || megaNav.element.getElementsByClassName(megaNav.classSearchVisible).length > 0 || (megaNav.element.getElementsByClassName(megaNav.classNavVisible).length > 0 && megaNav.layout == 'mobile')) 1184 ? Util.addClass(megaNav.element, megaNav.expandedClass) 1185 : Util.removeClass(megaNav.element, megaNav.expandedClass); 1186 }; 1187 1188 //initialize the MegaNav objects 1189 var megaNav = document.getElementsByClassName('js-mega-nav'); 1190 if (megaNav.length > 0) { 1191 var megaNavArray = []; 1192 for (var i = 0; i < megaNav.length; i++) { 1193 (function (i) { megaNavArray.push(new MegaNav(megaNav[i])); })(i); 1194 } 1195 1196 // key events 1197 window.addEventListener('keyup', function (event) { 1198 if ((event.keyCode && event.keyCode == 27) || (event.key && event.key.toLowerCase() == 'escape')) { // listen for esc key events 1199 for (var i = 0; i < megaNavArray.length; i++) { 1200 (function (i) { 1201 closeNavigation(megaNavArray[i]); 1202 })(i); 1203 } 1204 } 1205 // listen for tab key 1206 if ((event.keyCode && event.keyCode == 9) || (event.key && event.key.toLowerCase() == 'tab')) { // close search or nav if it looses focus 1207 for (var i = 0; i < megaNavArray.length; i++) { 1208 (function (i) { 1209 closeFocusNavigation(megaNavArray[i]); 1210 })(i); 1211 } 1212 } 1213 }); 1214 1215 window.addEventListener('click', function (event) { 1216 if (!event.target.closest('.js-mega-nav')) closeNavigation(megaNavArray[0]); 1217 }); 1218 1219 // resize - update menu layout 1220 var resizingId = false, 1221 customEvent = new CustomEvent('update-menu-layout'); 1222 window.addEventListener('resize', function (event) { 1223 clearTimeout(resizingId); 1224 resizingId = setTimeout(doneResizing, 200); 1225 }); 1226 1227 function doneResizing() { 1228 for (var i = 0; i < megaNavArray.length; i++) { 1229 (function (i) { megaNavArray[i].element.dispatchEvent(customEvent) })(i); 1230 }; 1231 }; 1232 1233 (window.requestAnimationFrame) // init mega site nav layout 1234 ? window.requestAnimationFrame(doneResizing) 1235 : doneResizing(); 1236 } 1237}());
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.