JSFiddle - React, Tailwind, and code Playground
by rgthree
HTML
<ul id="list"></ul>
<template id="list-item">
<li draggable="true">
<div class="drop-target drop-above"></div>
<div class="drop-target drop-under"></div>
<span></span>
<button data-action="up">Up</button>
<button data-action="down">Down</button>
</li>
</template>
CSS
ul {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #aaa;
padding-bottom: 20px;
}
ul > li {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
position: relative;
border: 1px solid #ccc;
border-width: 1px 0;
padding: 8px;
}
ul > li > span {
display: block;
padding: 4px;
margin-right: auto;
}
[data-action="drag"] {
width: 12px;
height: 24px;
background: gray;
border: 0;
appearance: none;
margin: 4px;
cursor: drag;
}
.drop-target {
position: absolute;
left: 0;
width: 100%;
pointer-events: none;
z-index: 2;
}
ul.is-dragging .drop-target {
pointer-events: all;
}
.drop-above {
top: -1px;
bottom: 50%;
}
.drop-above.dragging-over {
box-shadow: inset 0px 1px 0px blue, 0px -1px 0px blue;
}
.drop-under {
bottom: -1px;
top: 50%;
}
.drop-under.dragging-over {
box-shadow: inset 0px -1px 0px blue, 0px 1px 0px blue;
}
TypeScript
const list = document.querySelector('ul');
const tpl = document.querySelector('template#list-item');
const context = {
dragging: null,
draggingOver: null,
};
function onDrag(e: Event, item: HTMLElement) {
if (!context.dragging) {
context.dragging = item;
list.classList.add('is-dragging');
}
}
function onDragOver(e: Event, target: HTMLElement) {
e.preventDefault();
// If dragging over the list itself, then drop at the bottom.
if (target === list) {
target = list.querySelector('li:last-child .drop-under');
}
if (context.draggingOver != target) {
context.draggingOver = target;
const existingOverTarget = list.querySelector('.dragging-over');
if (existingOverTarget && existingOverTarget != target) {
existingOverTarget.classList.remove('dragging-over');
}
context.draggingOver && context.draggingOver.classList.add('dragging-over');
}
e.stopPropagation();
}
function onDragEnd(e: Event, item: HTMLElement) {
let draggingLayer = null;
if (context.draggingOver && context.dragging) {
const listChildren = Array.from(list.children);
const draggingIndex = listChildren.indexOf(context.dragging);
const droppingOnLi = context.draggingOver.parentElement;
if (droppingOnLi && droppingOnLi !== context.dragging) {
const dropOntoIndex = listChildren.indexOf(droppingOnLi);
const moveAbove = context.draggingOver.classList.contains('drop-above');
moveItemBefore(context.dragging, moveAbove ? droppingOnLi : droppingOnLi.nextElementSibling);
}
}
list.classList.remove('is-dragging');
context.draggingOver && context.draggingOver.classList.remove('dragging-over');
context.dragging = null;
context.draggingOver = null;
}
function moveItemBefore(item, referenceItem) {
if (referenceItem) {
list.insertBefore(item, referenceItem);
} else {
list.appendChild(item);
}
}
// Build the listui
do {
const item = tpl.content.cloneNode(true);
...