HTML5 Drag and Drop - The jQuery API - Web Development with jQuery (2015)

Web Development with jQuery (2015)

Part I. The jQuery API

Chapter 11. HTML5 Drag and Drop

In this chapter you learn how to use the HTML5 drag-and-drop specification with jQuery. The HTML5 drag-and-drop specification gives you a more powerful drag-and-drop implementation than jQuery UI's implementation in the Draggables and Droppables plugins, which you work with in Chapter 12, “Draggable and Droppable.” The HTML5 drag-and-drop specification enables you to drag and drop between multiple browser windows, and even multiple browser windows between completely different browsers. For example, you can initiate a drag in Safari and complete it in Chrome or Firefox. You can also use HTML5 drag and drop to upload documents from your desktop or file manager. You can drag files from your desktop, Finder, Windows Explorer, and such to a browser window, and there you can access the document or documents uploaded through JavaScript and display thumbnails and upload progress meters.

jQuery has nothing built into it that assists with using the HTML5 drag-and-drop specification, but you can use jQuery in an implementation of the HTML5 drag-and-drop API to attach events and manipulate HTML attributes or CSS to enable drag and drop. In the following section you learn more about how the drag-and-drop API came about and see an example implementing it.

Implementing Drag and Drop

HTML5 drag and drop can be summed up as mostly a collection of JavaScript events. There are some additional CSS/HTML attributes that enable drag and drop depending on the browser. The additional CSS/HTML portion is often criticized for the weird and divergent methods the browser makers have chosen to make it possible to initiate drag and drop. One such critique is an expletive-laden blog post by Peter-Paul Koch on his quirksmode.org site: http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html.

His rant sums up the problems with the HTML5 drag-and-drop API nicely; it basically boils down to it's a bit of a kludge because it was reverse-engineered and based on IE's legacy implementation. And then, in addition, there is the Safari browser team's diversion from the specification with its addition of CSS to instigate drag-and-drop behavior. However, the Safari team has since changed its implementation to match the official HTML 5 specification.

The merits of Koch's rant can be debated, but the frustration he expresses in learning how to use the drag-and-drop API is a common experience. What you learn in this chapter, hopefully, can significantly mitigate the frustration typically experienced when working with the drag-and-drop API for the first time.

The drag-and-drop API works in all modern browsers, and even some of the older ones with the addition of a line or two of legacy-enabling code. The drag-and-drop API originated in IE5. The modern API is a slight modification of the original IE5 API. The API was spec'd out by the Web Hypertext Application Technology Working Group (WHATWG) and later adopted as part of the formal W3C HTML5 specification, when the W3C took over HTML5 from the WHATWG. The IE API was adopted with some tweaks so that existing code already in use could be used without much difficulty.

Following are the drag-and-drop JavaScript events:

· dragstart—This event is fired when a drag begins, on the element the drag was initiated on.

· dragend—This event is fired when a drag ends, on the element that the drag was initiated on.

· dragenter—This event is fired when an element enters the space over the element this event is attached to; it is used to identify an appropriate drop zone for the drag element.

· dragleave—This event is fired when an element leaves the space over the element this event is attached to; it is also used to identify an appropriate drop zone for the drag element.

· dragover—This event is fired continuously while a draggable element is within the space over the element this event is attached to; this event is also used on the drop side.

· drag—This event is fired continuously while the element is dragged, on the element being dragged.

· drop—This event is fired when a draggable element is dropped on the element this event is attached to.

You need to implement event listeners for most of these events to implement drag and drop in a document. The following example implements the drag-and-drop API in a browser-based Mac OS Finder inspired file manager. Remember, you can download this book's source code for free from www.wrox.com/go/webdevwithjquery. This example is Example 11-1.html.

<!DOCTYPE HTML>

<html lang='en'>

<head>

<meta http-equiv='X-UA-Compatible' content='IE=Edge' />

<meta charset='utf-8' />

<title>Finder</title>

<script src='../jQuery.js'></script>

<script src='../jQueryUI.js'></script>

<script src='Example 11-1.js'></script>

<link href='Example 11-1.css' rel='stylesheet' />

</head>

<body>

<div id='finderFiles'>

<div class='finderDirectory' data-path='/Applications'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Applications</span>

</div>

</div>

<div class='finderDirectory' data-path='/Library'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Library</span>

</div>

</div>

<div class='finderDirectory' data-path='/Network'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Network</span>

</div>

</div>

<div class='finderDirectory' data-path='/Sites'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Sites</span>

</div>

</div>

<div class='finderDirectory' data-path='/System'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>System</span>

</div>

</div>

<div class='finderDirectory' data-path='/Users'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Users</span>

</div>

</div>

</div>

</body>

</html>

The preceding HTML is styled with the following style sheet:

html,

body {

width: 100%;

height: 100%;

}

body {

font: 12px "Lucida Grande", Arial, sans-serif;

background: rgb(189, 189, 189) url('images/Bottom.png') repeat-x bottom;

color: rgb(50, 50, 50);

margin: 0;

padding: 0;

}

div#finderFiles {

border-bottom: 1px solid rgb(64, 64, 64);

background: #fff;

position: absolute;

top: 0;

right: 0;

bottom: 23px;

left: 0;

overflow: auto;

user-select: none;

-webkit-user-select: none;

-moz-user-select: none;

-ms-user-select: none;

}

div.finderDirectory {

float: left;

width: 150px;

height: 100px;

overflow: hidden;

}

div.finderDirectory:-webkit-drag {

opacity: 0.5;

}

div.finderIcon {

background: url('images/Folder 48x48.png') no-repeat center;

background-size: 48px 48px;

height: 56px;

width: 54px;

margin: 10px auto 3px auto;

}

div.finderIconSelected,

div.finderDirectoryDrop div.finderIcon {

background-color: rgb(204, 204, 204);

border-radius: 5px;

}

div.finderDirectoryDrop div.finderIcon {

background-image: url('images/Open Folder 48x48.png');

}

div.finderDirectoryName {

text-align: center;

}

span.finderDirectoryNameSelected,

div.finderDirectoryDrop div.finderDirectoryName span {

background: rgb(56, 117, 215);

border-radius: 8px;

color: white;

padding: 1px 7px;

}

Finally, the following JavaScript brings everything to life:

$.fn.extend({

outerHTML : function()

{

var temporary = $("<div/>").append($(this).clone());

var html = temporary.html();

temporary.remove();

return html;

},

enableDragAndDrop : function()

{

return this.each(

function()

{

if (typeof this.style.WebkitUserDrag != 'undefined')

{

this.style.WebkitUserDrag = 'element';

}

if (typeof this.draggable != 'undefined')

{

this.draggable = true;

}

if (typeof this.dragDrop == 'function')

{

this.dragDrop();

}

}

);

}

});

