React drag and drop / 리액트 드래그 앤 드롭 이용해서 이동시키기
1. 박스 내에서 드래그를 시킨다.
2. 드래그 영역이 박스 밖으로 이탈 시 이전 포지션값으로 이동
원리
1. 큰박스( container ) 내 absolute 상태인 작은박스( dragComponent )의 포지션값을 계산해준다.
2. 드래그 시작시, 시작 포지션을 저장한다.
3. 드래그중에 실시간으로 position x y 좌표값을 변경해준다. (실제 drag할 요소가 위치하는 포지션값)
4. 드래그를 종료하는 시점에서 dragComponent의 left, right, top, bottom 값이 container 값 밖에 위치하는지 체크한다.
5. container를 벗어나면 드래그 시작시 저장한 포지션값을 실제 포지션에 set한다.
필요한 상태값
const containerRef = useRef<HTMLDivElement>(null); // 드래그 할 영역 네모 박스 Ref
const dragComponentRef = useRef<HTMLDivElement>(null); // // 움직일 드래그 박스 Ref
const [originPos, setOriginPos] = useState({ x: 0, y: 0 }); // 드래그 전 포지션값 (e.target.offset의 상대 위치)
const [clientPos, setClientPos] = useState({ x: 0, y: 0 }); // 실시간 커서위치인 e.client를 갱신하는값
const [pos, setPos] = useState({ left: 0, top: 0 }); // 실제 drag할 요소가 위치하는 포지션값
HTML
ref를 걸어주고, dragComponent에 스타일로 실시간 포지션값을 가져온다.
onDragStart // 드래그가 시작할때
onDrag // 드래그 중
onDragOver // 드래그가 겹칠때
onDragEnd // 드래그 종료시
<div className="container" ref={containerRef}>
<div
className="drag-component"
ref={dragComponent}
draggable
onDragStart={(e) => dragStartHandler(e)}
onDrag={(e) => dragHandler(e)}
onDragOver={(e) => dragOverHandler(e)}
onDragEnd={(e) => dragEndHandler(e)}
style={{ left: pos.left, top: pos.top }}
>
drag me!
</div>
onDragStart
드래그 할때 고스트이미지를 제거하려면, 투명 캔버스를 생성하여 띄워준다. 드래그 종료시 캔버스를 제거해주어야 한다.
투명 캔버스를 생성하게되면 필연적으로 스크롤이 생기게되는데,
이를 방지하기위해서 드래그 시작할때 바디의 overflow를 hidden으로 변경하고, 드래그 종료시 style attribute를 제거해주었다.
크롬에서는 드래그시에 초록색 + 아이콘이 따라오는데, 이를 제거하려면
e.dataTransfer.effectAllowed = "move"; 를 추가한다.
const dragStartHandler = (e: any) => {
const blankCanvas: any = document.createElement('canvas')
blankCanvas.classList.add("canvas");
e.dataTransfer?.setDragImage(blankCanvas, 0, 0);
document.body?.appendChild(blankCanvas); // 투명 캔버스를 생성하여 글로벌 아이콘 제거
e.dataTransfer.effectAllowed = "move"; // 크롬의그린 +아이콘 제거
const originPosTemp = { ...originPos };
originPosTemp["x"] = e.target.offsetLeft;
originPosTemp["y"] = e.target.offsetTop;
console.log("originPosTemp", originPosTemp);
setOriginPos(originPosTemp); //드래그 시작할때 드래그 전 위치값을 저장
const clientPosTemp = { ...clientPos };
clientPosTemp["x"] = e.clientX;
clientPosTemp["y"] = e.clientY;
setClientPos(clientPosTemp);
};
onDrag
const dragHandler = (e: any) => {
const PosTemp = { ...pos };
PosTemp["left"] = e.target.offsetLeft + e.clientX - clientPos.x;
PosTemp["top"] = e.target.offsetTop + e.clientY - clientPos.y;
setPos(PosTemp);
const clientPosTemp = { ...clientPos };
clientPosTemp["x"] = e.clientX;
clientPosTemp["y"] = e.clientY;
setClientPos(clientPosTemp);
};
onDragOver
const dragOverHandler = (e: any) => {
e.preventDefault(); // 드래그시에 플라잉백하는 고스트이미지를 제거한다
};
onDragEnd
const dragEndHandler = (e: any) => {
if (!isInsideDragArea(e)) {
const posTemp = { ...pos };
posTemp["left"] = originPos.x;
posTemp["top"] = originPos.y;
setPos(posTemp);
}
// 캔버스 제거
const canvases = document.getElementsByClassName("canvas");
for (let i = 0; i < canvases.length; i++) {
let canvas = canvases[i];
canvas.parentNode?.removeChild(canvas);
}
// 캔버스로 인해 발생한 스크롤 방지 어트리뷰트 제거
document.body.removeAttribute("style");
};
isInsideDragArea
드래그 종료 시점에 container 안에 있는지 확인하는 함수.
바깥이라면 드래그 시작할때 저장해둔 이전 좌표값을 넣는다.
...
*사파리에서 이슈가 있는지 반드시 확인하자