Thursday, March 28, 2024

Drag Files Into the Browser From the Desktop with HTML5


Drag Files into the Browser from the Desktop

HTML5 Drag & Drop lets you do basic drag & drop operations with a lot less JavaScript code. Two additional advantages to HTML5 Drag & Drop are the ability to combine it with other JavaScript utilities such as Ajax and the HTML5 FileReader and that it allows you to drag files directly from the Desktop, as in folders and Windows Explorer. Put that all together, and you can pull off some pretty impressive feats, like create a drop zone for image previews or file uploads. We’ll be tackling the former today and the next article will be dedicated to the uploading code. Today’s article will also suggest how to navigate the mine field that is asynchronous programming!

The File Display Page

In terms of HTML, all we need is a page that contains a few DIVs: One to display status messages, one in which to drop the files, and one to show the images:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=utf-8 />
<title>A File Display Demo</title>
<script type="text/javascript">
//script code will go here...
</script>
<style>
#drop {
  min-height: 150px;
  width: 250px;
  border: 1px solid blue;
  margin: 10px;
  padding: 10px;
}
</style>
</head>
<body>
  <h1 align=center>A File Preview Demo</h1>
  <DIV id="status">Drag the files from a folder to a selected area ...</DIV>

  <DIV id="drop">Drop files here.</DIV>
  <DIV id="list"></DIV>
</body>
</html>

Designating the “drop” DIV as a Drop Zone

Before anything happens, there is a check to make sure that the browser supports the HTML5 FileReader. Assuming it does, code is attached to the window’s onload() event so that the drop element is declared after the page has finished loading. To enable a DIV to accept dropped items, we have to cancel the default behavior for the ‘dragover’ and ‘dragenter’ events:

if(window.FileReader) { 
  addEventHandler(window, 'load', function() {
    var status = document.getElementById('status');
    var drop   = document.getElementById('drop');
    var list   = document.getElementById('list');
  	
    function cancel(e) {
      if (e.preventDefault) { e.preventDefault(); }
      return false;
    }
  
    // Tells the browser that we *can* drop on this target
    addEventHandler(drop, 'dragover', cancel);
    addEventHandler(drop, 'dragenter', cancel);
  });
} else { 
  document.getElementById('status').innerHTML = 'Your browser does not support the HTML5 FileReader.';
}

The addEventHanlder() is my own cross-browser implementation for binding a handler to an event:

function addEventHandler(obj, evt, handler) {
    if(obj.addEventListener) {
        // W3C method
        obj.addEventListener(evt, handler, false);
    } else if(obj.attachEvent) {
        // IE method.
        obj.attachEvent('on'+evt, handler);
    } else {
        // Old school method.
        obj['on'+evt] = handler;
    }
}

Processing Dropped Files

The ondrop event is where we place code to process the imported files. Again, we have to cancel the browser’s default behavior, which is to redirect to the dropped file. That’s OK if you want to display the image in the browser, but it limits the number to a single file, provides no additional information about the file, and does not allow you to perform any additional processing on it. Both the e.preventDefault() call and “return false” line at the end of the event handler play a role in cancelling the default browser behavior.

The event’s dataTransfer property contains a files collection that we can iterate through to process one file at a time. The FileReader.readAsDataURL() method is the one to use to display the image:

addEventHandler(drop, 'drop', function (e) {
  e = e || window.event; // get window.event if e argument missing (in IE)   
  if (e.preventDefault) { e.preventDefault(); } // stops the browser from redirecting off to the image.

  var dt    = e.dataTransfer;
  var files = dt.files;
  for (var i=0; i<files.length; i++) {
    var file = files[i];
    var reader = new FileReader();
      
    //attach event handlers here...
   
    reader.readAsDataURL(file);
  }
  return false;
});

The onloadend Event

When the FileReader has finished reading the file, it fires the onloadend event. That’s the time to do what ever it is you wish to do with the file. In our case, we’re going to display a progress report, some file information, and append the binary file contents to a list of images. That sounds so straight forward, but it isn’t on account of the asynchronous nature of the FileReader. For instance, just determining where we’re at in the processing order is a challenge. As we go through the loop, the i variable is incremented to the current file being read, but that’s the file reading order, not the order in which the files are finished being read! Imagine that you have two files where the first is several megs in size and the second is only about 35 KB. Unless something weird happens, you can bet that the second file’s onloadend will fire first. So don’t depend on the reading order to tell us the order in which the files have finished being read. Instead, find something that reflects the status of the file reads at the time that the onloadend handler executes. In my case, I chose to count the number of DIV tags in the file list to determine the number of files already processed (each file produces a information DIV and an IMG). Hence, the current file is one greater than what’s already been processed:

addEventHandler(reader, 'loadend', function(e, file) {
    var bin           = this.result; 
    var newFile       = document.createElement('div');
    newFile.innerHTML = 'Loaded : '+file.name+' size '+file.size+' B';
    list.appendChild(newFile);  
    var fileNumber = list.getElementsByTagName('div').length;
    status.innerHTML = fileNumber < files.length 
                     ? 'Loaded 100% of file '+fileNumber+' of '+files.length+'...' 
                     : 'Done loading. processed '+fileNumber+' files.';

    var img = document.createElement("img"); 
    img.file = file;   
    img.src = bin;
    list.appendChild(img);
}.bindToEventHandler(file));

We can’t get the full file path due to security restrictions, but we can display the image by setting it’s SRC property directly to the binary file content:

firebug_html_source_after_file_drag.gif

Binding the File to the Event Handler

File ordering is not the only problem caused by the asynchronous reading. Just as the i incrementor is out of date, so is the current file. In other words, the file that we are currently reading in the for loop is not necessarily the same one that we just finished reading. This is the same problem that JavaScript developers first encountered with Ajax callbacks. The optimal solution was to bind stateful data to the callback handler. Binding to an event handler is almost the same idea, except that you also need to deal with the event object. As you know, in Internet Explorer it is referenced in the global window.event property, while other browsers pass it directly to the handler. What I like to do is to have an argument for the event, and then set is as follows:

e = e || window.event; // get window.event if e argument missing (in IE)   

In my bindToEventHandler() Function method, I convert the bound parameters into a proper Array using Array.prototype.slice.call(). The I prepend the event object to the array via unshift(). The function-within-a-function structure of the bindToEventHandler() method causes a closure so that when the returned function is executed, it still has a reference to the original bound parameters. The real handler is then called using Function.apply():

Function.prototype.bindToEventHandler = function bindToEventHandler() {
  var handler = this;
  var boundParameters = Array.prototype.slice.call(arguments);
  //create closure
  return function(e) {
      e = e || window.event; // get window.event if e argument missing (in IE)   
      boundParameters.unshift(e);
      handler.apply(this, boundParameters);
  }
};

 

Going Forward

Today we saw how to use HTML5 Drag & Drop and FileReader to accept image files from the Desktop and display them in the browser. The key to getting this to work was to understand the implications of asynchronous operations. Now that we do, we’re going to sweeten the pot next time, and add some additional functionality by adding upload capability, error handling, and more detailed progress reports.

Robert Gravelle
Robert Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured