JSFiddle - React, Tailwind, and code Playground

by mnk

HTML

<div class="container">
  <div class="widget round">1</div>
  <div class="widget widget-2x2">2</div>
  <div class="widget">3</div>
  <div class="widget widget-1x2">4</div>

  <div class="widget widget-3x1">5</div>
  <div class="widget widget-1x2">6</div>
  <div class="widget round">7</div>
  <div class="widget widget-3x2">8</div>
  <div class="widget">9</div>
  <div class="widget widget-1x2">10</div>
</div>

CSS

body {
  margin: 0;
  padding: 0;
}

.container {
  background-color: #aaa;
  box-sizing: border-box;
  padding: 20px;
  width: 100vw;
  height: 100vh;
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(auto-fit, 120px);
  grid-auto-rows: 120px;
  grid-auto-flow: dense;

}

.widget {
  background-color: #FFF;
  border-radius: 5px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 2em;
}
.round {
  border-radius: 100%;
}
.widget-1x2 {
  grid-column: span 2;
}

.widget-3x1 {
  grid-column: span 3;
}
.widget-3x2 {
  grid-column: span 3;
  grid-row: span 2;
}
.widget-2x2 {
  grid-column: span 2;
  grid-row: span 2;
}
.widget.phantom {
  opacity: 0.3;
}

JavaScript

const container = document.querySelector('.container');
const widgets = container.querySelectorAll('.widget');

let dragSrc;
let lastTarget;
let mouseX = 0;
let mouseY = 0;

const onMouseMove = (e) => {
	mouseX = e.clientX;
  mouseY = e.clientY;
}

document.addEventListener('drag', onMouseMove)

const onDragStart = (e) => {
	dragSrc = e.target;
  lastTarget = null;
  e.target.classList.add('phantom');
  e.dataTransfer.setData('text/html', dragSrc.innerHTML);
}

const onDragEnd = (e) => {
	dragSrc = null;
  e.target.classList.remove('phantom');
}

const onDragEnter = (e) => {
	e.preventDefault();
}

const onDragLeave = (e) => {}

const isCursorOnLeftHalfOf = (target) => {
	const rect = target.getBoundingClientRect();
	const x = mouseX - rect.left;

  return x < rect.width / 2;
}
const onDragOver = (e) => {
  e.preventDefault();
	
  if(isCursorOnLeftHalfOf(e.target)) {
	  container.insertBefore(dragSrc, e.target);  	
  } else {
  	if(e.target.nextSibling) {
    	container.insertBefore(dragSrc, e.target.nextSibling);
    } else {
	    container.append(dragSrc)
    }
  }

  return false;
}

const onDrop = (e) => {
	e.stopPropagation();
  e.preventDefault();
  e.target.classList.remove('phantom');
  
  return false;
}


const addListeners = (el) => {
	el.addEventListener('dragstart', onDragStart.bind(this));
  el.addEventListener('dragover', onDragOver.bind(this));
  el.addEventListener('dragenter', onDragEnter.bind(this));
  el.addEventListener('dragleave', onDragLeave.bind(this));
  el.addEventListener('dragend', onDragEnd.bind(this));
  el.addEventListener('drop', onDrop.bind(this));
}

const createPhantomEl = (size) => {
  const p = document.createElement('div')
  p.classList.add('widget');
  p.classList.add('phantom');
  if(size) {
	  p.classList.add(size);
  }
	p.addEventListener('drop', onDrop.bind(this))
	
	return p;
}
widgets.forEach(widget => {
  widget.setAttribute('draggable', 'true')
  addListeners(widget);
})