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
1,650 lines (1,521 loc) • 59.2 kB
JavaScript
import { reactive, openBlock, createBlock, resolveDynamicComponent, normalizeClass, createSlots, withCtx, renderSlot, normalizeProps, guardReactiveProps, createElementBlock, createCommentVNode, renderList, TransitionGroup, h, nextTick } from 'vue';
function mitt(n){return {all:n=n||new Map,on:function(t,e){var i=n.get(t);i?i.push(e):n.set(t,[e]);},off:function(t,e){var i=n.get(t);i&&(e?i.splice(i.indexOf(e)>>>0,1):n.set(t,[]));},emit:function(t,e){var i=n.get(t);i&&i.slice().map(function(n){n(e);}),(i=n.get("*"))&&i.slice().map(function(n){n(t,e);});}}}
/**
* This is the class of the global object that holds the state of the drag and drop during its progress. It emits events
* reporting its state evolution during the progress of the drag and drop. Its data is reactive and listeners can be
* attached to it using the method on.
*/
class DnD {
inProgress = false;
type = null;
data = null;
source = null;
top = null;
position = null;
eventBus = mitt();
success = null;
startDrag (source, event, x, y, type, data) {
this.type = type;
this.data = data;
this.source = source;
this.position = { x, y };
this.top = null;
this.inProgress = true;
this.emit(event, 'dragstart');
this.emit(event, 'dragtopchanged', { previousTop: null });
}
resetVariables () {
this.inProgress = false;
this.data = null;
this.source = null;
this.position = null;
this.success = null;
}
stopDrag (event) {
this.success = this.top !== null && this.top['compatibleMode'] && this.top['dropAllowed'];
if (this.top !== null) {
this.emit(event, 'drop');
}
this.emit(event, 'dragend');
this.resetVariables();
}
cancelDrag (event) {
this.success = false;
this.emit(event, 'dragend');
this.resetVariables();
}
mouseMove (event, comp) {
if (this.inProgress) {
let prevent = false;
const previousTop = this.top;
if (comp === null) {
// The mouse move event reached the top of the document without hitting a drop component.
this.top = null;
prevent = true;
}
else if (comp['isDropMask']) {
// The mouse move event bubbled until it reached a drop mask.
this.top = null;
prevent = true;
}
else if (comp['candidate'](this.type, this.data, this.source)) {
// The mouse move event bubbled until it reached a drop component that participates in the current drag operation.
this.top = comp;
prevent = true;
}
if (prevent) {
// We prevent the mouse move event from bubbling further up the tree because it reached the foremost drop component and that component is all that matters.
event.stopPropagation();
}
if (this.top !== previousTop) {
this.emit(event.detail.native, 'dragtopchanged', { previousTop: previousTop });
}
this.position = {
x: event.detail.x,
y: event.detail.y
};
this.emit(event.detail.native, 'dragpositionchanged');
}
}
emit (native, event, data = {}) {
this.eventBus.emit(event, {
type: this.type,
data: this.data,
top: this.top,
source: this.source,
position: this.position,
success: this.success,
native,
...data
});
}
on (event, callback) {
this.eventBus.on(event, callback);
}
off (event, callback) {
this.eventBus.off(event, callback);
}
}
const dnd = reactive(new DnD());
var DragAwareMixin = {
data () {
return {
isDropMask: false
};
},
computed: {
dragInProgress () {
return dnd.inProgress;
},
dragData () {
return dnd.data;
},
dragType () {
return dnd.type;
},
dragPosition () {
return dnd.position;
},
dragSource () {
return dnd.source;
},
dragTop () {
return dnd.top;
}
}
};
/**
* This files contains the primitives required to create drag images from HTML elements that serve as models. A snapshot
* of the computed styles of the model elements is taken when creating the drag image, so that it will look the same as
* the model, no matter where the drag images is grafted into the DOM.
*/
/**
* Creates a drag image using the given element as model.
*/
function createDragImage (el) {
const clone = deepClone(el);
clone.style.position = 'fixed';
clone.style.margin = '0';
clone.style['z-index'] = '1000';
clone.style.transition = 'opacity 0.2s';
return clone;
}
/**
* Clones the given element and all its descendants.
*/
function deepClone (el) {
const clone = el.cloneNode(true);
copyStyle(el, clone);
const vSrcElements = el.getElementsByTagName('*');
const vDstElements = clone.getElementsByTagName('*');
for (let i = vSrcElements.length; i--;) {
const vSrcElement = vSrcElements[i];
const vDstElement = vDstElements[i];
copyStyle(vSrcElement, vDstElement);
}
return clone;
}
/**
* Copy the computed styles from src to destination.
*/
function copyStyle (src, destination) {
const computedStyle = window.getComputedStyle(src);
for (const key of computedStyle) {
destination.style.setProperty(
key,
computedStyle.getPropertyValue(key),
computedStyle.getPropertyPriority(key)
);
}
destination.style.pointerEvents = 'none';
}
// Forked from https://gist.github.com/gre/296291b8ce0d8fe6e1c3ea4f1d1c5c3b
const regex = /(auto|scroll)/;
const style = (node, prop) =>
getComputedStyle(node, null).getPropertyValue(prop);
const scroll = (node) =>
regex.test(
style(node, 'overflow') +
style(node, 'overflow-y') +
style(node, 'overflow-x'));
const scrollparent = (node) => {
if (!node || node === document.body) {
return document.body;
}
if (scroll(node)) {
return node;
}
return scrollparent(node.parentNode);
};
// Forked from https://github.com/bennadel/JavaScript-Demos/blob/master/demos/window-edge-scrolling/index.htm
// Code was altered to work with scrollable containers
let timer = null;
function cancelScrollAction () {
clearTimeout(timer);
}
function isBodyContainer (container) {
return container === document.body;
}
// Determine if user is inside an edge of the container
function isInEdge (container, clientX, clientY, edgeSize) {
// Get the viewport-relative coordinates of the mousemove event.
const rect = container.getBoundingClientRect();
const isBody = isBodyContainer(container);
let viewportX = clientX - rect.left;
let viewportY = clientY - rect.top;
if (isBody) {
viewportX = clientX;
viewportY = clientY;
}
// Get the viewport dimensions.
let viewportWidth = rect.width;
let viewportHeight = rect.height;
if (isBody) {
viewportWidth = document.documentElement.clientWidth;
viewportHeight = document.documentElement.clientHeight;
}
// Next, we need to determine if the mouse is within the "edge" of the
// viewport, which may require scrolling the window. To do this, we need to
// calculate the boundaries of the edge in the viewport (these coordinates
// are relative to the viewport grid system).
const edgeTop = edgeSize;
const edgeLeft = edgeSize;
const edgeBottom = ( viewportHeight - edgeSize );
const edgeRight = (viewportWidth - edgeSize);
const isInLeftEdge = ( viewportX < edgeLeft );
const isInRightEdge = ( viewportX > edgeRight );
const isInTopEdge = ( viewportY < edgeTop );
const isInBottomEdge = ( viewportY > edgeBottom );
if (!(isInLeftEdge || isInRightEdge || isInTopEdge || isInBottomEdge)) {
return null;
}
return {
rect,
viewportX,
viewportY,
viewportWidth,
viewportHeight,
isInLeftEdge,
isInTopEdge,
isInRightEdge,
isInBottomEdge,
edgeTop,
edgeLeft,
edgeBottom,
edgeRight
};
}
// Determine if the scroll container has offets which will allow it to be scrolled X or Y
function canContainerBeScrolled (container, viewportWidth, viewportHeight) {
const isBody = isBodyContainer(container);
// Get the document dimensions.
const documentWidth = Math.max(
container.scrollWidth,
container.offsetWidth,
container.clientWidth
);
const documentHeight = Math.max(
container.scrollHeight,
container.offsetHeight,
container.clientHeight
);
// Calculate the maximum scroll offset in each direction. Since you can only
// scroll the overflow portion of the document, the maximum represents the
// length of the document that is NOT in the viewport.
const maxScrollX = (documentWidth - viewportWidth);
const maxScrollY = (documentHeight - viewportHeight);
// Get the current scroll position of the document.
let currentScrollX = container.scrollLeft;
let currentScrollY = container.scrollTop;
if (isBody) {
currentScrollX = window.scrollX;
currentScrollY = window.scrollY;
}
// Determine if the window can be scrolled in any particular direction.
const canScrollUp = (currentScrollY > 0);
const canScrollDown = (currentScrollY < maxScrollY);
const canScrollLeft = (currentScrollX > 0);
const canScrollRight = (currentScrollX < maxScrollX);
return {
documentWidth,
documentHeight,
maxScrollX,
maxScrollY,
currentScrollX,
currentScrollY,
canScroll: (canScrollUp || canScrollDown || canScrollLeft || canScrollRight),
canScrollUp,
canScrollDown,
canScrollLeft,
canScrollRight
};
}
// Determine whether the user is able to scroll based on current edge position and scroll of the container
function canBeScrolledInCurrentDirection (container, edgeSize, edgeParams, scrollParams) {
const {
viewportX,
viewportY,
isInLeftEdge,
isInRightEdge,
isInTopEdge,
isInBottomEdge,
edgeTop,
edgeLeft,
edgeBottom,
edgeRight
} = edgeParams;
const {
maxScrollX,
maxScrollY,
currentScrollX,
currentScrollY,
canScrollUp,
canScrollLeft,
canScrollDown,
canScrollRight
} = scrollParams;
// Since we can potentially scroll in two directions at the same time,
// let's keep track of the next scroll, starting with the current scroll.
// Each of these values can then be adjusted independently in the logic
// below.
let nextScrollX = currentScrollX;
let nextScrollY = currentScrollY;
// As we examine the mouse position within the edge, we want to make the
// incremental scroll changes more "intense" the closer that the user
// gets the viewport edge. As such, we'll calculate the percentage that
// the user has made it "through the edge" when calculating the delta.
// Then, that use that percentage to back-off from the "max" step value.
const maxStep = 50;
// Should we scroll left?
if (isInLeftEdge && canScrollLeft) {
const intensity = ((edgeLeft - viewportX) / edgeSize);
nextScrollX = (nextScrollX - (maxStep * intensity));
}
// Should we scroll right?
else if (isInRightEdge && canScrollRight) {
const intensity = ((viewportX - edgeRight) / edgeSize);
nextScrollX = (nextScrollX + (maxStep * intensity));
}
// Should we scroll up?
if (isInTopEdge && canScrollUp) {
const intensity = ((edgeTop - viewportY) / edgeSize);
nextScrollY = (nextScrollY - (maxStep * intensity));
}
// Should we scroll down?
else if (isInBottomEdge && canScrollDown) {
const intensity = ((viewportY - edgeBottom) / edgeSize);
nextScrollY = (nextScrollY + (maxStep * intensity));
}
// Sanitize invalid maximums. An invalid scroll offset won't break the
// subsequent .scrollTo() call; however, it will make it harder to
// determine if the .scrollTo() method should have been called in the
// first place.
nextScrollX = Math.max(0, Math.min(maxScrollX, nextScrollX));
nextScrollY = Math.max(0, Math.min(maxScrollY, nextScrollY));
if ((nextScrollX !== currentScrollX) || (nextScrollY !== currentScrollY)) {
return { nextScrollX, nextScrollY };
}
return null;
}
/** Main function to determine whether a node can be scrolled based on current cursor pos and container edge + scroll pos **/
function isContainerReadyToEdgeScroll (container, clientX, clientY, edgeSize) {
// Check that the user's cursor is currently within an edge of this scrollable container
const edgeParams = isInEdge(container, clientX, clientY, edgeSize);
if (!edgeParams) {
return false;
}
// Check that the scrollable container still has remaining X or Y scroll
const { viewportWidth, viewportHeight } = edgeParams;
const scrollParams = canContainerBeScrolled(container, viewportWidth, viewportHeight);
if (!scrollParams.canScroll) {
return false;
}
// Check that the current cursor position sits within an edge and has remaining scroll to go
const canScrollInCurrDirection = canBeScrolledInCurrentDirection(container, edgeSize, edgeParams, scrollParams);
return !!canScrollInCurrDirection;
}
/** Main function for performing scroll action **/
function performEdgeScroll (container, clientX, clientY, edgeSize) {
if (!container || !edgeSize) {
cancelScrollAction();
return false;
}
// NOTE: Much of the information here, with regard to document dimensions,
// viewport dimensions, and window scrolling is derived from JavaScript.info.
// I am consuming it here primarily as NOTE TO SELF.
// --
// Read More: https://javascript.info/size-and-scroll-window
// --
// CAUTION: The viewport and document dimensions can all be CACHED and then
// recalculated on window-resize events (for the most part). I am keeping it
// all here in the mousemove event handler to remove as many of the moving
// parts as possible and keep the demo as simple as possible.
// If the mouse is not in the viewport edge, there's no need to calculate
// anything else.
const edgeParams = isInEdge(container, clientX, clientY, edgeSize);
if (!edgeParams) {
cancelScrollAction();
return false;
}
const { viewportWidth, viewportHeight } = edgeParams;
// If we made it this far, the user's mouse is located within the edge of the
// viewport. As such, we need to check to see if scrolling needs to be done.
// As we examine the mousemove event, we want to adjust the window scroll in
// immediate response to the event; but, we also want to continue adjusting
// the window scroll if the user rests their mouse in the edge boundary. To
// do this, we'll invoke the adjustment logic immediately. Then, we'll setup
// a timer that continues to invoke the adjustment logic while the window can
// still be scrolled in a particular direction.
(function checkForWindowScroll () {
cancelScrollAction();
if (adjustWindowScroll()) {
timer = setTimeout( checkForWindowScroll, 30 );
}
})();
// Adjust the window scroll based on the user's mouse position. Returns True
// or False depending on whether or not the window scroll was changed.
function adjustWindowScroll () {
const scrollParams = canContainerBeScrolled(container, viewportWidth, viewportHeight);
const nextScrollParams = canBeScrolledInCurrentDirection(container, edgeSize, edgeParams, scrollParams);
if (nextScrollParams) {
const { nextScrollX, nextScrollY } = nextScrollParams;
(isBodyContainer(container) ? window : container).scrollTo(nextScrollX, nextScrollY);
return true;
}
return false;
}
return true;
}
var DragMixin = {
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);
}
};
var script$4 = {
name: 'Drag',
mixins: [DragMixin],
props: {
/**
* Tag to be used as root of this component. Defaults to div.
*/
tag: {
type: [String, Object, Function],
default: 'div'
}
},
computed: {
dynamicSlots () {
return Object.entries(this.$slots).filter(([key]) => key !== 'drag-image' && key !== 'default');
}
}
};
const _hoisted_1$2 = {
key: 0,
ref: "drag-image",
class: "__drag-image"
};
function render$3(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createBlock(resolveDynamicComponent($props.tag), {
class: normalizeClass(_ctx.cssClasses)
}, createSlots({
default: withCtx(() => [
renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(_ctx.$slots['default'] || {}))),
(_ctx.dragInitialised)
? (openBlock(), createElementBlock("div", _hoisted_1$2, [
renderSlot(_ctx.$slots, "drag-image")
], 512 /* NEED_PATCH */))
: createCommentVNode("v-if", true)
]),
_: 2 /* DYNAMIC */
}, [
renderList($options.dynamicSlots, ([slot, args]) => {
return {
name: slot,
fn: withCtx(() => [
renderSlot(_ctx.$slots, slot, normalizeProps(guardReactiveProps(args)))
])
}
})
]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["class"]))
}
script$4.render = render$3;
script$4.__scopeId = "data-v-f87407ce";
function dropAllowed (inst) {
if (inst.dragInProgress && inst.typeAllowed) {
return inst.compatibleMode && inst.effectiveAcceptsData(inst.dragData, inst.dragType);
}
return null;
}
function doDrop (inst, event) {
inst.$emit('drop', event);
event.source.$emit(inst.mode, event);
}
function candidate (inst, type) {
return inst.effectiveAcceptsType(type);
}
var DropMixin = {
mixins: [DragAwareMixin],
props: {
acceptsType: {
type: [String, Array, Function],
default: null
},
acceptsData: {
type: Function,
default: () => {
return true;
}
},
mode: {
type: String,
default: 'copy'
},
dragImageOpacity: {
type: Number,
default: 0.7
}
},
emits: ['dragover', 'dragenter', 'dragleave', 'dragend', 'drop'],
data () {
return {
isDrop: true
};
},
computed: {
compatibleMode () {
return this.dragInProgress ? true : null;
},
dropIn () {
if (this.dragInProgress) {
return this.dragTop === this;
}
return null;
},
typeAllowed () {
if (this.dragInProgress) {
return this.effectiveAcceptsType(this.dragType);
}
return null;
},
dropAllowed () {
return dropAllowed(this);
},
cssClasses () {
const clazz = {
'dnd-drop': true
};
if (this.dropIn !== null) {
clazz['drop-in'] = this.dropIn;
clazz['drop-out'] = !this.dropIn;
}
if (this.typeAllowed !== null) {
clazz['type-allowed'] = this.typeAllowed;
clazz['type-forbidden'] = !this.typeAllowed;
}
if (this.dropAllowed !== null) {
clazz['drop-allowed'] = this.dropAllowed;
clazz['drop-forbidden'] = !this.dropAllowed;
}
return clazz;
}
},
methods: {
effectiveAcceptsType (type) {
if (this.acceptsType === null) {
return true;
}
else if (typeof (this.acceptsType) === 'string' || typeof(this.acceptsType) === 'number') {
return this.acceptsType === type;
}
else if (typeof (this.acceptsType) === 'object' && Array.isArray(this.acceptsType)) {
return this.acceptsType.includes(type);
}
else {
return this.acceptsType(type);
}
},
effectiveAcceptsData (data, type) {
return this.acceptsData(data, type);
},
onDragPositionChanged (event) {
if (this === event.top) {
this.$emit('dragover', event);
}
},
onDragTopChanged (event) {
if (this === event.top) {
this.$emit('dragenter', event);
}
if (this === event.previousTop) {
this.$emit('dragleave', event);
}
},
onDragEnd (event) {
if (this === event.top) {
this.$emit('dragend', event);
}
},
onDrop (event) {
if (this.dropIn && this.compatibleMode && this.dropAllowed) {
this.doDrop(event);
}
},
doDrop (event) {
doDrop(this, event);
},
/**
* Returns true if the current drop area participates in the current drag operation.
*/
candidate (type) {
return candidate(this, type);
},
createDragImage () {
let image = 'source';
if (this.$refs['drag-image']) {
const el = this.$refs['drag-image'];
if (el.childElementCount !== 1) {
image = createDragImage(el);
}
else {
image = createDragImage(el.children.item(0));
}
image['__opacity'] = this.dragImageOpacity;
image.classList.add('dnd-ghost');
}
return image;
},
onDnDMove (e) {
dnd.mouseMove(e, this);
}
},
created () {
dnd.on('dragpositionchanged', this.onDragPositionChanged);
dnd.on('dragtopchanged', this.onDragTopChanged);
dnd.on('drop', this.onDrop);
dnd.on('dragend', this.onDragEnd);
},
mounted () {
this.$el.addEventListener('easy-dnd-move', this.onDnDMove);
},
beforeUnmount () {
this.$el.removeEventListener('easy-dnd-move', this.onDnDMove);
dnd.off('dragpositionchanged', this.onDragPositionChanged);
dnd.off('dragtopchanged', this.onDragTopChanged);
dnd.off('drop', this.onDrop);
dnd.off('dragend', this.onDragEnd);
}
};
var script$3 = {
name: 'Drop',
mixins: [DropMixin],
props: {
tag: {
type: [String, Object, Function],
default: 'div'
}
},
computed: {
dynamicSlots () {
return Object.entries(this.$slots).filter(([key]) => key !== 'drag-image' && key !== 'default');
},
showDragImage () {
return this.dragInProgress && this.typeAllowed && !!this.$slots['drag-image'];
}
}
};
const _hoisted_1$1 = {
key: 0,
ref: "drag-image",
class: "__drag-image"
};
function render$2(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createBlock(resolveDynamicComponent($props.tag), {
class: normalizeClass(_ctx.cssClasses)
}, createSlots({
default: withCtx(() => [
renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(_ctx.$slots['default'] || {}))),
($options.showDragImage)
? (openBlock(), createElementBlock("div", _hoisted_1$1, [
renderSlot(_ctx.$slots, "drag-image", {
type: _ctx.dragType,
data: _ctx.dragData
})
], 512 /* NEED_PATCH */))
: createCommentVNode("v-if", true)
]),
_: 2 /* DYNAMIC */
}, [
renderList($options.dynamicSlots, ([slot, args]) => {
return {
name: slot,
fn: withCtx(() => [
renderSlot(_ctx.$slots, slot, normalizeProps(guardReactiveProps(args)))
])
}
})
]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["class"]))
}
script$3.render = render$2;
script$3.__scopeId = "data-v-12a39e52";
var script$2 = {
name: 'DropMask',
mixins: [DragAwareMixin],
props: {
tag: {
type: [String, Object, Function],
default: 'div'
}
},
data () {
return {
isDropMask: true
};
},
mounted () {
this.$el.addEventListener('easy-dnd-move', this.onDndMove);
},
beforeUnmount () {
this.$el.removeEventListener('easy-dnd-move', this.onDndMove);
},
methods: {
createDragImage () {
return 'source';
},
onDndMove (e) {
dnd.mouseMove(e, this);
}
}
};
function render$1(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createBlock(resolveDynamicComponent($props.tag), null, createSlots({ _: 2 /* DYNAMIC */ }, [
renderList(_ctx.$slots, (args, slot) => {
return {
name: slot,
fn: withCtx(() => [
renderSlot(_ctx.$slots, slot, normalizeProps(guardReactiveProps(args)))
])
}
})
]), 1024 /* DYNAMIC_SLOTS */))
}
script$2.render = render$1;
var script$1 = {
name: 'DragFeedback'
};
const _hoisted_1 = { class: "DragFeedback" };
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createElementBlock("div", _hoisted_1, [
renderSlot(_ctx.$slots, "default")
]))
}
script$1.render = render;
class Grid {
reference;
referenceOriginalPosition;
magnets = [];
constructor (collection, upToIndex, direction, fromIndex) {
this.reference = collection.item(0).parentNode;
this.referenceOriginalPosition = {
x: this.reference.getBoundingClientRect().left - this.reference.scrollLeft,
y: this.reference.getBoundingClientRect().top - this.reference.scrollTop,
};
let index = 0;
for (const child of collection) {
if (index > upToIndex) break;
const rect = child.getBoundingClientRect();
const hasNestedDrop = child.classList.contains('dnd-drop') || child.getElementsByClassName('dnd-drop').length > 0;
let horizontal = false;
if (hasNestedDrop) {
if (direction === 'auto') {
// Auto mode not supported for now. Row or column must be defined explicitly if there are nested drop lists.
throw 'Easy-DnD error : a drop list is missing one of these attributes : \'row\' or \'column\'.';
}
else {
horizontal = direction === 'row';
}
}
if (fromIndex === null) {
// Inserting mode.
this.magnets.push(hasNestedDrop ? this.before(rect, horizontal) : this.center(rect));
}
else {
// Reordering mode.
this.magnets.push(hasNestedDrop ? (
fromIndex < index ? this.after : this.before
)(rect, horizontal) : this.center(rect));
}
// Debug : show magnets :
//document.body.insertAdjacentHTML("beforeend", "<div style='background-color: red; position: fixed; width: 1px; height: 1px; top:" + this.magnets[index].y + "px; left:" + this.magnets[index].x + "px;' ></div>")
index++;
}
}
/**
* Returns the center of the rectangle.
*/
center (rect) {
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
/**
* When horizontal is true / false, returns middle of the left / top side of the rectangle.
*/
before (rect, horizontal) {
return horizontal ? {
x: rect.left,
y: rect.top + rect.height / 2
} : {
x: rect.left + rect.width / 2,
y: rect.top
};
}
/**
* When horizontal is true / false, returns middle of the right / bottom side of the rectangle.
*/
after (rect, horizontal) {
return horizontal ? {
x: rect.left + rect.width,
y: rect.top + rect.height / 2
} : {
x: rect.left + rect.width / 2,
y: rect.top + rect.height
};
}
/**
* In case the user scrolls during the drag, the position of the magnets are not what they used to be when the drag
* started. A correction must be applied that takes into account the amount of scroll. This correction is the
* difference between the current position of the parent element and its position when the drag started.
*/
correction () {
return {
x: this.reference.getBoundingClientRect().left - this.reference.scrollLeft - this.referenceOriginalPosition.x,
y: this.reference.getBoundingClientRect().top - this.reference.scrollTop - this.referenceOriginalPosition.y,
};
}
closestIndex (position) {
const x = position.x - this.correction().x;
const y = position.y - this.correction().y;
let minDist = 999999;
let index = -1;
for (let i = 0; i < this.magnets.length; i++) {
const magnet = this.magnets[i];
const dist = Math.sqrt(Math.pow(magnet.x - x, 2) + Math.pow(magnet.y - y, 2));
if (dist < minDist) {
minDist = dist;
index = i;
}
}
return index;
}
}
class DnDEvent {
type;
data;
top;
previousTop;
source;
position;
success;
native;
}
class ReorderEvent {
from;
to;
constructor (from, to) {
this.from = from;
this.to = to;
}
apply (array) {
const temp = array[this.from];
array.splice(this.from, 1);
array.splice(this.to, 0, temp);
}
}
class InsertEvent {
type;
data;
index;
constructor (type, data, index) {
this.type = type;
this.data = data;
this.index = index;
}
}
var script = {
name: 'DropList',
mixins: [DropMixin],
props: {
tag: {
type: [String, Object, Function],
default: 'div'
},
items: {
type: Array,
required: true
},
row: {
type: Boolean,
default: false
},
column: {
type: Boolean,
default: false
},
noAnimations: {
type: Boolean,
default: false
},
scrollingEdgeSize: {
type: Number,
default: undefined
}
},
emits: ['reorder', 'insert'],
data () {
return {
grid: null,
forbiddenKeys: [],
feedbackKey: null,
fromIndex: null
};
},
computed: {
rootTag () {
if (this.noAnimations) {
return this.tag;
}
return TransitionGroup;
},
rootProps () {
if (this.noAnimations) {
return {};
}
return {
tag: this.tag,
css: false
};
},
direction () {
// todo - rewrite this logic
if (this.row) return 'row';
if (this.column) return 'column';
return 'auto';
},
reordering () {
if (dnd.inProgress) {
return dnd.source.$el.parentElement === this.$el;
}
return null;
},
closestIndex () {
if (this.grid) {
return this.grid.closestIndex(dnd.position);
}
return null;
},
dropAllowed () {
if (this.dragInProgress) {
if (this.reordering) {
return this.items.length > 1;
}
else {
// todo - eventually refactor so that this isn't necessary
if (!dropAllowed(this)) {
return false;
}
if (this.forbiddenKeys !== null && this.feedbackKey !== null) {
return !this.forbiddenKeys.includes(this.feedbackKey);
}
return true;
}
}
return null;
},
itemsBeforeFeedback () {
if (this.closestIndex === 0) {
return [];
}
return this.items.slice(0, this.closestIndex);
},
itemsAfterFeedback () {
if (this.closestIndex === this.items.length) {
return [];
}
return this.items.slice(this.closestIndex);
},
itemsBeforeReorderingFeedback () {
if (this.closestIndex <= this.fromIndex) {
return this.items.slice(0, this.closestIndex);
}
return this.items.slice(0, this.closestIndex + 1);
},
itemsAfterReorderingFeedback () {
if (this.closestIndex <= this.fromIndex) {
return this.items.slice(this.closestIndex);
}
return this.items.slice(this.closestIndex + 1);
},
reorderedItems () {
const toIndex = this.closestIndex;
const reordered = [...this.items];
const temp = reordered[this.fromIndex];
reordered.splice(this.fromIndex, 1);
reordered.splice(toIndex, 0, temp);
return reordered;
},
clazz () {
return {
'drop-list': true,
'reordering': this.reordering === true,
'inserting': this.reordering === false,
...(this.reordering === false ? this.cssClasses : { 'dnd-drop': true })
};
},
showDragFeedback () {
return this.dragInProgress && this.typeAllowed && !this.reordering;
},
showInsertingDragImage () {
return this.dragInProgress && this.typeAllowed && !this.reordering && !!this.$slots['drag-image'];
},
showReorderingDragImage () {
return this.dragInProgress && this.reordering && !!this.$slots['reordering-drag-image'];
},
hasReorderingFeedback () {
return !!this.$slots['reordering-feedback'];
},
hasEmptySlot () {
return !!this.$slots['empty'];
}
},
created () {
dnd.on('dragstart', this.onDragStart);
dnd.on('dragend', this.onDragEnd);
},
beforeUnmount () {
dnd.off('dragstart', this.onDragStart);
dnd.off('dragend', this.onDragEnd);
},
methods: {
// Presence of feedback node in the DOM and of keys in the virtual DOM required => delayed until what
// depends on drag data has been processed.
refresh () {
this.$nextTick(() => {
this.grid = this.computeInsertingGrid();
this.feedbackKey = this.computeFeedbackKey();
this.forbiddenKeys = this.computeForbiddenKeys();
});
},
onDragStart (event) {
if (this.candidate(dnd.type)) {
if (this.reordering) {
this.fromIndex = Array.prototype.indexOf.call(event.source.$el.parentElement.children, event.source.$el);
this.grid = this.computeReorderingGrid();
}
else {
this.refresh();
}
}
},
onDragEnd () {
this.fromIndex = null;
this.feedbackKey = null;
this.forbiddenKeys = null;
this.grid = null;
},
doDrop (event) {
if (this.reordering) {
if (this.fromIndex !== this.closestIndex) {
this.$emit('reorder', new ReorderEvent(
this.fromIndex,
this.closestIndex
));
}
}
else {
// todo - eventually remove the need for this
doDrop(this, event);
this.$emit('insert', new InsertEvent(
event.type,
event.data,
this.closestIndex
));
}
},
candidate (type) {
return candidate(this, type) || this.reordering;
},
computeForbiddenKeys () {
return (this.noAnimations ? [] : this.$refs.component.$slots['default']())
.map(vn => vn.key)
.filter(k => !!k && k !== 'drag-image' && k !== 'drag-feedback');
},
computeFeedbackKey () {
return this.$refs['feedback']['$slots']['default']()[0]['key'];
},
computeInsertingGrid () {
if (this.$refs.feedback.$el.children.length < 1) {
return null;
}
const feedback = this.$refs.feedback.$el.children[0];
const clone = feedback.cloneNode(true);
const tg = this.$el;
if (tg.children.length > this.items.length) {
tg.insertBefore(clone, tg.children[this.items.length]);
}
else {
tg.appendChild(clone);
}
const grid = new Grid(tg.children, this.items.length, this.direction, null);
tg.removeChild(clone);
return grid;
},
computeReorderingGrid () {
return new Grid(this.$el.children, this.items.length - 1, this.direction, this.fromIndex);
},
createDragImage () {
let image;
if (this.$refs['drag-image']) {
const el = this.$refs['drag-image'];
let model;
if (el.childElementCount !== 1) {
model = el;
}
else {
model = el.children.item(0);
}
const clone = model.cloneNode(true);
const tg = this.$el;
tg.appendChild(clone);
image = createDragImage(clone);
tg.removeChild(clone);
image['__opacity'] = this.dragImageOpacity;
image.classList.add('dnd-ghost');
}
else {
image = 'source';
}
return image;
}
},
render () {
if (!this.$slots['item']) {
throw 'The "Item" slot must be defined to use Drop