Dissecting Gmail’s Email Attachments

The ability to upload a file in a webpage has been possible almost since the beginning of the consumer web era. However, doing so was always limited by the capabilities of the file input control. You know the one, with the ugly “Browse” button.

Even as the Ajax revolution sparked innovation and creativity in web browsing, it took quite a long time for file uploads to evolve. The increasing desire of consumers to share photos and videos online pushed web technology for better ways to upload files. Today with HTML5, we finally have the ability to create more usable and natural interfaces for uploading files.

The Gmail Way

One of the first major sites to incorporate new technology into the file upload process was Gmail. Gmail has a long history of incorporating new technology as it becomes available and was the first to attempt a more sane approach to uploading files (using a single link instead of the traditional form input). But the Gmail team didn’t stop there.

Firefox 3.5 introduced the ability to drag files from the desktop directly into the browser for the purpose of uploading them to a server. The Gmail team took advantage of this innovation and created one of the first drag-and-drop email attachment systems on a major consumer site. They implemented this functionality as a progressive enhancement, keeping the normal email attachment flow for everyone (including those for whom drag-and-drop was possible). In fact, there was nothing in the interface to indicate that you could drag and drop a file from the desktop.

Default Gmail attachment functionality with a single “Attach a file” link

Gmail’s drag-and-drop file upload works the same way now as it did then: drag a file from your desktop over the compose email form and a drop area appears, allowing you to drop the file on to the email to create an attachment.

Once the file is dropped, it begins to upload immediately and Gmail shows you a progress bar indicating how much of the file has already been uploaded. Today, this functionality is available in many more browsers (Firefox, Chrome, Opera, Safari and Internet Explorer 10) but works fundamentally the same way as it did when only Firefox supported it.

In a manner of speaking, the drag-and-drop file upload functionality in Gmail is hidden: you might not know it’s there unless someone tells you or you accidentally drag something over the interface. On the other hand, there’s an argument to be made that drag-and-drop is the way email attachments should work. Most desktop email applications allow you to drag and drop a file on to the email to make an attachment. As web applications get closer to their desktop counterparts, it’s not entirely unreasonable to assume that people will try the same interactions for the same tasks.

HTML5 Drag-and-Drop

The first step to creating a drag-and-drop file upload system similar to Gmail’s is to capture the file being dragged over the webpage. There are three events that monitor this action:

  • dragenter fires as soon as the file is dragged over the webpage or element.
  • dragover fires next, firing repeatedly as the file is moved within the droppable area.
  • dragleave fires when the file is dragged out of the webpage or element.

Gmail shows a message as soon as the file is dragged over the webpage, a very useful hint for users. You can accomplish the same thing by listening for these three events on the document and showing or hiding another element by assigning a class value to the body element. For example:

function handleDrag(event) {

  event.preventDefault();

  switch(event.type) {
    case "dragenter":
      document.body.className = "dragging";
      break;

    case "dragover":
      // do anything else you may need
      break;

    case "dragleave":
      document.body.className = "";
      break;
  }
}

document.addEventListener("dragenter", handleDrag, false);
document.addEventListener("dragover", handleDrag, false);
document.addEventListener("dragleave", handleDrag, false);

Assuming that the CSS class dragging is used to show a message, this code accomplishes the same behavior as Gmail when a file is first dragged over the webpage.

Note that event.preventDefault() is called for all of the events. This is necessary to subvert browsers’ default behavior of opening a file when it is dropped on to a webpage. Because you are uploading a file, it’s necessary to cancel this default behavior and provide your own.

Dropping Files

Gmail doesn’t allow you to drop a file just anywhere on the page. It has to be over a specific element: the area where the default “Attach a file” link appears. Gmail doesn’t allow you to drop a file onto the primary textbox because textbox controls have their own drag and drop functionality that you don’t want to interfere with.

Drop area for Gmail attachments, replacing the “Attach a file” link

In general, it’s a good idea to limit the areas on the page where files can be dropped. Specific areas on the page provide context to the action of dropping a file, so you may want to have different behaviors based on where the file is dropped. If you’re dropping a file on to an email message, for example, it makes sense that you intend to attach that file to the message.

Once you’re able to detect the file has been dragged over the webpage, the next step is to listen for the file being dropped. That is accomplished by listening for the drop event on the drop target.

Any HTML element can become a drop target by listening for the drop event and canceling the default behavior. The files that were dropped are contained in event.dataTransfer.files, which is a collection of File objects. The collection is array-like, so you can access files by numeric index and also use the length property to determine how many files were dropped.

Adding this to the previous example, the following code manages the dropping of files over an element with an ID of droptarget:

function handleDrag(event) {

  event.preventDefault();

  switch(event.type) {
    case "dragenter":
      document.body.className = "dragging";
      break;

    case "dragover":
      // do anything else you may need
      break;

    case "dragleave":
      document.body.className = "";
      break;

    case "drop":
      if (event.target.id == "droptarget") {
        console.log("Dropped %d files", event.dataTransfer.files.length);
      }
      break;
  }
}

document.addEventListener("dragenter", handleDrag, false);
document.addEventListener("dragover", handleDrag, false);
document.addEventListener("dragleave", handleDrag, false);
document.addEventListener("drop", handleDrag, false);

This code listens for the drop event on the document, but only takes action if the correct drop target is used. In this case, the message is output to the console for debugging purposes (you should remove this in production), but it’s a very short jump from this to uploading a file.

Uploading the File

