You are here: Home Web Java Creating a Mozilla/Firefox Drag and Drop file upload Script
Search
Advanced Search…
E-Mail

Webmail: webmail.wyden.com

E-Mail Preferences: postfix.wyden.com/users

E-Mail Administration: postfix.wyden.com

Statistics
Total: 473
Total Pages: 286
Total Folders: 87
Total Files: 18
Total Links: 26
Last modification: 19.04.2012 15:21
 

Creating a Mozilla/Firefox Drag and Drop file upload Script

by Wyden Silvan last modified 18.11.2009 09:38

There has been a lot of focus lately on web applications which have enhanced rich-client functionality, such as the oft-published AJAX approach adopted by Google Maps and GMail. One of the features which is present for desktop applications but which is difficult to accomplish with a web page is drag and drop of something that didn’t start off on the webpage (for example, dragging an image around a webpage is fairly easy to do, however dragging a file or shortcut from the desktop onto the webpage is much more difficult). In addition, although such functionality exists and is fairly easy to access with an ActiveX control for Internet Explorer, it is difficult to replicate inside of a Mozilla/Firefox browser due to the inherent complexities of cross-platform support as well as the stronger security model.

However, difficult does not mean impossible, and this article will describe how to create a control which allows a user to drag a file onto a webpage and upload it (automatically if desired) to a given server. I will mention now that this first part of the discussion will create a file which functions correctly on the local machine – Part 2 will deal with the difficulties of packaging up this code in a Jar, signing that Jar, and deploying it on a remote server.

Overview of what will be done

Before we dive in, I will give you a quick run-down of what we will accomplish with this code example. The steps which need to be followed are:

 


  1. Register for the Mozilla/Firefox drag and drop event, and attach an event handler

  2. Request permission to access the Mozilla XPConnect bridge

  3. Retrieve the low-level platform drag and drop event from the operating system (wrapped by a Mozilla XPCOM object)

  4. Determine if the data being dropped is in a format which we can handle

  5. Assuming valid file data, get the filename(s) which are being dropped onto the app

  6. Request permission to read a file from the local file system

  7. Save the filename(s) in the upload form’s file input control

  8. Marking the event as handled

Registering for the Drag and Drop event

The first thing that we need to do is tell the Mozilla/Firefox window that we want to register for the drag-and-drop event. Now, the way it would normally work for an event is that we would do the following:

window.addEventListener("dragdrop", dragDropHandler, true);

This would register the method dragDropHandler with the dragdrop event for the window. However, just doing this will not work correctly. We need to first tell the browser window explicitly that we want drag and drop events, and then register for the event. Code for this looks like:

window.captureEvents(Event.DRAGDROP);
window.addEventListener("dragdrop", dragDropHandler, true);

Finally, we’ll want to defer doing this until the page has loaded, to ensure that all the HTML and scripts are present before we try to use this. So, the final code for this will look like:

function loadMethod()
{
  // We need to register the window to capture Drag & Drop events
  // before we can receive them with our event handler.
  window.captureEvents(Event.DRAGDROP);

  // Register the event handler, and set the 'capture' flag to true
  // so we get this event before it bubbles up through the browser.
  window.addEventListener("dragdrop", dragDropHandler, true);
}
…
<body onload=”loadMethod();” …>

Requesting permission to access the XPConnect bridge

In Mozilla, the main browser functionality is wrapped in a library called XPCOM. This library is what handles most of the lower-level interactions with the host operating system, and is designed with cross-platform considerations in mind. If you wish to access XPCOM functionality from within a web page of some kind, you must use the XPConnect bridge which handles interfacing between XPCOM and Javascript. For a much more in-depth explanation of how all of these components work together, please see

the Mozilla XPConnect Page

.

 

Since XPConnect provides a lot of system access, permission to access it is only given after a security check. In order to request this security check, we must do the following:

netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');

This will ask the browser for the ability to communicate with the XPConnect bridge. If the page is not already a trusted page, this will cause a dialog to appear for the user asking them if they wish to grant permission for the page to "install and run software" (which I suppose is the primary use of the XPConnect bridge).

 

