terriajs
Version:
Geospatial data visualization platform.
161 lines • 6.94 kB
JavaScript
import { useCallback, useEffect, useRef, useState } from "react";
export const useDraggable = (options) => {
const [node, setNode] = useState();
// Use refs to track current values without triggering rerenders
const dxRef = useRef(0);
const dyRef = useRef(0);
const handleSelectorRef = useRef(options?.handleSelector);
// Update the ref if the handleSelector option changes
useEffect(() => {
handleSelectorRef.current = options?.handleSelector;
}, [options?.handleSelector]);
const ref = useCallback((nodeEle) => {
setNode(nodeEle);
}, []);
// Function to calculate bounds
const calculateBounds = useCallback(() => {
if (!node)
return null;
const parent = node?.parentElement;
if (!parent)
return null;
return {
minX: parent.offsetLeft,
maxX: parent.offsetLeft + parent.offsetWidth,
minY: parent.offsetTop,
maxY: parent.offsetTop + parent.offsetHeight
};
}, [node]);
// Function to constrain element within bounds
// Uses direct DOM manipulation to avoid React state batching delays
const constrainToBounds = useCallback(() => {
if (!node)
return;
const elementRect = node.getBoundingClientRect();
const bounds = calculateBounds();
if (!bounds)
return;
const { minX, maxX, minY, maxY } = bounds;
const currentDx = dxRef.current;
const currentDy = dyRef.current;
// Calculate constrained position
const constrainedDx = Math.min(Math.max(currentDx, minX - elementRect.left + currentDx), maxX - elementRect.width - elementRect.left + currentDx);
const constrainedDy = Math.min(Math.max(currentDy, minY - elementRect.top + currentDy), maxY - elementRect.height - elementRect.top + currentDy);
// Directly update the DOM for immediate visual effect
node.style.transform = `translate3d(${constrainedDx}px, ${constrainedDy}px, 0)`;
// Update refs to track current position
dxRef.current = constrainedDx;
dyRef.current = constrainedDy;
}, [node, calculateBounds]);
// Function to check if the event target is the handle or within the handle
const isValidDragHandle = useCallback((target) => {
if (!handleSelectorRef.current || !node || !target)
return true;
// If we have a handle selector, check if the target matches or is within a matching element
const handle = node.querySelector(handleSelectorRef.current);
return handle
? handle === target || handle.contains(target)
: false;
}, [node]);
// Shared function to update element position
const updateElementPosition = useCallback((dx, dy) => {
if (!node)
return;
node.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
dxRef.current = dx;
dyRef.current = dy;
}, [node]);
// Generic drag start handler
const startDrag = useCallback((clientX, clientY) => {
// Get element dimensions to ensure it stays within bounds
const elementRect = node?.getBoundingClientRect();
if (!elementRect)
return;
// Calculate the offset of the pointer within the element
const offsetX = clientX - elementRect.left;
const offsetY = clientY - elementRect.top;
// Capture the current dx and dy at the start of the drag operation
// These values need to be captured here, not read in the move handler
const initialDx = dxRef.current;
const initialDy = dyRef.current;
const moveHandler = (clientX, clientY) => {
// Calculate the new position relative to the start position
const newDx = clientX - elementRect.left - offsetX + initialDx;
const newDy = clientY - elementRect.top - offsetY + initialDy;
// Allow free movement during dragging
updateElementPosition(newDx, newDy);
};
const endHandler = () => {
// Apply constraints only at the end of the drag
constrainToBounds();
};
return { moveHandler, endHandler };
}, [node, updateElementPosition, constrainToBounds]);
const handleMouseDown = useCallback((e) => {
// Check if the event target is a valid drag handle
if (!isValidDragHandle(e.target))
return;
const dragResult = startDrag(e.clientX, e.clientY);
if (!dragResult)
return;
const { moveHandler, endHandler } = dragResult;
const handleMouseMove = (e) => {
moveHandler(e.clientX, e.clientY);
};
const handleMouseUp = () => {
endHandler();
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}, [isValidDragHandle, startDrag]);
const handleTouchStart = useCallback((e) => {
// Check if the event target is a valid drag handle
if (!isValidDragHandle(e.target))
return;
const touch = e.touches[0];
const dragResult = startDrag(touch.clientX, touch.clientY);
if (!dragResult)
return;
const { moveHandler, endHandler } = dragResult;
const handleTouchMove = (e) => {
const touch = e.touches[0];
moveHandler(touch.clientX, touch.clientY);
};
const handleTouchEnd = () => {
endHandler();
document.removeEventListener("touchmove", handleTouchMove);
document.removeEventListener("touchend", handleTouchEnd);
};
document.addEventListener("touchmove", handleTouchMove);
document.addEventListener("touchend", handleTouchEnd);
}, [isValidDragHandle, startDrag]);
// Check bounds when the component mounts
useEffect(() => {
constrainToBounds();
}, [constrainToBounds]);
// Handle window resize
useEffect(() => {
const handleResize = () => {
constrainToBounds();
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [constrainToBounds]);
useEffect(() => {
if (!node) {
return;
}
node.addEventListener("mousedown", handleMouseDown);
node.addEventListener("touchstart", handleTouchStart);
return () => {
node.removeEventListener("mousedown", handleMouseDown);
node.removeEventListener("touchstart", handleTouchStart);
};
}, [node, handleMouseDown, handleTouchStart]);
return [ref];
};
//# sourceMappingURL=useDraggable.js.map