svelte-dnd-action
Version:
*An awesome drag and drop library for Svelte 3 and 4 (not using the browser's built-in dnd, thanks god): Rich animations, nested containers, touch support and more *
113 lines (108 loc) • 4.34 kB
JavaScript
import {isPointInsideRect} from "./intersection";
const SCROLL_ZONE_PX = 30;
/**
* Will make a scroller that can scroll any element given to it in any direction
* @returns {{scrollIfNeeded: function(Point, HTMLElement): boolean, resetScrolling: function(void):void}}
*/
export function makeScroller() {
let scrollingInfo;
function resetScrolling() {
scrollingInfo = {directionObj: undefined, stepPx: 0};
}
resetScrolling();
// directionObj {x: 0|1|-1, y:0|1|-1} - 1 means down in y and right in x
function scrollContainer(containerEl) {
const {directionObj, stepPx} = scrollingInfo;
if (directionObj) {
containerEl.scrollBy(directionObj.x * stepPx, directionObj.y * stepPx);
window.requestAnimationFrame(() => scrollContainer(containerEl));
}
}
function calcScrollStepPx(distancePx) {
return SCROLL_ZONE_PX - distancePx;
}
/**
* @param {Point} pointer - the pointer will be used to decide in which direction to scroll
* @param {HTMLElement} elementToScroll - the scroll container
* If the pointer is next to the sides of the element to scroll, will trigger scrolling
* Can be called repeatedly with updated pointer and elementToScroll values without issues
* @return {boolean} - true if scrolling was needed
*/
function scrollIfNeeded(pointer, elementToScroll) {
if (!elementToScroll) {
return false;
}
const distances = calcInnerDistancesBetweenPointAndSidesOfElement(pointer, elementToScroll);
const isAlreadyScrolling = !!scrollingInfo.directionObj;
if (distances === null) {
if (isAlreadyScrolling) resetScrolling();
return false;
}
let [scrollingVertically, scrollingHorizontally] = [false, false];
// vertical
if (elementToScroll.scrollHeight > elementToScroll.clientHeight) {
if (distances.bottom < SCROLL_ZONE_PX) {
scrollingVertically = true;
scrollingInfo.directionObj = {x: 0, y: 1};
scrollingInfo.stepPx = calcScrollStepPx(distances.bottom);
} else if (distances.top < SCROLL_ZONE_PX) {
scrollingVertically = true;
scrollingInfo.directionObj = {x: 0, y: -1};
scrollingInfo.stepPx = calcScrollStepPx(distances.top);
}
if (!isAlreadyScrolling && scrollingVertically) {
scrollContainer(elementToScroll);
return true;
}
}
// horizontal
if (elementToScroll.scrollWidth > elementToScroll.clientWidth) {
if (distances.right < SCROLL_ZONE_PX) {
scrollingHorizontally = true;
scrollingInfo.directionObj = {x: 1, y: 0};
scrollingInfo.stepPx = calcScrollStepPx(distances.right);
} else if (distances.left < SCROLL_ZONE_PX) {
scrollingHorizontally = true;
scrollingInfo.directionObj = {x: -1, y: 0};
scrollingInfo.stepPx = calcScrollStepPx(distances.left);
}
if (!isAlreadyScrolling && scrollingHorizontally) {
scrollContainer(elementToScroll);
return true;
}
}
resetScrolling();
return false;
}
return {
scrollIfNeeded,
resetScrolling
};
}
/**
* If the point is inside the element returns its distances from the sides, otherwise returns null
* @param {Point} point
* @param {HTMLElement} el
* @return {null|{top: number, left: number, bottom: number, right: number}}
*/
function calcInnerDistancesBetweenPointAndSidesOfElement(point, el) {
// Even if the scrolling element is small it acts as a scroller for the viewport
const rect =
el === document.scrollingElement
? {
top: 0,
bottom: window.innerHeight,
left: 0,
right: window.innerWidth
}
: el.getBoundingClientRect();
if (!isPointInsideRect(point, rect)) {
return null;
}
return {
top: point.y - rect.top,
bottom: rect.bottom - point.y,
left: point.x - rect.left,
right: rect.right - point.x
};
}