Last Updated: February 25, 2016
·
3.634K
· lperrin

Taming HTML5 Drag and Drop

HTML5 DnD is notoriously bad. I will show you a simple plugin I wrote to make things saner. It will also help you understand how the API works.

Why is it bad ?

HTML5 has become a synonym of modern web, but the DnD API actually comes from IE5. It was subsequently implemented by all other browsers and became a de facto standard.

  • Tons of different events you don't really care about.
  • You need to cancel the defaults to get useful behaviors.
  • Events fire and bubble a lot.

You can read the longer story here:

http://pxdraw.com/HTML5DragAndDropSucks.com/#slide1

Define "saner"

We want Gmail-like drag and drop:

  • Use CSS as much as possible.
  • When user drags a file over the browser, highlight all droppable areas with body.dragging .droppable.
  • When user drags a file over a droppable area, highlight it with: .droppable.dragging.
  • You can catch drop events with $('.droppable').on('drop', …).
  • If the user drops the file on a non-droppable area, nothing happens (as opposed to opening the file and navigating away).

Explanations

We care about 3 events: dragenter, dragover, dragleave. Though it seems reasonable for an API, there is a big problem. Suppose you have something like that:

<div class="droppable">
   <span>Drop files here</span>
</div>

The div will fire dragenter when expected, but it will immediately fire dragleave as soon as you enter the span (though you're still inside the droppable). The span itself will fire dragenter. Of course, all these events bubble up, so you end up with tons of dragleave and dragenter events.

To fix this, we need to count events: +1 for dragenter, -1 for dragleave. If this dragCount is positive for a DOM node, then the user is still dragging a file inside it.

That's the basic idea, but there are some other tricks:

  • I also compute dragCount on <body>, so we know if the user is dragging a file anywhere in the browser.
  • I cancel defaults events so the browser will never navigate away.
  • I only attach events to the body.

The code !

;(function () {
  var $body = $('body');

  $body.on('dragenter dragleave', function (e) {
    e.preventDefault();

    var $hierarchy = $(e.target).parents().add(e.target),
        $droppables = $hierarchy.filter('.droppable'),
        countOffset = e.type === 'dragenter' ? 1 : -1;

    $droppables.add($body).each(function () {
      var dragCount = ($(this).data('dragCount') || 0) + countOffset;

      $(this)
        .data('dragCount', dragCount)
        .toggleClass('dragging', dragCount > 0);
    });
  });

  $body.on('dragover', function (e) {
    e.preventDefault();

    var isDroppable = false;
    $('.droppable').each(function () {
      if($(this).data('dragCount') > 0)
        isDroppable = true;
    });

    e.originalEvent.dataTransfer.dropEffect = isDroppable ? 'copy' : 'none';
  });

  $body.on('drop', function (e) {
    e.preventDefault();
    $('.droppable').add($body).removeClass('dragging').removeData('dragCount');
  });
})();

1 Response
Add your response

Interesting. As I am playing with Microdata and Draggable, using hover in CSS is something I am playing with.
[draggable="true"]:hover {color: #dddddd; background-color: #ffff00;}

over 1 year ago ·