20

Background

I am working on a sortable list, to avoid having to manually type in sorting order numbers in a database. It works via HTML5's drag'n'drop functionality, i.e. the new drag* events in Javascript.

I currently have it working for the most part. I can click, and drag, and it'll sort itself.

Problem

From what I can tell, the drop, along with the dragstart and dragend events, only are aware of the element they are going into. They can't tell if the mouse is on the top half of the dropzone, or the bottom half.

What I'd like, is when I'm hovering over the top half of a list item, for the dragged content to be placed ABOVE the item. Then if I'm hovering over the bottom half, for the dragged content to be placed BELOW the item.

Currently:

In the screencap below, I show a working (simplified) example of my code. I'm using a border-bottom on the drop target to show that it's the target. Notice how when "Item 1" is above "Item 2", "Item 2" is lit up on the bottom, regardless of if I'm hovering over the top half or bottom half.

enter image description here

Code

var dragging = null;

document.addEventListener('dragstart', function(event) {
  dragging = event.target;
    event.dataTransfer.setData('text/html', dragging);
});

document.addEventListener('dragover', function(event) {
    event.preventDefault();
});

document.addEventListener('dragenter', function(event) {
    event.target.style['border-bottom'] = 'solid 4px blue';
});

document.addEventListener('dragleave', function(event) {
    event.target.style['border-bottom'] = '';
});

document.addEventListener('drop', function(event) {
    event.preventDefault();
    event.target.style['border-bottom'] = '';
    event.target.parentNode.insertBefore(dragging, event.target.nextSibling);
});
ul {
  margin:0;
  padding:0
}
li {
  cursor:move;
  display:block;
  padding:20px 10px;
  background:white;
  border-bottom:solid 1px gray;
}
<ul>
    <li draggable="true" class="sortable-bulk">List Item 1</li>
    <li draggable="true" class="sortable-bulk">List Item 2</li>
    <li draggable="true" class="sortable-bulk">List Item 3</li>
    <li draggable="true" class="sortable-bulk">List Item 4</li>
    <li draggable="true" class="sortable-bulk">List Item 5</li>
    <li draggable="true" class="sortable-bulk">List Item 6</li>
    <li draggable="true" class="sortable-bulk">List Item 7</li>
    <li draggable="true" class="sortable-bulk">List Item 8</li>
    <li draggable="true" class="sortable-bulk">List Item 9</li>
    <li draggable="true" class="sortable-bulk">List Item 10</li>
</ul>

Question

Is there a way I can have it drop either above or below, not always below, depending on the mouse position while dragging?

Kelderic
  • 6,502
  • 8
  • 46
  • 85
  • I've tried to modify this using section and article as the html tags... but I'm getting target.getBoundingClientRect is not a function - I'm not sure exactly what to do, can you help? (I've subbed in LI with article, and body with section). – Jason Silver Jun 13 '23 at 16:15

1 Answers1

25

Answer

So in a twist of fate, a comment response on another question pointed me to the answer here. wolf-war deserves credit here for pointing me to the right event and method.

Anyway, on to the answer. The solution lies in using the dragover event, rather than using the dragenter event. dragover keeps firing as long as you are hovering.

Code Changes From Question Code

We get rid of the dragenter code:

document.addEventListener('dragenter', function(event) {
    event.target.style['border-bottom'] = 'solid 4px blue';
});

Replace it with:

document.addEventListener('dragover', function(event) {
    event.preventDefault();
    var bounding = event.target.getBoundingClientRect()
    var offset = bounding.y + (bounding.height/2);
    if ( event.clientY - offset > 0 ) {
        event.target.style['border-bottom'] = 'solid 4px blue';
        event.target.style['border-top'] = '';
    } else {
        event.target.style['border-top'] = 'solid 4px blue';
        event.target.style['border-bottom'] = '';
    }
});

Then in the drop section, we check to see which border the drop target has, and use that to insert the dragged content either above or below.

Full Working Code:

var dragging = null;

document.addEventListener('dragstart', function(event) {
    var target = getLI( event.target );
    dragging = target;
    event.dataTransfer.setData('text/plain', null);
    event.dataTransfer.setDragImage(self.dragging,0,0);
});

document.addEventListener('dragover', function(event) {
    event.preventDefault();
    var target = getLI( event.target );
    var bounding = target.getBoundingClientRect()
    var offset = bounding.y + (bounding.height/2);
    if ( event.clientY - offset > 0 ) {
        target.style['border-bottom'] = 'solid 4px blue';
        target.style['border-top'] = '';
    } else {
        target.style['border-top'] = 'solid 4px blue';
        target.style['border-bottom'] = '';
    }
});

document.addEventListener('dragleave', function(event) {
    var target = getLI( event.target );
    target.style['border-bottom'] = '';
    target.style['border-top'] = '';
});

document.addEventListener('drop', function(event) {
    event.preventDefault();
    var target = getLI( event.target );
    if ( target.style['border-bottom'] !== '' ) {
        target.style['border-bottom'] = '';
        target.parentNode.insertBefore(dragging, event.target.nextSibling);
    } else {
        target.style['border-top'] = '';
        target.parentNode.insertBefore(dragging, event.target);
    }
});

function getLI( target ) {
    while ( target.nodeName.toLowerCase() != 'li' && target.nodeName.toLowerCase() != 'body' ) {
        target = target.parentNode;
    }
    if ( target.nodeName.toLowerCase() == 'body' ) {
        return false;
    } else {
        return target;
    }
}
ul {
  margin:0;
  padding:0
}
li {
  cursor:move;
  display:block;
  padding:20px 10px;
  background:white;
  border-bottom:solid 1px gray;
}
<ul>
    <li draggable="true">List Item 1</li>
    <li draggable="true">List Item 2</li>
    <li draggable="true">List Item 3</li>
    <li draggable="true">List Item 4</li>
    <li draggable="true">List Item 5</li>
    <li draggable="true">List Item 6</li>
    <li draggable="true">List Item 7</li>
    <li draggable="true">List Item 8</li>
    <li draggable="true">List Item 9</li>
    <li draggable="true">List Item 10</li>
</ul>
Kelderic
  • 6,502
  • 8
  • 46
  • 85
  • Is it possible to make this to when you click on it the cursor:move still appears and not the arrow pointer? I'm trying to make something simple like the jQuery UI version but with just the cursor:move https://jqueryui.com/sortable/#default – drooh Nov 24 '20 at 15:03