UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

138 lines (135 loc) • 5.59 kB
import { frameData } from 'motion-dom'; const threshold = 50; // Pixels per millisecond (25px per frame at 60fps) const maxSpeed = 1.5; const overflowStyles = new Set(["auto", "scroll"]); /** * Per axis and scrollable element, for the current drag: the edge it's * auto-scrolling towards, "start" (top/left) or "end" (bottom/right), and * its scroll limit when that began (Bug 1 fix). A drag in a Reorder.Board * scrolls on both axes, often of one element (like the page), and can * scroll several, so they're kept per axis and all cleared when it ends. */ const scrollStates = { x: new Map(), y: new Map(), }; function resetAutoScrollState() { scrollStates.x.clear(); scrollStates.y.clear(); } function isScrollableElement(element, axis) { const style = getComputedStyle(element); const overflow = axis === "x" ? style.overflowX : style.overflowY; const isDocumentScroll = element === document.body || element === document.documentElement; /** * An element that scrolls on one axis computes overflow auto on both * (e.g. a column of a board that scrolls the other way), so it must also * have content to scroll on this one. */ return (isDocumentScroll || (overflowStyles.has(overflow) && (axis === "x" ? element.scrollWidth > element.clientWidth : element.scrollHeight > element.clientHeight))); } function findScrollableAncestor(element, axis) { let current = element?.parentElement; while (current) { if (isScrollableElement(current, axis)) { return current; } current = current.parentElement; } return null; } /** * Speed ramps up towards the edge and is capped at maxSpeed once the * pointer reaches or passes it. This runs from onDrag, within a frame, so * scale by the frame's elapsed time to scroll at the same speed whatever * the display's refresh rate. */ function scrollSpeed(distanceFromEdge) { const intensity = Math.min(1, 1 - distanceFromEdge / threshold); return maxSpeed * intensity * intensity * frameData.delta; } function getScrollAmount(pointerPosition, scrollElement, axis) { const rect = scrollElement.getBoundingClientRect(); const start = axis === "x" ? Math.max(0, rect.left) : Math.max(0, rect.top); const end = axis === "x" ? Math.min(window.innerWidth, rect.right) : Math.min(window.innerHeight, rect.bottom); const distanceFromStart = pointerPosition - start; const distanceFromEnd = end - pointerPosition; if (distanceFromStart < threshold) { return { amount: -scrollSpeed(distanceFromStart), edge: "start" }; } else if (distanceFromEnd < threshold) { return { amount: scrollSpeed(distanceFromEnd), edge: "end" }; } return { amount: 0, edge: null }; } function autoScrollIfNeeded(groupElement, pointerPosition, axis, velocity) { if (!groupElement) return; const scrollableAncestor = findScrollableAncestor(groupElement, axis); if (!scrollableAncestor) return; // Convert pointer position from page coordinates to viewport coordinates. // The gesture system uses pageX/pageY but getBoundingClientRect() returns // viewport-relative coordinates, so we need to account for page scroll. const viewportPointerPosition = pointerPosition - (axis === "x" ? window.scrollX : window.scrollY); const { amount: scrollAmount, edge } = getScrollAmount(viewportPointerPosition, scrollableAncestor, axis); const states = scrollStates[axis]; // If not in any threshold zone, clear this axis' state if (edge === null) { states.delete(scrollableAncestor); return; } let state = states.get(scrollableAncestor); const isDocumentScroll = scrollableAncestor === document.body || scrollableAncestor === document.documentElement; // If not currently scrolling this edge, check velocity to see if we should start if (state?.edge !== edge) { // Only start scrolling if velocity is towards the edge const shouldStart = (edge === "start" && velocity < 0) || (edge === "end" && velocity > 0); if (!shouldStart) return; // Activate this edge, and record the initial scroll limit // (prevents infinite scroll) state = { edge, limit: axis === "x" ? scrollableAncestor.scrollWidth - (isDocumentScroll ? window.innerWidth : scrollableAncestor.clientWidth) : scrollableAncestor.scrollHeight - (isDocumentScroll ? window.innerHeight : scrollableAncestor.clientHeight), }; states.set(scrollableAncestor, state); } // Cap scrolling at initial limit (prevents infinite scroll) if (scrollAmount > 0) { const currentScroll = axis === "x" ? (isDocumentScroll ? window.scrollX : scrollableAncestor.scrollLeft) : (isDocumentScroll ? window.scrollY : scrollableAncestor.scrollTop); if (currentScroll >= state.limit) return; } // Apply scroll if (axis === "x") { if (isDocumentScroll) { window.scrollBy({ left: scrollAmount }); } else { scrollableAncestor.scrollLeft += scrollAmount; } } else { if (isDocumentScroll) { window.scrollBy({ top: scrollAmount }); } else { scrollableAncestor.scrollTop += scrollAmount; } } } export { autoScrollIfNeeded, resetAutoScrollState }; //# sourceMappingURL=auto-scroll.mjs.map