smooth-scroll.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /*!
  2. jQuery Smooth Scroll
  3. Version: 2.2.0
  4. Plugin URL: https://github.com/kswedberg/jquery-smooth-scroll
  5. License: Copyright (c) 2017 Karl Swedberg | Licensed under MIT
  6. !*/
  7. (function(factory) {
  8. if (typeof define === 'function' && define.amd) {
  9. // AMD. Register as an anonymous module.
  10. define(['jquery'], factory);
  11. } else if (typeof module === 'object' && module.exports) {
  12. // CommonJS
  13. factory(require('jquery'));
  14. } else {
  15. // Browser globals
  16. factory(jQuery);
  17. }
  18. }(function($) {
  19. var version = '2.2.0';
  20. var optionOverrides = {};
  21. var defaults = {
  22. exclude: [],
  23. excludeWithin: [],
  24. offset: 0,
  25. // one of 'top' or 'left'
  26. direction: 'top',
  27. // if set, bind click events through delegation
  28. // supported since jQuery 1.4.2
  29. delegateSelector: null,
  30. // jQuery set of elements you wish to scroll (for $.smoothScroll).
  31. // if null (default), $('html, body').firstScrollable() is used.
  32. scrollElement: null,
  33. // only use if you want to override default behavior
  34. scrollTarget: null,
  35. // automatically focus the target element after scrolling to it
  36. autoFocus: false,
  37. // fn(opts) function to be called before scrolling occurs.
  38. // `this` is the element(s) being scrolled
  39. beforeScroll: function() {},
  40. // fn(opts) function to be called after scrolling occurs.
  41. // `this` is the triggering element
  42. afterScroll: function() {},
  43. // easing name. jQuery comes with "swing" and "linear." For others, you'll need an easing plugin
  44. // from jQuery UI or elsewhere
  45. easing: 'swing',
  46. // speed can be a number or 'auto'
  47. // if 'auto', the speed will be calculated based on the formula:
  48. // (current scroll position - target scroll position) / autoCoeffic
  49. speed: 400,
  50. // coefficient for "auto" speed
  51. autoCoefficient: 2,
  52. // $.fn.smoothScroll only: whether to prevent the default click action
  53. preventDefault: true
  54. };
  55. var getScrollable = function(opts) {
  56. var scrollable = [];
  57. var scrolled = false;
  58. var dir = opts.dir && opts.dir === 'left' ? 'scrollLeft' : 'scrollTop';
  59. this.each(function() {
  60. var el = $(this);
  61. if (this === document || this === window) {
  62. return;
  63. }
  64. if (document.scrollingElement && (this === document.documentElement || this === document.body)) {
  65. scrollable.push(document.scrollingElement);
  66. return false;
  67. }
  68. if (el[dir]() > 0) {
  69. scrollable.push(this);
  70. } else {
  71. // if scroll(Top|Left) === 0, nudge the element 1px and see if it moves
  72. el[dir](1);
  73. scrolled = el[dir]() > 0;
  74. if (scrolled) {
  75. scrollable.push(this);
  76. }
  77. // then put it back, of course
  78. el[dir](0);
  79. }
  80. });
  81. if (!scrollable.length) {
  82. this.each(function() {
  83. // If no scrollable elements and <html> has scroll-behavior:smooth because
  84. // "When this property is specified on the root element, it applies to the viewport instead."
  85. // and "The scroll-behavior property of the … body element is *not* propagated to the viewport."
  86. // → https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior
  87. if (this === document.documentElement && $(this).css('scrollBehavior') === 'smooth') {
  88. scrollable = [this];
  89. }
  90. // If still no scrollable elements, fall back to <body>,
  91. // if it's in the jQuery collection
  92. // (doing this because Safari sets scrollTop async,
  93. // so can't set it to 1 and immediately get the value.)
  94. if (!scrollable.length && this.nodeName === 'BODY') {
  95. scrollable = [this];
  96. }
  97. });
  98. }
  99. // Use the first scrollable element if we're calling firstScrollable()
  100. if (opts.el === 'first' && scrollable.length > 1) {
  101. scrollable = [scrollable[0]];
  102. }
  103. return scrollable;
  104. };
  105. var rRelative = /^([\-\+]=)(\d+)/;
  106. $.fn.extend({
  107. scrollable: function(dir) {
  108. var scrl = getScrollable.call(this, {dir: dir});
  109. return this.pushStack(scrl);
  110. },
  111. firstScrollable: function(dir) {
  112. var scrl = getScrollable.call(this, {el: 'first', dir: dir});
  113. return this.pushStack(scrl);
  114. },
  115. smoothScroll: function(options, extra) {
  116. options = options || {};
  117. if (options === 'options') {
  118. if (!extra) {
  119. return this.first().data('ssOpts');
  120. }
  121. return this.each(function() {
  122. var $this = $(this);
  123. var opts = $.extend($this.data('ssOpts') || {}, extra);
  124. $(this).data('ssOpts', opts);
  125. });
  126. }
  127. var opts = $.extend({}, $.fn.smoothScroll.defaults, options);
  128. var clickHandler = function(event) {
  129. var escapeSelector = function(str) {
  130. return str.replace(/(:|\.|\/)/g, '\\$1');
  131. };
  132. var link = this;
  133. var $link = $(this);
  134. var thisOpts = $.extend({}, opts, $link.data('ssOpts') || {});
  135. var exclude = opts.exclude;
  136. var excludeWithin = thisOpts.excludeWithin;
  137. var elCounter = 0;
  138. var ewlCounter = 0;
  139. var include = true;
  140. var clickOpts = {};
  141. var locationPath = $.smoothScroll.filterPath(location.pathname);
  142. var linkPath = $.smoothScroll.filterPath(link.pathname);
  143. var hostMatch = location.hostname === link.hostname || !link.hostname;
  144. var pathMatch = thisOpts.scrollTarget || (linkPath === locationPath);
  145. var thisHash = escapeSelector(link.hash);
  146. if (thisHash && !$(thisHash).length) {
  147. include = false;
  148. }
  149. if (!thisOpts.scrollTarget && (!hostMatch || !pathMatch || !thisHash)) {
  150. include = false;
  151. } else {
  152. while (include && elCounter < exclude.length) {
  153. if ($link.is(escapeSelector(exclude[elCounter++]))) {
  154. include = false;
  155. }
  156. }
  157. while (include && ewlCounter < excludeWithin.length) {
  158. if ($link.closest(excludeWithin[ewlCounter++]).length) {
  159. include = false;
  160. }
  161. }
  162. }
  163. if (include) {
  164. if (thisOpts.preventDefault) {
  165. event.preventDefault();
  166. }
  167. $.extend(clickOpts, thisOpts, {
  168. scrollTarget: thisOpts.scrollTarget || thisHash,
  169. link: link
  170. });
  171. $.smoothScroll(clickOpts);
  172. }
  173. };
  174. if (options.delegateSelector !== null) {
  175. this
  176. .off('click.smoothscroll', options.delegateSelector)
  177. .on('click.smoothscroll', options.delegateSelector, clickHandler);
  178. } else {
  179. this
  180. .off('click.smoothscroll')
  181. .on('click.smoothscroll', clickHandler);
  182. }
  183. return this;
  184. }
  185. });
  186. var getExplicitOffset = function(val) {
  187. var explicit = {relative: ''};
  188. var parts = typeof val === 'string' && rRelative.exec(val);
  189. if (typeof val === 'number') {
  190. explicit.px = val;
  191. } else if (parts) {
  192. explicit.relative = parts[1];
  193. explicit.px = parseFloat(parts[2]) || 0;
  194. }
  195. return explicit;
  196. };
  197. var onAfterScroll = function(opts) {
  198. var $tgt = $(opts.scrollTarget);
  199. if (opts.autoFocus && $tgt.length) {
  200. $tgt[0].focus();
  201. if (!$tgt.is(document.activeElement)) {
  202. $tgt.prop({tabIndex: -1});
  203. $tgt[0].focus();
  204. }
  205. }
  206. opts.afterScroll.call(opts.link, opts);
  207. };
  208. $.smoothScroll = function(options, px) {
  209. if (options === 'options' && typeof px === 'object') {
  210. return $.extend(optionOverrides, px);
  211. }
  212. var opts, $scroller, speed, delta;
  213. var explicitOffset = getExplicitOffset(options);
  214. var scrollTargetOffset = {};
  215. var scrollerOffset = 0;
  216. var offPos = 'offset';
  217. var scrollDir = 'scrollTop';
  218. var aniProps = {};
  219. var aniOpts = {};
  220. if (explicitOffset.px) {
  221. opts = $.extend({link: null}, $.fn.smoothScroll.defaults, optionOverrides);
  222. } else {
  223. opts = $.extend({link: null}, $.fn.smoothScroll.defaults, options || {}, optionOverrides);
  224. if (opts.scrollElement) {
  225. offPos = 'position';
  226. if (opts.scrollElement.css('position') === 'static') {
  227. opts.scrollElement.css('position', 'relative');
  228. }
  229. }
  230. if (px) {
  231. explicitOffset = getExplicitOffset(px);
  232. }
  233. }
  234. scrollDir = opts.direction === 'left' ? 'scrollLeft' : scrollDir;
  235. if (opts.scrollElement) {
  236. $scroller = opts.scrollElement;
  237. if (!explicitOffset.px && !(/^(?:HTML|BODY)$/).test($scroller[0].nodeName)) {
  238. scrollerOffset = $scroller[scrollDir]();
  239. }
  240. } else {
  241. $scroller = $('html, body').firstScrollable(opts.direction);
  242. }
  243. // beforeScroll callback function must fire before calculating offset
  244. opts.beforeScroll.call($scroller, opts);
  245. scrollTargetOffset = explicitOffset.px ? explicitOffset : {
  246. relative: '',
  247. px: ($(opts.scrollTarget)[offPos]() && $(opts.scrollTarget)[offPos]()[opts.direction]) || 0
  248. };
  249. aniProps[scrollDir] = scrollTargetOffset.relative + (scrollTargetOffset.px + scrollerOffset + opts.offset);
  250. speed = opts.speed;
  251. // automatically calculate the speed of the scroll based on distance / coefficient
  252. if (speed === 'auto') {
  253. // $scroller[scrollDir]() is position before scroll, aniProps[scrollDir] is position after
  254. // When delta is greater, speed will be greater.
  255. delta = Math.abs(aniProps[scrollDir] - $scroller[scrollDir]());
  256. // Divide the delta by the coefficient
  257. speed = delta / opts.autoCoefficient;
  258. }
  259. aniOpts = {
  260. duration: speed,
  261. easing: opts.easing,
  262. complete: function() {
  263. onAfterScroll(opts);
  264. }
  265. };
  266. if (opts.step) {
  267. aniOpts.step = opts.step;
  268. }
  269. if ($scroller.length) {
  270. $scroller.stop().animate(aniProps, aniOpts);
  271. } else {
  272. onAfterScroll(opts);
  273. }
  274. };
  275. $.smoothScroll.version = version;
  276. $.smoothScroll.filterPath = function(string) {
  277. string = string || '';
  278. return string
  279. .replace(/^\//, '')
  280. .replace(/(?:index|default).[a-zA-Z]{3,4}$/, '')
  281. .replace(/\/$/, '');
  282. };
  283. // default options
  284. $.fn.smoothScroll.defaults = defaults;
  285. }));