File Drag and Drop onto a Google Web Toolkit Application

Users increasingly expect to be able to drag and drop objects in applications, including web applications. I would like to show you how we recently added the ability to drop files on to our enterprise electronic notebook iExperiment. iExperiment’s client side is built on Google Web Toolkit (GWT). GWT currently does not support file drag and drop.

The work flow for file drag and drop is as follows:

  1. The user drops a file on to the client.
  2. The client sends the drop element id and the file to the server in a multipart POST for storage and analysis.
  3. The server sends back the POST response to the client with information on how to display the dropped file.
  4. The client presents the uploaded file’s information to the user.

We are using cutting edge HTML5 APIs in our drag and drop implementation. Support of these APIs is not available in all browsers.

  • This code works today in recent versions of Chrome and FireFox.
  • In Safari there is an issue with extra characters being added to the file names, which also complicates the file type assignment.
  • Internet Explorer does not currently support the required APIs.

The file drop event is handled by adding an “ondrop” attribute to the HTML elements. The “ondrop” attribute calls the droppedFile JavasScript routine that creates and sends an HTTP POST request to the server. Note that the first argument in the droppedFile JavaScript call is an “element-id”, and the event is the second argument. The drop element ID allows us to identify where the file was dropped on the page.

Example of “ondrop” Attribute in an HTML Table Element

<table ondrop="event.stopPropagation();
               event.preventDefault();
               droppedFile('element-id', event);">

The “ondrop” element is created by the static addOnDrop method in a client side class (DropFileConnector).

addOnDrop Method

/** Add an ondrop attribute to an element in the DOM, for example:
 * ondrop="event.stopPropagation(); droppedFile('element-id', event);"
 *
 * @param element DOM element
 * @param elementID Element Id to be sent to droppedFile routine.
 */
 public static void addOnDrop(Element element, String elementID) {
     element.setAttribute("ondrop",
         "event.stopPropagation(); droppedFile(\'" + elementID + "\', event);");
 }

The droppedFile JavaScript routine creates a FormData element which is transferred to the server using a multipart HTTP POST request. The first two items appended to the form are the drop element ID (dropElementID) and boundary that is used by the request onreadystatechange function. Two appends are made to the form for each file that is uploaded; the file length (line 16), and the file itself (line 17) are appended to the form. On lines 20-24 the information needed for the POST is created, and the upload handlers are attached to the request. Note that by using a FormData element we are able to eliminate the deprecated FireFox specific methods files[i].getAsBinary(), and replace request.sendAsBinary(postContent) with request.send(formData). These methods were used in our original post to added the files to the POST, and to send the POST, respectively.

The POST responses are handled by the request.onreadystatechange function on line 26-40, and are sent to the client through window.top.dropFileListener.droppedFile method (calls on lines 34 and 38.) Initially when the read state is 1 (readyState == 1) the file name and the MIME boundary are sent to GWT client application on lines 28-35. After succesful completion of the POST (readyState == 4), the POST response is forwarded to the GWT client application.

droppedFile JavaScript Routine

01  var boundary = null;
02
03  function droppedFile(dropElementID, event) {
04      if (window.File && window.FileList && window.top.dropFileListener) {
05          if (event.dataTransfer) {
06              url = secureURL + "upload";
07              boundary = "Colabrativ-" + Math.floor(999999999999999 * Math.random());
08
09              var formData = new FormData();
10              formData.append("drop-element-id", dropElementID);
11              formData.append("boundary", boundary);
12
13              var files = event.dataTransfer.files;
14
15              for (var i = 0; i < files.length; i++) {
16                  formData.append("file-length-" + i, files[i].size);
17                  formData.append("file-"        + i, files[i]);
18              }
19
20              var request = new XMLHttpRequest();
21              request.upload.onprogress = updateProgress;
22              request.upload.onload     = loaded;
23              request.upload.onerror    = loadError;
24              request.open("POST", url, true);
25
26              request.onreadystatechange = function() {
27                  if (this.readyState == 1) {
28                      var filesJSON = '"files":[';
29                      for (var i = 0; i < files.length; i++) {
30                          filesJSON = filesJSON + '"' + files[i].name + '"';
31                          if (i < files.length - 1) filesJSON = filesJSON + ', ';
32                      }
33                      filesJSON = filesJSON + ']'
34                      window.top.dropFileListener.droppedFile( '{' +
35                          '"requestBoundary": "' + boundary + '", ' + filesJSON + '}');
36                  }
37                  if (this.readyState == 4) {
38                     window.top.dropFileListener.droppedFile( request.responseText );
39                  }
40              }
41
42              request.send(formData);
43          }
44          else {
45              alert("Your browser does not support file drag and drop.  " +
46                    "We recommend that you upgrade your browser to one that supports HTML5, " +
47                    "such as Mozilla's FireFox or Google's Chrome.");
48          }
49      }
50      else {
51          alert("window.top.dropFileListener not found!");
52      }
53  }