A couple of things about the Mozilla permissions system need to be pointed out right now, as they will affect how this drag and drop example is built:

 


  • Only a script on the local machine’s file system or a signed script on a webpage can request that a privilege be enabled. If you attempt to call this method from an unsigned script, it will throw an exception. This does not matter for Part 1, but is the reason for Part 2 of this discussion

  • An enablePrivilege request has the same scope as a variable – if you are inside a method and request enablePrivilege, that privilege will go away when the method’s execution ends. Also, requesting the permission in a <SCRIPT> block on page startup will *not* grant that permission for the whole page – it will go away with the </SCRIPT> tag that ends the block

As a result of the second point, we must include the enablePrivilege call inside of our drag and drop handler, i.e.:

function dragDropHandler (evt) {
 
  // Request the XP Connect privilege so we can access the XPCOM API.
  // This will cause a dialog to be displayed to the user asking them
  // if they want to grant this privilege to this script.
  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
…
}

Retrieve the Drag and Drop Event from Mozilla

Now we’re going to get into the interesting stuff – how to load classes from XPCOM which handle the drag and drop operation. To start with, we need to load in the Mozilla Drag Service. This is done with:

var dragService =
  Components.classes["@mozilla.org/widget/dragservice;1"]
    .getService(Components.interfaces.nsIDragService);

What this line does is load in the Mozilla drag service class from the table of classes, and then retrieve the nsIDragService class (

API Reference for nsIDragService available here

) from that loaded class and return it. This class handles the creation and destruction of drag and drop sessions, and we will use it to retrieve the current drag-and-drop session with:

var dragSession = dragService.getCurrentSession();

We now have the in-progress drag and drop session, which is an nsIDragSession (

API Reference for nsIDragSession available here

). In order to draw the data out of this session, we need to create a new nsITransferable (

API Reference for nsITransferable available here

) and choose the types of drag-and-drop data we want to receive. This is done with:

var transferObject =
  Components.classes["@mozilla.org/widget/transferable;1"]
    .createInstance();
 
// Bind the object explicitly to the nsITransferable interface. We need to do
// this to ensure that methods and properties are present and work as expected
// later on.
transferObject =
  transferObject.QueryInterface(Components.interfaces.nsITransferable);

Take note of the second line of code above – the nsITransferable needs to be explicitly created/casted using the QueryInterface call to ensure that all the properties and methods of nsITransferable are present on the Javascript object.

Determining the Format of the Data being Dropped

So we’ve created an nsITransferable object, but we don’t yet know what types of data are present in the drag and drop session. In order to check if certain MIME types are supported by the session, we would call:

var isSupported = dragSession.isDataFlavorSupported("application/x-moz-file");

This will return a boolean (true or false) indicating whether the given MIME type is supported. For the sake of this example, I have foregone checking of the MIME type of the session data – this will result in an error if the thing dragged onto the Mozilla window is not a file.

 

Based on the assumption that a file will be present, I have chosen to only request the application/x-moz-file data flavor from the drag-and-drop session – this is the MIME type of a file, and it is all I want to be dropped. However, you can choose any MIME type you want -

a list of supported MIME types is available here

. The code to set the requested MIME types is:

transferObject.addDataFlavor("application/x-moz-file");

This will cause only the application/x-moz-file data to be retrieved from the Drag Session which is in progress.

Retrieving Filenames from the Drag session

Finally, we get to what we’ve wanted to do all along – retrieve the names of the files being dropped onto the browser window. First, we’ll want to figure out how many files are being dropped, which is done with:

var numItems = dragSession.numDropItems;

This will allow us to loop through each dragged item and handle it individually. Next, we want to enter the loop and grab the actual items from the drag session. This is done with:

for (var i = 0; i < numItems; i++)
{
  // Get the data for the given drag item from the drag
  // session into our prepared Transfer object.
  dragSession.getData(transferObject, i);
…
}

This will retrieve the given item from the Drag Session into the nsITransferable object which we created.

 

At this point we have the dragged file in a Transfer object, but we still need to retrieve the specific data-type from the Transfer object. In order to do this, we need to call the transferObject.getTransferData method (

Method API docs available here

). However, you will note that this method has two parameters listed as OUT parameters – what that means is that return values for this method will be returned in those objects. In order to properly handle an XPCOM OUT parameter, we need to create new Object()s and pass those in as the OUT parameters, and then check the Object’s value property. An example of how to do all of this (and return the data object at the same time) is:

