const easeInOutQuad = (t, b, c, d) => { t /= d / 2; if (t < 1) return (c / 2) * t * t + b; t--; return (-c / 2) * (t * (t - 2) - 1) + b; }; const jumper = () => { // private variable cache // no variables are created during a jump, preventing memory leaks let container; // container element to be scrolled (node) let element; // element to scroll to (node) let start; // where scroll starts (px) let stop; // where scroll stops (px) let offset; // adjustment from the stop position (px) let easing; // easing function (function) let a11y; // accessibility support flag (boolean) let distance; // distance of scroll (px) let duration; // scroll duration (ms) let timeStart; // time scroll started (ms) let timeElapsed; // time spent scrolling thus far (ms) let next; // next scroll position (px) let callback; // to call when done scrolling (function) // scroll position helper function location() { let top = container.scrollTop || container.scrollY || container.pageYOffset; top = typeof top === "undefined" ? 0 : top; return top; } // element offset helper function top(element) { const elementTop = element.getBoundingClientRect().top; const containerTop = container.getBoundingClientRect ? container.getBoundingClientRect().top : 0; return elementTop - containerTop + start; } // scrollTo helper function scrollTo(top) { container.scrollTo ? container.scrollTo(0, top) // window : (container.scrollTop = top); // custom container } // rAF loop helper function loop(timeCurrent) { // store time scroll started, if not started already if (!timeStart) { timeStart = timeCurrent; } // determine time spent scrolling so far timeElapsed = timeCurrent - timeStart; // calculate next scroll position next = easing(timeElapsed, start, distance, duration); // scroll to it scrollTo(next); // check progress timeElapsed < duration ? requestAnimationFrame(loop) // continue scroll loop : done(); // scrolling is done } // scroll finished helper function done() { // account for rAF time rounding inaccuracies scrollTo(start + distance); // if scrolling to an element, and accessibility is enabled if (element && a11y) { // add tabindex indicating programmatic focus element.setAttribute("tabindex", "-1"); // focus the element element.focus(); } // if it exists, fire the callback if (typeof callback === "function") { callback(); } // reset time for next jump timeStart = false; } // API function jump(target, options = {}) { // resolve options, or use defaults duration = options.duration || 1000; offset = options.offset || 0; callback = options.callback; // "undefined" is a suitable default, and won't be called easing = options.easing || easeInOutQuad; a11y = options.a11y || false; // resolve container switch (typeof options.container) { case "object": // we assume container is an HTML element (Node) container = options.container; break; case "string": container = document.querySelector(options.container); break; default: container = window; } // cache starting position start = location(); // resolve target switch (typeof target) { // scroll from current position case "number": element = undefined; // no element to scroll to a11y = false; // make sure accessibility is off stop = start + target; break; // scroll to element (node) // bounding rect is relative to the viewport case "object": element = target; stop = top(element); break; // scroll to element (selector) // bounding rect is relative to the viewport case "string": element = document.querySelector(target); stop = top(element); break; } // resolve scroll distance, accounting for offset distance = stop - start + offset; // resolve duration switch (typeof options.duration) { // number in ms case "number": duration = options.duration; break; // function passed the distance of the scroll case "function": duration = options.duration(distance); break; } // start the loop requestAnimationFrame(loop); } // expose only the jump method return jump; }; // export singleton const singleton = jumper(); export default singleton;