Coding, Insights, and Digital Discoveries 👩🏻‍💻

From Stay-at-Home Mom to a Developer After 50

Published on

Implementing Drag & Drop in a To-Do List with JavaScript

drag and drop

I am still working on improving my To-Do List app using vanilla JavaScript. So far, I have refreshed some core JavaScript concepts, integrated Tailwind CSS for styling, and made tasks editable. As I continue to enhance the project, I realized that adding a drag-and-drop feature would make the app more practical and user-friendly.

What is My Goal Using the Drag & Drop Feature?

Better User Experience – Users can reorder tasks easily.
Persistent Order – The new arrangement is saved in localStorage, so the order remains even after refreshing the page.
Visual Feedback – Users see clear indicators showing where the task will be dropped.

How to Implement This?

To achieve this, I use the Drag and Drop API and attach event listeners to the <ul> element rather than individual <li> items. This ensures efficient event handling while keeping the code clean and scalable.

Understanding the Drag & Drop Events - The Four Key Events

  1. dragstart → Triggers when an element starts being dragged.
  • Stores a reference to the dragged item.
  • Reduces opacity for a visual effect.
  1. dragover → Fires when a dragged item moves over a valid drop target.
  • Prevents default behavior to allow dropping.
  • Determines the best drop position dynamically.
  • Adds visual indicators for where the item will be inserted.
  1. drop → Fires when the dragged item is dropped.
  • Inserts the dragged item at the correct position.
  • Saves the new order in localStorage.
  1. dragend → Fires when the drag action is complete.
  • Removes the opacity effect.
  • Clears visual indicators.

JavaScript Implementation: Drag & Drop

Global Variable to Track Dragged Item
let draggedItem = null; // Stores the currently dragged item

This will temporarily store the <li> element that is being dragged.

Handling dragstart (When Dragging Begins)
function handleDragStart(e) {
    draggedItem = e.target; // Store reference to dragged <li>
    e.target.style.opacity = '0.5'; // Make it visually distinct
}
  • Stores the dragged <li> element in draggedItem.
  • Reduces opacity to give visual feedback.
Helper Function: Identify Drop Position
function getDropDetails(e) {
    const targetItem = e.target.closest('li'); // Identify nearest <li> under cursor
    if (!targetItem || targetItem === draggedItem) return null; // Ignore if no valid target

    const targetRect = targetItem.getBoundingClientRect();
    const midPoint = targetRect.top + targetRect.height / 2;
    const insertAfter = e.clientY > midPoint; // Check if dragging below midpoint

    return { targetItem, insertAfter };
}
  • Finds the nearest <li> under the cursor.
  • Uses getBoundingClientRect() to get the midpoint of the target <li>.
  • Determines if the item should be inserted above or below the target.
Helper Function: Remove Visual Indicators
function removeDropTargetStyles() {
    document.querySelectorAll('li').forEach(item => {
        item.style.borderTop = ''; // Reset borders
        item.style.borderBottom = '';
    });
}
  • Clears any previously added borders that indicate drop positions.
  • Prevents multiple visual indicators from appearing simultaneously.
Handling dragover (When Dragging Over Another Item)
function handleDragOver(e) {
    e.preventDefault(); // Allows dropping (default behavior blocks it)
    removeDropTargetStyles(); // Clean up existing drop indicators

    const details = getDropDetails(e); // Get target position
    if (!details) return;

    // Apply drop indicators based on position
    details.targetItem.style.borderTop = details.insertAfter ? '' : '2px solid #666';
    details.targetItem.style.borderBottom = details.insertAfter ? '2px solid #666' : '';
}
  • e.preventDefault() allows dropping (by default, dropping is disabled).
  • Calls removeDropTargetStyles() to avoid multiple borders appearing.
  • Uses getDropDetails(e) to determine where to insert.
  • Adds a border above/below the target to show the drop position.
Handling drop (When Dragging Stops and Item is Released)
function handleDrop(e) {
    e.preventDefault();

    const details = getDropDetails(e);
    if (!details) return;

    // Move the dragged item before/after the target
    if (details.insertAfter) {
        details.targetItem.parentNode.insertBefore(draggedItem, details.targetItem.nextSibling);
    } else {
        details.targetItem.parentNode.insertBefore(draggedItem, details.targetItem);
    }

    updateTaskOrder(); // Save new order to localStorage
}
  • Calls getDropDetails(e) to find where to insert the dragged item.
  • Inserts draggedItem before or after targetItem.
  • Calls updateTaskOrder() to save the new order in localStorage.
Handling dragend (After Dragging is Finished)
function handleDragEnd(e) {
    e.target.style.opacity = ''; // Reset opacity
    removeDropTargetStyles(); // Remove any remaining visual indicators
}
  • Restores the dragged element's opacity.
  • Clears any remaining drop indicators.

With this implementation, my To-Do List now supports smooth drag-and-drop reordering, and task order is maintained even after a page refresh.

← See All Posts