2 * jQuery File Upload Plugin
3 * https://github.com/blueimp/jQuery-File-Upload
5 * Copyright 2010, Sebastian Tschan
8 * Licensed under the MIT license:
9 * https://opensource.org/licenses/MIT
12 /* global define, require */
13 /* eslint-disable new-cap */
17 if (typeof define === 'function' && define.amd) {
18 // Register as an anonymous AMD module:
19 define(['jquery', 'jquery-ui/ui/widget'], factory);
20 } else if (typeof exports === 'object') {
22 factory(require('jquery'), require('./vendor/jquery.ui.widget'));
25 factory(window.jQuery);
30 // Detect file input support, based on
31 // https://viljamis.com/2012/file-upload-support-on-mobile/
32 $.support.fileInput = !(
34 // Handle devices which give false positives for the feature detection:
35 '(Android (1\\.[0156]|2\\.[01]))' +
36 '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
37 '|(w(eb)?OSBrowser)|(webOS)' +
38 '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
39 ).test(window.navigator.userAgent) ||
40 // Feature detection for all other devices:
41 $('<input type="file"/>').prop('disabled')
44 // The FileReader API is not actually used, but works as feature detection,
45 // as some Safari versions (5?) support XHR file uploads via the FormData API,
46 // but not non-multipart XHR file uploads.
47 // window.XMLHttpRequestUpload is not available on IE10, so we check for
48 // window.ProgressEvent instead to detect XHR2 file upload capability:
49 $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
50 $.support.xhrFormDataFileUpload = !!window.FormData;
52 // Detect support for Blob slicing (required for chunked uploads):
55 (Blob.prototype.slice ||
56 Blob.prototype.webkitSlice ||
57 Blob.prototype.mozSlice);
60 * Helper function to create drag handlers for dragover/dragenter/dragleave
62 * @param {string} type Event type
63 * @returns {Function} Drag handler
65 function getDragHandler(type) {
66 var isDragOver = type === 'dragover';
68 e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
69 var dataTransfer = e.dataTransfer;
72 $.inArray('Files', dataTransfer.types) !== -1 &&
73 this._trigger(type, $.Event(type, { delegatedEvent: e })) !== false
77 dataTransfer.dropEffect = 'copy';
83 // The fileupload widget listens for change events on file input fields defined
84 // via fileInput setting and paste or drop events of the given dropZone.
85 // In addition to the default jQuery Widget methods, the fileupload widget
86 // exposes the "add" and "send" methods, to add or directly send files using
87 // the fileupload API.
88 // By default, files added via file input selection, paste, drag & drop or
89 // "add" method are uploaded immediately, but it is possible to override
90 // the "add" callback option to queue file uploads.
91 $.widget('blueimp.fileupload', {
93 // The drop target element(s), by the default the complete document.
94 // Set to null to disable drag & drop support:
95 dropZone: $(document),
96 // The paste target element(s), by the default undefined.
97 // Set to a DOM node or jQuery object to enable file pasting:
99 // The file input field(s), that are listened to for change events.
100 // If undefined, it is set to the file input fields inside
101 // of the widget element on plugin initialization.
102 // Set to null to disable the change listener.
103 fileInput: undefined,
104 // By default, the file input field is replaced with a clone after
105 // each input field change event. This is required for iframe transport
106 // queues and allows change events to be fired for the same file
107 // selection, but can be disabled by setting the following option to false:
108 replaceFileInput: true,
109 // The parameter name for the file form data (the request argument name).
110 // If undefined or empty, the name property of the file input field is
111 // used, or "files[]" if the file input name property is also empty,
112 // can be a string or an array of strings:
113 paramName: undefined,
114 // By default, each file of a selection is uploaded using an individual
115 // request for XHR type uploads. Set to false to upload file
116 // selections in one request each:
117 singleFileUploads: true,
118 // To limit the number of files uploaded with one XHR request,
119 // set the following option to an integer greater than 0:
120 limitMultiFileUploads: undefined,
121 // The following option limits the number of files uploaded with one
122 // XHR request to keep the request size under or equal to the defined
124 limitMultiFileUploadSize: undefined,
125 // Multipart file uploads add a number of bytes to each uploaded file,
126 // therefore the following option adds an overhead for each file used
127 // in the limitMultiFileUploadSize configuration:
128 limitMultiFileUploadSizeOverhead: 512,
129 // Set the following option to true to issue all file upload requests
130 // in a sequential order:
131 sequentialUploads: false,
132 // To limit the number of concurrent uploads,
133 // set the following option to an integer greater than 0:
134 limitConcurrentUploads: undefined,
135 // Set the following option to true to force iframe transport uploads:
136 forceIframeTransport: false,
137 // Set the following option to the location of a redirect url on the
138 // origin server, for cross-domain iframe transport uploads:
140 // The parameter name for the redirect url, sent as part of the form
141 // data and set to 'redirect' if this option is empty:
142 redirectParamName: undefined,
143 // Set the following option to the location of a postMessage window,
144 // to enable postMessage transport uploads:
145 postMessage: undefined,
146 // By default, XHR file uploads are sent as multipart/form-data.
147 // The iframe transport is always using multipart/form-data.
148 // Set to false to enable non-multipart XHR uploads:
150 // To upload large files in smaller chunks, set the following option
151 // to a preferred maximum chunk size. If set to 0, null or undefined,
152 // or the browser does not support the required Blob API, files will
153 // be uploaded as a whole.
154 maxChunkSize: undefined,
155 // When a non-multipart upload or a chunked multipart upload has been
156 // aborted, this option can be used to resume the upload by setting
157 // it to the size of the already uploaded bytes. This option is most
158 // useful when modifying the options object inside of the "add" or
159 // "send" callbacks, as the options are cloned for each file upload.
160 uploadedBytes: undefined,
161 // By default, failed (abort or error) file uploads are removed from the
162 // global progress calculation. Set the following option to false to
163 // prevent recalculating the global progress data:
164 recalculateProgress: true,
165 // Interval in milliseconds to calculate and trigger progress events:
166 progressInterval: 100,
167 // Interval in milliseconds to calculate progress bitrate:
168 bitrateInterval: 500,
169 // By default, uploads are started automatically when adding files:
171 // By default, duplicate file names are expected to be handled on
172 // the server-side. If this is not possible (e.g. when uploading
173 // files directly to Amazon S3), the following option can be set to
174 // an empty object or an object mapping existing filenames, e.g.:
175 // { "image.jpg": true, "image (1).jpg": true }
176 // If it is set, all files will be uploaded with unique filenames,
177 // adding increasing number suffixes if necessary, e.g.:
179 uniqueFilenames: undefined,
181 // Error and info messages:
183 uploadedBytes: 'Uploaded bytes exceed file size'
186 // Translation function, gets the message key to be translated
187 // and an object with context specific data as arguments:
188 i18n: function (message, context) {
189 // eslint-disable-next-line no-param-reassign
190 message = this.messages[message] || message.toString();
192 $.each(context, function (key, value) {
193 // eslint-disable-next-line no-param-reassign
194 message = message.replace('{' + key + '}', value);
200 // Additional form data to be sent along with the file uploads can be set
201 // using this option, which accepts an array of objects with name and
202 // value properties, a function returning such an array, a FormData
203 // object (for XHR file uploads), or a simple object.
204 // The form of the first fileInput is given as parameter to the function:
205 formData: function (form) {
206 return form.serializeArray();
209 // The add callback is invoked as soon as files are added to the fileupload
210 // widget (via file input selection, drag & drop, paste or add API call).
211 // If the singleFileUploads option is enabled, this callback will be
212 // called once for each file in the selection for XHR file uploads, else
213 // once for each file selection.
215 // The upload starts when the submit method is invoked on the data parameter.
216 // The data object contains a files property holding the added files
217 // and allows you to override plugin options as well as define ajax settings.
219 // Listeners for this callback can also be bound the following way:
220 // .on('fileuploadadd', func);
222 // data.submit() returns a Promise object and allows to attach additional
223 // handlers using jQuery's Deferred callbacks:
224 // data.submit().done(func).fail(func).always(func);
225 add: function (e, data) {
226 if (e.isDefaultPrevented()) {
231 (data.autoUpload !== false &&
232 $(this).fileupload('option', 'autoUpload'))
234 data.process().done(function () {
242 // Callback for the submit event of each file upload:
243 // submit: function (e, data) {}, // .on('fileuploadsubmit', func);
245 // Callback for the start of each file upload request:
246 // send: function (e, data) {}, // .on('fileuploadsend', func);
248 // Callback for successful uploads:
249 // done: function (e, data) {}, // .on('fileuploaddone', func);
251 // Callback for failed (abort or error) uploads:
252 // fail: function (e, data) {}, // .on('fileuploadfail', func);
254 // Callback for completed (success, abort or error) requests:
255 // always: function (e, data) {}, // .on('fileuploadalways', func);
257 // Callback for upload progress events:
258 // progress: function (e, data) {}, // .on('fileuploadprogress', func);
260 // Callback for global upload progress events:
261 // progressall: function (e, data) {}, // .on('fileuploadprogressall', func);
263 // Callback for uploads start, equivalent to the global ajaxStart event:
264 // start: function (e) {}, // .on('fileuploadstart', func);
266 // Callback for uploads stop, equivalent to the global ajaxStop event:
267 // stop: function (e) {}, // .on('fileuploadstop', func);
269 // Callback for change events of the fileInput(s):
270 // change: function (e, data) {}, // .on('fileuploadchange', func);
272 // Callback for paste events to the pasteZone(s):
273 // paste: function (e, data) {}, // .on('fileuploadpaste', func);
275 // Callback for drop events of the dropZone(s):
276 // drop: function (e, data) {}, // .on('fileuploaddrop', func);
278 // Callback for dragover events of the dropZone(s):
279 // dragover: function (e) {}, // .on('fileuploaddragover', func);
281 // Callback before the start of each chunk upload request (before form data initialization):
282 // chunkbeforesend: function (e, data) {}, // .on('fileuploadchunkbeforesend', func);
284 // Callback for the start of each chunk upload request:
285 // chunksend: function (e, data) {}, // .on('fileuploadchunksend', func);
287 // Callback for successful chunk uploads:
288 // chunkdone: function (e, data) {}, // .on('fileuploadchunkdone', func);
290 // Callback for failed (abort or error) chunk uploads:
291 // chunkfail: function (e, data) {}, // .on('fileuploadchunkfail', func);
293 // Callback for completed (success, abort or error) chunk upload requests:
294 // chunkalways: function (e, data) {}, // .on('fileuploadchunkalways', func);
296 // The plugin options are used as settings object for the ajax calls.
297 // The following are jQuery ajax settings required for the file uploads:
304 // jQuery versions before 1.8 require promise.pipe if the return value is
305 // used, as promise.then in older versions has a different behavior, see:
306 // https://blog.jquery.com/2012/08/09/jquery-1-8-released/
307 // https://bugs.jquery.com/ticket/11010
308 // https://github.com/blueimp/jQuery-File-Upload/pull/3435
309 _promisePipe: (function () {
310 var parts = $.fn.jquery.split('.');
311 return Number(parts[0]) > 1 || Number(parts[1]) > 7 ? 'then' : 'pipe';
314 // A list of options that require reinitializing event listeners and/or
315 // special initialization code:
321 'forceIframeTransport'
325 $.support.blobSlice &&
327 var slice = this.slice || this.webkitSlice || this.mozSlice;
328 return slice.apply(this, arguments);
331 _BitrateTimer: function () {
332 this.timestamp = Date.now ? Date.now() : new Date().getTime();
335 this.getBitrate = function (now, loaded, interval) {
336 var timeDiff = now - this.timestamp;
337 if (!this.bitrate || !interval || timeDiff > interval) {
338 this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
339 this.loaded = loaded;
340 this.timestamp = now;
346 _isXHRUpload: function (options) {
348 !options.forceIframeTransport &&
349 ((!options.multipart && $.support.xhrFileUpload) ||
350 $.support.xhrFormDataFileUpload)
354 _getFormData: function (options) {
356 if ($.type(options.formData) === 'function') {
357 return options.formData(options.form);
359 if ($.isArray(options.formData)) {
360 return options.formData;
362 if ($.type(options.formData) === 'object') {
364 $.each(options.formData, function (name, value) {
365 formData.push({ name: name, value: value });
372 _getTotal: function (files) {
374 $.each(files, function (index, file) {
375 total += file.size || 1;
380 _initProgressObject: function (obj) {
387 $.extend(obj._progress, progress);
389 obj._progress = progress;
393 _initResponseObject: function (obj) {
396 for (prop in obj._response) {
397 if (Object.prototype.hasOwnProperty.call(obj._response, prop)) {
398 delete obj._response[prop];
406 _onProgress: function (e, data) {
407 if (e.lengthComputable) {
408 var now = Date.now ? Date.now() : new Date().getTime(),
412 data.progressInterval &&
413 now - data._time < data.progressInterval &&
421 (e.loaded / e.total) * (data.chunkSize || data._progress.total)
422 ) + (data.uploadedBytes || 0);
423 // Add the difference from the previously loaded state
424 // to the global loaded counter:
425 this._progress.loaded += loaded - data._progress.loaded;
426 this._progress.bitrate = this._bitrateTimer.getBitrate(
428 this._progress.loaded,
431 data._progress.loaded = data.loaded = loaded;
432 data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
437 // Trigger a custom progress event with a total data property set
438 // to the file size(s) of the current upload and a loaded data
439 // property calculated accordingly:
442 $.Event('progress', { delegatedEvent: e }),
445 // Trigger a global progress event for all current file uploads,
446 // including ajax calls queued for sequential file uploads:
449 $.Event('progressall', { delegatedEvent: e }),
455 _initProgressListener: function (options) {
457 xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
458 // Access to the native XHR object is required to add event listeners
459 // for the upload progress event:
461 $(xhr.upload).on('progress', function (e) {
462 var oe = e.originalEvent;
463 // Make sure the progress event properties get copied over:
464 e.lengthComputable = oe.lengthComputable;
465 e.loaded = oe.loaded;
467 that._onProgress(e, options);
469 options.xhr = function () {
475 _deinitProgressListener: function (options) {
476 var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
478 $(xhr.upload).off('progress');
482 _isInstanceOf: function (type, obj) {
483 // Cross-frame instanceof check
484 return Object.prototype.toString.call(obj) === '[object ' + type + ']';
487 _getUniqueFilename: function (name, map) {
488 // eslint-disable-next-line no-param-reassign
491 // eslint-disable-next-line no-param-reassign
493 /(?: \(([\d]+)\))?(\.[^.]+)?$/,
494 function (_, p1, p2) {
495 var index = p1 ? Number(p1) + 1 : 1;
497 return ' (' + index + ')' + ext;
500 return this._getUniqueFilename(name, map);
506 _initXHRData: function (options) {
509 file = options.files[0],
510 // Ignore non-multipart setting if not supported:
511 multipart = options.multipart || !$.support.xhrFileUpload,
513 $.type(options.paramName) === 'array'
514 ? options.paramName[0]
516 options.headers = $.extend({}, options.headers);
517 if (options.contentRange) {
518 options.headers['Content-Range'] = options.contentRange;
520 if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
521 options.headers['Content-Disposition'] =
522 'attachment; filename="' +
523 encodeURI(file.uploadName || file.name) +
527 options.contentType = file.type || 'application/octet-stream';
528 options.data = options.blob || file;
529 } else if ($.support.xhrFormDataFileUpload) {
530 if (options.postMessage) {
531 // window.postMessage does not allow sending FormData
532 // objects, so we just add the File/Blob objects to
533 // the formData array and let the postMessage window
534 // create the FormData object out of this array:
535 formData = this._getFormData(options);
542 $.each(options.files, function (index, file) {
545 ($.type(options.paramName) === 'array' &&
546 options.paramName[index]) ||
553 if (that._isInstanceOf('FormData', options.formData)) {
554 formData = options.formData;
556 formData = new FormData();
557 $.each(this._getFormData(options), function (index, field) {
558 formData.append(field.name, field.value);
565 file.uploadName || file.name
568 $.each(options.files, function (index, file) {
569 // This check allows the tests to run with
572 that._isInstanceOf('File', file) ||
573 that._isInstanceOf('Blob', file)
575 var fileName = file.uploadName || file.name;
576 if (options.uniqueFilenames) {
577 fileName = that._getUniqueFilename(
579 options.uniqueFilenames
583 ($.type(options.paramName) === 'array' &&
584 options.paramName[index]) ||
593 options.data = formData;
595 // Blob reference is not needed anymore, free memory:
599 _initIframeSettings: function (options) {
600 var targetHost = $('<a></a>').prop('href', options.url).prop('host');
601 // Setting the dataType to iframe enables the iframe transport:
602 options.dataType = 'iframe ' + (options.dataType || '');
603 // The iframe transport accepts a serialized array as form data:
604 options.formData = this._getFormData(options);
605 // Add redirect url to form data on cross-domain uploads:
606 if (options.redirect && targetHost && targetHost !== location.host) {
607 options.formData.push({
608 name: options.redirectParamName || 'redirect',
609 value: options.redirect
614 _initDataSettings: function (options) {
615 if (this._isXHRUpload(options)) {
616 if (!this._chunkedUpload(options, true)) {
618 this._initXHRData(options);
620 this._initProgressListener(options);
622 if (options.postMessage) {
623 // Setting the dataType to postmessage enables the
624 // postMessage transport:
625 options.dataType = 'postmessage ' + (options.dataType || '');
628 this._initIframeSettings(options);
632 _getParamName: function (options) {
633 var fileInput = $(options.fileInput),
634 paramName = options.paramName;
637 fileInput.each(function () {
639 name = input.prop('name') || 'files[]',
640 i = (input.prop('files') || [1]).length;
642 paramName.push(name);
646 if (!paramName.length) {
647 paramName = [fileInput.prop('name') || 'files[]'];
649 } else if (!$.isArray(paramName)) {
650 paramName = [paramName];
655 _initFormSettings: function (options) {
656 // Retrieve missing options from the input field and the
657 // associated form, if available:
658 if (!options.form || !options.form.length) {
659 options.form = $(options.fileInput.prop('form'));
660 // If the given file input doesn't have an associated form,
661 // use the default widget file input's form:
662 if (!options.form.length) {
663 options.form = $(this.options.fileInput.prop('form'));
666 options.paramName = this._getParamName(options);
668 options.url = options.form.prop('action') || location.href;
670 // The HTTP request method must be "POST" or "PUT":
673 ($.type(options.form.prop('method')) === 'string' &&
674 options.form.prop('method')) ||
678 options.type !== 'POST' &&
679 options.type !== 'PUT' &&
680 options.type !== 'PATCH'
682 options.type = 'POST';
684 if (!options.formAcceptCharset) {
685 options.formAcceptCharset = options.form.attr('accept-charset');
689 _getAJAXSettings: function (data) {
690 var options = $.extend({}, this.options, data);
691 this._initFormSettings(options);
692 this._initDataSettings(options);
696 // jQuery 1.6 doesn't provide .state(),
697 // while jQuery 1.8+ removed .isRejected() and .isResolved():
698 _getDeferredState: function (deferred) {
699 if (deferred.state) {
700 return deferred.state();
702 if (deferred.isResolved()) {
705 if (deferred.isRejected()) {
711 // Maps jqXHR callbacks to the equivalent
712 // methods of the given Promise object:
713 _enhancePromise: function (promise) {
714 promise.success = promise.done;
715 promise.error = promise.fail;
716 promise.complete = promise.always;
720 // Creates and returns a Promise object enhanced with
721 // the jqXHR methods abort, success, error and complete:
722 _getXHRPromise: function (resolveOrReject, context, args) {
723 var dfd = $.Deferred(),
724 promise = dfd.promise();
725 // eslint-disable-next-line no-param-reassign
726 context = context || this.options.context || promise;
727 if (resolveOrReject === true) {
728 dfd.resolveWith(context, args);
729 } else if (resolveOrReject === false) {
730 dfd.rejectWith(context, args);
732 promise.abort = dfd.promise;
733 return this._enhancePromise(promise);
736 // Adds convenience methods to the data callback argument:
737 _addConvenienceMethods: function (e, data) {
739 getPromise = function (args) {
740 return $.Deferred().resolveWith(that, args).promise();
742 data.process = function (resolveFunc, rejectFunc) {
743 if (resolveFunc || rejectFunc) {
744 data._processQueue = this._processQueue = (this._processQueue ||
746 [that._promisePipe](function () {
747 if (data.errorThrown) {
748 return $.Deferred().rejectWith(that, [data]).promise();
750 return getPromise(arguments);
752 [that._promisePipe](resolveFunc, rejectFunc);
754 return this._processQueue || getPromise([this]);
756 data.submit = function () {
757 if (this.state() !== 'pending') {
758 data.jqXHR = this.jqXHR =
761 $.Event('submit', { delegatedEvent: e }),
763 ) !== false && that._onSend(e, this);
765 return this.jqXHR || that._getXHRPromise();
767 data.abort = function () {
769 return this.jqXHR.abort();
771 this.errorThrown = 'abort';
772 that._trigger('fail', null, this);
773 return that._getXHRPromise(false);
775 data.state = function () {
777 return that._getDeferredState(this.jqXHR);
779 if (this._processQueue) {
780 return that._getDeferredState(this._processQueue);
783 data.processing = function () {
786 this._processQueue &&
787 that._getDeferredState(this._processQueue) === 'pending'
790 data.progress = function () {
791 return this._progress;
793 data.response = function () {
794 return this._response;
798 // Parses the Range header from the server response
799 // and returns the uploaded bytes:
800 _getUploadedBytes: function (jqXHR) {
801 var range = jqXHR.getResponseHeader('Range'),
802 parts = range && range.split('-'),
803 upperBytesPos = parts && parts.length > 1 && parseInt(parts[1], 10);
804 return upperBytesPos && upperBytesPos + 1;
807 // Uploads a file in multiple, sequential requests
808 // by splitting the file up in multiple blob chunks.
809 // If the second parameter is true, only tests if the file
810 // should be uploaded in chunks, but does not invoke any
812 _chunkedUpload: function (options, testOnly) {
813 options.uploadedBytes = options.uploadedBytes || 0;
815 file = options.files[0],
817 ub = options.uploadedBytes,
818 mcs = options.maxChunkSize || fs,
819 slice = this._blobSlice,
821 promise = dfd.promise(),
826 this._isXHRUpload(options) &&
828 (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs)
838 file.error = options.i18n('uploadedBytes');
839 return this._getXHRPromise(false, options.context, [
845 // The chunk upload method:
846 upload = function () {
847 // Clone the options object for each chunk upload:
848 var o = $.extend({}, options),
849 currentLoaded = o._progress.loaded;
853 ub + ($.type(mcs) === 'function' ? mcs(o) : mcs),
856 // Store the current chunk size, as the blob itself
857 // will be dereferenced after data processing:
858 o.chunkSize = o.blob.size;
859 // Expose the chunk bytes position range:
861 'bytes ' + ub + '-' + (ub + o.chunkSize - 1) + '/' + fs;
862 // Trigger chunkbeforesend to allow form data to be updated for this chunk
863 that._trigger('chunkbeforesend', null, o);
864 // Process the upload data (the blob and potential form data):
865 that._initXHRData(o);
866 // Add progress listeners for this chunk upload:
867 that._initProgressListener(o);
869 (that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
870 that._getXHRPromise(false, o.context)
872 .done(function (result, textStatus, jqXHR) {
873 ub = that._getUploadedBytes(jqXHR) || ub + o.chunkSize;
874 // Create a progress event if no final progress event
875 // with loaded equaling total has been triggered
877 if (currentLoaded + o.chunkSize - o._progress.loaded) {
879 $.Event('progress', {
880 lengthComputable: true,
881 loaded: ub - o.uploadedBytes,
882 total: ub - o.uploadedBytes
887 options.uploadedBytes = o.uploadedBytes = ub;
889 o.textStatus = textStatus;
891 that._trigger('chunkdone', null, o);
892 that._trigger('chunkalways', null, o);
894 // File upload not yet complete,
895 // continue with the next chunk:
898 dfd.resolveWith(o.context, [result, textStatus, jqXHR]);
901 .fail(function (jqXHR, textStatus, errorThrown) {
903 o.textStatus = textStatus;
904 o.errorThrown = errorThrown;
905 that._trigger('chunkfail', null, o);
906 that._trigger('chunkalways', null, o);
907 dfd.rejectWith(o.context, [jqXHR, textStatus, errorThrown]);
909 .always(function () {
910 that._deinitProgressListener(o);
913 this._enhancePromise(promise);
914 promise.abort = function () {
915 return jqXHR.abort();
921 _beforeSend: function (e, data) {
922 if (this._active === 0) {
923 // the start callback is triggered when an upload starts
924 // and no other uploads are currently running,
925 // equivalent to the global ajaxStart event:
926 this._trigger('start');
927 // Set timer for global bitrate progress calculation:
928 this._bitrateTimer = new this._BitrateTimer();
929 // Reset the global progress values:
930 this._progress.loaded = this._progress.total = 0;
931 this._progress.bitrate = 0;
933 // Make sure the container objects for the .response() and
934 // .progress() methods on the data object are available
935 // and reset to their initial state:
936 this._initResponseObject(data);
937 this._initProgressObject(data);
938 data._progress.loaded = data.loaded = data.uploadedBytes || 0;
939 data._progress.total = data.total = this._getTotal(data.files) || 1;
940 data._progress.bitrate = data.bitrate = 0;
942 // Initialize the global progress values:
943 this._progress.loaded += data.loaded;
944 this._progress.total += data.total;
947 _onDone: function (result, textStatus, jqXHR, options) {
948 var total = options._progress.total,
949 response = options._response;
950 if (options._progress.loaded < total) {
951 // Create a progress event if no final progress event
952 // with loaded equaling total has been triggered:
954 $.Event('progress', {
955 lengthComputable: true,
962 response.result = options.result = result;
963 response.textStatus = options.textStatus = textStatus;
964 response.jqXHR = options.jqXHR = jqXHR;
965 this._trigger('done', null, options);
968 _onFail: function (jqXHR, textStatus, errorThrown, options) {
969 var response = options._response;
970 if (options.recalculateProgress) {
971 // Remove the failed (error or abort) file upload from
972 // the global progress calculation:
973 this._progress.loaded -= options._progress.loaded;
974 this._progress.total -= options._progress.total;
976 response.jqXHR = options.jqXHR = jqXHR;
977 response.textStatus = options.textStatus = textStatus;
978 response.errorThrown = options.errorThrown = errorThrown;
979 this._trigger('fail', null, options);
982 _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
983 // jqXHRorResult, textStatus and jqXHRorError are added to the
984 // options object via done and fail callbacks
985 this._trigger('always', null, options);
988 _onSend: function (e, data) {
990 this._addConvenienceMethods(e, data);
997 options = that._getAJAXSettings(data),
1000 // Set timer for bitrate progress calculation:
1001 options._bitrateTimer = new that._BitrateTimer();
1008 $.Event('send', { delegatedEvent: e }),
1011 that._getXHRPromise(false, options.context, aborted)) ||
1012 that._chunkedUpload(options) ||
1015 .done(function (result, textStatus, jqXHR) {
1016 that._onDone(result, textStatus, jqXHR, options);
1018 .fail(function (jqXHR, textStatus, errorThrown) {
1019 that._onFail(jqXHR, textStatus, errorThrown, options);
1021 .always(function (jqXHRorResult, textStatus, jqXHRorError) {
1022 that._deinitProgressListener(options);
1032 options.limitConcurrentUploads &&
1033 options.limitConcurrentUploads > that._sending
1035 // Start the next queued upload,
1036 // that has not been aborted:
1037 var nextSlot = that._slots.shift();
1039 if (that._getDeferredState(nextSlot) === 'pending') {
1043 nextSlot = that._slots.shift();
1046 if (that._active === 0) {
1047 // The stop callback is triggered when all uploads have
1048 // been completed, equivalent to the global ajaxStop event:
1049 that._trigger('stop');
1054 this._beforeSend(e, options);
1056 this.options.sequentialUploads ||
1057 (this.options.limitConcurrentUploads &&
1058 this.options.limitConcurrentUploads <= this._sending)
1060 if (this.options.limitConcurrentUploads > 1) {
1061 slot = $.Deferred();
1062 this._slots.push(slot);
1063 pipe = slot[that._promisePipe](send);
1065 this._sequence = this._sequence[that._promisePipe](send, send);
1066 pipe = this._sequence;
1068 // Return the piped Promise object, enhanced with an abort method,
1069 // which is delegated to the jqXHR object of the current upload,
1070 // and jqXHR callbacks mapped to the equivalent Promise methods:
1071 pipe.abort = function () {
1072 aborted = [undefined, 'abort', 'abort'];
1075 slot.rejectWith(options.context, aborted);
1079 return jqXHR.abort();
1081 return this._enhancePromise(pipe);
1086 _onAdd: function (e, data) {
1089 options = $.extend({}, this.options, data),
1091 filesLength = files.length,
1092 limit = options.limitMultiFileUploads,
1093 limitSize = options.limitMultiFileUploadSize,
1094 overhead = options.limitMultiFileUploadSizeOverhead,
1096 paramName = this._getParamName(options),
1105 if (limitSize && files[0].size === undefined) {
1106 limitSize = undefined;
1109 !(options.singleFileUploads || limit || limitSize) ||
1110 !this._isXHRUpload(options)
1113 paramNameSet = [paramName];
1114 } else if (!(options.singleFileUploads || limitSize) && limit) {
1117 for (i = 0; i < filesLength; i += limit) {
1118 fileSet.push(files.slice(i, i + limit));
1119 paramNameSlice = paramName.slice(i, i + limit);
1120 if (!paramNameSlice.length) {
1121 paramNameSlice = paramName;
1123 paramNameSet.push(paramNameSlice);
1125 } else if (!options.singleFileUploads && limitSize) {
1128 for (i = 0; i < filesLength; i = i + 1) {
1129 batchSize += files[i].size + overhead;
1131 i + 1 === filesLength ||
1132 batchSize + files[i + 1].size + overhead > limitSize ||
1133 (limit && i + 1 - j >= limit)
1135 fileSet.push(files.slice(j, i + 1));
1136 paramNameSlice = paramName.slice(j, i + 1);
1137 if (!paramNameSlice.length) {
1138 paramNameSlice = paramName;
1140 paramNameSet.push(paramNameSlice);
1146 paramNameSet = paramName;
1148 data.originalFiles = files;
1149 $.each(fileSet || files, function (index, element) {
1150 var newData = $.extend({}, data);
1151 newData.files = fileSet ? element : [element];
1152 newData.paramName = paramNameSet[index];
1153 that._initResponseObject(newData);
1154 that._initProgressObject(newData);
1155 that._addConvenienceMethods(e, newData);
1156 result = that._trigger(
1158 $.Event('add', { delegatedEvent: e }),
1166 _replaceFileInput: function (data) {
1167 var input = data.fileInput,
1168 inputClone = input.clone(true),
1169 restoreFocus = input.is(document.activeElement);
1170 // Add a reference for the new cloned file input to the data argument:
1171 data.fileInputClone = inputClone;
1172 $('<form></form>').append(inputClone)[0].reset();
1173 // Detaching allows to insert the fileInput on another form
1174 // without losing the file input value:
1175 input.after(inputClone).detach();
1176 // If the fileInput had focus before it was detached,
1177 // restore focus to the inputClone.
1179 inputClone.trigger('focus');
1181 // Avoid memory leaks with the detached file input:
1182 $.cleanData(input.off('remove'));
1183 // Replace the original file input element in the fileInput
1184 // elements set with the clone, which has been copied including
1186 this.options.fileInput = this.options.fileInput.map(function (i, el) {
1187 if (el === input[0]) {
1188 return inputClone[0];
1192 // If the widget has been initialized on the file input itself,
1193 // override this.element with the file input clone:
1194 if (input[0] === this.element[0]) {
1195 this.element = inputClone;
1199 _handleFileTreeEntry: function (entry, path) {
1204 errorHandler = function (e) {
1205 if (e && !e.entry) {
1208 // Since $.when returns immediately if one
1209 // Deferred is rejected, we use resolve instead.
1210 // This allows valid files and invalid items
1211 // to be returned together in one set:
1214 successHandler = function (entries) {
1216 ._handleFileTreeEntries(entries, path + entry.name + '/')
1217 .done(function (files) {
1220 .fail(errorHandler);
1222 readEntries = function () {
1223 dirReader.readEntries(function (results) {
1224 if (!results.length) {
1225 successHandler(entries);
1227 entries = entries.concat(results);
1232 // eslint-disable-next-line no-param-reassign
1236 // Workaround for Chrome bug #149735
1237 entry._file.relativePath = path;
1238 dfd.resolve(entry._file);
1240 entry.file(function (file) {
1241 file.relativePath = path;
1245 } else if (entry.isDirectory) {
1246 dirReader = entry.createReader();
1249 // Return an empty list for file system items
1250 // other than files or directories:
1253 return dfd.promise();
1256 _handleFileTreeEntries: function (entries, path) {
1261 $.map(entries, function (entry) {
1262 return that._handleFileTreeEntry(entry, path);
1265 [this._promisePipe](function () {
1266 return Array.prototype.concat.apply([], arguments);
1270 _getDroppedFiles: function (dataTransfer) {
1271 // eslint-disable-next-line no-param-reassign
1272 dataTransfer = dataTransfer || {};
1273 var items = dataTransfer.items;
1277 (items[0].webkitGetAsEntry || items[0].getAsEntry)
1279 return this._handleFileTreeEntries(
1280 $.map(items, function (item) {
1282 if (item.webkitGetAsEntry) {
1283 entry = item.webkitGetAsEntry();
1285 // Workaround for Chrome bug #149735:
1286 entry._file = item.getAsFile();
1290 return item.getAsEntry();
1294 return $.Deferred().resolve($.makeArray(dataTransfer.files)).promise();
1297 _getSingleFileInputFiles: function (fileInput) {
1298 // eslint-disable-next-line no-param-reassign
1299 fileInput = $(fileInput);
1300 var entries = fileInput.prop('entries'),
1303 if (entries && entries.length) {
1304 return this._handleFileTreeEntries(entries);
1306 files = $.makeArray(fileInput.prop('files'));
1307 if (!files.length) {
1308 value = fileInput.prop('value');
1310 return $.Deferred().resolve([]).promise();
1312 // If the files property is not available, the browser does not
1313 // support the File API and we add a pseudo File object with
1314 // the input value as name with path information removed:
1315 files = [{ name: value.replace(/^.*\\/, '') }];
1316 } else if (files[0].name === undefined && files[0].fileName) {
1317 // File normalization for Safari 4 and Firefox 3:
1318 $.each(files, function (index, file) {
1319 file.name = file.fileName;
1320 file.size = file.fileSize;
1323 return $.Deferred().resolve(files).promise();
1326 _getFileInputFiles: function (fileInput) {
1327 if (!(fileInput instanceof $) || fileInput.length === 1) {
1328 return this._getSingleFileInputFiles(fileInput);
1331 .apply($, $.map(fileInput, this._getSingleFileInputFiles))
1332 [this._promisePipe](function () {
1333 return Array.prototype.concat.apply([], arguments);
1337 _onChange: function (e) {
1340 fileInput: $(e.target),
1341 form: $(e.target.form)
1343 this._getFileInputFiles(data.fileInput).always(function (files) {
1345 if (that.options.replaceFileInput) {
1346 that._replaceFileInput(data);
1351 $.Event('change', { delegatedEvent: e }),
1355 that._onAdd(e, data);
1360 _onPaste: function (e) {
1363 e.originalEvent.clipboardData &&
1364 e.originalEvent.clipboardData.items,
1365 data = { files: [] };
1366 if (items && items.length) {
1367 $.each(items, function (index, item) {
1368 var file = item.getAsFile && item.getAsFile();
1370 data.files.push(file);
1376 $.Event('paste', { delegatedEvent: e }),
1380 this._onAdd(e, data);
1385 _onDrop: function (e) {
1386 e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
1388 dataTransfer = e.dataTransfer,
1390 if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1392 this._getDroppedFiles(dataTransfer).always(function (files) {
1397 $.Event('drop', { delegatedEvent: e }),
1401 that._onAdd(e, data);
1407 _onDragOver: getDragHandler('dragover'),
1409 _onDragEnter: getDragHandler('dragenter'),
1411 _onDragLeave: getDragHandler('dragleave'),
1413 _initEventHandlers: function () {
1414 if (this._isXHRUpload(this.options)) {
1415 this._on(this.options.dropZone, {
1416 dragover: this._onDragOver,
1418 // event.preventDefault() on dragenter is required for IE10+:
1419 dragenter: this._onDragEnter,
1420 // dragleave is not required, but added for completeness:
1421 dragleave: this._onDragLeave
1423 this._on(this.options.pasteZone, {
1424 paste: this._onPaste
1427 if ($.support.fileInput) {
1428 this._on(this.options.fileInput, {
1429 change: this._onChange
1434 _destroyEventHandlers: function () {
1435 this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
1436 this._off(this.options.pasteZone, 'paste');
1437 this._off(this.options.fileInput, 'change');
1440 _destroy: function () {
1441 this._destroyEventHandlers();
1444 _setOption: function (key, value) {
1445 var reinit = $.inArray(key, this._specialOptions) !== -1;
1447 this._destroyEventHandlers();
1449 this._super(key, value);
1451 this._initSpecialOptions();
1452 this._initEventHandlers();
1456 _initSpecialOptions: function () {
1457 var options = this.options;
1458 if (options.fileInput === undefined) {
1459 options.fileInput = this.element.is('input[type="file"]')
1461 : this.element.find('input[type="file"]');
1462 } else if (!(options.fileInput instanceof $)) {
1463 options.fileInput = $(options.fileInput);
1465 if (!(options.dropZone instanceof $)) {
1466 options.dropZone = $(options.dropZone);
1468 if (!(options.pasteZone instanceof $)) {
1469 options.pasteZone = $(options.pasteZone);
1473 _getRegExp: function (str) {
1474 var parts = str.split('/'),
1475 modifiers = parts.pop();
1477 return new RegExp(parts.join('/'), modifiers);
1480 _isRegExpOption: function (key, value) {
1483 $.type(value) === 'string' &&
1484 /^\/.*\/[igm]{0,3}$/.test(value)
1488 _initDataAttributes: function () {
1490 options = this.options,
1491 data = this.element.data();
1492 // Initialize options set via HTML5 data-attributes:
1493 $.each(this.element[0].attributes, function (index, attr) {
1494 var key = attr.name.toLowerCase(),
1496 if (/^data-/.test(key)) {
1497 // Convert hyphen-ated key to camelCase:
1498 key = key.slice(5).replace(/-[a-z]/g, function (str) {
1499 return str.charAt(1).toUpperCase();
1502 if (that._isRegExpOption(key, value)) {
1503 value = that._getRegExp(value);
1505 options[key] = value;
1510 _create: function () {
1511 this._initDataAttributes();
1512 this._initSpecialOptions();
1514 this._sequence = this._getXHRPromise(true);
1515 this._sending = this._active = 0;
1516 this._initProgressObject(this);
1517 this._initEventHandlers();
1520 // This method is exposed to the widget API and allows to query
1521 // the number of active uploads:
1522 active: function () {
1523 return this._active;
1526 // This method is exposed to the widget API and allows to query
1527 // the widget upload progress.
1528 // It returns an object with loaded, total and bitrate properties
1529 // for the running uploads:
1530 progress: function () {
1531 return this._progress;
1534 // This method is exposed to the widget API and allows adding files
1535 // using the fileupload API. The data parameter accepts an object which
1536 // must have a files property and can contain additional options:
1537 // .fileupload('add', {files: filesList});
1538 add: function (data) {
1540 if (!data || this.options.disabled) {
1543 if (data.fileInput && !data.files) {
1544 this._getFileInputFiles(data.fileInput).always(function (files) {
1546 that._onAdd(null, data);
1549 data.files = $.makeArray(data.files);
1550 this._onAdd(null, data);
1554 // This method is exposed to the widget API and allows sending files
1555 // using the fileupload API. The data parameter accepts an object which
1556 // must have a files or fileInput property and can contain additional options:
1557 // .fileupload('send', {files: filesList});
1558 // The method returns a Promise object for the file upload call.
1559 send: function (data) {
1560 if (data && !this.options.disabled) {
1561 if (data.fileInput && !data.files) {
1564 promise = dfd.promise(),
1567 promise.abort = function () {
1570 return jqXHR.abort();
1572 dfd.reject(null, 'abort', 'abort');
1575 this._getFileInputFiles(data.fileInput).always(function (files) {
1579 if (!files.length) {
1584 jqXHR = that._onSend(null, data);
1586 function (result, textStatus, jqXHR) {
1587 dfd.resolve(result, textStatus, jqXHR);
1589 function (jqXHR, textStatus, errorThrown) {
1590 dfd.reject(jqXHR, textStatus, errorThrown);
1594 return this._enhancePromise(promise);
1596 data.files = $.makeArray(data.files);
1597 if (data.files.length) {
1598 return this._onSend(null, data);
1601 return this._getXHRPromise(false, data && data.context);