Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enabling drag'n'drop in Vue, without build tools

Tags:

vue.js

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:

Edit modest-stitch-8r3l8d

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:

  1. (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. 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.

  2. (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) With native Drag'n'Drop

like image 587
Nivyan Avatar asked Oct 18 '25 10:10

Nivyan


1 Answers

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)

like image 157
Reza Seyyedi Avatar answered Oct 21 '25 09:10

Reza Seyyedi