5

Does HTML5/Javascript support dragging an image onto a webpage?

i.e.

Can I create a webpage to allow user to drag an image from another site directly onto a placeholder on my site (all client side), then use another button to upload that image back to server?

insertusernamehere
  • 23,204
  • 9
  • 87
  • 126
Paul Taylor
  • 13,411
  • 42
  • 184
  • 351
  • See https://stackoverflow.com/questions/11214053 – Farhad Mortezapour Mar 10 '18 at 10:17
  • @insertusernamehere that link requires server calls because of cors – Paul Taylor Mar 10 '18 at 12:30
  • @FarhadMortezapour seems that gets me the url but doesn't actually display the image (or does it ?) – Paul Taylor Mar 10 '18 at 12:31
  • Normally no, but some browser might have different implementations. What I've seen is that when you drag an from one page, only the markup is actually dragged (along with src and some browser specific values) but not the image data itself. This means that while you'll be able to display this image, you'll still be tied by same-origin policy and thus won't be able to upload it as File in this case (but you could still fetch it from your server with only the url). What can be done though is 1, drag&drop on user disk, 2 d&d on your page. Here you will be able to catch the File. – Kaiido Mar 10 '18 at 13:36
  • @Kaiido hmm, so how should I do instead can you explain steps to achieve this in normal way please. – Paul Taylor Mar 10 '18 at 16:21

1 Answers1

4

I can't tell for sure how all browsers did / do / will behave in such a case, but what I've seen is that when you drag an image from a webpage and drop it to a webpage (the same or an other one), browsers won't create a File (or Blob) from it, but only its markup.

This means that you will still be tied by cross-origin policies in this case, i.e you won't be able to access the data of the image file, and thus won't be able to send it to your server either.

But this won't prevent you from displaying this image on your page, nor to grab its src.

So what you can do, is a two cases handling:

  1. Your user drops a File from its file-system: you will have to send it to your server, best through a FormData as multipart. To display it, create a new <img> element and set its src to a blobURI made from the File.
  2. They drop from an other webpage: The dataTransfer should contain a text/html item. If so, try to parse it and look for existing <img> elements. If there are, you can adopt them in your current document for display. However to save it on your server, you will have to fetch from your server directly (which is not tied by the same-origin policies), and thus you will have to send this URIs to your server.

var dropzone = document.getElementById('dropzone'),
  send_btn = document.getElementById('send'),
  res = document.getElementById('res'),
  imgList = [];

dropzone.ondragover = function ondragover(e) {
  e.preventDefault();
  dropzone.classList.add('dragover');
};
dropzone.ondrop = function ondrop(e) {
  e.preventDefault();
  dropzone.classList.remove('dragover');
  // try to get images from this dropevent
  var imageObjects = retrieveImageData(e.dataTransfer);
  if (!imageObjects) return;
  imageObjects.forEach(function appendToDoc(imgObj) {
    res.appendChild(imgObj.element);
  });
  // store it
  imgList = imgList.concat(imageObjects);
  if (imageObjects.length)
    send_btn.disabled = false;
};
dropzone.ondragexit = function(ondragexit) {
  dropzone.classList.remove('dragover');
};

function retrieveImageData(dT) {
  // first try to get Files
  var files = getFiles(dT);
  if (files.length) {
    return files;
  }
  // if none, try to get HTMLImage or SVGImage
  var elems = getHTMLMarkup(dT);
  if (elems && elems.length) {
    return elems;
  }
  // we could also try to getData('text/plain') hoping for an url
  // but this might not be such a good idea...
  console.warn('unable to retrieve any image in dropped data');
}

function getFiles(dT) {
  // quite simple: won't traverse folders
  var files = [],
    imgObj;
  if (dT.files && dT.files.length) {
    for (var i = 0; i < dT.files.length; i++) {
      // only image Files
      if (dT.files[i].type.indexOf('image/') === 0) {
        imgObj = {
          type: 'file',
          element: new Image(),
          file: dT.files[i]
        };
        imgObj.element.onerror = onIMGError;
        imgObj.element.src = URL.createObjectURL(imgObj.file);
        files.push(imgObj);
      }
    }
  }
  return files;
}

