UNPKG

@mantine/hooks

Version:

A collection of 50+ hooks for state and UI management

266 lines (265 loc) 8.44 kB
"use client"; import { useCallback, useRef, useState } from "react"; //#region packages/@mantine/hooks/src/use-drag/use-drag.ts const VELOCITY_DECAY_MS = 100; function sign(n) { if (n > 0) return 1; if (n < 0) return -1; return 0; } function getThresholdVector(threshold) { const t = threshold ?? 0; if (typeof t === "number") return [t, t]; return t; } function createInitialState() { return { isActive: false, pointerId: -1, startXY: [0, 0], prevXY: [0, 0], startTimestamp: 0, prevTimestamp: 0, thresholdMet: false, firstFired: false, lockedAxis: null, canceled: false, lastVelocity: [0, 0] }; } function useDrag(handler, options = {}) { const [active, setActive] = useState(false); const handlerRef = useRef(handler); handlerRef.current = handler; const optionsRef = useRef(options); optionsRef.current = options; const stateRef = useRef(createInitialState()); const documentControllerRef = useRef(null); return { ref: useCallback((node) => { if (!node) return; const elementController = new AbortController(); const applyAxisConstraint = (v) => { const opts = optionsRef.current; const s = stateRef.current; if (opts.axis === "x") return [v[0], 0]; if (opts.axis === "y") return [0, v[1]]; if (opts.axis === "lock") { if (s.lockedAxis === null) { const t = opts.axisThreshold ?? 1; if (Math.abs(v[0]) > t || Math.abs(v[1]) > t) s.lockedAxis = Math.abs(v[0]) >= Math.abs(v[1]) ? "x" : "y"; } if (s.lockedAxis === "x") return [v[0], 0]; if (s.lockedAxis === "y") return [0, v[1]]; } return v; }; const resetDrag = () => { const s = stateRef.current; s.isActive = false; s.pointerId = -1; s.thresholdMet = false; s.firstFired = false; s.lockedAxis = null; s.canceled = false; setActive(false); document.body.style.userSelect = ""; document.body.style.webkitUserSelect = ""; documentControllerRef.current?.abort(); documentControllerRef.current = null; }; const cancel = () => { if (stateRef.current.isActive) { stateRef.current.canceled = true; resetDrag(); } }; const activateDrag = () => { setActive(true); document.body.style.userSelect = "none"; document.body.style.webkitUserSelect = "none"; }; const onPointerDown = (event) => { if (optionsRef.current.enabled === false) return; if (event.button !== 0) return; if (stateRef.current.isActive) return; const s = stateRef.current; s.isActive = true; s.pointerId = event.pointerId; s.startXY = [event.clientX, event.clientY]; s.prevXY = [event.clientX, event.clientY]; s.startTimestamp = event.timeStamp; s.prevTimestamp = event.timeStamp; s.thresholdMet = false; s.firstFired = false; s.lockedAxis = null; s.canceled = false; s.lastVelocity = [0, 0]; const [tx, ty] = getThresholdVector(optionsRef.current.threshold); if (tx === 0 && ty === 0) { s.thresholdMet = true; s.firstFired = true; activateDrag(); handlerRef.current({ xy: [event.clientX, event.clientY], initial: [event.clientX, event.clientY], movement: [0, 0], delta: [0, 0], distance: [0, 0], direction: [0, 0], velocity: [0, 0], elapsedTime: 0, first: true, last: false, active: true, tap: false, canceled: false, cancel, event }); } documentControllerRef.current?.abort(); documentControllerRef.current = new AbortController(); const sig = documentControllerRef.current.signal; document.addEventListener("pointermove", onPointerMove, { signal: sig }); document.addEventListener("pointerup", onPointerUp, { signal: sig }); document.addEventListener("pointercancel", onPointerCancel, { signal: sig }); }; const onPointerMove = (event) => { const s = stateRef.current; if (!s.isActive || event.pointerId !== s.pointerId) return; const rawMovement = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]]; if (!s.thresholdMet) { const [tx, ty] = getThresholdVector(optionsRef.current.threshold); if (Math.abs(rawMovement[0]) < tx && Math.abs(rawMovement[1]) < ty) { s.prevXY = [event.clientX, event.clientY]; s.prevTimestamp = event.timeStamp; return; } s.thresholdMet = true; activateDrag(); } const movement = applyAxisConstraint(rawMovement); const delta = applyAxisConstraint([event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]]); const timeDelta = event.timeStamp - s.prevTimestamp; const velocity = timeDelta > 0 ? [Math.abs(delta[0]) / timeDelta, Math.abs(delta[1]) / timeDelta] : s.lastVelocity; s.lastVelocity = velocity; const isFirst = !s.firstFired; s.firstFired = true; s.prevXY = [event.clientX, event.clientY]; s.prevTimestamp = event.timeStamp; handlerRef.current({ xy: [event.clientX, event.clientY], initial: [...s.startXY], movement, delta, distance: [Math.abs(movement[0]), Math.abs(movement[1])], direction: [sign(delta[0]), sign(delta[1])], velocity, elapsedTime: event.timeStamp - s.startTimestamp, first: isFirst, last: false, active: true, tap: false, canceled: false, cancel, event }); }; const onPointerUp = (event) => { const s = stateRef.current; if (!s.isActive || event.pointerId !== s.pointerId) return; const opts = optionsRef.current; if (!s.thresholdMet) { if (opts.filterTaps) { const mov = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]); const dist = [Math.abs(mov[0]), Math.abs(mov[1])]; const isTap = Math.max(dist[0], dist[1]) < (opts.tapThreshold ?? 3); handlerRef.current({ xy: [event.clientX, event.clientY], initial: [...s.startXY], movement: mov, delta: mov, distance: dist, direction: [sign(mov[0]), sign(mov[1])], velocity: [0, 0], elapsedTime: event.timeStamp - s.startTimestamp, first: true, last: true, active: false, tap: isTap, canceled: false, cancel, event }); } resetDrag(); return; } const movement = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]); const distance = [Math.abs(movement[0]), Math.abs(movement[1])]; const delta = applyAxisConstraint([event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]]); const velocity = event.timeStamp - s.prevTimestamp > VELOCITY_DECAY_MS ? [0, 0] : s.lastVelocity; const maxDistance = Math.max(distance[0], distance[1]); const tap = opts.filterTaps === true && maxDistance < (opts.tapThreshold ?? 3); handlerRef.current({ xy: [event.clientX, event.clientY], initial: [...s.startXY], movement, delta, distance, direction: [sign(delta[0]), sign(delta[1])], velocity, elapsedTime: event.timeStamp - s.startTimestamp, first: !s.firstFired, last: true, active: false, tap, canceled: false, cancel, event }); resetDrag(); }; const onPointerCancel = (event) => { const s = stateRef.current; if (!s.isActive || event.pointerId !== s.pointerId) return; const movement = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]); handlerRef.current({ xy: [event.clientX, event.clientY], initial: [...s.startXY], movement, delta: [0, 0], distance: [Math.abs(movement[0]), Math.abs(movement[1])], direction: [0, 0], velocity: [0, 0], elapsedTime: event.timeStamp - s.startTimestamp, first: !s.firstFired, last: true, active: false, tap: false, canceled: true, cancel, event }); resetDrag(); }; node.addEventListener("pointerdown", onPointerDown, { signal: elementController.signal }); return () => { elementController.abort(); documentControllerRef.current?.abort(); documentControllerRef.current = null; if (stateRef.current.isActive) { stateRef.current.isActive = false; setActive(false); document.body.style.userSelect = ""; document.body.style.webkitUserSelect = ""; } }; }, []), active }; } //#endregion export { useDrag }; //# sourceMappingURL=use-drag.mjs.map