Javascript Drag and Drop Hover Temporary Div Placeholder similar to Trello?

Viewed 3720

I have a basic drag and drop trello-like kanban board. You can drag tasks between different grey boxes. It uses HTML drag and drop API found here https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API.

var dropTarget = document.querySelector(".drop-target");
var draggables = document.querySelectorAll(".drag-task");

// Tells the other side what data is being passed (e.g. the ID is targeted)
draggables.forEach(item => {
  item.addEventListener("dragstart", function(ev){
    ev.dataTransfer.setData("srcId", ev.target.id);
  });
})
// The end destination, prevent browsers default drag and drop (disabling breaks feature)
// because it's disabled by browsers by default
dropTarget.addEventListener('dragover', function(ev) {
  ev.preventDefault();
});
// End destination where item is dropped into
dropTarget.addEventListener('drop', function(ev) {
  ev.preventDefault();
  let target = ev.target;
  let droppable  = target.classList.contains('drag-box');
  let srcId = ev.dataTransfer.getData("srcId");

  if (droppable) {
    ev.target.appendChild(document.getElementById(srcId));
  }
});
/***********DRAGGABLE BACKGROUND ****************/
.drag-box {
  background-color: lightgray;
  float: right;
  width: 120px;
  min-height: 50px;
  padding-bottom: 30px;
  height: auto;
  margin: 30px;
}
.drag-task {
  background-color: white;
  margin: 15px;
}
.drop-active {
  border: 1px dashed red;
}
<div class="drop-target">
    <div class="drag-box">
      <div class="drag-card">
        <div draggable="true" id="task1" class="drag-task">Test Card 1</div>
      </div>
      <div class="drag-card">
        <div draggable="true" id="task2" class="drag-task">Test Card 2</div>
      </div>
      <div class="drag-card">
        <div draggable="true" id="task3" class="drag-task">Test Card 3</div>
      </div>
    </div>
    <div class="drag-box">
    </div>
    <div class="drag-box">
    </div>
  </div>

What I want to do to achieve is an effect similar to this gif found here. This creates another <div> element on the same level as drag-card class on a draghover effect, and repositions itself accordingly.

enter image description here

I know I have to use dragover and dragleave event listeners but that's as far as I got. I added this code at the end of the file. I have never used drag event listeners so this is new to me.

var makeHoverElement= true;
dropTarget.addEventListener("dragover", function(ev){
  if(makeHoverElement){
    let newNode =document.createElement('div');
    newNode.className ='drop-active'
    ev.target.parentElement.prepend(newNode);
    makeHoverElement = false;
  }
});

dropTarget.addEventListener("dragleave", function(ev){
   // really I have no idea how to make this effect
});

Results so far have not turned out as I expected. Dragover is applying to element where the task item originated from

enter image description here

2 Answers

The problem is in ev.target.parentElement.prepend(newNode); Your ev.target is still a child of the node you are dragging it from. That's why the dotted border div gets added to the 'old' box. I suggest that in your 'dragover' function you explicitly find the element the mouse is over and add your newNode to it. For example, you can select it by document.querySelector(":hover" ) or try to handle 'mouseover' events there.

As for the 'dragleave' effect, I suggest you clone your ev.target with Node.cloneNode() method and append the clone to the ev.target.parentElement using Node.insertBefore(). MDN on .insertBefore()

Using jquery and jquery UI, I did something quite like this a while ago. I didn't create a "make new card" function, I began with a "launchpad" and created two droppable areas that cards could be appended to and switched between - similar to what you have. Using "intersect" as I remember was a tipping point to getting it to work as I wanted - being able to move elements up and down the list (so they don't necessarily move back to where they originated). Perhaps it could be a starting point for you? Here's the fiddle (the jquery is old.. recommend updating to newer versions)

Hope this helps.

EDIT: I made a couple of small tweaks to your code to add an outline and change the cursor on move. According to a comment on another question, adding a border is the most efficient way to create the visual 'outline' effect. There is a longer way to create the 'sortable' effect which is demoed in this codepen I found, and explained simply, the function is based around calculating hover position and if the dragged element is half-way over an item in the list, the effect displays and the item can be dropped in between list items.

Hope this is clear enough!

// Tells the other side what data is being passed (e.g. the ID is targeted)
var dropTarget = document.querySelector(".drop-target");
var draggables = document.querySelectorAll(".drag-task");