There are several ways to upload a file via Ajax, but the easiest way is to use a FormData object. These objects represent HTML form data and can be created dynamically at any point in time. One of the problems with older ways of uploading files was using custom server code to interpret data from the browser. That meant needing to have two different code paths on the server, one for regular form submission and one for Ajax submission. That’s a significant maintenance cost for any web application.

With FormData, you can send files to the server and make it look like the file was uploaded using a traditional HTML form. Doing so greatly simplifies your JavaScript code, as well as server code, which typically expects HTML form submissions. You need only create a FormData object, add the files directly into it, and then pass the object into an XMLHttpRequest object’s send() method. Here’s what the code looks like for the example:

function uploadFiles(files) {

  var data = new FormData();

  for (var i=0, len=files.length; i < len; i++) {
    data.append("File" + i, files[i]);
  }

  var xhr = new XMLHttpRequest();
  xhr.open("post", "/files", true);
  xhr.onload = function() {
    switch(this.status) {
      case 200: // request complete and successful
        console.log("Files uploaded"); //debug purposes
        break;
            
      default: // request complete but with unexpected response
        console.log("Files not uploaded"); //debug purposes
     }
  };

  xhr.send(data);

}

function handleDrag(event) {

  event.preventDefault();

  switch(event.type) {
    case "dragenter":
      document.body.className = "dragging";
      break;

    case "dragover":
      // do anything else you may need
      break;

    case "dragleave":
      document.body.className = "";
      break;

    case "drop":
      if (event.target.id == "droptarget") {
        uploadFiles(event.dataTransfer.files);
      }
        break;
  }
}

document.addEventListener("dragenter", handleDrag, false);
document.addEventListener("dragover", handleDrag, false);
document.addEventListener("dragleave", handleDrag, false);
document.addEventListener("drop", handleDrag, false);

Note that you just need to set the Ajax request to be a POST. Passing the FormData object into send() automatically sets all appropriate headers to ensure that the request is treated as if it were an HTML form submission. The files are added using append(), providing a name and passing a file as the value (you can always choose to accept just one file as well).

Monitoring Upload Progress

The last piece of the Gmail drag-and-drop interface is monitoring upload progress. This can be done using the onprogress event handler. The event object passed into onprogress has two important properties:

  • loaded, which indicates the number of bytes that have been sent
  • total, which is the total number of bytes to be sent

You can use these two values to come up with a percentage representing the progress of the upload. The onprogress handler is called multiple times as the data is being sent to the server, so you’re able to update the user interface with appropriate messaging.

Gmail’s file upload progress bar

In order to create a progress bar like Gmail’s, you can use a div or other element whose width represents the progress. For example:

<div class="progress-container">
    <div id="progress"></div>
</div>

<style>
.progress-container {
    width: 300px;
}

#progress {
    background: navy;
    height: 100%;
    width: 0;
}
</style>

The inner div in this example is initially set to a width of 0. As the files are uploaded, its width gradually increases to represent the overall upload progress. The JavaScript for monitoring the upload progress is as follows:

function uploadFiles(files) {

  var data = new FormData();

  for (var i=0, len=files.length; i < len; i++) {
    data.append("File" + i, files[i]);
  }

  var xhr = new XMLHttpRequest();
  xhr.open("post", "/files", true);
  xhr.onload = function() {
    switch(this.status) {
      case 200: // request complete and successful
        console.log("Files uploaded"); //debug purposes
        break;
            
      default: // request complete but with unexpected response
        console.log("Files not uploaded"); //debug purposes
     }
  };

  xhr.onprogress = function(event) {
    var percentage = event.loaded / event.total * 100;

    // update progressbar width
    var element = document.getElementById("progress");
    progress.style.width = percentage + "%";
  };

  xhr.send(data);

}

This code uses onprogress information to calculate the percentage of data that has been sent to the server. That percentage is then used as the width value of an element, indicating upload progress.

That’s It!

Looking back over the example in this article, you might be surprised to see that the total amount of JavaScript needed to implement a drag-and-drop file upload system similar to Gmail’s is remarkably small (less than 60 lines of code). That is all thanks to the HTML5 drag-and-drop API as well as FormData and onprogress for XMLHttpRequest. A complete code listing is available in a Gist.

Perhaps the best news about this functionality is that you can implement it in a progressively enhanced way. As with Gmail, you can (and should) provide a regular file input, so that file uploading is possible even without JavaScript. Adding this functionality on top of any web application that requires file uploads makes for happier users and better user experiences.

Going the Extra Mile

  • If you allow uploading multiple files, use a separate progress bar for each file. This helps to deal with file-specific issues during upload.
  • Provide a tooltip over the area where files can be dropped. The tooltip can contain instructions for how to use the drop area.
  • To allow uploading of multiple files without drag-and-drop, use the multiple attribute on an input.
  • Use the accept attribute to specify the types of files a file input control can accept.

Pitfalls to Avoid

  • Always verify the type of file being uploaded. Users may accidentally try uploading dangerous file types such as executables.
  • Make sure there’s a native form upload control available as a fallback for users without JavaScript enabled.
  • Avoid having a different server-side component for uploading via Ajax. Using the same server-side code for both Ajax and regular form submissions decreases maintenance overhead.

Things to Do

  • Use drag-and-drop file upload as a progressive enhancement on top of regular file upload functionality.
  • Provide a large drop area in an appropriate context for users to drop their files.
  • Display upload progress as files are being sent to the server.

Further Reading