The DropFileConnector class connects the dropFileListener in the JavaScript to the GWT code. The DropFileConnector class has a ClientSideDropFileSupport interface that defines the droppedFile method in the JavaScript. The observer (theObserver) is a static ClientSideDropFileSupport incidence that connects to the JavaScript droppedFile method to ClientSideDropFile’s droppedFile method.

DropFileConnector class

package com.colabrativ.common.client; 

import com.google.gwt.dom.client.Element; 

public class DropFileConnector {
    /** Used to pass information to the GWT application. Register your implementations with
      *  {@link com.colabrativ.common.client.DropFileConnector#connect( ClientSideDropFileSupport)}
      */
    public interface ClientSideDropFileSupport {
        /** The page calls this method to send dropped file information to application
          *
          * @param json JSON string containing the file name information.
          */
        void droppedFile(String json);
    } 

    /** An observer of drop file events, supplied by the GWT application. */
    static private ClientSideDropFileSupport theObserver; 

    /**
      * Connect the GWT application to the drop file JavaScript. The observer is notified
      * whenever the user drops a file on the application area (id="app-area").
      *
      * @param observer
      */
     public static void connect( ClientSideDropFileSupport observer ) {
         theObserver = observer;
         connectToPanel();
     }

     // These methods are "glue" methods that let the simple JNDI call talk to the observer through its interface.
     @SuppressWarnings("all")
     private static void droppedFile(String s) { theObserver.droppedFile(s); }

     private static native void connectToPanel() /*-{
         var listener = new Object();
         listener.droppedFile = function(json) {
             json = unescape( json );
             @com.colabrativ.common.client.DropFileConnector::droppedFile( Ljava/lang/String;)( json );
         };

         $wnd.top.dropFileListener = listener;
     }-*/; 

     /**
       * Add an ondrop attribute to an element in the DOM, for example:
       * ondrop="event.stopPropagation(); droppedFile('element-id', event);"
       *
       * @param element DOM element
       * @param elementId Element Id to be sent to droppedFile routine.
       */
      public static void addOnDrop(Element element, String elementId) {
          element.setAttribute("ondrop",
              "event.stopPropagation(); droppedFile(\'" + elementId + "\', event);");
      }
} 

Finally, a DropFileConnector.connect call is made in a client class that handles the POST responses. Our upload servlet adds either an OKAY or a FAIL to the POST response that the DropFileConnector handles. In our implementation, the POST responses are encapsulated in JSON. The upload progress monitoring is also handled by the DropFileConnector. In our implementation, the MIME boundary is used as an Id for the upload, and the monitoring is managed by the singleton of the UploadManager class.

DropFileConnector Connection

DropFileConnector.connect(
    new DropFileConnector.ClientSideDropFileSupport() {
        public void droppedFile(String response) {
            if (response.startsWith("OKAY:")) {
                String json = response.substring( response.indexOf(": ") + 2); // Remove OKAY:
                determineWhatToDoWithResponse(json);
            }
            else if (response.startsWith("FAIL:")) {
                // Do Nothing?
            }
            else {
                JSONObject responseJSON = (JSONObject) JSONParser.parseStrict( response );
                JSONString boundaryJSON = (JSONString) responseJSON.get( UploadResponse.requestBoundary);

                if (boundaryJSON != null) {
                    String boundary = boundaryJSON.toString();
                    JSONArray filesJSON = (JSONArray) responseJSON.get( UploadResponse.files);

                    if (filesJSON != null) {
                        UploadManager.getInstance().addProgressDialog( boundary, filesJSON);
                    }
                    else {
                        UploadManager.getInstance().update( boundary, responseJSON);
                    }
                }
            }
        }
    }
); 

Several resources helped us create our file drag and drop; these included:

  1. W3C XMLHttpRequest Level 2 http://www.w3.org/TR/XMLHttpRequest2/
  2. W3C File API http://www.w3.org/TR/file-upload/
  3. Using FormData object in Mozilla’s Developers Network page on Using XMLHttpRequest at https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest#Using_FormData_objects
  4. How to Use HTML5 File Drag & Drop by Craig Buckler at http://www.sitepoint.com/html5-file-drag-and-drop/
  5. XHR progress and rich file upload feedback by Austin King at http://hacks.mozilla.org/2009/06/xhr-progress-and-richer-file-uploading-feedback/
Posted in Technical | Tagged , , , , | Comments Off