In the HTML5 spec it's possible to make elements drag-and-drop-able. This is not new news: the excellent HTML5 Rocks and HTML5 Doctor articles cover it well.
However. When you drag a thing, what gets dragged is a "ghost image" of it. See for yourself: drag the green box below, and what you see when you're dragging is a green copy of it.
<div class="dragdemo" draggable="true">drag me</div>
What if you want the "ghost image" to be something other than a copy of the element? Well, there's event.dataTransfer.setDragImage()
, which tells us that "if the node is an HTML img element, an HTML canvas element or a XUL image element, the image data is used. Otherwise, image should be a visible node and the drag image will be created from this." So, you can set an image perfectly happily:
<div id="drag-with-image" class="dragdemo" draggable="true">drag me</div>
<script>
document.getElementById("drag-with-image").addEventListener("dragstart", function(e) {
var img = document.createElement("img");
img.src = "http://kryogenix.org/images/hackergotchi-simpler.png";
e.dataTransfer.setDragImage(img, 0, 0);
}, false);
</script>
But what you want isn't always an image. Sometimes you want a proper HTML element. Let's say… you want the ghost image that you've got to look like your actual draggable box, but with a red background. How do we do this?
Well, you could change the actual draggable box when you get a dragstart
event,
and then change it back on dragend
.
<div id="drag-with-colour" class="dragdemo" draggable="true">drag me</div>
<script>
document.getElementById("drag-with-colour").addEventListener("dragstart", function(e) {
this.style.backgroundColor = "red";
e.dataTransfer.setDragImage(img, 0, 0);
}, false);
document.getElementById("drag-with-colour").addEventListener("dragend", function(e) {
this.style.backgroundColor = "green";
}, false);
</script>
But I don't want that, you cry. I want the element I'm drawing to stay green, but the ghost image to be red. Hey, you think, I've got a plan: I'll dynamically create an element inside dragstart, and use that!
<div id="drag-with-create" class="dragdemo" draggable="true">drag me</div>
<script>
document.getElementById("drag-with-create").addEventListener("dragstart", function(e) {
var crt = this.cloneNode(true);
crt.style.backgroundColor = "red";
e.dataTransfer.setDragImage(crt, 0, 0);
}, false);
</script>
That doesn't work. This is because, as the spec says, "Otherwise, image should be a visible node and the drag image will be created from this." Gotta be a visible node, and a node created but not in the document isn't visible. So, if we drop that node into the document, then we can use it as a drag image:
<div id="drag-with-create-add" class="dragdemo" draggable="true">drag me</div>
<script>
document.getElementById("drag-with-create-add").addEventListener("dragstart", function(e) {
var crt = this.cloneNode(true);
crt.style.backgroundColor = "red";
document.body.appendChild(crt);
e.dataTransfer.setDragImage(crt, 0, 0);
}, false);
</script>
So that's fine, and everyone understands up until now. Now we come to the slightly more advanced class. Because, as you saw, that extra red box, which we're creating in order that dragImage can copy it; it appears in the document. We don't want that. So, how can we hide it but still make it copyable?
There are a bunch of ways of hiding an element. display: none
,
visibility: hidden
, position: absolute
and then move it
off-screen, transform: translate()
it off-screen,
opacity: 0
…
display: none
position: absolute
top: -150px
transform: translateX(-500px)
opacity: 0
<div id="drag-something" class="dragdemo" draggable="true">drag me</div>
<script>
document.getElementById("drag-something").addEventListener("dragstart", function(e) {
var crt = this.cloneNode(true);
crt.style.backgroundColor = "red";
crt.style.display = "none"; /* or visibility: hidden, or any of the above */
document.body.appendChild(crt);
e.dataTransfer.setDragImage(crt, 0, 0);
}, false);
</script>
And now you've tried those, and none of them work; that is, you get no drag image. Because the thing you're trying to set as the drag image isn't visible*. To prove this point, here's an element where we position the thing to copy for the drag image half off the screen and half on:
<div id="drag-with-create-add" class="dragdemo" draggable="true">drag me</div>
<script>
document.getElementById("drag-half-on").addEventListener("dragstart", function(e) {
var crt = this.cloneNode(true);
crt.style.backgroundColor = "red";
crt.style.position = "absolute"; crt.style.top = "0px"; crt.style.left = "-100px";
document.body.appendChild(crt);
e.dataTransfer.setDragImage(crt, 0, 0);
}, false);
</script>
Try dragging it. The drag image is half a thing*. Lolz.
So, we need a way to have an element to copy for the drag image, and for that element to be visible as far as the browser is concerned, but for it to be invisible as far as the user is concerned. How is this inconsistent miracle to be achieved?
<div id="drag-coveredup" class="dragdemo" draggable="true">drag me</div>
<div id="coverup"></div>
<style>
#coverup {
background: white;
width: 170px;
height: 100px;
position: absolute;
top: 0;
right: 0;
z-index: 2;
}
</style>
<script>
document.getElementById("drag-coveredup").addEventListener("dragstart", function(e) {
var crt = this.cloneNode(true);
crt.style.backgroundColor = "red";
crt.style.position = "absolute"; crt.style.top = "0px"; crt.style.right = "0px";
document.body.appendChild(crt);
e.dataTransfer.setDragImage(crt, 0, 0);
}, false);
</script>
What you do is: you position our element-to-copy somewhere specific on the screen, and then... put a div at the same position, in the background colour. So your element-to-copy is hidden. By a background-coloured box.
I'm going to hell when I die.
Stuart Langridge, June 2013