84

I'm trying to use the HTML5 draggable API (though I realize it has its problems). So far, the only showstopper I've encountered is that I can't figure out a way to determine what is being dragged when a dragover or dragenter event fires:

el.addEventListener('dragenter', function(e) {
  // what is the draggable element?
});

I realize I could assume that it's the last element to fire a dragstart event, but... multitouch. I've also tried using e.dataTransfer.setData from the dragstart to attach a unique identifier, but apparently that data is inaccessible from dragover/dragenter:

This data will only be available once a drop occurs during the drop event.

So, any ideas?

Update: As of this writing, HTML5 drag-and-drop does not appear to be implemented in any major mobile browser, making the point about multitouch moot in practice. However, I'd like a solution that's guaranteed to work across any implementation of the spec, which does not appear to preclude multiple elements from being dragged simultaneously.

I've posted a working solution below, but it's an ugly hack. I'm still hoping for a better answer.

Community
  • 1
  • 1
Trevor Burnham
  • 76,828
  • 33
  • 160
  • 196
  • 1
    normally you should be able to use the "this" property inside the function, which is linked to the object that fired the event. – lgomezma Jun 16 '12 at 18:15
  • 2
    @lgomezma No, `this` in a `dragenter`/`dragover` event handler points to the element that's being dragged over, not to the element being dragged. It's equal to `e.target`. Example: http://jsfiddle.net/TrevorBurnham/jWCSB/ – Trevor Burnham Jun 16 '12 at 19:45
  • I don't think Microsoft had multitouch in mind when they [originally designed and implemented drag-and-drop for IE 5](http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html). – Jeffery To Jun 20 '12 at 10:13
  • @JefferyTo I'm aware of that, but now that their design has been codified as a standard, I'd like to future-proof by working against the [WHATWG spec](http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html), not just existing implementations. – Trevor Burnham Jun 20 '12 at 16:24
  • Filed this bug against the spec - https://www.w3.org/Bugs/Public/show_bug.cgi?id=23486 – broofa Oct 12 '13 at 11:56

10 Answers10

82

I wanted to add a very clear answer here so that it was obvious to everyone who wanders past here. It's been said several times in other answers, but here it is, as clear as I can make it:

dragover DOES NOT HAVE THE RIGHTS to see the data in the drag event.

This information is only available during the DRAG_START and DRAG_END (drop).

The issue is it's not obvious at all and maddening until you happen to read deeply enough on the spec or places like here.

WORK-AROUND:

As a possible work-around I have added special keys to the DataTransfer object and tested those. For example, to improve efficiency I wanted to look up some "drop target" rules when my drag started instead of every time a "drag over" occurred. To do this I added keys identifying each rule onto the dataTransfer object and tested those with "contains".

ev.originalEvent.dataTransfer.types.includes("allow_drop_in_non_folders")

And things like that. To be clear, that "includes" is not a magic bullet and can become a performance concern itself. Take care to understand your usage and scenarios.

John Weisz
  • 30,137
  • 13
  • 89
  • 132
