[JavaScript] VanilaJS로 드래그 앤 드롭 구현하기

장유진·2023년 4월 27일
0

Implementation

목록 보기
3/4


드래그 앤 드롭은 마우스나 터치 스크린을 사용하여 요소의 위치를 이동시키는 사용자 인터페이스 기술입니다. 이 기술은 사용자가 웹 페이지와 상호 작용할 수 있는 간단하고 직관적인 방법을 제공하기 때문에 웹 개발에서 널리 사용됩니다.

1. 목표

외부 라이브러리를 사용하지 않고 드래그 앤 드롭을 구현합니다.

이 글에서는 바닐라 자바스크립트를 사용하여 드래그 앤 드롭을 구현하는 방법을 설명합니다.

다음은 드래그 앤 드롭에 구현하고자 하는 목표입니다.

  • 지정된 위치에 요소 이동
  • 드래그 스타일 추가

2. 드래그 앤 드롭의 이해

2-1. 드래그 앤 드롭 이벤트

JavaScript에서 드래그 앤 드롭 기능을 활성화하려면 드래그 가능하게 만들려는 요소에서 "draggable" 속성을 "true"로 설정해야 합니다. 그런 다음 위의 이벤트 유형을 사용하여 다양한 드래그 앤 드롭 이벤트를 처리할 수 있습니다.

1. dragstart

dragstart 이벤트는 사용자가 끌 수 있는 요소에서 마우스 버튼을 클릭한 상태로 요소를 끌기 시작할 때 발생합니다. 이 이벤트를 사용하면 이벤트 개체에 "dataTransfer" 속성을 설정하여 끌 데이터를 지정할 수 있습니다. 이 속성을 사용하여 요소 간에 또는 다른 애플리케이션으로 데이터를 전송할 수 있습니다. 예를 들어 "dataTransfer" 속성을 사용자가 드래그하는 URL 또는 이미지 파일로 설정할 수 있습니다.

2. drag

drag 이벤트는 사용자가 요소를 끌 때 계속 발생합니다. 이 이벤트를 사용하면 드래그한 요소의 위치나 스타일을 변경하여 사용자가 드래그할 때 피드백을 제공할 수 있습니다.

3. dragend

dragend 이벤트는 사용자가 요소 드래그를 완료하면(예: 마우스 버튼에서 손을 떼는 경우) 트리거됩니다. 이 이벤트를 사용하여 드래그 중 임시로 할당한 스타일을 초기 화할 수 있습니다.

4. dragenter

드래그 중인 요소가 드롭 영역 안에 진입할 경우 dragenter가 트리거됩니다. 이 이벤트를 사용하여 드래그 된 요소를 해당 위치에 드롭할 수 있다는 피드백을 사용자에게 전달할 수 있습니다.

5. dragover

드래그 중인 요소가 드롭 영역 위로 이동할 때 계속해서 트리거됩니다. 드롭이 가능함을 나타내는 시각적 피드백을 사용자에게 제공할 수 있기 때문에 드래그 앤 드롭에서 가장 중요한 이벤트입니다.

dragover 이벤트는 클라이언트 윈도우를 기준으로 한 마우스의 위치(event.clientXevent.clientY)
와 함께, 이벤트 요소의 왼쪽 상단을 기준으로 마우스의 위치를 반환합니다(event.offsetXevent.offsetY)

dragover 이벤트는 예상하지 못한 동작을 방지하기 위해 event.preventDefault() 와 함께 사용하는 것을 권장합니다. 드롭 영역이 링크인 경우, 링크를 따라가는 문제를 방지할 수 있습니다.

6. dragleave

드래그 중인 요소가 드롭 영역 밖으로 벗어날 때 트리거됩니다. 이 이벤트를 사용하면 드래그 요소가 드롭 영역에 진입할 때 추가했던 피드백 스타일을 제거할 수 있습니다.

7. drop

drop 이벤트는 드래그 된 요소를 드롭 영역 위에 놓을 때 트리거됩니다.

2-2. dataTransfer 객체

'dataTransfer'는 드래그 앤 드롭에서 드래그 요소와 드롭 요소 간 데이터를 전송에 사용되는 객체입니다. dragstartdrag 및 dragend와 같은 드래그 이벤트의 속성으로 사용할 수 있습니다.

3. 구현 미리보기

4. 구현하기

HTML MarkUp

<ul class="sortable-list">
    <li class="item" draggable="true">
      <div class="details">
        <span>Category 1</span>
      </div>
      <i class="uil uil-draggabledots"></i>
    </li>
    <li class="item" draggable="true">
      <div class="details">
        <span>Category 2</span>
      </div>
      <i class="uil uil-draggabledots"></i>
    </li>
    <li class="item" draggable="true">
      <div class="details">
        <span>Category 3</span>
      </div>
      <i class="uil uil-draggabledots"></i>
    </li>
  </ul>

CSS Styling

.sortable-list {
  width: 425px;
  padding: 25px;
  background: #fff;
  border-radius: 7px;
  padding: 30px 25px 20px;
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
}
.sortable-list .item {
  list-style: none;
  display: flex;
  cursor: move;
  background: #fff;
  align-items: center;
  border-radius: 5px;
  padding: 10px 13px;
  margin-bottom: 11px;
  border: 1px solid #ccc;
  justify-content: space-between;
}
.item .details {
  display: flex;
  align-items: center;
}

.item .details span {
  font-size: 1.13rem;
}
.item i {
  color: #474747;
  font-size: 1.13rem;
}
.item.dragging {
  opacity: 0.6;
}
.item.dragging :where(.details, i) {
  opacity: 0;
}

JavaScript

const sortableList = document.querySelector(".sortable-list");
const items = sortableList.querySelectorAll(".item");

	// 모든 item(ul>li)에 이벤트 등록
  items.forEach(item => {
	// 드래그 이미지(커서를 따라가는 고스트 이미지)를 브라우저가 제대로 생성하는데 약간의 시간이 걸리기 때문에 setTimeout()를 사용합니다.
    item.addEventListener("dragstart", () => {
      setTimeout(() => item.classList.add("dragging"), 0);
    });
    item.addEventListener("dragend", () => item.classList.remove("dragging"));
  });

  const initSortableList = (e) => {
    e.preventDefault();
    const draggingItem = document.querySelector(".dragging");

	// dragover는 드래그 중 계속해서 트리거 되기 때문에, initSortableList가 트리거 될 때마다 siblings를 새로 계산합니다.
    // 드래그 앤 드롭 중에 일어난 목록의 정렬 여부에 상관없이 올바른 위치에 삽입할 수 있습니다.
    let siblings = [...sortableList.querySelectorAll(".item:not(.dragging)")];

	// sibling.offsetTop + sibling.offsetHeignt / 2 -> 각 형제요소의 중심점을 기준으로 삽입점을 판단합니다.
    // 각 형제 요소의 위치를 확인해서 마우스 커서가 어떤 형제 요소의 중심점 위에 위치한 경우
    // 드래그 하고 있는 요소를 해당 형제 앞에, 아닐 경우 뒤에 삽입합니다.
    let nextSibling = siblings.find(sibling => {
      return e.clientY <= sibling.offsetTop + sibling.offsetHeight / 2;
    });

    sortableList.insertBefore(draggingItem, nextSibling);
  }

  sortableList.addEventListener("dragover", initSortableList);
  sortableList.addEventListener("dragenter", e => e.preventDefault());

0개의 댓글