Drag and Drop이 가능한 트리를 구현해달라는 요구사항을 받고, 관련 라이브러리를 찾다가 문제를 발견했다.
그렇지만 위의 사항들을 만족하는 vue3 라이브러리가 없어서 직접 구현하기로 결정..!
const data = [
{
seq: 1,
title: "사과 🍎",
},
{
seq: 2,
title: "배 🍐",
},
{
seq: 3,
title: "포도 🍇",
},
];
<template>
<div class="item">
<ul v-bind:key="item.seq" v-for="item in data">
<li class="draggable" draggable="true">
{{ item.title }}
</li>
</ul>
</div>
</template>
실행 화면은 다음과 같다
Drag and Drop 각 이벤트 단계에 맞는 액션을 연결해주면 되기에 우선 각 단계에 맞는 함수들을 정의했다.
1. 액션별 기능 정의
// 1. 마운트되면 각각 아이템들에 드래그앤드롭 감지 함수들을 연결
onMounted(() => {
var listItems = document.querySelectorAll(".draggable");
listItems.forEach(function (item) {
addEventsDragAndDrop(item);
});
});
function addEventsDragAndDrop(el) {
el.addEventListener("dragstart", dragStart);
el.addEventListener("dragover", dragOver);
el.addEventListener("dragleave", dragLeave);
el.addEventListener("drop", dragDrop);
el.addEventListener("dragend", dragEnd);
}
function dragStart(e) {
// css 추가
// 드래그 하는 대상의 데이터 값 감지 필요
}
function dragLeave(e) {
// css 해제
}
function dragOver(e) {
// css 추가
}
function dragDrop(e) {
// 선택한 아이템과 놓은 자리의 아이템이 바뀌어야함
}
function dragEnd(e) {
// css 해제
}
2. CSS 설정
유저입장에서 편할 수 있도록 선택한 아이템이 드래그될 때는 투명도를 낮춰서 선택되어있음을 인지할 수 있게 하였고, 드래그로 아이템을 가져다 놓으려는 곳의 크기를 살짝 키워줬다. 단, 위아래로만 키워서 화면 변화가 너무 크지 않도록 하였다.
// 1. 마운트되면 각각 아이템들에 드래그앤드롭 감지 함수들을 연결
onMounted(() => {
var listItems = document.querySelectorAll(".draggable");
listItems.forEach(function (item) {
addEventsDragAndDrop(item);
});
});
function addEventsDragAndDrop(el) {
el.addEventListener("dragstart", dragStart);
el.addEventListener("dragover", dragOver);
el.addEventListener("dragleave", dragLeave);
el.addEventListener("drop", dragDrop);
el.addEventListener("dragend", dragEnd);
}
function dragStart(e) {
// css 추가
// 드래그 하는 대상의 데이터 값 감지 필요
this.style.opacity = "0.4";
}
function dragLeave(e) {
// css 해제
this.classList.remove("over");
}
function dragOver(e) {
// css 추가
this.classList.add("over");
}
function dragDrop(e) {
// 선택한 아이템과 놓은 자리의 아이템이 바뀌어야함
this.classList.remove("over");
}
function dragEnd(e) {
// css 해제
this.style.opacity = "1";
this.classList.remove("over");
}
<style>
.over {
transform: scale(1, 1.1);
}
</style>
우선 드래그했을 때 대상을 저장하는 변수를 하나 둔다
let dragSrcEl = null;
그 다음 드래그 시작시에는 그 값을 지정해주고, 드래그 드롭시에 값을 바꿔준다.
어떤 태그가 위에 머무를 때 작동하는 이벤트인데, Over 이벤트의 default 속성이 다음과 같다.
Reset the current drag operation to "none".
즉, drag 기능이 꺼지므로 preventDefault()를 이용해서 기본 기능을 비활성화시켜줘야한다.
<script lang="ts" setup>
import { onMounted } from "vue";
onMounted(() => {
var listItems = document.querySelectorAll(".draggable");
listItems.forEach(function (item) {
addEventsDragAndDrop(item);
});
});
let dragSrcEl = null;
function addEventsDragAndDrop(el) {
el.addEventListener("dragstart", dragStart);
el.addEventListener("dragover", dragOver);
el.addEventListener("dragleave", dragLeave);
el.addEventListener("drop", dragDrop);
el.addEventListener("dragend", dragEnd);
}
function dragStart(e) {
this.style.opacity = "0.4";
dragSrcEl = this;
e.dataTransfer.setData("text/html", this.innerHTML);
}
function dragLeave(e) {
this.classList.remove("over");
}
function dragOver(e) {
e.preventDefault();
this.classList.add("over");
}
function dragDrop(e) {
if (dragSrcEl != this) {
dragSrcEl.innerHTML = this.innerHTML;
this.innerHTML = e.dataTransfer.getData("text/html");
}
this.classList.remove("over");
}
function dragEnd(e) {
this.style.opacity = "1";
this.classList.remove("over");
var changedItem = document.querySelector(".item");
}
</script>
그렇다면 현재 요구 사항은 두 개나 해결했다.
마지막 데이터가 바뀌었다면 백엔드로 api를 요청하는 요구사항이 남았다.
저장 버튼을 두고 싶었지만 요구사항이라 어쩔 수 없었다 😅
Vue에서의 변화를 감지하는 보통의 방법은 watch 다. 그렇지만 위의 코드에는 굳이 watch가 필요없다.
왜냐하면 둘의 태그가 같은지를 감지하고 같다면 dragOver를 실행하기에 이 시점에서 데이터 구조를 바꾸고 이를 찍어보면 이게 바뀐 값이다.
참고
HTML 드래그 앤 드롭 API
HTML5 드래그 앤 드롭 API
드래그 앤 드롭(Drag and Drop) 기능 이해 & 구현하기
출처: https://inpa.tistory.com/entry/드래그-앤-드롭-Drag-Drop-기능 [Inpa Dev 👨💻:티스토리]