Only commit raw text after OCR for now.
[redakcja.git] / src / fileupload / static / lib / jQuery-File-Upload-10.32.0 / js / jquery.fileupload.js
1 /*
2  * jQuery File Upload Plugin
3  * https://github.com/blueimp/jQuery-File-Upload
4  *
5  * Copyright 2010, Sebastian Tschan
6  * https://blueimp.net
7  *
8  * Licensed under the MIT license:
9  * https://opensource.org/licenses/MIT
10  */
11
12 /* global define, require */
13 /* eslint-disable new-cap */
14
15 (function (factory) {
16   'use strict';
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') {
21     // Node/CommonJS:
22     factory(require('jquery'), require('./vendor/jquery.ui.widget'));
23   } else {
24     // Browser globals:
25     factory(window.jQuery);
26   }
27 })(function ($) {
28   'use strict';
29
30   // Detect file input support, based on
31   // https://viljamis.com/2012/file-upload-support-on-mobile/
32   $.support.fileInput = !(
33     new RegExp(
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')
42   );
43
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;
51
52   // Detect support for Blob slicing (required for chunked uploads):
53   $.support.blobSlice =
54     window.Blob &&
55     (Blob.prototype.slice ||
56       Blob.prototype.webkitSlice ||
57       Blob.prototype.mozSlice);
58
59   /**
60    * Helper function to create drag handlers for dragover/dragenter/dragleave
61    *
62    * @param {string} type Event type
63    * @returns {Function} Drag handler
64    */
65   function getDragHandler(type) {
66     var isDragOver = type === 'dragover';
67     return function (e) {
68       e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
69       var dataTransfer = e.dataTransfer;
70       if (
71         dataTransfer &&
72         $.inArray('Files', dataTransfer.types) !== -1 &&
73         this._trigger(type, $.Event(type, { delegatedEvent: e })) !== false
74       ) {
75         e.preventDefault();
76         if (isDragOver) {
77           dataTransfer.dropEffect = 'copy';
78         }
79       }
80     };
81   }
82
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', {
92     options: {
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:
98       pasteZone: undefined,
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
123       // limit in bytes:
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:
139       redirect: undefined,
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:
149       multipart: true,
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:
170       autoUpload: true,
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.:
178       // "image (2).jpg"
179       uniqueFilenames: undefined,
180
181       // Error and info messages:
182       messages: {
183         uploadedBytes: 'Uploaded bytes exceed file size'
184       },
185
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();
191         if (context) {
192           $.each(context, function (key, value) {
193             // eslint-disable-next-line no-param-reassign
194             message = message.replace('{' + key + '}', value);
195           });
196         }
197         return message;
198       },
199
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();
207       },
208
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.
214       //
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.
218       //
219       // Listeners for this callback can also be bound the following way:
220       // .on('fileuploadadd', func);
221       //
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()) {
227           return false;
228         }
229         if (
230           data.autoUpload ||
231           (data.autoUpload !== false &&
232             $(this).fileupload('option', 'autoUpload'))
233         ) {
234           data.process().done(function () {
235             data.submit();
236           });
237         }
238       },
239
240       // Other callbacks:
241
242       // Callback for the submit event of each file upload:
243       // submit: function (e, data) {}, // .on('fileuploadsubmit', func);
244
245       // Callback for the start of each file upload request:
246       // send: function (e, data) {}, // .on('fileuploadsend', func);
247
248       // Callback for successful uploads:
249       // done: function (e, data) {}, // .on('fileuploaddone', func);
250
251       // Callback for failed (abort or error) uploads:
252       // fail: function (e, data) {}, // .on('fileuploadfail', func);
253
254       // Callback for completed (success, abort or error) requests:
255       // always: function (e, data) {}, // .on('fileuploadalways', func);
256
257       // Callback for upload progress events:
258       // progress: function (e, data) {}, // .on('fileuploadprogress', func);
259
260       // Callback for global upload progress events:
261       // progressall: function (e, data) {}, // .on('fileuploadprogressall', func);
262
263       // Callback for uploads start, equivalent to the global ajaxStart event:
264       // start: function (e) {}, // .on('fileuploadstart', func);
265
266       // Callback for uploads stop, equivalent to the global ajaxStop event:
267       // stop: function (e) {}, // .on('fileuploadstop', func);
268
269       // Callback for change events of the fileInput(s):
270       // change: function (e, data) {}, // .on('fileuploadchange', func);
271
272       // Callback for paste events to the pasteZone(s):
273       // paste: function (e, data) {}, // .on('fileuploadpaste', func);
274
275       // Callback for drop events of the dropZone(s):
276       // drop: function (e, data) {}, // .on('fileuploaddrop', func);
277
278       // Callback for dragover events of the dropZone(s):
279       // dragover: function (e) {}, // .on('fileuploaddragover', func);
280
281       // Callback before the start of each chunk upload request (before form data initialization):
282       // chunkbeforesend: function (e, data) {}, // .on('fileuploadchunkbeforesend', func);
283
284       // Callback for the start of each chunk upload request:
285       // chunksend: function (e, data) {}, // .on('fileuploadchunksend', func);
286
287       // Callback for successful chunk uploads:
288       // chunkdone: function (e, data) {}, // .on('fileuploadchunkdone', func);
289
290       // Callback for failed (abort or error) chunk uploads:
291       // chunkfail: function (e, data) {}, // .on('fileuploadchunkfail', func);
292
293       // Callback for completed (success, abort or error) chunk upload requests:
294       // chunkalways: function (e, data) {}, // .on('fileuploadchunkalways', func);
295
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:
298       processData: false,
299       contentType: false,
300       cache: false,
301       timeout: 0
302     },
303
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';
312     })(),
313
314     // A list of options that require reinitializing event listeners and/or
315     // special initialization code:
316     _specialOptions: [
317       'fileInput',
318       'dropZone',
319       'pasteZone',
320       'multipart',
321       'forceIframeTransport'
322     ],
323
324     _blobSlice:
325       $.support.blobSlice &&
326       function () {
327         var slice = this.slice || this.webkitSlice || this.mozSlice;
328         return slice.apply(this, arguments);
329       },
330
331     _BitrateTimer: function () {
332       this.timestamp = Date.now ? Date.now() : new Date().getTime();
333       this.loaded = 0;
334       this.bitrate = 0;
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;
341         }
342         return this.bitrate;
343       };
344     },
345
346     _isXHRUpload: function (options) {
347       return (
348         !options.forceIframeTransport &&
349         ((!options.multipart && $.support.xhrFileUpload) ||
350           $.support.xhrFormDataFileUpload)
351       );
352     },
353
354     _getFormData: function (options) {
355       var formData;
356       if ($.type(options.formData) === 'function') {
357         return options.formData(options.form);
358       }
359       if ($.isArray(options.formData)) {
360         return options.formData;
361       }
362       if ($.type(options.formData) === 'object') {
363         formData = [];
364         $.each(options.formData, function (name, value) {
365           formData.push({ name: name, value: value });
366         });
367         return formData;
368       }
369       return [];
370     },
371
372     _getTotal: function (files) {
373       var total = 0;
374       $.each(files, function (index, file) {
375         total += file.size || 1;
376       });
377       return total;
378     },
379
380     _initProgressObject: function (obj) {
381       var progress = {
382         loaded: 0,
383         total: 0,
384         bitrate: 0
385       };
386       if (obj._progress) {
387         $.extend(obj._progress, progress);
388       } else {
389         obj._progress = progress;
390       }
391     },
392
393     _initResponseObject: function (obj) {
394       var prop;
395       if (obj._response) {
396         for (prop in obj._response) {
397           if (Object.prototype.hasOwnProperty.call(obj._response, prop)) {
398             delete obj._response[prop];
399           }
400         }
401       } else {
402         obj._response = {};
403       }
404     },
405
406     _onProgress: function (e, data) {
407       if (e.lengthComputable) {
408         var now = Date.now ? Date.now() : new Date().getTime(),
409           loaded;
410         if (
411           data._time &&
412           data.progressInterval &&
413           now - data._time < data.progressInterval &&
414           e.loaded !== e.total
415         ) {
416           return;
417         }
418         data._time = now;
419         loaded =
420           Math.floor(
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(
427           now,
428           this._progress.loaded,
429           data.bitrateInterval
430         );
431         data._progress.loaded = data.loaded = loaded;
432         data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
433           now,
434           loaded,
435           data.bitrateInterval
436         );
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:
440         this._trigger(
441           'progress',
442           $.Event('progress', { delegatedEvent: e }),
443           data
444         );
445         // Trigger a global progress event for all current file uploads,
446         // including ajax calls queued for sequential file uploads:
447         this._trigger(
448           'progressall',
449           $.Event('progressall', { delegatedEvent: e }),
450           this._progress
451         );
452       }
453     },
454
455     _initProgressListener: function (options) {
456       var that = this,
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:
460       if (xhr.upload) {
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;
466           e.total = oe.total;
467           that._onProgress(e, options);
468         });
469         options.xhr = function () {
470           return xhr;
471         };
472       }
473     },
474
475     _deinitProgressListener: function (options) {
476       var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
477       if (xhr.upload) {
478         $(xhr.upload).off('progress');
479       }
480     },
481
482     _isInstanceOf: function (type, obj) {
483       // Cross-frame instanceof check
484       return Object.prototype.toString.call(obj) === '[object ' + type + ']';
485     },
486
487     _getUniqueFilename: function (name, map) {
488       // eslint-disable-next-line no-param-reassign
489       name = String(name);
490       if (map[name]) {
491         // eslint-disable-next-line no-param-reassign
492         name = name.replace(
493           /(?: \(([\d]+)\))?(\.[^.]+)?$/,
494           function (_, p1, p2) {
495             var index = p1 ? Number(p1) + 1 : 1;
496             var ext = p2 || '';
497             return ' (' + index + ')' + ext;
498           }
499         );
500         return this._getUniqueFilename(name, map);
501       }
502       map[name] = true;
503       return name;
504     },
505
506     _initXHRData: function (options) {
507       var that = this,
508         formData,
509         file = options.files[0],
510         // Ignore non-multipart setting if not supported:
511         multipart = options.multipart || !$.support.xhrFileUpload,
512         paramName =
513           $.type(options.paramName) === 'array'
514             ? options.paramName[0]
515             : options.paramName;
516       options.headers = $.extend({}, options.headers);
517       if (options.contentRange) {
518         options.headers['Content-Range'] = options.contentRange;
519       }
520       if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
521         options.headers['Content-Disposition'] =
522           'attachment; filename="' +
523           encodeURI(file.uploadName || file.name) +
524           '"';
525       }
526       if (!multipart) {
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);
536           if (options.blob) {
537             formData.push({
538               name: paramName,
539               value: options.blob
540             });
541           } else {
542             $.each(options.files, function (index, file) {
543               formData.push({
544                 name:
545                   ($.type(options.paramName) === 'array' &&
546                     options.paramName[index]) ||
547                   paramName,
548                 value: file
549               });
550             });
551           }
552         } else {
553           if (that._isInstanceOf('FormData', options.formData)) {
554             formData = options.formData;
555           } else {
556             formData = new FormData();
557             $.each(this._getFormData(options), function (index, field) {
558               formData.append(field.name, field.value);
559             });
560           }
561           if (options.blob) {
562             formData.append(
563               paramName,
564               options.blob,
565               file.uploadName || file.name
566             );
567           } else {
568             $.each(options.files, function (index, file) {
569               // This check allows the tests to run with
570               // dummy objects:
571               if (
572                 that._isInstanceOf('File', file) ||
573                 that._isInstanceOf('Blob', file)
574               ) {
575                 var fileName = file.uploadName || file.name;
576                 if (options.uniqueFilenames) {
577                   fileName = that._getUniqueFilename(
578                     fileName,
579                     options.uniqueFilenames
580                   );
581                 }
582                 formData.append(
583                   ($.type(options.paramName) === 'array' &&
584                     options.paramName[index]) ||
585                     paramName,
586                   file,
587                   fileName
588                 );
589               }
590             });
591           }
592         }
593         options.data = formData;
594       }
595       // Blob reference is not needed anymore, free memory:
596       options.blob = null;
597     },
598
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
610         });
611       }
612     },
613
614     _initDataSettings: function (options) {
615       if (this._isXHRUpload(options)) {
616         if (!this._chunkedUpload(options, true)) {
617           if (!options.data) {
618             this._initXHRData(options);
619           }
620           this._initProgressListener(options);
621         }
622         if (options.postMessage) {
623           // Setting the dataType to postmessage enables the
624           // postMessage transport:
625           options.dataType = 'postmessage ' + (options.dataType || '');
626         }
627       } else {
628         this._initIframeSettings(options);
629       }
630     },
631
632     _getParamName: function (options) {
633       var fileInput = $(options.fileInput),
634         paramName = options.paramName;
635       if (!paramName) {
636         paramName = [];
637         fileInput.each(function () {
638           var input = $(this),
639             name = input.prop('name') || 'files[]',
640             i = (input.prop('files') || [1]).length;
641           while (i) {
642             paramName.push(name);
643             i -= 1;
644           }
645         });
646         if (!paramName.length) {
647           paramName = [fileInput.prop('name') || 'files[]'];
648         }
649       } else if (!$.isArray(paramName)) {
650         paramName = [paramName];
651       }
652       return paramName;
653     },
654
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'));
664         }
665       }
666       options.paramName = this._getParamName(options);
667       if (!options.url) {
668         options.url = options.form.prop('action') || location.href;
669       }
670       // The HTTP request method must be "POST" or "PUT":
671       options.type = (
672         options.type ||
673         ($.type(options.form.prop('method')) === 'string' &&
674           options.form.prop('method')) ||
675         ''
676       ).toUpperCase();
677       if (
678         options.type !== 'POST' &&
679         options.type !== 'PUT' &&
680         options.type !== 'PATCH'
681       ) {
682         options.type = 'POST';
683       }
684       if (!options.formAcceptCharset) {
685         options.formAcceptCharset = options.form.attr('accept-charset');
686       }
687     },
688
689     _getAJAXSettings: function (data) {
690       var options = $.extend({}, this.options, data);
691       this._initFormSettings(options);
692       this._initDataSettings(options);
693       return options;
694     },
695
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();
701       }
702       if (deferred.isResolved()) {
703         return 'resolved';
704       }
705       if (deferred.isRejected()) {
706         return 'rejected';
707       }
708       return 'pending';
709     },
710
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;
717       return promise;
718     },
719
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);
731       }
732       promise.abort = dfd.promise;
733       return this._enhancePromise(promise);
734     },
735
736     // Adds convenience methods to the data callback argument:
737     _addConvenienceMethods: function (e, data) {
738       var that = this,
739         getPromise = function (args) {
740           return $.Deferred().resolveWith(that, args).promise();
741         };
742       data.process = function (resolveFunc, rejectFunc) {
743         if (resolveFunc || rejectFunc) {
744           data._processQueue = this._processQueue = (this._processQueue ||
745             getPromise([this]))
746             [that._promisePipe](function () {
747               if (data.errorThrown) {
748                 return $.Deferred().rejectWith(that, [data]).promise();
749               }
750               return getPromise(arguments);
751             })
752             [that._promisePipe](resolveFunc, rejectFunc);
753         }
754         return this._processQueue || getPromise([this]);
755       };
756       data.submit = function () {
757         if (this.state() !== 'pending') {
758           data.jqXHR = this.jqXHR =
759             that._trigger(
760               'submit',
761               $.Event('submit', { delegatedEvent: e }),
762               this
763             ) !== false && that._onSend(e, this);
764         }
765         return this.jqXHR || that._getXHRPromise();
766       };
767       data.abort = function () {
768         if (this.jqXHR) {
769           return this.jqXHR.abort();
770         }
771         this.errorThrown = 'abort';
772         that._trigger('fail', null, this);
773         return that._getXHRPromise(false);
774       };
775       data.state = function () {
776         if (this.jqXHR) {
777           return that._getDeferredState(this.jqXHR);
778         }
779         if (this._processQueue) {
780           return that._getDeferredState(this._processQueue);
781         }
782       };
783       data.processing = function () {
784         return (
785           !this.jqXHR &&
786           this._processQueue &&
787           that._getDeferredState(this._processQueue) === 'pending'
788         );
789       };
790       data.progress = function () {
791         return this._progress;
792       };
793       data.response = function () {
794         return this._response;
795       };
796     },
797
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;
805     },
806
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
811     // upload requests:
812     _chunkedUpload: function (options, testOnly) {
813       options.uploadedBytes = options.uploadedBytes || 0;
814       var that = this,
815         file = options.files[0],
816         fs = file.size,
817         ub = options.uploadedBytes,
818         mcs = options.maxChunkSize || fs,
819         slice = this._blobSlice,
820         dfd = $.Deferred(),
821         promise = dfd.promise(),
822         jqXHR,
823         upload;
824       if (
825         !(
826           this._isXHRUpload(options) &&
827           slice &&
828           (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs)
829         ) ||
830         options.data
831       ) {
832         return false;
833       }
834       if (testOnly) {
835         return true;
836       }
837       if (ub >= fs) {
838         file.error = options.i18n('uploadedBytes');
839         return this._getXHRPromise(false, options.context, [
840           null,
841           'error',
842           file.error
843         ]);
844       }
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;
850         o.blob = slice.call(
851           file,
852           ub,
853           ub + ($.type(mcs) === 'function' ? mcs(o) : mcs),
854           file.type
855         );
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:
860         o.contentRange =
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);
868         jqXHR = (
869           (that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
870           that._getXHRPromise(false, o.context)
871         )
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
876             // for this chunk:
877             if (currentLoaded + o.chunkSize - o._progress.loaded) {
878               that._onProgress(
879                 $.Event('progress', {
880                   lengthComputable: true,
881                   loaded: ub - o.uploadedBytes,
882                   total: ub - o.uploadedBytes
883                 }),
884                 o
885               );
886             }
887             options.uploadedBytes = o.uploadedBytes = ub;
888             o.result = result;
889             o.textStatus = textStatus;
890             o.jqXHR = jqXHR;
891             that._trigger('chunkdone', null, o);
892             that._trigger('chunkalways', null, o);
893             if (ub < fs) {
894               // File upload not yet complete,
895               // continue with the next chunk:
896               upload();
897             } else {
898               dfd.resolveWith(o.context, [result, textStatus, jqXHR]);
899             }
900           })
901           .fail(function (jqXHR, textStatus, errorThrown) {
902             o.jqXHR = jqXHR;
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]);
908           })
909           .always(function () {
910             that._deinitProgressListener(o);
911           });
912       };
913       this._enhancePromise(promise);
914       promise.abort = function () {
915         return jqXHR.abort();
916       };
917       upload();
918       return promise;
919     },
920
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;
932       }
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;
941       this._active += 1;
942       // Initialize the global progress values:
943       this._progress.loaded += data.loaded;
944       this._progress.total += data.total;
945     },
946
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:
953         this._onProgress(
954           $.Event('progress', {
955             lengthComputable: true,
956             loaded: total,
957             total: total
958           }),
959           options
960         );
961       }
962       response.result = options.result = result;
963       response.textStatus = options.textStatus = textStatus;
964       response.jqXHR = options.jqXHR = jqXHR;
965       this._trigger('done', null, options);
966     },
967
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;
975       }
976       response.jqXHR = options.jqXHR = jqXHR;
977       response.textStatus = options.textStatus = textStatus;
978       response.errorThrown = options.errorThrown = errorThrown;
979       this._trigger('fail', null, options);
980     },
981
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);
986     },
987
988     _onSend: function (e, data) {
989       if (!data.submit) {
990         this._addConvenienceMethods(e, data);
991       }
992       var that = this,
993         jqXHR,
994         aborted,
995         slot,
996         pipe,
997         options = that._getAJAXSettings(data),
998         send = function () {
999           that._sending += 1;
1000           // Set timer for bitrate progress calculation:
1001           options._bitrateTimer = new that._BitrateTimer();
1002           jqXHR =
1003             jqXHR ||
1004             (
1005               ((aborted ||
1006                 that._trigger(
1007                   'send',
1008                   $.Event('send', { delegatedEvent: e }),
1009                   options
1010                 ) === false) &&
1011                 that._getXHRPromise(false, options.context, aborted)) ||
1012               that._chunkedUpload(options) ||
1013               $.ajax(options)
1014             )
1015               .done(function (result, textStatus, jqXHR) {
1016                 that._onDone(result, textStatus, jqXHR, options);
1017               })
1018               .fail(function (jqXHR, textStatus, errorThrown) {
1019                 that._onFail(jqXHR, textStatus, errorThrown, options);
1020               })
1021               .always(function (jqXHRorResult, textStatus, jqXHRorError) {
1022                 that._deinitProgressListener(options);
1023                 that._onAlways(
1024                   jqXHRorResult,
1025                   textStatus,
1026                   jqXHRorError,
1027                   options
1028                 );
1029                 that._sending -= 1;
1030                 that._active -= 1;
1031                 if (
1032                   options.limitConcurrentUploads &&
1033                   options.limitConcurrentUploads > that._sending
1034                 ) {
1035                   // Start the next queued upload,
1036                   // that has not been aborted:
1037                   var nextSlot = that._slots.shift();
1038                   while (nextSlot) {
1039                     if (that._getDeferredState(nextSlot) === 'pending') {
1040                       nextSlot.resolve();
1041                       break;
1042                     }
1043                     nextSlot = that._slots.shift();
1044                   }
1045                 }
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');
1050                 }
1051               });
1052           return jqXHR;
1053         };
1054       this._beforeSend(e, options);
1055       if (
1056         this.options.sequentialUploads ||
1057         (this.options.limitConcurrentUploads &&
1058           this.options.limitConcurrentUploads <= this._sending)
1059       ) {
1060         if (this.options.limitConcurrentUploads > 1) {
1061           slot = $.Deferred();
1062           this._slots.push(slot);
1063           pipe = slot[that._promisePipe](send);
1064         } else {
1065           this._sequence = this._sequence[that._promisePipe](send, send);
1066           pipe = this._sequence;
1067         }
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'];
1073           if (!jqXHR) {
1074             if (slot) {
1075               slot.rejectWith(options.context, aborted);
1076             }
1077             return send();
1078           }
1079           return jqXHR.abort();
1080         };
1081         return this._enhancePromise(pipe);
1082       }
1083       return send();
1084     },
1085
1086     _onAdd: function (e, data) {
1087       var that = this,
1088         result = true,
1089         options = $.extend({}, this.options, data),
1090         files = data.files,
1091         filesLength = files.length,
1092         limit = options.limitMultiFileUploads,
1093         limitSize = options.limitMultiFileUploadSize,
1094         overhead = options.limitMultiFileUploadSizeOverhead,
1095         batchSize = 0,
1096         paramName = this._getParamName(options),
1097         paramNameSet,
1098         paramNameSlice,
1099         fileSet,
1100         i,
1101         j = 0;
1102       if (!filesLength) {
1103         return false;
1104       }
1105       if (limitSize && files[0].size === undefined) {
1106         limitSize = undefined;
1107       }
1108       if (
1109         !(options.singleFileUploads || limit || limitSize) ||
1110         !this._isXHRUpload(options)
1111       ) {
1112         fileSet = [files];
1113         paramNameSet = [paramName];
1114       } else if (!(options.singleFileUploads || limitSize) && limit) {
1115         fileSet = [];
1116         paramNameSet = [];
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;
1122           }
1123           paramNameSet.push(paramNameSlice);
1124         }
1125       } else if (!options.singleFileUploads && limitSize) {
1126         fileSet = [];
1127         paramNameSet = [];
1128         for (i = 0; i < filesLength; i = i + 1) {
1129           batchSize += files[i].size + overhead;
1130           if (
1131             i + 1 === filesLength ||
1132             batchSize + files[i + 1].size + overhead > limitSize ||
1133             (limit && i + 1 - j >= limit)
1134           ) {
1135             fileSet.push(files.slice(j, i + 1));
1136             paramNameSlice = paramName.slice(j, i + 1);
1137             if (!paramNameSlice.length) {
1138               paramNameSlice = paramName;
1139             }
1140             paramNameSet.push(paramNameSlice);
1141             j = i + 1;
1142             batchSize = 0;
1143           }
1144         }
1145       } else {
1146         paramNameSet = paramName;
1147       }
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(
1157           'add',
1158           $.Event('add', { delegatedEvent: e }),
1159           newData
1160         );
1161         return result;
1162       });
1163       return result;
1164     },
1165
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.
1178       if (restoreFocus) {
1179         inputClone.trigger('focus');
1180       }
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
1185       // event handlers:
1186       this.options.fileInput = this.options.fileInput.map(function (i, el) {
1187         if (el === input[0]) {
1188           return inputClone[0];
1189         }
1190         return el;
1191       });
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;
1196       }
1197     },
1198
1199     _handleFileTreeEntry: function (entry, path) {
1200       var that = this,
1201         dfd = $.Deferred(),
1202         entries = [],
1203         dirReader,
1204         errorHandler = function (e) {
1205           if (e && !e.entry) {
1206             e.entry = entry;
1207           }
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:
1212           dfd.resolve([e]);
1213         },
1214         successHandler = function (entries) {
1215           that
1216             ._handleFileTreeEntries(entries, path + entry.name + '/')
1217             .done(function (files) {
1218               dfd.resolve(files);
1219             })
1220             .fail(errorHandler);
1221         },
1222         readEntries = function () {
1223           dirReader.readEntries(function (results) {
1224             if (!results.length) {
1225               successHandler(entries);
1226             } else {
1227               entries = entries.concat(results);
1228               readEntries();
1229             }
1230           }, errorHandler);
1231         };
1232       // eslint-disable-next-line no-param-reassign
1233       path = path || '';
1234       if (entry.isFile) {
1235         if (entry._file) {
1236           // Workaround for Chrome bug #149735
1237           entry._file.relativePath = path;
1238           dfd.resolve(entry._file);
1239         } else {
1240           entry.file(function (file) {
1241             file.relativePath = path;
1242             dfd.resolve(file);
1243           }, errorHandler);
1244         }
1245       } else if (entry.isDirectory) {
1246         dirReader = entry.createReader();
1247         readEntries();
1248       } else {
1249         // Return an empty list for file system items
1250         // other than files or directories:
1251         dfd.resolve([]);
1252       }
1253       return dfd.promise();
1254     },
1255
1256     _handleFileTreeEntries: function (entries, path) {
1257       var that = this;
1258       return $.when
1259         .apply(
1260           $,
1261           $.map(entries, function (entry) {
1262             return that._handleFileTreeEntry(entry, path);
1263           })
1264         )
1265         [this._promisePipe](function () {
1266           return Array.prototype.concat.apply([], arguments);
1267         });
1268     },
1269
1270     _getDroppedFiles: function (dataTransfer) {
1271       // eslint-disable-next-line no-param-reassign
1272       dataTransfer = dataTransfer || {};
1273       var items = dataTransfer.items;
1274       if (
1275         items &&
1276         items.length &&
1277         (items[0].webkitGetAsEntry || items[0].getAsEntry)
1278       ) {
1279         return this._handleFileTreeEntries(
1280           $.map(items, function (item) {
1281             var entry;
1282             if (item.webkitGetAsEntry) {
1283               entry = item.webkitGetAsEntry();
1284               if (entry) {
1285                 // Workaround for Chrome bug #149735:
1286                 entry._file = item.getAsFile();
1287               }
1288               return entry;
1289             }
1290             return item.getAsEntry();
1291           })
1292         );
1293       }
1294       return $.Deferred().resolve($.makeArray(dataTransfer.files)).promise();
1295     },
1296
1297     _getSingleFileInputFiles: function (fileInput) {
1298       // eslint-disable-next-line no-param-reassign
1299       fileInput = $(fileInput);
1300       var entries = fileInput.prop('entries'),
1301         files,
1302         value;
1303       if (entries && entries.length) {
1304         return this._handleFileTreeEntries(entries);
1305       }
1306       files = $.makeArray(fileInput.prop('files'));
1307       if (!files.length) {
1308         value = fileInput.prop('value');
1309         if (!value) {
1310           return $.Deferred().resolve([]).promise();
1311         }
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;
1321         });
1322       }
1323       return $.Deferred().resolve(files).promise();
1324     },
1325
1326     _getFileInputFiles: function (fileInput) {
1327       if (!(fileInput instanceof $) || fileInput.length === 1) {
1328         return this._getSingleFileInputFiles(fileInput);
1329       }
1330       return $.when
1331         .apply($, $.map(fileInput, this._getSingleFileInputFiles))
1332         [this._promisePipe](function () {
1333           return Array.prototype.concat.apply([], arguments);
1334         });
1335     },
1336
1337     _onChange: function (e) {
1338       var that = this,
1339         data = {
1340           fileInput: $(e.target),
1341           form: $(e.target.form)
1342         };
1343       this._getFileInputFiles(data.fileInput).always(function (files) {
1344         data.files = files;
1345         if (that.options.replaceFileInput) {
1346           that._replaceFileInput(data);
1347         }
1348         if (
1349           that._trigger(
1350             'change',
1351             $.Event('change', { delegatedEvent: e }),
1352             data
1353           ) !== false
1354         ) {
1355           that._onAdd(e, data);
1356         }
1357       });
1358     },
1359
1360     _onPaste: function (e) {
1361       var items =
1362           e.originalEvent &&
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();
1369           if (file) {
1370             data.files.push(file);
1371           }
1372         });
1373         if (
1374           this._trigger(
1375             'paste',
1376             $.Event('paste', { delegatedEvent: e }),
1377             data
1378           ) !== false
1379         ) {
1380           this._onAdd(e, data);
1381         }
1382       }
1383     },
1384
1385     _onDrop: function (e) {
1386       e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
1387       var that = this,
1388         dataTransfer = e.dataTransfer,
1389         data = {};
1390       if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1391         e.preventDefault();
1392         this._getDroppedFiles(dataTransfer).always(function (files) {
1393           data.files = files;
1394           if (
1395             that._trigger(
1396               'drop',
1397               $.Event('drop', { delegatedEvent: e }),
1398               data
1399             ) !== false
1400           ) {
1401             that._onAdd(e, data);
1402           }
1403         });
1404       }
1405     },
1406
1407     _onDragOver: getDragHandler('dragover'),
1408
1409     _onDragEnter: getDragHandler('dragenter'),
1410
1411     _onDragLeave: getDragHandler('dragleave'),
1412
1413     _initEventHandlers: function () {
1414       if (this._isXHRUpload(this.options)) {
1415         this._on(this.options.dropZone, {
1416           dragover: this._onDragOver,
1417           drop: this._onDrop,
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
1422         });
1423         this._on(this.options.pasteZone, {
1424           paste: this._onPaste
1425         });
1426       }
1427       if ($.support.fileInput) {
1428         this._on(this.options.fileInput, {
1429           change: this._onChange
1430         });
1431       }
1432     },
1433
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');
1438     },
1439
1440     _destroy: function () {
1441       this._destroyEventHandlers();
1442     },
1443
1444     _setOption: function (key, value) {
1445       var reinit = $.inArray(key, this._specialOptions) !== -1;
1446       if (reinit) {
1447         this._destroyEventHandlers();
1448       }
1449       this._super(key, value);
1450       if (reinit) {
1451         this._initSpecialOptions();
1452         this._initEventHandlers();
1453       }
1454     },
1455
1456     _initSpecialOptions: function () {
1457       var options = this.options;
1458       if (options.fileInput === undefined) {
1459         options.fileInput = this.element.is('input[type="file"]')
1460           ? this.element
1461           : this.element.find('input[type="file"]');
1462       } else if (!(options.fileInput instanceof $)) {
1463         options.fileInput = $(options.fileInput);
1464       }
1465       if (!(options.dropZone instanceof $)) {
1466         options.dropZone = $(options.dropZone);
1467       }
1468       if (!(options.pasteZone instanceof $)) {
1469         options.pasteZone = $(options.pasteZone);
1470       }
1471     },
1472
1473     _getRegExp: function (str) {
1474       var parts = str.split('/'),
1475         modifiers = parts.pop();
1476       parts.shift();
1477       return new RegExp(parts.join('/'), modifiers);
1478     },
1479
1480     _isRegExpOption: function (key, value) {
1481       return (
1482         key !== 'url' &&
1483         $.type(value) === 'string' &&
1484         /^\/.*\/[igm]{0,3}$/.test(value)
1485       );
1486     },
1487
1488     _initDataAttributes: function () {
1489       var that = this,
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(),
1495           value;
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();
1500           });
1501           value = data[key];
1502           if (that._isRegExpOption(key, value)) {
1503             value = that._getRegExp(value);
1504           }
1505           options[key] = value;
1506         }
1507       });
1508     },
1509
1510     _create: function () {
1511       this._initDataAttributes();
1512       this._initSpecialOptions();
1513       this._slots = [];
1514       this._sequence = this._getXHRPromise(true);
1515       this._sending = this._active = 0;
1516       this._initProgressObject(this);
1517       this._initEventHandlers();
1518     },
1519
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;
1524     },
1525
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;
1532     },
1533
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) {
1539       var that = this;
1540       if (!data || this.options.disabled) {
1541         return;
1542       }
1543       if (data.fileInput && !data.files) {
1544         this._getFileInputFiles(data.fileInput).always(function (files) {
1545           data.files = files;
1546           that._onAdd(null, data);
1547         });
1548       } else {
1549         data.files = $.makeArray(data.files);
1550         this._onAdd(null, data);
1551       }
1552     },
1553
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) {
1562           var that = this,
1563             dfd = $.Deferred(),
1564             promise = dfd.promise(),
1565             jqXHR,
1566             aborted;
1567           promise.abort = function () {
1568             aborted = true;
1569             if (jqXHR) {
1570               return jqXHR.abort();
1571             }
1572             dfd.reject(null, 'abort', 'abort');
1573             return promise;
1574           };
1575           this._getFileInputFiles(data.fileInput).always(function (files) {
1576             if (aborted) {
1577               return;
1578             }
1579             if (!files.length) {
1580               dfd.reject();
1581               return;
1582             }
1583             data.files = files;
1584             jqXHR = that._onSend(null, data);
1585             jqXHR.then(
1586               function (result, textStatus, jqXHR) {
1587                 dfd.resolve(result, textStatus, jqXHR);
1588               },
1589               function (jqXHR, textStatus, errorThrown) {
1590                 dfd.reject(jqXHR, textStatus, errorThrown);
1591               }
1592             );
1593           });
1594           return this._enhancePromise(promise);
1595         }
1596         data.files = $.makeArray(data.files);
1597         if (data.files.length) {
1598           return this._onSend(null, data);
1599         }
1600       }
1601       return this._getXHRPromise(false, data && data.context);
1602     }
1603   });
1604 });