/*! * smooth-scroll v12.1.3: 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: 500, offset: 0, easing: 'easeInOutCubic', 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 viewport's height * @returns {Number} */ var getViewportHeight = function() { return Math.max(document.documentElement.clientHeight, window.innerHeight || 0); }; /** * Determine the document's height * @returns {Number} */ var getDocumentHeight = function () { return parseInt(window.getComputedStyle(document.documentElement).height, 10); }; /** * 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 Math.min(location, getDocumentHeight() - getViewportHeight()); }; /** * 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 // startAnimateScroll(); smoothScroll.cancelScroll(); window.requestAnimationFrame(loopAnimateScroll); }; /** * Handle has change event */ var hashChangeHandler = function (event) { // Get hash from URL var hash; try { hash = escapeCharacters(decodeURIComponent(window.location.hash)); } catch(e) { hash = escapeCharacters(window.location.hash); } // 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; }));