├── .hgtags ├── README.md ├── jquery.filedrop.js └── package.json /.hgtags: -------------------------------------------------------------------------------- 1 | 0d76f94877b480b43284226a8d95c56847bf7cf3 0.1.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jQuery filedrop plugin 2 | ====================== 3 | 4 | HTML5 drag desktop files into browser 5 | ------------------------------------- 6 | 7 | jQuery filedrop uses the HTML5 File API to allow users 8 | to drag multiple files from desktop to the browser, uploading 9 | each file to a user-specified URL. 10 | 11 | filedrop uses HTML5 FileReader() to read file data. 12 | 13 | Browser Support 14 | --------------- 15 | Works on Chrome, Firefox 3.6+, IE10+, and Opera 12+. 16 | 17 | Would love contribution for Safari support. 18 | 19 | filedrop also allows users to define functions to handle the `BrowserNotSupported` error. 20 | 21 | Usage Example 22 | --------------- 23 | 24 | ```javascript 25 | $('#dropzone').filedrop({ 26 | fallback_id: 'upload_button', // an identifier of a standard file input element, becomes the target of "click" events on the dropzone 27 | fallback_dropzoneClick : true, // if true, clicking dropzone triggers fallback file selection and the fallback element is made invisible. 28 | url: 'upload.php', // upload handler, handles each file separately, can also be a function taking the file and returning a url 29 | paramname: 'userfile', // POST parameter name used on serverside to reference file, can also be a function taking the filename and returning the paramname 30 | withCredentials: true, // make a cross-origin request with cookies 31 | data: { 32 | param1: 'value1', // send POST variables 33 | param2: function(){ 34 | return calculated_data; // calculate data at time of upload 35 | }, 36 | }, 37 | headers: { // Send additional request headers 38 | 'header': 'value' 39 | }, 40 | error: function(err, file) { 41 | switch(err) { 42 | case 'BrowserNotSupported': 43 | alert('browser does not support HTML5 drag and drop') 44 | break; 45 | case 'TooManyFiles': 46 | // user uploaded more than 'maxfiles' 47 | break; 48 | case 'FileTooLarge': 49 | // program encountered a file whose size is greater than 'maxfilesize' 50 | // FileTooLarge also has access to the file which was too large 51 | // use file.name to reference the filename of the culprit file 52 | break; 53 | case 'FileTypeNotAllowed': 54 | // The file type is not in the specified list 'allowedfiletypes' 55 | break; 56 | case 'FileExtensionNotAllowed': 57 | // The file extension is not in the specified list 'allowedfileextensions' 58 | break; 59 | default: 60 | break; 61 | } 62 | }, 63 | allowedfiletypes: ['image/jpeg','image/png','image/gif'], // filetypes allowed by Content-Type. Empty array means no restrictions 64 | allowedfileextensions: ['.jpg','.jpeg','.png','.gif'], // file extensions allowed. Empty array means no restrictions 65 | maxfiles: 25, 66 | maxfilesize: 20, // max file size in MBs 67 | dragOver: function() { 68 | // user dragging files over #dropzone 69 | }, 70 | dragLeave: function() { 71 | // user dragging files out of #dropzone 72 | }, 73 | docOver: function() { 74 | // user dragging files anywhere inside the browser document window 75 | }, 76 | docLeave: function() { 77 | // user dragging files out of the browser document window 78 | }, 79 | drop: function() { 80 | // user drops file 81 | }, 82 | uploadStarted: function(i, file, len){ 83 | // a file began uploading 84 | // i = index => 0, 1, 2, 3, 4 etc 85 | // file is the actual file of the index 86 | // len = total files user dropped 87 | }, 88 | uploadFinished: function(i, file, response, time) { 89 | // response is the data you got back from server in JSON format. 90 | }, 91 | progressUpdated: function(i, file, progress) { 92 | // this function is used for large files and updates intermittently 93 | // progress is the integer value of file being uploaded percentage to completion 94 | }, 95 | globalProgressUpdated: function(progress) { 96 | // progress for all the files uploaded on the current instance (percentage) 97 | // ex: $('#progress div').width(progress+"%"); 98 | }, 99 | speedUpdated: function(i, file, speed) { 100 | // speed in kb/s 101 | }, 102 | rename: function(name) { 103 | // name in string format 104 | // must return alternate name as string 105 | }, 106 | beforeEach: function(file) { 107 | // file is a file object 108 | // return false to cancel upload 109 | }, 110 | beforeSend: function(file, i, done) { 111 | // file is a file object 112 | // i is the file index 113 | // call done() to start the upload 114 | }, 115 | afterAll: function() { 116 | // runs after all files have been uploaded or otherwise dealt with 117 | } 118 | }); 119 | ``` 120 | 121 | Queueing Usage Example 122 | ---------------------- 123 | 124 | To enable the upload of a large number of files, a queueing option was added that enables you to configure how many files should be processed at a time. The upload will process that number in parallel, backing off and then processing the remaining ones in the queue as empty upload slots become available. 125 | 126 | This is controlled via one of two parameters: 127 | 128 | ```javascript 129 | maxfiles: 10 // Limit the total number of uploads possible - default behaviour 130 | queuefiles: 2 // Control how many uploads are attempted in parallel (ignores maxfiles setting) 131 | ``` 132 | 133 | Not setting a value for `queuefiles` will disable queueing. 134 | 135 | Contributions 136 | ------------- 137 | 138 | * [Reactor5](http://github.com/Reactor5/) (Brian Hicks) 139 | * [jpb0104](http://github.com/jpb0104) 140 | * [boughtonp](https://github.com/boughtonp) (Peter Boughton) 141 | -------------------------------------------------------------------------------- /jquery.filedrop.js: -------------------------------------------------------------------------------- 1 | /*global jQuery:false, alert:false */ 2 | 3 | /* 4 | * Default text - jQuery plugin for html5 dragging files from desktop to browser 5 | * 6 | * Author: Weixi Yen 7 | * 8 | * Email: [Firstname][Lastname]@gmail.com 9 | * 10 | * Copyright (c) 2010 Resopollution 11 | * 12 | * Licensed under the MIT license: 13 | * http://www.opensource.org/licenses/mit-license.php 14 | * 15 | * Project home: 16 | * http://www.github.com/weixiyen/jquery-filedrop 17 | * 18 | * Version: 0.1.0 19 | * 20 | * Features: 21 | * Allows sending of extra parameters with file. 22 | * Works with Firefox 3.6+ 23 | * Future-compliant with HTML5 spec (will work with Webkit browsers and IE9) 24 | * Usage: 25 | * See README at project homepage 26 | * 27 | */ 28 | ;(function($) { 29 | 30 | var default_opts = { 31 | fallback_id: '', 32 | fallback_dropzoneClick : true, 33 | url: '', 34 | refresh: 1000, 35 | paramname: 'userfile', 36 | requestType: 'POST', // just in case you want to use another HTTP verb 37 | allowedfileextensions:[], 38 | allowedfiletypes:[], 39 | maxfiles: 25, // Ignored if queuefiles is set > 0 40 | maxfilesize: 1, // MB file size limit 41 | queuefiles: 0, // Max files before queueing (for large volume uploads) 42 | queuewait: 200, // Queue wait time if full 43 | data: {}, 44 | headers: {}, 45 | drop: empty, 46 | dragStart: empty, 47 | dragEnter: empty, 48 | dragOver: empty, 49 | dragLeave: empty, 50 | docEnter: empty, 51 | docOver: empty, 52 | docLeave: empty, 53 | beforeEach: empty, 54 | afterAll: empty, 55 | rename: empty, 56 | error: function(err, file, i, status) { 57 | alert(err); 58 | }, 59 | uploadStarted: empty, 60 | uploadFinished: empty, 61 | progressUpdated: empty, 62 | globalProgressUpdated: empty, 63 | speedUpdated: empty 64 | }, 65 | errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError", "FileExtensionNotAllowed"]; 66 | 67 | $.fn.filedrop = function(options) { 68 | var opts = $.extend({}, default_opts, options), 69 | global_progress = [], 70 | doc_leave_timer, stop_loop = false, 71 | files_count = 0, 72 | files; 73 | 74 | if ( opts.fallback_dropzoneClick === true ) 75 | { 76 | $('#' + opts.fallback_id).css({ 77 | display: 'none', 78 | width: 0, 79 | height: 0 80 | }); 81 | } 82 | 83 | this.on('drop', drop).on('dragstart', opts.dragStart).on('dragenter', dragEnter).on('dragover', dragOver).on('dragleave', dragLeave); 84 | $(document).on('drop', docDrop).on('dragenter', docEnter).on('dragover', docOver).on('dragleave', docLeave); 85 | 86 | if ( opts.fallback_dropzoneClick === true ) 87 | { 88 | if ( this.find('#' + opts.fallback_id).length > 0 ) 89 | { 90 | throw "Fallback element ["+opts.fallback_id+"] cannot be inside dropzone, unless option fallback_dropzoneClick is false"; 91 | } 92 | else 93 | { 94 | this.on('click', function(e){ 95 | $('#' + opts.fallback_id).trigger(e); 96 | }); 97 | } 98 | } 99 | 100 | $('#' + opts.fallback_id).change(function(e) { 101 | opts.drop(e); 102 | files = e.target.files; 103 | files_count = files.length; 104 | upload(); 105 | }); 106 | 107 | function drop(e) { 108 | if( opts.drop.call(this, e) === false ) return false; 109 | if(!e.originalEvent.dataTransfer) 110 | return; 111 | files = e.originalEvent.dataTransfer.files; 112 | if (files === null || files === undefined || files.length === 0) { 113 | opts.error(errors[0]); 114 | return false; 115 | } 116 | files_count = files.length; 117 | upload(); 118 | e.preventDefault(); 119 | return false; 120 | } 121 | 122 | function getBuilder(filename, filedata, mime, boundary) { 123 | var dashdash = '--', 124 | crlf = '\r\n', 125 | builder = '', 126 | paramname = opts.paramname; 127 | 128 | if (opts.data) { 129 | var params = $.param(opts.data).replace(/\+/g, '%20').split(/&/); 130 | 131 | $.each(params, function() { 132 | var pair = this.split("=", 2), 133 | name = decodeURIComponent(pair[0]), 134 | val = decodeURIComponent(pair[1]); 135 | 136 | if (pair.length !== 2) { 137 | return; 138 | } 139 | 140 | builder += dashdash; 141 | builder += boundary; 142 | builder += crlf; 143 | builder += 'Content-Disposition: form-data; name="' + name + '"'; 144 | builder += crlf; 145 | builder += crlf; 146 | builder += val; 147 | builder += crlf; 148 | }); 149 | } 150 | 151 | if (jQuery.isFunction(paramname)){ 152 | paramname = paramname(filename); 153 | } 154 | 155 | builder += dashdash; 156 | builder += boundary; 157 | builder += crlf; 158 | builder += 'Content-Disposition: form-data; name="' + (paramname||"") + '"'; 159 | builder += '; filename="' + encodeURIComponent(filename) + '"'; 160 | builder += crlf; 161 | 162 | builder += 'Content-Type: ' + mime; 163 | builder += crlf; 164 | builder += crlf; 165 | 166 | builder += filedata; 167 | builder += crlf; 168 | 169 | builder += dashdash; 170 | builder += boundary; 171 | builder += dashdash; 172 | builder += crlf; 173 | return builder; 174 | } 175 | 176 | function progress(e) { 177 | if (e.lengthComputable) { 178 | var percentage = Math.round((e.loaded * 100) / e.total); 179 | if (this.currentProgress !== percentage) { 180 | 181 | this.currentProgress = percentage; 182 | opts.progressUpdated(this.index, this.file, this.currentProgress); 183 | 184 | global_progress[this.global_progress_index] = this.currentProgress; 185 | globalProgress(); 186 | 187 | var elapsed = new Date().getTime(); 188 | var diffTime = elapsed - this.currentStart; 189 | if (diffTime >= opts.refresh) { 190 | var diffData = e.loaded - this.startData; 191 | var speed = diffData / diffTime; // KB per second 192 | opts.speedUpdated(this.index, this.file, speed); 193 | this.startData = e.loaded; 194 | this.currentStart = elapsed; 195 | } 196 | } 197 | } 198 | } 199 | 200 | function globalProgress() { 201 | if (global_progress.length === 0) { 202 | return; 203 | } 204 | 205 | var total = 0, index; 206 | for (index in global_progress) { 207 | if(global_progress.hasOwnProperty(index)) { 208 | total = total + global_progress[index]; 209 | } 210 | } 211 | 212 | opts.globalProgressUpdated(Math.round(total / global_progress.length)); 213 | } 214 | 215 | // Respond to an upload 216 | function upload() { 217 | stop_loop = false; 218 | 219 | if (!files) { 220 | opts.error(errors[0]); 221 | return false; 222 | } 223 | 224 | if (opts.allowedfiletypes.push && opts.allowedfiletypes.length) { 225 | for(var fileIndex = files.length;fileIndex--;) { 226 | if(!files[fileIndex].type || $.inArray(files[fileIndex].type, opts.allowedfiletypes) < 0) { 227 | opts.error(errors[3], files[fileIndex]); 228 | return false; 229 | } 230 | } 231 | } 232 | 233 | if (opts.allowedfileextensions.push && opts.allowedfileextensions.length) { 234 | for(var fileIndex = files.length;fileIndex--;) { 235 | var allowedextension = false; 236 | for (i=0;i opts.maxfiles && opts.queuefiles === 0) { 254 | opts.error(errors[1]); 255 | return false; 256 | } 257 | 258 | // Define queues to manage upload process 259 | var workQueue = []; 260 | var processingQueue = []; 261 | var doneQueue = []; 262 | 263 | // Add everything to the workQueue 264 | for (var i = 0; i < files_count; i++) { 265 | workQueue.push(i); 266 | } 267 | 268 | // Helper function to enable pause of processing to wait 269 | // for in process queue to complete 270 | var pause = function(timeout) { 271 | setTimeout(process, timeout); 272 | return; 273 | }; 274 | 275 | // Process an upload, recursive 276 | var process = function() { 277 | 278 | var fileIndex; 279 | 280 | if (stop_loop) { 281 | return false; 282 | } 283 | 284 | // Check to see if are in queue mode 285 | if (opts.queuefiles > 0 && processingQueue.length >= opts.queuefiles) { 286 | return pause(opts.queuewait); 287 | } else { 288 | // Take first thing off work queue 289 | fileIndex = workQueue[0]; 290 | workQueue.splice(0, 1); 291 | 292 | // Add to processing queue 293 | processingQueue.push(fileIndex); 294 | } 295 | 296 | try { 297 | if (beforeEach(files[fileIndex]) !== false) { 298 | if (fileIndex === files_count) { 299 | return; 300 | } 301 | var reader = new FileReader(), 302 | max_file_size = 1048576 * opts.maxfilesize; 303 | 304 | reader.index = fileIndex; 305 | if (files[fileIndex].size > max_file_size) { 306 | opts.error(errors[2], files[fileIndex], fileIndex); 307 | // Remove from queue 308 | processingQueue.forEach(function(value, key) { 309 | if (value === fileIndex) { 310 | processingQueue.splice(key, 1); 311 | } 312 | }); 313 | filesRejected++; 314 | return true; 315 | } 316 | 317 | reader.onerror = function(e) { 318 | switch(e.target.error.code) { 319 | case e.target.error.NOT_FOUND_ERR: 320 | opts.error(errors[4]); 321 | return false; 322 | case e.target.error.NOT_READABLE_ERR: 323 | opts.error(errors[5]); 324 | return false; 325 | case e.target.error.ABORT_ERR: 326 | opts.error(errors[6]); 327 | return false; 328 | default: 329 | opts.error(errors[7]); 330 | return false; 331 | }; 332 | }; 333 | 334 | reader.onloadend = !opts.beforeSend ? send : function (e) { 335 | opts.beforeSend(files[fileIndex], fileIndex, function () { send(e); }); 336 | }; 337 | 338 | reader.readAsDataURL(files[fileIndex]); 339 | 340 | } else { 341 | filesRejected++; 342 | } 343 | } catch (err) { 344 | // Remove from queue 345 | processingQueue.forEach(function(value, key) { 346 | if (value === fileIndex) { 347 | processingQueue.splice(key, 1); 348 | } 349 | }); 350 | opts.error(errors[0]); 351 | return false; 352 | } 353 | 354 | // If we still have work to do, 355 | if (workQueue.length > 0) { 356 | process(); 357 | } 358 | }; 359 | 360 | var send = function(e) { 361 | 362 | var fileIndex = (e.srcElement || e.target).index; 363 | 364 | // Sometimes the index is not attached to the 365 | // event object. Find it by size. Hack for sure. 366 | if (e.target.index === undefined) { 367 | e.target.index = getIndexBySize(e.total); 368 | } 369 | 370 | var xhr = new XMLHttpRequest(), 371 | upload = xhr.upload, 372 | file = files[e.target.index], 373 | index = e.target.index, 374 | start_time = new Date().getTime(), 375 | boundary = '------multipartformboundary' + (new Date()).getTime(), 376 | global_progress_index = global_progress.length, 377 | builder, 378 | newName = rename(file.name), 379 | mime = file.type; 380 | 381 | if (opts.withCredentials) { 382 | xhr.withCredentials = opts.withCredentials; 383 | } 384 | 385 | var encodedString = e.target.result.split(',')[1]; 386 | var data = encodedString === undefined ? '' : atob(encodedString); 387 | if (typeof newName === "string") { 388 | builder = getBuilder(newName, data, mime, boundary); 389 | } else { 390 | builder = getBuilder(file.name, data, mime, boundary); 391 | } 392 | 393 | upload.index = index; 394 | upload.file = file; 395 | upload.downloadStartTime = start_time; 396 | upload.currentStart = start_time; 397 | upload.currentProgress = 0; 398 | upload.global_progress_index = global_progress_index; 399 | upload.startData = 0; 400 | upload.addEventListener("progress", progress, false); 401 | 402 | // Allow url to be a method 403 | if (jQuery.isFunction(opts.url)) { 404 | xhr.open(opts.requestType, opts.url(upload), true); 405 | } else { 406 | xhr.open(opts.requestType, opts.url, true); 407 | } 408 | 409 | xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' + boundary); 410 | xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 411 | 412 | // Add headers 413 | $.each(opts.headers, function(k, v) { 414 | xhr.setRequestHeader(k, v); 415 | }); 416 | 417 | if(!xhr.sendAsBinary){ 418 | xhr.sendAsBinary = function(datastr) { 419 | function byteValue(x) { 420 | return x.charCodeAt(0) & 0xff; 421 | } 422 | var ords = Array.prototype.map.call(datastr, byteValue); 423 | var ui8a = new Uint8Array(ords); 424 | this.send(ui8a.buffer); 425 | } 426 | } 427 | 428 | xhr.sendAsBinary(builder); 429 | 430 | global_progress[global_progress_index] = 0; 431 | globalProgress(); 432 | 433 | opts.uploadStarted(index, file, files_count); 434 | 435 | xhr.onload = function() { 436 | var serverResponse = null; 437 | 438 | if (xhr.responseText) { 439 | try { 440 | serverResponse = jQuery.parseJSON(xhr.responseText); 441 | } 442 | catch (e) { 443 | serverResponse = xhr.responseText; 444 | } 445 | } 446 | 447 | var now = new Date().getTime(), 448 | timeDiff = now - start_time, 449 | result = opts.uploadFinished(index, file, serverResponse, timeDiff, xhr); 450 | filesDone++; 451 | 452 | // Remove from processing queue 453 | processingQueue.forEach(function(value, key) { 454 | if (value === fileIndex) { 455 | processingQueue.splice(key, 1); 456 | } 457 | }); 458 | 459 | // Add to donequeue 460 | doneQueue.push(fileIndex); 461 | 462 | // Make sure the global progress is updated 463 | global_progress[global_progress_index] = 100; 464 | globalProgress(); 465 | 466 | if (filesDone === (files_count - filesRejected)) { 467 | afterAll(); 468 | } 469 | if (result === false) { 470 | stop_loop = true; 471 | } 472 | 473 | 474 | // Pass any errors to the error option 475 | if (xhr.status < 200 || xhr.status > 299) { 476 | opts.error(xhr.statusText, file, fileIndex, xhr.status); 477 | } 478 | }; 479 | }; 480 | 481 | // Initiate the processing loop 482 | process(); 483 | } 484 | 485 | function getIndexBySize(size) { 486 | for (var i = 0; i < files_count; i++) { 487 | if (files[i].size === size) { 488 | return i; 489 | } 490 | } 491 | 492 | return undefined; 493 | } 494 | 495 | function rename(name) { 496 | return opts.rename(name); 497 | } 498 | 499 | function beforeEach(file) { 500 | return opts.beforeEach(file); 501 | } 502 | 503 | function afterAll() { 504 | return opts.afterAll(); 505 | } 506 | 507 | function dragEnter(e) { 508 | clearTimeout(doc_leave_timer); 509 | e.preventDefault(); 510 | opts.dragEnter.call(this, e); 511 | } 512 | 513 | function dragOver(e) { 514 | clearTimeout(doc_leave_timer); 515 | e.preventDefault(); 516 | opts.docOver.call(this, e); 517 | opts.dragOver.call(this, e); 518 | } 519 | 520 | function dragLeave(e) { 521 | clearTimeout(doc_leave_timer); 522 | opts.dragLeave.call(this, e); 523 | e.stopPropagation(); 524 | } 525 | 526 | function docDrop(e) { 527 | e.preventDefault(); 528 | opts.docLeave.call(this, e); 529 | return false; 530 | } 531 | 532 | function docEnter(e) { 533 | clearTimeout(doc_leave_timer); 534 | e.preventDefault(); 535 | opts.docEnter.call(this, e); 536 | return false; 537 | } 538 | 539 | function docOver(e) { 540 | clearTimeout(doc_leave_timer); 541 | e.preventDefault(); 542 | opts.docOver.call(this, e); 543 | return false; 544 | } 545 | 546 | function docLeave(e) { 547 | doc_leave_timer = setTimeout((function(_this) { 548 | return function() { 549 | opts.docLeave.call(_this, e); 550 | }; 551 | })(this), 200); 552 | } 553 | 554 | return this; 555 | }; 556 | 557 | function empty() {} 558 | 559 | try { 560 | if (XMLHttpRequest.prototype.sendAsBinary) { 561 | return; 562 | } 563 | XMLHttpRequest.prototype.sendAsBinary = function(datastr) { 564 | function byteValue(x) { 565 | return x.charCodeAt(0) & 0xff; 566 | } 567 | var ords = Array.prototype.map.call(datastr, byteValue); 568 | var ui8a = new Uint8Array(ords); 569 | 570 | // Not pretty: Chrome 22 deprecated sending ArrayBuffer, moving instead 571 | // to sending ArrayBufferView. Sadly, no proper way to detect this 572 | // functionality has been discovered. Happily, Chrome 22 also introduced 573 | // the base ArrayBufferView class, not present in Chrome 21. 574 | if ('ArrayBufferView' in window) 575 | this.send(ui8a); 576 | else 577 | this.send(ui8a.buffer); 578 | }; 579 | } catch (e) {} 580 | 581 | })(jQuery); 582 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "jquery-filedrop", 3 | "version" : "0.1.0", 4 | "main" : "jquery.filedrop.js" 5 | } 6 | --------------------------------------------------------------------------------