Skip to content

Instantly share code, notes, and snippets.

@uhop
Last active March 3, 2023 18: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/f03355033012601ff95bd5f1beb947a7 to your computer and use it in GitHub Desktop.
Save uhop/f03355033012601ff95bd5f1beb947a7 to your computer and use it in GitHub Desktop.
The facelift of the minimalistic DnD code from https://gist.github.com/uhop/d87365fac38ba6b8cbf0b890d0c2258e
'use strict';
// from https://gist.github.com/uhop/d87365fac38ba6b8cbf0b890d0c2258e
const noop = () => {};
class DndMove {
static supportedEvents = {
pointerup: 'onPointerUp',
pointermove: 'onPointerMove',
touchstart: 'stopEvent',
touchmove: 'stopEvent',
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 => {
if (name == 'selectstart') return this.node.ownerDocument.body.addEventListener(name, this);
this.node.ownerDocument.addEventListener(name, this, {passive: false});
});
(this.options.init || noop)(this);
}
destroy() {
// remove event listeners
Object.keys(DndMove.supportedEvents).forEach(name => {
if (name == 'selectstart') return this.node.ownerDocument.body.removeEventListener(name, this);
this.node.ownerDocument.removeEventListener(name, 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',
handler =
typeof filter == 'function' ? e => filter(e) && callback(e) : e => e.target.closest(filter) && callback(e);
container.addEventListener('pointerdown', handler);
return {
container,
options,
remove: () => {
container.removeEventListener('pointerdown', handler);
}
};
}
// events
handleEvent(e) {
this[DndMove.supportedEvents[e.type]](e);
}
onPointerUp(e) {
DndMove.stopEvent(e);
(this.options.drop || noop)(this);
this.avatar.parentNode.removeChild(this.avatar);
this.destroy();
}
onPointerMove(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);
}
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 => {
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')) {
// if (
// typeof node.hasPointerCapture == 'function' &&
// node.hasPointerCapture(e.pointerId) &&
// typeof node.releasePointerCapture == 'function'
// ) {
// node.releasePointerCapture(e.pointerId);
// }
e.preventDefault();
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;
@uhop
Copy link
Author

uhop commented Jul 23, 2022

The differences with https://gist.github.com/uhop/d87365fac38ba6b8cbf0b890d0c2258e are minor:

  • Switched to pointer events.
  • Suppressed touch events while dragging to avoid page scrolling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment