Drag to Reorder

Reorder list items using native HTML5 drag and drop.

Live Demo

Source

HTML + JavaScript
<div data-component="drag-reorder">
    <div id="drag-list" data-list="items" data-key="id">
        <template>
            <div class="drag-item" draggable="true">
                <span>&#9776;</span>
                <span data-bind="label"></span>
            </div>
        </template>
    </div>
</div>

<script>
wildflower.component('drag-reorder', {
    state: {
        items: [
            { id: 1, label: 'First item' },
            { id: 2, label: 'Second item' },
            { id: 3, label: 'Third item' },
            { id: 4, label: 'Fourth item' }
        ]
    },
    init() {
        const list = this.find('#drag-list');
        let dragIdx = -1;

        const clearIndicators = () => {
            list.querySelectorAll('.drag-over-above, .drag-over-below')
                .forEach(el => el.classList.remove('drag-over-above', 'drag-over-below'));
        };

        list.addEventListener('dragstart', (e) => {
            const el = e.target.closest('.drag-item');
            if (!el) return;
            dragIdx = [...list.querySelectorAll('.drag-item')].indexOf(el);
            e.dataTransfer.effectAllowed = 'move';
            requestAnimationFrame(() => el.classList.add('dragging'));
        });

        list.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            const target = e.target.closest('.drag-item');
            if (!target || dragIdx < 0) return;
            clearIndicators();
            const rect = target.getBoundingClientRect();
            const mid = rect.top + rect.height / 2;
            target.classList.add(e.clientY < mid ? 'drag-over-above' : 'drag-over-below');
        });

        list.addEventListener('drop', (e) => {
            e.preventDefault();
            clearIndicators();
            const target = e.target.closest('.drag-item');
            if (!target || dragIdx < 0) return;
            const items = [...list.querySelectorAll('.drag-item')];
            let dropIdx = items.indexOf(target);
            const below = e.clientY >= target.getBoundingClientRect().top
                        + target.getBoundingClientRect().height / 2;
            if (below && dragIdx > dropIdx) dropIdx++;
            else if (!below && dragIdx < dropIdx) dropIdx--;
            if (dragIdx === dropIdx) return;
            const arr = [...this.items];
            const [item] = arr.splice(dragIdx, 1);
            arr.splice(dropIdx, 0, item);
            this.items = arr;
            dragIdx = -1;
        });

        list.addEventListener('dragend', (e) => {
            clearIndicators();
            const el = e.target.closest('.drag-item');
            if (el) el.classList.remove('dragging');
            dragIdx = -1;
        });
    }
});
</script>

Key Points

  • HTML5 drag/drop requires e.preventDefault() on dragover synchronously. Native listeners in init() guarantee this
  • Event delegation on the list container handles all items with a single set of listeners
  • Copy the array, splice, then reassign for clean reactivity with this.items = arr
  • data-key="id" ensures efficient reconciliation when items move positions