就算拖拖拉拉,也可以把待办事项处理好
昨天虽然已经知道该如何使用 Drag & Drop API 了,不过今天会实际用它来做个「拖拉式待办清单」,用具体的范例来让我们更加理解它的运用技巧。
由於主要是为了示范 Drag & Drop,所以就不额外使用前端框架,并且也不进行资料的处理,完全使用 DOM 的增删操作来完成作品,如果各位想要完善范例的话可以再自行采用更方便的技术。
首先准备好我们 ToDo List 的结构和样式,大致上长成下面这样,样式部分各位可以各自发挥,这边就不秀出完整 CSS 了。
<div class="wrap">
<div class="column">
<div class="title todo">待处理</div>
<div class="input-wrap">
<input type="text" placeholder="+ 新增事项" />
<button>新增</button>
</div>
<ol class="list"></ol>
</div>
<div class="column">
<div class="title handle">进行中</div>
<ol class="list"></ol>
</div>
<div class="column">
<div class="title complete">已完成</div>
<ol class="list"></ol>
</div>
</div>
<div class="delete">删除</div>
首先我们先来透过 <input>
和 <button>
来完成「新增任务」的功能,透过点击按钮或按下 Enter 就会执行 createToDo
函式,用来创造一个 li
元素,并加上属性及文字後丢掉「待处理」的 ol
中。
const input = document.querySelector("input");
const button = document.querySelector("button");
const todoList = input.parentElement.nextElementSibling;
function createToDo(content) {
const newItem = document.createElement("li");
newItem.classList.add("item");
// 记得要加上 draggable,这样任务才可以拖曳
newItem.setAttribute("draggable", true);
newItem.textContent = content;
todoList.appendChild(newItem);
input.value = "";
}
input.addEventListener("keydown", (e) => {
if (!input.value.trim() || e.which !== 13) return;
createToDo(input.value);
});
button.addEventListener("click", () => {
if (!input.value.trim()) return;
createToDo(input.value);
});
接着我们要让任务可以进行「拖曳」,且三个不同的区块都要可以「被放置」任务,也就是 Drag & Drop API 的部分了,分别把 Drag Source 和 Drop Location 监听事件的流程包装成函式,然後在新增任务时把元素加上 drag
相关事件,以及为三个状态区块加上 drop
相关事件。
// 用来暂存被 drag 的元素
let source = null;
function addDragEvt(element) {
element.addEventListener("dragstart", (e) => {
e.target.classList.add("dragging");
source = e.target;
});
element.addEventListener("dragend", (e) => {
e.target.classList.remove("dragging");
source = null;
});
}
function createToDo(content) {
// ...前面省略
// 记得在 createToDo 中加入这一行来为新增的 li 监听事件
addDragEvt(newItem);
todoList.appendChild(newItem);
input.value = "";
}
function addDropEvt(element) {
element.addEventListener("dragover", (e) => {
e.preventDefault();
});
element.addEventListener("drop", (e) => {
e.currentTarget.querySelector("ol").appendChild(source);
});
}
const columns = document.querySelectorAll(".column");
columns.forEach((column) => {
addDropEvt(column);
});
现在各项任务已经可以通过拖曳放置在不同状态的区块了,现在要来处理排序问题了,我们可以先透过 dragover
事件来取得鼠标的位置,得以判断使用者想要把项目放在哪一个位置,并且利用样式的改变让使用者能更清楚知道他放开滑鼠後,任务会被加在哪里:
.item {
position: relative;
}
.item::before,
.item::after {
content: "";
position: absolute;
display: block;
width: 100%;
height: 4px;
background: lightblue;
opacity: 0;
}
.item.before::before {
top: -2px;
left: 0;
opacity: 1;
}
.item.after::after {
bottom: -2px;
left: 0;
opacity: 1;
}
// 用来暂存被 dragover 的元素
let overItem = null;
// 重置被 dragover 的元素
function clearOverItem() {
if (!overItem) return;
overItem.classList.remove("before");
overItem.classList.remove("after");
overItem = null;
}
function addDropEvt(element) {
element.addEventListener("dragover", (e) => {
clearOverItem();
// 如果 dragover 的元素也是任务项目且不是目前被 drag 的 source 时执行
if (e.target.getAttribute("draggable") && e.target !== source) {
overItem = e.target;
if (e.offsetY > overItem.offsetHeight / 2) {
// 如果鼠标在元素的下半部显示下方的蓝条
overItem.classList.add("after");
} else {
// 反之,显示上方的蓝条
overItem.classList.add("before");
}
}
e.preventDefault();
});
//...以下省略
}
接着我们只要在修改一下 drop
事件,在当中判断目前被 dragover
元素的状态就可以放到对应的位置了:
function addDropEvt(element) {
//...以上省略
element.addEventListener("drop", (e) => {
const list = e.currentTarget.querySelector("ol");
if (overItem) {
if (overItem.classList.contains("before")) {
// 如果 overItem 有 before class 就将 source 移动到它的前面
list.insertBefore(source, overItem);
} else {
// 反之,有 after class 就将 source 移动到它的後面
list.insertBefore(source, overItem.nextElementSibling);
}
} else {
// 如果没有 overItem 也没有更换状态就不动作
if (e.currentTarget.contains(source)) return;
// 反之,加到最後面
else list.appendChild(source);
}
clearOverItem();
});
}
最後在把删除的功能给补上,这样一切就大功告成了。
const del = document.querySelector(".delete");
del.addEventListener("dragover", (e) => {
e.preventDefault();
});
del.addEventListener("drop", (e) => {
source.remove();
clearOverItem();
});
整个范例做完後,希望各位对於 Drag & Drop API 能有更深更具体的认识,如果你在动手做之前想先试玩看看的话,我把原始码放在 CodePen 罗,如果文章中的说明看不是很懂的话,也可以在 CodePen 看看,有任何问题或建议也好欢迎各位提出~
<<: 【DAY 27】Microsoft 365 X Dynamic 365该怎麽选才好呢? (上)
今日远端帮客户处理LINE连线不上问题, 很奇妙,上网都正常,但就是一直显示网路错误, 登入後有跳验...
https://youtu.be/vpwC347cXog 陷入低潮 了解低潮 专注在可控的短期 充...
双开 WebView 并开启 Google Translate 网页 先来看看今天想要完成的功能的样...
继上一篇的DAY8: process.nextTick(),今天要介绍新方法并相互比较。 setIm...
个人正在写一个场地租借系统, 提前开放2周给人预约, 租借的过期纪录要保留起来作系统或规则改善研究,...