JS 实现拖拽元素的功能
这篇笔记比较短,主要过一遍 draggable
的事件。
首先简单看一下 HTML 实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
ul {
list-style: none;
border: 1px solid #333;
padding: 0.5em;
margin: 1em auto;
max-width: 720px;
width: 80%;
}
li {
cursor: grab;
}
.card {
border-radius: 10px;
border: 1px solid #bbb;
padding: 0.5em;
margin: 0.8em;
box-shadow: 1px 1px 0.5em #ccc;
}
.dragging {
cursor: grabbing;
}
.droppable {
background-color: #ffe0ec;
}
</style>
</head>
<body>
<ul>
<li id="p1" class="card" draggable="true">
<h2>Title for p1</h2>
<p>Paragraph body for p1</p>
</li>
<li id="p2" class="card" draggable="true">
<h2>Title for p2</h2>
<p>Paragraph body for p2</p>
</li>
</ul>
<ul>
<li id="p3" class="card" draggable="true">
<h2>Title for p3</h2>
<p>Paragraph body for p3</p>
</li>
<li id="p4" class="card" draggable="true">
<h2>Title for p4</h2>
<p>Paragraph body for p4</p>
</li>
</ul>
<script src="./draggable.js"></script>
</body>
</html>
效果如下:
这里简单的加了一点 CSS(list 和 card 的部分),其他抓取元素后会出现的悬浮效果全都是浏览器的实现,而实现抓取的功能也挺简单的,就是在想要抓取的元素上添加 draggable="true"
这一属性。
所以接下来要做的,就是绑定正确的事件,并且实现 draggable 中对应的事件。
首先修改一下 css:
<style>
li {
cursor: grab;
}
</style>
这样鼠标在进入的时候就是抓取的样式,这样也提示用户当前元素可以被抓取:
随后是在 js 里面添加最初的设定,获取所有的 list item,并且绑定事件。绑定事件的部分会在后面一个个实现,所以现在先加一个 placeholder:
const listItems = document.querySelectorAll('li');
listItems.forEach((li) => {});
dragstart
这是一个单独的事件处理,所有的内容会被绑定到 dragstart
的事件下面:
// callback 可以拉出来单独做一个 function
li.addEventListener('dragstart', (e) => {
// code here
});
dragstart 是开始抓取的这一部分,实现的功能包括:
-
更改鼠标光标现实正在抓取的状态
这一部分依旧通过 js 实现,通过
style
这一属性改变cursor
的状态代码为:
li.style.cursor = 'grabbing';
或者通过 class 名称修改,这需要添加对应的 css:
li.classList.add('dragging');
-
更新传输数据
这里使用的是
DataTransfer
对象,根据 MDN 所说DataTransfer
是在实现 drag 功能中,用来保存被拖拽的数据的一个对象。所以这里主要实现的有两个部分:
- 传输当前被选中的 li 的 id
- 限定 drag 的操作只有
move
e.dataTransfer.setData('text/plain', li.id); e.dataTransfer.effectAllowed = 'move';
drop
drop 需要实现 4 个部分:dragenter
、 dragover
, dragleave
和 drop
。另外,drop 的这个操作也是作用于 ul 之上,而不是 li,这部分的基础代码为:
// drop
const lists = document.querySelectorAll('ul');
lists.forEach((list) => {
// code here
});
dragenter & dragover
这里实现的功能主要是当被拖拽的部分进入到可被 drop 的地方,那么可被 drop 的部分应该会有一个颜色改变的提示。
list.addEventListener('dragenter', (e) => {
// check only accept the correct type of data
if (e.dataTransfer.types[0] === 'text/plain') {
e.preventDefault();
list.classList.add('droppable');
}
});
这里加了一个 check e.dataTransfer.types[0] === 'text/plain'
,主要也是因为在真实的案例中,很可能会有抓取一些 html、DOM 结点的操作,这里想要确认被拖拽的只是字符串部分。
如果想要实现 drop 的功能,dragover 是个必须要调用 preventDefault
去阻止默认功能的实现:
list.addEventListener('dragover', (e) => {
// prevent default to allow drop
if (e.dataTransfer.types[0] === 'text/plain') {
e.preventDefault();
}
});
实现后效果如下:
dragleave
当 item 进入可被 drop 区域时添加的 class,同样也需要在 item 离开该区域时被移除,这一部分就可以在 dragleave 中实现:
list.addEventListener('dragleave', (e) => {
list.classList.remove('droppable');
});
这时候实现完了会有一个小麻烦,那就是当被拖拽的对象进入另一个子结点的时候,它也算离开了当前结点:
这一部分的修改具体还是需要依赖实现去完成,这里主要通过寻找最近的 ul
,并与当前的 ul 进行判断,如果不是同一个的话就会进行删除:
list.addEventListener('dragleave', (e) => {
if (e.relatedTarget.closest('ul') !== list)
list.classList.remove('droppable');
});
drop
drop 的部分就需要获取被拉动的 id,判断当前 list 是否包含对应 id:
- 是的话停止继续执行
- 否的话先删除当前元素,并且在对应的 list 中添加被删除的元素
实现如下:
list.addEventListener('drop', (e) => {
const id = e.dataTransfer.getData('text/plain');
const listArr = Array.from(list.children);
if (listArr.find((li) => li.id === id)) return;
const listItem = document.querySelector(`#${id}`);
listItem.remove();
list.appendChild(listItem);
list.classList.remove('droppable');
});
效果如下:
完整 JS 代码
// drag
const listItems = document.querySelectorAll('li');
const connectDrag = (e, li) => {
li.classList.add('dragging');
e.dataTransfer.setData('text/plain', li.id);
e.dataTransfer.effectAllowed = 'move';
};
listItems.forEach((li) =>
li.addEventListener('dragstart', (e) => connectDrag(e, li))
);
// drop
const lists = document.querySelectorAll('ul');
lists.forEach((list) => {
list.addEventListener('dragenter', (e) => {
// check only accept the correct type of data
if (e.dataTransfer.types[0] === 'text/plain') {
e.preventDefault();
list.classList.add('droppable');
}
});
list.addEventListener('dragleave', (e) => {
if (e.relatedTarget.closest('ul') !== list)
list.classList.remove('droppable');
});
list.addEventListener('dragover', (e) => {
// prevent default to allow drop
if (e.dataTransfer.types[0] === 'text/plain') {
e.preventDefault();
}
});
list.addEventListener('drop', (e) => {
const id = e.dataTransfer.getData('text/plain');
const listArr = Array.from(list.children);
if (listArr.find((li) => li.id === id)) return;
const listItem = document.querySelector(`#${id}`);
listItem.remove();
list.appendChild(listItem);
list.classList.remove('droppable');
});
});
reference
- HTMLElement: dragover event
- DataTransfer: setData() method
- DataTransfer: effectAllowed property