svelte-scrollto
Version:
Svelte action that listens for click events and scrolls to elements with animation. Inspired by rigor789/vue-scrollto.
199 lines (168 loc) • 4.13 kB
JavaScript
import { cubicInOut } from "svelte/easing";
import { noop, loop, now } from "svelte/internal";
import _ from "./helper";
const defaultOptions = {
container: "body",
duration: 500,
delay: 0,
offset: 0,
easing: cubicInOut,
onStart: noop,
onDone: noop,
onAborting: noop,
scrollX: false,
scrollY: true
};
const _scrollTo = options => {
let {
offset,
duration,
delay,
easing,
x=0,
y=0,
scrollX,
scrollY,
onStart,
onDone,
container,
onAborting,
element
} = options;
if (typeof offset === "function") {
offset = offset();
}
var cumulativeOffsetContainer = _.cumulativeOffset(container);
var cumulativeOffsetTarget = element
? _.cumulativeOffset(element)
: { top: y, left: x };
var initialX = _.scrollLeft(container);
var initialY = _.scrollTop(container);
var targetX =
cumulativeOffsetTarget.left - cumulativeOffsetContainer.left + offset;
var targetY =
cumulativeOffsetTarget.top - cumulativeOffsetContainer.top + offset;
var diffX = targetX - initialX;
var diffY = targetY - initialY;
let scrolling = true;
let started = false;
let start_time = now() + delay;
let end_time = start_time + duration;
function scrollToTopLeft(element, top, left) {
if (scrollX) _.scrollLeft(element, left);
if (scrollY) _.scrollTop(element, top);
}
function start(delayStart) {
if (!delayStart) {
started = true;
onStart(element, {x, y});
}
}
function tick(progress) {
scrollToTopLeft(
container,
initialY + diffY * progress,
initialX + diffX * progress
);
}
function stop() {
scrolling = false;
}
loop(now => {
if (!started && now >= start_time) {
start(false)
}
if (started && now >= end_time) {
tick(1);
stop();
onDone(element, {x, y});
}
if (!scrolling) {
onAborting(element, {x, y});
return false;
}
if (started) {
const p = now - start_time;
const t = 0 + 1 * easing(p / duration);
tick(t);
}
return true;
});
start(delay);
tick(0);
return stop;
};
const proceedOptions = options => {
let 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;
} else {
let body = document.body;
let html = document.documentElement;
return Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);
}
};
export const setGlobalOptions = options => {
_.extend(defaultOptions, options || {});
};
export const scrollTo = options => {
return _scrollTo(proceedOptions(options));
};
export const scrollToBottom = options => {
options = proceedOptions(options);
return _scrollTo(
_.extend(options, {
element: null,
y: scrollContainerHeight(options.container)
})
);
};
export const scrollToTop = options => {
options = proceedOptions(options);
return _scrollTo(
_.extend(options, {
element: null,
y: 0
})
);
};
export const makeScrollToAction = scrollToFunc => {
return (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(options) {
current = options;
},
destroy() {
node.removeEventListener("click", handle);
node.removeEventListener("touchstart", handle);
}
};
};
};
export const scrollto = makeScrollToAction(scrollTo);
export const scrolltotop = makeScrollToAction(scrollToTop);
export const scrolltobottom = makeScrollToAction(scrollToBottom);