Alex Tech Adventures The webs best tutorials!

drag and drop upload with user data using HTML5 with FireFox

(0 votes, average 0 out of 5)

Drag and Drop event handling is one of the new toys in HTML5 available since FireFox 3.6. You can now let users drag files of just about any type directory onto web page without having to use <input type="file">, let them see the preview immediately without an intermediate upload and upload directly through a new binary XmlHttpRequest. Sending some other data, say a description of the image being uploaded, together with the binary file is a more complex story for which I did not see a solution yet and had to figure out for myself.

Getting the dropped file to display on the page is elementary enough and is well outlined here (event handling), here (File Api), and here (example to display image). If you want to upload multiple files along with some extra data you can build your own POST string, but the technique seems way too complicated than it should be. Here's my way which uses JSON and PHP's seldomly used raw input stream.

I will start by setting up JSON object for a single image upload first so you have a chance to expand along the way.
Now let's get the boring stuff out of the way, which would be the design layout where you would let your users drag the files to and then see the preview. As most of you already know, I am not a designer so please bare with the uglyness of the following:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
</head>
<body>
<div id="uploadArea" style="background-color: green; width: 100px; height: 100px;">
upload here
</div>
<form method="post" action="backend.php" enctype="multipart/form-data" id="imageform">
<input type="button" onClick="uploadImages()" value="Upload" />
<input type="text" name="description" />
<input type="submit" />
</form>

Nothing special yet. Its just a form with input for image description and a button that will call uploadImages() method.
This next part you would probably use with assistance of jQuery or similar to make sure DOM is fully loaded and accessible but for this tutorial I will use raw JS and to avoid problems put all JS code at the end. This code can be found in all articles on drag and drop blogs so I won't say much about it besides mentioning that all it does is create an event listener for the drop event on the <div> we just created so we can do some display manipulation. We are also reading the files dropped so we can process them later in handleFiles() method.

13
14
15
16
17
18
19
20
21
22
23
24
25
<script type="text/javascript">
uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener("dragover", function(event) {
 event.preventDefault();
}, true);
uploadArea.addEventListener("drop", function(event) {
 event.preventDefault();
 
 var dt = event.dataTransfer;
 var files = dt.files;
 
 handleFiles(files);
}, true);

So with event listeners setup and files read into files array on lines 21 and 22, we are calling handleFiles() which cycles through the files array which drop event collected.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function handleFiles(files){
 for (var i = 0; i<files.length; i++) {
   var file = files[i];
   var img = document.createElement("img");
   img.setAttribute('id', 'image1');
   img.file = file;
   document.getElementById("imageform").appendChild(img);
 
   var reader = new FileReader();
   reader.onloadend = function() {
    img.src = reader.result;
   }
   reader.readAsDataURL(file);
 }
}

 

Still no upload yet. This is purely to show users what they are about to upload by grabbing file's raw binary data in 35 using FileReader from 34. The way this is done, however, does not seem standard from first glance. You would expect something like img.src=reader.readAsDataURL(file) but instead the passage of data from file to src of img tag is a result of an event rather than a direct action, hence it is created as a part of an event handler between 35 and 37 with 38 triggering the actual event. To display the image as a part of the page you add it to the overall document DOM, or whichever part of it, around 32. At this point you have an opportunity to layout your images around form data in any way.

Now for the fun uploading part. As already stated, uploadImages() function does the job:
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function uploadImages() {
  image = document.getElementsByTagName('img')[0].getAttribute('src');
  var jsonobject = {"image1":image};
  var req = new XMLHttpRequest();  
 
  req.addEventListener("progress", function(event) {
    if(event.lengthComputable){
     var percentage = Math.round((e.loaded * 100) / e.total);
     document.write(percentage);
    }
  }, false);
 
  var jsonstring = JSON.stringify(jsonobject);
  req.open("POST", 'backend.php', true);
  req.overrideMimeType('text/plain; charset=x-user-defined-binary');
  req.sendAsBinary(jsonstring);
  req.onload = function(event) { 
          alert(req.responseText);   
  };
}

 