$(document).ready(

function()

{

$(document).on(

'mousedown.finder',

'div.finderDirectory, div.finderFile',

function(event)

{

$(this).enableDragAndDrop();

$('div.finderIconSelected')

.removeClass('finderIconSelected');

$('span.finderDirectoryNameSelected')

.removeClass('finderDirectoryNameSelected');

$(this).find('div.finderIcon')

.addClass('finderIconSelected');

$(this).find('div.finderDirectoryName span')

.addClass('finderDirectoryNameSelected');

}

);

$('div.finderDirectory, div.finderFile')

.on(

'dragstart.finder',

function(event)

{

event.stopPropagation();

var html = $(this).outerHTML();

var dataTransfer = event.originalEvent.dataTransfer;

dataTransfer.effectAllowed = 'copyMove';

try

{

dataTransfer.setData('text/html', html);

dataTransfer.setData('text/plain', html);

}

catch (error)

{

dataTransfer.setData('Text', html);

}

}

)

.on(

'dragend.finder',

function(event)

{

if ($('div.finderDirectoryDrop').length)

{

$(this).removeClass('finderDirectoryDrop');

$(this).remove();

}

}

)

.on(

'dragenter.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

}

)

.on(

'dragover.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

if ($(this).is('div.finderDirectory'))

{

$(this).addClass('finderDirectoryDrop');

}

}

)

.on(

'dragleave.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

$(this).removeClass('finderDirectoryDrop');

}

)

.on(

'drop.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

var dataTransfer = event.originalEvent.dataTransfer;

try

{

var html = dataTransfer.getData('text/html');

}

catch (error)

{

var html = dataTransfer.getData('Text');

}

html = $(html);

var drop = $(this);

var dontAcceptTheDrop = (

drop.data('path') == html.data('path') ||

drop.is('div.finderFile')

);

if (dontAcceptTheDrop)

{

// Prevent file from being dragged onto itself

drop.removeClass('finderDirectoryDrop');

return;

}

if (html.hasClass('finderDirectory finderFile'))

{

// Do something with the dropped file

console.log(html);

}

}

);

}

);

Figure 11.1 shows the preceding example results.

NOTE Note To run this example in Internet Explorer, you should upload the documents to a web server.

image

Figure 11.1

Prerequisite Plugins

This example begins with the creation of two jQuery plugins, $.outerHTML() and $.enableDragAndDrop(). The $.outerHTML() plugin is designed to implement IE's native outerHTML property as a jQuery plugin. The purpose of this is to enable easily pasting HTML snippets to the system clipboard using the drag-and-drop API. Using jQuery's existing html() method would get only the content of an element, for example:

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Sites</span>

</div>

Using drag and drop to relocate a complete element, it is instead desirable to have the outer <div> element as well as its content.

<div class='finderDirectory' data-path='/Sites'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Sites</span>

</div>

</div>

The $.outerHTML() plugin provided here implements the functionality of the IE property in browsers that haven't implemented the IE property, such as Safari. These snippets can be taken out of the DOM and then reinserted when a successful drag-and-drop operation has taken place.

