앞편에 이어서 본격적으로 이동 로직을 구현해보자.
container에 dragend 이벤트 핸들러를 추가한다.
const handleDragOver: React.DragEventHandler<HTMLUListElement> = (ev) => {
ev.preventDefault();
// 초기화
ghostRef.current?.remove();
ghostRef.current = undefined;
const container = ev.currentTarget as Element;
const getAfterElement = (mouseY: number) => {
const dragItems = [...container.querySelectorAll('.dragitem:not(.dragging')];
const result = dragItems.reduce<{
offset: number;
element: Element | undefined;
}>(
(closest, child) => {
const rect = child.getBoundingClientRect();
const offset = mouseY - rect.top - rect.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY, element: undefined }
);
return result.element;
};
const afterElement = getAfterElement(ev.clientY);
const dragging = container.querySelector('.dragging');
if (dragging && afterElement) {
container.insertBefore(dragging, afterElement);
}
};
return (
<ul
ref={containerRef}
className={'droparea ' + direction}
onDragOver={handleDragOver}
>
{children}
</ul>
);
이전엔 mousemove, mouseup, mousedown을 직접 구현했었는데 이 블로그보니까 깔끔하게 정리해주셔서 여기 로직을 따라했다.
로직을 간단하게 살펴보면 아래 코드는 드래그한 위치의 다음에 놓일 element를 찾는다.
const getAfterElement = (mouseY: number) => {
const dragItems = [...container.querySelectorAll('.dragitem:not(.dragging')];
const result = dragItems.reduce<{
offset: number;
element: Element | undefined;
}>(
(closest, child) => {
const rect = child.getBoundingClientRect();
// 마우스 위치와 element 중앙(rect.top + rect.height/2) 사이의 offset을 구한다.
const offset = mouseY - rect.top - rect.height / 2;
if (offset < 0 && offset > closest.offset) {
// mouse보다 아래 있으면서 offset이 최소가 되는(=가장 가까운) element를 리턴한다.
return { offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY, element: undefined }
);
return result.element;
};
그러나 이 경우엔 element를 가장 하단에 두고 싶을 땐 요소가 움직이지 않았다.
원인은 아래 코드에서 마우스를 가장 하단의 위치로 옮기면 afterElement가 가장 하단에 있는 요소가 되는데 그 앞에 우리가 드래깅한 대상의 요소를 insert를 하기 때문이다.
container.insertBefore(dragging, afterElement);
그래서 로직을 살짝 바꾸기로 한다.
const closest = document
.elementFromPoint(ev.clientX, ev.clientY)
?.closest<HTMLElement>(".dragitem:not(.dragging)");
// 현재 마우스 위치에서 가장 가까운 element를 찾는다.
const dragging = container.querySelector(".dragging");
if (!dragging || !closest) return;
const items = [...container.querySelectorAll(".dragitem")];
const fromIdx = items.findIndex((el) => el.id === dragging.id);
const toIdx = items.findIndex((el) => el.id === closest.id);
if (fromIdx < toIdx) {
// 아래로 이동한다면 dragging 앞에 타겟을 둔다.
containerRef.current?.insertBefore(closest, dragging);
} else {
// 위로 이동한다면 타겟 앞에 dragging을 둔다.
containerRef.current?.insertBefore(dragging, closest);
}
이렇게 하면 요소 간 순서 변경이 가능하다.
