UNPKG

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
// 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 };