$.fn.extend({

outerHTML : function()

{

var temporary = $("<div/>").append($(this).clone());

var html = temporary.html();

temporary.remove();

return html;

},

The block of code begins with $.fn.extend(), which as you learned in Chapter 9, “Plugins,” is used to create jQuery plugins. outerHTML : function() begins the first plugin, which implements outerHTML functionality. The block of markup that you want to retrieve theouterHTML from is cloned using $(this).clone() and is inserted inside a temporary <div> element. The temporary <div> is created using a string “<div/>”, which jQuery internally converts to a <div> element object. The cloned object is inserted within the <div> element using the append() method. Then the newly inserted object is retrieved from the newly created <div> element using the html() method and is assigned to the variable named html. The html() method uses the innerHTML property internally, which is implemented universally, in all browsers. Then the temporary <div> element is cleaned up from memory with a call to remove(), which deletes it, and the html source is returned as a string. The HTML snippet that is returned is now portable and can be transported to anywhere that supports rendering HTML, or as plain text, using your operating system's drag-and-drop clipboard. You learn more about the drag-and-drop clipboard later in this section.

The second plugin that you create enables drag and drop using the three different methods that exist for doing so since the drag and drop API was first created by Microsoft with the release of IE5.

enableDragAndDrop : function()

{

return this.each(

function()

{

if (typeof this.style.WebkitUserDrag != 'undefined')

{

this.style.WebkitUserDrag = 'element';

}

if (typeof this.draggable != 'undefined')

{

this.draggable = true;

}

if (typeof this.dragDrop == 'function')

{

this.dragDrop();

}

}

);

}

This plugin does not assume that you are working with just one element. Because it may be desirable to enable drag and drop on many elements at once, it iterates over the potential collection of elements present in this using each(). jQuery always passes elements to plugins as an array, never as a single element, to make working with jQuery and writing plugins for jQuery simpler.

Drag-and-drop functionality is first enabled in older WebKit-based browsers, such as Safari and Chrome. (It is worth noting that the order in which these methods are used isn't important, though.) To enable drag and drop in older WebKit-based browsers, the proprietary CSS property -webkit-user-drag is set to the value element. But before you set the value, you first test the CSS property to see if it exists by looking for whether the typeof is undefined. If the property exists, the typeof will not be undefined, but will instead bestring.

if (typeof this.style.WebkitUserDrag != 'undefined')

{

this.style.WebkitUserDrag = 'element';

}

When you set a proprietary CSS property in JavaScript, the hyphens are omitted, and the first letter is capitalized, so –webkit-user-drag becomes WebkitUserDrag. If it were a property implemented in Firefox, you'd have –moz-user-drag and MozUserDrag, instead.

Next, you check for the existence of the draggable attribute. The draggable attribute is recommended by the W3C HTML 5 drag-and-drop specification as the official way to enable drag and drop. This attribute is supported in the latest versions of Safari, Chrome, Firefox, and Internet Explorer. Like the CSS property, you must check to see if the typeof is not undefined to see if the attribute is implemented in the browser.

if (typeof this.draggable != 'undefined')

{

this.draggable = true;

}

The draggable attribute is a boolean attribute. Setting it to true enables drag and drop of the element, and setting it to false disables drag and drop. The behavior that you get by setting either the WebkitUserDrag CSS property or draggable attribute is default behavior. Typically, you can move the element around, but nothing happens when you drop it because that behavior has to be defined with JavaScript.

The last method of enabling drag and drop is used for older versions of Internet Explorer. Internet Explorer 9 and later implement the newer HTML 5 drag-and-drop specification and require using the draggable attribute instead of the legacy method used here. To enable drag and drop in IE8 and earlier, first test for the existence of the dragDrop method. Test to see if the typeof the dragDrop method is function to find out whether you can use it. (You can also check to see if the typeof is not undefined like the CSS property and the HTML attribute, if you like.)

if (typeof this.dragDrop == 'function')

{

this.dragDrop();

}

If the dragDrop method exists, simply calling it on the element enables drag and drop on that element in IE8 and earlier.

Event Setup

Now that you have defined these two jQuery plugins, you set up the events that you need to implement the drag-and-drop API.

$(document).ready(

function()

{

$(document).on(

'mousedown.finder',

'div.finderDirectory, div.finderFile',

function(event)

{

$(this).enableDragAndDrop();

$('div.finderIconSelected')

.removeClass('finderIconSelected');

$('span.finderDirectoryNameSelected')

.removeClass('finderDirectoryNameSelected');

$(this).find('div.finderIcon')

.addClass('finderIconSelected');

$(this).find('div.finderDirectoryName span')

.addClass('finderDirectoryNameSelected');

}

);

The first event that you create is a mousedown event that enables drag-and-drop functionality on each <div> element with the class names finderDirectory and finderFile. Because it uses the on() method, it is applied automatically when new <div> elements with those class names are added to the DOM. You'll expand on the concept of dynamically applying events to take care of the file or folders added to the folder you're viewing later in this chapter in Example 11-2. The mousedown event is applied with an event namespace, finder, which you learned about in Chapter 3, “Events.” Using jQuery's event namespaces allows you more control over binding and unbinding event handlers. Using the namespace you can unbind only the events in the finder namespace, if wanted, without affecting events in other namespaces.

The next thing you do is to begin applying drag-and-drop events to each file and folder <div> element. Along with the CSS property WebkitUserDrag, the HTML attribute draggable, and the dragDrop() method, the application of these events controls what happens when a user drags and drops elements. The drag-and-drop events fire in the following order on the element dragged:

1. dragstart

2. drag

3. dragend

The drag-and-drop events fire in the following order on the drop element:

1. dragenter

2. dragover

3. drop or dragleave

Most of the drag-and-drop events require either event.preventDefault(), or event.stopPropagation(), or both, to block either the default action or to prevent the event from propagating up the DOM tree.

The dragstart event sets the contents of the operating system's drag-and-drop clipboard. It also provides an opportunity to set the effectAllowed property. The effectAllowed property does little more than change the mouse cursor to give the user an indication of what's possible when dragging an element. Because you're working with files and folders, the effectAllowed that makes the most sense is 'copyMove'.

$('div.finderDirectory, div.finderFile')

.on(

'dragstart.finder',

function(event)

{

event.stopPropagation();

var html = $(this).outerHTML();

var dataTransfer = event.originalEvent.dataTransfer;

dataTransfer.effectAllowed = 'copyMove';

try

{

dataTransfer.setData('text/html', html);

dataTransfer.setData('text/plain', html);

}

catch (error)

{

dataTransfer.setData('Text', html);

}

}

)

The possible values of the effectAllowed property follow:

· none—No operation by drag and drop is permitted.

· copy—Only copy by drag and drop is permitted.

· move—Only move by drag and drop is permitted.

· link—Only link by drag and drop is permitted.

· copyMove—Both copy and move are permitted.

· copyLink—Both copy and link are permitted.

· linkMove—Both link and move are permitted.

· all—Copy, link, and move are all permitted.

When two or more operations are supported by the effectAllowed property, the second or third operation is typically invoked by holding down a key on the keyboard.

The system drag-and-drop clipboard is also set in the dragstart event. The clipboard is set by first retrieving the outerHTML() of the element. Then the HTML is copied to the clipboard and identified on the clipboard by the MIME type. In this case, both of the MIME types text/plain and text/html are set. Setting the MIME types allows other applications on your computer to work with the data that you copy to the system clipboard. For example, after copying the HTML to the clipboard in the dragstart event, you can now drag and drop elements outside the browser window to other applications. Any application that supports text/html or text/plain can work with the data copied to the clipboard. You can drag and drop from the browser to a text editor, including editors that only support the text/plain MIME type. You can drag and drop between completely different browsers.

A try / catch exception differentiates between using the setData() method with Internet Explorer's method and the HTML5 standard method. IE supports just two options: 'Text' and 'URL'. All the other browsers use a MIME type. Using an exception automatically switches off to the IE method when using a MIME type fails and throws an error.

The next event that you attach is the dragend event.

.on(

'dragend.finder',

function(event)

{

if ($('div.finderDirectoryDrop').length)

{

$(this).removeClass('finderDirectoryDrop');

$(this).remove();

}

}

)

The dragend event is fired when the drag has completed, and it is fired on the element that was dragged. There is an issue with the dragend event that is difficult or outright impossible to work with. There is no way of knowing when a drag is completed to an acceptable drop zone when a drag and drop is executed from the browser window to another browser window or an outside application. One potential workaround involves sending an AJAX request to the server from the side receiving the drop and then using web sockets to listen for that drop to occur on the side where the drag originates. But that approach is way over the top for this simple demonstration of the drag-and-drop API.

For drag and drops that originate and terminate in the same browser window, the dragend event looks for the existence of a <div> element with the class name finderDirectoryDrop. If this <div> element is detected, that is an indication that a drag and drop was completed on an acceptable drop zone, which means that the element dragged can be removed from the DOM. Because the element is removed from the DOM, this makes the default action of a drag and drop move. If you were to implement a copy action, it would then, of course, be desirable to keep the original element. Such an action might be implemented by holding down the Option (Mac) or Ctrl (Windows) key when doing a drag and drop. You'd look for the Option/Alt key by checking whether event.altKey evaluates totrue within the dragstart event listener. Other options are the Control key, event.ctrlKey, the Shift key, event.shiftKey, or the Command/Windows key, event.metaKey.

The next event attached is the dragenter event:

.on(

'dragenter.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

}

)

The action taken for the dragenter event is simply to prevent the default action and to stop event propagation. The action taken for the dragover event is similar:

.on(

'dragover.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

if ($(this).is('div.finderDirectory'))

{

$(this).addClass('finderDirectoryDrop');

}

}

)

The dragover event also requires canceling the default action and stopping event propagation. In addition, if the element is a <div> element with the classname finderDirectory, the classname finderDirectoryDrop is added, which changes the icon used for the directory from a closed folder to an open folder.

Likewise, the dragleave event also cancels the default action and stops event propagation:

.on(

'dragleave.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

$(this).removeClass('finderDirectoryDrop');

}

)

Then the classname finderDirectoryDrop is removed from the <div> element, which indicates that the dragging element is no longer over this element.

Finally, the drop event is applied, and it also begins with preventing the default action and stopping event propagation:

.on(

'drop.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

var dataTransfer = event.originalEvent.dataTransfer;

try

{

var html = dataTransfer.getData('text/html');

}

catch (error)

{

var html = dataTransfer.getData('Text');

}

html = $(html);

var drop = $(this);

var dontAcceptTheDrop = (

drop.data('path') == html.data('path') ||

drop.is('div.finderFile')

);

if (dontAcceptTheDrop)

{

// Prevent file from being dragged onto itself

drop.removeClass('finderDirectoryDrop');

return;

}

if (html.hasClass('finderDirectory finderFile'))

{

// Do something with the dropped file

console.log(html);

}

}

);

The drop event listener continues with assigning the dataTransfer object from event.originalEvent.dataTransfer to dataTransfer, which is done to keep the code from getting too wide. The HTML that was copied to the system clipboard under the text/html MIME type during the dragstart event is retrieved from the system clipboard using the getData() method and the same MIME type, text/html. The HTML comes from the clipboard as plain text and is assigned to the html variable. The html variable is converted into a DOM object that jQuery can work with by passing the HTML snippet to the jQuery method, $(html). This makes it possible to do things with the <div> element retrieved from the system clipboard using jQuery methods.

Another try / catch exception is used on the getData() method to differentiate between Internet Explorer's method of retrieving data from the clipboard and the standard way of retrieving data from the clipboard. As you did with setData(), IE requires just 'Text'instead of a MIME type to retrieve data, and using an exception here automatically switches from the MIME type method to the IE method.

NOTE For security reasons the dataTransfer object can be accessed only from drag-and-drop event handlers while those drag-and-drop event handlers are firing. This is done to protect users from unauthorized access to their system's clipboard. Access to the dataTransfer object may also be further limited by the domain name origin (similar to the frame and AJAX cross-domain security limitations).

Next, the drop element is passed through jQuery, $(this), and assigned to the variable named drop.

The variable dontAcceptTheDrop checks to see that an element isn't being dropped on itself and that the drop target is a directory, rather than a file. If dontAcceptTheDrop is true, the finderDirectoryDrop classname is removed, and execution of the event listener terminates with the call to return.

Finally, the <div> object created from the HTML snippet is checked to see that it has either the classname finderDirectory or finderFile as a final validation that the HTML snippet is HTML that you want to work with.

In the next section, you learn how to further extend this example to accept drag-and-drop file uploads, in addition to implementing drag and drop on the folder window. You extend the example to dynamically reapply events to a dragged and dropped file or folder.

Implementing Drag-and-Drop File Uploads

File uploads by drag and drop have evolved during the past few years—beginning with only allowing one or more files to be uploaded by drag and drop and then expanding to allowing drag-and-drop downloads. File uploads then expanded again to allow both files and folders to be uploaded by drag and drop. Presently, the latest versions of all the major browsers support file upload by drag and drop. Chrome supports upload of both files and folders, and drag and drop downloads.

In the following example, you build on Example 11-1, adding drag-and-drop upload support to it, as well as some other tweaks that improve the drag-and-drop experience. Drag-and-drop uploading is accompanied with thumbnail previews of image files, as well as an upload progress bar. To realistically test the following example, you need to add a server-side script into the mix to receive the uploaded files. The server-side portion of this is not covered by this example, but I have provided a remedial PHP script that you can use to examine uploaded file metadata. This example is available in the book's source code download materials as Example 11-2.

<!DOCTYPE HTML>

<html lang='en'>

<head>

<meta http-equiv='X-UA-Compatible' content='IE=Edge' />

<meta charset='utf-8' />

<title>Finder</title>

<script src='../jQuery.js'></script>

<script src='../jQueryUI.js'></script>

<script src='Example 11-2.js'></script>

<link href='Example 11-2.css' rel='stylesheet' />

</head>

<body>

<div id='finderFiles' data-path='/'>

<div class='finderDirectory' data-path='/Applications'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Applications</span>

</div>

</div>

<div class='finderDirectory' data-path='/Library'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Library</span>

</div>

</div>

<div class='finderDirectory' data-path='/Network'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Network</span>

</div>

</div>

<div class='finderDirectory' data-path='/Sites'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Sites</span>

</div>

</div>

<div class='finderDirectory' data-path='/System'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>System</span>

</div>

</div>

<div class='finderDirectory' data-path='/Users'>

<div class='finderIcon'></div>

<div class='finderDirectoryName'>

<span>Users</span>

</div>

</div>

</div>

<div id='finderDragAndDropDialogue'>

<div id='finderDragAndDropDialogueWrapper'>

<h4>File Upload Queue</h4>

<div id='finderDragAndDropDialogueProgress'>

<span>0</span>%

</div>

<img id='finderDragAndDropDialogueActivity'

src='images/Upload Activity.gif'

alt='Upload Activity' />

<div id='finderDragAndDropDialogueProgressMeter'>

<div></div>

</div>

<div id='finderDragAndDropDialogueFiles'>

<table>

<thead>

<tr>

<th class='finderDragAndDropDialogueFileIcon'>

</th>

<th class='finderDragAndDropDialogueFile'>

File

</th>

<th class='finderDragAndDropDialogueFileSize'>

Size

</th>

</tr>

</thead>

<tbody>

<tr class='finderDragAndDropDialogueTemplate'>

<td class='finderDragAndDropDialogueFileIcon'>

</td>

<td class='finderDragAndDropDialogueFile'>

</td>

<td class='finderDragAndDropDialogueFileSize'>

</td>

</tr>

</tbody>

</table>

</div>

</div>

</div>

</body>

</html>

The preceding file is saved as Example 11-2.html and is styled with the following CSS:

html,

body {

width: 100%;

height: 100%;

}

body {

font: 12px "Lucida Grande", Arial, sans-serif;

background:

rgb(189, 189, 189)

url('images/Bottom.png')

repeat-x bottom;

color: rgb(50, 50, 50);

margin: 0;

padding: 0;

}

div#finderFiles {

border-bottom: 1px solid rgb(64, 64, 64);

background: #fff;

position: absolute;

top: 0;

right: 0;

bottom: 23px;

left: 0;

overflow: auto;

user-select: none;

-webkit-user-select: none;

-moz-user-select: none;

-ms-user-select: none;

}