// We need to pass in Javascript 'Object's to any XPConnect method which
// requires OUT parameters. The out value will then be saved as a new
// property called Object.value.
var dataObj = new Object();
var dropSizeObj = new Object();
 
// Get the Mozilla File data type from the ITransferable object.
transferObject.getTransferData("application/x-moz-file", dataObj, dropSizeObj);
 
// Display the return value of this OUT parameter.
alert(“Dropped object size is “ + dropSizeObj.value);

As we can see, we have created two new OUT parameters, called the getTransferData method to retrieve the dragged data, and displayed the value of the second OUT parameter in a dialog box using the Object.value property.

Request File System Read Permissions

Before we can set the value of a File input field, we must request local file read permissions. This is done with:

netscape.security.PrivilegeManager.enablePrivilege('UniversalFileRead');

Please note that if we don’t do this, we will get a security exception when we attempt to set the value of the file input field. Also remember the restrictions on enablePermissions which were described in the

Request permission to access the Mozilla XPConnect bridge

section – the same restrictions apply here.

Save the Filename to the Form’s File Input Field

At long last, we’ve reached the end. In order to save the filename of the dragged file into the form’s file input field, we need to take the value of the data object which we were returned from the getTransferData call and cast it as an nsIFile (

API Reference for nsIFile available here

). Then, we take the path from the nsIFile and save it in the file input field’s value property. This is done with:

var droppedFile = dataObj.value.QueryInterface(Components.interfaces.nsIFile);
 
var fileIn = document.getElementById("fileInputField");
 
fileIn.value = droppedFile.path;

Note the QueryInterface call to ensure that the nsIFile capabilities were applied. Also take note of the retrieval of the file input field – in this example, we will retrieve the same file input field every time through the loop and overwrite it’s value with the value of the next dragged file. If we wanted to do something more intelligent like add a new file input field for each file, we could use DOM to create a new file input node, e.g.:

var newFileIn = document.createElement(“input”);
 
newFileIn.setAttribute("type", "file");
newFileIn.setAttribute("name", "fileData" + i);
newFileIn.setAttribute("id", "fileData" + i);
 
var parentForm = document.getElemendById(“formID”);
parentForm.appendChild(newFileIn);

It would also probably be advisable to set the form to be hidden, so the user isn’t seeing all of these file input fields appearing and such. This can be done by setting the form to have a style of ‘visibility: hidden’, e.g.:

<form
    id="formID"
    action="/path/to/uploadServlet"
    method="post"
    enctype="multipart/form-data"
    style="visibility: hidden;"
>

Also take note of the encoding type of multipart/form-data – since we’re uploading a file, we will want to encode with this type to ensure the file isn’t mangled in transit (as the default form encoding of application/x-www-form-urlencoded will do).

 

At this point, the file data is saved in the form. When the form is submitted, it will be uploaded to the server. Depending upon your application, you may wish for this to be an automatic process – in that case, you would simply call formObject.submit() at the end of your Javascript method, and the form would be uploaded.

Marking the Event as handled

Now that we’re done and we’ve handled the event,

make sure to let Mozilla know!

If you don’t mark the event as handled, then the default handler will also kick in and do unexpected and undesirable things (like open the document inside of the current window). This marking is done with:

evt.stopPropagation();

Where evt was the event that we were passed in to our drag and drop handler.

Full code for this discussion

This code is the full code of the sample which I built. I have included several debugging output statements which were not discussed above, which show the state of the drag and drop operation at different points. It must also be noted that, in order to have a production-level set of usable code, you will want to implement error checking and exception catching (which I have left out as it contributes nothing but clutter to the demonstration of functionality). In particular, every permission check can throw an exception if the permission is not granted, so that should be handled in a more graceful manner.

 

<HTML>
<HEAD>
<SCRIPT language='Javascript'>

// This code comes from an article which is 
// Copyright (C) 2005, Codesta LLC. All Rights Reserved.
// All of the research that I did for this article was
// on company time, and Codesta agreed to let me publish it.
// The information in it is free for you to use, but don't
// go claiming it's yours - it's not.

