vue-easy-dnd
Version:
Easy-DnD is a drag and drop implementation for Vue 3 that uses only standard mouse events instead of the HTML5 drag and drop API, which is [impossible to work with](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html). Think of it as a wa
277 lines (239 loc) • 9.88 kB
JavaScript
// Forked from https://github.com/bennadel/JavaScript-Demos/blob/master/demos/window-edge-scrolling/index.htm
// Code was altered to work with scrollable containers
let timer = null;
export function cancelScrollAction () {
clearTimeout(timer);
}
function isBodyContainer (container) {
return container === document.body;
}
// Determine if user is inside an edge of the container
function isInEdge (container, clientX, clientY, edgeSize) {
// Get the viewport-relative coordinates of the mousemove event.
const rect = container.getBoundingClientRect();
const isBody = isBodyContainer(container);
let viewportX = clientX - rect.left;
let viewportY = clientY - rect.top;
if (isBody) {
viewportX = clientX;
viewportY = clientY;
}
// Get the viewport dimensions.
let viewportWidth = rect.width;
let viewportHeight = rect.height;
if (isBody) {
viewportWidth = document.documentElement.clientWidth;
viewportHeight = document.documentElement.clientHeight;
}
// Next, we need to determine if the mouse is within the "edge" of the
// viewport, which may require scrolling the window. To do this, we need to
// calculate the boundaries of the edge in the viewport (these coordinates
// are relative to the viewport grid system).
const edgeTop = edgeSize;
const edgeLeft = edgeSize;
const edgeBottom = ( viewportHeight - edgeSize );
const edgeRight = (viewportWidth - edgeSize);
const isInLeftEdge = ( viewportX < edgeLeft );
const isInRightEdge = ( viewportX > edgeRight );
const isInTopEdge = ( viewportY < edgeTop );
const isInBottomEdge = ( viewportY > edgeBottom );
if (!(isInLeftEdge || isInRightEdge || isInTopEdge || isInBottomEdge)) {
return null;
}
return {
rect,
viewportX,
viewportY,
viewportWidth,
viewportHeight,
isInLeftEdge,
isInTopEdge,
isInRightEdge,
isInBottomEdge,
edgeTop,
edgeLeft,
edgeBottom,
edgeRight
};
}
// Determine if the scroll container has offets which will allow it to be scrolled X or Y
function canContainerBeScrolled (container, viewportWidth, viewportHeight) {
const isBody = isBodyContainer(container);
// Get the document dimensions.
const documentWidth = Math.max(
container.scrollWidth,
container.offsetWidth,
container.clientWidth
);
const documentHeight = Math.max(
container.scrollHeight,
container.offsetHeight,
container.clientHeight
);
// Calculate the maximum scroll offset in each direction. Since you can only
// scroll the overflow portion of the document, the maximum represents the
// length of the document that is NOT in the viewport.
const maxScrollX = (documentWidth - viewportWidth);
const maxScrollY = (documentHeight - viewportHeight);
// Get the current scroll position of the document.
let currentScrollX = container.scrollLeft;
let currentScrollY = container.scrollTop;
if (isBody) {
currentScrollX = window.scrollX;
currentScrollY = window.scrollY;
}
// Determine if the window can be scrolled in any particular direction.
const canScrollUp = (currentScrollY > 0);
const canScrollDown = (currentScrollY < maxScrollY);
const canScrollLeft = (currentScrollX > 0);
const canScrollRight = (currentScrollX < maxScrollX);
return {
documentWidth,
documentHeight,
maxScrollX,
maxScrollY,
currentScrollX,
currentScrollY,
canScroll: (canScrollUp || canScrollDown || canScrollLeft || canScrollRight),
canScrollUp,
canScrollDown,
canScrollLeft,
canScrollRight
};
}
// Determine whether the user is able to scroll based on current edge position and scroll of the container
function canBeScrolledInCurrentDirection (container, edgeSize, edgeParams, scrollParams) {
const {
viewportX,
viewportY,
isInLeftEdge,
isInRightEdge,
isInTopEdge,
isInBottomEdge,
edgeTop,
edgeLeft,
edgeBottom,
edgeRight
} = edgeParams;
const {
maxScrollX,
maxScrollY,
currentScrollX,
currentScrollY,
canScrollUp,
canScrollLeft,
canScrollDown,
canScrollRight
} = scrollParams;
// Since we can potentially scroll in two directions at the same time,
// let's keep track of the next scroll, starting with the current scroll.
// Each of these values can then be adjusted independently in the logic
// below.
let nextScrollX = currentScrollX;
let nextScrollY = currentScrollY;
// As we examine the mouse position within the edge, we want to make the
// incremental scroll changes more "intense" the closer that the user
// gets the viewport edge. As such, we'll calculate the percentage that
// the user has made it "through the edge" when calculating the delta.
// Then, that use that percentage to back-off from the "max" step value.
const maxStep = 50;
// Should we scroll left?
if (isInLeftEdge && canScrollLeft) {
const intensity = ((edgeLeft - viewportX) / edgeSize);
nextScrollX = (nextScrollX - (maxStep * intensity));
}
// Should we scroll right?
else if (isInRightEdge && canScrollRight) {
const intensity = ((viewportX - edgeRight) / edgeSize);
nextScrollX = (nextScrollX + (maxStep * intensity));
}
// Should we scroll up?
if (isInTopEdge && canScrollUp) {
const intensity = ((edgeTop - viewportY) / edgeSize);
nextScrollY = (nextScrollY - (maxStep * intensity));
}
// Should we scroll down?
else if (isInBottomEdge && canScrollDown) {
const intensity = ((viewportY - edgeBottom) / edgeSize);
nextScrollY = (nextScrollY + (maxStep * intensity));
}
// Sanitize invalid maximums. An invalid scroll offset won't break the
// subsequent .scrollTo() call; however, it will make it harder to
// determine if the .scrollTo() method should have been called in the
// first place.
nextScrollX = Math.max(0, Math.min(maxScrollX, nextScrollX));
nextScrollY = Math.max(0, Math.min(maxScrollY, nextScrollY));
if ((nextScrollX !== currentScrollX) || (nextScrollY !== currentScrollY)) {
return { nextScrollX, nextScrollY };
}
return null;
}
/** Main function to determine whether a node can be scrolled based on current cursor pos and container edge + scroll pos **/
export function isContainerReadyToEdgeScroll (container, clientX, clientY, edgeSize) {
// Check that the user's cursor is currently within an edge of this scrollable container
const edgeParams = isInEdge(container, clientX, clientY, edgeSize);
if (!edgeParams) {
return false;
}
// Check that the scrollable container still has remaining X or Y scroll
const { viewportWidth, viewportHeight } = edgeParams;
const scrollParams = canContainerBeScrolled(container, viewportWidth, viewportHeight);
if (!scrollParams.canScroll) {
return false;
}
// Check that the current cursor position sits within an edge and has remaining scroll to go
const canScrollInCurrDirection = canBeScrolledInCurrentDirection(container, edgeSize, edgeParams, scrollParams);
return !!canScrollInCurrDirection;
}
/** Main function for performing scroll action **/
export function performEdgeScroll (container, clientX, clientY, edgeSize) {
if (!container || !edgeSize) {
cancelScrollAction();
return false;
}
// NOTE: Much of the information here, with regard to document dimensions,
// viewport dimensions, and window scrolling is derived from JavaScript.info.
// I am consuming it here primarily as NOTE TO SELF.
// --
// Read More: https://javascript.info/size-and-scroll-window
// --
// CAUTION: The viewport and document dimensions can all be CACHED and then
// recalculated on window-resize events (for the most part). I am keeping it
// all here in the mousemove event handler to remove as many of the moving
// parts as possible and keep the demo as simple as possible.
// If the mouse is not in the viewport edge, there's no need to calculate
// anything else.
const edgeParams = isInEdge(container, clientX, clientY, edgeSize);
if (!edgeParams) {
cancelScrollAction();
return false;
}
const { viewportWidth, viewportHeight } = edgeParams;
// If we made it this far, the user's mouse is located within the edge of the
// viewport. As such, we need to check to see if scrolling needs to be done.
// As we examine the mousemove event, we want to adjust the window scroll in
// immediate response to the event; but, we also want to continue adjusting
// the window scroll if the user rests their mouse in the edge boundary. To
// do this, we'll invoke the adjustment logic immediately. Then, we'll setup
// a timer that continues to invoke the adjustment logic while the window can
// still be scrolled in a particular direction.
(function checkForWindowScroll () {
cancelScrollAction();
if (adjustWindowScroll()) {
timer = setTimeout( checkForWindowScroll, 30 );
}
})();
// Adjust the window scroll based on the user's mouse position. Returns True
// or False depending on whether or not the window scroll was changed.
function adjustWindowScroll () {
const scrollParams = canContainerBeScrolled(container, viewportWidth, viewportHeight);
const nextScrollParams = canBeScrolledInCurrentDirection(container, edgeSize, edgeParams, scrollParams);
if (nextScrollParams) {
const { nextScrollX, nextScrollY } = nextScrollParams;
(isBodyContainer(container) ? window : container).scrollTo(nextScrollX, nextScrollY);
return true;
}
return false;
}
return true;
}