UNPKG

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,472 lines (1,346 loc) 66.9 kB
var VueEasyDnD = (function (exports, vue) { 'use strict'; 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 = vue.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 (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent($props.tag), { class: vue.normalizeClass(_ctx.cssClasses) }, vue.createSlots({ default: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, "default", vue.normalizeProps(vue.guardReactiveProps(_ctx.$slots['default'] || {}))), (_ctx.dragInitialised) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$2, [ vue.renderSlot(_ctx.$slots, "drag-image") ], 512 /* NEED_PATCH */)) : vue.createCommentVNode("v-if", true) ]), _: 2 /* DYNAMIC */ }, [ vue.renderList($options.dynamicSlots, ([slot, args]) => { return { name: slot, fn: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, slot, vue.normalizeProps(vue.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 (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent($props.tag), { class: vue.normalizeClass(_ctx.cssClasses) }, vue.createSlots({ default: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, "default", vue.normalizeProps(vue.guardReactiveProps(_ctx.$slots['default'] || {}))), ($options.showDragImage) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [ vue.renderSlot(_ctx.$slots, "drag-image", { type: _ctx.dragType, data: _ctx.dragData }) ], 512 /* NEED_PATCH */)) : vue.createCommentVNode("v-if", true) ]), _: 2 /* DYNAMIC */ }, [ vue.renderList($options.dynamicSlots, ([slot, args]) => { return { name: slot, fn: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, slot, vue.normalizeProps(vue.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 (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent($props.tag), null, vue.createSlots({ _: 2 /* DYNAMIC */ }, [ vue.renderList(_ctx.$slots, (args, slot) => { return { name: slot, fn: vue.withCtx(() => [ vue.renderSlot(_ctx.$slots, slot, vue.normalizeProps(vue.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 (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [ vue.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 vue.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) {