vue-easy-dnd
Version:
Easy-DnD is a drag and drop implementation for Vue 3 that uses only standard mouse events instead of the HTML5 drag and drop API, which is [impossible to work with](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html). Think of it as a wa
415 lines (388 loc) • 13.6 kB
JavaScript
import DragAwareMixin from './DragAwareMixin';
import { createDragImage } from '../js/createDragImage';
import { dnd } from '../js/DnD';
import scrollparent from '../helpers/scrollparent';
import { cancelScrollAction, performEdgeScroll, isContainerReadyToEdgeScroll } from '../helpers/edgescroller';
export default {
mixins: [DragAwareMixin],
props: {
type: {
type: String,
default: null
},
data: {
default: null
},
dragImageOpacity: {
type: Number,
default: 0.7
},
disabled: {
type: Boolean,
default: false
},
goBack: {
type: Boolean,
default: false
},
handle: {
type: String,
default: null
},
delta: {
type: Number,
default: 0
},
delay: {
type: Number,
default: 0
},
dragClass: {
type: String,
default: null
},
vibration: {
type: Number,
default: 0
},
scrollingEdgeSize: {
type: Number,
default: 100
}
},
emits: ['dragstart', 'dragend', 'cut', 'copy'],
data () {
return {
dragInitialised: false,
dragStarted: false,
ignoreNextClick: false,
initialUserSelect: null,
downEvent: null,
startPosition: null,
delayTimer: null,
scrollContainer: null
};
},
computed: {
cssClasses () {
const clazz = {
'dnd-drag': true
};
if (!this.disabled) {
return {
...clazz,
'drag-source': this.dragInProgress && this.dragSource === this,
'drag-mode-copy': this.currentDropMode === 'copy',
'drag-mode-cut': this.currentDropMode === 'cut',
'drag-mode-reordering': this.currentDropMode === 'reordering',
'drag-no-handle': !this.handle
};
}
else {
return clazz;
}
},
currentDropMode () {
if (this.dragInProgress && this.dragSource === this) {
if (this.dragTop && this.dragTop['dropAllowed']) {
if (this.dragTop['reordering']) {
return 'reordering';
}
else {
return this.dragTop['mode'];
}
}
else {
return null;
}
}
else {
return null;
}
}
},
methods: {
onSelectStart (e) {
e.stopPropagation();
e.preventDefault();
},
performVibration () {
// If browser can perform vibration and user has defined a vibration, perform it
if (this.vibration > 0 && window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(this.vibration);
}
},
onMouseDown (e) {
let target = null;
let goodButton = false;
if (e.type === 'mousedown') {
const mouse = e;
target = e.target;
goodButton = mouse.buttons === 1;
}
else {
const touch = e;
target = touch.touches[0].target;
goodButton = true;
}
if (this.disabled || this.downEvent !== null || !goodButton) {
return;
}
// Check that the target element is eligible for starting a drag
// Includes checking against the handle selector
// or whether the element contains 'dnd-no-drag' class (which should disable dragging from that
// sub-element of a draggable parent)
const goodTarget = !target.matches('.dnd-no-drag, .dnd-no-drag *') &&
(
!this.handle ||
target.matches(this.handle + ', ' + this.handle + ' *')
);
if (!goodTarget) {
return;
}
this.scrollContainer = scrollparent(target);
this.initialUserSelect = document.body.style.userSelect;
document.documentElement.style.userSelect = 'none'; // Permet au drag de se poursuivre normalement même
// quand on quitte un élémént avec overflow: hidden.
this.dragStarted = false;
this.downEvent = e;
if (this.downEvent.type === 'mousedown') {
const mouse = e;
this.startPosition = {
x: mouse.clientX,
y: mouse.clientY
};
}
else {
const touch = e;
this.startPosition = {
x: touch.touches[0].clientX,
y: touch.touches[0].clientY
};
}
if (this.delay) {
this.dragInitialised = false;
clearTimeout(this.delayTimer);
this.delayTimer = setTimeout(() => {
this.dragInitialised = true;
this.performVibration();
}, this.delay);
}
else {
this.dragInitialised = true;
this.performVibration();
}
document.addEventListener('click', this.onMouseClick, true);
document.addEventListener('mouseup', this.onMouseUp);
document.addEventListener('touchend', this.onMouseUp);
document.addEventListener('selectstart', this.onSelectStart);
document.addEventListener('keyup', this.onKeyUp);
setTimeout(() => {
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('touchmove', this.onMouseMove, { passive: false });
document.addEventListener('easy-dnd-move', this.onEasyDnDMove);
}, 0);
// Prevents event from bubbling to ancestor drag components and initiate several drags at the same time
e.stopPropagation();
},
// Prevent the user from accidentally causing a click event
// if they have just attempted a drag event
onMouseClick (e) {
if (this.ignoreNextClick) {
e.preventDefault();
e.stopPropagation && e.stopPropagation();
e.stopImmediatePropagation && e.stopImmediatePropagation();
this.ignoreNextClick = false;
return false;
}
},
onMouseMove (e) {
// We ignore the mousemove event that follows touchend :
if (this.downEvent === null) return;
// On touch devices, we ignore fake mouse events and deal with touch events only.
if (this.downEvent.type === 'touchstart' && e.type === 'mousemove') return;
// Find out event target and pointer position :
let target = null;
let x = null;
let y = null;
if (e.type === 'touchmove') {
const touch = e;
x = touch.touches[0].clientX;
y = touch.touches[0].clientY;
target = document.elementFromPoint(x, y);
if (!target) {
// Mouse going off screen. Ignore event.
return;
}
}
else {
const mouse = e;
x = mouse.clientX;
y = mouse.clientY;
target = mouse.target;
}
// Distance between current event and start position :
const dist = Math.sqrt(Math.pow(this.startPosition.x - x, 2) + Math.pow(this.startPosition.y - y, 2));
// If the drag has not begun yet and distance from initial point is greater than delta, we start the drag :
if (!this.dragStarted && dist > this.delta) {
// If they have dragged greater than the delta before the delay period has ended,
// It means that they attempted to perform another action (such as scrolling) on the page
if (!this.dragInitialised) {
clearTimeout(this.delayTimer);
}
else {
this.ignoreNextClick = true;
this.dragStarted = true;
dnd.startDrag(this, this.downEvent, this.startPosition.x, this.startPosition.y, this.type, this.data);
document.documentElement.classList.add('drag-in-progress');
}
}
// Dispatch custom easy-dnd-move event :
if (this.dragStarted) {
// If cursor/touch is at edge of container, perform scroll if available
// If this.dragTop is defined, it means they are dragging on top of another DropList/EasyDnd component
// if dropTop is a DropList, use the scrollingEdgeSize of that container if it exists, otherwise use the scrollingEdgeSize of the Drag component
const currEdgeSize = this.dragTop && this.dragTop.$props.scrollingEdgeSize !== undefined ?
this.dragTop.$props.scrollingEdgeSize :
this.scrollingEdgeSize;
if (currEdgeSize) {
// Create an array of all scrollable elements going upward until the body is hit
let currScrollContainer = this.dragTop ? scrollparent(this.dragTop.$el) : this.scrollContainer;
const nodes = [currScrollContainer];
do {
if (currScrollContainer === document.body) {
break;
}
currScrollContainer = scrollparent(currScrollContainer.parentNode);
if (!currScrollContainer || currScrollContainer === document.body) {
break;
}
nodes.push(currScrollContainer);
} while (currScrollContainer && currScrollContainer !== document.body);
// Iterate through all these nodes starting from the closest to body, and work towards current node
for (let i = nodes.length - 1; i >= 0; i--) {
cancelScrollAction();
const thisNode = nodes[i];
// Check that the current cursor pos + edge of container + scroll pos of container allows user
// to start/continue scrolling in current direction
if (isContainerReadyToEdgeScroll(thisNode, x, y, currEdgeSize)) {
performEdgeScroll(thisNode, x, y, currEdgeSize);
break;
}
}
}
else {
cancelScrollAction();
}
const custom = new CustomEvent('easy-dnd-move', {
bubbles: true,
cancelable: true,
detail: {
x,
y,
native: e
}
});
target.dispatchEvent(custom);
}
// Prevent scroll on touch devices if they were performing a drag
if (this.dragInitialised && e.cancelable) {
e.preventDefault();
}
},
onEasyDnDMove (e) {
dnd.mouseMove(e, null);
},
onMouseUp (e) {
// On touch devices, we ignore fake mouse events and deal with touch events only.
if (this.downEvent.type === 'touchstart' && e.type === 'mouseup') return;
// This delay makes sure that when the click event that results from the mouseup is produced, the drag is
// still in progress. So by checking the flag dnd.inProgress, one can tell apart true clicks from drag and
// drop artefacts.
setTimeout(() => {
this.cancelDragActions();
if (this.dragStarted) {
dnd.stopDrag(e);
}
this.finishDrag();
}, 0);
},
onKeyUp (e) {
// If ESC is pressed, cancel the drag
if (e.key === 'Escape') {
this.cancelDragActions();
setTimeout(() => {
dnd.cancelDrag(e);
this.finishDrag();
}, 0);
}
},
cancelDragActions () {
this.dragInitialised = false;
clearTimeout(this.delayTimer);
cancelScrollAction();
},
finishDrag () {
this.downEvent = null;
this.scrollContainer = null;
if (this.dragStarted) {
document.documentElement.classList.remove('drag-in-progress');
}
document.removeEventListener('click', this.onMouseClick, true);
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('touchmove', this.onMouseMove);
document.removeEventListener('easy-dnd-move', this.onEasyDnDMove);
document.removeEventListener('mouseup', this.onMouseUp);
document.removeEventListener('touchend', this.onMouseUp);
document.removeEventListener('selectstart', this.onSelectStart);
document.removeEventListener('keyup', this.onKeyUp);
document.documentElement.style.userSelect = this.initialUserSelect;
},
dndDragStart (ev) {
if (ev.source === this) {
this.$emit('dragstart', ev);
}
},
dndDragEnd (ev) {
if (ev.source === this) {
this.$emit('dragend', ev);
}
},
createDragImage (selfTransform) {
let image;
if (this.$slots['drag-image']) {
const el = this.$refs['drag-image'] || document.createElement('div');
if (el.childElementCount !== 1) {
image = createDragImage(el);
}
else {
image = createDragImage(el.children.item(0));
}
}
else {
image = createDragImage(this.$el);
image.style.transform = selfTransform;
}
if (this.dragClass) {
image.classList.add(this.dragClass);
}
image.classList.add('dnd-ghost');
image['__opacity'] = this.dragImageOpacity;
return image;
}
},
created () {
dnd.on('dragstart', this.dndDragStart);
dnd.on('dragend', this.dndDragEnd);
},
mounted () {
this.$el.addEventListener('mousedown', this.onMouseDown, { passive: true });
this.$el.addEventListener('touchstart', this.onMouseDown, { passive: true });
},
beforeUnmount () {
dnd.off('dragstart', this.dndDragStart);
dnd.off('dragend', this.dndDragEnd);
this.$el.removeEventListener('mousedown', this.onMouseDown);
this.$el.removeEventListener('touchstart', this.onMouseDown);
}
};