/*! * headroom.js v0.7.0 - give your page some headroom. hide your header until you need it * copyright (c) 2014 nick williams - http://wicky.nillia.ms/headroom.js * license: mit */ (function(window, document) { 'use strict'; /* exported features */ var features = { bind : !!(function(){}.bind), classlist : 'classlist' in document.documentelement, raf : !!(window.requestanimationframe || window.webkitrequestanimationframe || window.mozrequestanimationframe) }; window.requestanimationframe = window.requestanimationframe || window.webkitrequestanimationframe || window.mozrequestanimationframe; /** * handles debouncing of events via requestanimationframe * @see http://www.html5rocks.com/en/tutorials/speed/animations/ * @param {function} callback the callback to handle whichever event */ function debouncer (callback) { this.callback = callback; this.ticking = false; } debouncer.prototype = { constructor : debouncer, /** * dispatches the event to the supplied callback * @private */ update : function() { this.callback && this.callback(); this.ticking = false; }, /** * ensures events don't get stacked * @private */ requesttick : function() { if(!this.ticking) { requestanimationframe(this.rafcallback || (this.rafcallback = this.update.bind(this))); this.ticking = true; } }, /** * attach this as the event listeners */ handleevent : function() { this.requesttick(); } }; /** * check if object is part of the dom * @constructor * @param {object} obj element to check */ function isdomelement(obj) { return obj && typeof window !== 'undefined' && (obj === window || obj.nodetype); } /** * helper function for extending objects */ function extend (object /*, objectn ... */) { if(arguments.length <= 0) { throw new error('missing arguments in extend function'); } var result = object || {}, key, i; for (i = 1; i < arguments.length; i++) { var replacement = arguments[i] || {}; for (key in replacement) { // recurse into object except if the object is a dom element if(typeof result[key] === 'object' && ! isdomelement(result[key])) { result[key] = extend(result[key], replacement[key]); } else { result[key] = result[key] || replacement[key]; } } } return result; } /** * helper function for normalizing tolerance option to object format */ function normalizetolerance (t) { return t === object(t) ? t : { down : t, up : t }; } /** * ui enhancement for fixed headers. * hides header when scrolling down * shows header when scrolling up * @constructor * @param {domelement} elem the header element * @param {object} options options for the widget */ function headroom (elem, options) { options = extend(options, headroom.options); this.lastknownscrolly = 0; this.elem = elem; this.debouncer = new debouncer(this.update.bind(this)); this.tolerance = normalizetolerance(options.tolerance); this.classes = options.classes; this.offset = options.offset; this.scroller = options.scroller; this.initialised = false; this.onpin = options.onpin; this.onunpin = options.onunpin; this.ontop = options.ontop; this.onnottop = options.onnottop; } headroom.prototype = { constructor : headroom, /** * initialises the widget */ init : function() { if(!headroom.cutsthemustard) { return; } this.elem.classlist.add(this.classes.initial); // defer event registration to handle browser // potentially restoring previous scroll position settimeout(this.attachevent.bind(this), 100); return this; }, /** * unattaches events and removes any classes that were added */ destroy : function() { var classes = this.classes; this.initialised = false; this.elem.classlist.remove(classes.unpinned, classes.pinned, classes.top, classes.initial); this.scroller.removeeventlistener('scroll', this.debouncer, false); }, /** * attaches the scroll event * @private */ attachevent : function() { if(!this.initialised){ this.lastknownscrolly = this.getscrolly(); this.initialised = true; this.scroller.addeventlistener('scroll', this.debouncer, false); this.debouncer.handleevent(); } }, /** * unpins the header if it's currently pinned */ unpin : function() { var classlist = this.elem.classlist, classes = this.classes; if(classlist.contains(classes.pinned) || !classlist.contains(classes.unpinned)) { classlist.add(classes.unpinned); classlist.remove(classes.pinned); this.onunpin && this.onunpin.call(this); } }, /** * pins the header if it's currently unpinned */ pin : function() { var classlist = this.elem.classlist, classes = this.classes; if(classlist.contains(classes.unpinned)) { classlist.remove(classes.unpinned); classlist.add(classes.pinned); this.onpin && this.onpin.call(this); } }, /** * handles the top states */ top : function() { var classlist = this.elem.classlist, classes = this.classes; if(!classlist.contains(classes.top)) { classlist.add(classes.top); classlist.remove(classes.nottop); this.ontop && this.ontop.call(this); } }, /** * handles the not top state */ nottop : function() { var classlist = this.elem.classlist, classes = this.classes; if(!classlist.contains(classes.nottop)) { classlist.add(classes.nottop); classlist.remove(classes.top); this.onnottop && this.onnottop.call(this); } }, /** * gets the y scroll position * @see https://developer.mozilla.org/en-us/docs/web/api/window.scrolly * @return {number} pixels the page has scrolled along the y-axis */ getscrolly : function() { return (this.scroller.pageyoffset !== undefined) ? this.scroller.pageyoffset : (this.scroller.scrolltop !== undefined) ? this.scroller.scrolltop : (document.documentelement || document.body.parentnode || document.body).scrolltop; }, /** * gets the height of the viewport * @see http://andylangton.co.uk/blog/development/get-viewport-size-width-and-height-javascript * @return {int} the height of the viewport in pixels */ getviewportheight : function () { return window.innerheight || document.documentelement.clientheight || document.body.clientheight; }, /** * gets the height of the document * @see http://james.padolsey.com/javascript/get-document-height-cross-browser/ * @return {int} the height of the document in pixels */ getdocumentheight : function () { var body = document.body, documentelement = document.documentelement; return math.max( body.scrollheight, documentelement.scrollheight, body.offsetheight, documentelement.offsetheight, body.clientheight, documentelement.clientheight ); }, /** * gets the height of the dom element * @param {object} elm the element to calculate the height of which * @return {int} the height of the element in pixels */ getelementheight : function (elm) { return math.max( elm.scrollheight, elm.offsetheight, elm.clientheight ); }, /** * gets the height of the scroller element * @return {int} the height of the scroller element in pixels */ getscrollerheight : function () { return (this.scroller === window || this.scroller === document.body) ? this.getdocumentheight() : this.getelementheight(this.scroller); }, /** * determines if the scroll position is outside of document boundaries * @param {int} currentscrolly the current y scroll position * @return {bool} true if out of bounds, false otherwise */ isoutofbounds : function (currentscrolly) { var pasttop = currentscrolly < 0, pastbottom = currentscrolly + this.getviewportheight() > this.getscrollerheight(); return pasttop || pastbottom; }, /** * determines if the tolerance has been exceeded * @param {int} currentscrolly the current scroll y position * @return {bool} true if tolerance exceeded, false otherwise */ toleranceexceeded : function (currentscrolly, direction) { return math.abs(currentscrolly-this.lastknownscrolly) >= this.tolerance[direction]; }, /** * determine if it is appropriate to unpin * @param {int} currentscrolly the current y scroll position * @param {bool} toleranceexceeded has the tolerance been exceeded? * @return {bool} true if should unpin, false otherwise */ shouldunpin : function (currentscrolly, toleranceexceeded) { var scrollingdown = currentscrolly > this.lastknownscrolly, pastoffset = currentscrolly >= this.offset; return scrollingdown && pastoffset && toleranceexceeded; }, /** * determine if it is appropriate to pin * @param {int} currentscrolly the current y scroll position * @param {bool} toleranceexceeded has the tolerance been exceeded? * @return {bool} true if should pin, false otherwise */ shouldpin : function (currentscrolly, toleranceexceeded) { var scrollingup = currentscrolly < this.lastknownscrolly, pastoffset = currentscrolly <= this.offset; return (scrollingup && toleranceexceeded) || pastoffset; }, /** * handles updating the state of the widget */ update : function() { var currentscrolly = this.getscrolly(), scrolldirection = currentscrolly > this.lastknownscrolly ? 'down' : 'up', toleranceexceeded = this.toleranceexceeded(currentscrolly, scrolldirection); if(this.isoutofbounds(currentscrolly)) { // ignore bouncy scrolling in osx return; } if (currentscrolly <= this.offset ) { this.top(); } else { this.nottop(); } if(this.shouldunpin(currentscrolly, toleranceexceeded)) { this.unpin(); } else if(this.shouldpin(currentscrolly, toleranceexceeded)) { this.pin(); } this.lastknownscrolly = currentscrolly; } }; /** * default options * @type {object} */ headroom.options = { tolerance : { up : 0, down : 0 }, offset : 0, scroller: window, classes : { pinned : 'headroom--pinned', unpinned : 'headroom--unpinned', top : 'headroom--top', nottop : 'headroom--not-top', initial : 'headroom' } }; headroom.cutsthemustard = typeof features !== 'undefined' && features.raf && features.bind && features.classlist; window.headroom = headroom; }(window, document));