How to restrict dragging and dropping of elements to specific drop zones

Viewed 40

How can I prevent items with a specific identifier from being dragged and then dropped into restricted drop zones?

How would I do this in Vue.js or plain JavaScript?

My HTML code looks like this:

    <div draggable="true" id="type-one">Item 1, Type 1</div>  
    <div draggable="true" id="type-one">Item 2, Type 1</div>  

    <div draggable="true" id="type-two">Item 1, Type 2</div>
    <div draggable="true" id="type-two">Item 2, Type 2</div>

    <div id="drop-zone-one">
        <!--Only items with id="type-one" are allowed-->
        <!--Lock zone and add visual cue when dragging id="type-two"-->
    </div>

    <div id="drop-zone-two">
        <!--Only items with id="type-two" are allowed-->
        <!--Lock zone and add visual cue when dragging id="type-one"-->
    </div>

My Vue code looks something like this:

    <div v-for="(item, index) in itemsListOne" id="item.id">
        <div draggable=""true">{{ item.name }}</div>
    </div>
    <div v-for="(item, index) in itemsListTwo" id="item.id">
        <div draggable=""true">{{ item.name }}</div>
    </div>
    <div id="drop-zone-one">
        <!--dropped items-->
    </div>
    <div id="drop-zone-two">
        <!--dropped items-->
    </div>

I don't want to use any libraries like Sortable, Draggable, or similar.

1 Answers

PURE JAVASCRIPT APPROACH

The important part of the solution is capturing the id of the dragged element in dragStart with:
event.dataTransfer.setData("draggedItem", event.target.id);

This id then needs to be recovered in the handleDrop function with:
const draggedItemId = event.dataTransfer.getData("draggedItem"); for example.

I am using the text of the id of the dropZones as classes on the drag items to identify to which drop zone a particular drag item is allowed to be dropped into. However, there will be many other ways to implement that particular logic.

An important line of code to understand is:
dropZone.appendChild(draggedItem);

The above line appends the draggedItem to the child list of the destination container and simultaneously removes the draggedItem element from its current container. This is important as there is no need to manipulate nodelists or arrays of elements.

In addition to moving items to the pink and orange containers, I have added a yellow item that cannot leave the yellow container.

In the code snippet, simply drag the items from one container to another.

const dragitems = document.querySelectorAll(".drag-item");

dragitems.forEach((dragitem) => {
  dragitem.addEventListener("dragstart", dragStart);
});

function dragStart(event) {
  event.dataTransfer.setData("draggedItem", event.target.id);
}

let dropzones = document.querySelectorAll(".drop-zone");
dropzones.forEach(function(dropzone) {
  dropzone.addEventListener("dragover", handleDragOver, false);
  dropzone.addEventListener("drop", handleDrop, false);
});

function handleDragOver(event) {
  event.preventDefault();
}

function handleDrop(event) {
  const draggedItemId = event.dataTransfer.getData("draggedItem");
  const draggedItem = document.getElementById(draggedItemId);
  const dropZone = this;

  if (draggedItem.classList.contains(dropZone.id) || dropZone.id === "drop-zone-zero") {
    dropZone.appendChild(draggedItem);
  }
}
.drop-zone {
  border: solid 2px black;
  height: 200px;
  width: 200px;
  padding: 10px;
}

.drag-item {
  width: 200px;
  border: solid 2px black;
}

.container {
  display: flex;
  flex-direction: row;
}

.drop-zone-one,
#drop-zone-one {
  background: pink;
}

.drop-zone-two,
#drop-zone-two {
  background: orange;
}

.drop-zone-zero,
#drop-zone-zero {
  background: yellow;
}
<div class="container">
  <div class="drop-zone" id="drop-zone-zero">
    <div class="drag-item drop-zone-one" draggable="true" id="1">Item 1, Type 1<br>(drag to pink or yellow)</div>
    <div class="drag-item drop-zone-one" draggable="true" id="2">Item 2, Type 1<br>(drag to pink or yellow)</div>
    <div class="drag-item drop-zone-two" draggable="true" id="3">Item 1, Type 2<br>(drag to orange or yellow)</div>
    <div class="drag-item drop-zone-two" draggable="true" id="4">Item 2, Type 2<br>(drag to orange or yellow)</div>
    <div class="drag-item drop-zone-zero" draggable="true" id="5">Item 1, Type 0<br>(cannot leave yellow)</div>
  </div>
  <div class="drop-zone" id="drop-zone-one">
  </div>
  <div class="drop-zone" id="drop-zone-two">
  </div>
