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));