2 * jQuery File Upload Plugin
\r
3 * https://github.com/blueimp/jQuery-File-Upload
\r
5 * Copyright 2010, Sebastian Tschan
\r
6 * https://blueimp.net
\r
8 * Licensed under the MIT license:
\r
9 * http://www.opensource.org/licenses/MIT
\r
12 /* jshint nomen:false */
\r
13 /* global define, require, window, document, location, Blob, FormData */
\r
15 (function (factory) {
\r
17 if (typeof define === 'function' && define.amd) {
\r
18 // Register as an anonymous AMD module:
\r
23 } else if (typeof exports === 'object') {
\r
27 require('./vendor/jquery.ui.widget')
\r
31 factory(window.jQuery);
\r
36 // Detect file input support, based on
\r
37 // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
\r
38 $.support.fileInput = !(new RegExp(
\r
39 // Handle devices which give false positives for the feature detection:
\r
40 '(Android (1\\.[0156]|2\\.[01]))' +
\r
41 '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
\r
42 '|(w(eb)?OSBrowser)|(webOS)' +
\r
43 '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
\r
44 ).test(window.navigator.userAgent) ||
\r
45 // Feature detection for all other devices:
\r
46 $('<input type="file">').prop('disabled'));
\r
48 // The FileReader API is not actually used, but works as feature detection,
\r
49 // as some Safari versions (5?) support XHR file uploads via the FormData API,
\r
50 // but not non-multipart XHR file uploads.
\r
51 // window.XMLHttpRequestUpload is not available on IE10, so we check for
\r
52 // window.ProgressEvent instead to detect XHR2 file upload capability:
\r
53 $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
\r
54 $.support.xhrFormDataFileUpload = !!window.FormData;
\r
56 // Detect support for Blob slicing (required for chunked uploads):
\r
57 $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
\r
58 Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
\r
60 // Helper function to create drag handlers for dragover/dragenter/dragleave:
\r
61 function getDragHandler(type) {
\r
62 var isDragOver = type === 'dragover';
\r
63 return function (e) {
\r
64 e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
\r
65 var dataTransfer = e.dataTransfer;
\r
66 if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
\r
69 $.Event(type, {delegatedEvent: e})
\r
73 dataTransfer.dropEffect = 'copy';
\r
79 // The fileupload widget listens for change events on file input fields defined
\r
80 // via fileInput setting and paste or drop events of the given dropZone.
\r
81 // In addition to the default jQuery Widget methods, the fileupload widget
\r
82 // exposes the "add" and "send" methods, to add or directly send files using
\r
83 // the fileupload API.
\r
84 // By default, files added via file input selection, paste, drag & drop or
\r
85 // "add" method are uploaded immediately, but it is possible to override
\r
86 // the "add" callback option to queue file uploads.
\r
87 $.widget('blueimp.fileupload', {
\r
90 // The drop target element(s), by the default the complete document.
\r
91 // Set to null to disable drag & drop support:
\r
92 dropZone: $(document),
\r
93 // The paste target element(s), by the default undefined.
\r
94 // Set to a DOM node or jQuery object to enable file pasting:
\r
95 pasteZone: undefined,
\r
96 // The file input field(s), that are listened to for change events.
\r
97 // If undefined, it is set to the file input fields inside
\r
98 // of the widget element on plugin initialization.
\r
99 // Set to null to disable the change listener.
\r
100 fileInput: undefined,
\r
101 // By default, the file input field is replaced with a clone after
\r
102 // each input field change event. This is required for iframe transport
\r
103 // queues and allows change events to be fired for the same file
\r
104 // selection, but can be disabled by setting the following option to false:
\r
105 replaceFileInput: true,
\r
106 // The parameter name for the file form data (the request argument name).
\r
107 // If undefined or empty, the name property of the file input field is
\r
108 // used, or "files[]" if the file input name property is also empty,
\r
109 // can be a string or an array of strings:
\r
110 paramName: undefined,
\r
111 // By default, each file of a selection is uploaded using an individual
\r
112 // request for XHR type uploads. Set to false to upload file
\r
113 // selections in one request each:
\r
114 singleFileUploads: true,
\r
115 // To limit the number of files uploaded with one XHR request,
\r
116 // set the following option to an integer greater than 0:
\r
117 limitMultiFileUploads: undefined,
\r
118 // The following option limits the number of files uploaded with one
\r
119 // XHR request to keep the request size under or equal to the defined
\r
121 limitMultiFileUploadSize: undefined,
\r
122 // Multipart file uploads add a number of bytes to each uploaded file,
\r
123 // therefore the following option adds an overhead for each file used
\r
124 // in the limitMultiFileUploadSize configuration:
\r
125 limitMultiFileUploadSizeOverhead: 512,
\r
126 // Set the following option to true to issue all file upload requests
\r
127 // in a sequential order:
\r
128 sequentialUploads: false,
\r
129 // To limit the number of concurrent uploads,
\r
130 // set the following option to an integer greater than 0:
\r
131 limitConcurrentUploads: undefined,
\r
132 // Set the following option to true to force iframe transport uploads:
\r
133 forceIframeTransport: false,
\r
134 // Set the following option to the location of a redirect url on the
\r
135 // origin server, for cross-domain iframe transport uploads:
\r
136 redirect: undefined,
\r
137 // The parameter name for the redirect url, sent as part of the form
\r
138 // data and set to 'redirect' if this option is empty:
\r
139 redirectParamName: undefined,
\r
140 // Set the following option to the location of a postMessage window,
\r
141 // to enable postMessage transport uploads:
\r
142 postMessage: undefined,
\r
143 // By default, XHR file uploads are sent as multipart/form-data.
\r
144 // The iframe transport is always using multipart/form-data.
\r
145 // Set to false to enable non-multipart XHR uploads:
\r
147 // To upload large files in smaller chunks, set the following option
\r
148 // to a preferred maximum chunk size. If set to 0, null or undefined,
\r
149 // or the browser does not support the required Blob API, files will
\r
150 // be uploaded as a whole.
\r
151 maxChunkSize: undefined,
\r
152 // When a non-multipart upload or a chunked multipart upload has been
\r
153 // aborted, this option can be used to resume the upload by setting
\r
154 // it to the size of the already uploaded bytes. This option is most
\r
155 // useful when modifying the options object inside of the "add" or
\r
156 // "send" callbacks, as the options are cloned for each file upload.
\r
157 uploadedBytes: undefined,
\r
158 // By default, failed (abort or error) file uploads are removed from the
\r
159 // global progress calculation. Set the following option to false to
\r
160 // prevent recalculating the global progress data:
\r
161 recalculateProgress: true,
\r
162 // Interval in milliseconds to calculate and trigger progress events:
\r
163 progressInterval: 100,
\r
164 // Interval in milliseconds to calculate progress bitrate:
\r
165 bitrateInterval: 500,
\r
166 // By default, uploads are started automatically when adding files:
\r
169 // Error and info messages:
\r
171 uploadedBytes: 'Uploaded bytes exceed file size'
\r
174 // Translation function, gets the message key to be translated
\r
175 // and an object with context specific data as arguments:
\r
176 i18n: function (message, context) {
\r
177 message = this.messages[message] || message.toString();
\r
179 $.each(context, function (key, value) {
\r
180 message = message.replace('{' + key + '}', value);
\r
186 // Additional form data to be sent along with the file uploads can be set
\r
187 // using this option, which accepts an array of objects with name and
\r
188 // value properties, a function returning such an array, a FormData
\r
189 // object (for XHR file uploads), or a simple object.
\r
190 // The form of the first fileInput is given as parameter to the function:
\r
191 formData: function (form) {
\r
192 return form.serializeArray();
\r
195 // The add callback is invoked as soon as files are added to the fileupload
\r
196 // widget (via file input selection, drag & drop, paste or add API call).
\r
197 // If the singleFileUploads option is enabled, this callback will be
\r
198 // called once for each file in the selection for XHR file uploads, else
\r
199 // once for each file selection.
\r
201 // The upload starts when the submit method is invoked on the data parameter.
\r
202 // The data object contains a files property holding the added files
\r
203 // and allows you to override plugin options as well as define ajax settings.
\r
205 // Listeners for this callback can also be bound the following way:
\r
206 // .bind('fileuploadadd', func);
\r
208 // data.submit() returns a Promise object and allows to attach additional
\r
209 // handlers using jQuery's Deferred callbacks:
\r
210 // data.submit().done(func).fail(func).always(func);
\r
211 add: function (e, data) {
\r
212 if (e.isDefaultPrevented()) {
\r
215 if (data.autoUpload || (data.autoUpload !== false &&
\r
216 $(this).fileupload('option', 'autoUpload'))) {
\r
217 data.process().done(function () {
\r
223 // Other callbacks:
\r
225 // Callback for the submit event of each file upload:
\r
226 // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
\r
228 // Callback for the start of each file upload request:
\r
229 // send: function (e, data) {}, // .bind('fileuploadsend', func);
\r
231 // Callback for successful uploads:
\r
232 // done: function (e, data) {}, // .bind('fileuploaddone', func);
\r
234 // Callback for failed (abort or error) uploads:
\r
235 // fail: function (e, data) {}, // .bind('fileuploadfail', func);
\r
237 // Callback for completed (success, abort or error) requests:
\r
238 // always: function (e, data) {}, // .bind('fileuploadalways', func);
\r
240 // Callback for upload progress events:
\r
241 // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
\r
243 // Callback for global upload progress events:
\r
244 // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
\r
246 // Callback for uploads start, equivalent to the global ajaxStart event:
\r
247 // start: function (e) {}, // .bind('fileuploadstart', func);
\r
249 // Callback for uploads stop, equivalent to the global ajaxStop event:
\r
250 // stop: function (e) {}, // .bind('fileuploadstop', func);
\r
252 // Callback for change events of the fileInput(s):
\r
253 // change: function (e, data) {}, // .bind('fileuploadchange', func);
\r
255 // Callback for paste events to the pasteZone(s):
\r
256 // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
\r
258 // Callback for drop events of the dropZone(s):
\r
259 // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
\r
261 // Callback for dragover events of the dropZone(s):
\r
262 // dragover: function (e) {}, // .bind('fileuploaddragover', func);
\r
264 // Callback for the start of each chunk upload request:
\r
265 // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
\r
267 // Callback for successful chunk uploads:
\r
268 // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
\r
270 // Callback for failed (abort or error) chunk uploads:
\r
271 // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
\r
273 // Callback for completed (success, abort or error) chunk upload requests:
\r
274 // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
\r
276 // The plugin options are used as settings object for the ajax calls.
\r
277 // The following are jQuery ajax settings required for the file uploads:
\r
278 processData: false,
\r
279 contentType: false,
\r
284 // A list of options that require reinitializing event listeners and/or
\r
285 // special initialization code:
\r
291 'forceIframeTransport'
\r
294 _blobSlice: $.support.blobSlice && function () {
\r
295 var slice = this.slice || this.webkitSlice || this.mozSlice;
\r
296 return slice.apply(this, arguments);
\r
299 _BitrateTimer: function () {
\r
300 this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
\r
303 this.getBitrate = function (now, loaded, interval) {
\r
304 var timeDiff = now - this.timestamp;
\r
305 if (!this.bitrate || !interval || timeDiff > interval) {
\r
306 this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
\r
307 this.loaded = loaded;
\r
308 this.timestamp = now;
\r
310 return this.bitrate;
\r
314 _isXHRUpload: function (options) {
\r
315 return !options.forceIframeTransport &&
\r
316 ((!options.multipart && $.support.xhrFileUpload) ||
\r
317 $.support.xhrFormDataFileUpload);
\r
320 _getFormData: function (options) {
\r
322 if ($.type(options.formData) === 'function') {
\r
323 return options.formData(options.form);
\r
325 if ($.isArray(options.formData)) {
\r
326 return options.formData;
\r
328 if ($.type(options.formData) === 'object') {
\r
330 $.each(options.formData, function (name, value) {
\r
331 formData.push({name: name, value: value});
\r
338 _getTotal: function (files) {
\r
340 $.each(files, function (index, file) {
\r
341 total += file.size || 1;
\r
346 _initProgressObject: function (obj) {
\r
352 if (obj._progress) {
\r
353 $.extend(obj._progress, progress);
\r
355 obj._progress = progress;
\r
359 _initResponseObject: function (obj) {
\r
361 if (obj._response) {
\r
362 for (prop in obj._response) {
\r
363 if (obj._response.hasOwnProperty(prop)) {
\r
364 delete obj._response[prop];
\r
368 obj._response = {};
\r
372 _onProgress: function (e, data) {
\r
373 if (e.lengthComputable) {
\r
374 var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
\r
376 if (data._time && data.progressInterval &&
\r
377 (now - data._time < data.progressInterval) &&
\r
378 e.loaded !== e.total) {
\r
382 loaded = Math.floor(
\r
383 e.loaded / e.total * (data.chunkSize || data._progress.total)
\r
384 ) + (data.uploadedBytes || 0);
\r
385 // Add the difference from the previously loaded state
\r
386 // to the global loaded counter:
\r
387 this._progress.loaded += (loaded - data._progress.loaded);
\r
388 this._progress.bitrate = this._bitrateTimer.getBitrate(
\r
390 this._progress.loaded,
\r
391 data.bitrateInterval
\r
393 data._progress.loaded = data.loaded = loaded;
\r
394 data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
\r
397 data.bitrateInterval
\r
399 // Trigger a custom progress event with a total data property set
\r
400 // to the file size(s) of the current upload and a loaded data
\r
401 // property calculated accordingly:
\r
404 $.Event('progress', {delegatedEvent: e}),
\r
407 // Trigger a global progress event for all current file uploads,
\r
408 // including ajax calls queued for sequential file uploads:
\r
411 $.Event('progressall', {delegatedEvent: e}),
\r
417 _initProgressListener: function (options) {
\r
419 xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
\r
420 // Accesss to the native XHR object is required to add event listeners
\r
421 // for the upload progress event:
\r
423 $(xhr.upload).bind('progress', function (e) {
\r
424 var oe = e.originalEvent;
\r
425 // Make sure the progress event properties get copied over:
\r
426 e.lengthComputable = oe.lengthComputable;
\r
427 e.loaded = oe.loaded;
\r
428 e.total = oe.total;
\r
429 that._onProgress(e, options);
\r
431 options.xhr = function () {
\r
437 _isInstanceOf: function (type, obj) {
\r
438 // Cross-frame instanceof check
\r
439 return Object.prototype.toString.call(obj) === '[object ' + type + ']';
\r
442 _initXHRData: function (options) {
\r
445 file = options.files[0],
\r
446 // Ignore non-multipart setting if not supported:
\r
447 multipart = options.multipart || !$.support.xhrFileUpload,
\r
448 paramName = $.type(options.paramName) === 'array' ?
\r
449 options.paramName[0] : options.paramName;
\r
450 options.headers = $.extend({}, options.headers);
\r
451 if (options.contentRange) {
\r
452 options.headers['Content-Range'] = options.contentRange;
\r
454 if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
\r
455 options.headers['Content-Disposition'] = 'attachment; filename="' +
\r
456 encodeURI(file.name) + '"';
\r
459 options.contentType = file.type || 'application/octet-stream';
\r
460 options.data = options.blob || file;
\r
461 } else if ($.support.xhrFormDataFileUpload) {
\r
462 if (options.postMessage) {
\r
463 // window.postMessage does not allow sending FormData
\r
464 // objects, so we just add the File/Blob objects to
\r
465 // the formData array and let the postMessage window
\r
466 // create the FormData object out of this array:
\r
467 formData = this._getFormData(options);
\r
468 if (options.blob) {
\r
471 value: options.blob
\r
474 $.each(options.files, function (index, file) {
\r
476 name: ($.type(options.paramName) === 'array' &&
\r
477 options.paramName[index]) || paramName,
\r
483 if (that._isInstanceOf('FormData', options.formData)) {
\r
484 formData = options.formData;
\r
486 formData = new FormData();
\r
487 $.each(this._getFormData(options), function (index, field) {
\r
488 formData.append(field.name, field.value);
\r
491 if (options.blob) {
\r
492 formData.append(paramName, options.blob, file.name);
\r
494 $.each(options.files, function (index, file) {
\r
495 // This check allows the tests to run with
\r
497 if (that._isInstanceOf('File', file) ||
\r
498 that._isInstanceOf('Blob', file)) {
\r
500 ($.type(options.paramName) === 'array' &&
\r
501 options.paramName[index]) || paramName,
\r
503 file.uploadName || file.name
\r
509 options.data = formData;
\r
511 // Blob reference is not needed anymore, free memory:
\r
512 options.blob = null;
\r
515 _initIframeSettings: function (options) {
\r
516 var targetHost = $('<a></a>').prop('href', options.url).prop('host');
\r
517 // Setting the dataType to iframe enables the iframe transport:
\r
518 options.dataType = 'iframe ' + (options.dataType || '');
\r
519 // The iframe transport accepts a serialized array as form data:
\r
520 options.formData = this._getFormData(options);
\r
521 // Add redirect url to form data on cross-domain uploads:
\r
522 if (options.redirect && targetHost && targetHost !== location.host) {
\r
523 options.formData.push({
\r
524 name: options.redirectParamName || 'redirect',
\r
525 value: options.redirect
\r
530 _initDataSettings: function (options) {
\r
531 if (this._isXHRUpload(options)) {
\r
532 if (!this._chunkedUpload(options, true)) {
\r
533 if (!options.data) {
\r
534 this._initXHRData(options);
\r
536 this._initProgressListener(options);
\r
538 if (options.postMessage) {
\r
539 // Setting the dataType to postmessage enables the
\r
540 // postMessage transport:
\r
541 options.dataType = 'postmessage ' + (options.dataType || '');
\r
544 this._initIframeSettings(options);
\r
548 _getParamName: function (options) {
\r
549 var fileInput = $(options.fileInput),
\r
550 paramName = options.paramName;
\r
553 fileInput.each(function () {
\r
554 var input = $(this),
\r
555 name = input.prop('name') || 'files[]',
\r
556 i = (input.prop('files') || [1]).length;
\r
558 paramName.push(name);
\r
562 if (!paramName.length) {
\r
563 paramName = [fileInput.prop('name') || 'files[]'];
\r
565 } else if (!$.isArray(paramName)) {
\r
566 paramName = [paramName];
\r
571 _initFormSettings: function (options) {
\r
572 // Retrieve missing options from the input field and the
\r
573 // associated form, if available:
\r
574 if (!options.form || !options.form.length) {
\r
575 options.form = $(options.fileInput.prop('form'));
\r
576 // If the given file input doesn't have an associated form,
\r
577 // use the default widget file input's form:
\r
578 if (!options.form.length) {
\r
579 options.form = $(this.options.fileInput.prop('form'));
\r
582 options.paramName = this._getParamName(options);
\r
583 if (!options.url) {
\r
584 options.url = options.form.prop('action') || location.href;
\r
586 // The HTTP request method must be "POST" or "PUT":
\r
587 options.type = (options.type ||
\r
588 ($.type(options.form.prop('method')) === 'string' &&
\r
589 options.form.prop('method')) || ''
\r
591 if (options.type !== 'POST' && options.type !== 'PUT' &&
\r
592 options.type !== 'PATCH') {
\r
593 options.type = 'POST';
\r
595 if (!options.formAcceptCharset) {
\r
596 options.formAcceptCharset = options.form.attr('accept-charset');
\r
600 _getAJAXSettings: function (data) {
\r
601 var options = $.extend({}, this.options, data);
\r
602 this._initFormSettings(options);
\r
603 this._initDataSettings(options);
\r
607 // jQuery 1.6 doesn't provide .state(),
\r
608 // while jQuery 1.8+ removed .isRejected() and .isResolved():
\r
609 _getDeferredState: function (deferred) {
\r
610 if (deferred.state) {
\r
611 return deferred.state();
\r
613 if (deferred.isResolved()) {
\r
616 if (deferred.isRejected()) {
\r
622 // Maps jqXHR callbacks to the equivalent
\r
623 // methods of the given Promise object:
\r
624 _enhancePromise: function (promise) {
\r
625 promise.success = promise.done;
\r
626 promise.error = promise.fail;
\r
627 promise.complete = promise.always;
\r
631 // Creates and returns a Promise object enhanced with
\r
632 // the jqXHR methods abort, success, error and complete:
\r
633 _getXHRPromise: function (resolveOrReject, context, args) {
\r
634 var dfd = $.Deferred(),
\r
635 promise = dfd.promise();
\r
636 context = context || this.options.context || promise;
\r
637 if (resolveOrReject === true) {
\r
638 dfd.resolveWith(context, args);
\r
639 } else if (resolveOrReject === false) {
\r
640 dfd.rejectWith(context, args);
\r
642 promise.abort = dfd.promise;
\r
643 return this._enhancePromise(promise);
\r
646 // Adds convenience methods to the data callback argument:
\r
647 _addConvenienceMethods: function (e, data) {
\r
649 getPromise = function (args) {
\r
650 return $.Deferred().resolveWith(that, args).promise();
\r
652 data.process = function (resolveFunc, rejectFunc) {
\r
653 if (resolveFunc || rejectFunc) {
\r
654 data._processQueue = this._processQueue =
\r
655 (this._processQueue || getPromise([this])).pipe(
\r
657 if (data.errorThrown) {
\r
658 return $.Deferred()
\r
659 .rejectWith(that, [data]).promise();
\r
661 return getPromise(arguments);
\r
663 ).pipe(resolveFunc, rejectFunc);
\r
665 return this._processQueue || getPromise([this]);
\r
667 data.submit = function () {
\r
668 if (this.state() !== 'pending') {
\r
669 data.jqXHR = this.jqXHR =
\r
672 $.Event('submit', {delegatedEvent: e}),
\r
674 ) !== false) && that._onSend(e, this);
\r
676 return this.jqXHR || that._getXHRPromise();
\r
678 data.abort = function () {
\r
680 return this.jqXHR.abort();
\r
682 this.errorThrown = 'abort';
\r
683 that._trigger('fail', null, this);
\r
684 return that._getXHRPromise(false);
\r
686 data.state = function () {
\r
688 return that._getDeferredState(this.jqXHR);
\r
690 if (this._processQueue) {
\r
691 return that._getDeferredState(this._processQueue);
\r
694 data.processing = function () {
\r
695 return !this.jqXHR && this._processQueue && that
\r
696 ._getDeferredState(this._processQueue) === 'pending';
\r
698 data.progress = function () {
\r
699 return this._progress;
\r
701 data.response = function () {
\r
702 return this._response;
\r
706 // Parses the Range header from the server response
\r
707 // and returns the uploaded bytes:
\r
708 _getUploadedBytes: function (jqXHR) {
\r
709 var range = jqXHR.getResponseHeader('Range'),
\r
710 parts = range && range.split('-'),
\r
711 upperBytesPos = parts && parts.length > 1 &&
\r
712 parseInt(parts[1], 10);
\r
713 return upperBytesPos && upperBytesPos + 1;
\r
716 // Uploads a file in multiple, sequential requests
\r
717 // by splitting the file up in multiple blob chunks.
\r
718 // If the second parameter is true, only tests if the file
\r
719 // should be uploaded in chunks, but does not invoke any
\r
720 // upload requests:
\r
721 _chunkedUpload: function (options, testOnly) {
\r
722 options.uploadedBytes = options.uploadedBytes || 0;
\r
724 file = options.files[0],
\r
726 ub = options.uploadedBytes,
\r
727 mcs = options.maxChunkSize || fs,
\r
728 slice = this._blobSlice,
\r
729 dfd = $.Deferred(),
\r
730 promise = dfd.promise(),
\r
733 if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
\r
741 file.error = options.i18n('uploadedBytes');
\r
742 return this._getXHRPromise(
\r
745 [null, 'error', file.error]
\r
748 // The chunk upload method:
\r
749 upload = function () {
\r
750 // Clone the options object for each chunk upload:
\r
751 var o = $.extend({}, options),
\r
752 currentLoaded = o._progress.loaded;
\r
753 o.blob = slice.call(
\r
759 // Store the current chunk size, as the blob itself
\r
760 // will be dereferenced after data processing:
\r
761 o.chunkSize = o.blob.size;
\r
762 // Expose the chunk bytes position range:
\r
763 o.contentRange = 'bytes ' + ub + '-' +
\r
764 (ub + o.chunkSize - 1) + '/' + fs;
\r
765 // Process the upload data (the blob and potential form data):
\r
766 that._initXHRData(o);
\r
767 // Add progress listeners for this chunk upload:
\r
768 that._initProgressListener(o);
\r
769 jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
\r
770 that._getXHRPromise(false, o.context))
\r
771 .done(function (result, textStatus, jqXHR) {
\r
772 ub = that._getUploadedBytes(jqXHR) ||
\r
773 (ub + o.chunkSize);
\r
774 // Create a progress event if no final progress event
\r
775 // with loaded equaling total has been triggered
\r
777 if (currentLoaded + o.chunkSize - o._progress.loaded) {
\r
778 that._onProgress($.Event('progress', {
\r
779 lengthComputable: true,
\r
780 loaded: ub - o.uploadedBytes,
\r
781 total: ub - o.uploadedBytes
\r
784 options.uploadedBytes = o.uploadedBytes = ub;
\r
786 o.textStatus = textStatus;
\r
788 that._trigger('chunkdone', null, o);
\r
789 that._trigger('chunkalways', null, o);
\r
791 // File upload not yet complete,
\r
792 // continue with the next chunk:
\r
797 [result, textStatus, jqXHR]
\r
801 .fail(function (jqXHR, textStatus, errorThrown) {
\r
803 o.textStatus = textStatus;
\r
804 o.errorThrown = errorThrown;
\r
805 that._trigger('chunkfail', null, o);
\r
806 that._trigger('chunkalways', null, o);
\r
809 [jqXHR, textStatus, errorThrown]
\r
813 this._enhancePromise(promise);
\r
814 promise.abort = function () {
\r
815 return jqXHR.abort();
\r
821 _beforeSend: function (e, data) {
\r
822 if (this._active === 0) {
\r
823 // the start callback is triggered when an upload starts
\r
824 // and no other uploads are currently running,
\r
825 // equivalent to the global ajaxStart event:
\r
826 this._trigger('start');
\r
827 // Set timer for global bitrate progress calculation:
\r
828 this._bitrateTimer = new this._BitrateTimer();
\r
829 // Reset the global progress values:
\r
830 this._progress.loaded = this._progress.total = 0;
\r
831 this._progress.bitrate = 0;
\r
833 // Make sure the container objects for the .response() and
\r
834 // .progress() methods on the data object are available
\r
835 // and reset to their initial state:
\r
836 this._initResponseObject(data);
\r
837 this._initProgressObject(data);
\r
838 data._progress.loaded = data.loaded = data.uploadedBytes || 0;
\r
839 data._progress.total = data.total = this._getTotal(data.files) || 1;
\r
840 data._progress.bitrate = data.bitrate = 0;
\r
842 // Initialize the global progress values:
\r
843 this._progress.loaded += data.loaded;
\r
844 this._progress.total += data.total;
\r
847 _onDone: function (result, textStatus, jqXHR, options) {
\r
848 var total = options._progress.total,
\r
849 response = options._response;
\r
850 if (options._progress.loaded < total) {
\r
851 // Create a progress event if no final progress event
\r
852 // with loaded equaling total has been triggered:
\r
853 this._onProgress($.Event('progress', {
\r
854 lengthComputable: true,
\r
859 response.result = options.result = result;
\r
860 response.textStatus = options.textStatus = textStatus;
\r
861 response.jqXHR = options.jqXHR = jqXHR;
\r
862 this._trigger('done', null, options);
\r
865 _onFail: function (jqXHR, textStatus, errorThrown, options) {
\r
866 var response = options._response;
\r
867 if (options.recalculateProgress) {
\r
868 // Remove the failed (error or abort) file upload from
\r
869 // the global progress calculation:
\r
870 this._progress.loaded -= options._progress.loaded;
\r
871 this._progress.total -= options._progress.total;
\r
873 response.jqXHR = options.jqXHR = jqXHR;
\r
874 response.textStatus = options.textStatus = textStatus;
\r
875 response.errorThrown = options.errorThrown = errorThrown;
\r
876 this._trigger('fail', null, options);
\r
879 _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
\r
880 // jqXHRorResult, textStatus and jqXHRorError are added to the
\r
881 // options object via done and fail callbacks
\r
882 this._trigger('always', null, options);
\r
885 _onSend: function (e, data) {
\r
886 if (!data.submit) {
\r
887 this._addConvenienceMethods(e, data);
\r
894 options = that._getAJAXSettings(data),
\r
895 send = function () {
\r
896 that._sending += 1;
\r
897 // Set timer for bitrate progress calculation:
\r
898 options._bitrateTimer = new that._BitrateTimer();
\r
900 ((aborted || that._trigger(
\r
902 $.Event('send', {delegatedEvent: e}),
\r
905 that._getXHRPromise(false, options.context, aborted)) ||
\r
906 that._chunkedUpload(options) || $.ajax(options)
\r
907 ).done(function (result, textStatus, jqXHR) {
\r
908 that._onDone(result, textStatus, jqXHR, options);
\r
909 }).fail(function (jqXHR, textStatus, errorThrown) {
\r
910 that._onFail(jqXHR, textStatus, errorThrown, options);
\r
911 }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
\r
918 that._sending -= 1;
\r
920 if (options.limitConcurrentUploads &&
\r
921 options.limitConcurrentUploads > that._sending) {
\r
922 // Start the next queued upload,
\r
923 // that has not been aborted:
\r
924 var nextSlot = that._slots.shift();
\r
926 if (that._getDeferredState(nextSlot) === 'pending') {
\r
927 nextSlot.resolve();
\r
930 nextSlot = that._slots.shift();
\r
933 if (that._active === 0) {
\r
934 // The stop callback is triggered when all uploads have
\r
935 // been completed, equivalent to the global ajaxStop event:
\r
936 that._trigger('stop');
\r
941 this._beforeSend(e, options);
\r
942 if (this.options.sequentialUploads ||
\r
943 (this.options.limitConcurrentUploads &&
\r
944 this.options.limitConcurrentUploads <= this._sending)) {
\r
945 if (this.options.limitConcurrentUploads > 1) {
\r
946 slot = $.Deferred();
\r
947 this._slots.push(slot);
\r
948 pipe = slot.pipe(send);
\r
950 this._sequence = this._sequence.pipe(send, send);
\r
951 pipe = this._sequence;
\r
953 // Return the piped Promise object, enhanced with an abort method,
\r
954 // which is delegated to the jqXHR object of the current upload,
\r
955 // and jqXHR callbacks mapped to the equivalent Promise methods:
\r
956 pipe.abort = function () {
\r
957 aborted = [undefined, 'abort', 'abort'];
\r
960 slot.rejectWith(options.context, aborted);
\r
964 return jqXHR.abort();
\r
966 return this._enhancePromise(pipe);
\r
971 _onAdd: function (e, data) {
\r
974 options = $.extend({}, this.options, data),
\r
975 files = data.files,
\r
976 filesLength = files.length,
\r
977 limit = options.limitMultiFileUploads,
\r
978 limitSize = options.limitMultiFileUploadSize,
\r
979 overhead = options.limitMultiFileUploadSizeOverhead,
\r
981 paramName = this._getParamName(options),
\r
987 if (!filesLength) {
\r
990 if (limitSize && files[0].size === undefined) {
\r
991 limitSize = undefined;
\r
993 if (!(options.singleFileUploads || limit || limitSize) ||
\r
994 !this._isXHRUpload(options)) {
\r
996 paramNameSet = [paramName];
\r
997 } else if (!(options.singleFileUploads || limitSize) && limit) {
\r
1000 for (i = 0; i < filesLength; i += limit) {
\r
1001 fileSet.push(files.slice(i, i + limit));
\r
1002 paramNameSlice = paramName.slice(i, i + limit);
\r
1003 if (!paramNameSlice.length) {
\r
1004 paramNameSlice = paramName;
\r
1006 paramNameSet.push(paramNameSlice);
\r
1008 } else if (!options.singleFileUploads && limitSize) {
\r
1010 paramNameSet = [];
\r
1011 for (i = 0; i < filesLength; i = i + 1) {
\r
1012 batchSize += files[i].size + overhead;
\r
1013 if (i + 1 === filesLength ||
\r
1014 ((batchSize + files[i + 1].size + overhead) > limitSize) ||
\r
1015 (limit && i + 1 - j >= limit)) {
\r
1016 fileSet.push(files.slice(j, i + 1));
\r
1017 paramNameSlice = paramName.slice(j, i + 1);
\r
1018 if (!paramNameSlice.length) {
\r
1019 paramNameSlice = paramName;
\r
1021 paramNameSet.push(paramNameSlice);
\r
1027 paramNameSet = paramName;
\r
1029 data.originalFiles = files;
\r
1030 $.each(fileSet || files, function (index, element) {
\r
1031 var newData = $.extend({}, data);
\r
1032 newData.files = fileSet ? element : [element];
\r
1033 newData.paramName = paramNameSet[index];
\r
1034 that._initResponseObject(newData);
\r
1035 that._initProgressObject(newData);
\r
1036 that._addConvenienceMethods(e, newData);
\r
1037 result = that._trigger(
\r
1039 $.Event('add', {delegatedEvent: e}),
\r
1047 _replaceFileInput: function (data) {
\r
1048 var input = data.fileInput,
\r
1049 inputClone = input.clone(true),
\r
1050 restoreFocus = input.is(document.activeElement);
\r
1051 // Add a reference for the new cloned file input to the data argument:
\r
1052 data.fileInputClone = inputClone;
\r
1053 $('<form></form>').append(inputClone)[0].reset();
\r
1054 // Detaching allows to insert the fileInput on another form
\r
1055 // without loosing the file input value:
\r
1056 input.after(inputClone).detach();
\r
1057 // If the fileInput had focus before it was detached,
\r
1058 // restore focus to the inputClone.
\r
1059 if (restoreFocus) {
\r
1060 inputClone.focus();
\r
1062 // Avoid memory leaks with the detached file input:
\r
1063 $.cleanData(input.unbind('remove'));
\r
1064 // Replace the original file input element in the fileInput
\r
1065 // elements set with the clone, which has been copied including
\r
1066 // event handlers:
\r
1067 this.options.fileInput = this.options.fileInput.map(function (i, el) {
\r
1068 if (el === input[0]) {
\r
1069 return inputClone[0];
\r
1073 // If the widget has been initialized on the file input itself,
\r
1074 // override this.element with the file input clone:
\r
1075 if (input[0] === this.element[0]) {
\r
1076 this.element = inputClone;
\r
1080 _handleFileTreeEntry: function (entry, path) {
\r
1082 dfd = $.Deferred(),
\r
1083 errorHandler = function (e) {
\r
1084 if (e && !e.entry) {
\r
1087 // Since $.when returns immediately if one
\r
1088 // Deferred is rejected, we use resolve instead.
\r
1089 // This allows valid files and invalid items
\r
1090 // to be returned together in one set:
\r
1093 successHandler = function (entries) {
\r
1094 that._handleFileTreeEntries(
\r
1096 path + entry.name + '/'
\r
1097 ).done(function (files) {
\r
1098 dfd.resolve(files);
\r
1099 }).fail(errorHandler);
\r
1101 readEntries = function () {
\r
1102 dirReader.readEntries(function (results) {
\r
1103 if (!results.length) {
\r
1104 successHandler(entries);
\r
1106 entries = entries.concat(results);
\r
1111 dirReader, entries = [];
\r
1112 path = path || '';
\r
1113 if (entry.isFile) {
\r
1114 if (entry._file) {
\r
1115 // Workaround for Chrome bug #149735
\r
1116 entry._file.relativePath = path;
\r
1117 dfd.resolve(entry._file);
\r
1119 entry.file(function (file) {
\r
1120 file.relativePath = path;
\r
1121 dfd.resolve(file);
\r
1124 } else if (entry.isDirectory) {
\r
1125 dirReader = entry.createReader();
\r
1128 // Return an empy list for file system items
\r
1129 // other than files or directories:
\r
1132 return dfd.promise();
\r
1135 _handleFileTreeEntries: function (entries, path) {
\r
1137 return $.when.apply(
\r
1139 $.map(entries, function (entry) {
\r
1140 return that._handleFileTreeEntry(entry, path);
\r
1142 ).pipe(function () {
\r
1143 return Array.prototype.concat.apply(
\r
1150 _getDroppedFiles: function (dataTransfer) {
\r
1151 dataTransfer = dataTransfer || {};
\r
1152 var items = dataTransfer.items;
\r
1153 if (items && items.length && (items[0].webkitGetAsEntry ||
\r
1154 items[0].getAsEntry)) {
\r
1155 return this._handleFileTreeEntries(
\r
1156 $.map(items, function (item) {
\r
1158 if (item.webkitGetAsEntry) {
\r
1159 entry = item.webkitGetAsEntry();
\r
1161 // Workaround for Chrome bug #149735:
\r
1162 entry._file = item.getAsFile();
\r
1166 return item.getAsEntry();
\r
1170 return $.Deferred().resolve(
\r
1171 $.makeArray(dataTransfer.files)
\r
1175 _getSingleFileInputFiles: function (fileInput) {
\r
1176 fileInput = $(fileInput);
\r
1177 var entries = fileInput.prop('webkitEntries') ||
\r
1178 fileInput.prop('entries'),
\r
1181 if (entries && entries.length) {
\r
1182 return this._handleFileTreeEntries(entries);
\r
1184 files = $.makeArray(fileInput.prop('files'));
\r
1185 if (!files.length) {
\r
1186 value = fileInput.prop('value');
\r
1188 return $.Deferred().resolve([]).promise();
\r
1190 // If the files property is not available, the browser does not
\r
1191 // support the File API and we add a pseudo File object with
\r
1192 // the input value as name with path information removed:
\r
1193 files = [{name: value.replace(/^.*\\/, '')}];
\r
1194 } else if (files[0].name === undefined && files[0].fileName) {
\r
1195 // File normalization for Safari 4 and Firefox 3:
\r
1196 $.each(files, function (index, file) {
\r
1197 file.name = file.fileName;
\r
1198 file.size = file.fileSize;
\r
1201 return $.Deferred().resolve(files).promise();
\r
1204 _getFileInputFiles: function (fileInput) {
\r
1205 if (!(fileInput instanceof $) || fileInput.length === 1) {
\r
1206 return this._getSingleFileInputFiles(fileInput);
\r
1208 return $.when.apply(
\r
1210 $.map(fileInput, this._getSingleFileInputFiles)
\r
1211 ).pipe(function () {
\r
1212 return Array.prototype.concat.apply(
\r
1219 _onChange: function (e) {
\r
1222 fileInput: $(e.target),
\r
1223 form: $(e.target.form)
\r
1225 this._getFileInputFiles(data.fileInput).always(function (files) {
\r
1226 data.files = files;
\r
1227 if (that.options.replaceFileInput) {
\r
1228 that._replaceFileInput(data);
\r
1230 if (that._trigger(
\r
1232 $.Event('change', {delegatedEvent: e}),
\r
1235 that._onAdd(e, data);
\r
1240 _onPaste: function (e) {
\r
1241 var items = e.originalEvent && e.originalEvent.clipboardData &&
\r
1242 e.originalEvent.clipboardData.items,
\r
1243 data = {files: []};
\r
1244 if (items && items.length) {
\r
1245 $.each(items, function (index, item) {
\r
1246 var file = item.getAsFile && item.getAsFile();
\r
1248 data.files.push(file);
\r
1251 if (this._trigger(
\r
1253 $.Event('paste', {delegatedEvent: e}),
\r
1256 this._onAdd(e, data);
\r
1261 _onDrop: function (e) {
\r
1262 e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
\r
1264 dataTransfer = e.dataTransfer,
\r
1266 if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
\r
1267 e.preventDefault();
\r
1268 this._getDroppedFiles(dataTransfer).always(function (files) {
\r
1269 data.files = files;
\r
1270 if (that._trigger(
\r
1272 $.Event('drop', {delegatedEvent: e}),
\r
1275 that._onAdd(e, data);
\r
1281 _onDragOver: getDragHandler('dragover'),
\r
1283 _onDragEnter: getDragHandler('dragenter'),
\r
1285 _onDragLeave: getDragHandler('dragleave'),
\r
1287 _initEventHandlers: function () {
\r
1288 if (this._isXHRUpload(this.options)) {
\r
1289 this._on(this.options.dropZone, {
\r
1290 dragover: this._onDragOver,
\r
1291 drop: this._onDrop,
\r
1292 // event.preventDefault() on dragenter is required for IE10+:
\r
1293 dragenter: this._onDragEnter,
\r
1294 // dragleave is not required, but added for completeness:
\r
1295 dragleave: this._onDragLeave
\r
1297 this._on(this.options.pasteZone, {
\r
1298 paste: this._onPaste
\r
1301 if ($.support.fileInput) {
\r
1302 this._on(this.options.fileInput, {
\r
1303 change: this._onChange
\r
1308 _destroyEventHandlers: function () {
\r
1309 this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
\r
1310 this._off(this.options.pasteZone, 'paste');
\r
1311 this._off(this.options.fileInput, 'change');
\r
1314 _setOption: function (key, value) {
\r
1315 var reinit = $.inArray(key, this._specialOptions) !== -1;
\r
1317 this._destroyEventHandlers();
\r
1319 this._super(key, value);
\r
1321 this._initSpecialOptions();
\r
1322 this._initEventHandlers();
\r
1326 _initSpecialOptions: function () {
\r
1327 var options = this.options;
\r
1328 if (options.fileInput === undefined) {
\r
1329 options.fileInput = this.element.is('input[type="file"]') ?
\r
1330 this.element : this.element.find('input[type="file"]');
\r
1331 } else if (!(options.fileInput instanceof $)) {
\r
1332 options.fileInput = $(options.fileInput);
\r
1334 if (!(options.dropZone instanceof $)) {
\r
1335 options.dropZone = $(options.dropZone);
\r
1337 if (!(options.pasteZone instanceof $)) {
\r
1338 options.pasteZone = $(options.pasteZone);
\r
1342 _getRegExp: function (str) {
\r
1343 var parts = str.split('/'),
\r
1344 modifiers = parts.pop();
\r
1346 return new RegExp(parts.join('/'), modifiers);
\r
1349 _isRegExpOption: function (key, value) {
\r
1350 return key !== 'url' && $.type(value) === 'string' &&
\r
1351 /^\/.*\/[igm]{0,3}$/.test(value);
\r
1354 _initDataAttributes: function () {
\r
1356 options = this.options,
\r
1357 data = this.element.data();
\r
1358 // Initialize options set via HTML5 data-attributes:
\r
1360 this.element[0].attributes,
\r
1361 function (index, attr) {
\r
1362 var key = attr.name.toLowerCase(),
\r
1364 if (/^data-/.test(key)) {
\r
1365 // Convert hyphen-ated key to camelCase:
\r
1366 key = key.slice(5).replace(/-[a-z]/g, function (str) {
\r
1367 return str.charAt(1).toUpperCase();
\r
1369 value = data[key];
\r
1370 if (that._isRegExpOption(key, value)) {
\r
1371 value = that._getRegExp(value);
\r
1373 options[key] = value;
\r
1379 _create: function () {
\r
1380 this._initDataAttributes();
\r
1381 this._initSpecialOptions();
\r
1383 this._sequence = this._getXHRPromise(true);
\r
1384 this._sending = this._active = 0;
\r
1385 this._initProgressObject(this);
\r
1386 this._initEventHandlers();
\r
1389 // This method is exposed to the widget API and allows to query
\r
1390 // the number of active uploads:
\r
1391 active: function () {
\r
1392 return this._active;
\r
1395 // This method is exposed to the widget API and allows to query
\r
1396 // the widget upload progress.
\r
1397 // It returns an object with loaded, total and bitrate properties
\r
1398 // for the running uploads:
\r
1399 progress: function () {
\r
1400 return this._progress;
\r
1403 // This method is exposed to the widget API and allows adding files
\r
1404 // using the fileupload API. The data parameter accepts an object which
\r
1405 // must have a files property and can contain additional options:
\r
1406 // .fileupload('add', {files: filesList});
\r
1407 add: function (data) {
\r
1409 if (!data || this.options.disabled) {
\r
1412 if (data.fileInput && !data.files) {
\r
1413 this._getFileInputFiles(data.fileInput).always(function (files) {
\r
1414 data.files = files;
\r
1415 that._onAdd(null, data);
\r
1418 data.files = $.makeArray(data.files);
\r
1419 this._onAdd(null, data);
\r
1423 // This method is exposed to the widget API and allows sending files
\r
1424 // using the fileupload API. The data parameter accepts an object which
\r
1425 // must have a files or fileInput property and can contain additional options:
\r
1426 // .fileupload('send', {files: filesList});
\r
1427 // The method returns a Promise object for the file upload call.
\r
1428 send: function (data) {
\r
1429 if (data && !this.options.disabled) {
\r
1430 if (data.fileInput && !data.files) {
\r
1432 dfd = $.Deferred(),
\r
1433 promise = dfd.promise(),
\r
1436 promise.abort = function () {
\r
1439 return jqXHR.abort();
\r
1441 dfd.reject(null, 'abort', 'abort');
\r
1444 this._getFileInputFiles(data.fileInput).always(
\r
1445 function (files) {
\r
1449 if (!files.length) {
\r
1453 data.files = files;
\r
1454 jqXHR = that._onSend(null, data);
\r
1456 function (result, textStatus, jqXHR) {
\r
1457 dfd.resolve(result, textStatus, jqXHR);
\r
1459 function (jqXHR, textStatus, errorThrown) {
\r
1460 dfd.reject(jqXHR, textStatus, errorThrown);
\r
1465 return this._enhancePromise(promise);
\r
1467 data.files = $.makeArray(data.files);
\r
1468 if (data.files.length) {
\r
1469 return this._onSend(null, data);
\r
1472 return this._getXHRPromise(false, data && data.context);
\r