/*! * smooth-scroll v12.1.5: Animate scrolling to anchor links * (c) 2017 Chris Ferdinandi * MIT License * http://github.com/cferdinandi/smooth-scroll */ /** * closest() polyfill * @link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill */ if (window.Element && !Element.prototype.closest) { Element.prototype.closest = function (s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s), i, el = this; do { i = matches.length; while (--i >= 0 && matches.item(i) !== el) {} } while (i < 0 && (el = el.parentElement)); return el; }; } /** * requestAnimationFrame() polyfill * By Erik Möller. Fixes from Paul Irish and Tino Zijdel. * @link http://paulirish.com/2011/requestanimationframe-for-smart-animating/ * @link http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating * @license MIT */ (function () { var lastTime = 0; var vendors = ["ms", "moz", "webkit", "o"]; for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"]; window.cancelAnimationFrame = window[vendors[x] + "CancelAnimationFrame"] || window[vendors[x] + "CancelRequestAnimationFrame"]; } if (!window.requestAnimationFrame) { window.requestAnimationFrame = function (callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function () { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function (id) { clearTimeout(id); }; } })(); (function (root, factory) { if (typeof define === "function" && define.amd) { define([], function () { return factory(root); }); } else if (typeof exports === "object") { module.exports = factory(root); } else { root.SmoothScroll = factory(root); } })( typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : this, function (window) { "use strict"; // // Feature Test // var supports = "querySelector" in document && "addEventListener" in window && "requestAnimationFrame" in window && "closest" in window.Element.prototype; // // Default settings // var defaults = { // Selectors ignore: "[data-scroll-ignore]", header: null, // Speed & Easing speed: 600, offset: 50, easing: "easeInOutQuart", customEasing: null, // Callback API before: function () {}, after: function () {}, }; // // Utility Methods // /** * Merge two or more objects. Returns a new object. * @param {Object} objects The objects to merge together * @returns {Object} Merged values of defaults and options */ var extend = function () { // Variables var extended = {}; var deep = false; var i = 0; var length = arguments.length; // Merge the object into the extended object var merge = function (obj) { for (var prop in obj) { if (obj.hasOwnProperty(prop)) { extended[prop] = obj[prop]; } } }; // Loop through each object and conduct a merge for (; i < length; i++) { var obj = arguments[i]; merge(obj); } return extended; }; /** * Get the height of an element. * @param {Node} elem The element to get the height of * @return {Number} The element's height in pixels */ var getHeight = function (elem) { return parseInt(window.getComputedStyle(elem).height, 10); }; /** * Escape special characters for use with querySelector * @param {String} id The anchor ID to escape * @author Mathias Bynens * @link https://github.com/mathiasbynens/CSS.escape */ var escapeCharacters = function (id) { // Remove leading hash if (id.charAt(0) === "#") { id = id.substr(1); } var string = String(id); var length = string.length; var index = -1; var codeUnit; var result = ""; var firstCodeUnit = string.charCodeAt(0); while (++index < length) { codeUnit = string.charCodeAt(index); // Note: there’s no need to special-case astral symbols, surrogate // pairs, or lone surrogates. // If the character is NULL (U+0000), then throw an // `InvalidCharacterError` exception and terminate these steps. if (codeUnit === 0x0000) { throw new InvalidCharacterError( "Invalid character: the input contains U+0000." ); } if ( // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is // U+007F, […] (codeUnit >= 0x0001 && codeUnit <= 0x001f) || codeUnit == 0x007f || // If the character is the first character and is in the range [0-9] // (U+0030 to U+0039), […] (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || // If the character is the second character and is in the range [0-9] // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] (index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && firstCodeUnit === 0x002d) ) { // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point result += "\\" + codeUnit.toString(16) + " "; continue; } // If the character is not handled by one of the above rules and is // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to // U+005A), or [a-z] (U+0061 to U+007A), […] if ( codeUnit >= 0x0080 || codeUnit === 0x002d || codeUnit === 0x005f || (codeUnit >= 0x0030 && codeUnit <= 0x0039) || (codeUnit >= 0x0041 && codeUnit <= 0x005a) || (codeUnit >= 0x0061 && codeUnit <= 0x007a) ) { // the character itself result += string.charAt(index); continue; } // Otherwise, the escaped character. // http://dev.w3.org/csswg/cssom/#escape-a-character result += "\\" + string.charAt(index); } return "#" + result; }; /** * Calculate the easing pattern * @link https://gist.github.com/gre/1650294 * @param {String} type Easing pattern * @param {Number} time Time animation should take to complete * @returns {Number} */ var easingPattern = function (settings, time) { var pattern; // Default Easing Patterns if (settings.easing === "easeInQuad") pattern = time * time; // accelerating from zero velocity if (settings.easing === "easeOutQuad") pattern = time * (2 - time); // decelerating to zero velocity if (settings.easing === "easeInOutQuad") pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration if (settings.easing === "easeInCubic") pattern = time * time * time; // accelerating from zero velocity if (settings.easing === "easeOutCubic") pattern = --time * time * time + 1; // decelerating to zero velocity if (settings.easing === "easeInOutCubic") pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration if (settings.easing === "easeInQuart") pattern = time * time * time * time; // accelerating from zero velocity if (settings.easing === "easeOutQuart") pattern = 1 - --time * time * time * time; // decelerating to zero velocity if (settings.easing === "easeInOutQuart") pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * --time * time * time * time; // acceleration until halfway, then deceleration if (settings.easing === "easeInQuint") pattern = time * time * time * time * time; // accelerating from zero velocity if (settings.easing === "easeOutQuint") pattern = 1 + --time * time * time * time * time; // decelerating to zero velocity if (settings.easing === "easeInOutQuint") pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * --time * time * time * time * time; // acceleration until halfway, then deceleration // Custom Easing Patterns if (!!settings.customEasing) pattern = settings.customEasing(time); return pattern || time; // no easing, no acceleration }; /** * Determine the document's height * @returns {Number} */ var getDocumentHeight = function () { return Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); }; /** * Calculate how far to scroll * @param {Element} anchor The anchor element to scroll to * @param {Number} headerHeight Height of a fixed header, if any * @param {Number} offset Number of pixels by which to offset scroll * @returns {Number} */ var getEndLocation = function (anchor, headerHeight, offset) { var location = 0; if (anchor.offsetParent) { do { location += anchor.offsetTop; anchor = anchor.offsetParent; } while (anchor); } location = Math.max(location - headerHeight - offset, 0); return location; }; /** * Get the height of the fixed header * @param {Node} header The header * @return {Number} The height of the header */ var getHeaderHeight = function (header) { return !header ? 0 : getHeight(header) + header.offsetTop; }; /** * Bring the anchored element into focus * @param {Node} anchor The anchor element * @param {Number} endLocation The end location to scroll to * @param {Boolean} isNum If true, scroll is to a position rather than an element */ var adjustFocus = function (anchor, endLocation, isNum) { // Don't run if scrolling to a number on the page if (isNum) return; // Otherwise, bring anchor element into focus anchor.focus(); if (document.activeElement.id !== anchor.id) { anchor.setAttribute("tabindex", "-1"); anchor.focus(); anchor.style.outline = "none"; } window.scrollTo(0, endLocation); }; /** * Check to see if user prefers reduced motion * @param {Object} settings Script settings */ var reduceMotion = function (settings) { if ( "matchMedia" in window && window.matchMedia("(prefers-reduced-motion)").matches ) { return true; } return false; }; // // SmoothScroll Constructor // var SmoothScroll = function (selector, options) { // // Variables // var smoothScroll = {}; // Object for public APIs var settings, anchor, toggle, fixedHeader, headerHeight, eventTimeout, animationInterval; // // Methods // /** * Cancel a scroll-in-progress */ smoothScroll.cancelScroll = function () { // clearInterval(animationInterval); cancelAnimationFrame(animationInterval); }; /** * Start/stop the scrolling animation * @param {Node|Number} anchor The element or position to scroll to * @param {Element} toggle The element that toggled the scroll event * @param {Object} options */ smoothScroll.animateScroll = function (anchor, toggle, options) { // Local settings var animateSettings = extend(settings || defaults, options || {}); // Merge user options with defaults // Selectors and variables var isNum = Object.prototype.toString.call(anchor) === "[object Number]" ? true : false; var anchorElem = isNum || !anchor.tagName ? null : anchor; if (!isNum && !anchorElem) return; var startLocation = window.pageYOffset; // Current location on the page if (animateSettings.header && !fixedHeader) { // Get the fixed header if not already set fixedHeader = document.querySelector(animateSettings.header); } if (!headerHeight) { // Get the height of a fixed header if one exists and not already set headerHeight = getHeaderHeight(fixedHeader); } var endLocation = isNum ? anchor : getEndLocation( anchorElem, headerHeight, parseInt( typeof animateSettings.offset === "function" ? animateSettings.offset() : animateSettings.offset, 10 ) ); // Location to scroll to var distance = endLocation - startLocation; // distance to travel var documentHeight = getDocumentHeight(); var timeLapsed = 0; var start, percentage, position; /** * Stop the scroll animation when it reaches its target (or the bottom/top of page) * @param {Number} position Current position on the page * @param {Number} endLocation Scroll to location * @param {Number} animationInterval How much to scroll on this loop */ var stopAnimateScroll = function (position, endLocation) { // Get the current location var currentLocation = window.pageYOffset; // Check if the end location has been reached yet (or we've hit the end of the document) if ( position == endLocation || currentLocation == endLocation || (startLocation < endLocation && window.innerHeight + currentLocation) >= documentHeight ) { // Clear the animation timer smoothScroll.cancelScroll(); // Bring the anchored element into focus adjustFocus(anchor, endLocation, isNum); // Run callback after animation complete animateSettings.after(anchor, toggle); // Reset start start = null; return true; } }; /** * Loop scrolling animation */ var loopAnimateScroll = function (timestamp) { if (!start) { start = timestamp; } timeLapsed += timestamp - start; percentage = timeLapsed / parseInt(animateSettings.speed, 10); percentage = percentage > 1 ? 1 : percentage; position = startLocation + distance * easingPattern(animateSettings, percentage); window.scrollTo(0, Math.floor(position)); if (!stopAnimateScroll(position, endLocation)) { window.requestAnimationFrame(loopAnimateScroll); start = timestamp; } }; /** * Reset position to fix weird iOS bug * @link https://github.com/cferdinandi/smooth-scroll/issues/45 */ if (window.pageYOffset === 0) { window.scrollTo(0, 0); } // Run callback before animation starts animateSettings.before(anchor, toggle); // Start scrolling animation smoothScroll.cancelScroll(); window.requestAnimationFrame(loopAnimateScroll); }; /** * Handle has change event */ var hashChangeHandler = function (event) { // Only run if there's an anchor element to scroll to if (!anchor) return; // Reset the anchor element's ID anchor.id = anchor.getAttribute("data-scroll-id"); // Scroll to the anchored content smoothScroll.animateScroll(anchor, toggle); // Reset anchor and toggle anchor = null; toggle = null; }; /** * If smooth scroll element clicked, animate scroll */ var clickHandler = function (event) { // Don't run if the user prefers reduced motion if (reduceMotion(settings)) return; // Don't run if right-click or command/control + click if (event.button !== 0 || event.metaKey || event.ctrlKey) return; // Check if a smooth scroll link was clicked toggle = event.target.closest(selector); if ( !toggle || toggle.tagName.toLowerCase() !== "a" || event.target.closest(settings.ignore) ) return; // Only run if link is an anchor and points to the current page if ( toggle.hostname !== window.location.hostname || toggle.pathname !== window.location.pathname || !/#/.test(toggle.href) ) return; // Get the sanitized hash var hash; try { hash = escapeCharacters(decodeURIComponent(toggle.hash)); } catch (e) { hash = escapeCharacters(toggle.hash); } // If the hash is empty, scroll to the top of the page if (hash === "#") { // Prevent default link behavior event.preventDefault(); // Set the anchored element anchor = document.body; // Save or create the ID as a data attribute and remove it (prevents scroll jump) var id = anchor.id ? anchor.id : "smooth-scroll-top"; anchor.setAttribute("data-scroll-id", id); anchor.id = ""; // If no hash change event will happen, fire manually // Otherwise, update the hash if (window.location.hash.substring(1) === id) { hashChangeHandler(); } else { window.location.hash = id; } return; } // Get the anchored element anchor = document.querySelector(hash); // If anchored element exists, save the ID as a data attribute and remove it (prevents scroll jump) if (!anchor) return; anchor.setAttribute("data-scroll-id", anchor.id); anchor.id = ""; // If no hash change event will happen, fire manually if (toggle.hash === window.location.hash) { event.preventDefault(); hashChangeHandler(); } }; /** * On window scroll and resize, only run events at a rate of 15fps for better performance */ var resizeThrottler = function (event) { if (!eventTimeout) { eventTimeout = setTimeout(function () { eventTimeout = null; // Reset timeout headerHeight = getHeaderHeight(fixedHeader); // Get the height of a fixed header if one exists }, 66); } }; /** * Destroy the current initialization. */ smoothScroll.destroy = function () { // If plugin isn't already initialized, stop if (!settings) return; // Remove event listeners document.removeEventListener("click", clickHandler, false); window.removeEventListener("resize", resizeThrottler, false); // Cancel any scrolls-in-progress smoothScroll.cancelScroll(); // Reset variables settings = null; anchor = null; toggle = null; fixedHeader = null; headerHeight = null; eventTimeout = null; animationInterval = null; }; /** * Initialize Smooth Scroll * @param {Object} options User settings */ smoothScroll.init = function (options) { // feature test if (!supports) return; // Destroy any existing initializations smoothScroll.destroy(); // Selectors and variables settings = extend(defaults, options || {}); // Merge user options with defaults fixedHeader = settings.header ? document.querySelector(settings.header) : null; // Get the fixed header headerHeight = getHeaderHeight(fixedHeader); // When a toggle is clicked, run the click handler document.addEventListener("click", clickHandler, false); // Listen for hash changes window.addEventListener("hashchange", hashChangeHandler, false); // If window is resized and there's a fixed header, recalculate its size if (fixedHeader) { window.addEventListener("resize", resizeThrottler, false); } }; // // Initialize plugin // smoothScroll.init(options); // // Public APIs // return smoothScroll; }; return SmoothScroll; } ); var scroll = new SmoothScroll('a[href*="#"]');