|
import { useRef, useEffect, useState } from 'react' |
|
|
|
|
|
|
|
|
|
export const Draggable = (props) => { |
|
const { children } = props |
|
const draggableRef = useRef(null) |
|
const rafRef = useRef(null) |
|
const [moving, setMoving] = useState(false) |
|
let currentObj, offsetX, offsetY |
|
|
|
useEffect(() => { |
|
const draggableElements = document.getElementsByClassName('draggable') |
|
|
|
|
|
function e(event) { |
|
if (!event) { |
|
event = window.event |
|
event.target = event.srcElement |
|
event.layerX = event.offsetX |
|
event.layerY = event.offsetY |
|
} |
|
|
|
if (event.type === 'touchstart' || event.type === 'touchmove') { |
|
event.clientX = event.touches[0].clientX |
|
event.clientY = event.touches[0].clientY |
|
} |
|
|
|
event.mx = event.pageX || event.clientX + document.body.scrollLeft |
|
|
|
event.my = event.pageY || event.clientY + document.body.scrollTop |
|
|
|
|
|
return event |
|
} |
|
|
|
|
|
|
|
document.onmousedown = start |
|
document.ontouchstart = start |
|
|
|
function start (event) { |
|
if (!draggableElements) return |
|
event = e(event) |
|
|
|
for (const drag of draggableElements) { |
|
|
|
if (inDragBox(event, drag)) { |
|
currentObj = drag.firstElementChild |
|
} |
|
} |
|
if (currentObj) { |
|
if (event.type === 'touchstart') { |
|
event.preventDefault() |
|
document.documentElement.style.overflow = 'hidden' |
|
} |
|
|
|
setMoving(true) |
|
offsetX = event.mx - currentObj.offsetLeft |
|
offsetY = event.my - currentObj.offsetTop |
|
|
|
document.onmousemove = move |
|
document.ontouchmove = move |
|
document.onmouseup = stop |
|
document.ontouchend = stop |
|
} |
|
} |
|
|
|
function move(event) { |
|
event = e(event) |
|
rafRef.current = requestAnimationFrame(() => updatePosition(event)) |
|
} |
|
|
|
const stop = (event) => { |
|
event = e(event) |
|
document.documentElement.style.overflow = 'auto' |
|
cancelAnimationFrame(rafRef.current) |
|
setMoving(false) |
|
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null |
|
} |
|
|
|
const updatePosition = (event) => { |
|
if (currentObj) { |
|
const left = event.mx - offsetX |
|
const top = event.my - offsetY |
|
currentObj.style.left = left + 'px' |
|
currentObj.style.top = top + 'px' |
|
checkInWindow() |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function inDragBox(event, drag) { |
|
const { clientX, clientY } = event |
|
const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild |
|
const horizontal = clientX > offsetLeft && clientX < offsetLeft + offsetWidth |
|
const vertical = clientY > offsetTop && clientY < offsetTop + offsetHeight |
|
|
|
if (horizontal && vertical) { |
|
return true |
|
} |
|
|
|
return false |
|
} |
|
|
|
|
|
|
|
|
|
function checkInWindow() { |
|
|
|
for (const drag of draggableElements) { |
|
|
|
const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild |
|
const { clientHeight, clientWidth } = document.documentElement |
|
if (offsetTop < 0) { |
|
drag.firstElementChild.style.top = 0 |
|
} |
|
if (offsetTop > (clientHeight - offsetHeight)) { |
|
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px' |
|
} |
|
if (offsetLeft < 0) { |
|
drag.firstElementChild.style.left = 0 |
|
} |
|
if (offsetLeft > (clientWidth - offsetWidth)) { |
|
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px' |
|
} |
|
} |
|
} |
|
|
|
window.addEventListener('resize', checkInWindow) |
|
|
|
return () => { |
|
return () => { |
|
window.removeEventListener('resize', checkInWindow) |
|
cancelAnimationFrame(rafRef.current) |
|
} |
|
} |
|
}, []) |
|
|
|
return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}> |
|
{children} |
|
</div> |
|
} |
|
|
|
Draggable.defaultProps = { left: 0, top: 0 } |
|
|