// I leave this as an explicit method because it comes in handy a lot when debugging issues.
function listAllMozillaXPCOMInterfaces(elemToWriteTo)
{
  // Pass this through to the property-listing method.
  listAllPropsOnObject(Components.interfaces, elemToWriteTo);
}
 
// Lists all of the properties on the given object by writing them as text to the given page element.
function listAllPropsOnObject(objToList, elemToWriteTo)
{
  elemToWriteTo.appendChild(document.createElement("br"));
  elemToWriteTo.appendChild(document.createTextNode("Properties for [" + objToList + "]:"));
  elemToWriteTo.appendChild(document.createElement("hr"));
            
  for ( var prop in objToList ) {
    with (objToList)
    {
      elemToWriteTo.appendChild(
        document.createTextNode("[" + prop + "] = " + eval(prop))
      );
      elemToWriteTo.appendChild(document.createElement("br"));
    }
  }

  elemToWriteTo.appendChild(document.createElement("br"));
  elemToWriteTo.appendChild(document.createElement("br"));
}
 
// These security privileges are what are needed to do the interesting things we may want to do.
// Please note that you need to request the privilege within the scope of when you intend to use
// the associated permission - for example, I cannot declare the privileges in the Load method
// because the privilege will expire when the load method ends. You can think of this like a
// standard variable - it's available in the declaring scope (and for any child function calls)
// but goes away when the end curly-brace for that scope is reached.
// Each is summarized below.
 
// This allows us to read and upload files from the local filesystem. If we don't get this
// privilege, we can't read from the file system manually. When we get to the point that
// we are uploading the dragged file to the server, this permission will be required.
//netscape.security.PrivilegeManager.enablePrivilege('UniversalFileRead');
 
// This allows us to access the Mozilla XPConnect API. We need to do this so that we can read
// from the native event system. The Javascript event that is given to us when a Drag/Drop
// occurs has no native data in it id the drag/drop started outside the browser window, so
// we need to use XPCOM (Via XPConnect) to get at the native drag/drop event.
//netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 
// A final note about these privileges - a privilege can only be requested by a script that is:
//
// a) on the local machine
// b) inside of a signed JAR
//
// So, when we want to deploy this to the server, we will need to JAR it up and sign it.
 