div.finderDirectory {

float: left;

width: 150px;

height: 100px;

overflow: hidden;

}

div.finderDirectory:-webkit-drag {

opacity: 0.5;

}

div.finderIcon {

background:

url('images/Folder 48x48.png')

no-repeat center;

background-size: 48px 48px;

height: 56px;

width: 54px;

margin: 10px auto 3px auto;

}

div.finderIconSelected,

div.finderDirectoryDrop > div.finderIcon {

background-color: rgb(204, 204, 204);

border-radius: 5px;

}

div.finderDirectoryDrop > div.finderIcon {

background-image: url('images/Open Folder 48x48.png');

}

div.finderDirectoryName {

text-align: center;

}

span.finderDirectoryNameSelected,

div.finderDirectoryDrop > div.finderDirectoryName > span {

background: rgb(56, 117, 215);

border-radius: 8px;

color: white;

padding: 1px 7px;

}

div#finderDragAndDropDialogue {

position: fixed;

width: 500px;

height: 500px;

top: 50%;

left: 50%;

margin: -250px 0 0 -250px;

box-shadow: 0 7px 100px rgba(0, 0, 0, 0.6);

background: #fff;

padding: 1px;

border-radius: 4px;

display: none;

}

div#finderDragAndDropDialogue h4 {

