[React] Drag&Drop 구현하기(2)

Jimin Lee·2024년 11월 28일
0

DnD

목록 보기
2/2
post-thumbnail

앞편에 이어서 본격적으로 이동 로직을 구현해보자.

5. 이동 로직 구현하기

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);
    }

이렇게 하면 요소 간 순서 변경이 가능하다.

0개의 댓글