2 * jQuery File Upload Plugin 5.19.8
3 * https://github.com/blueimp/jQuery-File-Upload
5 * Copyright 2010, Sebastian Tschan
8 * Licensed under the MIT license:
9 * http://www.opensource.org/licenses/MIT
12 /*jslint nomen: true, unparam: true, regexp: true */
13 /*global define, window, document, File, Blob, FormData, location */
17 if (typeof define
=== 'function' && define
.amd
) {
18 // Register as an anonymous AMD module:
25 factory(window
.jQuery
);
30 // The FileReader API is not actually used, but works as feature detection,
31 // as e.g. Safari supports XHR file uploads via the FormData API,
32 // but not non-multipart XHR file uploads:
33 $.support
.xhrFileUpload
= !!(window
.XMLHttpRequestUpload
&& window
.FileReader
);
34 $.support
.xhrFormDataFileUpload
= !!window
.FormData
;
36 // The fileupload widget listens for change events on file input fields defined
37 // via fileInput setting and paste or drop events of the given dropZone.
38 // In addition to the default jQuery Widget methods, the fileupload widget
39 // exposes the "add" and "send" methods, to add or directly send files using
40 // the fileupload API.
41 // By default, files added via file input selection, paste, drag & drop or
42 // "add" method are uploaded immediately, but it is possible to override
43 // the "add" callback option to queue file uploads.
44 $.widget('blueimp.fileupload', {
47 // The drop target element(s), by the default the complete document.
48 // Set to null to disable drag & drop support:
49 dropZone
: $(document
),
50 // The paste target element(s), by the default the complete document.
51 // Set to null to disable paste support:
52 pasteZone
: $(document
),
53 // The file input field(s), that are listened to for change events.
54 // If undefined, it is set to the file input fields inside
55 // of the widget element on plugin initialization.
56 // Set to null to disable the change listener.
58 // By default, the file input field is replaced with a clone after
59 // each input field change event. This is required for iframe transport
60 // queues and allows change events to be fired for the same file
61 // selection, but can be disabled by setting the following option to false:
62 replaceFileInput
: true,
63 // The parameter name for the file form data (the request argument name).
64 // If undefined or empty, the name property of the file input field is
65 // used, or "files[]" if the file input name property is also empty,
66 // can be a string or an array of strings:
68 // By default, each file of a selection is uploaded using an individual
69 // request for XHR type uploads. Set to false to upload file
70 // selections in one request each:
71 singleFileUploads
: true,
72 // To limit the number of files uploaded with one XHR request,
73 // set the following option to an integer greater than 0:
74 limitMultiFileUploads
: undefined,
75 // Set the following option to true to issue all file upload requests
76 // in a sequential order:
77 sequentialUploads
: false,
78 // To limit the number of concurrent uploads,
79 // set the following option to an integer greater than 0:
80 limitConcurrentUploads
: undefined,
81 // Set the following option to true to force iframe transport uploads:
82 forceIframeTransport
: false,
83 // Set the following option to the location of a redirect url on the
84 // origin server, for cross-domain iframe transport uploads:
86 // The parameter name for the redirect url, sent as part of the form
87 // data and set to 'redirect' if this option is empty:
88 redirectParamName
: undefined,
89 // Set the following option to the location of a postMessage window,
90 // to enable postMessage transport uploads:
91 postMessage
: undefined,
92 // By default, XHR file uploads are sent as multipart/form-data.
93 // The iframe transport is always using multipart/form-data.
94 // Set to false to enable non-multipart XHR uploads:
96 // To upload large files in smaller chunks, set the following option
97 // to a preferred maximum chunk size. If set to 0, null or undefined,
98 // or the browser does not support the required Blob API, files will
99 // be uploaded as a whole.
100 maxChunkSize
: undefined,
101 // When a non-multipart upload or a chunked multipart upload has been
102 // aborted, this option can be used to resume the upload by setting
103 // it to the size of the already uploaded bytes. This option is most
104 // useful when modifying the options object inside of the "add" or
105 // "send" callbacks, as the options are cloned for each file upload.
106 uploadedBytes
: undefined,
107 // By default, failed (abort or error) file uploads are removed from the
108 // global progress calculation. Set the following option to false to
109 // prevent recalculating the global progress data:
110 recalculateProgress
: true,
111 // Interval in milliseconds to calculate and trigger progress events:
112 progressInterval
: 100,
113 // Interval in milliseconds to calculate progress bitrate:
114 bitrateInterval
: 500,
116 // Additional form data to be sent along with the file uploads can be set
117 // using this option, which accepts an array of objects with name and
118 // value properties, a function returning such an array, a FormData
119 // object (for XHR file uploads), or a simple object.
120 // The form of the first fileInput is given as parameter to the function:
121 formData: function (form
) {
122 return form
.serializeArray();
125 // The add callback is invoked as soon as files are added to the fileupload
126 // widget (via file input selection, drag & drop, paste or add API call).
127 // If the singleFileUploads option is enabled, this callback will be
128 // called once for each file in the selection for XHR file uplaods, else
129 // once for each file selection.
130 // The upload starts when the submit method is invoked on the data parameter.
131 // The data object contains a files property holding the added files
132 // and allows to override plugin options as well as define ajax settings.
133 // Listeners for this callback can also be bound the following way:
134 // .bind('fileuploadadd', func);
135 // data.submit() returns a Promise object and allows to attach additional
136 // handlers using jQuery's Deferred callbacks:
137 // data.submit().done(func).fail(func).always(func);
138 add: function (e
, data
) {
143 // Callback for the submit event of each file upload:
144 // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
145 // Callback for the start of each file upload request:
146 // send: function (e, data) {}, // .bind('fileuploadsend', func);
147 // Callback for successful uploads:
148 // done: function (e, data) {}, // .bind('fileuploaddone', func);
149 // Callback for failed (abort or error) uploads:
150 // fail: function (e, data) {}, // .bind('fileuploadfail', func);
151 // Callback for completed (success, abort or error) requests:
152 // always: function (e, data) {}, // .bind('fileuploadalways', func);
153 // Callback for upload progress events:
154 // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
155 // Callback for global upload progress events:
156 // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
157 // Callback for uploads start, equivalent to the global ajaxStart event:
158 // start: function (e) {}, // .bind('fileuploadstart', func);
159 // Callback for uploads stop, equivalent to the global ajaxStop event:
160 // stop: function (e) {}, // .bind('fileuploadstop', func);
161 // Callback for change events of the fileInput(s):
162 // change: function (e, data) {}, // .bind('fileuploadchange', func);
163 // Callback for paste events to the pasteZone(s):
164 // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
165 // Callback for drop events of the dropZone(s):
166 // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
167 // Callback for dragover events of the dropZone(s):
168 // dragover: function (e) {}, // .bind('fileuploaddragover', func);
170 // The plugin options are used as settings object for the ajax calls.
171 // The following are jQuery ajax settings required for the file uploads:
177 // A list of options that require a refresh after assigning a new value:
178 _refreshOptionsList
: [
183 'forceIframeTransport'
186 _BitrateTimer: function () {
187 this.timestamp
= +(new Date());
190 this.getBitrate = function (now
, loaded
, interval
) {
191 var timeDiff
= now
- this.timestamp
;
192 if (!this.bitrate
|| !interval
|| timeDiff
> interval
) {
193 this.bitrate
= (loaded
- this.loaded
) * (1000 / timeDiff
) * 8;
194 this.loaded
= loaded
;
195 this.timestamp
= now
;
201 _isXHRUpload: function (options
) {
202 return !options
.forceIframeTransport
&&
203 ((!options
.multipart
&& $.support
.xhrFileUpload
) ||
204 $.support
.xhrFormDataFileUpload
);
207 _getFormData: function (options
) {
209 if (typeof options
.formData
=== 'function') {
210 return options
.formData(options
.form
);
212 if ($.isArray(options
.formData
)) {
213 return options
.formData
;
215 if (options
.formData
) {
217 $.each(options
.formData
, function (name
, value
) {
218 formData
.push({name
: name
, value
: value
});
225 _getTotal: function (files
) {
227 $.each(files
, function (index
, file
) {
228 total
+= file
.size
|| 1;
233 _onProgress: function (e
, data
) {
234 if (e
.lengthComputable
) {
235 var now
= +(new Date()),
238 if (data
._time
&& data
.progressInterval
&&
239 (now
- data
._time
< data
.progressInterval
) &&
240 e
.loaded
!== e
.total
) {
244 total
= data
.total
|| this._getTotal(data
.files
);
246 e
.loaded
/ e
.total
* (data
.chunkSize
|| total
),
248 ) + (data
.uploadedBytes
|| 0);
249 this._loaded
+= loaded
- (data
.loaded
|| data
.uploadedBytes
|| 0);
250 data
.lengthComputable
= true;
251 data
.loaded
= loaded
;
253 data
.bitrate
= data
._bitrateTimer
.getBitrate(
258 // Trigger a custom progress event with a total data property set
259 // to the file size(s) of the current upload and a loaded data
260 // property calculated accordingly:
261 this._trigger('progress', e
, data
);
262 // Trigger a global progress event for all current file uploads,
263 // including ajax calls queued for sequential file uploads:
264 this._trigger('progressall', e
, {
265 lengthComputable
: true,
266 loaded
: this._loaded
,
268 bitrate
: this._bitrateTimer
.getBitrate(
277 _initProgressListener: function (options
) {
279 xhr
= options
.xhr
? options
.xhr() : $.ajaxSettings
.xhr();
280 // Accesss to the native XHR object is required to add event listeners
281 // for the upload progress event:
283 $(xhr
.upload
).bind('progress', function (e
) {
284 var oe
= e
.originalEvent
;
285 // Make sure the progress event properties get copied over:
286 e
.lengthComputable
= oe
.lengthComputable
;
287 e
.loaded
= oe
.loaded
;
289 that
._onProgress(e
, options
);
291 options
.xhr = function () {
297 _initXHRData: function (options
) {
299 file
= options
.files
[0],
300 // Ignore non-multipart setting if not supported:
301 multipart
= options
.multipart
|| !$.support
.xhrFileUpload
,
302 paramName
= options
.paramName
[0];
303 options
.headers
= options
.headers
|| {};
304 if (options
.contentRange
) {
305 options
.headers
['Content-Range'] = options
.contentRange
;
308 options
.headers
['Content-Disposition'] = 'attachment; filename="' +
309 encodeURI(file
.name
) + '"';
310 options
.contentType
= file
.type
;
311 options
.data
= options
.blob
|| file
;
312 } else if ($.support
.xhrFormDataFileUpload
) {
313 if (options
.postMessage
) {
314 // window.postMessage does not allow sending FormData
315 // objects, so we just add the File/Blob objects to
316 // the formData array and let the postMessage window
317 // create the FormData object out of this array:
318 formData
= this._getFormData(options
);
325 $.each(options
.files
, function (index
, file
) {
327 name
: options
.paramName
[index
] || paramName
,
333 if (options
.formData
instanceof FormData
) {
334 formData
= options
.formData
;
336 formData
= new FormData();
337 $.each(this._getFormData(options
), function (index
, field
) {
338 formData
.append(field
.name
, field
.value
);
342 options
.headers
['Content-Disposition'] = 'attachment; filename="' +
343 encodeURI(file
.name
) + '"';
344 formData
.append(paramName
, options
.blob
, file
.name
);
346 $.each(options
.files
, function (index
, file
) {
347 // Files are also Blob instances, but some browsers
348 // (Firefox 3.6) support the File API but not Blobs.
349 // This check allows the tests to run with
351 if ((window
.Blob
&& file
instanceof Blob
) ||
352 (window
.File
&& file
instanceof File
)) {
354 options
.paramName
[index
] || paramName
,
362 options
.data
= formData
;
364 // Blob reference is not needed anymore, free memory:
368 _initIframeSettings: function (options
) {
369 // Setting the dataType to iframe enables the iframe transport:
370 options
.dataType
= 'iframe ' + (options
.dataType
|| '');
371 // The iframe transport accepts a serialized array as form data:
372 options
.formData
= this._getFormData(options
);
373 // Add redirect url to form data on cross-domain uploads:
374 if (options
.redirect
&& $('<a></a>').prop('href', options
.url
)
375 .prop('host') !== location
.host
) {
376 options
.formData
.push({
377 name
: options
.redirectParamName
|| 'redirect',
378 value
: options
.redirect
383 _initDataSettings: function (options
) {
384 if (this._isXHRUpload(options
)) {
385 if (!this._chunkedUpload(options
, true)) {
387 this._initXHRData(options
);
389 this._initProgressListener(options
);
391 if (options
.postMessage
) {
392 // Setting the dataType to postmessage enables the
393 // postMessage transport:
394 options
.dataType
= 'postmessage ' + (options
.dataType
|| '');
397 this._initIframeSettings(options
, 'iframe');
401 _getParamName: function (options
) {
402 var fileInput
= $(options
.fileInput
),
403 paramName
= options
.paramName
;
406 fileInput
.each(function () {
408 name
= input
.prop('name') || 'files[]',
409 i
= (input
.prop('files') || [1]).length
;
411 paramName
.push(name
);
415 if (!paramName
.length
) {
416 paramName
= [fileInput
.prop('name') || 'files[]'];
418 } else if (!$.isArray(paramName
)) {
419 paramName
= [paramName
];
424 _initFormSettings: function (options
) {
425 // Retrieve missing options from the input field and the
426 // associated form, if available:
427 if (!options
.form
|| !options
.form
.length
) {
428 options
.form
= $(options
.fileInput
.prop('form'));
429 // If the given file input doesn't have an associated form,
430 // use the default widget file input's form:
431 if (!options
.form
.length
) {
432 options
.form
= $(this.options
.fileInput
.prop('form'));
435 options
.paramName
= this._getParamName(options
);
437 options
.url
= options
.form
.prop('action') || location
.href
;
439 // The HTTP request method must be "POST" or "PUT":
440 options
.type
= (options
.type
|| options
.form
.prop('method') || '')
442 if (options
.type
!== 'POST' && options
.type
!== 'PUT' &&
443 options
.type
!== 'PATCH') {
444 options
.type
= 'POST';
446 if (!options
.formAcceptCharset
) {
447 options
.formAcceptCharset
= options
.form
.attr('accept-charset');
451 _getAJAXSettings: function (data
) {
452 var options
= $.extend({}, this.options
, data
);
453 this._initFormSettings(options
);
454 this._initDataSettings(options
);
458 // Maps jqXHR callbacks to the equivalent
459 // methods of the given Promise object:
460 _enhancePromise: function (promise
) {
461 promise
.success
= promise
.done
;
462 promise
.error
= promise
.fail
;
463 promise
.complete
= promise
.always
;
467 // Creates and returns a Promise object enhanced with
468 // the jqXHR methods abort, success, error and complete:
469 _getXHRPromise: function (resolveOrReject
, context
, args
) {
470 var dfd
= $.Deferred(),
471 promise
= dfd
.promise();
472 context
= context
|| this.options
.context
|| promise
;
473 if (resolveOrReject
=== true) {
474 dfd
.resolveWith(context
, args
);
475 } else if (resolveOrReject
=== false) {
476 dfd
.rejectWith(context
, args
);
478 promise
.abort
= dfd
.promise
;
479 return this._enhancePromise(promise
);
482 // Parses the Range header from the server response
483 // and returns the uploaded bytes:
484 _getUploadedBytes: function (jqXHR
) {
485 var range
= jqXHR
.getResponseHeader('Range'),
486 parts
= range
&& range
.split('-'),
487 upperBytesPos
= parts
&& parts
.length
> 1 &&
488 parseInt(parts
[1], 10);
489 return upperBytesPos
&& upperBytesPos
+ 1;
492 // Uploads a file in multiple, sequential requests
493 // by splitting the file up in multiple blob chunks.
494 // If the second parameter is true, only tests if the file
495 // should be uploaded in chunks, but does not invoke any
497 _chunkedUpload: function (options
, testOnly
) {
499 file
= options
.files
[0],
501 ub
= options
.uploadedBytes
= options
.uploadedBytes
|| 0,
502 mcs
= options
.maxChunkSize
|| fs
,
503 slice
= file
.slice
|| file
.webkitSlice
|| file
.mozSlice
,
505 promise
= dfd
.promise(),
508 if (!(this._isXHRUpload(options
) && slice
&& (ub
|| mcs
< fs
)) ||
516 file
.error
= 'Uploaded bytes exceed file size';
517 return this._getXHRPromise(
520 [null, 'error', file
.error
]
523 // The chunk upload method:
524 upload = function (i
) {
525 // Clone the options object for each chunk upload:
526 var o
= $.extend({}, options
);
533 // Store the current chunk size, as the blob itself
534 // will be dereferenced after data processing:
535 o
.chunkSize
= o
.blob
.size
;
536 // Expose the chunk bytes position range:
537 o
.contentRange
= 'bytes ' + ub
+ '-' +
538 (ub
+ o
.chunkSize
- 1) + '/' + fs
;
539 // Process the upload data (the blob and potential form data):
540 that
._initXHRData(o
);
541 // Add progress listeners for this chunk upload:
542 that
._initProgressListener(o
);
543 jqXHR
= ($.ajax(o
) || that
._getXHRPromise(false, o
.context
))
544 .done(function (result
, textStatus
, jqXHR
) {
545 ub
= that
._getUploadedBytes(jqXHR
) ||
547 // Create a progress event if upload is done and
548 // no progress event has been invoked for this chunk:
550 that
._onProgress($.Event('progress', {
551 lengthComputable
: true,
552 loaded
: ub
- o
.uploadedBytes
,
553 total
: ub
- o
.uploadedBytes
556 options
.uploadedBytes
= o
.uploadedBytes
= ub
;
558 // File upload not yet complete,
559 // continue with the next chunk:
564 [result
, textStatus
, jqXHR
]
568 .fail(function (jqXHR
, textStatus
, errorThrown
) {
571 [jqXHR
, textStatus
, errorThrown
]
575 this._enhancePromise(promise
);
576 promise
.abort = function () {
577 return jqXHR
.abort();
583 _beforeSend: function (e
, data
) {
584 if (this._active
=== 0) {
585 // the start callback is triggered when an upload starts
586 // and no other uploads are currently running,
587 // equivalent to the global ajaxStart event:
588 this._trigger('start');
589 // Set timer for global bitrate progress calculation:
590 this._bitrateTimer
= new this._BitrateTimer();
593 // Initialize the global progress values:
594 this._loaded
+= data
.uploadedBytes
|| 0;
595 this._total
+= this._getTotal(data
.files
);
598 _onDone: function (result
, textStatus
, jqXHR
, options
) {
599 if (!this._isXHRUpload(options
)) {
600 // Create a progress event for each iframe load:
601 this._onProgress($.Event('progress', {
602 lengthComputable
: true,
607 options
.result
= result
;
608 options
.textStatus
= textStatus
;
609 options
.jqXHR
= jqXHR
;
610 this._trigger('done', null, options
);
613 _onFail: function (jqXHR
, textStatus
, errorThrown
, options
) {
614 options
.jqXHR
= jqXHR
;
615 options
.textStatus
= textStatus
;
616 options
.errorThrown
= errorThrown
;
617 this._trigger('fail', null, options
);
618 if (options
.recalculateProgress
) {
619 // Remove the failed (error or abort) file upload from
620 // the global progress calculation:
621 this._loaded
-= options
.loaded
|| options
.uploadedBytes
|| 0;
622 this._total
-= options
.total
|| this._getTotal(options
.files
);
626 _onAlways: function (jqXHRorResult
, textStatus
, jqXHRorError
, options
) {
628 options
.textStatus
= textStatus
;
629 if (jqXHRorError
&& jqXHRorError
.always
) {
630 options
.jqXHR
= jqXHRorError
;
631 options
.result
= jqXHRorResult
;
633 options
.jqXHR
= jqXHRorResult
;
634 options
.errorThrown
= jqXHRorError
;
636 this._trigger('always', null, options
);
637 if (this._active
=== 0) {
638 // The stop callback is triggered when all uploads have
639 // been completed, equivalent to the global ajaxStop event:
640 this._trigger('stop');
641 // Reset the global progress values:
642 this._loaded
= this._total
= 0;
643 this._bitrateTimer
= null;
647 _onSend: function (e
, data
) {
653 options
= that
._getAJAXSettings(data
),
656 // Set timer for bitrate progress calculation:
657 options
._bitrateTimer
= new that
._BitrateTimer();
659 ((aborted
|| that
._trigger('send', e
, options
) === false) &&
660 that
._getXHRPromise(false, options
.context
, aborted
)) ||
661 that
._chunkedUpload(options
) || $.ajax(options
)
662 ).done(function (result
, textStatus
, jqXHR
) {
663 that
._onDone(result
, textStatus
, jqXHR
, options
);
664 }).fail(function (jqXHR
, textStatus
, errorThrown
) {
665 that
._onFail(jqXHR
, textStatus
, errorThrown
, options
);
666 }).always(function (jqXHRorResult
, textStatus
, jqXHRorError
) {
674 if (options
.limitConcurrentUploads
&&
675 options
.limitConcurrentUploads
> that
._sending
) {
676 // Start the next queued upload,
677 // that has not been aborted:
678 var nextSlot
= that
._slots
.shift(),
681 // jQuery 1.6 doesn't provide .state(),
682 // while jQuery 1.8+ removed .isRejected():
683 isPending
= nextSlot
.state
?
684 nextSlot
.state() === 'pending' :
685 !nextSlot
.isRejected();
690 nextSlot
= that
._slots
.shift();
696 this._beforeSend(e
, options
);
697 if (this.options
.sequentialUploads
||
698 (this.options
.limitConcurrentUploads
&&
699 this.options
.limitConcurrentUploads
<= this._sending
)) {
700 if (this.options
.limitConcurrentUploads
> 1) {
702 this._slots
.push(slot
);
703 pipe
= slot
.pipe(send
);
705 pipe
= (this._sequence
= this._sequence
.pipe(send
, send
));
707 // Return the piped Promise object, enhanced with an abort method,
708 // which is delegated to the jqXHR object of the current upload,
709 // and jqXHR callbacks mapped to the equivalent Promise methods:
710 pipe
.abort = function () {
711 aborted
= [undefined, 'abort', 'abort'];
714 slot
.rejectWith(options
.context
, aborted
);
718 return jqXHR
.abort();
720 return this._enhancePromise(pipe
);
725 _onAdd: function (e
, data
) {
728 options
= $.extend({}, this.options
, data
),
729 limit
= options
.limitMultiFileUploads
,
730 paramName
= this._getParamName(options
),
735 if (!(options
.singleFileUploads
|| limit
) ||
736 !this._isXHRUpload(options
)) {
737 fileSet
= [data
.files
];
738 paramNameSet
= [paramName
];
739 } else if (!options
.singleFileUploads
&& limit
) {
742 for (i
= 0; i
< data
.files
.length
; i
+= limit
) {
743 fileSet
.push(data
.files
.slice(i
, i
+ limit
));
744 paramNameSlice
= paramName
.slice(i
, i
+ limit
);
745 if (!paramNameSlice
.length
) {
746 paramNameSlice
= paramName
;
748 paramNameSet
.push(paramNameSlice
);
751 paramNameSet
= paramName
;
753 data
.originalFiles
= data
.files
;
754 $.each(fileSet
|| data
.files
, function (index
, element
) {
755 var newData
= $.extend({}, data
);
756 newData
.files
= fileSet
? element
: [element
];
757 newData
.paramName
= paramNameSet
[index
];
758 newData
.submit = function () {
759 newData
.jqXHR
= this.jqXHR
=
760 (that
._trigger('submit', e
, this) !== false) &&
761 that
._onSend(e
, this);
764 result
= that
._trigger('add', e
, newData
);
770 _replaceFileInput: function (input
) {
771 var inputClone
= input
.clone(true);
772 $('<form></form>').append(inputClone
)[0].reset();
773 // Detaching allows to insert the fileInput on another form
774 // without loosing the file input value:
775 input
.after(inputClone
).detach();
776 // Avoid memory leaks with the detached file input:
777 $.cleanData(input
.unbind('remove'));
778 // Replace the original file input element in the fileInput
779 // elements set with the clone, which has been copied including
781 this.options
.fileInput
= this.options
.fileInput
.map(function (i
, el
) {
782 if (el
=== input
[0]) {
783 return inputClone
[0];
787 // If the widget has been initialized on the file input itself,
788 // override this.element with the file input clone:
789 if (input
[0] === this.element
[0]) {
790 this.element
= inputClone
;
794 _handleFileTreeEntry: function (entry
, path
) {
797 errorHandler = function (e
) {
801 // Since $.when returns immediately if one
802 // Deferred is rejected, we use resolve instead.
803 // This allows valid files and invalid items
804 // to be returned together in one set:
811 // Workaround for Chrome bug #149735
812 entry
._file
.relativePath
= path
;
813 dfd
.resolve(entry
._file
);
815 entry
.file(function (file
) {
816 file
.relativePath
= path
;
820 } else if (entry
.isDirectory
) {
821 dirReader
= entry
.createReader();
822 dirReader
.readEntries(function (entries
) {
823 that
._handleFileTreeEntries(
825 path
+ entry
.name
+ '/'
826 ).done(function (files
) {
828 }).fail(errorHandler
);
831 // Return an empy list for file system items
832 // other than files or directories:
835 return dfd
.promise();
838 _handleFileTreeEntries: function (entries
, path
) {
842 $.map(entries
, function (entry
) {
843 return that
._handleFileTreeEntry(entry
, path
);
846 return Array
.prototype.concat
.apply(
853 _getDroppedFiles: function (dataTransfer
) {
854 dataTransfer
= dataTransfer
|| {};
855 var items
= dataTransfer
.items
;
856 if (items
&& items
.length
&& (items
[0].webkitGetAsEntry
||
857 items
[0].getAsEntry
)) {
858 return this._handleFileTreeEntries(
859 $.map(items
, function (item
) {
861 if (item
.webkitGetAsEntry
) {
862 entry
= item
.webkitGetAsEntry();
864 // Workaround for Chrome bug #149735:
865 entry
._file
= item
.getAsFile();
869 return item
.getAsEntry();
873 return $.Deferred().resolve(
874 $.makeArray(dataTransfer
.files
)
878 _getSingleFileInputFiles: function (fileInput
) {
879 fileInput
= $(fileInput
);
880 var entries
= fileInput
.prop('webkitEntries') ||
881 fileInput
.prop('entries'),
884 if (entries
&& entries
.length
) {
885 return this._handleFileTreeEntries(entries
);
887 files
= $.makeArray(fileInput
.prop('files'));
889 value
= fileInput
.prop('value');
891 return $.Deferred().resolve([]).promise();
893 // If the files property is not available, the browser does not
894 // support the File API and we add a pseudo File object with
895 // the input value as name with path information removed:
896 files
= [{name
: value
.replace(/^.*\\/, '')}];
897 } else if (files
[0].name
=== undefined && files
[0].fileName
) {
898 // File normalization for Safari 4 and Firefox 3:
899 $.each(files
, function (index
, file
) {
900 file
.name
= file
.fileName
;
901 file
.size
= file
.fileSize
;
904 return $.Deferred().resolve(files
).promise();
907 _getFileInputFiles: function (fileInput
) {
908 if (!(fileInput
instanceof $) || fileInput
.length
=== 1) {
909 return this._getSingleFileInputFiles(fileInput
);
913 $.map(fileInput
, this._getSingleFileInputFiles
)
915 return Array
.prototype.concat
.apply(
922 _onChange: function (e
) {
925 fileInput
: $(e
.target
),
926 form
: $(e
.target
.form
)
928 this._getFileInputFiles(data
.fileInput
).always(function (files
) {
930 if (that
.options
.replaceFileInput
) {
931 that
._replaceFileInput(data
.fileInput
);
933 if (that
._trigger('change', e
, data
) !== false) {
934 that
._onAdd(e
, data
);
939 _onPaste: function (e
) {
940 var cbd
= e
.originalEvent
.clipboardData
,
941 items
= (cbd
&& cbd
.items
) || [],
943 $.each(items
, function (index
, item
) {
944 var file
= item
.getAsFile
&& item
.getAsFile();
946 data
.files
.push(file
);
949 if (this._trigger('paste', e
, data
) === false ||
950 this._onAdd(e
, data
) === false) {
955 _onDrop: function (e
) {
957 dataTransfer
= e
.dataTransfer
= e
.originalEvent
.dataTransfer
,
959 if (dataTransfer
&& dataTransfer
.files
&& dataTransfer
.files
.length
) {
962 this._getDroppedFiles(dataTransfer
).always(function (files
) {
964 if (that
._trigger('drop', e
, data
) !== false) {
965 that
._onAdd(e
, data
);
970 _onDragOver: function (e
) {
971 var dataTransfer
= e
.dataTransfer
= e
.originalEvent
.dataTransfer
;
972 if (this._trigger('dragover', e
) === false) {
975 if (dataTransfer
&& $.inArray('Files', dataTransfer
.types
) !== -1) {
976 dataTransfer
.dropEffect
= 'copy';
981 _initEventHandlers: function () {
982 if (this._isXHRUpload(this.options
)) {
983 this._on(this.options
.dropZone
, {
984 dragover
: this._onDragOver
,
987 this._on(this.options
.pasteZone
, {
991 this._on(this.options
.fileInput
, {
992 change
: this._onChange
996 _destroyEventHandlers: function () {
997 this._off(this.options
.dropZone
, 'dragover drop');
998 this._off(this.options
.pasteZone
, 'paste');
999 this._off(this.options
.fileInput
, 'change');
1002 _setOption: function (key
, value
) {
1003 var refresh
= $.inArray(key
, this._refreshOptionsList
) !== -1;
1005 this._destroyEventHandlers();
1007 this._super(key
, value
);
1009 this._initSpecialOptions();
1010 this._initEventHandlers();
1014 _initSpecialOptions: function () {
1015 var options
= this.options
;
1016 if (options
.fileInput
=== undefined) {
1017 options
.fileInput
= this.element
.is('input[type="file"]') ?
1018 this.element
: this.element
.find('input[type="file"]');
1019 } else if (!(options
.fileInput
instanceof $)) {
1020 options
.fileInput
= $(options
.fileInput
);
1022 if (!(options
.dropZone
instanceof $)) {
1023 options
.dropZone
= $(options
.dropZone
);
1025 if (!(options
.pasteZone
instanceof $)) {
1026 options
.pasteZone
= $(options
.pasteZone
);
1030 _create: function () {
1031 var options
= this.options
;
1032 // Initialize options set via HTML5 data-attributes:
1033 $.extend(options
, $(this.element
[0].cloneNode(false)).data());
1034 this._initSpecialOptions();
1036 this._sequence
= this._getXHRPromise(true);
1037 this._sending
= this._active
= this._loaded
= this._total
= 0;
1038 this._initEventHandlers();
1041 _destroy: function () {
1042 this._destroyEventHandlers();
1045 // This method is exposed to the widget API and allows adding files
1046 // using the fileupload API. The data parameter accepts an object which
1047 // must have a files property and can contain additional options:
1048 // .fileupload('add', {files: filesList});
1049 add: function (data
) {
1051 if (!data
|| this.options
.disabled
) {
1054 if (data
.fileInput
&& !data
.files
) {
1055 this._getFileInputFiles(data
.fileInput
).always(function (files
) {
1057 that
._onAdd(null, data
);
1060 data
.files
= $.makeArray(data
.files
);
1061 this._onAdd(null, data
);
1065 // This method is exposed to the widget API and allows sending files
1066 // using the fileupload API. The data parameter accepts an object which
1067 // must have a files or fileInput property and can contain additional options:
1068 // .fileupload('send', {files: filesList});
1069 // The method returns a Promise object for the file upload call.
1070 send: function (data
) {
1071 if (data
&& !this.options
.disabled
) {
1072 if (data
.fileInput
&& !data
.files
) {
1075 promise
= dfd
.promise(),
1078 promise
.abort = function () {
1081 return jqXHR
.abort();
1083 dfd
.reject(null, 'abort', 'abort');
1086 this._getFileInputFiles(data
.fileInput
).always(
1092 jqXHR
= that
._onSend(null, data
).then(
1093 function (result
, textStatus
, jqXHR
) {
1094 dfd
.resolve(result
, textStatus
, jqXHR
);
1096 function (jqXHR
, textStatus
, errorThrown
) {
1097 dfd
.reject(jqXHR
, textStatus
, errorThrown
);
1102 return this._enhancePromise(promise
);
1104 data
.files
= $.makeArray(data
.files
);
1105 if (data
.files
.length
) {
1106 return this._onSend(null, data
);
1109 return this._getXHRPromise(false, data
&& data
.context
);