1// https://github.com/arielsalminen/responsive-nav.js but extended taken from MTM project 2 3(function (document, window, index) { 4 // Index is used to keep multiple navs on the same page namespaced 5 6 "use strict"; 7 8 var responsiveNav = function (el, options) { 9 10 var computed = !!window.getComputedStyle; 11 var elemsByClassName = !!document.getElementsByClassName; 12 13 /** 14 * getComputedStyle polyfill for old browsers 15 */ 16 if (!computed) { 17 window.getComputedStyle = function (el) { 18 this.el = el; 19 this.getPropertyValue = function (prop) { 20 var re = /(\-([a-z]){1})/g; 21 if (prop === "float") { 22 prop = "styleFloat"; 23 } 24 if (re.test(prop)) { 25 prop = prop.replace(re, function () { 26 return arguments[2].toUpperCase(); 27 }); 28 } 29 return el.currentStyle[prop] ? el.currentStyle[prop] : null; 30 }; 31 return this; 32 }; 33 } 34 35 /** 36 * GetElementsByClassName polyfill for old browsers 37 */ 38 if (!elemsByClassName) { 39 document.getElementsByClassName = function (match) { 40 var result = [], 41 elements = document.getElementsByTagName("*"), 42 i, elem; 43 match = " " + match + " "; 44 for (i = 0; i < elements.length; i++) { 45 elem = elements[i]; 46 if ((" " + (elem.className || elem.getAttribute("class")) + " ").indexOf(match) > -1) { 47 result.push(elem); 48 } 49 } 50 return result; 51 }; 52 } 53 /* exported addEvent, removeEvent, getChildren, setAttributes, addClass, removeClass, hasClass, forEach */ 54 55 /** 56 * Add Event 57 * fn arg can be an object or a function, thanks to handleEvent 58 * read more at: http://www.thecssninja.com/javascript/handleevent 59 * 60 * @param {element} element 61 * @param {event} event 62 * @param {Function} fn 63 * @param {boolean} bubbling 64 */ 65 var addEvent = function (el, evt, fn, bubble) { 66 if ("addEventListener" in el) { 67 // BBOS6 doesn't support handleEvent, catch and polyfill 68 try { 69 el.addEventListener(evt, fn, bubble); 70 } catch (e) { 71 if (typeof fn === "object" && fn.handleEvent) { 72 el.addEventListener(evt, function (e) { 73 // Bind fn as this and set first arg as event object 74 fn.handleEvent.call(fn, e); 75 }, bubble); 76 } else { 77 throw e; 78 } 79 } 80 } else if ("attachEvent" in el) { 81 // check if the callback is an object and contains handleEvent 82 if (typeof fn === "object" && fn.handleEvent) { 83 el.attachEvent("on" + evt, function () { 84 // Bind fn as this 85 fn.handleEvent.call(fn); 86 }); 87 } else { 88 el.attachEvent("on" + evt, fn); 89 } 90 } 91 }, 92 93 /** 94 * Remove Event 95 * 96 * @param {element} element 97 * @param {event} event 98 * @param {Function} fn 99 * @param {boolean} bubbling 100 */ 101 removeEvent = function (el, evt, fn, bubble) { 102 if ("removeEventListener" in el) { 103 try { 104 el.removeEventListener(evt, fn, bubble); 105 } catch (e) { 106 if (typeof fn === "object" && fn.handleEvent) { 107 el.removeEventListener(evt, function (e) { 108 fn.handleEvent.call(fn, e); 109 }, bubble); 110 } else { 111 throw e; 112 } 113 } 114 } else if ("detachEvent" in el) { 115 if (typeof fn === "object" && fn.handleEvent) { 116 el.detachEvent("on" + evt, function () { 117 fn.handleEvent.call(fn); 118 }); 119 } else { 120 el.detachEvent("on" + evt, fn); 121 } 122 } 123 }, 124 125 /** 126 * Get the children of any element 127 * 128 * @param {element} 129 * @return {array} Returns matching elements in an array 130 */ 131 getChildren = function (e) { 132 if (e.children.length < 1) { 133 throw new Error("The Nav container has no containing elements"); 134 } 135 // Store all children in array 136 var children = []; 137 // Loop through children and store in array if child != TextNode 138 for (var i = 0; i < e.children.length; i++) { 139 if (e.children[i].nodeType === 1) { 140 children.push(e.children[i]); 141 } 142 } 143 return children; 144 }, 145 146 /** 147 * Sets multiple attributes at once 148 * 149 * @param {element} element 150 * @param {attrs} attrs 151 */ 152 setAttributes = function (el, attrs) { 153 for (var key in attrs) { 154 el.setAttribute(key, attrs[key]); 155 } 156 }, 157 158 /** 159 * Adds a class to any element 160 * 161 * @param {element} element 162 * @param {string} class 163 */ 164 addClass = function (el, cls) { 165 if (!hasClass(el, cls)) { 166 el.className += " " + cls; 167 el.className = el.className.replace(/(^\s*)|(\s*$)/g, ""); 168 } 169 }, 170 171 /** 172 * Remove a class from any element 173 * 174 * @param {element} element 175 * @param {string} class 176 */ 177 removeClass = function (el, cls) { 178 var reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); 179 el.className = el.className.replace(reg, " ").replace(/(^\s*)|(\s*$)/g, ""); 180 }, 181 182 /** 183 * Checks if an element has certain class 184 * 185 * @param {element} element 186 * @param {string} class name 187 * @return {Boolean} 188 */ 189 hasClass = function (el, cls) { 190 return el.className && new RegExp("(\\s|^)" + cls + "(\\s|$)").test(el.className); 191 }, 192 193 /** 194 * forEach method that passes back the stuff we need 195 * 196 * @param {array} array 197 * @param {Function} callback 198 * @param {scope} scope 199 */
200 forEach = function (array, callback, scope) { 201 for (var i = 0; i < array.length; i++) { 202 callback.call(scope, i, array[i]); 203 } 204 }; 205 206 var nav, 207 opts, 208 navToggle, 209 dropdownToggle, 210 styleElement = document.createElement("style"), 211 htmlEl = document.documentElement, 212 hasAnimFinished, 213 isMobile, 214 navOpen; 215 216 var ResponsiveNav = function (el, options) { 217 var i; 218 219 220 /** 221 * Default options 222 * @type {Object} 223 */ 224 this.options = { 225 animate: true, // Boolean: Use CSS3 transitions, true or false 226 transition: 284, // Integer: Speed of the transition, in milliseconds 227 label: "Menu", // String: Label for the navigation toggle 228 insert: "before", // String: Insert the toggle before or after the navigation 229 customToggle: "", // Selector: Specify the ID of a custom toggle 230 closeOnNavClick: false, // Boolean: Close the navigation when one of the links are clicked 231 openPos: "relative", // String: Position of the opened nav, relative or static 232 navClass: "nav-collapse", // String: Default CSS class. If changed, you need to edit the CSS too! 233 navActiveClass: "js-nav-active", // String: Class that is added to <html> element when nav is active 234 jsClass: "js", // String: 'JS enabled' class which is added to <html> element 235 init: function () { }, // Function: Init callback 236 open: function () { }, // Function: Open callback 237 close: function () { } // Function: Close callback 238 }; 239 240 // User defined options 241 for (i in options) { 242 this.options[i] = options[i]; 243 } 244 245 // Adds "js" class for <html> 246 addClass(htmlEl, this.options.jsClass); 247 248 // Wrapper 249 this.wrapperEl = el.replace("#", ""); 250 251 // Try selecting ID first 252 if (document.getElementById(this.wrapperEl)) { 253 this.wrapper = document.getElementById(this.wrapperEl); 254 255 // If element with an ID doesn't exist, use querySelector 256 } else if (document.querySelector(this.wrapperEl)) { 257 this.wrapper = document.querySelector(this.wrapperEl); 258 259 // If element doesn't exists, stop here. 260 } else { 261 throw new Error("The nav element you are trying to select doesn't exist"); 262 } 263 264 // Inner wrapper 265 this.wrapper.inner = getChildren(this.wrapper); 266 267 // For minification 268 opts = this.options; 269 nav = this.wrapper; 270 271 // Init 272 this._init(this); 273 }; 274 275 ResponsiveNav.prototype = { 276 277 /** 278 * Unattaches events and removes any classes that were added 279 */ 280 destroy: function () { 281 this._removeStyles(); 282 removeClass(nav, "closed"); 283 removeClass(nav, "opened"); 284 removeClass(nav, opts.navClass); 285 removeClass(nav, opts.navClass + "-" + this.index); 286 removeClass(htmlEl, opts.navActiveClass); 287 nav.removeAttribute("style"); 288 nav.removeAttribute("aria-hidden"); 289 290 removeEvent(window, "resize", this, false); 291 removeEvent(document.body, "touchmove", this, false); 292 removeEvent(navToggle, "touchstart", this, false); 293 removeEvent(navToggle, "touchend", this, false); 294 removeEvent(navToggle, "mouseup", this, false); 295 removeEvent(navToggle, "keyup", this, false); 296 removeEvent(navToggle, "click", this, false); 297 298 var self = this; 299 forEach(dropdownToggle, function (i, el) { 300 removeEvent(el, "touchstart", self, false); 301 removeEvent(el, "touchend", self, false); 302 removeEvent(el, "mouseup", self, false); 303 removeEvent(el, "keyup", self, false); 304 removeEvent(el, "click", self, false); 305 }); 306 307 if (!opts.customToggle) { 308 navToggle.parentNode.removeChild(navToggle); 309 } else { 310 navToggle.removeAttribute("aria-hidden"); 311 } 312 }, 313 314 /** 315 * Toggles the navigation open/close 316 */ 317 toggle: function () { 318 if (hasAnimFinished === true) { 319 if (!navOpen) { 320 this.open(); 321 } else { 322 this.close(); 323 } 324 325 // Enable pointer events again 326 this._enablePointerEvents(); 327 } 328 }, 329 330 /** 331 * Opens the navigation 332 */ 333 open: function () { 334 if (!navOpen) { 335 removeClass(nav, "closed"); 336 addClass(nav, "opened"); 337 addClass(htmlEl, opts.navActiveClass); 338 addClass(navToggle, "active"); 339 nav.style.position = opts.openPos; 340 setAttributes(nav, { "aria-hidden": "false" }); 341 navOpen = true; 342 opts.open(); 343 } 344 }, 345 346 /** 347 * Closes the navigation 348 */ 349 close: function () { 350 if (navOpen) { 351 addClass(nav, "closed"); 352 removeClass(nav, "opened"); 353 removeClass(htmlEl, opts.navActiveClass); 354 removeClass(navToggle, "active"); 355 setAttributes(nav, { "aria-hidden": "true" }); 356 357 // If animations are enabled, wait until they finish 358 if (opts.animate) { 359 hasAnimFinished = false;
360 setTimeout(function () { 361 nav.style.position = "absolute"; 362 hasAnimFinished = true; 363 removeClass(nav, "dropdown-active"); 364 forEach(dropdownToggle, function (i, el) { 365 removeClass(el.parentNode, "opened"); 366 }); 367 }, opts.transition + 10); 368 369 // Animations aren't enabled, we can do these immediately 370 } else { 371 nav.style.position = "absolute"; 372 removeClass(nav, "dropdown-active"); 373 forEach(dropdownToggle, function (i, el) { 374 removeClass(el.parentNode, "opened"); 375 }); 376 } 377 378 navOpen = false; 379 opts.close(); 380 } 381 }, 382 383 /** 384 * Resize is called on window resize and orientation change. 385 * It initializes the CSS styles and height calculations. 386 */ 387 resize: function () { 388 389 // Resize watches navigation toggle's display state 390 if (window.getComputedStyle(navToggle, null).getPropertyValue("display") !== "none") { 391 392 isMobile = true; 393 setAttributes(navToggle, { "aria-hidden": "false" }); 394 395 // If the navigation is hidden 396 if (nav.className.match(/(^|\s)closed(\s|$)/)) { 397 setAttributes(nav, { "aria-hidden": "true" }); 398 nav.style.position = "absolute"; 399 } 400 401 this._createStyles(); 402 this._calcHeight(); 403 } else { 404 405 isMobile = false; 406 setAttributes(navToggle, { "aria-hidden": "true" }); 407 setAttributes(nav, { "aria-hidden": "false" }); 408 nav.style.position = opts.openPos; 409 removeClass(nav, "dropdown-active"); 410 forEach(dropdownToggle, function (i, el) { 411 removeClass(el.parentNode, "opened"); 412 }); 413 this._removeStyles(); 414 } 415 }, 416 417 /** 418 * Takes care of all even handling 419 * 420 * @param {event} event 421 * @return {type} returns the type of event that should be used 422 */ 423 handleEvent: function (e) { 424 var evt = e || window.event; 425 426 switch (evt.type) { 427 case "touchstart": 428 this._onTouchStart(evt); 429 break; 430 case "touchmove": 431 this._onTouchMove(evt); 432 break; 433 case "touchend": 434 case "mouseup": 435 this._onTouchEnd(evt); 436 break; 437 case "click": 438 this._preventDefault(evt); 439 break; 440 case "keyup": 441 this._onKeyUp(evt); 442 break; 443 case "resize": 444 this.resize(evt); 445 break; 446 } 447 }, 448 449 /** 450 * Initializes the widget 451 */ 452 _init: function () { 453 this.index = index++; 454 455 addClass(nav, opts.navClass); 456 addClass(nav, opts.navClass + "-" + this.index); 457 addClass(nav, "closed"); 458 hasAnimFinished = true; 459 navOpen = false; 460 461 this._closeOnNavClick(); 462 this._createToggle(); 463 this._createDropdowns(); 464 this._transitions(); 465 this.resize(); 466 467 /** 468 * On IE8 the resize event triggers too early for some reason 469 * so it's called here again on init to make sure all the 470 * calculated styles are correct. 471 */ 472 var self = this; 473 setTimeout(function () { 474 self.resize(); 475 }, 20); 476 477 addEvent(window, "resize", this, false); 478 addEvent(document.body, "touchmove", this, false); 479 addEvent(navToggle, "touchstart", this, false); 480 addEvent(navToggle, "touchend", this, false); 481 addEvent(navToggle, "mouseup", this, false); 482 addEvent(navToggle, "keyup", this, false); 483 addEvent(navToggle, "click", this, false); 484 485 /** 486 * Init callback here 487 */ 488 opts.init(); 489 }, 490 491 /** 492 * Creates Styles to the <head> 493 */ 494 _createStyles: function () { 495 if (!styleElement.parentNode) { 496 styleElement.type = "text/css"; 497 document.getElementsByTagName("head")[0].appendChild(styleElement); 498 } 499 }, 500 501 /** 502 * Removes styles from the <head> 503 */ 504 _removeStyles: function () { 505 if (styleElement.parentNode) styleElement.parentNode.removeChild(styleElement); 506 }, 507 508 /** 509 * Creates Navigation Toggle 510 */ 511 _createToggle: function () { 512 513 // If there's no toggle, let's create one 514 if (!opts.customToggle) { 515 var toggle = document.createElement("a"); 516 toggle.innerHTML = opts.label; 517 setAttributes(toggle, { 518 "href": "#", 519 "class": "nav-toggle" 520 }); 521 522 // Determine where to insert the toggle 523 if (opts.insert === "after") { 524 nav.parentNode.insertBefore(toggle, nav.nextSibling); 525 } else { 526 nav.parentNode.insertBefore(toggle, nav); 527 } 528 529 navToggle = toggle; 530 531 // There is a toggle already, let's use that one 532 } else { 533 var toggleEl = opts.customToggle.replace("#", ""); 534 535 if (document.getElementById(toggleEl)) { 536 navToggle = document.getElementById(toggleEl); 537 } else if (document.querySelector(toggleEl)) { 538 navToggle = document.querySelector(toggleEl); 539 } else { 540 throw new Error("The custom nav toggle you are trying to select doesn't exist"); 541 } 542 } 543 }, 544 545 /** 546 * Attaches event listeners to all sub-navigation toggles 547 */ 548 _createDropdowns: function () { 549 var self = this; 550 551 // If getElementsByClassName is supported 552 if (elemsByClassName) { 553 dropdownToggle = nav.getElementsByClassName("dropdown-toggle"); 554 555 // If not, use polyfill version (on old IEs) 556 } else { 557 dropdownToggle = document.getElementsByClassName("dropdown-toggle"); 558 } 559
560 forEach(dropdownToggle, function (i, el) { 561 addEvent(el, "touchstart", self, false); 562 addEvent(el, "touchend", self, false); 563 addEvent(el, "mouseup", self, false); 564 addEvent(el, "keyup", self, false); 565 addEvent(el, "click", self, false); 566 }); 567 }, 568 569 /** 570 * Toggles sub-navigations open/closed 571 * 572 * @param {element} The toggle that was tapped 573 */ 574 _toggleDropdown: function (targetEl) { 575 576 // Enable active class to let the navigation expand over 577 // the calculated max height 578 addClass(nav, "dropdown-active"); 579 580 // Check if the sub-navigation is inside another sub-nav 581 var parentEl = targetEl.parentNode, 582 isInsideSub = hasClass(parentEl.parentNode.parentNode, "dropdown"); 583 584 // If it's already open, close it 585 if (hasClass(parentEl, "opened")) { 586 removeClass(parentEl, "opened"); 587 588 // If not inside sub-navigation, we can reset the whole nav 589 if (!isInsideSub) removeClass(nav, "dropdown-active"); 590 591 // navigation is closed, let's open it 592 } else { 593 594 // If it's inside sub-navigation 595 if (isInsideSub) { 596 forEach(getChildren(parentEl.parentNode), function (i, el) { 597 removeClass(el, "opened"); 598 }); 599 addClass(parentEl, "opened"); 600 601 // Not inside another sub-nav 602 } else { 603 forEach(dropdownToggle, function (i, el) { 604 removeClass(el.parentNode, "opened"); 605 }); 606 addClass(parentEl, "opened"); 607 } 608 } 609 610 // Enable pointer events again 611 this._enablePointerEvents(); 612 }, 613 614 /** 615 * Closes the navigation when a link inside is clicked 616 */ 617 _closeOnNavClick: function () { 618 if (opts.closeOnNavClick) { 619 var links = nav.getElementsByTagName("a"), 620 self = this; 621 forEach(links, function (i, el) { 622 addEvent(links[i], "click", function () { 623 if (isMobile) self.toggle(); 624 }, false); 625 }); 626 } 627 }, 628 629 /** 630 * Prevents the default tap functionality 631 * 632 * @param {event} event 633 */ 634 _preventDefault: function (e) { 635 // If you want dropdown links to be clickable on desktop remove comments: 636 /*if (!isMobile) return;*/ 637 if (e.preventDefault) { 638 if (e.stopImmediatePropagation) e.stopImmediatePropagation(); 639 e.preventDefault(); 640 e.stopPropagation(); 641 return false; 642 643 // This is strictly for old IE 644 } else { 645 e.returnValue = false; 646 } 647 }, 648 649 /** 650 * On touch start get the location of the touch 651 * and disable pointer events on the body. 652 * 653 * @param {event} event 654 */ 655 _onTouchStart: function (e) { 656 this._preventDefault(e); 657 addClass(document.body, "disable-pointer-events"); 658 this.startX = e.touches[0].clientX; 659 this.startY = e.touches[0].clientY; 660 this.touchHasMoved = false; 661 662 /** 663 * We remove mouseup event completely here to avoid 664 * double triggering of events. 665 */ 666 removeEvent(e.target, "mouseup", this, false); 667 }, 668 669 /** 670 * Check if the user is scrolling instead of tapping and 671 * re-enable pointer events if movement happed. 672 * 673 * @param {event} event 674 */ 675 _onTouchMove: function (e) { 676 if (Math.abs(e.touches[0].clientX - this.startX) > 10 || 677 Math.abs(e.touches[0].clientY - this.startY) > 10) { 678 this._enablePointerEvents(); 679 this.touchHasMoved = true; 680 } 681 }, 682 683 /** 684 * On touch end toggle either the whole navigation or 685 * a sub-navigation depending on which one was tapped. 686 * 687 * @param {event} event 688 */ 689 _onTouchEnd: function (e) { 690 this._preventDefault(e); 691 if (!isMobile) return; 692 693 // Get event.target, the old IE way 694 var thisEvent = e || window.event, 695 targetEl = thisEvent.target || thisEvent.srcElement, 696 isDropdownTapped = false;
697 698 // Was it sub-navigation toggle or the main toggle? 699 if (hasClass(targetEl, "dropdown-toggle")) isDropdownTapped = true; 700 701 // If the user isn't scrolling 702 if (!this.touchHasMoved) { 703 704 // If the event type is touch 705 if (e.type === "touchend") { 706 707 // If sub-navigation toggle was tapped 708 if (isDropdownTapped) { 709 this._toggleDropdown(targetEl); 710 711 // If the main toggle was tapped 712 } else { 713 this.toggle(); 714 } 715 return; 716 717 // Event type was click, not touch 718 } else { 719 var evt = e || window.event; 720 721 // If it isn't a right click, do toggling 722 if (!(evt.which === 3 || evt.button === 2)) { 723 if (isDropdownTapped) { 724 this._toggleDropdown(targetEl); 725 } else { 726 this.toggle(); 727 } 728 } 729 } 730 } 731 }, 732 733 /** 734 * For keyboard accessibility, toggle the navigation on Enter 735 * keypress too (also sub-navigation is keyboard accessible 736 * which explains the complexity here) 737 * 738 * @param {event} event 739 */ 740 _onKeyUp: function (e) { 741 var evt = e || window.event, 742 targetEl = e.target, 743 isDropdownTapped = false; 744 if (hasClass(targetEl, "dropdown-toggle")) isDropdownTapped = true; 745 if (evt.keyCode === 13) { 746 if (isDropdownTapped) { 747 this._toggleDropdown(targetEl); 748 } else { 749 this.toggle(); 750 } 751 } 752 }, 753 754 /** 755 * Enable pointer events 756 */ 757 _enablePointerEvents: function () { 758 removeClass(document.body, "disable-pointer-events"); 759 }, 760 761 /** 762 * Adds the needed CSS transitions if animations are enabled 763 */ 764 _transitions: function () { 765 if (opts.animate) { 766 var objStyle = nav.style, 767 transition = "max-height " + opts.transition + "ms"; 768 769 objStyle.WebkitTransition = transition; 770 objStyle.MozTransition = transition; 771 objStyle.OTransition = transition; 772 objStyle.transition = transition; 773 } 774 }, 775 776 /** 777 * Calculates the height of the navigation and then creates 778 * styles which are later added to the page <head> 779 */ 780 _calcHeight: function () { 781 var savedHeight = 0; 782 for (var i = 0; i < nav.inner.length; i++) { 783 savedHeight += nav.inner[i].offsetHeight; 784 } 785 786 // Pointer event styles are also here since they might only be confusing inside the stylesheet 787 var innerStyles = "." + opts.jsClass + " ." + opts.navClass + "-" + this.index + ".opened{max-height:" + savedHeight + "px !important} ." + opts.jsClass + " .disable-pointer-events{pointer-events:none !important} ." + opts.jsClass + " ." + opts.navClass + "-" + this.index + ".opened.dropdown-active {max-height:9999px !important}"; 788 789 if (styleElement.styleSheet) { 790 styleElement.styleSheet.cssText = innerStyles; 791 } else { 792 styleElement.innerHTML = innerStyles; 793 } 794 795 innerStyles = ""; 796 } 797 798 }; 799 800 /** 801 * Return new Responsive Nav 802 */ 803 return new ResponsiveNav(el, options); 804 805 }; 806 807 window.responsiveNav = responsiveNav; 808 809}(document, window, 0));
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.