javascriptdrag-and-dropinteract.js

Creating/moving a dropzone while dragging with interact.js


I'm writing an application where I use interact.js for DND(drag and drop). Often the start of a DND interaction causes a change in the page; dropzones changing position, being deleted, and new ones being created.

Creation (main issue)

In the snippet below, you get to initiate new dropzones by hovering on the orange box with the draggable. But they don't activate until you drop the draggable and pick it up again.

// DRAGGABLE
const draggable = document.getElementById("draggable");
let x = 0;
let y = 0;
interact(draggable).draggable({
  autoScroll: false,
  onmove(e) {
    x += e.dx;
    y += e.dy;
    // translate the element
    draggable.style.transform = "translate(" + x + "px, " + y + "px)";
  }
});

// HOVERZONE
const hoverzone = document.getElementById("hoverzone");
setupDropzone(hoverzone, addNewZone);

// ADDED ZONES
function addNewZone() {
  // Make html element
  const node = document.createElement("div");
  node.classList.add("box", "zone");
  const textNode = document.createTextNode("Hi!");
  node.appendChild(textNode);
  document.body.appendChild(node);
  // Ininialize interactable
  setupDropzone(node);
}

// HELPER
function setupDropzone(element, ondragenter = () => {}) {
  interact(element).dropzone({
    accept: "#draggable",
    ondrop(e) {
      element.classList.remove("drag-hover");
    },
    ondragenter(e) {
      element.classList.add("drag-hover");
      ondragenter();
    },
    ondragleave(e) {
      element.classList.remove("drag-hover");
    }
  });
}
body {
  display: flex;
}

.box {
  width: 120px;
  border-radius: 8px;
  padding: 20px;
  margin: 1rem;
  background-color: peru;
  color: white;

  touch-action: none;
  user-select: none;

  box-sizing: border-box;
}

#draggable {
  background-color: #29e;
  z-index: 99;
}

.zone {
  background-color: pink;
}

.drag-hover {
  outline: 3px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/interact.js/1.10.27/interact.min.js"></script>

<div class="box" id="hoverzone">
  Hover draggable on me!
</div>

<div class="box" id="draggable">
  Drag me!
</div>

At some point, I tried using event.interaction.start(...) in new dropzones (like in here) by storing the dragstart event of the draggable, but that just broke things and lead nowhere.

I also found no way to delay the DND process (rect calculation, firing dragactivate on dropzones, etc.) after dragstart, until the new zones initialize.

Do you have either a solution to this with interact.js, or a similar DND library that solves this issue and the issue below elegantly?


Movement (solved but worth mentioning)

I solved the issue of dropzones moving using interact.dynamicDrop(true);. This is unideal as it recalculates rectangles on every dragmove, while the actual movement happens rarely in my case. Is there a way to just trigger recalculation once? Doing something like below complicated my code in way that's not worth it and felt too hacky:

  1. Enable dynamicDrop
  2. Wait for dragmove
  3. Disable dynamicDrop

Solution

  • You can do this with the native HTML Drag and Drop API. As I understand the issue you would like to add a new dropzone each time you start a new drag (or rather hover over the hoverzone).

    In the example the event listener for dragover and drop are on the element #dropzone, and each time another dropzone is added it's a child of the #dropzone. So, there is no need for any "dynamic drop" because the event listener is on the parent element.

    I don't know if it makes sense in your case, but I use the timeStamp property on the dragstart event to "decide" what dropzone should be the target dropzone for this particular drag.

    const draggable = document.getElementById("draggable");
    const hoverzone = document.getElementById("hoverzone");
    const dropzone = document.getElementById("dropzone");
    
    draggable.addEventListener('dragstart', e => {
      e.dataTransfer.setData('text/plain', e.timeStamp);
    });
    
    hoverzone.addEventListener('dragover', e => {
      e.preventDefault();
      let timestamp = e.dataTransfer.getData('text/plain');
      let dropzoneelm = dropzone.querySelector(`div[data-timestamp="${timestamp}"]`);
      if (!dropzoneelm) {
        // Make html element
        const node = document.createElement("div");
        node.dataset.timestamp = timestamp;
        node.classList.add("box", "zone");
        const textNode = document.createTextNode("Hi!");
        node.appendChild(textNode);
        dropzone.appendChild(node);
      }
    });
    
    dropzone.addEventListener('dragover', e => {
      e.preventDefault();
      dropzone.querySelectorAll(`div[data-timestamp]`)
        .forEach(elm => elm.classList.remove('drag-hover'));
      let timestamp = e.dataTransfer.getData('text/plain');
      let dropzoneelm = e.target.closest('.zone');
      if(dropzoneelm && dropzoneelm.dataset.timestamp == timestamp){
        dropzoneelm.classList.add('drag-hover');
      }
    });
    
    dropzone.addEventListener('drop', e => {
      e.preventDefault();
      dropzone.querySelectorAll(`div[data-timestamp]`)
        .forEach(elm => elm.classList.remove('drag-hover'));
      let timestamp = e.dataTransfer.getData('text/plain');
      let dropzoneelm = e.target.closest('.zone');
      if(dropzoneelm && dropzoneelm.dataset.timestamp == timestamp){
        dropzoneelm.innerHTML += `<p>${timestamp} added.</p>`;
      }
    });
    body {
      display: flex;
    }
    
    .box {
      width: 120px;
      border-radius: 8px;
      padding: 20px;
      margin: 1rem;
      background-color: peru;
      color: white;
    
      touch-action: none;
      user-select: none;
    
      box-sizing: border-box;
    }
    
    #draggable {
      background-color: #29e;
      z-index: 99;
    }
    
    .zone {
      background-color: pink;
    }
    
    .drag-hover {
      outline: 3px solid black;
    }
    <div class="box" id="hoverzone">
      Hover draggable on me!
    </div>
    
    <div class="box" id="draggable" draggable="true">
      Drag me!
    </div>
    
    <div id="dropzone"></div>