Search code examples
javascriptjqueryhtmldraggable

Issue with Drag and Drop API or?


I'm encountering an issue with the HTML5 Drag and Drop API.

Long story short, there are three events other than dragstart, dragover, and drop, which are critical to my objective. Those three events are mousedown, mouseup, and mouseleave.

 

onmousedown

When the user triggers a mousedown event on a given element, the following function applies attributes and listeners to itself and the target element.

function doThis() {
  d.setAttribute('draggable', true)
  d.addEventListener('dragstart', dragStart)
  t.style.background = 'black'
  t.addEventListener('dragover', dragOver)
  t.addEventListener('drop', drop)
}

 

onmouseup & mouseleave

When the user triggers a mouseup or mouseleave event, those attributes and listeners are removed.

function thenThis(){
  d.setAttribute('draggable', false)
  d.removeEventListener('dragstart', dragStart)
  t.style.background = 'none'
  t.removeEventListener('dragover', dragOver)
  t.removeEventListener('drop', drop)
}

I've had to add the mouseleave listener since the mouseup event must occur over the element to which it is binded for it to be triggered. Without applying the mouseleave event, the user is able to trigger the mousedown event while avoiding the mouseup event altogether (due to the browser's native detection of the dblclick event) by double clicking the element and then sliding the cursor off of the element while not letting go of the mouse button until it is beyond the element's perimeter.


The issue...

If the user clicks the draggable element too close to its edge, the element becomes undraggable despite the mousedown event being triggered.

Here is an codepen MVC which can also be viewed below.

Why does this occur? The mousedown even is triggered, but the element is still undraggable...

Does anybody know how I can fix this or prevent it from happening in the first place?

var d = document.getElementById('draggable')
var t = document.getElementById('target')

d.addEventListener('mousedown', doThis)
d.addEventListener('mouseup', thenThis)
d.addEventListener('mouseleave', thenThis)
d.addEventListener('mouseleave', alertLeave)

function doThis(){
  d.setAttribute('draggable', true)
  d.addEventListener('dragstart', dragStart)
  t.style.background = 'black'
  t.addEventListener('dragover', dragOver)
  t.addEventListener('drop', drop)
}

function thenThis(){
  d.setAttribute('draggable', false)
  d.removeEventListener('dragstart', dragStart)
  t.style.background = 'none'
  t.removeEventListener('dragover', dragOver)
  t.removeEventListener('drop', drop)
}

function dragStart(e){
  e.dataTransfer.setData('text/plain', e.target.id);
  e.dataTransfer.effectAllowed = 'move';
}

function dragOver(e){
  e.preventDefault();
}

function drop(e){
  var data = e.dataTransfer.getData('text/plain');
  e.preventDefault();
  e.target.parentElement.appendChild(document.getElementById(data));
}

function alertLeave(){
  //alert('mouseleave')
}
body {
  display: flex;
  justify-content: space-around;
}

div {
  width: 50px;
  height: 50px;
}

#draggable {
  background: purple;
}

#target {
  border: solid 3px black;
}
<div id="draggable"></div>
<div id="target"></div>


Solution

  • The problem, when you try to grab the left element close to its right edge, is that mouseleave is triggered as soon as you move the mouse to the right, before dragstart has a chance to be triggered. You can see the events in this code snippet:

    var d = document.getElementById('draggable')
    var t = document.getElementById('target')
    
    d.addEventListener('mousedown', doThis)
    d.addEventListener('mouseleave', thenThis)
    
    function doThis(){
      console.log('mousedown');
      d.setAttribute('draggable', true);
      d.addEventListener('dragstart', dragStart);
      t.style.background = 'black';
      t.addEventListener('dragover', dragOver);
      t.addEventListener('drop', drop);
    }
    
    function thenThis(){
      console.log('mouseleave');
      d.setAttribute('draggable', false);
      d.removeEventListener('dragstart', dragStart);
      t.style.background = 'none';
      t.removeEventListener('dragover', dragOver);
      t.removeEventListener('drop', drop);
    }
    
    function dragStart(e){
      console.log('dragstart');
      e.dataTransfer.setData('text/plain', e.target.id);
      e.dataTransfer.effectAllowed = 'move';
    }
    
    function dragOver(e){
      e.preventDefault();
    }
    
    function drop(e){
      var data = e.dataTransfer.getData('text/plain');
      e.preventDefault();
      e.target.parentElement.appendChild(document.getElementById(data));
    }
    body {
      display: flex;
      justify-content: space-around;
    }
    
    .square {
      width: 50px;
      height: 50px;
    }
    
    #draggable {
      background: purple;
    }
    
    #target {
      border: solid 3px black;
    }
    <div id="draggable" class="square"></div>
    <div id="target" class="square"></div>

    To avoid that situation, you can capture the mouse instead of processing mouseleave. Once the dragging operation begins, you can release the mouse capture and rely on the dragend event. The draggable element properties will be reset when the mouse is released by the user (if the element was not dragged) or when the drag operation ends.

    var d = document.getElementById('draggable')
    var t = document.getElementById('target')
    var releaseCapture = null;
    
    d.addEventListener('mousedown', mouseDown);
    d.addEventListener('dragend', resetDraggableElement);
    
    function mouseDown() {
      releaseCapture = captureMouse(resetDraggableElement);
      d.setAttribute('draggable', true);
      d.addEventListener('dragstart', dragStart);
      t.style.background = 'black';
      t.addEventListener('dragover', dragOver);
      t.addEventListener('drop', drop);
    }
    
    function resetDraggableElement() {
      d.setAttribute('draggable', false);
      d.removeEventListener('dragstart', dragStart);
      t.style.background = 'none';
      t.removeEventListener('dragover', dragOver);
      t.removeEventListener('drop', drop);
    }
    
    function dragStart(e) {
      if (releaseCapture) {
        releaseCapture();
        releaseCapture = null;
      }
      e.dataTransfer.setData('text/plain', e.target.id);
      e.dataTransfer.effectAllowed = 'move';
    }
    
    function dragOver(e) {
      e.preventDefault();
    }
    
    function drop(e) {
      var data = e.dataTransfer.getData('text/plain');
      e.preventDefault();
      e.target.parentElement.appendChild(document.getElementById(data));
    }
    
    function captureMouse(mouseUpHandler) {
      var releaseCapture = function() {
        document.removeEventListener("mouseup", lostCaptureHandler, false);
      }
      var lostCaptureHandler = function() {
        releaseCapture();
        if (mouseUpHandler) {
          mouseUpHandler();
        }
      };
      document.addEventListener("mouseup", lostCaptureHandler, false);
      return releaseCapture;
    }
    body {
      display: flex;
      justify-content: space-around;
    }
    
    .square {
      width: 50px;
      height: 50px;
    }
    
    #draggable {
      background: purple;
    }
    
    #target {
      border: solid 3px black;
    }
    <div id="draggable" class="square"></div>
    <div id="target" class="square"></div>