Kanban Board in Plain Javascript
Building a Dynamic Kanban Board: Deep Dive into Drag-and-Drop Functionality
We will be building a simple kanban board in plain javascript , that can shuffle other task, so you can place the task at exact positions.
This is asked a lot of times in machine coding rounds for frontend interviews also.
Link : github.com/biohacker0/Kanban-Board
I am not discussing the html and css of the project, that you can see on the github.
We are focusing on the core logic.
Understanding the Setup:We have draggable items (tasks) represented by HTML elements with the class name "task" and dropable areas (swimlanes) represented by HTML elements with the class name "swim-lane". We've attached event listeners to handle drag and drop events for these elements
.
Base HTML Structure that is repeted , so we have swim-lane , which is the drop area driv.
And we have div's called task , there are things that we will drag , so these are our tasks.
<div class="swim-lane">
<div class="task" draggable="true">Task 1</div>
<div class="task" draggable="true">Task 2</div>
<!-- More tasks -->
</div>
<!-- More swim-lanes -->
Breakdown of Key Components:
Retrieving Elements:
draggableItems = document.getElementsByClassName("task");
- Selects all HTML elements with the class "task," which likely represent individual tasks.
dropableDivs = document.getElementsByClassName("swim-lane");
- Selects all HTML elements with the class "swim-lane," which likely represent the different swim lanes or columns.
let draggableItems = document.getElementsByClassName("task"); let dropableDivs = document.getElementsByClassName("swim-lane");
Event Listeners for Tasks:
dragCb(event)
: When a task is dragged (dragstart event):- Adds the class "is-dragging" to the task, indicating that it's currently being dragged.
dragendCB(event)
: When dragging stops (dragend event):- Removes the "is-dragging" class, visually signifying the end of dragging.
function dragendCB(event) {
event.target.classList.remove("is-dragging");
}
function dragCb(event) {
event.target.classList.add("is-dragging");
}
for (let task of draggableItems) {
task.addEventListener("dragstart", dragCb);
task.addEventListener("dragend", dragendCB);
}
Drop Event Listener for Swim Lanes:
dragoverCB(event)
: When a dragged task is hovered over a swim lane (dragover event):Prevents the default browser behavior, which might interfere with custom drag-and-drop logic.
Calls the helper function
insertAboveTask
to find the appropriate insertion point within the swim lane.Retrieves the currently dragged task element using
document.querySelector(".is-dragging")
.If there's no task below the mouse pointer (meaning the task would be appended to the bottom):
- Simply appends the dragged task to the swim lane using
appendChild
.
- Simply appends the dragged task to the swim lane using
Otherwise, inserts the dragged task before the task that's closest below the mouse pointer using
insertBefore
.for (let dropableDiv of dropableDivs) { dropableDiv.addEventListener("dragover", function dragoverCB(event) { event.preventDefault(); const bottomTask = insertAboveTask(dropableDiv, event.clientY); const currentTask = document.querySelector(".is-dragging"); if (!bottomTask) dropableDiv.appendChild(currentTask); else dropableDiv.insertBefore(currentTask, bottomTask); }); }
Finding the Insertion Point (
insertAboveTask
function):Takes the swim lane element and the mouse Y coordinate as input.
Initializes variables to track the closest task and its offset from the mouse pointer.
Loops through all task elements within the swim lane:
Uses
getBoundingClientRect()
to get the task's top position.Calculates the offset between the mouse pointer and the task's top position.
If the offset is negative (i.e., the mouse pointer is above the task) and closer than the current closest offset:
- Updates the closest offset and closest task variables.
Returns the closest task, which will be used to determine the insertion point.
-
function insertAboveTask(dropableDiv, mouseY) { const currentDropableDivTasks = dropableDiv.getElementsByClassName("task"); let closestTask = null; let closestOffset = Number.NEGATIVE_INFINITY; for (task of currentDropableDivTasks) { const { top } = task.getBoundingClientRect(); const offset = mouseY - top; if (offset < 0 && offset > closestOffset) { closestOffset = offset; closestTask = task; } } return closestTask; }
Core Logic to decide who is closest task to use :
Core Logic Breakdown
Event Listeners:
dragstart
anddragend
events are attached to draggable elements (tasks) to track the drag state and add/remove a visual indicator.dragover
event is attached to dropable elements (swimlanes) to handle the drag-and-drop process.
insertAboveTask
Function:Takes a
dropableDiv
andmouseY
as input.Iterates through all
task
elements within thedropableDiv
.Calculates the offset between the mouse's Y coordinate (
mouseY
) and the top of eachtask
.Finds the
task
with the smallest negative offset (closest to the mouse cursor above its current position).If no suitable
task
is found (the mouse is above all tasks), the new task is appended to the end of thedropableDiv
.Otherwise, the new task is inserted before the identified
task
.
Understanding Offset Calculations
The insertAboveTask
function's key logic lies in its offset calculations. Here's a breakdown:
offset = mouseY - top
: This calculates the difference between the mouse's Y coordinate and the top edge of each task.offset < 0 && offset > closestOffset
: This ensures theoffset
is negative (mouse is above the task) and closer to 0 than any previously encountered offset, indicating it's the closest task above the mouse.Finding the Closest Task: The loop iterates through all tasks, comparing offsets and updating
closestTask
if a closer one is found.Insertion Decision: If no suitable
closestTask
is found (offset === Number.NEGATIVE_INFINITY
), the new task is appended to the end. Otherwise, it's inserted before theclosestTask
.
Calculations of different scenarios :
Sample dropableDiv with 3 tasks:
Task 1: top = 100px
Task 2: top = 200px
Task 3: top = 300px
Mouse position (mouseY):
Case 1: mouseY = 150px
Case 2: mouseY = 250px
Case 3: mouseY = 350px
Calculations and Results:
Case 1:
Loop iterates through tasks.
Task 1: offset = 50 (positive, ignored)
Task 2: offset = -50 (negative and closest, closestTask updated)
Task 3: offset = -150 (negative but further, ignored)
insertAboveTask
returnsTask 2
(dragged task inserted below).
Case 2:
- Similar logic, but
closestTask
will beTask 3
as offset is closer to 0.
- Similar logic, but
Case 3:
- No negative offsets found,
closestTask
remains null, and the dragged task is appended to the end.
- No negative offsets found,
With this the core logic of kanban is done , yay