// Tells the other side what data is being passed (e.g. the ID is targeted)
draggables.forEach(item => {
  item.addEventListener("dragstart", function(ev) {
    ev.dataTransfer.setData("srcId", ev.target.id);
  });
})
// The end destination, prevent browsers default drag and drop (disabling breaks feature)
// because it's disabled by browsers by default
dropTarget.addEventListener('dragover', function(ev) {
  ev.preventDefault();
});
// End destination where item is dropped into
dropTarget.addEventListener('drop', function(ev) {
  ev.preventDefault();
  let target = ev.target;
  let droppable = target.classList.contains('drag-box');
  let srcId = ev.dataTransfer.getData("srcId");

  if (droppable) {
    ev.target.appendChild(document.getElementById(srcId));
  }
});
.drag-box {
  background-color: lightgray;
  float: left;
  width: 120px;
  min-height: 80px; /*lengthened the height slightly*/
  padding-bottom: 30px;
  height: auto;
  margin: 30px;
  cursor: move; /*added the 'cross' cursor*/
}
.drag-task {
  background-color: white;
  margin: 10px;
  padding: 5px; /*added padding to make tiles bigger*/
  border:1px dashed #000000; /*set an outline*/
}

.drop-active {
  border: 1px dashed red;
  cursor: pointer; /*change the pointer back to the default cursor while moving between lists*/
}
<div class="drop-target">
  <div class="drag-box">
    <div class="drag-card">
      <div draggable="true" id="task1" class="drag-task">Test Card 1</div>
    </div>
    <div class="drag-card">
      <div draggable="true" id="task2" class="drag-task">Test Card 2</div>
    </div>
    <div class="drag-card">
      <div draggable="true" id="task3" class="drag-task">Test Card 3</div>
    </div>
  </div>
  
  <!-- added tiles to the 2nd list (and deleted 3rd box)-->
  <div class="drag-box">
    <div class="drag-card">
      <div draggable="true" id="orange" class="drag-task">Orange</div>
    </div>
    <div class="drag-card">
      <div draggable="true" id="apple" class="drag-task">Apple</div>
    </div>
    <div class="drag-card">
      <div draggable="true" id="pear" class="drag-task">Pear</div>
    </div>
  </div>

$("#launchPad").height($(window).height() - 20);
var dropSpace = $(window).width() - $("#launchPad").width();
$("#dropZone").width(dropSpace - 70);
$("#dropZone").height($("#launchPad").height());

$(".card").draggable({
    appendTo: "#launchPad",
    cursor: "move",
    helper: 'clone',
    revert: "invalid",

});

$("#launchPad").droppable({
    tolerance: "intersect",
    accept: ".card",
    activeClass: "ui-state-default",
    hoverClass: "ui-state-hover",
    drop: function(event, ui) {
        $("#launchPad").append($(ui.draggable));
    }
});

$(".stackDrop").droppable({
    tolerance: "intersect",
    accept: ".card",
    activeClass: "ui-state-default",
    hoverClass: "ui-state-hover",
    drop: function(event, ui) {        
        $(this).append($(ui.draggable));
    }
});
body { 
    margin: 0;
    background-color: #ffffcc;
}
#launchPad {
    width:170px;
    float:left;
    border: 1px solid #eaeaea;
    background-color: #f5f5f5;
}
#dropZone {
    float:right;
    border: 1px solid #eaeaea;
    background-color: #ffffcc;
}
.card { 
    width: 130px; 
    padding: 5px 10px;
    margin:5px;
    border:1px solid #ccc;
    background-color: #eaeaea;
}
.stack {
  display:inline-block;
   vertical-align:top;
    width: 180px;
    border: 1px solid #ccc;
    background-color: #f5f5f5;
    margin: 20px;
}
.stackHdr {
    background-color: #eaeaea;
    border: 1px solid #fff;
    padding: 5px 
}
.stackDrop {
    min-height:100px;
    padding: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script src="http://code.jquery.com/ui/1.9.2/jquery-ui.js"></script>

<div id="launchPad">    
    <div class="card draggable" >
        apple
    </div> 
    <div class="card draggable">
        orange
    </div> 
    <div class="card draggable">
        banana
    </div> 
    <div class="card draggable">
        car
    </div> 
    <div class="card draggable">
        bus
    </div> 
</div>

<div id="dropZone">
    <div class="stack">
        <div class="stackHdr">
            Drop here 
        </div>
        <div class="stackDrop droppable">
            
        </div>
    </div>
    
    <div class="stack">
        <div class="stackHdr">
            Or here
        </div>
        <div class="stackDrop droppable">
            
        </div>
    </div>
</div>

Related