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);
 ...