Lots going on here. First we got to grab the binary content of the image we want to upload and thats line 42. You can do this anyway you want. In fact you can avoid traversing DOM altogether and pass binary image as a parameter. Then we create a JSON object which will act as a temporary storage for all we want to send to PHP. At this point, line 43 prepares only for a single image upload. I will expand this later to handle full form upload. Line 44 creates the XMLHttpRequest which will be used to communicate with PHP in the background. 46-51 uses a new event listener available since FireFox 3.5 "progress" allowing to monitor upload progress without need of complicated Perl scripts on the server. You can show the upload progress in anyway you wish; I am just dumping the percentage values on the page. Unfortunately, if this is a local app where transmission is instantaneous you will not see it.
So 43 grabs the image to a JSON object which now needs to be presented in a format other languages, like PHP, can interpret therefore it needs to be "translated" to a more familiar string notation at 53. 54 opens the connection to the PHP backend using POST and 55 lets PHP know that the content coming in is in no specific standard so it needs not touch it, otherwise it will try to make it more "friendly" to us, the way it does with usual application/x-www-form-urlencoded, and break the binary string.
56 is where the magic happens. New XmlHttpRequest is capable of sending raw binary without messing it up. Before it, for binary data to survive you had to encode the binary using base64 which is extremely resource intensive and hanged up my 8 core i7 920 CPU for half a minute while processing a 1.7Mb image.

So all is well on JavaScript side and the image, along with whatever other data you want which I will take care of in just a bit, arrives at the server but PHP has no idea what to do with this binary blurb. Do not even try using $_POST or $_FILE: we have to break up the binary blurb manually :D

This is where we use php://input which gives us direct access to raw input stream that PHP looks at. We access it using file_get_contents (2). Thankfully, we know what the binary blurb is: it is a JSON object so we decode it straight away with json_decode (3). We will get an array with both binary data and user text input. Unfortunately, FireFox adds something annoying to the top of all binary images which gets in the way of further processing so thats going to be removed next (4). Finally, the binary sections of the array can be decoded using base64_decode (5)and you can carry on processing data as required (6-8).

1
2
3
4
5
6
7
8
9
<?php
$rawdata = file_get_contents('php://input');
$data = json_decode($rawdata, true);
$data = str_replace('data:image/jpeg;base64,', '', $data['image1']);
$image = base64_decode($data);
 
$uploadPath = "/home/tmth/webdev/playground/fileAPI/uploads/";
$uploadPath = $uploadPath.'image.jpg';
file_put_contents($uploadPath, $image);


Now as promised I have to add the rest of user's input and ability to have multiple images if necessary. Here is the complete code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<html>
<head>
</head>
<body>
<div id="uploadArea" style="background-color: green; width: 100px; height: 100px;">
upload here
</div>
<form method="post" action="backend.php" enctype="multipart/form-data" id="imageform">
<input type="button" onClick="uploadImages()" value="Upload" />
<input type="submit" />
</form>
 
<script type="text/javascript">
uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener("dragover", function(event) {
 event.preventDefault();
}, true);
uploadArea.addEventListener("drop", function(event) {
 event.preventDefault();
 
 var dt = event.dataTransfer;
 var files = dt.files;
 
 handleFiles(files);
}, true);
 
function handleFiles(files){
 for (var i = 0; i<files.length; i++) {
   var file = files[i];
   var img = document.createElement("img");
   img.setAttribute('id', 'image'+file.name);
   img.setAttribute('class', 'droppedImage');
   img.setAttribute('filename', file.name);
   img.file = file;
 
   input = document.createElement("input");
   input.setAttribute('type', 'text');
   input.setAttribute('id', 'description_image'+file.name);
 
   imageform = document.getElementById("imageform");
   imageform.appendChild(img);
   imageform.appendChild(input);
   imageform.appendChild(document.createElement("br"));
 
   var reader = new FileReader();
   reader.onloadend = function() {
    img.src = reader.result;
   }
   reader.readAsDataURL(file);
 }
}
 
function uploadImages() {
  images = document.getElementsByClassName('droppedImage');
  imagesJson = new Object;
  imagesJson.images = new Array;
  for(var i = 0; i<images.length; i++){
    imagesJson.images[i] = new Object;
    imagesJson.images[i].image = images[i].getAttribute('src');
 
    descriptionField = document.getElementById('description_'+images[i].getAttribute('id'));
    imagesJson.images[i].description = descriptionField.value;
 
    imagesJson.images[i].filename = images[i].getAttribute('filename');
  }
  var req = new XMLHttpRequest();  
 
  req.addEventListener("progress", function(event) {
    if(event.lengthComputable){
     var percentage = Math.round((e.loaded * 100) / e.total);
     document.write(percentage);
    }
  }, false);
 
  var jsonstring = JSON.stringify(imagesJson);
  var param = 'data='+jsonstring;
  req.open("POST", 'backend.php', true);
  req.overrideMimeType('text/plain; charset=x-user-defined-binary');
  req.sendAsBinary(jsonstring);
  req.onload = function(event) { 
          alert(req.responseText);   
  };
}
</script>
 