margin: 0;

padding: 10px;

}

img#finderDragAndDropDialogueActivity {

position: absolute;

top: 8px;

right: 50px;

}

div#finderDragAndDropDialogueProgressMeter {

position: absolute;

top: 11px;

right: 55px;

width: 210px;

height: 11px;

border-radius: 3px;

border: 1px solid rgb(181, 187, 200);

display: none;

}

div#finderDragAndDropDialogueProgressMeter div {

position: absolute;

top: 0;

left: 0;

height: 11px;

font-size: 0;

line-height: 0;

border-top-left-radius: 3px;

border-bottom-left-radius: 3px;

background: rgb(225, 228, 233);

width: 0;

display: none;

}

div#finderDragAndDropDialogueProgress {

position: absolute;

top: 10px;

right: 10px;

}

div#finderDragAndDropDialogueFiles table {

table-layout: fixed;

border-collapse: collapse;

margin: 0;

padding: 0;

width: 100%;

height: 100%;

}

div#finderDragAndDropDialogueFiles {

position: absolute;

overflow: auto;

top: 35px;

right: 5px;

bottom: 5px;

left: 5px;

border: 1px solid rgb(222, 222, 222);

}

div#finderDragAndDropDialogueFiles table th {

background: rgb(233, 233, 233);

border: 1px solid rgb(222, 222, 222);

text-align: left;

padding: 5px;

}

div#finderDragAndDropDialogueFiles table td {

padding: 5px;

border-left: 1px solid rgb(222, 222, 222);

border-right: 1px solid rgb(222, 222, 222);

overflow: hidden;

text-overflow: ellipsis;

vertical-align: top;

}

td.finderDragAndDropDialogueFileIcon img {

max-height: 100px;

}

The CSS is saved as Example 11-2.css. Finally, the following script and on the following pages completes the HTML drag-and-drop API demo:

$.fn.extend({

outerHTML : function()

{

var temporary = $("<div/>").append($(this).clone());

var html = temporary.html();

temporary.remove();

return html;

},

enableDragAndDrop : function()

{

return this.each(

function()

{

if (typeof this.style.WebkitUserDrag != 'undefined')

{

this.style.WebkitUserDrag = 'element';

}

if (typeof this.draggable != 'undefined')

{

this.draggable = true;

}

if (typeof this.dragDrop == 'function')

{

this.dragDrop();

}

}

);

}

});

dragAndDrop = {

path : null,

files : [],

openProgressDialogue : function(files, path)

{

this.path = path;

$('div#finderDragAndDropDialogue')

.fadeIn('fast');

this.files = [];

$(files).each(

function(key, file)

{

dragAndDrop.addFileToQueue(file);

}

);

if (this.files.length)

{

this.upload();

}

else

{

this.closeProgressDialogue();

}

},

closeProgressDialogue : function()

{

// Uncomment this section to automatically close the

// dialogue after upload

//$('div#finderDragAndDropDialogue')

// .fadeOut('fast');

//$('div#finderDragAndDropDialogue tbody tr')

// .not('tr.finderDragAndDropDialogueTemplate')

// .remove();

},

addFileToQueue : function(file)

{

if (!file.name && file.fileName)

{

file.name = file.fileName;

}

if (!file.size && file.fileSize)

{

file.size = file.fileSize;

}

this.files.push(file);

var tr = $('tr.finderDragAndDropDialogueTemplate').clone(true);

tr.removeClass('finderDragAndDropDialogueTemplate');

// Preview image uploads by showing a thumbnail of the image

if (file.type.match(/∧image\/.*$/) && FileReader)

{

var img = document.createElement('img');

img.file = file;

tr.find('td.finderDragAndDropDialogueFileIcon')

.html(img);

var reader = new FileReader();

reader.onload = function(event)

{

img.src = event.target.result;

};

reader.readAsDataURL(file);

}

tr.find('td.finderDragAndDropDialogueFile')

.text(file.name);

tr.find('td.finderDragAndDropDialogueFileSize')

.text(this.getFileSize(file.size));

tr.attr('title', file.name);

$('div#finderDragAndDropDialogueFiles tbody').append(tr);

},

http : null,

upload : function()

{

this.http = new XMLHttpRequest();

if (this.http.upload && this.http.upload.addEventListener)

{

this.http.upload.addEventListener(

'progress',

function(event)

{

if (event.lengthComputable)

{

$('div#finderDragAndDropDialogueProgressMeter')

.show();

$('div#finderDragAndDropDialogueProgressMeter div')

.show();

var progress = Math.round(

(event.loaded * 100) / event.total

);

$('div#finderDragAndDropDialogueProgress span')

.text(progress);

$('div#finderDragAndDropDialogueProgressMeter div')

.css('width', progress + '%');

}

},

false

);

this.http.upload.addEventListener(

'load',

function(event)

{

$('div#finderDragAndDropDialogueProgress span')

.text(100);

$('div#finderDragAndDropDialogueProgressMeter div')

.css('width', '100%');

}

);

}

this.http.addEventListener(

'load',

function(event)

{

// This event is fired when the upload completes and

// the server-side script /file/upload.json sends back

// a response.

dragAndDrop.closeProgressDialogue();

// If the server-side script sends back a JSON response,

// this is how you'd access it and do something with it.

var json = $.parseJSON(dragAndDrop.http.responseText);

},

false

);

if (typeof FormData !== 'undefined')

{

var form = new FormData();

// The form object invoked here is a built-in object, provided

// by the browser; it allows you to specify POST variables

// in the request for the file upload.

form.append('path', this.path);

$(this.files).each(

function(key, file)

{

form.append('file[]', file);

form.append('name[]', file.name);

form.append('replaceFile[]', 1);

}

);

// This sends a POST request to the server at the path

// /file/upload.php. This is the server-side file that will

// handle the file upload.

this.http.open('POST', 'file/upload.json');

this.http.send(form);

}

else

{

console.log(

'This browser does not support HTML 5 ' +

'drag and drop file uploads.'

);

this.closeProgressDialogue();

}

},

getFileSize : function(bytes)

{

switch (true)

{

case (bytes < Math.pow(2,10)):

{

return bytes + ' Bytes';

}

case (bytes >= Math.pow(2,10) && bytes < Math.pow(2,20)):

{

return Math.round(

bytes / Math.pow(2,10)

) +' KB';

}

case (bytes >= Math.pow(2,20) && bytes < Math.pow(2,30)):

{

return Math.round(

(bytes / Math.pow(2,20)) * 10

) / 10 + ' MB';

}

case (bytes > Math.pow(2,30)):

{

return Math.round(

(bytes / Math.pow(2,30)) * 100

) / 100 + ' GB';

}

}

},

applyEvents : function()

{

var context = null;

if (arguments[0])

{

context = arguments[0];

}

else

{

context = $('div.finderDirectory, div.finderFile');

}

context

.on(

'dragstart.finder',

function(event)

{

event.stopPropagation();

var html = $(this).outerHTML();

var dataTransfer = event.originalEvent.dataTransfer;

dataTransfer.effectAllowed = 'copyMove';

try

{

dataTransfer.setData('text/html', html);

dataTransfer.setData('text/plain', html);

}

catch (error)

{

dataTransfer.setData('Text', html);

}

}

)

.on(

'dragend.finder',

function(event)

{

if ($('div.finderDirectoryDrop').length)

{

$(this).removeClass('finderDirectoryDrop');

$(this).remove();

}

}

)

.on(

'dragenter.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

}

)

.on(

'dragover.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

if ($(this).is('div.finderDirectory'))

{

$(this).addClass('finderDirectoryDrop');

}

}

)

