smooth-scroll.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. /**
  2. * Smooth Scroll v4.7.2
  3. * Animate scrolling to anchor links, by Chris Ferdinandi.
  4. * http://gomakethings.com
  5. *
  6. * Additional contributors:
  7. * https://github.com/cferdinandi/smooth-scroll#contributors
  8. *
  9. * Free to use under the MIT License.
  10. * http://gomakethings.com/mit/
  11. */
  12. (function (root, factory) {
  13. if ( typeof define === 'function' && define.amd ) {
  14. define('smoothScroll', factory(root));
  15. } else if ( typeof exports === 'object' ) {
  16. module.smoothScroll = factory(root);
  17. } else {
  18. root.smoothScroll = factory(root);
  19. }
  20. })(this, function (root) {
  21. 'use strict';
  22. //
  23. // Variables
  24. //
  25. var exports = {}; // Object for public APIs
  26. var supports = !!document.querySelector && !!root.addEventListener; // Feature test
  27. // Default settings
  28. var defaults = {
  29. speed: 500,
  30. easing: 'easeInOutCubic',
  31. offset: 0,
  32. updateURL: false,
  33. callbackBefore: function () {},
  34. callbackAfter: function () {}
  35. };
  36. //
  37. // Methods
  38. //
  39. /**
  40. * Merge defaults with user options
  41. * @private
  42. * @param {Object} defaults Default settings
  43. * @param {Object} options User options
  44. * @returns {Object} Merged values of defaults and options
  45. */
  46. var extend = function ( defaults, options ) {
  47. for ( var key in options ) {
  48. if (Object.prototype.hasOwnProperty.call(options, key)) {
  49. defaults[key] = options[key];
  50. }
  51. }
  52. return defaults;
  53. };
  54. /**
  55. * A simple forEach() implementation for Arrays, Objects and NodeLists
  56. * @private
  57. * @param {Array|Object|NodeList} collection Collection of items to iterate
  58. * @param {Function} callback Callback function for each iteration
  59. * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`)
  60. */
  61. var forEach = function (collection, callback, scope) {
  62. if (Object.prototype.toString.call(collection) === '[object Object]') {
  63. for (var prop in collection) {
  64. if (Object.prototype.hasOwnProperty.call(collection, prop)) {
  65. callback.call(scope, collection[prop], prop, collection);
  66. }
  67. }
  68. } else {
  69. for (var i = 0, len = collection.length; i < len; i++) {
  70. callback.call(scope, collection[i], i, collection);
  71. }
  72. }
  73. };
  74. /**
  75. * Calculate the easing pattern
  76. * @private
  77. * @param {String} type Easing pattern
  78. * @param {Number} time Time animation should take to complete
  79. * @returns {Number}
  80. */
  81. var easingPattern = function ( type, time ) {
  82. var pattern;
  83. if ( type === 'easeInQuad' ) pattern = time * time; // accelerating from zero velocity
  84. if ( type === 'easeOutQuad' ) pattern = time * (2 - time); // decelerating to zero velocity
  85. if ( type === 'easeInOutQuad' ) pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration
  86. if ( type === 'easeInCubic' ) pattern = time * time * time; // accelerating from zero velocity
  87. if ( type === 'easeOutCubic' ) pattern = (--time) * time * time + 1; // decelerating to zero velocity
  88. if ( type === 'easeInOutCubic' ) pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration
  89. if ( type === 'easeInQuart' ) pattern = time * time * time * time; // accelerating from zero velocity
  90. if ( type === 'easeOutQuart' ) pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity
  91. if ( type === 'easeInOutQuart' ) pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration
  92. if ( type === 'easeInQuint' ) pattern = time * time * time * time * time; // accelerating from zero velocity
  93. if ( type === 'easeOutQuint' ) pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity
  94. if ( type === 'easeInOutQuint' ) pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration
  95. return pattern || time; // no easing, no acceleration
  96. };
  97. /**
  98. * Calculate how far to scroll
  99. * @private
  100. * @param {Element} anchor The anchor element to scroll to
  101. * @param {Number} headerHeight Height of a fixed header, if any
  102. * @param {Number} offset Number of pixels by which to offset scroll
  103. * @returns {Number}
  104. */
  105. var getEndLocation = function ( anchor, headerHeight, offset ) {
  106. var location = 0;
  107. if (anchor.offsetParent) {
  108. do {
  109. location += anchor.offsetTop;
  110. anchor = anchor.offsetParent;
  111. } while (anchor);
  112. }
  113. location = location - headerHeight - offset;
  114. return location >= 0 ? location : 0;
  115. };
  116. /**
  117. * Determine the document's height
  118. * @private
  119. * @returns {Number}
  120. */
  121. var getDocumentHeight = function () {
  122. return Math.max(
  123. document.body.scrollHeight, document.documentElement.scrollHeight,
  124. document.body.offsetHeight, document.documentElement.offsetHeight,
  125. document.body.clientHeight, document.documentElement.clientHeight
  126. );
  127. };
  128. /**
  129. * Remove whitespace from a string
  130. * @private
  131. * @param {String} string
  132. * @returns {String}
  133. */
  134. var trim = function ( string ) {
  135. return string.replace(/^\s+|\s+$/g, '');
  136. };
  137. /**
  138. * Convert data-options attribute into an object of key/value pairs
  139. * @private
  140. * @param {String} options Link-specific options as a data attribute string
  141. * @returns {Object}
  142. */
  143. var getDataOptions = function ( options ) {
  144. var settings = {};
  145. // Create a key/value pair for each setting
  146. if ( options ) {
  147. options = options.split(';');
  148. options.forEach( function(option) {
  149. option = trim(option);
  150. if ( option !== '' ) {
  151. option = option.split(':');
  152. settings[option[0]] = trim(option[1]);
  153. }
  154. });
  155. }
  156. return settings;
  157. };
  158. /**
  159. * Update the URL
  160. * @private
  161. * @param {Element} anchor The element to scroll to
  162. * @param {Boolean} url Whether or not to update the URL history
  163. */
  164. var updateUrl = function ( anchor, url ) {
  165. if ( history.pushState && (url || url === 'true') ) {
  166. history.pushState( {
  167. pos: anchor.id
  168. }, '', anchor );
  169. }
  170. };
  171. /**
  172. * Start/stop the scrolling animation
  173. * @public
  174. * @param {Element} toggle The element that toggled the scroll event
  175. * @param {Element} anchor The element to scroll to
  176. * @param {Object} settings
  177. * @param {Event} event
  178. */
  179. exports.animateScroll = function ( toggle, anchor, options, event ) {
  180. // Options and overrides
  181. var settings = extend( defaults, options || {} ); // Merge user options with defaults
  182. var overrides = getDataOptions( toggle ? toggle.getAttribute('data-options') : null );
  183. settings = extend( settings, overrides );
  184. // Selectors and variables
  185. var fixedHeader = document.querySelector('[data-scroll-header]'); // Get the fixed header
  186. var headerHeight = fixedHeader === null ? 0 : (fixedHeader.offsetHeight + fixedHeader.offsetTop); // Get the height of a fixed header if one exists
  187. var startLocation = root.pageYOffset; // Current location on the page
  188. var endLocation = getEndLocation( document.querySelector(anchor), headerHeight, parseInt(settings.offset, 10) ); // Scroll to location
  189. var animationInterval; // interval timer
  190. var distance = endLocation - startLocation; // distance to travel
  191. var documentHeight = getDocumentHeight();
  192. var timeLapsed = 0;
  193. var percentage, position;
  194. // Prevent default click event
  195. if ( toggle && toggle.tagName.toLowerCase() === 'a' && event ) {
  196. event.preventDefault();
  197. }
  198. // Update URL
  199. updateUrl(anchor, settings.updateURL);
  200. /**
  201. * Stop the scroll animation when it reaches its target (or the bottom/top of page)
  202. * @private
  203. * @param {Number} position Current position on the page
  204. * @param {Number} endLocation Scroll to location
  205. * @param {Number} animationInterval How much to scroll on this loop
  206. */
  207. var stopAnimateScroll = function (position, endLocation, animationInterval) {
  208. var currentLocation = root.pageYOffset;
  209. if ( position == endLocation || currentLocation == endLocation || ( (root.innerHeight + currentLocation) >= documentHeight ) ) {
  210. clearInterval(animationInterval);
  211. settings.callbackAfter( toggle, anchor ); // Run callbacks after animation complete
  212. }
  213. };
  214. /**
  215. * Loop scrolling animation
  216. * @private
  217. */
  218. var loopAnimateScroll = function () {
  219. timeLapsed += 16;
  220. percentage = ( timeLapsed / parseInt(settings.speed, 10) );
  221. percentage = ( percentage > 1 ) ? 1 : percentage;
  222. position = startLocation + ( distance * easingPattern(settings.easing, percentage) );
  223. root.scrollTo( 0, Math.floor(position) );
  224. stopAnimateScroll(position, endLocation, animationInterval);
  225. };
  226. /**
  227. * Set interval timer
  228. * @private
  229. */
  230. var startAnimateScroll = function () {
  231. settings.callbackBefore( toggle, anchor ); // Run callbacks before animating scroll
  232. animationInterval = setInterval(loopAnimateScroll, 16);
  233. };
  234. /**
  235. * Reset position to fix weird iOS bug
  236. * @link https://github.com/cferdinandi/smooth-scroll/issues/45
  237. */
  238. if ( root.pageYOffset === 0 ) {
  239. root.scrollTo( 0, 0 );
  240. }
  241. // Start scrolling animation
  242. startAnimateScroll();
  243. };
  244. /**
  245. * Initialize Smooth Scroll
  246. * @public
  247. * @param {Object} options User settings
  248. */
  249. exports.init = function ( options ) {
  250. // feature test
  251. if ( !supports ) return;
  252. // Selectors and variables
  253. var settings = extend( defaults, options || {} ); // Merge user options with defaults
  254. var toggles = document.querySelectorAll('[data-scroll]'); // Get smooth scroll toggles
  255. // When a toggle is clicked, run the click handler
  256. forEach(toggles, function (toggle) {
  257. toggle.addEventListener('click', exports.animateScroll.bind( null, toggle, toggle.getAttribute('href'), settings ), false);
  258. });
  259. };
  260. //
  261. // Public APIs
  262. //
  263. return exports;
  264. });