svelte-scrollto-element
Version:
Svelte action that listens for click events and scrolls to elements with animation. Inspired by rigor789/vue-scrollto.
139 lines (138 loc) • 4.48 kB
JavaScript
// animateScroll.ts
import { cubicInOut } from 'svelte/easing';
import { $, cumulativeOffset, extend, noop, scrollLeft, scrollTop, startAnimationLoop } from '../helpers/helper.js';
// Default options
const defaultOptions = {
container: 'body',
duration: 500,
delay: 0,
offset: 0,
easing: cubicInOut,
onStart: noop,
onDone: noop,
onAborting: noop,
scrollX: false,
scrollY: true
};
// Scroll to internal implementation
const scrollToInternal = (options) => {
const { duration, delay, easing, x = 0, y = 0, scrollX, scrollY, onStart, onDone, container, onAborting, element } = options;
let { offset } = options;
if (typeof offset === 'function') {
offset = offset();
}
const cumulativeOffsetContainer = cumulativeOffset(container);
const cumulativeOffsetTarget = element ? cumulativeOffset(element) : { top: y, left: x };
const initialX = scrollLeft(container);
const initialY = scrollTop(container);
const targetX = cumulativeOffsetTarget.left - cumulativeOffsetContainer.left + offset;
const targetY = cumulativeOffsetTarget.top - cumulativeOffsetContainer.top + offset;
const diffX = targetX - initialX;
const diffY = targetY - initialY;
let scrolling = true;
let started = false;
const startTime = performance.now() + delay;
const endTime = startTime + duration;
const scrollToTopLeft = (el, top, left) => {
if (scrollX)
scrollLeft(el, left);
if (scrollY)
scrollTop(el, top);
};
const start = () => {
if (!started) {
started = true;
onStart(element, { x, y });
}
};
const tick = (progress) => {
scrollToTopLeft(container, initialY + diffY * progress, initialX + diffX * progress);
};
const stop = () => {
scrolling = false;
};
startAnimationLoop((now) => {
if (!started && now >= startTime) {
start();
}
if (started && now >= endTime) {
tick(1);
stop();
onDone(element, { x, y });
}
if (!scrolling) {
onAborting(element, { x, y });
return false;
}
if (started) {
const p = (now - startTime) / duration;
const t = easing(p);
tick(t);
}
return scrolling;
});
tick(0); // Initial tick
return stop;
};
const proceedOptions = (options) => {
const opts = extend({}, defaultOptions, options);
opts.container = $(opts.container);
opts.element = $(opts.element);
return opts;
};
const scrollContainerHeight = (containerElement) => {
if (containerElement && containerElement !== document && containerElement !== document.body) {
return containerElement.scrollHeight - containerElement.offsetHeight;
}
const { body } = document;
const html = document.documentElement;
return Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
};
const setGlobalOptions = (options) => {
extend(defaultOptions, options || {});
};
// Scroll functions
const scrollTo = (options) => scrollToInternal(proceedOptions(options));
const scrollToBottom = (options) => {
const opts = proceedOptions(options);
return scrollToInternal(extend(opts, {
element: null,
y: scrollContainerHeight(opts.container)
}));
};
const scrollToTop = (options) => {
const opts = proceedOptions(options);
return scrollToInternal(extend(opts, {
element: null,
y: 0
}));
};
const makeScrollToAction = (scrollToFunc) => (node, options) => {
let current = options;
const handle = (e) => {
e.preventDefault();
scrollToFunc(typeof current === 'string' ? { element: current } : current);
};
node.addEventListener('click', handle);
node.addEventListener('touchstart', handle);
return {
update(opts) {
current = opts;
},
destroy() {
node.removeEventListener('click', handle);
node.removeEventListener('touchstart', handle);
}
};
};
// Actions
export const scrollto = makeScrollToAction(scrollTo);
export const scrolltotop = makeScrollToAction(scrollToTop);
export const scrolltobottom = makeScrollToAction(scrollToBottom);
// Methods
export const animateScroll = {
scrollTo,
scrollToTop,
scrollToBottom,
setGlobalOptions
};