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
.
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)
}
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.
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>
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>