요소를 드래그 가능하게 만들고, 특정 영역에 드랍하면 관련 데이터를 transfer해서 새로운 요소를 생성하는 기능을 구현해 보았다 (+요소간 데이터 비교해서 제일 큰 값 하이라이트 주기..!)
드래그를 시작하는 순간 실행되는 이벤트. 다음과 같은 함수를 걸어줬다. 옆에 숫자는.. 이해를 돕기 위한...
1 const dragStartHandler = e => {
2 const img = new Image();
3 e.dataTransfer.setDragImage(img, 0, 0);
4
5 posX = e.clientX;
6 posY = e.clientY;
7
8 originalX = e.target.offsetLeft;
9 originalY = e.target.offsetTop;
10 };
2, 3 👉 요소에 draggable 속성을 주면 드래그했을 때 요소가 반투명하게 따라오는데 그걸 없애기 위해 빈 이미지를 생성해 setDragImage로 넣어준 것
5, 6 👉 드래그 시 요소를 실제로 이동시키기 위해 필요한 코드 (💻 2.에서 설명)
8, 9 👉 드래그하던 요소를 드랍했을 때 원래 위치로 돌려놓기 위해 초기 위치값을 저장함
드래그 중일 때 실행되는 이벤트. onDragStart 직후부터 onDragEnd 직전까지 계속 호출됨!
1 const dragHandler = e => {
2 e.target.style.left = `${e.target.offsetLeft + e.clientX - posX}px`;
3 e.target.style.top = `${e.target.offsetTop + e.clientY - posY}px`;
4 posY = e.clientY;
5 posX = e.clientX;
6 };
2~6 👉 드래그 시 커서의 움직임에 따라 요소를 이동시키기 위해 필요한 코드 (💻 2.에서 설명)
마우스를 놓았을 때 드래그가 끝나면서 실행되는 이벤트! 여기서 해야할 건
(1) 올바른 영역 안에서 드랍 되었는지 체크 후 새로운 요소 생성하기
(2) 드래그하며 움직였던 요소 제자리에 돌려놓기
1 const dragEndHandler = e => {
2 if (
3 box.left < e.clientX &&
4 e.clientX < box.right &&
5 box.top < e.clientY &&
6 e.clientY < box.bottom
7 ) {
8 setTargets(targets => {
9 const newTargets = [...targets];
10 newTargets.push({
11 id: parseInt(e.timeStamp),
12 top: e.target.offsetTop + e.clientY - posY,
13 left: e.target.offsetLeft + e.clientX - posX,
14 details: STOCK_DATA[e.target.id],
15 });
16 return newTargets;
17 });
18 }
19
20 e.target.style.left = `${originalX}px`;
21 e.target.style.top = `${originalY}px`;
22 };
2~7 👉 '커서가 원하는 영역 안에 들어오면'을 표현한 조건. box는 요소를 드랍할 수 있는 영역의 위치정보를 아래처럼 getBoundingClienRect를 이용해 담아놓은 것..!
//drop할 영역이 위치한 컴포넌트
const stockContainer = useRef();
const [targets, setTargets] = useRecoilState(targetsState);
useEffect(() => {
const box = stockContainer.current.getBoundingClientRect();
setBox({
top: box.top,
left: box.left,
bottom: box.top + box.height,
right: box.left + box.width,
});
}, []);
recoil을 쓴 이유는 드래그할 요소들이 위치한 컴포넌트와 드랍할 타겟 영역이 위치한 컴포넌트가 달라서 전역 상태관리가 필요했기 때문!
8~17 👉 커서가 타겟 영역에 들어왔으면, 드랍 당시 요소의 위치(top, left)와 드래그한 요소에 따라 관련된 데이터(details)를 저장해서 새로운 요소 생성하기 (💻 3.에서 설명)
20, 21 👉 요소를 드래그 onDrag에서 저장해 둔 원래 위치로 돌려놓기!
let posX = 0;
let posY = 0;
let originalX = 0;
let originalY = 0;
드래그하며 커서가 이동할 때마다 위치를 잡아서 계산해 줘야 하는데, 그걸 시시각각 담을 수 있는 변수가 필요하다.
이건 드래그를 끝냈을 때 요소를 원래 위치로 돌려놓기 위해서 기존의 요소 left, top 좌표를 저장해 놓은 것! 그냥 이동시킨 곳에 그대로 둘 거면 필요 ❌❌
네 변수 모두 하나의 함수에서만 쓰이는 게 아니라 onDragStart, onDrag, onDragEnd에서 호출되는 함수에서 다 필요하기 때문에 함수 바깥에 전역 scope로 선언해 줌!
우선 onDragStart의 콜백함수에서 드래그를 시작했을 때 커서의 위치를 posX, posY에 할당해 준다.
const dragStartHandler = e => {
...
posX = e.clientX;
posY = e.clientY;
originalX = e.target.offsetLeft;
originalY = e.target.offsetTop;
};
다음으로 onDrag의 콜백함수에서 요소(e.target)의 위치를 계산&반영해준다. 계산은 요소의 현재위치에서 드래그로 인해 변화한 커서의 위치를 반영해야 하기 때문에 [현재 요소의 좌표(left, top) + 현재 커서의 좌표 - 직전 커서의 좌표]
로 해 줌!
const dragHandler = e => {
e.target.style.left = `${e.target.offsetLeft + e.clientX - posX}px`;
e.target.style.top = `${e.target.offsetTop + e.clientY - posY}px`;
posX = e.clientX;
posY = e.clientY;
};
그후 이전 onDrag시 커서의 위치였던 posX, posY를 현재 커서의 위치로 업데이트해서 다음 onDrag에서 사용할 수 있도록 한다.
그다음 onDragEnd의 콜백 함수에서 요소를 처음 위치로 다시 옮겨준다.
const dragEndHandler = e => {
...
e.target.style.left = `${originalX}px`;
e.target.style.top = `${originalY}px`;
};
만약 드래그가 끝났을 때 요소를 그냥 그 자리에 두고 싶다면 left/top에 originalX/Y 대신 onDrag에서 계산했던 것처럼 e.target.offsetLeft + e.clientX/Y - posX/Y
를 넣어 주면 된당.
새로 엘리먼트를 만드는 건... 어떻게 하지? 고민하다가 이것저것 서치해 봤는데, React.createElement나 cloneNode 같은 방법들이 나왔다. 근데 빈 Array를 만들어서 원하는 대로 map을 돌려 놓고, drop시마다 거기에 데이터를 추가하는 방식으로도 할 수 있을 것 같아서 구현해 봄!
따라서 새로운 요소를 생성하려면 onDropEnd에서 데이터를 전달해 줘야 한다.
사실 데이터 전달은 dataTransfer의 setData, getData를 이용해서 할 수도 있는데.. 데이터 자체가 innerText형식으로 요소 안에 내재되어 있는 게 아니라, STOCK_DATA라는 별도의 변수에서 꺼내야 하다 보니까 dataTransfer 없이도 되길래... 그냥 함.ㅎㅎ;; 대신 각 요소의 id로 인덱스를 설정하고, e.target.id로 추출해서 STOCK_DATA[e.target.id]
형식으로 데이터를 꺼내왔다.
const dragEndHandler = e => {
if (
box.left < e.clientX &&
e.clientX < box.right &&
box.top < e.clientY &&
e.clientY < box.bottom
) {
setTargets(targets => {
const newTargets = [...targets];
newTargets.push({
id: parseInt(e.timeStamp),
top: e.target.offsetTop + e.clientY - posY,
left: e.target.offsetLeft + e.clientX - posX,
details: STOCK_DATA[e.target.id],
});
return newTargets;
});
}
...
};
detail외에 그 밖의 요소는
그리고 state의 불변성을 지키기 위해 target을 복사한 newTargets을 조작해서 리턴했다.
친절한 글을 쓰고 싶었는데..... 나만 알아볼 수 있는 기록이 된 것 같은 기분>.<;;
갓도은님! 너무 멋있어요!!! 소중한 자료 감사합니다 :) 👍