function dragDropHandler (evt) {
 
  // Select the element to write out all of our data to.
  var draggee = document.getElementById("dragTableData");

  // Request the XP Connect privilege so we can access the XPCOM API. This will
  // cause a dialog to be displayed to the user asking them if they want to grant
  // this privilege to this script.
  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');

  // Write out all of the XPCOM Interfaces that are available via the XPConnect bridge.
  listAllMozillaXPCOMInterfaces(draggee);

  // Load in the native DragService manager from the browser.
  var dragService =
    Components.classes["@mozilla.org/widget/dragservice;1"]
      .getService(Components.interfaces.nsIDragService);

  // Load in the currently-executing Drag/drop session.
  var dragSession = dragService.getCurrentSession();

  // List off all of the properties that are on the Drag/drop session.
  listAllPropsOnObject(dragSession, draggee);

  // Create an instance of an nsITransferable object using reflection.
  var transferObject =
    Components.classes["@mozilla.org/widget/transferable;1"]
      .createInstance();

  // Bind the object explicitly to the nsITransferable interface. We need to do this to ensure that
  // methods and properties are present and work as expected later on.
  transferObject = transferObject.QueryInterface(Components.interfaces.nsITransferable);

  // This list of flavors was retrieved from http://www.mozilla.org/xpfe/xptoolkit/DataFlavors.html.
  // This is a simple debug message which indicates which flavors are supported by the data currently
  // on the drag/drop clipboard.
  alert(
    "text/x-moz-url? " + dragSession.isDataFlavorSupported("text/x-moz-url") + "\n" +
    "text/unicode? " + dragSession.isDataFlavorSupported("text/unicode") + "\n" +
    "text/xif? " + dragSession.isDataFlavorSupported("text/xif") + "\n" +
    "text/html? " + dragSession.isDataFlavorSupported("text/html") + "\n" +
    "text/plain? " + dragSession.isDataFlavorSupported("text/plain") + "\n" +
    "image/png? " + dragSession.isDataFlavorSupported("image/png") + "\n" +
    "image/jpg? " + dragSession.isDataFlavorSupported("image/jpg") + "\n" +
    "image/gif? " + dragSession.isDataFlavorSupported("image/gif") + "\n" +
    "application/x-moz-url? " + dragSession.isDataFlavorSupported("application/x-moz-url") + "\n" +
    "application/x-moz-file? " + dragSession.isDataFlavorSupported("application/x-moz-file") + "\n" +
    "AOLMAIL? " + dragSession.isDataFlavorSupported("AOLMAIL")
  );

  // I've chosen to add only the x-moz-file MIME type. Any type can be added, and the data for that format
  // will be retrieved from the Drag/drop service.
  transferObject.addDataFlavor("application/x-moz-file");

  // Get the number of items currently being dropped in this drag/drop operation.
  var numItems = dragSession.numDropItems;
  var fileIn = document.getElementById("fileInputField");

  // Request the 'file read' privilege. We need to do this in order to be able to set the value of
  // a FileInput box. If we don't do this, we'll get a security exception when we try to set that
  // value.
  netscape.security.PrivilegeManager.enablePrivilege('UniversalFileRead');
 
  for (var i = 0; i < numItems; i++)
  {
    // Get the data for the given drag item from the drag session into our prepared
    // Transfer object.
    dragSession.getData(transferObject, i);

    // Read off all of the properties on this transfer object.
    listAllPropsOnObject(transferObject, draggee);

    // Start creating a String to hold all the text that will be in a debug alert.
    var supportedTypes = "transferobject supported transfer types array is:\n";

    // Get the set of all flavors supported by this Transfer object.
    var typeArray = transferObject.flavorsTransferableCanExport();
    var curItem = null;

    // Iterate through the array of supported MIME types, and write them all out to the
    // temporary output string.
    for (var j = 0; j < typeArray.Count(); j++)
    {
      curItem = typeArray.GetElementAt(j);

      // Cast this object as a C String via the QueryInterface method.
      curItem = curItem.QueryInterface(Components.interfaces.nsISupportsCString);

      supportedTypes += (curItem + "\n");
    }

    // Display our list of supported flavors
    alert(supportedTypes);

    // We need to pass in Javascript 'Object's to any XPConnect method which
    // requires OUT parameters. The out value will then be saved as a new
    // property called Object.value.
    var dataObj = new Object();
    var dropSizeObj = new Object();

    // Get the Mozilla File data type from the ITransferable object.
    transferObject.getTransferData("application/x-moz-file", dataObj, dropSizeObj);

    // Cast the returned data object as an nsIFile so that we can retrieve it's filename.
    var droppedFile = dataObj.value.QueryInterface(Components.interfaces.nsIFile);

    // Display all of the returned parameters with an Alert dialog.
    alert("dataObj filename is " + droppedFile.path + ", num is " + dropSizeObj.value);

    // Set the File Input box's value to be the path to the file that was just dropped.
               fileIn.value = droppedFile.path;
  }

  // Since the event is handled, prevent it from going to a higher-level event handler.
  evt.stopPropagation();
}

function loadMethod()
{
  // We need to register the window to capture Drag & Drop events before we can receive them with
  // our event handler.
  window.captureEvents(Event.DRAGDROP);
 
  // Register the event handler, and set the 'capture' flag to true so we get this event
  // before it bubbles up through the browser.
  window.addEventListener("dragdrop", dragDropHandler, true);
}
 
</SCRIPT>
</HEAD>
<BODY onload="loadMethod();">
<FORM NAME="formName" id="fileForm"
      ACTION="yourFileUploadHandler"
      METHOD="post"
      ENCTYPE="multipart/form-data"
> 
<INPUT TYPE="file" NAME="fileName" id="fileInputField">
<INPUT TYPE="submit">
</FORM>
 
<table><tr><td id="dragTableData">
This is a cell which will be filled with debugging info.
</td></tr></table>
</BODY>
</HTML>

 

Link: http://straxus.javadevelopersjournal.com/creating_a_mozillafirefox_drag_and_drop_file_upload_script_p.htm