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:
- The user drops a file on to the client.
- The client sends the drop element id and the file to the server in a multipart POST for storage and analysis.
- The server sends back the POST response to the client with information on how to display the dropped file.
- The client presents the upload file’s information to the user.
The file drop event is handled by adding “ondrop” attribute to the HTML elements. The “ondrop” attribute calls the droppedFile Javascript 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 as the second argument. The drop element ID allows us to identify where the file was drop on the page.
<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).
/** Add a ondrop attribute to an element in the DOM, example: * ondrop="event.stopPropagation(); droppedFile('element-id', event);" * * @param element DOM element * @param elementID Element Id to be sent to droppedFile joutine. */ public static void addOnDrop(Element element, String elementID) { element.setAttribute("ondrop", "event.stopPropagation(); droppedFile(\'" + elementID + "\', event);"); }
The droppedFile Javascript routine creates a multipart HTTP POST request. On lines 6-9 the information needed for the POST is gathered and created, including the URL, files and boundary. The XMLHttpRequest is created on line 10 and upload progress monitoring is set up in lines 12-14. The POST is created on lines 16-35 and sent on line 43. The first part of the POST contains the element Id (dropElementID) and the subsequent parts (lines 26-33) contain the files that were drop on the page. Note, that a binary file get .getAsBinary()
is used on line 32.
The POST responses are handled by the request.onreadystatechange function on line 37-51, and are sent to the client through window.top.dropFileListener.droppedFile( );
calls on lines 45 and 49. Initially (readyState == 1
) the file name being uploaded along with the MIME boundary are sent to the client on lines 39-46. After succesful completion of the POST (readyState == 4
), the POST response is forward to the GWT application.
01 var boundary = null; 02 03 function droppedFile(dropElementID, event) { 04 if( window.top.dropFileListener ) { 05 if (event.dataTransfer) { 06 var url = secureURL + "upload"; 07 var token = document.cookie.substring( 26, document.cookie.indexOf("userName") - 12); 08 var files = event.dataTransfer.files; 09 boundary = "BoUnDaRy_._-_._-_._" + Math.floor(999999999999999 * Math.random()); 10 var request = new XMLHttpRequest(); 11 12 request.upload.onprogress = updateProgress; 13 request.upload.onload = loaded; 14 request.upload.onerror = loadError; 15 16 request.open("POST", url, true); // open asynchronous post 17 request.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary); 18 request.setRequestHeader("Session-Token", token); 19 20 var postContent = "--" + boundary + "\r\n" + 21 "Content-Disposition: info; name=\"drop-element-id\"\r\n" + 22 "\r\n" + 23 dropElementID + "\r\n" + 24 "--" + boundary; 25 26 for (var i = 0; i < files.length; i++) { 27 postContent = postContent + "\r\n" + 28 "Content-Disposition: file; name=\"file\"; filename=\"" + files[i].name + "\"\r\n" + 29 "Content-Type: application/xml\r\n" + 30 "File-Length: " + files[i].size + "\r\n" + 31 "\r\n" + 32 files[i].getAsBinary() + "\r\n" + 33 "--" + boundary; 34 } 35 postContent = postContent + "--\r\n"; 36 37 request.onreadystatechange = function() { 38 if(this.readyState == 1) { 39 var filesJSON = '"files":['; 40 for (var i = 0; i < files.length; i++) { 41 filesJSON = filesJSON + '"' + files[i].name + '"'; 42 if (i < files.length - 1) filesJSON = filesJSON + ', '; 43 } 44 filesJSON = filesJSON + ']' 45 window.top.dropFileListener.droppedFile( '{' + 46 '"requestBoundary": "' + boundary + '", ' + filesJSON + '}'); 47 } 48 if(this.readyState == 4) { 49 window.top.dropFileListener.droppedFile( request.responseText ); 50 } 51 } 62 53 request.sendAsBinary(postContent); 54 } 55 else { 56 alert("Your browser does not support file drag and drop. " + 57 "We recommend that upgrade your browser to one that support HTML5, " + 58 "such as Mozella's FireFox or Apple's Safari or Google's Chrome."); 59 } 60 } 61 else { 62 alert("window.top.dropFileListener not found!"); 63 } 64 }
The DropFileConnector class connects the dropFileListener in the Javascript to the GWT code. DropFileConnector class has a ClientSideDropFileSupport interface that defines the droppedFile method in the JavaScript. theObserver is a static ClientSideDropFileSupport incidence that connects to Javascript droppedFile method to ClientSideDropFile’s droppedFile method.
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(); } // This method 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 a ondrop attribute to an element in the DOM, 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 a OKAY or a FAIL to the POST response that the DropFileConnector handles. In our implementation the POST responces 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.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:
- Box.net and html5 drag and drop blog by Christopher Blizzasrd at http://hacks.mozilla.org/2010/06/html5-adoption-stories-box-net-and-html5-drag-and-drop/
- Mozilla’s developers Network page on Using XMLHttpRequest https://developer.mozilla.org/En/Using_XMLHttpRequest
- XHR progress and rich file upload feedback by Austin King at http://hacks.mozilla.org/2009/06/xhr-progress-and-richer-file-uploading-feedback/
Right on!
Hey, Thank you for this amazing info. It was very helpful to me and to the one who read this page. You truly is a expert writer!
Great blog, Simply wanted to be able to comment will not connect with the rss or atom supply, you may choose install the right wordpress tool for that in order to workthat.
Excellent post. I was checking continuously this blog and I am impressed! Extremely helpful information specifically the last part I care for such information a lot. I was seeking this particular info for a long time. Thank you and best of luck.
Does this work in all browsers? I don’t think it will work in IE.
Isn’t the sendAsBinary not supported in chrome
You are correct this currently only works with FireFox 4, 5 and 6. I looking into using FormData for Chrome and Safari, but have not had time to work out the details. I have yet to figure out how to get it to work with IE.