</div>

VUE.JS APPROACH

This problem is very interesting because, on the surface, it appears that the JavaScript approach is easier and more elegant than the Vue.js approach. Thanks to the DOM's appendChild function it is possible to move elements from one container to another without having to manipulate arrays or lists.

In Vue.js, we should not manipulate the DOM. Vue does this for us. So, now we have a problem of how to move items from one container to another. Well, the solution is NOT to maintain multiple lists of items, one for each container. The solution is to maintain one list of items, and each item will have an containerId property that identifies the container that it currently belongs to.

In this way, we can use the same logic as used in the Pure JavaScript Approach to identify the item being dragged and simply update the containerId property with the new containerId and Vue will take care of re-rendering the containers and their contents.

In the code snippet, simply drag the items from one container to another.

A difference between the Pure JavaScript approach and the Vue.js approach is ordering of the items in the containers. This has not been addressed here as that was not part of the question.

Vue.createApp({
  data: () => ({
    containers: [
      { id: 0, type: 0 },
      { id: 1, type: 1 },
      { id: 2, type: 2 }
    ],
    items: [
      { id: 0, name: "Item 0, Pink",   type: 1, containerId: 0 },
      { id: 1, name: "Item 1, Yellow", type: 0, containerId: 0 }, 
      { id: 2, name: "Item 2, Orange", type: 2, containerId: 0 },
      { id: 3, name: "Item 3, Orange", type: 2, containerId: 0 }, 
      { id: 4, name: "Item 4, Pink",   type: 1, containerId: 0 }, 
      { id: 5, name: "Item 5, Orange", type: 2, containerId: 0 }, 
      { id: 6, name: "Item 6, Orange", type: 2, containerId: 0 }, 
      { id: 7, name: "Item 7, Pink",   type: 1, containerId: 0 }, 
      { id: 8, name: "Item 8, Yellow", type: 0, containerId: 0 }
    ]
  }),
  methods: {
    dragstart(item, event) {
      event.dataTransfer.setData('draggedItem', item.id);
    },
    dragEnd(containerId, event) {
      let draggedItemId = event.dataTransfer.getData('draggedItem');
      if (this.items[draggedItemId].type === containerId || containerId === 0) {
        this.items[draggedItemId].containerId = containerId;
      }
    },
    dropZoneClass(containerType) { // Only needed to set drop zone colors
      let dropZoneClass = containerType === 2 ? 'drop-zone-two' : containerType === 1 ? 'drop-zone-one' : 'drop-zone-zero';
      return "drop-zone " + dropZoneClass;
    },
    dragItemClass(itemType) { // Only needed to set drag item colors
      let dragItemClass = itemType === 2 ? 'drop-zone-two' : itemType === 1 ? 'drop-zone-one' : 'drop-zone-zero';
      return "drag-item " + dragItemClass;
    }
  }

}).mount('#app')
.drop-zone {
  border: solid 2px black;
  height: 200px;
  width: 200px;
  padding: 10px;
}

.drag-item {
  width: 150px;
  border: solid 2px black;
}

.container {
  display: flex;
  flex-direction: row;
}

.drop-zone-one {
  background: pink;
}

.drop-zone-two {
  background: orange;
}

.drop-zone-zero {
  background: yellow;
}
<script src="https://unpkg.com/vue@3.2.30/dist/vue.global.prod.js"></script>

<div id="app" class="container">
  <div v-for="container in containers">
    <div :class="dropZoneClass(container.type)" @dragover.prevent @drop="dragEnd(container.id, $event)">
      <div v-for="item in items">
        <div v-if="item.containerId === container.id" :class="dragItemClass(item.type)" draggable="true" @dragstart="dragstart(item, $event)">{{ item.name }}</div>
      </div>
    </div>
  </div>
</div>

Related