/*! * headroom.js v0.12.0 - Give your page some headroom. Hide your header until you need it * Copyright (c) 2020 Nick Williams - http://wicky.nillia.ms/headroom.js * License: MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Headroom = factory()); }(this, function () { 'use strict'; function isBrowser() { return typeof window !== "undefined"; } /** * Used to detect browser support for adding an event listener with options * Credit: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener */ function passiveEventsSupported() { var supported = false; try { var options = { // eslint-disable-next-line getter-return get passive() { supported = true; } }; window.addEventListener("test", options, options); window.removeEventListener("test", options, options); } catch (err) { supported = false; } return supported; } function isSupported() { return !!( isBrowser() && function() {}.bind && "classList" in document.documentElement && Object.assign && Object.keys && requestAnimationFrame ); } function isDocument(obj) { return obj.nodeType === 9; // Node.DOCUMENT_NODE === 9 } function isWindow(obj) { // `obj === window` or `obj instanceof Window` is not sufficient, // as the obj may be the window of an iframe. return obj && obj.document && isDocument(obj.document); } function windowScroller(win) { var doc = win.document; var body = doc.body; var html = doc.documentElement; return { /** * @see http://james.padolsey.com/javascript/get-document-height-cross-browser/ * @return {Number} the scroll height of the document in pixels */ scrollHeight: function() { return Math.max( body.scrollHeight, html.scrollHeight, body.offsetHeight, html.offsetHeight, body.clientHeight, html.clientHeight ); }, /** * @see http://andylangton.co.uk/blog/development/get-viewport-size-width-and-height-javascript * @return {Number} the height of the viewport in pixels */ height: function() { return win.innerHeight || html.clientHeight || body.clientHeight; }, /** * Gets the Y scroll position * @return {Number} pixels the page has scrolled along the Y-axis */ scrollY: function() { if (win.pageYOffset !== undefined) { return win.pageYOffset; } return (html || body.parentNode || body).scrollTop; } }; } function elementScroller(element) { return { /** * @return {Number} the scroll height of the element in pixels */ scrollHeight: function() { return Math.max( element.scrollHeight, element.offsetHeight, element.clientHeight ); }, /** * @return {Number} the height of the element in pixels */ height: function() { return Math.max(element.offsetHeight, element.clientHeight); }, /** * Gets the Y scroll position * @return {Number} pixels the element has scrolled along the Y-axis */ scrollY: function() { return element.scrollTop; } }; } function createScroller(element) { return isWindow(element) ? windowScroller(element) : elementScroller(element); } /** * @param element EventTarget */ function trackScroll(element, options, callback) { var isPassiveSupported = passiveEventsSupported(); var rafId; var scrolled = false; var scroller = createScroller(element); var lastScrollY = scroller.scrollY(); var details = {}; function update() { var scrollY = Math.round(scroller.scrollY()); var height = scroller.height(); var scrollHeight = scroller.scrollHeight(); // reuse object for less memory churn details.scrollY = scrollY; details.lastScrollY = lastScrollY; details.direction = scrollY > lastScrollY ? "down" : "up"; details.distance = Math.abs(scrollY - lastScrollY); details.isOutOfBounds = scrollY < 0 || scrollY + height > scrollHeight; details.top = scrollY <= options.offset[details.direction]; details.bottom = scrollY + height >= scrollHeight; details.toleranceExceeded = details.distance > options.tolerance[details.direction]; callback(details); lastScrollY = scrollY; scrolled = false; } function handleScroll() { if (!scrolled) { scrolled = true; rafId = requestAnimationFrame(update); } } var eventOptions = isPassiveSupported ? { passive: true, capture: false } : false; element.addEventListener("scroll", handleScroll, eventOptions); update(); return { destroy: function() { cancelAnimationFrame(rafId); element.removeEventListener("scroll", handleScroll, eventOptions); } }; } function normalizeUpDown(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 = options || {}; Object.assign(this, Headroom.options, options); this.classes = Object.assign({}, Headroom.options.classes, options.classes); this.elem = elem; this.tolerance = normalizeUpDown(this.tolerance); this.offset = normalizeUpDown(this.offset); this.initialised = false; this.frozen = false; } Headroom.prototype = { constructor: Headroom, /** * Start listening to scrolling * @public */ init: function() { if (Headroom.cutsTheMustard && !this.initialised) { this.addClass("initial"); this.initialised = true; // defer event registration to handle browser // potentially restoring previous scroll position setTimeout( function(self) { self.scrollTracker = trackScroll( self.scroller, { offset: self.offset, tolerance: self.tolerance }, self.update.bind(self) ); }, 100, this ); } return this; }, /** * Destroy the widget, clearing up after itself * @public */ destroy: function() { this.initialised = false; Object.keys(this.classes).forEach(this.removeClass, this); this.scrollTracker.destroy(); }, /** * Unpin the element * @public */ unpin: function() { if (this.hasClass("pinned") || !this.hasClass("unpinned")) { this.addClass("unpinned"); this.removeClass("pinned"); if (this.onUnpin) { this.onUnpin.call(this); } } }, /** * Pin the element * @public */ pin: function() { if (this.hasClass("unpinned")) { this.addClass("pinned"); this.removeClass("unpinned"); if (this.onPin) { this.onPin.call(this); } } }, /** * Freezes the current state of the widget * @public */ freeze: function() { this.frozen = true; this.addClass("frozen"); }, /** * Re-enables the default behaviour of the widget * @public */ unfreeze: function() { this.frozen = false; this.removeClass("frozen"); }, top: function() { if (!this.hasClass("top")) { this.addClass("top"); this.removeClass("notTop"); if (this.onTop) { this.onTop.call(this); } } }, notTop: function() { if (!this.hasClass("notTop")) { this.addClass("notTop"); this.removeClass("top"); if (this.onNotTop) { this.onNotTop.call(this); } } }, bottom: function() { if (!this.hasClass("bottom")) { this.addClass("bottom"); this.removeClass("notBottom"); if (this.onBottom) { this.onBottom.call(this); } } }, notBottom: function() { if (!this.hasClass("notBottom")) { this.addClass("notBottom"); this.removeClass("bottom"); if (this.onNotBottom) { this.onNotBottom.call(this); } } }, shouldUnpin: function(details) { var scrollingDown = details.direction === "down"; return scrollingDown && !details.top && details.toleranceExceeded; }, shouldPin: function(details) { var scrollingUp = details.direction === "up"; return (scrollingUp && details.toleranceExceeded) || details.top; }, addClass: function(className) { this.elem.classList.add.apply( this.elem.classList, this.classes[className].split(" ") ); }, removeClass: function(className) { this.elem.classList.remove.apply( this.elem.classList, this.classes[className].split(" ") ); }, hasClass: function(className) { return this.classes[className].split(" ").every(function(cls) { return this.classList.contains(cls); }, this.elem); }, update: function(details) { if (details.isOutOfBounds) { // Ignore bouncy scrolling in OSX return; } if (this.frozen === true) { return; } if (details.top) { this.top(); } else { this.notTop(); } if (details.bottom) { this.bottom(); } else { this.notBottom(); } if (this.shouldUnpin(details)) { this.unpin(); } else if (this.shouldPin(details)) { this.pin(); } } }; /** * Default options * @type {Object} */ Headroom.options = { tolerance: { up: 0, down: 0 }, offset: 0, scroller: isBrowser() ? window : null, classes: { frozen: "headroom--frozen", pinned: "headroom--pinned", unpinned: "headroom--unpinned", top: "headroom--top", notTop: "headroom--not-top", bottom: "headroom--bottom", notBottom: "headroom--not-bottom", initial: "headroom" } }; Headroom.cutsTheMustard = isSupported(); return Headroom; })); (function() { var myElement = document.querySelector("#header"); // construct an instance of Headroom, passing the element var headroom = new Headroom(myElement); // initialise headroom.init(); }());