Drag And Drop

Minimum requirements are a set of draggables and a drop zone.

  1. Draggables need a dragstart event handler
  2. Dropzone needs a dragover event handler
  3. Dropzone needs a drop event handler

Draggables

These are the elements that can be dragged. List items, table rows or whatever. Add draggable="true" to them:

<ul class="list-unstyled">
    <li draggable="true">Item 1</li>
    <li draggable="true">Item 2</li>
    <li draggable="true">Item 3</li>
    <li draggable="true">Item 4</li>
</ul>

DragStart

Event that fires when a drag operation starts. Use this to get a reference to the element being dragged, for example:

let elementBeingDragged;
const draggables = document.querySelectorAll('[draggable="true"]');
draggables.forEach(draggable => draggable.addEventListener('dragstart', event => 
    elementBeingDragged = event.currentTarget
));

event.target = element that triggered event
event.currentTarget = element that listens to event.

Drop Zone

An element with both dragover and drop event handlers attached. If the ul above is the dropzone, the event handlers will be attached like this:

const dropZone = document.querySelector('ul.list-unstyled');
dropZone.addEventListener('dragover', event => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'move';
});
dropZone.addEventListener('drop', event => {
    event.preventDefault();
    // process the drop event
});

Processing the drop event

Commonly, you might want to change the order of the items in the draggable collection. So you compare the index of the dropped item to that of the dropZone to determine whether to place the dropped item before or after the dropZone:

const dropTargets = Array.from(dropZone.children);
if (dropTargets.indexOf(event.target.parentNode) > dropTargets.indexOf(elementBeingDragged)) {
    event.target.parentNode.after(elementBeingDragged);
} else {
    event.target.parentNode.before(elementBeingDragged);
}

Working code (HTMLTableElement is drop zone)

const initDraggables = () => {
    const draggables = document.querySelectorAll('tbody [draggable="true"]') as NodeListOf<HTMLElement>;
    const dropZone = document.querySelector('tbody') as HTMLElement;
    let row;
    if (draggables.length) {
        draggables.forEach(draggable => {
            draggable.classList.add('draggable');
            draggable.addEventListener('dragstart', event => {
                row = event.currentTarget;
                row.classList.remove('dropping');
                row.classList.remove('dropped');
                row.classList.add('dragging')
            })
        });
    }
    if (dropZone) {
        dropZone.addEventListener('dragover', event => {
            event.preventDefault();
            event.dataTransfer.dropEffect = 'move';
        });
        dropZone.addEventListener('drop', (event: DragEvent) => {
            row.classList.remove('dragging')
            row.classList.add('dropping');
            event.preventDefault();
            const dropTarget = (event.target as HTMLElement).closest('[draggable="true"]');
            const children = [...dropZone.children];
            if (children.indexOf(dropTarget) > children.indexOf(row)) {
                dropTarget.after(row);
            } else {
                dropTarget.before(row);
            }
            window.setTimeout(_ => {
                row.classList.add('dropped');
            }, 100);

            const formData = new FormData();
            draggables.forEach(milestone => {
                const id = milestone.dataset.id;
                formData.append(`milestones.index`, id);
                formData.append(`milestones[${id}].MilestoneId`, id);
                formData.append(`milestones[${id}].DisplayOrder`, [...dropZone.children].indexOf(milestone).toString());
            });
            fetch('/milestones?handler=updatedisplayorder', {
                method: 'post',
                headers: formHeaderWithToken,
                body: new URLSearchParams(formData as any)
            }).then(response => {
                if (response.ok) {
                    showToast('Items reordered successfully');
                } else {
                    showToast(`There was a problem. The new order may not have been saved. Error code is ${response.status}`, true);
                }
            });
        });
    }
}
Last updated: 2/6/2024 10:41:14 AM

Latest Updates

© 0 - 2025 - Mike Brind.
All rights reserved.
Contact me at Mike dot Brind at Outlook.com