vendor/assets/javascripts/responsive-nav.js in responsive-nav-rails-1.0.32 vs vendor/assets/javascripts/responsive-nav.js in responsive-nav-rails-1.0.33

- old
+ new

@@ -5,18 +5,21 @@ * Copyright (c) 2014 @viljamis * Available under the MIT license */ (function (document, window, index) { + // Index is used to keep multiple navs on the same page namespaced "use strict"; var responsiveNav = function (el, options) { var computed = !!window.getComputedStyle; - - // getComputedStyle polyfill + + /** + * getComputedStyle polyfill for old browsers + */ if (!computed) { window.getComputedStyle = function(el) { this.el = el; this.getPropertyValue = function(prop) { var re = /(\-([a-z]){1})/g; @@ -32,12 +35,21 @@ }; return this; }; } /* exported addEvent, removeEvent, getChildren, setAttributes, addClass, removeClass, forEach */ - // fn arg can be an object or a function, thanks to handleEvent - // read more at: http://www.thecssninja.com/javascript/handleevent + + /** + * Add Event + * fn arg can be an object or a function, thanks to handleEvent + * read more at: http://www.thecssninja.com/javascript/handleevent + * + * @param {element} element + * @param {event} event + * @param {Function} fn + * @param {boolean} bubbling + */ var addEvent = function (el, evt, fn, bubble) { if ("addEventListener" in el) { // BBOS6 doesn't support handleEvent, catch and polyfill try { el.addEventListener(evt, fn, bubble); @@ -61,11 +73,19 @@ } else { el.attachEvent("on" + evt, fn); } } }, - + + /** + * Remove Event + * + * @param {element} element + * @param {event} event + * @param {Function} fn + * @param {boolean} bubbling + */ removeEvent = function (el, evt, fn, bubble) { if ("removeEventListener" in el) { try { el.removeEventListener(evt, fn, bubble); } catch (e) { @@ -85,11 +105,17 @@ } else { el.detachEvent("on" + evt, fn); } } }, - + + /** + * Get the children of any element + * + * @param {element} + * @return {array} Returns matching elements in an array + */ getChildren = function (e) { if (e.children.length < 1) { throw new Error("The Nav container has no containing elements"); } // Store all children in array @@ -100,30 +126,54 @@ children.push(e.children[i]); } } return children; }, - + + /** + * Sets multiple attributes at once + * + * @param {element} element + * @param {attrs} attrs + */ setAttributes = function (el, attrs) { for (var key in attrs) { el.setAttribute(key, attrs[key]); } }, - + + /** + * Adds a class to any element + * + * @param {element} element + * @param {string} class + */ addClass = function (el, cls) { if (el.className.indexOf(cls) !== 0) { el.className += " " + cls; el.className = el.className.replace(/(^\s*)|(\s*$)/g,""); } }, - + + /** + * Remove a class from any element + * + * @param {element} element + * @param {string} class + */ removeClass = function (el, cls) { var reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); el.className = el.className.replace(reg, " ").replace(/(^\s*)|(\s*$)/g,""); }, - - // forEach method that passes back the stuff we need + + /** + * forEach method that passes back the stuff we need + * + * @param {array} array + * @param {Function} callback + * @param {scope} scope + */ forEach = function (array, callback, scope) { for (var i = 0; i < array.length; i++) { callback.call(scope, i, array[i]); } }; @@ -138,11 +188,14 @@ navOpen; var ResponsiveNav = function (el, options) { var i; - // Default options + /** + * Default options + * @type {Object} + */ this.options = { animate: true, // Boolean: Use CSS3 transitions, true or false transition: 284, // Integer: Speed of the transition, in milliseconds label: "Menu", // String: Label for the navigation toggle insert: "before", // String: Insert the toggle before or after the navigation @@ -192,11 +245,13 @@ this._init(this); }; ResponsiveNav.prototype = { - // Public methods + /** + * Unattaches events and removes any classes that were added + */ destroy: function () { this._removeStyles(); removeClass(nav, "closed"); removeClass(nav, "opened"); removeClass(nav, opts.navClass); @@ -204,10 +259,11 @@ removeClass(htmlEl, opts.navActiveClass); nav.removeAttribute("style"); nav.removeAttribute("aria-hidden"); removeEvent(window, "resize", this, false); + removeEvent(window, "focus", this, false); removeEvent(document.body, "touchmove", this, false); removeEvent(navToggle, "touchstart", this, false); removeEvent(navToggle, "touchend", this, false); removeEvent(navToggle, "mouseup", this, false); removeEvent(navToggle, "keyup", this, false); @@ -218,20 +274,29 @@ } else { navToggle.removeAttribute("aria-hidden"); } }, + /** + * Toggles the navigation open/close + */ toggle: function () { if (hasAnimFinished === true) { if (!navOpen) { this.open(); } else { this.close(); } + + // Enable pointer events again + this._enablePointerEvents(); } }, + /** + * Opens the navigation + */ open: function () { if (!navOpen) { removeClass(nav, "closed"); addClass(nav, "opened"); addClass(htmlEl, opts.navActiveClass); @@ -241,34 +306,46 @@ navOpen = true; opts.open(); } }, + /** + * Closes the navigation + */ close: function () { if (navOpen) { addClass(nav, "closed"); removeClass(nav, "opened"); removeClass(htmlEl, opts.navActiveClass); removeClass(navToggle, "active"); setAttributes(nav, {"aria-hidden": "true"}); + // If animations are enabled, wait until they finish if (opts.animate) { hasAnimFinished = false; setTimeout(function () { nav.style.position = "absolute"; hasAnimFinished = true; }, opts.transition + 10); + + // Animations aren't enabled, we can do these immediately } else { nav.style.position = "absolute"; } navOpen = false; opts.close(); } }, + /** + * Resize is called on window resize and orientation change. + * It initializes the CSS styles and height calculations. + */ resize: function () { + + // Resize watches navigation toggle's display state if (window.getComputedStyle(navToggle, null).getPropertyValue("display") !== "none") { isMobile = true; setAttributes(navToggle, {"aria-hidden": "false"}); @@ -288,10 +365,16 @@ nav.style.position = opts.openPos; this._removeStyles(); } }, + /** + * Takes care of all even handling + * + * @param {event} event + * @return {type} returns the type of event that should be used + */ handleEvent: function (e) { var evt = e || window.event; switch (evt.type) { case "touchstart": @@ -308,17 +391,20 @@ this._preventDefault(evt); break; case "keyup": this._onKeyUp(evt); break; + case "focus": case "resize": this.resize(evt); break; } }, - // Private methods + /** + * Initializes the widget + */ _init: function () { this.index = index++; addClass(nav, opts.navClass); addClass(nav, opts.navClass + "-" + this.index); @@ -329,57 +415,78 @@ this._closeOnNavClick(); this._createToggle(); this._transitions(); this.resize(); - // IE8 hack + /** + * On IE8 the resize event triggers too early for some reason + * so it's called here again on init to make sure all the + * calculated styles are correct. + */ var self = this; setTimeout(function () { self.resize(); }, 20); addEvent(window, "resize", this, false); + addEvent(window, "focus", this, false); addEvent(document.body, "touchmove", this, false); addEvent(navToggle, "touchstart", this, false); addEvent(navToggle, "touchend", this, false); addEvent(navToggle, "mouseup", this, false); addEvent(navToggle, "keyup", this, false); addEvent(navToggle, "click", this, false); - // Init callback + /** + * Init callback here + */ opts.init(); }, + /** + * Creates Styles to the <head> + */ _createStyles: function () { if (!styleElement.parentNode) { styleElement.type = "text/css"; document.getElementsByTagName("head")[0].appendChild(styleElement); } }, + /** + * Removes styles from the <head> + */ _removeStyles: function () { if (styleElement.parentNode) { styleElement.parentNode.removeChild(styleElement); } }, + /** + * Creates Navigation Toggle + */ _createToggle: function () { + + // If there's no toggle, let's create one if (!opts.customToggle) { var toggle = document.createElement("a"); toggle.innerHTML = opts.label; setAttributes(toggle, { "href": "#", "class": "nav-toggle" }); + // Determine where to insert the toggle if (opts.insert === "after") { nav.parentNode.insertBefore(toggle, nav.nextSibling); } else { nav.parentNode.insertBefore(toggle, nav); } navToggle = toggle; + + // There is a toggle already, let's use that one } else { var toggleEl = opts.customToggle.replace("#", ""); if (document.getElementById(toggleEl)) { navToggle = document.getElementById(toggleEl); @@ -389,13 +496,16 @@ throw new Error("The custom nav toggle you are trying to select doesn't exist"); } } }, + /** + * Closes the navigation when a link inside is clicked + */ _closeOnNavClick: function () { - if (opts.closeOnNavClick && "querySelectorAll" in document) { - var links = nav.querySelectorAll("a"), + if (opts.closeOnNavClick) { + var links = nav.getElementsByTagName("a"), self = this; forEach(links, function (i, el) { addEvent(links[i], "click", function () { if (isMobile) { self.toggle(); @@ -403,65 +513,125 @@ }, false); }); } }, + /** + * Prevents the default tap functionality + * + * @param {event} event + */ _preventDefault: function(e) { if (e.preventDefault) { + if (e.stopImmediatePropagation) { + e.stopImmediatePropagation(); + } e.preventDefault(); e.stopPropagation(); + return false; + + // This is strictly for old IE } else { e.returnValue = false; } }, + /** + * On touch start get the location of the touch + * and disable pointer events on the body. + * + * @param {event} event + */ _onTouchStart: function (e) { - e.stopPropagation(); - if (opts.insert === "after") { - addClass(document.body, "disable-pointer-events"); - } + this._preventDefault(e); + addClass(document.body, "disable-pointer-events"); this.startX = e.touches[0].clientX; this.startY = e.touches[0].clientY; this.touchHasMoved = false; + + /** + * We remove mouseup event completely here to avoid + * double triggering of events. + */ removeEvent(navToggle, "mouseup", this, false); }, + /** + * Check if the user is scrolling instead of tapping and + * re-enable pointer events if movement happed. + * + * @param {event} event + */ _onTouchMove: function (e) { if (Math.abs(e.touches[0].clientX - this.startX) > 10 || Math.abs(e.touches[0].clientY - this.startY) > 10) { + this._enablePointerEvents(); this.touchHasMoved = true; } }, + /** + * On touch end toggle either the whole navigation or + * a sub-navigation depending on which one was tapped. + * + * @param {event} event + */ _onTouchEnd: function (e) { this._preventDefault(e); + if (!isMobile) { + return; + } + + // If the user isn't scrolling if (!this.touchHasMoved) { + + // If the event type is touch if (e.type === "touchend") { this.toggle(); if (opts.insert === "after") { setTimeout(function () { removeClass(document.body, "disable-pointer-events"); }, opts.transition + 300); } return; + + // Event type was click, not touch } else { var evt = e || window.event; - // If it isn't a right click + + // If it isn't a right click, do toggling if (!(evt.which === 3 || evt.button === 2)) { this.toggle(); } } } }, + /** + * For keyboard accessibility, toggle the navigation on Enter + * keypress too (also sub-navigation is keyboard accessible + * which explains the complexity here) + * + * @param {event} event + */ _onKeyUp: function (e) { var evt = e || window.event; if (evt.keyCode === 13) { this.toggle(); } }, + /** + * Enable pointer events + */ + _enablePointerEvents: function () { + removeClass(document.body, "disable-pointer-events"); + }, + + /** + * Adds the needed CSS transitions if animations are enabled + */ _transitions: function () { if (opts.animate) { var objStyle = nav.style, transition = "max-height " + opts.transition + "ms"; @@ -470,17 +640,24 @@ objStyle.OTransition = transition; objStyle.transition = transition; } }, + /** + * Calculates the height of the navigation and then creates + * styles which are later added to the page <head> + */ _calcHeight: function () { var savedHeight = 0; for (var i = 0; i < nav.inner.length; i++) { savedHeight += nav.inner[i].offsetHeight; } - var innerStyles = "." + opts.jsClass + " ." + opts.navClass + "-" + this.index + ".opened{max-height:" + savedHeight + "px !important}"; + // Pointer event styles are also here since they might only be confusing inside the stylesheet + 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}"; + + if (styleElement.styleSheet) { styleElement.styleSheet.cssText = innerStyles; } else { styleElement.innerHTML = innerStyles; } @@ -488,12 +665,15 @@ innerStyles = ""; } }; + /** + * Return new Responsive Nav + */ return new ResponsiveNav(el, options); }; window.responsiveNav = responsiveNav; -}(document, window, 0)); \ No newline at end of file +}(document, window, 0));