.on(

'dragleave.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

$(this).removeClass('finderDirectoryDrop');

}

)

.on(

'drop.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

var dataTransfer = event.originalEvent.dataTransfer;

var drop = $(this);

if (drop.hasClass('finderDirectory'))

{

if (dataTransfer.files && dataTransfer.files.length)

{

// Files dropped from outside the browser

dragAndDrop.openProgressDialogue(

dataTransfer.files,

node.data('path')

);

}

else

{

try

{

var html = dataTransfer.getData('text/html');

}

catch (error)

{

var html = dataTransfer.getData('Text');

}

html = $(html);

var dontAcceptTheDrop = (

html.data('path') == drop.data('path') ||

drop.is('div.finderFile')

);

if (dontAcceptTheDrop)

{

// Prevent file from being dragged onto itself

drop.removeClass('finderDirectoryDrop');

return;

}

if (html.hasClass('finderDirectory finderFile'))

{

// Do something with the dropped file

console.log(html);

}

}

}

}

);

}

};

$(document).ready(

function()

{

$(document).on(

'mousedown.finder',

'div.finderDirectory, div.finderFile',

function(event)

{

$(this).enableDragAndDrop();

$('div.finderIconSelected')

.removeClass('finderIconSelected');

$('span.finderDirectoryNameSelected')

.removeClass('finderDirectoryNameSelected');

$(this).find('div.finderIcon')

.addClass('finderIconSelected');

$(this).find('div.finderDirectoryName span')

.addClass('finderDirectoryNameSelected');

}

);

dragAndDrop.applyEvents();

$('div#finderFiles')

.on(

'dragenter.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

}

)

.on(

'dragover.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

$(this).addClass('finderDirectoryDrop');

}

)

.on(

'dragleave.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

$(this).removeClass('finderDirectoryDrop');

}

)

.on(

'drop.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

var dataTransfer = event.originalEvent.dataTransfer;

var drop = $(this);

if (dataTransfer.files && dataTransfer.files.length)

{

dragAndDrop.openProgressDialogue(

dataTransfer.files,

drop.data('path')

);

}

else

{

try

{

var html = dataTransfer.getData('text/html');

}

catch (error)

{

var html = dataTransfer.getData('Text');

}

html = $(html);

if (drop.data('path') == html.data('path'))

{

// Prevent file from being dragged onto itself

drop.removeClass('finderDirectoryDrop');

return;

}

if (!html.hasClass('finderDirectory finderFile'))

{

return;

}

var fileExists = false;

$('div.finderFile, div.finderDirectory').each(

function()

{

if ($(this).data('path') == html.data('path'))

{

fileExists = true;

return false;

}

}

);

if (!fileExists)

{

dragAndDrop.applyEvents(html);

drop.append(html);

}

}

}

);

}

);

The preceding JavaScript is saved as Example 11-2.js, and loading Example 11-2.html in Safari produces the screen shot that you see in Figure 11.2 when you drag some files onto the browser window.

image

Figure 11.2

The example presented in Example 11-2 is significantly longer than Example 11-1, but it offers a more complete implementation of the drag-and-drop API in a web-based file/folder manager paradigm. The following is an examination of the bits and pieces ofExample 11-2 that are new from Example 11-1.

Adding the File Information Data Object

The first new piece is the creation of a new JavaScript object called dragAndDrop. This new object holds most of the logic for the implementation of drag-and-drop file uploads. You define two new properties, path and files, which keep track of the current file path that you're uploading to and what files you're uploading to that location. The first method on the new dragAndDrop object that you created is called openProgressDialogue(). (Dialogue is spelled the English way, rather than the American way, which is simply a personal idiosyncrasy.)

