Skip to content

Instantly share code, notes, and snippets.

@uhop
Last active August 3, 2021 23:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save uhop/d87365fac38ba6b8cbf0b890d0c2258e to your computer and use it in GitHub Desktop.
Save uhop/d87365fac38ba6b8cbf0b890d0c2258e to your computer and use it in GitHub Desktop.
A minimalistic generic DnD code. A facelift of https://gist.github.com/uhop/21fa11bf387e234e0d79bf90d7b08735
<!DOCTYPE html>
<html>
<head>
<title>DnD test bed - grouping</title>
<link rel="stylesheet" href="./dnd.css" />
</head>
<body>
<h1>DnD test bed - grouping</h1>
<div class="container">
<div class="item dnd-item dnd-handle">One</div>
<div class="item dnd-item dnd-handle">Two</div>
<div class="item dnd-item dnd-handle">Three</div>
<div class="item dnd-item dnd-handle">Four</div>
<div class="item dnd-item dnd-handle">Five</div>
</div>
<p>
Lorem ipsum dolor sit amet, odio consul ea pri, has ridens repudiandae ea, pro ex aliquid accusata. Vix te magna iudicabit, in quot nobis has, mel ex
ludus melius ocurreret. Ad homero vidisse quo, soluta possim omittam pri in. Eam adhuc erroribus ex, sed lorem delectus liberavisse ad.
</p>
<p>
Nisl integre scriptorem te has, eam te iisque denique qualisque. Mei laudem aperiam facilisi ut, id altera tritani recteque nam, est id vitae facilisi
evertitur. Democritum repudiandae ea ius. Duo cu tritani invenire, ius illum alterum phaedrum eu, nec officiis interesset ei. Mei elit disputationi ad.
</p>
<p>
Te quidam labitur abhorreant usu. Habeo deseruisse ut eum, usu ei unum tractatos. Id est ferri movet nostrum, nullam platonem periculis no quo. Falli
vivendum at vis, amet possim moderatius et pri. Salutandi instructior mei ut, augue quidam necessitatibus mel id. Mutat primis qui ex, adipisci quaestio
eu duo, at sea wisi causae periculis.
</p>
<p>
Per summo debitis ut. Quem mucius constituam mel ut, eos quodsi animal appetere ex. At doming nostrud vel, inani voluptatum no sed. Et vis exerci vidisse
praesent, eam impedit definitionem ea, apeirian vituperatoribus sed no.
</p>
<p>
Nec augue dolor laoreet ad. Ut eos perfecto nominati. Dolorum appellantur id sed, vis probatus constituam appellantur ea. Cu animal alterum molestie has.
Tempor praesent tincidunt eu ius, ei oportere posidonium comprehensam pri.
</p>
<script type="module">
import DndMove from './dnd.js';
document.addEventListener('DOMContentLoaded', () => {
DndMove.start(document.querySelector('.container'), {
init: mover => {
DndMove.init(mover);
mover.avatar.classList.remove('dnd-target');
},
moving: DndMove.movingY,
over: DndMove.over,
drop: mover => {
if (mover.previousOverItem) {
if (mover.previousOverItem !== mover.node) {
let target = mover.previousOverItem.parentNode.classList.contains('dnd-target') ? mover.previousOverItem.parentNode : mover.previousOverItem;
if (target.classList.contains('dnd-item')) {
target = target.ownerDocument.createElement('div');
target.classList.add('dnd-target');
mover.previousOverItem.classList.remove('dnd-target');
mover.previousOverItem.parentNode.replaceChild(target, mover.previousOverItem);
target.appendChild(mover.previousOverItem);
}
mover.node.classList.remove('dnd-target');
const parent = mover.node.parentNode;
target.appendChild(mover.node);
if (!parent.firstElementChild) {
parent.parentNode.removeChild(parent);
}
}
} else {
const parent = mover.node.parentNode;
mover.container.appendChild(mover.node);
if (!parent.firstElementChild) {
parent.parentNode.removeChild(parent);
}
}
}
});
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>DnD test bed - horizontal</title>
<link rel="stylesheet" href="./dnd.css" />
</head>
<body>
<h1>DnD test bed - horizontal</h1>
<div class="horizontal container">
<div class="item dnd-item dnd-handle">One</div>
<div class="item dnd-item dnd-handle">Two</div>
<div class="item dnd-item dnd-handle">Three</div>
<div class="item dnd-item dnd-handle">Four</div>
<div class="item dnd-item dnd-handle">Five</div>
</div>
<p>
Lorem ipsum dolor sit amet, odio consul ea pri, has ridens repudiandae ea, pro ex aliquid accusata. Vix te magna iudicabit, in quot nobis has, mel ex
ludus melius ocurreret. Ad homero vidisse quo, soluta possim omittam pri in. Eam adhuc erroribus ex, sed lorem delectus liberavisse ad.
</p>
<p>
Nisl integre scriptorem te has, eam te iisque denique qualisque. Mei laudem aperiam facilisi ut, id altera tritani recteque nam, est id vitae facilisi
evertitur. Democritum repudiandae ea ius. Duo cu tritani invenire, ius illum alterum phaedrum eu, nec officiis interesset ei. Mei elit disputationi ad.
</p>
<p>
Te quidam labitur abhorreant usu. Habeo deseruisse ut eum, usu ei unum tractatos. Id est ferri movet nostrum, nullam platonem periculis no quo. Falli
vivendum at vis, amet possim moderatius et pri. Salutandi instructior mei ut, augue quidam necessitatibus mel id. Mutat primis qui ex, adipisci quaestio
eu duo, at sea wisi causae periculis.
</p>
<p>
Per summo debitis ut. Quem mucius constituam mel ut, eos quodsi animal appetere ex. At doming nostrud vel, inani voluptatum no sed. Et vis exerci vidisse
praesent, eam impedit definitionem ea, apeirian vituperatoribus sed no.
</p>
<p>
Nec augue dolor laoreet ad. Ut eos perfecto nominati. Dolorum appellantur id sed, vis probatus constituam appellantur ea. Cu animal alterum molestie has.
Tempor praesent tincidunt eu ius, ei oportere posidonium comprehensam pri.
</p>
<script type="module">
import DndMove from './dnd.js';
document.addEventListener('DOMContentLoaded', () => {
DndMove.start(document.querySelector('.container'), {
init: DndMove.init,
over: DndMove.over,
moving: DndMove.movingX,
drop: DndMove.dropX
});
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>DnD test bed - handle</title>
<link rel="stylesheet" href="./dnd.css" />
</head>
<body>
<h1>DnD test bed - handle</h1>
<div class="container">
<div class="item dnd-item"><span class="dnd-handle">&vellip;&vellip;</span>&nbsp;One</div>
<div class="item dnd-item"><span class="dnd-handle">&vellip;&vellip;</span>&nbsp;Two</div>
<div class="item dnd-item"><span class="dnd-handle">&vellip;&vellip;</span>&nbsp;Three</div>
<div class="item dnd-item"><span class="dnd-handle">&vellip;&vellip;</span>&nbsp;Four</div>
<div class="item dnd-item"><span class="dnd-handle">&vellip;&vellip;</span>&nbsp;Five</div>
</div>
<p>
Lorem ipsum dolor sit amet, odio consul ea pri, has ridens repudiandae ea, pro ex aliquid accusata. Vix te magna iudicabit, in quot nobis has, mel ex
ludus melius ocurreret. Ad homero vidisse quo, soluta possim omittam pri in. Eam adhuc erroribus ex, sed lorem delectus liberavisse ad.
</p>
<p>
Nisl integre scriptorem te has, eam te iisque denique qualisque. Mei laudem aperiam facilisi ut, id altera tritani recteque nam, est id vitae facilisi
evertitur. Democritum repudiandae ea ius. Duo cu tritani invenire, ius illum alterum phaedrum eu, nec officiis interesset ei. Mei elit disputationi ad.
</p>
<p>
Te quidam labitur abhorreant usu. Habeo deseruisse ut eum, usu ei unum tractatos. Id est ferri movet nostrum, nullam platonem periculis no quo. Falli
vivendum at vis, amet possim moderatius et pri. Salutandi instructior mei ut, augue quidam necessitatibus mel id. Mutat primis qui ex, adipisci quaestio
eu duo, at sea wisi causae periculis.
</p>
<p>
Per summo debitis ut. Quem mucius constituam mel ut, eos quodsi animal appetere ex. At doming nostrud vel, inani voluptatum no sed. Et vis exerci vidisse
praesent, eam impedit definitionem ea, apeirian vituperatoribus sed no.
</p>
<p>
Nec augue dolor laoreet ad. Ut eos perfecto nominati. Dolorum appellantur id sed, vis probatus constituam appellantur ea. Cu animal alterum molestie has.
Tempor praesent tincidunt eu ius, ei oportere posidonium comprehensam pri.
</p>
<script type="module">
import DndMove from './dnd.js';
document.addEventListener('DOMContentLoaded', () => {
DndMove.start(document.querySelector('.container'), {
init: DndMove.init,
over: DndMove.over,
moving: DndMove.movingY,
drop: DndMove.dropY
});
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>DnD test bed - reorder</title>
<link rel="stylesheet" href="./dnd.css" />
</head>
<body>
<h1>DnD test bed - reorder</h1>
<div class="container">
<div class="item dnd-item dnd-handle">One</div>
<div class="item dnd-item dnd-handle">Two</div>
<div class="item dnd-item dnd-handle">Three</div>
<div class="item dnd-item dnd-handle">Four</div>
<div class="item dnd-item dnd-handle">Five</div>
</div>
<p>
Lorem ipsum dolor sit amet, odio consul ea pri, has ridens repudiandae ea, pro ex aliquid accusata. Vix te magna iudicabit, in quot nobis has, mel ex
ludus melius ocurreret. Ad homero vidisse quo, soluta possim omittam pri in. Eam adhuc erroribus ex, sed lorem delectus liberavisse ad.
</p>
<p>
Nisl integre scriptorem te has, eam te iisque denique qualisque. Mei laudem aperiam facilisi ut, id altera tritani recteque nam, est id vitae facilisi
evertitur. Democritum repudiandae ea ius. Duo cu tritani invenire, ius illum alterum phaedrum eu, nec officiis interesset ei. Mei elit disputationi ad.
</p>
<p>
Te quidam labitur abhorreant usu. Habeo deseruisse ut eum, usu ei unum tractatos. Id est ferri movet nostrum, nullam platonem periculis no quo. Falli
vivendum at vis, amet possim moderatius et pri. Salutandi instructior mei ut, augue quidam necessitatibus mel id. Mutat primis qui ex, adipisci quaestio
eu duo, at sea wisi causae periculis.
</p>
<p>
Per summo debitis ut. Quem mucius constituam mel ut, eos quodsi animal appetere ex. At doming nostrud vel, inani voluptatum no sed. Et vis exerci vidisse
praesent, eam impedit definitionem ea, apeirian vituperatoribus sed no.
</p>
<p>
Nec augue dolor laoreet ad. Ut eos perfecto nominati. Dolorum appellantur id sed, vis probatus constituam appellantur ea. Cu animal alterum molestie has.
Tempor praesent tincidunt eu ius, ei oportere posidonium comprehensam pri.
</p>
<script type="module">
import DndMove from './dnd.js';
document.addEventListener('DOMContentLoaded', () => {
DndMove.start(document.querySelector('.container'), {
init: DndMove.init,
over: DndMove.over,
moving: DndMove.movingY,
newOver: mover => {
if (!mover.previousOverItem || mover.previousOverItem === mover.node) return;
// move the dragged node
let beforeNode = mover.previousOverItem;
for (let current = mover.container.firstElementChild; current; current = current.nextElementSibling) {
if (current === mover.node) {
beforeNode = beforeNode.nextElementSibling;
break;
}
if (current === mover.previousOverItem) break;
}
mover.container.insertBefore(mover.node, beforeNode);
// recalculate bounding boxes
mover.calculateBoundingBoxes();
},
drop: () => {}
});
});
</script>
</body>
</html>
.container {
font-size: 18pt;
color: black;
display: flex;
flex-direction: column;
margin: 2em;
border: 3px solid #ddd;
padding: 1em;
max-width: 8em;
}
.container.horizontal {
flex-direction: row;
max-width: auto;
}
.item {
font-size: 18pt;
color: black;
flex: 1 1 auto;
margin: 0.5em;
border: 3px solid #ccc;
padding: 1em;
}
.dnd-handle {
cursor: pointer;
}
.dnd-handle::selection {
background-color: transparent;
}
.dnd-avatar {
background-color: #fcc;
opacity: 0.5;
}
.dnd-dragged {
background-color: #cfc;
opacity: 0.5;
}
.dnd-over {
background-color: #ffe;
}
body {
transition: background-color 0.5s;
}
.dnd-in-flight body {
background-color: #ddd;
}
.dnd-dragged-container {
background-color: white;
}
/* grouping extension */
.dnd-target {
margin: 0.5em;
}
.dnd-target .dnd-item {
margin: 0;
}
<!DOCTYPE html>
<html>
<head>
<title>DnD test bed</title>
<link rel="stylesheet" href="./dnd.css" />
</head>
<body>
<h1>DnD test bed</h1>
<div class="container">
<div class="item dnd-item dnd-handle">One</div>
<div class="item dnd-item dnd-handle">Two</div>
<div class="item dnd-item dnd-handle">Three</div>
<div class="item dnd-item dnd-handle">Four</div>
<div class="item dnd-item dnd-handle">Five</div>
</div>
<p>
Lorem ipsum dolor sit amet, odio consul ea pri, has ridens repudiandae ea, pro ex aliquid accusata. Vix te magna iudicabit, in quot nobis has, mel ex
ludus melius ocurreret. Ad homero vidisse quo, soluta possim omittam pri in. Eam adhuc erroribus ex, sed lorem delectus liberavisse ad.
</p>
<p>
Nisl integre scriptorem te has, eam te iisque denique qualisque. Mei laudem aperiam facilisi ut, id altera tritani recteque nam, est id vitae facilisi
evertitur. Democritum repudiandae ea ius. Duo cu tritani invenire, ius illum alterum phaedrum eu, nec officiis interesset ei. Mei elit disputationi ad.
</p>
<p>
Te quidam labitur abhorreant usu. Habeo deseruisse ut eum, usu ei unum tractatos. Id est ferri movet nostrum, nullam platonem periculis no quo. Falli
vivendum at vis, amet possim moderatius et pri. Salutandi instructior mei ut, augue quidam necessitatibus mel id. Mutat primis qui ex, adipisci quaestio
eu duo, at sea wisi causae periculis.
</p>
<p>
Per summo debitis ut. Quem mucius constituam mel ut, eos quodsi animal appetere ex. At doming nostrud vel, inani voluptatum no sed. Et vis exerci vidisse
praesent, eam impedit definitionem ea, apeirian vituperatoribus sed no.
</p>
<p>
Nec augue dolor laoreet ad. Ut eos perfecto nominati. Dolorum appellantur id sed, vis probatus constituam appellantur ea. Cu animal alterum molestie has.
Tempor praesent tincidunt eu ius, ei oportere posidonium comprehensam pri.
</p>
<script type="module">
import DndMove from './dnd.js';
document.addEventListener('DOMContentLoaded', () => {
DndMove.start(document.querySelector('.container'), {
init: DndMove.init,
over: DndMove.over,
moving: DndMove.movingY,
drop: DndMove.dropY
});
});
</script>
</body>
</html>
'use strict';
const noop = () => {};
class DndMove {
static supportedEvents = {
mouseup: 'onMouseUp',
mousemove: 'onMouseMove',
touchend: 'onMouseUp',
touchcancel: 'onMouseUp',
touchmove: 'onTouchMove',
dragstart: 'stopEvent',
selectstart: 'stopEvent'
};
static formNode = {a: 1, input: 1, select: 1, button: 1, textarea: 1, option: 1};
constructor(container, options, node, e) {
this.container = container;
this.options = options || {};
this.node = node;
// handle the state
this.container.classList.add('dnd-dragged-container');
this.node.ownerDocument.documentElement.classList.add('dnd-in-flight');
this.node.classList.add('dnd-dragged');
this.mouseX = e.pageX;
this.mouseY = e.pageY;
this.avatar = (this.options.makeAvatar || DndMove.clone)(this);
this.moving = this.options.moving || DndMove.moving;
this.over = this.options.over || noop;
this.newOver = this.options.newOver || noop;
// listen for events
Object.keys(DndMove.supportedEvents).forEach(name => name != 'selectstart' && this.node.ownerDocument.addEventListener(name, this));
this.node.ownerDocument.body.addEventListener('selectstart', this);
(this.options.init || noop)(this);
}
destroy() {
// remove event listeners
Object.keys(DndMove.supportedEvents).forEach(name => name != 'selectstart' && this.node.ownerDocument.removeEventListener(name, this));
this.node.ownerDocument.body.removeEventListener('selectstart', this);
// handle the state
this.container.classList.remove('dnd-dragged-container');
this.node.ownerDocument.documentElement.classList.remove('dnd-in-flight');
this.node.classList.remove('dnd-dragged');
if (this.previousOverItem) {
this.previousOverItem.classList.remove('dnd-over');
}
(this.options.destroy || noop)(this);
}
calculateBoundingBoxes() {
const items = this.container.querySelectorAll(this.options.target || '.dnd-item');
this.itemBoxes = Array.from(items).map(item => {
const rect = item.getBoundingClientRect();
return {
node: item,
left: rect.left + window.pageXOffset,
right: rect.right + window.pageXOffset,
top: rect.top + window.pageYOffset,
bottom: rect.bottom + window.pageYOffset
};
});
}
// the starter
static start(container, options) {
options = options || {};
const callback = DndMove.process(container, options),
filter = options.filter || '.dnd-handle';
let handler = typeof filter == 'function' ? e => filter(e) && callback(e) : e => e.target.closest(filter) && callback(e);
container.addEventListener('mousedown', handler);
container.addEventListener('touchstart', handler);
return {
container,
options,
remove: () => {
container.removeEventListener('mousedown', handler);
container.removeEventListener('touchstart', handler);
}
};
}
// events
handleEvent(e) {
this[DndMove.supportedEvents[e.type]](e);
}
onMouseUp(e) {
// done
DndMove.stopEvent(e);
(this.options.drop || noop)(this);
this.avatar.parentNode.removeChild(this.avatar);
this.destroy();
}
onMouseMove(e) {
DndMove.stopEvent(e);
this.moving(this, e);
this.mouseX = e.pageX;
this.mouseY = e.pageY;
this.avatar.style.left = this.x + 'px';
this.avatar.style.top = this.y + 'px';
this.over(this);
}
onTouchMove(e) {
DndMove.stopEvent(e);
const fakeEvent = e.targetTouches[0];
this.moving(this, fakeEvent);
this.mouseX = fakeEvent.pageX;
this.mouseY = fakeEvent.pageY;
this.avatar.style.left = this.x + 'px';
this.avatar.style.top = this.y + 'px';
this.over(this);
}
stopEvent(e) {
DndMove.stopEvent(e);
}
// helpers
static init(mover) {
// container box
let style = window.getComputedStyle(mover.container);
const rect = mover.container.getBoundingClientRect(),
box = {
left: rect.left + window.pageXOffset + parseFloat(style.borderLeftWidth),
right: rect.right + window.pageXOffset - parseFloat(style.borderRightWidth) - parseFloat(style.marginRight),
top: rect.top + window.pageYOffset + parseFloat(style.borderTopWidth),
bottom: rect.bottom + window.pageYOffset - parseFloat(style.borderBottomWidth) - parseFloat(style.marginBottom)
};
style = window.getComputedStyle(mover.avatar);
box.left -= parseFloat(style.marginLeft);
box.right -= parseFloat(style.marginLeft) + parseFloat(style.borderLeftWidth) + parseFloat(style.width) + parseFloat(style.borderRightWidth);
box.top -= parseFloat(style.marginTop);
box.bottom -= parseFloat(style.marginTop) + parseFloat(style.borderTopWidth) + parseFloat(style.height) + parseFloat(style.borderBottomWidth);
mover.containerBox = box;
mover.calculateBoundingBoxes();
}
static over(mover) {
// binary search is better, but linear will do in a pinch
const done = mover.itemBoxes.some(item => {
if (item.left <= mover.mouseX && mover.mouseX < item.right && item.top <= mover.mouseY && mover.mouseY < item.bottom) {
if (item.node !== mover.previousOverItem) {
mover.previousOverItem && mover.previousOverItem.classList.remove('dnd-over');
mover.previousOverItem = item.node;
mover.previousOverItem.classList.add('dnd-over');
mover.newOver(mover);
}
return true;
}
return false;
});
if (!done && mover.previousOverItem) {
mover.previousOverItem && mover.previousOverItem.classList.remove('dnd-over');
mover.previousOverItem = null;
mover.newOver(mover);
}
}
static movingX(mover, e) {
mover.x += e.pageX - mover.mouseX;
mover.x = Math.max(mover.containerBox.left, Math.min(mover.containerBox.right, mover.x));
}
static movingY(mover, e) {
mover.y += e.pageY - mover.mouseY;
mover.y = Math.max(mover.containerBox.top, Math.min(mover.containerBox.bottom, mover.y));
}
static moving(mover, e) {
mover.x += e.pageX - mover.mouseX;
mover.x = Math.max(mover.containerBox.left, Math.min(mover.containerBox.right, mover.x));
mover.y += e.pageY - mover.mouseY;
mover.y = Math.max(mover.containerBox.top, Math.min(mover.containerBox.bottom, mover.y));
}
static dropX(mover) {
if (!mover.previousOverItem) return;
// binary search is better, but linear will do in a pinch
const done = mover.itemBoxes.some(item => {
if (mover.mouseX < (item.left + item.right) / 2) {
item.node !== mover.node && mover.container.insertBefore(mover.node, item.node);
return true;
}
return false;
});
!done && mover.container.appendChild(mover.node);
}
static dropY(mover) {
if (!mover.previousOverItem) return;
// binary search is better, but linear will do in a pinch
const done = mover.itemBoxes.some(item => {
if (mover.mouseY < (item.top + item.bottom) / 2) {
item.node !== mover.node && mover.container.insertBefore(mover.node, item.node);
return true;
}
return false;
});
!done && mover.container.appendChild(mover.node);
}
// statics
static process(container, options) {
return e => {
e.preventDefault();
const node = e.target;
if (!e.button && DndMove.formNode[node.tagName.toLowerCase()] !== 1 && !node.classList.contains('dnd-ignore')) {
const item = node.closest('.dnd-item');
if (item && !item.classList.contains('dnd-ignore')) {
new DndMove(container, options, item, e);
}
}
};
}
static clone(mover) {
const node = mover.node,
box = node.getBoundingClientRect(),
style = window.getComputedStyle(node),
avatar = node.cloneNode(true);
mover.x = box.left - parseFloat(style.marginLeft) + window.pageXOffset;
mover.y = box.top - parseFloat(style.marginTop) + window.pageYOffset;
avatar.style.position = 'absolute';
avatar.style.height = style.height;
avatar.style.width = style.width;
avatar.style.left = mover.x + 'px';
avatar.style.top = mover.y + 'px';
avatar.classList.remove('dnd-dragged');
avatar.classList.add(mover.options.avatarClass || 'dnd-avatar');
node.ownerDocument.body.appendChild(avatar);
return avatar;
}
static stopEvent(e) {
e.preventDefault();
e.stopPropagation();
}
}
export default DndMove;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment