I'm trying to have some Items drag'n'droppable. which are inserted within two levels of containers, with a calculated height and top value.
I've tried with HTML' native drag'n'drop, Sortable.JS and other libraries which support Vue without build tools which is also a requirement.
EDIT: I'd highly prefer if dragging an item snaps to the smallContainer(s) elements it passes over, while updating the top variable.
Here's a simplified sandbox:
I've been stuck for over a week now, and would really appreciate any help.
Here's some gifs of my attempts that doesn't fulfill all requirements:
(With Draggable.JS) This does what I want, but because i'm modifying the original element, I can't drag it downwards as the element itself is blocking the dragging, until you pass it.
(With native drag'n'drop) - This doesn't update the time, nor allows me to drag to an item that is already occupied by the same element (ie. slightly down)
the solution I came up with is based on creating a ghost element, removing dragged Item from items array.
First of All put itemId and innerContainerId on the item and innerContainer components, and also define eventListeners on item component:
<div
v-for="innerContainer in innerContainers"
:data-innerContainerId="innerContainer.id"
class="innerContainersStyle drop-zone"
>
for item:
<Item
class="itemStyle"
:item="item"
:data-itemId="item.id"
@dragstart="dragStart"
@drag="dragMove"
v-for="item in getItemsFromInnerContainer(innerContainer.id)"
>{{item.name}}</Item>
declaring functions:
const dragStart = (event) => {
event.preventDefault();
//find Item in items and save
this.draggingItem = this.items.find(i => { i.id == event.target.dataset.itemId})
//remove item from initial container:
this.items.splice(1, this.items.indexOf(this.draggingItem))
//make the ghost elem: (big father is a parent element that can safely be relatively positioned)
const bigFather = document.querySelector('body')
const newDiv = `${event.target.outerHTML}`
bigFather.style.position = 'relative'
bigFather.insertAdjacentHTML('beforeend', newDiv)
let element = bigFather.lastElementChild;
// set the ghost element id to easily remove it on drop:
element.id = 'ghost-element'
setDraggingElStyle(element, event)
}
Set ghost element style:
const setDraggingElStyle = (element, event) => {
const targetRect = event.target.getBoundingClientRect()
const translateLeft = `${event.clientX - targetRect.left}px`
const translateTop = `${event.clientY - targetRect.top}px`
element.style.position = 'absolute';
element.style.width = `${window.getComputedStyle(event.target).width}`
element.style.height = `${window.getComputedStyle(event.target).height}`
//you can set ghost element opacity here:
element.style.opacity = `0.5`
element.style.margin = '0 !important'
// set the ghost element background image, also you can set its opacity through rgba
element.style.backgroundImage = 'linear-gradient( rgba(0,0,0,.5) , rgba(0,0,0,.2) , rgba(0,0,0,.5) ) '
element.dir = 'rtl'
element.style.transform = `translate(-${translateLeft}, -${translateTop})`
setTopLeft(event, element)
}
this function moves the ghost element anywhere you drag it:
const setTopLeft = (event, ghostElement) => {
if (!ghostElement) ghostElement = document.querySelector('#ghost-element')
let touchX = event.clientX
let touchY = event.clientY
try {
element.style.top = `${ window.scrollY + touchY}px`
element.style.left = `${window.scrollX + touchX}px`
} catch(err) {
console.log(element)
}
}
It has to update ghost elem top and left on drag:
const dragMove = (event, element) => setTopLeft(event, element)
const dragEnd = (event, cb) => {
let targetEl;
// targetEl is set to be the destination innerContainer
targetEl = document.elementFromPoint(event.clientX, event.clientY);
const isDropZone = returnRealDropZone(targetEl, 2)
if (!isDropZone) return
targetEl = returnRealDropZone(targetEl, 2)
let targetContainerId = targetEl.dataset.innerContainerId
this.draggingItem.innerContainer = targetContainerId
// by pushing dragging item into items array (of course with the new innerContainer ID) it will be
// shown in the new container items.
this.items.push(this.draggingItem)
// you may want to prevent new created item from being created at the end of target container array.
// I suggest you index the container's children to put this item exactly where you want
//delete ghost element:
document.querySelector(`#ghost-element`).remove()
if (!targetEl ) return console.log('there is no targetEl')
cb(targetEl, event)
}
function returnRealDropZone(element, parentsCount) {
if (element.classList.contains('drop-zone')) return element
else if (parentsCount > 0 ) {
return returnRealDropZone(element.parentElement, parentsCount-1)
} else {
return false
}
}
UPDATE
here is code sandbox fit to your simplified project: (Also changed the Item.js and put itemStyle in computed properties)
computed: {
itemStyle() {
return {
width: this.getWidth() + "px",
height: this.calcHeight() + "px",
top: this.calcTop() + "px"
};
}
},
you can see codesanbox that works for me:
https://codesandbox.io/s/gifted-roentgen-pxyhdf?file=/Item.js
UPDATE 2
I can see that my solution won't work on Firefox; that's because Firefox returns DragEvent.clientX = 0. To fix that, I have an idea which I'm pretty sure is not an economic and clean way! On Mounted hook, you can get the mouse position from dragover event (on window) and set it on component properties (this.mouseX and this.mouseY)
mounted() {
//get mouse coordinations for firefox
//(because firefox doesnt get DragEvent.clientX and DragEvent.clientY properly)
window.addEventListener('dragover', event => {
this.mouseX = event.x
this.mouseY = event.y
})
}
Then, you can use this properties in setTopLeft function in case the browser (firefox) return DragEvent.clientX = 0.
setTopLeft(event) {
let touchX = event.clientX || this.mouseX ;
let touchY = event.clientY || this.mouseY ;
.
.
.
}
(I also edited codesandbox)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With