jump.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. const easeInOutQuad = (t, b, c, d) => {
  2. t /= d / 2;
  3. if (t < 1) return (c / 2) * t * t + b;
  4. t--;
  5. return (-c / 2) * (t * (t - 2) - 1) + b;
  6. };
  7. const jumper = () => {
  8. // private variable cache
  9. // no variables are created during a jump, preventing memory leaks
  10. let container; // container element to be scrolled (node)
  11. let element; // element to scroll to (node)
  12. let start; // where scroll starts (px)
  13. let stop; // where scroll stops (px)
  14. let offset; // adjustment from the stop position (px)
  15. let easing; // easing function (function)
  16. let a11y; // accessibility support flag (boolean)
  17. let distance; // distance of scroll (px)
  18. let duration; // scroll duration (ms)
  19. let timeStart; // time scroll started (ms)
  20. let timeElapsed; // time spent scrolling thus far (ms)
  21. let next; // next scroll position (px)
  22. let callback; // to call when done scrolling (function)
  23. // scroll position helper
  24. function location() {
  25. let top = container.scrollTop || container.scrollY || container.pageYOffset;
  26. top = typeof top === "undefined" ? 0 : top;
  27. return top;
  28. }
  29. // element offset helper
  30. function top(element) {
  31. const elementTop = element.getBoundingClientRect().top;
  32. const containerTop = container.getBoundingClientRect
  33. ? container.getBoundingClientRect().top
  34. : 0;
  35. return elementTop - containerTop + start;
  36. }
  37. // scrollTo helper
  38. function scrollTo(top) {
  39. container.scrollTo
  40. ? container.scrollTo(0, top) // window
  41. : (container.scrollTop = top); // custom container
  42. }
  43. // rAF loop helper
  44. function loop(timeCurrent) {
  45. // store time scroll started, if not started already
  46. if (!timeStart) {
  47. timeStart = timeCurrent;
  48. }
  49. // determine time spent scrolling so far
  50. timeElapsed = timeCurrent - timeStart;
  51. // calculate next scroll position
  52. next = easing(timeElapsed, start, distance, duration);
  53. // scroll to it
  54. scrollTo(next);
  55. // check progress
  56. timeElapsed < duration
  57. ? requestAnimationFrame(loop) // continue scroll loop
  58. : done(); // scrolling is done
  59. }
  60. // scroll finished helper
  61. function done() {
  62. // account for rAF time rounding inaccuracies
  63. scrollTo(start + distance);
  64. // if scrolling to an element, and accessibility is enabled
  65. if (element && a11y) {
  66. // add tabindex indicating programmatic focus
  67. element.setAttribute("tabindex", "-1");
  68. // focus the element
  69. element.focus();
  70. }
  71. // if it exists, fire the callback
  72. if (typeof callback === "function") {
  73. callback();
  74. }
  75. // reset time for next jump
  76. timeStart = false;
  77. }
  78. // API
  79. function jump(target, options = {}) {
  80. // resolve options, or use defaults
  81. duration = options.duration || 1000;
  82. offset = options.offset || 0;
  83. callback = options.callback; // "undefined" is a suitable default, and won't be called
  84. easing = options.easing || easeInOutQuad;
  85. a11y = options.a11y || false;
  86. // resolve container
  87. switch (typeof options.container) {
  88. case "object":
  89. // we assume container is an HTML element (Node)
  90. container = options.container;
  91. break;
  92. case "string":
  93. container = document.querySelector(options.container);
  94. break;
  95. default:
  96. container = window;
  97. }
  98. // cache starting position
  99. start = location();
  100. // resolve target
  101. switch (typeof target) {
  102. // scroll from current position
  103. case "number":
  104. element = undefined; // no element to scroll to
  105. a11y = false; // make sure accessibility is off
  106. stop = start + target;
  107. break;
  108. // scroll to element (node)
  109. // bounding rect is relative to the viewport
  110. case "object":
  111. element = target;
  112. stop = top(element);
  113. break;
  114. // scroll to element (selector)
  115. // bounding rect is relative to the viewport
  116. case "string":
  117. element = document.querySelector(target);
  118. stop = top(element);
  119. break;
  120. }
  121. // resolve scroll distance, accounting for offset
  122. distance = stop - start + offset;
  123. // resolve duration
  124. switch (typeof options.duration) {
  125. // number in ms
  126. case "number":
  127. duration = options.duration;
  128. break;
  129. // function passed the distance of the scroll
  130. case "function":
  131. duration = options.duration(distance);
  132. break;
  133. }
  134. // start the loop
  135. requestAnimationFrame(loop);
  136. }
  137. // expose only the jump method
  138. return jump;
  139. };
  140. // export singleton
  141. const singleton = jumper();
  142. export default singleton;