February 17, 2016
I would like to write you my observations what I did when I tried to implement native HTML5 drag and drop support for an existing app. And just to be precise, today is February, 2016.
At work we work on quite complex web app which uses drag and drop based UI for building some page content. Doesn’t matter what’s exactly going on. It’s in development process for almost 3 years and from the beginning we use jQueryUI draggable and droppable support. It was very pragmatic solution, because when the project started, we needed to support really old and obscure browsers, so we benefit from jQuery abstraction. So far, so good.
We used jQueryUI just for handling drag-and-drop events, we didn’t use a UI components. We used one of our company’s internal UI component framework. It looked quite good, it used knockout and jQuery and everything was cool.
But one day, that internal framework added a feature to their Table component. One was able add columns to a table by dragging and dropping a page element to it. Cool!!! We need to support such an awesome feature! It looked really easy to implement, documentation said that you just need to add draggable="true"
attribute to that draggable element.
So I did it. And that was just the beginning…
I had one element on a page, which could be dropped either to Table component (which used HTML5 native Drop support), or directly to the page (where we used jQueryUI droppable). And that was a problem. My naive solution was just to add draggable="true"
to such an element and I hoped that it will work. That jQuery will handle that. That it uses HTML5 native DaD support internally if possible. But I was soooo wrong. It didn’t work. Really not. I also tried to fire dragstart
event manually. Nothing. End.
So I ended up at the very beginning. That wasn’t a good day.
I realized that the only working solution would be to rewrite our entire app to use native HTML5 Drag and Drop instead of jQueryUI draggable and droppable support.
Well, ok. We have a year 2016, we don’t need to support IE < 11, it can’t be so hard to rewrite it! Another wrong assumption…
Everybody who tried to work with HTML5 native Drag and Drop support bumped into it. 8 events and you must implement some of them just because you need to call event.preventDefault();
or to return false;
? Uff. The most knows example is that you must implement dragover
and call event.preventDefault();
because without it drop
event doesn’t work. Strange, isn’t it?
So I fixed that and then tried to handle some actions which should be triggered when dragged element is dragged over or out of the droppable element. It seemed to be a very simple thing. Just implement dragenter
event and dragleave
event. Ok so I did it…and it didn’t work. I was getting tens of events even though I was still over the droppable zone! Wtf? I googled a bit and found that these events have a problem with child elements of a droppable zone. Every child fires dragenter
and dragleave
even though you didn’t put an event listener on them.
Ok, someone surely solved that issue before me, so I googled again. I found and tried a lot of examples how to solve it. It included counters of dragenters, or remembering of first dragentered element, comparing various event targets with the element on which I added a listener.
Nothing worked properly.
The one problem was that my child elements were generated dynamically during the drag event and put under the cursor. The another problem was that my child elements were stretched over the droppable element, so there was no space (also no border) of the droppable element visible.
The only working solution was to set CSS property pointer-events: none;
to that child elements.
All my fight took place in Chrome browser. I like it and its dev tools are the best. But a little devil sitting on my left shoulder whispered me “Try it in a Firefox! Just for fun!“. So I tried it. And nothing worked. Even things that worked in Chrome didn’t work in Firefox. So when I tried to drag an element, it wasn’t dragged at all. That was a really baaad day.
After some attempts to get it to work, which were absolutely out of place, I saw somewhere that people are setting event.dataTransfer.setData('text', null);
in dragstart
event. I didn’t do that, because I didn’t want to transfer any data, so. But, do you see it? null
! So I tried it and it worked like a charm!
In Firefox, you must set at least nothing to dataTransfer
object, otherwise it doesn’t work. Holy crap!
Closed that Devil’s tool and switched back to Chrome.
Yes, it should. In a normal world, one should just provide an html element which should be positioned automatically and absolutely alongside with the dragging cursor.
It looks like that it works this way, but it doesn’t. Ideally you should provide some image, or canvas to some event.dataTransfer.setDropImage();
method. Image?? Omg…ok, someone wrote somewhere that an html element can be passed to that method as well. Great, we won! But just a bit. What the browser actually does is that it takes that element and renders an image from it, which is then used as a ghost. Yep, image again.
But who cares, you say. I say that everyone cares. I can make my element, style it, attach somewhere to body…and that’s it. But not when that element’s position is hidden by another element or is out of a viewport. That element must be visible. But ok, we can live with it. Until you wanna style that “element” during the drag. You absolutely can’t. Oh, sorry…you can. You can style that element, but not its image which is actually beeing dragged. Sorry…
The only way how to at least somehow modify a dragging cursor, is by event.dataTransfer.allowedEffect
and event.dataTransfer.dropEffect
properties.
The first one must be set in dragstart
event and tries to say what kind of opperation this draggable supports. E.g. copyMove
which means that the dragged element can be copied and moved.
The second is used in dragenter
and dragover
event. Yes, in that strange dragover
event. If you don’t use it there, it is possible that the cursor will not change at all (or will use some defaults). And you can set it to one of 4 predefined strings: none
, move
, copy
and link
.
There aren’t used common well known css cursor
property values neither in allowedEffect
, nor in dropEffect
. There are just some predefined strings which are represented somehow by the browser itself. So yes, you can’t set a cursor to no-drop! The only possible/similar-like value is none
, but it shows a common drag cursor. That’s really bad, because you can not set no-drop
by default and just change it to e.g. move
in dragenter
event! That’s sooooo crappy.
I didn’t end up with struggling yet, but I already know, that I will not use it anymore in any other project. If possible.
So if possible, save your nerves and do not try to use it. You will have a very bad day and it is not worth it…
Bye!
"What I bumped into in my developer's life", by Ondrej Brejla