4

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

7
  • 1
    Did you try using the drag api? You can add events to your item with for example: @drag, @dragstart, @dragend and @drop. Commented Aug 1, 2022 at 10:00
  • I have tried this. The main issue is that the 'ghost image' / event.dataTransfer.setDragImage require a visibile element. I'd highly prefer removing the original while dragging (which isn't supported by the API) - and the transparent image having a full opacity. So that it looks like you're dragging the actual element (for the end-user). Finally, the elements doesn't snap - which would be nice to have as well. Commented Aug 1, 2022 at 10:03
  • There is a hacky solution for this, see this answer. Then you can update the x y cords of your dragged element to simulate what draggable JS does and still use stuff like dragenter and dragover to fetch or snpan to the right position. Commented Aug 1, 2022 at 11:11
  • Or you can do what shopify Draggable does. A bit cleaner than my previous suggestion Commented Aug 1, 2022 at 11:16
  • @S.Visser Correct. This can be replicated similar to gif #1 - however, then I'm unable to drag an item to a container it itself is blocking - because the element remains in the DOM. I haven't tried that exact same solution which Draggable uses. I'll try to see if it works. Commented Aug 1, 2022 at 11:18

1 Answer 1

2
+50

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)

Sign up to request clarification or add additional context in comments.

5 Comments

I fail to be able to implement this in Vue. I've tried to make some changes to fit - or just paste it directly, but neither seems to work. Could I convince you to share a codesandbox with your changes?
sure, I will fit it into your sample code and edit my answer in the next few hours.
@Nivyan, Just updated my answer with a codesandbox, let me know if it works for your original project too.
Hey, sorry for responding so late. Real life happened. Your solution is perfect! ...except it doesn't work on Firefox :'( I'll try and implement your work, thank you so much! If you happen to know of a work-around for Firefox, I'd appreciate the extra work. But I'll mark your answer as accepted :-)
You're welcome, hope your real life thing goes well :-) , I updated my answer again and added a not-so-pretty solution for Firefox!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.