Keine Beschreibung

smoothscroll.js 12KB

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