Bảng Kanban với drag and drop
Hướng dẫn này xây dựng một bảng Kanban sử dụng API HTML Drag and Drop. Bảng Kanban cho phép người dùng kéo thẻ nhiệm vụ giữa các cột thể hiện trạng thái khác nhau (ví dụ: "Cần làm", "Đang làm", "Hoàn thành").
Tổng quan
Bảng Kanban là ứng dụng đặc trưng của việc kéo và thả các phần tử trong một trang web. Trong ví dụ này, người dùng có thể kéo thẻ nhiệm vụ từ cột này sang cột khác.
Thiết kế cấu trúc HTML
Bảng Kanban bao gồm nhiều cột, mỗi cột chứa các thẻ nhiệm vụ:
html
<div class="kanban-board">
<div class="column" id="todo">
<h2>To Do</h2>
<div class="card" draggable="true" id="card1">Task 1</div>
<div class="card" draggable="true" id="card2">Task 2</div>
</div>
<div class="column" id="in-progress">
<h2>In Progress</h2>
<div class="card" draggable="true" id="card3">Task 3</div>
</div>
<div class="column" id="done">
<h2>Done</h2>
</div>
</div>
CSS cơ bản
css
.kanban-board {
display: flex;
gap: 1rem;
}
.column {
flex: 1;
background: #f4f5f7;
padding: 1rem;
border-radius: 4px;
min-height: 300px;
}
.column.drag-over {
background: #e2e8f0;
border: 2px dashed #667eea;
}
.card {
background: white;
padding: 0.5rem 1rem;
margin-bottom: 0.5rem;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: grab;
}
.card.dragging {
opacity: 0.5;
}
JavaScript xử lý kéo và thả
js
// Theo dõi thẻ đang được kéo
let draggedCard = null;
// Thiết lập sự kiện cho tất cả thẻ
document.querySelectorAll(".card").forEach((card) => {
card.addEventListener("dragstart", (event) => {
draggedCard = event.target;
event.target.classList.add("dragging");
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", event.target.id);
});
card.addEventListener("dragend", (event) => {
event.target.classList.remove("dragging");
draggedCard = null;
});
});
// Thiết lập sự kiện cho tất cả cột
document.querySelectorAll(".column").forEach((column) => {
column.addEventListener("dragover", (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
column.classList.add("drag-over");
});
column.addEventListener("dragleave", (event) => {
// Chỉ xóa khi rời khỏi cột, không phải khi vào phần tử con
if (!column.contains(event.relatedTarget)) {
column.classList.remove("drag-over");
}
});
column.addEventListener("drop", (event) => {
event.preventDefault();
column.classList.remove("drag-over");
if (draggedCard && draggedCard.parentElement !== column) {
column.appendChild(draggedCard);
}
});
});
Chèn vào vị trí đúng
Phiên bản trên thêm thẻ vào cuối cột. Để chèn vào vị trí cụ thể, chúng ta có thể tính toán vị trí dựa trên sự kiện drag:
js
function getDragAfterElement(column, y) {
const draggableElements = [
...column.querySelectorAll(".card:not(.dragging)"),
];
return draggableElements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY },
).element;
}
column.addEventListener("drop", (event) => {
event.preventDefault();
column.classList.remove("drag-over");
const afterElement = getDragAfterElement(column, event.clientY);
if (afterElement == null) {
column.appendChild(draggedCard);
} else {
column.insertBefore(draggedCard, afterElement);
}
});