html drag/drop setDragImage doesnt set ghost image on first drag

8.3k views Asked by At

I am trying to create a drag and drop menu where a user can drag an image thumbnail from a div to a canvas.

The issue is the source div uses a sprite to display its a background thumbnail, so I have to use the setDragImage to allow an image to be displayed whilst dragging the div.

I can successfully drag the div to the canvas and drop the image fine, however my problem is whilst dragging the ghost image is not shown until the second time I drag the div.

I use this code from a previous answer: [HTML5 Drag and Drop events and setDragImage browser support

and here's my slightly modified version of this code:

var isIE =  (typeof document.createElement("span").dragDrop === "function");


$.fn.customDragImage = function(options) {
    var offsetX = 0,
        offsetY = 0;

    var createDragImage = function($node, x, y) {
        var $img = $(options.createDragImage($node));
        icon = "icon" + window.draggedimgsrc;
        offsetX = window[icon][2] / 2;
        offsetY = window[icon][3] / 2;
        $img.css({
            "top": Math.max(0, y-offsetY)+"px",
            "left": Math.max(0, x-offsetX)+"px",
            "position": "absolute",
            "pointerEvents": "none"
        }).appendTo(document.body);

        setTimeout(function() {
            $img.remove();
        });

        return $img[0];
    };

    if (isIE) {
        $(this).on("mousedown", function(e) {
            var originalEvent = e.originalEvent,
                node = createDragImage($(this), originalEvent.pageX, originalEvent.pageY);
            node.dragDrop();
        });
    }

    $(this).on("dragstart", function(e) {
       var originalEvent = e.originalEvent,
           dt = originalEvent.dataTransfer;
        if (typeof dt.setDragImage === "function") {
            node = createDragImage($(this), originalEvent.pageX, originalEvent.pageY);
            console.log("node="+node);
            dt.setDragImage(node, offsetX, offsetY);  
        }
    });

    return this;

};

$("[draggable='true']").customDragImage({
    createDragImage: function($node) {
        //init icon [0] = icon filename | [1] = icon set | [2] = icon width | [3] = icon height
        icon = "icon" + window.draggedimgsrc;
        window.draggedimgset = window[icon][1];
        image="/boards/markers/soccerm/set" + window[icon][1] + "/" + window[icon][0] + ".png";
        return $node.clone().css("width", window[icon][2]).css("height", window[icon][3]).css("background", "transparent url(" + image + ") no-repeat center");        }
}).on("dragstart", function(e) {
    e.originalEvent.dataTransfer.setData("Text", "Foo");
});

What's strange is that when I pop a border on the $node.clone() it gets set when i do the first drag it just doesn't seem to put the image in there.

I've also put a manual width and height in so I know its not the size of the image.

And I preload the image before the menu appears.

Any ideas?

3

There are 3 answers

0
benvc On BEST ANSWER

Pre-loading the drag feedback images outside of the event listener seems to avoid the issue where the drag image does not appear on the first drag. There are a variety of ways to do this and the best one is highly dependent on exactly what you are trying to do. Following is an approach, where the url for the drag image is stored in a data attribute on the draggable element and the drag feedback image for each element is created and stored in an object beforehand.

Following are jQuery and vanilla JS examples:

const images = {};
const draggable = $('div');
draggable.each((i, elem) => {
  const src = $(elem).data('src');
  const img = new Image();
  img.src = src;
  images[src] = img;
});

draggable.on('dragstart', (event) => {
  const src = $(event.currentTarget).data('src');
  const img = images[src];
  const drag = event.originalEvent.dataTransfer;
  drag.setDragImage(img, 0, 0);
  drag.setData('text/uri-list', src);
});
div {
  border: 1px solid #000000;
  height: 100px;
  width: 100px;
  line-height: 100px;
  text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div draggable="true" data-src="https://via.placeholder.com/50/0000FF">drag me</div>
<div draggable="true" data-src="https://via.placeholder.com/50/FF0000">drag me</div>

const images = {};
const elems = document.querySelectorAll('div');
for (const elem of elems) {
  const source = elem.dataset.src;
  const image = new Image();
  image.src = source;
  images[source] = image;
  elem.addEventListener('dragstart', (event) => {
    const src = event.currentTarget.dataset.src;
    const img = images[src];
    event.dataTransfer.setDragImage(img, 0, 0);
    event.dataTransfer.setData('text/uri-list', src);
  });
}
div {
  border: 1px solid #000000;
  height: 100px;
  width: 100px;
  line-height: 100px;
  text-align: center;
}
<div draggable="true" data-src="https://via.placeholder.com/50/0000FF">drag me</div>
<div draggable="true" data-src="https://via.placeholder.com/50/FF0000">drag me</div>

1
Ashley Oakes On

So this is old, but I've just been struggling with this as well. I found that the issue was that I was creating the ghost image inside the dragstart event, which I see you're doing as well.

So the drag would have already started by the time you create your custom ghost image, meaning it wont be visible until you start a new drag.

So just try calling createDragImage outside of the dragstart. Worked for me.

0
Zied Hf On

I agree with this response. And I would add a small detail maybe it will be useful for someone else.

I found the same problem in a dedicated React Hook that I created to manage my DnD logic in my App when I display the preview (especially with icons or images) in the onDragStart event.

Adding the preview when the component mount will solve the problem.

However, since I don't want display the preview into the DOM always, I decided to display the element for few seconds when the component mount then after let's say 5 seconds I remove it.

Then I display the element as the first process of onDragStart. It will be displayed as expected thanks to the cache.

An Example :

React.useEffect((
  createPreview(previewElement); // Will add the preview element to the document
  setTimeout(() => removePreview(previewElement), 5000); // will remove it, give it sometimes to load the images
), []);

const onStartDrag = (event) => {
  createPreview(previewElement);
  // code
  event.dataTransfer.setDragImage(previewElement, x, y);
};

const onDragEnd = () => {
  removePreview(previewElement);
};