Search code examples
javascripthtmlcssdrag-and-drophtml5-draggable

HTML5 DnD - Why is ghost image only working as expected AFTER dragging & dropping an element?


I'm trying to get a p element with a nested img to show as the ghost image when dragging.

I'm not sure how to debug this but I have noticed that once the images are cached or have been dragged and dropped somewhere on the page, it works as expected. I've made a MWE here:

gif_of_dragging

The smiley face is dragged on first load of the page and shows the erroneous behavior - the emoji doesn't show during the drag. The sad face is dragged, released, and then redragged, which results in the expected behavior - the emoji does show as part of the ghost image. This is true of all the images.

What I've tried: I thought it might be an issue with the way the page elements are loaded, so I moved the javascript to the bottom of the body (trying to ensure all elements are loaded before the script runs). This doesn't solve the issue.

MWE code: I got the emojis from here, but I guess any pngs you have lying around on your machine will do to reproduce this.

index.php:

<html>
<head>
  <link rel="stylesheet" type="text/css" href="stylesheet.css">
</head>
<body>
  <h2>Order these items</h2>
  <div id="main_wrapper">
    <?php
      $json = json_decode(file_get_contents("image_set.json"), true);
      echo '<div id="home_container" ondrop="drop(event, this)" ondragover="allowDrop(event)">';
      $i = 0;
      foreach($json as $k => $v)  {
        echo '<p class="drag_item" draggable="true" ondragstart="drag(event)" id="drag'.$i.'"><img draggable="false" src="/images/'.$v['fn'].'" width=200 height=200>'.$v['text'].'</p>';
        $i++;
      }
      echo '</div>';
    ?>
    <div id="buffer" style="min-height:100px; width:100%;"></div>
    <div id="dropzone_wrapper">
    <?php
      for($i = 0; $i < count($json); $i++) {
        echo '<div class="dropzone" id="dropzone'.$i.'" ondrop="drop(event, this)" ondragover="allowDrop(event)"></div>';
        if($i < count($json)-1){echo '&lt;';}
      }
    ?>
    </div>
    <div id="msg"></div>
  </div>
  <script>
  function allowDrop(ev) {
    ev.preventDefault();
  }

  function drag(ev) {
    var dataList = ev.dataTransfer.items;
    dataList.add(ev.target.id, "text/plain");
  }

  function drop(ev, el) {
    ev.preventDefault();
    var data            = ev.dataTransfer.getData("text");
    var element_to_drop = document.getElementById(data);
    let droppable       = true;

    // If the dropzone already contains something (not text due to
    // spaces in markup being counted as text), don't allow 
    // another drop to occur.
    if (el.childNodes.length > 0) {
      el.childNodes.forEach(function(obj) {
        if(obj.nodeName != '#text') {
          droppable = false;
        }
      });
    }

    if(droppable)
      el.appendChild(document.getElementById(data));
  }

  function reset() {
    // Put all drag items back into the home container
    let home  = document.getElementById('home_container');
    let cards = document.querySelectorAll('.drag_item');

    for(var i = 0; i < cards.length; i++) {
      home.appendChild(cards[i]);
    }
  }
  </script>
</body>
</html>

image_set.json:

{
  "happy": {
    "fn":"happy.png",
    "text":"A happy face"
  },
  "sad": {
    "fn":"sad.png",
    "text":"A sad face"
  },
  "angry": {
    "fn":"angry.png",
    "text":"An angry face"
  },
  "confused": {
    "fn":"confused.png",
    "text":"A confused face"
  },
  "sleepy": {
    "fn":"sleepy.png",
    "text":"A sleepy face"
  }
}

stylesheet.css:

* {
  box-sizing:border-box;
  padding:0px;
  margin:0px;
  font-family:sans-serif;
  font-weight:100;
}

body {
  padding:20px;
}

h2 {
  padding:20px 0;
  font-size:4em;
}

p.drag_item {
  text-align:center;
  transition:0.5s;
  width:200px;
  height:200px;
}

.drag_item:hover {
  cursor:move;
}

#home_container, #dropzone_wrapper {
  min-height:200px;
  width:100%;
  display:flex;
  flex-direction:row;
  justify-content:space-around;
  margin:20px 0;
  align-items:center;
}

#dropzone_wrapper {
  font-size:3em;
}

#dropzone_wrapper p {
  font-size:initial;
}

#home_container {
  border:1px solid black;
  border-radius:8px;
  background-color:#e5e5e5;
}

#home_container p {
  width:200px;
  font-size:16px;
}

#msg {
  display:block;
  font-size:2.5em;
}

.dropzone {
  min-height:200px;
  width:200px;
  border:1px dashed black;
  background-color:#00a8bd;
}

Solution

  • I've done a bit research to find the problem. This was a bit hard for me, because Firefox was the only browser where the ghost image was not shown on the first load of the page and the first drag. I opened the Network tab and found out that the image is only requested on the first drag (which I don't really understand, because the images were completely loaded).

    Anyways, I finally I managed to get this to work, by changing the draggable element to the image instead of the paragraph.

    index.php:

    <div id="main_wrapper">
        <?php
            $json = json_decode(file_get_contents("image_set.json"), true);
            echo '<div id="home_container" ondrop="drop(event, this)" ondragover="allowDrop(event)">';
            $i = 0;
            foreach($json as $k => $v)  {
                echo '<p class="drag_item"><img ondragstart="drag(event)" id="drag'.$i.'" draggable="true" src="images/'.$v['fn'].'" width=200 height=200>'.$v['text'].'</p>';
                $i++;
            }
            echo '</div>';
        ?>
        <div id="buffer" style="min-height:100px; width:100%;"></div>
        <div id="dropzone_wrapper">
            <?php
                for($i = 0; $i < count($json); $i++) {
                    echo '<div class="dropzone" id="dropzone'.$i.'" ondrop="drop(event, this)" ondragover="allowDrop(event)"></div>';
                    if($i < count($json)-1){echo '&lt;';}
                }
            ?>
        </div>
        <div id="msg"></div>
    </div>
    

    JS:

    function allowDrop(ev) {
        ev.preventDefault();
    }
    
    function drag(ev) {
        // get the cursor position relative to the element
        var x = (ev.pageX - ev.target.offsetLeft) + document.body.scrollLeft;
        var y = (ev.pageY - ev.target.offsetTop) + document.body.scrollTop;
    
        ev.dataTransfer.setData("text", ev.target.id);
    
        // set the parent element (the paragraph) as the custom ghost image and set the position of the ghost image (x, y)
        ev.dataTransfer.setDragImage(ev.target.parentElement, x, y);
      }
    
    function drop(ev, el) {
        ev.preventDefault();
        var data = ev.dataTransfer.getData("text");
        var element_to_drop = document.getElementById(data);
        let droppable = true;
    
        if (el.childNodes.length > 0) {
            el.childNodes.forEach(function(obj) {
                if(obj.nodeName != '#text') {
                    droppable = false;
                }
            });
        }
    
        if(droppable)
            el.appendChild(document.getElementById(data).parentElement);
    }
    
    function reset() {
        let home = document.getElementById('home_container');
        let cards = document.querySelectorAll('.drag_item');
    
        for(var i = 0; i < cards.length; i++) {
            home.appendChild(cards[i]);
        }
    }
    

    This works pretty well in: Chrome, Edge, IE 11

    NOTE: This only works perfectly in Firefox (the paragraph text only appears in this browser)