dragAndDrop = {

path : null,

files : [],

openProgressDialogue : function(files, path)

{

this.path = path;

$('div#finderDragAndDropDialogue')

.fadeIn('fast');

this.files = [];

$(files).each(

function(key, file)

{

dragAndDrop.addFileToQueue(file);

}

);

if (this.files.length)

{

this.upload();

}

else

{

this.closeProgressDialogue();

}

},

Within the openProgressDialogue() method, you copy the path argument, which indicates the path you want to upload the files to, to this.path. And you make the progress dialogue visible by calling the fadeIn('fast') method on the <div> element with the classnamefinderDragAndDropDialogue. The files dragged and dropped for upload are passed in the files argument. The files variable is an array (it remains an array whether one file is uploaded or many), and it is iterated using jQuery's each() method. The call todragAndDrop.addFileToQueue() adds the file to the this.files array and also adds the file to the progress dialogue's table so that the user can preview upload progress. If this.files has a length greater than zero, the method this.upload() is called to execute the file upload. If this.files has a length of zero, this.closeProgressDialogue() is called to close the progress dialogue. Logically speaking, the this.closeProgressDialogue() route should be impossible given that the dialogue is not opened unless one or more files are present to upload. This route is represented to cover all bases in implementing a reusable file upload API.

The next method implemented in the dragAndDrop object is the closeProgressDialogue() method.

closeProgressDialogue : function()

{

// Uncomment this section to automatically close the

// dialogue after upload

//$('div#finderDragAndDropDialogue')

// .fadeOut('fast');

//$('div#finderDragAndDropDialogue tbody tr')

// .not('tr.finderDragAndDropDialogueTemplate')

// .remove();

},

The closeProgressDialogue() method is called automatically when the file upload has completed. It contains some code that you want to uncomment, upon implementing the server-side portion, which closes and resets the progress dialogue.

The following method, addFileToQueue(), sets up the <table> in the progress dialogue with a summary of each file uploaded so that the user can see visual feedback regarding their upload attempt. It creates thumbnails for any images uploaded and adds the files to thethis.files array.

addFileToQueue : function(file)

{

if (!file.name && file.fileName)

{

file.name = file.fileName;

}

if (!file.size && file.fileSize)

{

file.size = file.fileSize;

}

The first section normalizes the file object, moving the file.fileName property to file.name and the file.fileSize property to file.size in browsers whose makers preferred the longer property names. Then the file object is added to the this.files array via a call topush().

this.files.push(file);

The next line of code clones the <tr> element with the classname finderDragAndDropDialogueTemplate with a call to clone(true), which is ultimately added to the <table> with summary data about each file uploaded.

var tr = $('tr.finderDragAndDropDialogueTemplate').clone(true);

The finderDragAndDropDialogueTemplate classname is removed from the template. The classname hides the template from the user and identifies the <tr> element as a template.

tr.removeClass('finderDragAndDropDialogueTemplate');

The next line examines the MIME type of the uploaded file by checking to see if the MIME type begins with the string 'image/' using a regular expression, and it checks to see if the FileReader object exists, which is needed to display thumbnails of the uploading image files to the user. At present it is only possible to display thumbnails of image files.

// Preview image uploads by showing a thumbnail of the image

if (file.type.match(/∧image\/.*$/) && FileReader)

{

The thumbnail creation begins by creating a new <img /> element. The file is assigned to the file property of the <img /> element.

var img = document.createElement('img');

img.file = file;

The <img /> element is added to the <td> element with the classname finderDragAndDropDialogueFileIcon with a call to html().

tr.find('td.finderDragAndDropDialogueFileIcon')

.html(img);

The FileReader object is instantiated, which plays the critical role of reading the file to display the image, which is ultimately scaled down to thumbnail size by the style sheet.

var reader = new FileReader();

An onload event is created that assigns the src attribute of the <img /> element; each image src is created using data URIs. The FileReader object provides a base64-encoded data URI representation of the image, which is assigned to the src attribute, thus making it possible to preview each image file.

reader.onload = function(event)

{

img.src = event.target.result;

};

reader.readAsDataURL(file);

}

Each filename is placed inside the <td> with the classname finderDragAndDropDialogueFile.

tr.find('td.finderDragAndDropDialogueFile')

.text(file.name);

And each file size is converted from bytes to a human-readable number in one of bytes, kilobytes, megabytes, and such depending on the size of the number by virtue of a call to dragAndDrop.getFileSize(). The resulting value is placed inside the <td> element with the classname finderDragAndDropDialogueFileSize.

tr.find('td.finderDragAndDropDialogueFileSize')

.text(this.getFileSize(file.size));

The file.name is assigned to the title attribute of the <tr> element.

tr.attr('title', file.name);

Finally, the completed <tr> template is added to the <tbody> element.

$('div#finderDragAndDropDialogueFiles tbody').append(tr);

},

Using a Custom XMLHttpRequest Object

The next property and method provide the data transfer functionality for the uploaded files. This involves setting up a custom XMLHttpRequest object, which is in turn assigned to this.http.

http : null,

upload : function()

{

this.http = new XMLHttpRequest();

A series of events is set up to monitor upload progress and to watch out for upload completion. First, you check whether the upload object exists on the XMLHttpRequest (hereafter, I will simply call the XMLHttpRequest object just http), and you check to see whether theaddEventListener method exists on the upload object.

if (this.http.upload && this.http.upload.addEventListener)

{

You next set up an event listener for the progress event on the upload object. This event ultimately tells you the overall progress of the file upload, whether one file is uploaded or many.

this.http.upload.addEventListener(

'progress',

function(event)

{

The event.lengthComputable property tells you whether there is any progress to report.

if (event.lengthComputable)

{

The <div> with id name finderDragAndDropDialogueProgressMeter is displayed, as well as the <div> element within that one.

$('div#finderDragAndDropDialogueProgressMeter')

.show();

$('div#finderDragAndDropDialogueProgressMeter div')

.show();

File upload progress is calculated as a rounded percentage from the event.loaded and event.total properties that are provided in the event object.

var progress = Math.round(

(event.loaded * 100) / event.total

);

The resulting progress figure is added to the <span> nested with the <div> element with id name finderDragAndDropDialogueProgress.

$('div#finderDragAndDropDialogueProgress span')

.text(progress);

Then the <div> nested within the <div> with id name finderDragAndDropDialogueProgressMeter is given a percentage width, which also indicates the progress.

$('div#finderDragAndDropDialogueProgressMeter div')

.css('width', progress + '%');

}

},

false

);

Next, a load event is attached to the upload object to cover what happens when 100 percent upload progress is reached.

this.http.upload.addEventListener(

'load',

function(event)

{

$('div#finderDragAndDropDialogueProgress span')

.text(100);

$('div#finderDragAndDropDialogueProgressMeter div')

.css('width', '100%');

}

);

}

A load event is also attached to the http object, which is fired when the server side has responded to the upload request.

this.http.addEventListener(

'load',

function(event)

{

// This event is fired when the upload completes and

// the server-side script /file/upload.json sends back

// a response.

When the upload request is completed, the progress dialogue is closed with a call to dragAndDrop.closeProgressDialogue().

dragAndDrop.closeProgressDialogue();

// If the server-side script sends back a JSON response,

// this is how you'd access it and do something with it.

If the server-side sends back a JSON response, it can be read and parsed from the http.responseText property, and the application can respond to data in the JSON response appropriately.

var json = $.parseJSON(dragAndDrop.http.responseText);

},

false

);

Check for the existence of the FormData object to see if a more recent revision of drag-and-drop upload is supported by your browser. The FormData object is provided by the browser and facilitates the creation of the HTTP request that will ultimately pass the uploaded file data across the Internet. In this example, the FormData object creates a POST request with encoding multipart/form-data. In a traditional file upload, you would have to add multipart/form-data to the <form> element in the enctype attribute. The FormData object takes care of this for you automatically.

if (typeof FormData !== 'undefined')

{

var form = new FormData();

// The form object invoked here is a built-in object, provided

// by the browser; it allows you to specify POST variables

// in the request for the file upload.

You can append arguments to the FormData object by using the append() method. The first argument appended is the path argument. This creates a POST variable called path on the server side and signals to the server-side script where you want to upload the files.

form.append('path', this.path);

Each file is iterated by passing the this.files array to the each() method. The file is passed to the server side in a file[] array. The square brackets are used by PHP to signal the creation of an array. The syntax used to do this might be different in your server-side language of choice. Some additional information is passed on to the server in a name[] variable and the replaceFile[] variable. You can create as many variables as you need.

$(this.files).each(

function(key, file)

{

form.append('file[]', file);

form.append('name[]', file.name);

form.append('replaceFile[]', 1);

}

);

Finally, the entire POST request including the uploaded file data is sent on to the server-side script for processing. file/upload.json provides a canned JSON response in the absence of a real server-side script.

// This sends a POST request to the server at the path

// /file/upload.php. This is the server-side file that will

// handle the file upload.

this.http.open('POST', 'file/upload.json');

this.http.send(form);

}

If your browser does not support the FormData object, you print a message to the JavaScript console and close the progress dialogue.

else

{

console.log(

'This browser does not support HTML 5 ' +

'drag and drop file uploads.'

);

this.closeProgressDialogue();

}

},

Additional Utilities

The remaining methods in the JavaScript act as utilities for simplifying the remaining actions of size calculation, string manipulation, and event application.

The getFileSize() method returns a human-readable representation of file size. The file size in bytes is fed into the method, and a number representing the file size in bytes, kilobytes (KB), megabytes (MB), or gigabytes (GB) is returned. This method uses theMath.pow() method, where Math.pow(2,10) = 1 KB, Math.pow(2,20) = 1 MB, and Math.pow(2,30) = 1 GB.

getFileSize : function(bytes)

{

switch (true)

{

case (bytes < Math.pow(2,10)):

{

return bytes + ' Bytes';

}

case (bytes >= Math.pow(2,10) && bytes < Math.pow(2,20)):

{

return Math.round(

bytes / Math.pow(2,10)

) +' KB';

}

case (bytes >= Math.pow(2,20) && bytes < Math.pow(2,30)):

{

return Math.round(

(bytes / Math.pow(2,20)) * 10

) / 10 + ' MB';

}

case (bytes > Math.pow(2,30)):

{

return Math.round(

(bytes / Math.pow(2,30)) * 100

) / 100 + ' GB';

}

}

},

The applyEvents() method applies all the drag-and-drop events that you first implemented in Example 11-1. You start the method by creating a context variable, which decides how the events are applied. If you apply the events to a file or folder object that has been dragged and dropped from outside the browser window, this method applies each drag-and-drop event to the newly moved file or folder object. Otherwise, each event is applied to every file and folder object that is present.

applyEvents : function()

{

var context = null;

if (arguments[0])

{

context = arguments[0];

}

else

{

context = $('div.finderDirectory, div.finderFile');

}

context

The dragstart, dragend, dragenter, dragover, and dragleave events remain unchanged from Example 11-1. The drop event has been modified to accommodate drag-and-drop file uploads as well as moving around existing files or folders.

.on(

'drop.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

var dataTransfer = event.originalEvent.dataTransfer;

var drop = $(this);

if (drop.hasClass('finderDirectory'))

{

If the user has dragged and dropped files for upload to the browser window, the files are present in the dataTransfer.files property, and the files property has a length greater than zero. The files, along with the path of the folder the files were dropped on, passes along to the openProgressDialogue() method for processing and upload.

if (dataTransfer.files && dataTransfer.files.length)

{

// Files dropped from outside the browser

dragAndDrop.openProgressDialogue(

dataTransfer.files,

node.data('path')

);

}

else

{

try

{

var html = dataTransfer.getData('text/html');

}

catch (error)

{

var html = dataTransfer.getData('Text');

}

html = $(html);

Other than file uploads by drag and drop, the drop event listener works the same as it did in Example 11-1.

Along with the drag-and-drop events that are applied to each file and folder object, a new collection of drag-and-drop events are also applied to the <div> element with id name finderFiles, which is the folder view taking up nearly all the browser window that holds all the file and folder elements. This <div> element receives dragenter, dragover, and dragleave events that are identical to the dragenter, dragover, and dragleave events already placed on <div> elements with classnames finderFile or finderDirectory. This leaves the dropevent, which is slightly different.

.on(

'drop.finder',

function(event)

{

event.preventDefault();

event.stopPropagation();

var dataTransfer = event.originalEvent.dataTransfer;

var drop = $(this);

As you did with the other drop event, check that the files property has a length greater than zero, which lets your application know that the user has dropped files onto the browser window.

if (dataTransfer.files && dataTransfer.files.length)

{

dragAndDrop.openProgressDialogue(

dataTransfer.files,

drop.data('path')

);

}

else

{

try

{

var html = dataTransfer.getData('text/html');

}

catch (error)

{

var html = dataTransfer.getData('Text');

}

html = $(html);

You also make sure that the folder isn't dropped on itself:

if (drop.data('path') == html.data('path'))

{

// Prevent file from being dragged onto itself

drop.removeClass('finderDirectoryDrop');

return;

}

You make sure that the dropped HTML has the finderDirectory and finderFile class names:

if (!html.hasClass('finderDirectory finderFile'))

{

return;

}

Finally, check that any file or folder dropped onto the directory doesn't already exist in the directory by examining each of the filenames in that directory locally. In this example, the application simply stops when it detects a duplicate file locally. Another approach is upon detecting a duplicate file, you ask users if they wants to replace the duplicate file; then you pass that selection onto the server side, which should also perform validation for existing files or folders. In addition, the same duplicate filename check should be done for drag-and-drop file uploads. I have removed the extra validation in the interest of keeping the script shorter and more to the point.

var fileExists = false;

$('div.finderFile, div.finderDirectory').each(

function()

{

if ($(this).data('path') == html.data('path'))

{

fileExists = true;

return false;

}

}

);

If the file or folder does not already exist, you would do something with the dropped HTML here.

if (!fileExists)

{

dragAndDrop.applyEvents(html);

drop.append(html);

}

}

}

);

Summary

In this chapter you learned how to use jQuery to leverage the HTML5 drag-and-drop API. You implemented the drag-and-drop API using the CSS property –webkit-user-drag, the draggable HTML attribute, and the legacy dragDrop() method. You also learned how to implement the drag-and-drop API in JavaScript by virtue of attaching listeners to the following events: dragstart, drag, dragend, dragenter, dragover, drop, and dragleave.

You also learned how to implement drag-and-drop file uploads using the drag-and-drop API, which includes looking for the files property on the dataTransfer object. You learned how to preview thumbnails of uploading image files using the FileReader object. You learned how to monitor upload progress by attaching progress and load events to the upload property of the XMLHttpRequest object. Finally, you learned how to customize an HTTP POST request and submit the file upload to the server side using the XMLHttpRequest andFormData objects.

Exercises

1. Describe how you enable drag-and-drop functionality on an element. Which methods are legacy methods and what browsers do the legacy methods exist for?

2. List the events in the order that they fire that are used to drag an element.

3. List the events in the order that they fire that are used to drop an element.

4. When you implement drag-and-drop file uploads, what property do you look for, and using which event, to detect that a drag-and-drop file upload has taken place? For extra credit, what property would you use if you weren't using jQuery?

5. When implementing a thumbnail preview of image files, what format is used to view preview images?

6. When creating a drag-and-drop file upload, what events can monitor file upload progress? What object do you attach these events to?

7. Which event properties calculate file upload progress percentages?

8. Describe how you would create custom POST variables in the HTTP request that is generated for a drag-and-drop file upload.

9. How do you know that a file upload was successful?