bladnman
  • 2,591
  • 1
  • 25
  • 20
  • 1
    This is good advice if you don't have support IE/Edge. IE/Edge only allows two values as type: "text" or "url". – wawka Feb 18 '16 at 13:41
  • The workaround could even pass more data using `JSON.stringify` and `JSON.parse` ([poc](https://codepen.io/BuonOmo/pen/RZZbvZ)). Very inelegant though – Ulysse BN Aug 11 '17 at 18:02
  • 4
    I like clear answers that shout in my face. Easy to remember :D – Wilt Jun 24 '19 at 15:18
  • 1
    Am I correct that this answer is outdated, at least partially? With [`DataTransfer.items`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items) one can at least check the MIME type of a dragged file in the `dragenter` event handler. There's [one answer](https://stackoverflow.com/a/45471116/1483676) (far below) that mentions it. – cubuspl42 Dec 06 '21 at 14:50
27

The short answer to my question turns out to be: No. The WHATWG spec doesn't provide a reference to the element being dragged (called the "source node" in the spec) in the dragenter, dragover, or dragleave events.

Why not? Two reasons:

First, as Jeffery points out in his comment, the WHATWG spec is based on IE5+'s implementation of drag-and-drop, which predated multi-touch devices. (As of this writing, no major multi-touch browser implements HTML drag-and-drop.) In a "single-touch" context, it's easy to store a global reference to the current dragged element on dragstart.

Second, HTML drag-and-drop allows you to drag elements across multiple documents. This is awesome, but it also means that providing a reference to the element being dragged in every dragenter, dragover, or dragleave event wouldn't make sense; you can't reference an element in a different document. It's a strength of the API that those events work the same way whether the drag originated in the same document or a different one.

But the inability to provide serialized information to all drag events, except through dataTransfer.types (as described in my working solution answer), is a glaring omission in the API. I've submitted a proposal for public data in drag events to the WHATWG, and I hope you'll express your support.

Community
  • 1
  • 1
Trevor Burnham
  • 76,828
  • 33
  • 160
  • 196
  • I had the same problem and came to the conclusion that the best bet here is to stick a custom mime type in the data transfer. since the main purpose of the drag over function you provide is to stop the event I agree it requires some information on which to base this. for many purposes it seems fine to be able to say "the drag contains application/myapp and I can handle that so cancel the event". – Woody Jul 21 '13 at 17:49
  • 1
    I've seen your proposal link but I really don't know where I can read answers to this, or even express my support – Ulysse BN Aug 11 '17 at 18:05
14

A (very inelegant) solution is to store a selector as a type of data in the dataTransfer object. Here is an example: http://jsfiddle.net/TrevorBurnham/eKHap/

The active lines here are

e.dataTransfer.setData('text/html', 'foo');
e.dataTransfer.setData('draggable', '');

Then in the dragover and dragenter events, e.dataTransfer.types contains the string 'draggable', which is the ID needed to determine which element is being dragged. (Note that browsers apparently require data to be set for a recognized MIME type like text/html as well in order for this to work. Tested in Chrome and Firefox.)

It's an ugly, ugly hack, and if someone can give me a better solution, I'll happily grant them the bounty.

Update: One caveat worth adding is that, in addition to being inelegant, the spec states that all data types will be converted to lower-case ASCII. So be warned that selectors involving capital letters or unicode will break. Jeffery's solution sidesteps this issue.

Community
  • 1
  • 1
Trevor Burnham
  • 76,828
  • 33
  • 160
  • 196
7

Given the current spec, I don't think there is any solution that isn't a "hack". Petitioning the WHATWG is one way to get this fixed :-)

Expanding on the "(very inelegant) solution" (demo):

  • Create a global hash of all elements currently being dragged:

    var dragging = {};
    
  • In the dragstart handler, assign a drag ID to the element (if it doesn't have one already), add the element to the global hash, then add the drag ID as a data type:

    var dragId = this.dragId;
    
    if (!dragId) {
        dragId = this.dragId = (Math.random() + '').replace(/\D/g, '');
    }
    
    dragging[dragId] = this;
    
    e.dataTransfer.setData('text/html', dragId);
    e.dataTransfer.setData('dnd/' + dragId, dragId);
    
  • In the dragenter handler, find the drag ID among the data types and retrieve the original element from the global hash:

    var types = e.dataTransfer.types, l = types.length, i = 0, match, el;
    
    for ( ; i < l; i++) {
        match = /^dnd\/(\w+)$/.exec(types[i].toLowerCase());
    
        if (match) {
            el = dragging[match[1]];
    
            // do something with el
        }
    }
    

If you keep the dragging hash private to your own code, third-party code would not be able to find the original element, even though they can access the drag ID.

This assumes that each element can only be dragged once; with multi-touch I suppose it would be possible to drag the same element multiple times using different fingers...


Update: To allow for multiple drags on the same element, we can include a drag count in the global hash: http://jsfiddle.net/jefferyto/eKHap/2/

Jeffery To
  • 11,836
  • 1
  • 27
  • 42
  • Ooph, it didn't occur to me that the *same element* could conceivably be dragged multiple times simultaneously. The spec doesn't appear to preclude it. – Trevor Burnham Jun 20 '12 at 22:41
  • @TrevorBurnham I've updated my answer to allow for multiple drags on the same element, but honestly I have no idea how the spec will change to allow for multi-touch. – Jeffery To Jun 21 '12 at 02:58
5

To check if it is a file use:

e.originalEvent.dataTransfer.items[0].kind

To check the type use:

e.originalEvent.dataTransfer.items[0].type

i.e. I want to allow only one single file jpg, png, gif, bmp

var items = e.originalEvent.dataTransfer.items;
var allowedTypes = ["image/jpg", "image/png", "image/gif", "image/bmp"];
if (items.length > 1 || items["0"].kind != "file" || items["0"].type == "" || allowedTypes.indexOf(items["0"].type) == -1) {
    //Type not allowed
}

Reference: https://developer.mozilla.org/it/docs/Web/API/DataTransferItem

MotKohn
  • 3,485
  • 1
  • 24
  • 41
user2272143
  • 469
  • 5
  • 22
  • I don't think `originalEvent` is populated in most of cases, even for Chrome. – windmaomao Dec 26 '19 at 17:47
  • @windmaomao I tested it with Chrome, Edge and Firefox and it works fine for all. – user2272143 Dec 28 '19 at 13:04
  • `evt.originaEvent` is for when you're using jQuery. You might want to do something like `const evt = $evt.originalEvent;` to accommodate jQuery and carry on with your code. – MarkMYoung May 17 '22 at 15:19
3

You can determine what is being dragged when the drag starts and save this in a variable to use when the dragover/dragenter events are fired:

var draggedElement = null;

function drag(event) {
    draggedElement = event.srcElement || event.target;
};

function dragEnter(event) {
    // use the dragged element here...
};
xcopy
  • 2,248
  • 18
  • 24
  • I mentioned this in my question. The approach is untenable on multitouch devices, where multiple elements can be dragged simultaneously. – Trevor Burnham Jun 18 '12 at 18:54
3

In the drag event, copy event.x and event.y to an object and set it as the value of an expando property on the dragged element.

function drag(e) {
    this.draggingAt = { x: e.x, y: e.y };
}

In the dragenter and dragleave events find the element whose expando property value matches the event.x and event.y of the current event.

function dragEnter(e) {
    var draggedElement = dragging.filter(function(el) {
        return el.draggingAt.x == e.x && el.draggingAt.y == e.y;
    })[0];
}

To reduce the number of elements you need to look at, you can keep track of elements by adding them to an array or assigning a class in the dragstart event, and undoing that in the dragend event.

var dragging = [];
function dragStart(e) {
    e.dataTransfer.setData('text/html', '');
    dragging.push(this);
}
function dragEnd(e) {
    dragging.splice(dragging.indexOf(this), 1);
}

http://jsfiddle.net/gilly3/4bVhL/

Now, in theory this should work. However, I don't know how to enable dragging for a touch device, so I wasn't able to test it. This link is mobile formatted, but touch and slide didn't cause dragging to start on my android. http://fiddle.jshell.net/gilly3/4bVhL/1/show/

Edit: From what I've read, it doesn't look like HTML5 draggable is supported on any touch devices. Are you able to get draggable working on any touch devices? If not, multi-touch wouldn't be an issue and you can resort to just storing the dragged element in a variable.

gilly3
  • 87,962
  • 25
  • 144
  • 176
  • @Trevor - Really? [My jsfiddle](http://jsfiddle.net/gilly3/4bVhL/) works for me in Chrome. – gilly3 Jun 19 '12 at 16:17
  • @gilly3 How does your method work? I've not used a multi-touch device but I presume you can drag multiple objects at the same time. Is the element that last called the drag event guaranteed to be the element that first calls the drag enter event? (my wording is not the clearest but hopefully you know what I mean) – Jules Jun 19 '12 at 16:32
  • @Jules - I would *expect* that drag and dragenter would be called in immediate succession for the same element as you suggest in a multi-touch environment (that supports HTML5 draggable), and I would be surprised if that wasn't the case. **But,** I'd be more surprised if that was guaranteed (ie, defined that way by the spec). My method keeps an array of elements being dragged and searches for the element that is at the same position as the current event. I know what position each dragged element is at because I update a custom property on the element in the `drag` event handler. – gilly3 Jun 19 '12 at 16:59
  • @gilly3 I see what you mean. Thinking about it, a dragenter would only be checked straight after a drag so there would be no reason to not call the dragenter immediately, before checking for any other drags. – Jules Jun 19 '12 at 17:11
  • @gilly3 The spec does guarantee that `drag` always fires before `dragenter` (the fact that it wasn't firing for me was due to a bug in a very specific version of Chrome). However, it doesn't guarantee that `x` and `y` will have consistent values. Overall, this is a clever response and I'm giving it a +1, but I don't think I can accept it as an answer. – Trevor Burnham Jun 19 '12 at 17:19
0

From what I have read on MDN, what you are doing is correct.

MDN lists some recommended drag types, such as text/html, but if none are suitable then just store the id as text using the 'text/html' type, or create your own type, such as 'application/node-id'.

Jules
  • 4,319
  • 3
  • 44
  • 72
  • As I say in the question: You can try to store the id as `text/html` data, but you can't access that data until the drop because of the security model. See [the spec](http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-data-store-mode), esp. "Protected mode." – Trevor Burnham Jun 19 '12 at 13:17
  • Sorry, I missed that! Is this to give a visual cue if it is ok to drop? Could you at least try/catch in the dragenter event. When it comes to dropping, you have all the info available then and can then cancel the drop if it is not applicable. – Jules Jun 19 '12 at 16:25
0

As the other answers are already explaining, the data set using DataTransfer.setData(format, data) is not accessible by dragenter and dragover event handlers, whereas the format is accessible in the DataTransfer.types array. We can abuse this fact to put the data in the format.

Other than what the other answers explain, we can use the format not just to store individual attributes, but for actual JSON objects as well. From what I understand, the format will be converted to lower case and I'm not sure special characters will cause any trouble, so I'm encoding the data to a hex string (a more sophisticated solution would be to use base32).

const PREFIX = 'my-app-';

function setPublicDragData(dataTransfer, data) {
    const json = JSON.stringify(data);
    let encoded = '';
    for (let i = 0; i < json.length; i++) {
        encoded += json.charCodeAt(i).toString(16).padStart(2, '0');
    }
    dataTransfer.setData(`${PREFIX}${encoded}`, '');
}

function getPublicDragData(dataTransfer) {
    for (const type of dataTransfer.types) {
        if (type.startsWith(PREFIX)) {
                const encoded = type.slice(PREFIX.length);
                let json = '';
                for (let i = 0; i < encoded.length; i += 2) {
                    json += String.fromCharCode(parseInt(encoded.slice(i, i + 2), 16));
                }
                return JSON.parse(json);
            }
        }
    }
}

With this, you can set the data in the dragstart event using setPublicDragData(e.dataTransfer, data) and retrieve it in the dragenter/dragover events using getPublicDragData(e.dataTransfer).

Other than solutions using global variables or DOM elements, this solution should also work when dragging from one window to another.

Note that depending on your use case, you may want to add exception handling to the code (to handle invalid JSON). Also, I don't know what the size limit is.

cdauth
  • 6,171
  • 3
  • 41
  • 49
-1

I think you can get it by calling e.relatedTarget See: http://help.dottoro.com/ljogqtqm.php

OK, I tried e.target.previousElementSibling and it works, sorta.... http://jsfiddle.net/Gz8Qw/4/ I think it hangs up because the event is being fired twice. Once for the div and once for the text node (when it fires for text node, it is undefined). Not sure if that will get you where you want to be or not...

solidau
  • 4,021
  • 3
  • 24
  • 45
  • Nope. [Try it for yourself](http://jsfiddle.net/TrevorBurnham/Gz8Qw/). In Chrome, `e.relatedTarget` is `null`. In Firefox, it's some odd `XrayWrapper`. – Trevor Burnham Jun 18 '12 at 18:51
  • I did not get these results (for me it returned #dragTarget), but i realized this is still not what you were looking for. – solidau Jun 18 '12 at 19:23
  • Amending my previous comment: The `XrayWrapper` is a quirk of Firefox's development tools, appearing whenever you `console.log` a DOM element. In any event, what it wrapped around was the thing being moused over, not the draggable. – Trevor Burnham Jun 18 '12 at 19:26
  • 1
    `e.target.previousElementSibling` only works because the `draggable` is the element before the drag target... – Trevor Burnham Jun 18 '12 at 19:26
  • ahh, i see, i was thinking 'previous' in a different context. I see now that it is literally previous element in the DOM. I'm glad you're pointing all this out (instead of just leaving blank), helping me to understand as well - ty. – solidau Jun 18 '12 at 19:33
  • It would be nice if you could update to answer for people who don't read the comments. Thank you for your effort. – Pipo Jan 04 '16 at 07:21