</body>
</html>

 

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$uploadPath = "/home/tmth/webdev/playground/fileAPI/uploads/";
 
$rawdata = file_get_contents('php://input');
$data = json_decode($rawdata, true);
$images = $data['images'];
foreach($images as $image) {
  $imageBin = str_replace('data:image/jpeg;base64,', '', $image['image']);
  $imageBin = base64_decode($imageBin);
  file_put_contents($uploadPath.$image['filename'], $imageBin);
  echo $image['description'].'<br />';
}

 

Lots of changes here but the principle is identical. The only thing that really changed is that instead of hard coding names of input I am cycling through them using loops.
  • at 32 I am also adding a class attribute so I can quickly lookup all images that have been dragged on and on 31 I am adding an ID that I could use to lookup corresponding description field which will be description_imageID.
  • 33 adds filename attribute which will be used by PHP.
  • 36-38 add corresponding input field for description input.
  • 54 gets all tags with class name droppedImage, which were added by 32, which are then cycled over in a for array at 57.
  • 55 and 56 simply prepare an object to be used as a JSON container to store data.
  • 58-64 simply iterate over array created at 54 and fill out JSON container from above.
  • complete JSON array is sent as usual at 79
  • Nothing different from PHP either except that $data is now an array of images instead of a single image so foreach loop is used.  And because it came from a complete JSON object we also have access to everything user typed in child array.


I hope this made sense. Please ask any questions or suggest better ways to do this.

Now for the rant.
This is something that was ever available in AMF protocol of flash so here is another case scenario for "HTML5 a flash killer" argument.  Well, maybe in another 20 years.  This feature has two major problems. First, it is almost brand new and is available only for a very limited audience: FireFox 3.6+.  Secondly, it is still too much work.  Hopefully, new FormData API in FireFox 3.7 will address this but then the first issue becomes x100.  How is this too much work?  Assembling that JSON string is a series labor undertaking compared to what I would have to do in Flex, or rather having to do nothing in Flex.  In Flex I am constantly working around Data Objects rather than raw strings and they are easily accessible without having to resort to endless selectors.  Once I am finished filling out Data Object, be it in the background or through user actions, it is done, I just send that object straight through AMF stream and PHP picks it up stress free. PHP AMF reader from Zend is even simpler than having to dig through JSON due to a more object oriented nature.

Amount of code aside, the fact that this is locked in to a single (as of this writing) version of browser makes it useless for a wider audience.  I can only afford to do this because this is for a single client, not the whole world to use so I have no problems saying "you can only use this with FF 3.6 or up" (I am still not brave enough to say "you can only use this with FF 3.7 alpha 5" in order to use FormData).  I know HTML5 will eventually become standard and all of these features will be similar across all browsers, but some functions in this area are so browser specific that to achieve certain effects one will have to write a separate script for each browser type and we are back at IE6 problem again!!  A "generic" plugin avoids this danger. 

I may sound like "pro Flash", but this is not the case.  On this adventure I cam across many interesting functions.  One could achieve incredible results in features and interactivity if writing specifically to a certain engine, say Gecko 1.9.3, rather than applying to global standards.  And I cannot find any text editors for Flex that could do at least 25% of what TinyMCE can. You just have to be careful and not anger your clients when forcing them to use something specific.  So no one will be killed, Java Applets are still around where they belong as will Flash and HTML5: each will be used where appropriate.

Last Updated ( Saturday, 15 May 2010 03:59 )  
You need to login or register to post comments.
Discuss...
You are here: Home Development (X) HTML / CSS / JavaScript drag and drop upload with user data using HTML5 with FireFox

Statistics

Members : 1401
Content : 42
Web Links : 1
Content View Hits : 191198

Poll

Interested in TinyBrowser and TinyMce plugin for ZF?
 

Who's Online

We have 39 guests online