function getHTMLMarkup(dT) {
  var markup = dT.getData('text/html');
  if (markup) {
    var doc = new DOMParser().parseFromString(markup, 'text/html');
    var imgs = doc && doc.querySelectorAll('img,image') ||  [];
    imgs.forEach(toImageObject);
    return Array.prototype.map.call(imgs, toImageObject);
  }

  function toImageObject(element) {
    var img;
    if (element instanceof SVGImageElement) {
      img = new Image();
      img.src = element.getAttributeNS('http://www.w3.org/1999/xlink', 'href') ||
        element.getAttribute('href');
    } else {
      img = document.adoptNode(element);
    }
    img.onerror = onIMGError;
    return {
      type: 'element',
      element: img
    };
  }
}
// Once we got everything, time to retrieve our objects
send_btn.onclick = function sendData() {
  var fD = new FormData();
  // send Files data directly
  var files = imgList.filter(function isFile(obj) {
    return obj.type === 'file';
  });
  files.forEach(function appendToFD(obj) {
    fD.append('files[]', obj.file);
  });
  // for elems, we will need to grab the data from the server
  var elems = imgList.filter(function isElem(obj) {
    return obj.type === "element";
  });
  var urls = elems.map(function grabURL(obj) {
    return obj.element.src;
  });
  if (urls.length)
    fD.append('urls', JSON.stringify(urls));

  sendFormData(fD);
};

function sendFormData(fD) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', 'your_url');
  // you would normally send it
  //xhr.send(fD);

  // but here we will just log the formData's content
  var files = fD.getAll('files[]');
  console.log('files: ', files);
  var urls = fD.get('urls');
  console.log('urls', urls);
}
// in case we can't load it
function onIMGError() {
  var img = this;
  var index = -1;
  imgList.forEach(function search(obj, i) {
    if (index < 0 && obj && obj.element === img)
      index = i;
  });
  // remove from our list
  if (index > -1) {
    imgList.splice(index, 1);
    if (img.parentNode) img.parentNode.removeChild(img);
  }
}
#dropzone {
  width: 300px;
  height: 150px;
  border: 1px solid black;
}

#dropzone.dragover {
  background: rgba(0, 0, 0, .5);
}

#res {
  border: 1px solid black;
}
<div id="dropzone">drop here</div>
<button id="send" disabled>log saved objects</button>
<div id="res">results:<br></div>

Also note that the default of a contenteditable container would be to display this <img>. I didn't wanted to integrate this case in this long enough example, but I'm sure you'll find a way to handle it if needed.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • @Kalido, thankyou I got your example working on my own page but its not quite what I want, is it possible to drag the image into the dropzone and have it displayed in the dropzone rather than the seperate res div. And it is possible fro the dropzone to contain an existing Image (from when page originally loaded) and then when you drop a new image onto it that replaces the image rather than appends to the dropzone area, My thinking is user can simply replace existing image by drag/dropping image over it then when they click on Start button it can actually be uploaded to server – Paul Taylor Mar 13 '18 at 10:26
  • @Kalido I got it to drop into dropzone by modifying line in appendToDoc from res.appendChild(imgObj.element) to dropzone.appendChild(imgObj.element) however the webpage doesn't resize to allow for fittig image into dropzone instead it just extends past it and displays ontop of other html ! – Paul Taylor Mar 13 '18 at 19:01
  • @Kalido the code only needs to support dropping a single image rather than multiple images, as I say I dont actually want to append images so if they drag a second image that should replace the first. – Paul Taylor Mar 13 '18 at 19:04
  • @Kalido okay I the basics working except for my server is not receiving the data. I have marked your question right but riased another question if you have ideas https://stackoverflow.com/questions/49325607/correct-content-type-for-sending-this-ajax-post-data – Paul Taylor Mar 16 '18 at 18:49