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)
@drag,@dragstart,@dragendand@drop.dragenteranddragoverto fetch or snpan to the right position.