smoothscroll.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. 'use strict';
  2. // polyfill
  3. function polyfill() {
  4. // aliases
  5. var w = window;
  6. var d = document;
  7. // return if scroll behavior is supported and polyfill is not forced
  8. if (
  9. 'scrollBehavior' in d.documentElement.style &&
  10. w.__forceSmoothScrollPolyfill__ !== true
  11. ) {
  12. return;
  13. }
  14. // globals
  15. var Element = w.HTMLElement || w.Element;
  16. var SCROLL_TIME = 468;
  17. // object gathering original scroll methods
  18. var original = {
  19. scroll: w.scroll || w.scrollTo,
  20. scrollBy: w.scrollBy,
  21. elementScroll: Element.prototype.scroll || scrollElement,
  22. scrollIntoView: Element.prototype.scrollIntoView
  23. };
  24. // define timing method
  25. var now =
  26. w.performance && w.performance.now
  27. ? w.performance.now.bind(w.performance)
  28. : Date.now;
  29. /**
  30. * indicates if a the current browser is made by Microsoft
  31. * @method isMicrosoftBrowser
  32. * @param {String} userAgent
  33. * @returns {Boolean}
  34. */
  35. function isMicrosoftBrowser(userAgent) {
  36. var userAgentPatterns = ['MSIE ', 'Trident/', 'Edge/'];
  37. return new RegExp(userAgentPatterns.join('|')).test(userAgent);
  38. }
  39. /*
  40. * IE has rounding bug rounding down clientHeight and clientWidth and
  41. * rounding up scrollHeight and scrollWidth causing false positives
  42. * on hasScrollableSpace
  43. */
  44. var ROUNDING_TOLERANCE = isMicrosoftBrowser(w.navigator.userAgent) ? 1 : 0;
  45. /**
  46. * changes scroll position inside an element
  47. * @method scrollElement
  48. * @param {Number} x
  49. * @param {Number} y
  50. * @returns {undefined}
  51. */
  52. function scrollElement(x, y) {
  53. this.scrollLeft = x;
  54. this.scrollTop = y;
  55. }
  56. /**
  57. * returns result of applying ease math function to a number
  58. * @method ease
  59. * @param {Number} k
  60. * @returns {Number}
  61. */
  62. function ease(k) {
  63. return 0.5 * (1 - Math.cos(Math.PI * k));
  64. }
  65. /**
  66. * indicates if a smooth behavior should be applied
  67. * @method shouldBailOut
  68. * @param {Number|Object} firstArg
  69. * @returns {Boolean}
  70. */
  71. function shouldBailOut(firstArg) {
  72. if (
  73. firstArg === null ||
  74. typeof firstArg !== 'object' ||
  75. firstArg.behavior === undefined ||
  76. firstArg.behavior === 'auto' ||
  77. firstArg.behavior === 'instant'
  78. ) {
  79. // first argument is not an object/null
  80. // or behavior is auto, instant or undefined
  81. return true;
  82. }
  83. if (typeof firstArg === 'object' && firstArg.behavior === 'smooth') {
  84. // first argument is an object and behavior is smooth
  85. return false;
  86. }
  87. // throw error when behavior is not supported
  88. throw new TypeError(
  89. 'behavior member of ScrollOptions ' +
  90. firstArg.behavior +
  91. ' is not a valid value for enumeration ScrollBehavior.'
  92. );
  93. }
  94. /**
  95. * indicates if an element has scrollable space in the provided axis
  96. * @method hasScrollableSpace
  97. * @param {Node} el
  98. * @param {String} axis
  99. * @returns {Boolean}
  100. */
  101. function hasScrollableSpace(el, axis) {
  102. if (axis === 'Y') {
  103. return el.clientHeight + ROUNDING_TOLERANCE < el.scrollHeight;
  104. }
  105. if (axis === 'X') {
  106. return el.clientWidth + ROUNDING_TOLERANCE < el.scrollWidth;
  107. }
  108. }
  109. /**
  110. * indicates if an element has a scrollable overflow property in the axis
  111. * @method canOverflow
  112. * @param {Node} el
  113. * @param {String} axis
  114. * @returns {Boolean}
  115. */
  116. function canOverflow(el, axis) {
  117. var overflowValue = w.getComputedStyle(el, null)['overflow' + axis];
  118. return overflowValue === 'auto' || overflowValue === 'scroll';
  119. }
  120. /**
  121. * indicates if an element can be scrolled in either axis
  122. * @method isScrollable
  123. * @param {Node} el
  124. * @param {String} axis
  125. * @returns {Boolean}
  126. */
  127. function isScrollable(el) {
  128. var isScrollableY = hasScrollableSpace(el, 'Y') && canOverflow(el, 'Y');
  129. var isScrollableX = hasScrollableSpace(el, 'X') && canOverflow(el, 'X');
  130. return isScrollableY || isScrollableX;
  131. }
  132. /**
  133. * finds scrollable parent of an element
  134. * @method findScrollableParent
  135. * @param {Node} el
  136. * @returns {Node} el
  137. */
  138. function findScrollableParent(el) {
  139. var isBody;
  140. do {
  141. el = el.parentNode;
  142. isBody = el === d.body;
  143. } while (isBody === false && isScrollable(el) === false);
  144. isBody = null;
  145. return el;
  146. }
  147. /**
  148. * self invoked function that, given a context, steps through scrolling
  149. * @method step
  150. * @param {Object} context
  151. * @returns {undefined}
  152. */
  153. function step(context) {
  154. var time = now();
  155. var value;
  156. var currentX;
  157. var currentY;
  158. var elapsed = (time - context.startTime) / SCROLL_TIME;
  159. // avoid elapsed times higher than one
  160. elapsed = elapsed > 1 ? 1 : elapsed;
  161. // apply easing to elapsed time
  162. value = ease(elapsed);
  163. currentX = context.startX + (context.x - context.startX) * value;
  164. currentY = context.startY + (context.y - context.startY) * value;
  165. context.method.call(context.scrollable, currentX, currentY);
  166. // scroll more if we have not reached our destination
  167. if (currentX !== context.x || currentY !== context.y) {
  168. w.requestAnimationFrame(step.bind(w, context));
  169. }
  170. }
  171. /**
  172. * scrolls window or element with a smooth behavior
  173. * @method smoothScroll
  174. * @param {Object|Node} el
  175. * @param {Number} x
  176. * @param {Number} y
  177. * @returns {undefined}
  178. */
  179. function smoothScroll(el, x, y) {
  180. var scrollable;
  181. var startX;
  182. var startY;
  183. var method;
  184. var startTime = now();
  185. // define scroll context
  186. if (el === d.body) {
  187. scrollable = w;
  188. startX = w.scrollX || w.pageXOffset;
  189. startY = w.scrollY || w.pageYOffset;
  190. method = original.scroll;
  191. } else {
  192. scrollable = el;
  193. startX = el.scrollLeft;
  194. startY = el.scrollTop;
  195. method = scrollElement;
  196. }
  197. // scroll looping over a frame
  198. step({
  199. scrollable: scrollable,
  200. method: method,
  201. startTime: startTime,
  202. startX: startX,
  203. startY: startY,
  204. x: x,
  205. y: y
  206. });
  207. }
  208. // ORIGINAL METHODS OVERRIDES
  209. // w.scroll and w.scrollTo
  210. w.scroll = w.scrollTo = function() {
  211. // avoid action when no arguments are passed
  212. if (arguments[0] === undefined) {
  213. return;
  214. }
  215. // avoid smooth behavior if not required
  216. if (shouldBailOut(arguments[0]) === true) {
  217. original.scroll.call(
  218. w,
  219. arguments[0].left !== undefined
  220. ? arguments[0].left
  221. : typeof arguments[0] !== 'object'
  222. ? arguments[0]
  223. : w.scrollX || w.pageXOffset,
  224. // use top prop, second argument if present or fallback to scrollY
  225. arguments[0].top !== undefined
  226. ? arguments[0].top
  227. : arguments[1] !== undefined
  228. ? arguments[1]
  229. : w.scrollY || w.pageYOffset
  230. );
  231. return;
  232. }
  233. // LET THE SMOOTHNESS BEGIN!
  234. smoothScroll.call(
  235. w,
  236. d.body,
  237. arguments[0].left !== undefined
  238. ? ~~arguments[0].left
  239. : w.scrollX || w.pageXOffset,
  240. arguments[0].top !== undefined
  241. ? ~~arguments[0].top
  242. : w.scrollY || w.pageYOffset
  243. );
  244. };
  245. // w.scrollBy
  246. w.scrollBy = function() {
  247. // avoid action when no arguments are passed
  248. if (arguments[0] === undefined) {
  249. return;
  250. }
  251. // avoid smooth behavior if not required
  252. if (shouldBailOut(arguments[0])) {
  253. original.scrollBy.call(
  254. w,
  255. arguments[0].left !== undefined
  256. ? arguments[0].left
  257. : typeof arguments[0] !== 'object' ? arguments[0] : 0,
  258. arguments[0].top !== undefined
  259. ? arguments[0].top
  260. : arguments[1] !== undefined ? arguments[1] : 0
  261. );
  262. return;
  263. }
  264. // LET THE SMOOTHNESS BEGIN!
  265. smoothScroll.call(
  266. w,
  267. d.body,
  268. ~~arguments[0].left + (w.scrollX || w.pageXOffset),
  269. ~~arguments[0].top + (w.scrollY || w.pageYOffset)
  270. );
  271. };
  272. // Element.prototype.scroll and Element.prototype.scrollTo
  273. Element.prototype.scroll = Element.prototype.scrollTo = function() {
  274. // avoid action when no arguments are passed
  275. if (arguments[0] === undefined) {
  276. return;
  277. }
  278. // avoid smooth behavior if not required
  279. if (shouldBailOut(arguments[0]) === true) {
  280. // if one number is passed, throw error to match Firefox implementation
  281. if (typeof arguments[0] === 'number' && arguments[1] === undefined) {
  282. throw new SyntaxError('Value could not be converted');
  283. }
  284. original.elementScroll.call(
  285. this,
  286. // use left prop, first number argument or fallback to scrollLeft
  287. arguments[0].left !== undefined
  288. ? ~~arguments[0].left
  289. : typeof arguments[0] !== 'object' ? ~~arguments[0] : this.scrollLeft,
  290. // use top prop, second argument or fallback to scrollTop
  291. arguments[0].top !== undefined
  292. ? ~~arguments[0].top
  293. : arguments[1] !== undefined ? ~~arguments[1] : this.scrollTop
  294. );
  295. return;
  296. }
  297. var left = arguments[0].left;
  298. var top = arguments[0].top;
  299. // LET THE SMOOTHNESS BEGIN!
  300. smoothScroll.call(
  301. this,
  302. this,
  303. typeof left === 'undefined' ? this.scrollLeft : ~~left,
  304. typeof top === 'undefined' ? this.scrollTop : ~~top
  305. );
  306. };
  307. // Element.prototype.scrollBy
  308. Element.prototype.scrollBy = function() {
  309. // avoid action when no arguments are passed
  310. if (arguments[0] === undefined) {
  311. return;
  312. }
  313. // avoid smooth behavior if not required
  314. if (shouldBailOut(arguments[0]) === true) {
  315. original.elementScroll.call(
  316. this,
  317. arguments[0].left !== undefined
  318. ? ~~arguments[0].left + this.scrollLeft
  319. : ~~arguments[0] + this.scrollLeft,
  320. arguments[0].top !== undefined
  321. ? ~~arguments[0].top + this.scrollTop
  322. : ~~arguments[1] + this.scrollTop
  323. );
  324. return;
  325. }
  326. this.scroll({
  327. left: ~~arguments[0].left + this.scrollLeft,
  328. top: ~~arguments[0].top + this.scrollTop,
  329. behavior: arguments[0].behavior
  330. });
  331. };
  332. // Element.prototype.scrollIntoView
  333. Element.prototype.scrollIntoView = function() {
  334. // avoid smooth behavior if not required
  335. if (shouldBailOut(arguments[0]) === true) {
  336. original.scrollIntoView.call(
  337. this,
  338. arguments[0] === undefined ? true : arguments[0]
  339. );
  340. return;
  341. }
  342. // LET THE SMOOTHNESS BEGIN!
  343. var scrollableParent = findScrollableParent(this);
  344. var parentRects = scrollableParent.getBoundingClientRect();
  345. var clientRects = this.getBoundingClientRect();
  346. if (scrollableParent !== d.body) {
  347. // reveal element inside parent
  348. smoothScroll.call(
  349. this,
  350. scrollableParent,
  351. scrollableParent.scrollLeft + clientRects.left - parentRects.left,
  352. scrollableParent.scrollTop + clientRects.top - parentRects.top
  353. );
  354. // reveal parent in viewport unless is fixed
  355. if (w.getComputedStyle(scrollableParent).position !== 'fixed') {
  356. w.scrollBy({
  357. left: parentRects.left,
  358. top: parentRects.top,
  359. behavior: 'smooth'
  360. });
  361. }
  362. } else {
  363. // reveal element in viewport
  364. w.scrollBy({
  365. left: clientRects.left,
  366. top: clientRects.top,
  367. behavior: 'smooth'
  368. });
  369. }
  370. };
  371. }
  372. if (typeof exports === 'object' && typeof module !== 'undefined') {
  373. // commonjs
  374. module.exports = { polyfill: polyfill };
  375. } else {
  376. // global
  377. polyfill();
  378. }