jquery-fileupload-rails 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +38 -16
  3. data/{vendor → app}/assets/images/loading.gif +0 -0
  4. data/{vendor → app}/assets/images/progressbar.gif +0 -0
  5. data/app/assets/javascripts/jquery-fileupload/angularjs.js +12 -0
  6. data/app/assets/javascripts/jquery-fileupload/basic-plus.js +11 -0
  7. data/app/assets/javascripts/jquery-fileupload/basic.js +3 -0
  8. data/{vendor → app}/assets/javascripts/jquery-fileupload/cors/jquery.postmessage-transport.js +4 -4
  9. data/{vendor → app}/assets/javascripts/jquery-fileupload/cors/jquery.xdr-transport.js +1 -2
  10. data/app/assets/javascripts/jquery-fileupload/index.js +13 -0
  11. data/app/assets/javascripts/jquery-fileupload/jquery-ui.js +13 -0
  12. data/app/assets/javascripts/jquery-fileupload/jquery.fileupload-angular.js +429 -0
  13. data/app/assets/javascripts/jquery-fileupload/jquery.fileupload-audio.js +106 -0
  14. data/app/assets/javascripts/jquery-fileupload/jquery.fileupload-image.js +315 -0
  15. data/app/assets/javascripts/jquery-fileupload/jquery.fileupload-jquery-ui.js +152 -0
  16. data/app/assets/javascripts/jquery-fileupload/jquery.fileupload-process.js +172 -0
  17. data/{vendor → app}/assets/javascripts/jquery-fileupload/jquery.fileupload-ui.js +178 -273
  18. data/app/assets/javascripts/jquery-fileupload/jquery.fileupload-validate.js +119 -0
  19. data/app/assets/javascripts/jquery-fileupload/jquery.fileupload-video.js +106 -0
  20. data/{vendor → app}/assets/javascripts/jquery-fileupload/jquery.fileupload.js +481 -188
  21. data/{vendor → app}/assets/javascripts/jquery-fileupload/jquery.iframe-transport.js +43 -14
  22. data/{vendor → app}/assets/javascripts/jquery-fileupload/locale.js +0 -0
  23. data/{vendor → app}/assets/javascripts/jquery-fileupload/vendor/canvas-to-blob.js +9 -5
  24. data/{vendor → app}/assets/javascripts/jquery-fileupload/vendor/jquery.ui.widget.js +72 -44
  25. data/app/assets/javascripts/jquery-fileupload/vendor/load-image.all.min.js +1 -0
  26. data/{vendor → app}/assets/javascripts/jquery-fileupload/vendor/tmpl.js +9 -8
  27. data/app/assets/stylesheets/jquery.fileupload-noscript.scss +22 -0
  28. data/app/assets/stylesheets/jquery.fileupload-ui-noscript.scss +17 -0
  29. data/app/assets/stylesheets/jquery.fileupload-ui.scss +57 -0
  30. data/app/assets/stylesheets/jquery.fileupload.scss +36 -0
  31. data/lib/jquery/fileupload/rails/version.rb +1 -1
  32. metadata +43 -39
  33. data/vendor/assets/javascripts/jquery-fileupload/basic.js +0 -4
  34. data/vendor/assets/javascripts/jquery-fileupload/index.js +0 -9
  35. data/vendor/assets/javascripts/jquery-fileupload/jquery.fileupload-fp.js +0 -223
  36. data/vendor/assets/javascripts/jquery-fileupload/vendor/load-image.js +0 -121
  37. data/vendor/assets/stylesheets/jquery.fileupload-ui.scss +0 -84
@@ -0,0 +1,119 @@
1
+ /*
2
+ * jQuery File Upload Validation Plugin 1.1.2
3
+ * https://github.com/blueimp/jQuery-File-Upload
4
+ *
5
+ * Copyright 2013, Sebastian Tschan
6
+ * https://blueimp.net
7
+ *
8
+ * Licensed under the MIT license:
9
+ * http://www.opensource.org/licenses/MIT
10
+ */
11
+
12
+ /* global define, window */
13
+
14
+ (function (factory) {
15
+ 'use strict';
16
+ if (typeof define === 'function' && define.amd) {
17
+ // Register as an anonymous AMD module:
18
+ define([
19
+ 'jquery',
20
+ './jquery.fileupload-process'
21
+ ], factory);
22
+ } else {
23
+ // Browser globals:
24
+ factory(
25
+ window.jQuery
26
+ );
27
+ }
28
+ }(function ($) {
29
+ 'use strict';
30
+
31
+ // Append to the default processQueue:
32
+ $.blueimp.fileupload.prototype.options.processQueue.push(
33
+ {
34
+ action: 'validate',
35
+ // Always trigger this action,
36
+ // even if the previous action was rejected:
37
+ always: true,
38
+ // Options taken from the global options map:
39
+ acceptFileTypes: '@',
40
+ maxFileSize: '@',
41
+ minFileSize: '@',
42
+ maxNumberOfFiles: '@',
43
+ disabled: '@disableValidation'
44
+ }
45
+ );
46
+
47
+ // The File Upload Validation plugin extends the fileupload widget
48
+ // with file validation functionality:
49
+ $.widget('blueimp.fileupload', $.blueimp.fileupload, {
50
+
51
+ options: {
52
+ /*
53
+ // The regular expression for allowed file types, matches
54
+ // against either file type or file name:
55
+ acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
56
+ // The maximum allowed file size in bytes:
57
+ maxFileSize: 10000000, // 10 MB
58
+ // The minimum allowed file size in bytes:
59
+ minFileSize: undefined, // No minimal file size
60
+ // The limit of files to be uploaded:
61
+ maxNumberOfFiles: 10,
62
+ */
63
+
64
+ // Function returning the current number of files,
65
+ // has to be overriden for maxNumberOfFiles validation:
66
+ getNumberOfFiles: $.noop,
67
+
68
+ // Error and info messages:
69
+ messages: {
70
+ maxNumberOfFiles: 'Maximum number of files exceeded',
71
+ acceptFileTypes: 'File type not allowed',
72
+ maxFileSize: 'File is too large',
73
+ minFileSize: 'File is too small'
74
+ }
75
+ },
76
+
77
+ processActions: {
78
+
79
+ validate: function (data, options) {
80
+ if (options.disabled) {
81
+ return data;
82
+ }
83
+ var dfd = $.Deferred(),
84
+ settings = this.options,
85
+ file = data.files[data.index],
86
+ fileSize;
87
+ if (options.minFileSize || options.maxFileSize) {
88
+ fileSize = file.size;
89
+ }
90
+ if ($.type(options.maxNumberOfFiles) === 'number' &&
91
+ (settings.getNumberOfFiles() || 0) + data.files.length >
92
+ options.maxNumberOfFiles) {
93
+ file.error = settings.i18n('maxNumberOfFiles');
94
+ } else if (options.acceptFileTypes &&
95
+ !(options.acceptFileTypes.test(file.type) ||
96
+ options.acceptFileTypes.test(file.name))) {
97
+ file.error = settings.i18n('acceptFileTypes');
98
+ } else if (fileSize > options.maxFileSize) {
99
+ file.error = settings.i18n('maxFileSize');
100
+ } else if ($.type(fileSize) === 'number' &&
101
+ fileSize < options.minFileSize) {
102
+ file.error = settings.i18n('minFileSize');
103
+ } else {
104
+ delete file.error;
105
+ }
106
+ if (file.error || data.files.error) {
107
+ data.files.error = true;
108
+ dfd.rejectWith(this, [data]);
109
+ } else {
110
+ dfd.resolveWith(this, [data]);
111
+ }
112
+ return dfd.promise();
113
+ }
114
+
115
+ }
116
+
117
+ });
118
+
119
+ }));
@@ -0,0 +1,106 @@
1
+ /*
2
+ * jQuery File Upload Video Preview Plugin 1.0.3
3
+ * https://github.com/blueimp/jQuery-File-Upload
4
+ *
5
+ * Copyright 2013, Sebastian Tschan
6
+ * https://blueimp.net
7
+ *
8
+ * Licensed under the MIT license:
9
+ * http://www.opensource.org/licenses/MIT
10
+ */
11
+
12
+ /* jshint nomen:false */
13
+ /* global define, window, document */
14
+
15
+ (function (factory) {
16
+ 'use strict';
17
+ if (typeof define === 'function' && define.amd) {
18
+ // Register as an anonymous AMD module:
19
+ define([
20
+ 'jquery',
21
+ 'load-image',
22
+ './jquery.fileupload-process'
23
+ ], factory);
24
+ } else {
25
+ // Browser globals:
26
+ factory(
27
+ window.jQuery,
28
+ window.loadImage
29
+ );
30
+ }
31
+ }(function ($, loadImage) {
32
+ 'use strict';
33
+
34
+ // Prepend to the default processQueue:
35
+ $.blueimp.fileupload.prototype.options.processQueue.unshift(
36
+ {
37
+ action: 'loadVideo',
38
+ // Use the action as prefix for the "@" options:
39
+ prefix: true,
40
+ fileTypes: '@',
41
+ maxFileSize: '@',
42
+ disabled: '@disableVideoPreview'
43
+ },
44
+ {
45
+ action: 'setVideo',
46
+ name: '@videoPreviewName',
47
+ disabled: '@disableVideoPreview'
48
+ }
49
+ );
50
+
51
+ // The File Upload Video Preview plugin extends the fileupload widget
52
+ // with video preview functionality:
53
+ $.widget('blueimp.fileupload', $.blueimp.fileupload, {
54
+
55
+ options: {
56
+ // The regular expression for the types of video files to load,
57
+ // matched against the file type:
58
+ loadVideoFileTypes: /^video\/.*$/
59
+ },
60
+
61
+ _videoElement: document.createElement('video'),
62
+
63
+ processActions: {
64
+
65
+ // Loads the video file given via data.files and data.index
66
+ // as video element if the browser supports playing it.
67
+ // Accepts the options fileTypes (regular expression)
68
+ // and maxFileSize (integer) to limit the files to load:
69
+ loadVideo: function (data, options) {
70
+ if (options.disabled) {
71
+ return data;
72
+ }
73
+ var file = data.files[data.index],
74
+ url,
75
+ video;
76
+ if (this._videoElement.canPlayType &&
77
+ this._videoElement.canPlayType(file.type) &&
78
+ ($.type(options.maxFileSize) !== 'number' ||
79
+ file.size <= options.maxFileSize) &&
80
+ (!options.fileTypes ||
81
+ options.fileTypes.test(file.type))) {
82
+ url = loadImage.createObjectURL(file);
83
+ if (url) {
84
+ video = this._videoElement.cloneNode(false);
85
+ video.src = url;
86
+ video.controls = true;
87
+ data.video = video;
88
+ return data;
89
+ }
90
+ }
91
+ return data;
92
+ },
93
+
94
+ // Sets the video element as a property of the file object:
95
+ setVideo: function (data, options) {
96
+ if (data.video && !options.disabled) {
97
+ data.files[data.index][options.name || 'preview'] = data.video;
98
+ }
99
+ return data;
100
+ }
101
+
102
+ }
103
+
104
+ });
105
+
106
+ }));
@@ -1,5 +1,5 @@
1
1
  /*
2
- * jQuery File Upload Plugin 5.21
2
+ * jQuery File Upload Plugin 5.42.0
3
3
  * https://github.com/blueimp/jQuery-File-Upload
4
4
  *
5
5
  * Copyright 2010, Sebastian Tschan
@@ -9,8 +9,8 @@
9
9
  * http://www.opensource.org/licenses/MIT
10
10
  */
11
11
 
12
- /*jslint nomen: true, unparam: true, regexp: true */
13
- /*global define, window, document, File, Blob, FormData, location */
12
+ /* jshint nomen:false */
13
+ /* global define, window, document, location, Blob, FormData */
14
14
 
15
15
  (function (factory) {
16
16
  'use strict';
@@ -27,26 +27,48 @@
27
27
  }(function ($) {
28
28
  'use strict';
29
29
 
30
+ // Detect file input support, based on
31
+ // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
32
+ $.support.fileInput = !(new RegExp(
33
+ // Handle devices which give false positives for the feature detection:
34
+ '(Android (1\\.[0156]|2\\.[01]))' +
35
+ '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
36
+ '|(w(eb)?OSBrowser)|(webOS)' +
37
+ '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
38
+ ).test(window.navigator.userAgent) ||
39
+ // Feature detection for all other devices:
40
+ $('<input type="file">').prop('disabled'));
41
+
30
42
  // The FileReader API is not actually used, but works as feature detection,
31
- // as e.g. Safari supports XHR file uploads via the FormData API,
32
- // but not non-multipart XHR file uploads:
33
- $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
43
+ // as some Safari versions (5?) support XHR file uploads via the FormData API,
44
+ // but not non-multipart XHR file uploads.
45
+ // window.XMLHttpRequestUpload is not available on IE10, so we check for
46
+ // window.ProgressEvent instead to detect XHR2 file upload capability:
47
+ $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
34
48
  $.support.xhrFormDataFileUpload = !!window.FormData;
35
49
 
36
- // The form.elements propHook is added to filter serialized elements
37
- // to not include file inputs in jQuery 1.9.0.
38
- // This hooks directly into jQuery.fn.serializeArray.
39
- // For more info, see http://bugs.jquery.com/ticket/13306
40
- $.propHooks.elements = {
41
- get: function (form) {
42
- if ($.nodeName(form, 'form')) {
43
- return $.grep(form.elements, function (elem) {
44
- return !$.nodeName(elem, 'input') || elem.type !== 'file';
45
- });
50
+ // Detect support for Blob slicing (required for chunked uploads):
51
+ $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
52
+ Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
53
+
54
+ // Helper function to create drag handlers for dragover/dragenter/dragleave:
55
+ function getDragHandler(type) {
56
+ var isDragOver = type === 'dragover';
57
+ return function (e) {
58
+ e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
59
+ var dataTransfer = e.dataTransfer;
60
+ if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
61
+ this._trigger(
62
+ type,
63
+ $.Event(type, {delegatedEvent: e})
64
+ ) !== false) {
65
+ e.preventDefault();
66
+ if (isDragOver) {
67
+ dataTransfer.dropEffect = 'copy';
68
+ }
46
69
  }
47
- return null;
48
- }
49
- };
70
+ };
71
+ }
50
72
 
51
73
  // The fileupload widget listens for change events on file input fields defined
52
74
  // via fileInput setting and paste or drop events of the given dropZone.
@@ -62,9 +84,9 @@
62
84
  // The drop target element(s), by the default the complete document.
63
85
  // Set to null to disable drag & drop support:
64
86
  dropZone: $(document),
65
- // The paste target element(s), by the default the complete document.
66
- // Set to null to disable paste support:
67
- pasteZone: $(document),
87
+ // The paste target element(s), by the default undefined.
88
+ // Set to a DOM node or jQuery object to enable file pasting:
89
+ pasteZone: undefined,
68
90
  // The file input field(s), that are listened to for change events.
69
91
  // If undefined, it is set to the file input fields inside
70
92
  // of the widget element on plugin initialization.
@@ -87,6 +109,14 @@
87
109
  // To limit the number of files uploaded with one XHR request,
88
110
  // set the following option to an integer greater than 0:
89
111
  limitMultiFileUploads: undefined,
112
+ // The following option limits the number of files uploaded with one
113
+ // XHR request to keep the request size under or equal to the defined
114
+ // limit in bytes:
115
+ limitMultiFileUploadSize: undefined,
116
+ // Multipart file uploads add a number of bytes to each uploaded file,
117
+ // therefore the following option adds an overhead for each file used
118
+ // in the limitMultiFileUploadSize configuration:
119
+ limitMultiFileUploadSizeOverhead: 512,
90
120
  // Set the following option to true to issue all file upload requests
91
121
  // in a sequential order:
92
122
  sequentialUploads: false,
@@ -127,6 +157,25 @@
127
157
  progressInterval: 100,
128
158
  // Interval in milliseconds to calculate progress bitrate:
129
159
  bitrateInterval: 500,
160
+ // By default, uploads are started automatically when adding files:
161
+ autoUpload: true,
162
+
163
+ // Error and info messages:
164
+ messages: {
165
+ uploadedBytes: 'Uploaded bytes exceed file size'
166
+ },
167
+
168
+ // Translation function, gets the message key to be translated
169
+ // and an object with context specific data as arguments:
170
+ i18n: function (message, context) {
171
+ message = this.messages[message] || message.toString();
172
+ if (context) {
173
+ $.each(context, function (key, value) {
174
+ message = message.replace('{' + key + '}', value);
175
+ });
176
+ }
177
+ return message;
178
+ },
130
179
 
131
180
  // Additional form data to be sent along with the file uploads can be set
132
181
  // using this option, which accepts an array of objects with name and
@@ -140,18 +189,29 @@
140
189
  // The add callback is invoked as soon as files are added to the fileupload
141
190
  // widget (via file input selection, drag & drop, paste or add API call).
142
191
  // If the singleFileUploads option is enabled, this callback will be
143
- // called once for each file in the selection for XHR file uplaods, else
192
+ // called once for each file in the selection for XHR file uploads, else
144
193
  // once for each file selection.
194
+ //
145
195
  // The upload starts when the submit method is invoked on the data parameter.
146
196
  // The data object contains a files property holding the added files
147
- // and allows to override plugin options as well as define ajax settings.
197
+ // and allows you to override plugin options as well as define ajax settings.
198
+ //
148
199
  // Listeners for this callback can also be bound the following way:
149
200
  // .bind('fileuploadadd', func);
201
+ //
150
202
  // data.submit() returns a Promise object and allows to attach additional
151
203
  // handlers using jQuery's Deferred callbacks:
152
204
  // data.submit().done(func).fail(func).always(func);
153
205
  add: function (e, data) {
154
- data.submit();
206
+ if (e.isDefaultPrevented()) {
207
+ return false;
208
+ }
209
+ if (data.autoUpload || (data.autoUpload !== false &&
210
+ $(this).fileupload('option', 'autoUpload'))) {
211
+ data.process().done(function () {
212
+ data.submit();
213
+ });
214
+ }
155
215
  },
156
216
 
157
217
  // Other callbacks:
@@ -214,8 +274,9 @@
214
274
  cache: false
215
275
  },
216
276
 
217
- // A list of options that require a refresh after assigning a new value:
218
- _refreshOptionsList: [
277
+ // A list of options that require reinitializing event listeners and/or
278
+ // special initialization code:
279
+ _specialOptions: [
219
280
  'fileInput',
220
281
  'dropZone',
221
282
  'pasteZone',
@@ -223,8 +284,13 @@
223
284
  'forceIframeTransport'
224
285
  ],
225
286
 
287
+ _blobSlice: $.support.blobSlice && function () {
288
+ var slice = this.slice || this.webkitSlice || this.mozSlice;
289
+ return slice.apply(this, arguments);
290
+ },
291
+
226
292
  _BitrateTimer: function () {
227
- this.timestamp = +(new Date());
293
+ this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
228
294
  this.loaded = 0;
229
295
  this.bitrate = 0;
230
296
  this.getBitrate = function (now, loaded, interval) {
@@ -246,13 +312,13 @@
246
312
 
247
313
  _getFormData: function (options) {
248
314
  var formData;
249
- if (typeof options.formData === 'function') {
315
+ if ($.type(options.formData) === 'function') {
250
316
  return options.formData(options.form);
251
317
  }
252
318
  if ($.isArray(options.formData)) {
253
319
  return options.formData;
254
320
  }
255
- if (options.formData) {
321
+ if ($.type(options.formData) === 'object') {
256
322
  formData = [];
257
323
  $.each(options.formData, function (name, value) {
258
324
  formData.push({name: name, value: value});
@@ -270,10 +336,35 @@
270
336
  return total;
271
337
  },
272
338
 
339
+ _initProgressObject: function (obj) {
340
+ var progress = {
341
+ loaded: 0,
342
+ total: 0,
343
+ bitrate: 0
344
+ };
345
+ if (obj._progress) {
346
+ $.extend(obj._progress, progress);
347
+ } else {
348
+ obj._progress = progress;
349
+ }
350
+ },
351
+
352
+ _initResponseObject: function (obj) {
353
+ var prop;
354
+ if (obj._response) {
355
+ for (prop in obj._response) {
356
+ if (obj._response.hasOwnProperty(prop)) {
357
+ delete obj._response[prop];
358
+ }
359
+ }
360
+ } else {
361
+ obj._response = {};
362
+ }
363
+ },
364
+
273
365
  _onProgress: function (e, data) {
274
366
  if (e.lengthComputable) {
275
- var now = +(new Date()),
276
- total,
367
+ var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
277
368
  loaded;
278
369
  if (data._time && data.progressInterval &&
279
370
  (now - data._time < data.progressInterval) &&
@@ -281,16 +372,19 @@
281
372
  return;
282
373
  }
283
374
  data._time = now;
284
- total = data.total || this._getTotal(data.files);
285
- loaded = parseInt(
286
- e.loaded / e.total * (data.chunkSize || total),
287
- 10
375
+ loaded = Math.floor(
376
+ e.loaded / e.total * (data.chunkSize || data._progress.total)
288
377
  ) + (data.uploadedBytes || 0);
289
- this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
290
- data.lengthComputable = true;
291
- data.loaded = loaded;
292
- data.total = total;
293
- data.bitrate = data._bitrateTimer.getBitrate(
378
+ // Add the difference from the previously loaded state
379
+ // to the global loaded counter:
380
+ this._progress.loaded += (loaded - data._progress.loaded);
381
+ this._progress.bitrate = this._bitrateTimer.getBitrate(
382
+ now,
383
+ this._progress.loaded,
384
+ data.bitrateInterval
385
+ );
386
+ data._progress.loaded = data.loaded = loaded;
387
+ data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
294
388
  now,
295
389
  loaded,
296
390
  data.bitrateInterval
@@ -298,19 +392,18 @@
298
392
  // Trigger a custom progress event with a total data property set
299
393
  // to the file size(s) of the current upload and a loaded data
300
394
  // property calculated accordingly:
301
- this._trigger('progress', e, data);
395
+ this._trigger(
396
+ 'progress',
397
+ $.Event('progress', {delegatedEvent: e}),
398
+ data
399
+ );
302
400
  // Trigger a global progress event for all current file uploads,
303
401
  // including ajax calls queued for sequential file uploads:
304
- this._trigger('progressall', e, {
305
- lengthComputable: true,
306
- loaded: this._loaded,
307
- total: this._total,
308
- bitrate: this._bitrateTimer.getBitrate(
309
- now,
310
- this._loaded,
311
- data.bitrateInterval
312
- )
313
- });
402
+ this._trigger(
403
+ 'progressall',
404
+ $.Event('progressall', {delegatedEvent: e}),
405
+ this._progress
406
+ );
314
407
  }
315
408
  },
316
409
 
@@ -334,20 +427,29 @@
334
427
  }
335
428
  },
336
429
 
430
+ _isInstanceOf: function (type, obj) {
431
+ // Cross-frame instanceof check
432
+ return Object.prototype.toString.call(obj) === '[object ' + type + ']';
433
+ },
434
+
337
435
  _initXHRData: function (options) {
338
- var formData,
436
+ var that = this,
437
+ formData,
339
438
  file = options.files[0],
340
439
  // Ignore non-multipart setting if not supported:
341
440
  multipart = options.multipart || !$.support.xhrFileUpload,
342
- paramName = options.paramName[0];
343
- options.headers = options.headers || {};
441
+ paramName = $.type(options.paramName) === 'array' ?
442
+ options.paramName[0] : options.paramName;
443
+ options.headers = $.extend({}, options.headers);
344
444
  if (options.contentRange) {
345
445
  options.headers['Content-Range'] = options.contentRange;
346
446
  }
347
- if (!multipart) {
447
+ if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
348
448
  options.headers['Content-Disposition'] = 'attachment; filename="' +
349
449
  encodeURI(file.name) + '"';
350
- options.contentType = file.type;
450
+ }
451
+ if (!multipart) {
452
+ options.contentType = file.type || 'application/octet-stream';
351
453
  options.data = options.blob || file;
352
454
  } else if ($.support.xhrFormDataFileUpload) {
353
455
  if (options.postMessage) {
@@ -364,13 +466,14 @@
364
466
  } else {
365
467
  $.each(options.files, function (index, file) {
366
468
  formData.push({
367
- name: options.paramName[index] || paramName,
469
+ name: ($.type(options.paramName) === 'array' &&
470
+ options.paramName[index]) || paramName,
368
471
  value: file
369
472
  });
370
473
  });
371
474
  }
372
475
  } else {
373
- if (options.formData instanceof FormData) {
476
+ if (that._isInstanceOf('FormData', options.formData)) {
374
477
  formData = options.formData;
375
478
  } else {
376
479
  formData = new FormData();
@@ -379,21 +482,18 @@
379
482
  });
380
483
  }
381
484
  if (options.blob) {
382
- options.headers['Content-Disposition'] = 'attachment; filename="' +
383
- encodeURI(file.name) + '"';
384
485
  formData.append(paramName, options.blob, file.name);
385
486
  } else {
386
487
  $.each(options.files, function (index, file) {
387
- // Files are also Blob instances, but some browsers
388
- // (Firefox 3.6) support the File API but not Blobs.
389
488
  // This check allows the tests to run with
390
489
  // dummy objects:
391
- if ((window.Blob && file instanceof Blob) ||
392
- (window.File && file instanceof File)) {
490
+ if (that._isInstanceOf('File', file) ||
491
+ that._isInstanceOf('Blob', file)) {
393
492
  formData.append(
394
- options.paramName[index] || paramName,
493
+ ($.type(options.paramName) === 'array' &&
494
+ options.paramName[index]) || paramName,
395
495
  file,
396
- file.name
496
+ file.uploadName || file.name
397
497
  );
398
498
  }
399
499
  });
@@ -406,13 +506,13 @@
406
506
  },
407
507
 
408
508
  _initIframeSettings: function (options) {
509
+ var targetHost = $('<a></a>').prop('href', options.url).prop('host');
409
510
  // Setting the dataType to iframe enables the iframe transport:
410
511
  options.dataType = 'iframe ' + (options.dataType || '');
411
512
  // The iframe transport accepts a serialized array as form data:
412
513
  options.formData = this._getFormData(options);
413
514
  // Add redirect url to form data on cross-domain uploads:
414
- if (options.redirect && $('<a></a>').prop('href', options.url)
415
- .prop('host') !== location.host) {
515
+ if (options.redirect && targetHost && targetHost !== location.host) {
416
516
  options.formData.push({
417
517
  name: options.redirectParamName || 'redirect',
418
518
  value: options.redirect
@@ -434,7 +534,7 @@
434
534
  options.dataType = 'postmessage ' + (options.dataType || '');
435
535
  }
436
536
  } else {
437
- this._initIframeSettings(options, 'iframe');
537
+ this._initIframeSettings(options);
438
538
  }
439
539
  },
440
540
 
@@ -477,8 +577,10 @@
477
577
  options.url = options.form.prop('action') || location.href;
478
578
  }
479
579
  // The HTTP request method must be "POST" or "PUT":
480
- options.type = (options.type || options.form.prop('method') || '')
481
- .toUpperCase();
580
+ options.type = (options.type ||
581
+ ($.type(options.form.prop('method')) === 'string' &&
582
+ options.form.prop('method')) || ''
583
+ ).toUpperCase();
482
584
  if (options.type !== 'POST' && options.type !== 'PUT' &&
483
585
  options.type !== 'PATCH') {
484
586
  options.type = 'POST';
@@ -495,6 +597,21 @@
495
597
  return options;
496
598
  },
497
599
 
600
+ // jQuery 1.6 doesn't provide .state(),
601
+ // while jQuery 1.8+ removed .isRejected() and .isResolved():
602
+ _getDeferredState: function (deferred) {
603
+ if (deferred.state) {
604
+ return deferred.state();
605
+ }
606
+ if (deferred.isResolved()) {
607
+ return 'resolved';
608
+ }
609
+ if (deferred.isRejected()) {
610
+ return 'rejected';
611
+ }
612
+ return 'pending';
613
+ },
614
+
498
615
  // Maps jqXHR callbacks to the equivalent
499
616
  // methods of the given Promise object:
500
617
  _enhancePromise: function (promise) {
@@ -519,6 +636,66 @@
519
636
  return this._enhancePromise(promise);
520
637
  },
521
638
 
639
+ // Adds convenience methods to the data callback argument:
640
+ _addConvenienceMethods: function (e, data) {
641
+ var that = this,
642
+ getPromise = function (args) {
643
+ return $.Deferred().resolveWith(that, args).promise();
644
+ };
645
+ data.process = function (resolveFunc, rejectFunc) {
646
+ if (resolveFunc || rejectFunc) {
647
+ data._processQueue = this._processQueue =
648
+ (this._processQueue || getPromise([this])).pipe(
649
+ function () {
650
+ if (data.errorThrown) {
651
+ return $.Deferred()
652
+ .rejectWith(that, [data]).promise();
653
+ }
654
+ return getPromise(arguments);
655
+ }
656
+ ).pipe(resolveFunc, rejectFunc);
657
+ }
658
+ return this._processQueue || getPromise([this]);
659
+ };
660
+ data.submit = function () {
661
+ if (this.state() !== 'pending') {
662
+ data.jqXHR = this.jqXHR =
663
+ (that._trigger(
664
+ 'submit',
665
+ $.Event('submit', {delegatedEvent: e}),
666
+ this
667
+ ) !== false) && that._onSend(e, this);
668
+ }
669
+ return this.jqXHR || that._getXHRPromise();
670
+ };
671
+ data.abort = function () {
672
+ if (this.jqXHR) {
673
+ return this.jqXHR.abort();
674
+ }
675
+ this.errorThrown = 'abort';
676
+ that._trigger('fail', null, this);
677
+ return that._getXHRPromise(false);
678
+ };
679
+ data.state = function () {
680
+ if (this.jqXHR) {
681
+ return that._getDeferredState(this.jqXHR);
682
+ }
683
+ if (this._processQueue) {
684
+ return that._getDeferredState(this._processQueue);
685
+ }
686
+ };
687
+ data.processing = function () {
688
+ return !this.jqXHR && this._processQueue && that
689
+ ._getDeferredState(this._processQueue) === 'pending';
690
+ };
691
+ data.progress = function () {
692
+ return this._progress;
693
+ };
694
+ data.response = function () {
695
+ return this._response;
696
+ };
697
+ },
698
+
522
699
  // Parses the Range header from the server response
523
700
  // and returns the uploaded bytes:
524
701
  _getUploadedBytes: function (jqXHR) {
@@ -535,12 +712,13 @@
535
712
  // should be uploaded in chunks, but does not invoke any
536
713
  // upload requests:
537
714
  _chunkedUpload: function (options, testOnly) {
715
+ options.uploadedBytes = options.uploadedBytes || 0;
538
716
  var that = this,
539
717
  file = options.files[0],
540
718
  fs = file.size,
541
- ub = options.uploadedBytes = options.uploadedBytes || 0,
719
+ ub = options.uploadedBytes,
542
720
  mcs = options.maxChunkSize || fs,
543
- slice = file.slice || file.webkitSlice || file.mozSlice,
721
+ slice = this._blobSlice,
544
722
  dfd = $.Deferred(),
545
723
  promise = dfd.promise(),
546
724
  jqXHR,
@@ -553,7 +731,7 @@
553
731
  return true;
554
732
  }
555
733
  if (ub >= fs) {
556
- file.error = 'Uploaded bytes exceed file size';
734
+ file.error = options.i18n('uploadedBytes');
557
735
  return this._getXHRPromise(
558
736
  false,
559
737
  options.context,
@@ -561,9 +739,10 @@
561
739
  );
562
740
  }
563
741
  // The chunk upload method:
564
- upload = function (i) {
742
+ upload = function () {
565
743
  // Clone the options object for each chunk upload:
566
- var o = $.extend({}, options);
744
+ var o = $.extend({}, options),
745
+ currentLoaded = o._progress.loaded;
567
746
  o.blob = slice.call(
568
747
  file,
569
748
  ub,
@@ -585,10 +764,10 @@
585
764
  .done(function (result, textStatus, jqXHR) {
586
765
  ub = that._getUploadedBytes(jqXHR) ||
587
766
  (ub + o.chunkSize);
588
- // Create a progress event if upload is done and no progress
589
- // event has been invoked for this chunk, or there has been
590
- // no progress event with loaded equaling total:
591
- if (!o.loaded || o.loaded < o.total) {
767
+ // Create a progress event if no final progress event
768
+ // with loaded equaling total has been triggered
769
+ // for this chunk:
770
+ if (currentLoaded + o.chunkSize - o._progress.loaded) {
592
771
  that._onProgress($.Event('progress', {
593
772
  lengthComputable: true,
594
773
  loaded: ub - o.uploadedBytes,
@@ -640,61 +819,66 @@
640
819
  this._trigger('start');
641
820
  // Set timer for global bitrate progress calculation:
642
821
  this._bitrateTimer = new this._BitrateTimer();
822
+ // Reset the global progress values:
823
+ this._progress.loaded = this._progress.total = 0;
824
+ this._progress.bitrate = 0;
643
825
  }
826
+ // Make sure the container objects for the .response() and
827
+ // .progress() methods on the data object are available
828
+ // and reset to their initial state:
829
+ this._initResponseObject(data);
830
+ this._initProgressObject(data);
831
+ data._progress.loaded = data.loaded = data.uploadedBytes || 0;
832
+ data._progress.total = data.total = this._getTotal(data.files) || 1;
833
+ data._progress.bitrate = data.bitrate = 0;
644
834
  this._active += 1;
645
835
  // Initialize the global progress values:
646
- this._loaded += data.uploadedBytes || 0;
647
- this._total += this._getTotal(data.files);
836
+ this._progress.loaded += data.loaded;
837
+ this._progress.total += data.total;
648
838
  },
649
839
 
650
840
  _onDone: function (result, textStatus, jqXHR, options) {
651
- if (!this._isXHRUpload(options) || !options.loaded ||
652
- options.loaded < options.total) {
653
- var total = this._getTotal(options.files) || 1;
654
- // Create a progress event for each iframe load,
655
- // or if there has been no progress event with
656
- // loaded equaling total for XHR uploads:
841
+ var total = options._progress.total,
842
+ response = options._response;
843
+ if (options._progress.loaded < total) {
844
+ // Create a progress event if no final progress event
845
+ // with loaded equaling total has been triggered:
657
846
  this._onProgress($.Event('progress', {
658
847
  lengthComputable: true,
659
848
  loaded: total,
660
849
  total: total
661
850
  }), options);
662
851
  }
663
- options.result = result;
664
- options.textStatus = textStatus;
665
- options.jqXHR = jqXHR;
852
+ response.result = options.result = result;
853
+ response.textStatus = options.textStatus = textStatus;
854
+ response.jqXHR = options.jqXHR = jqXHR;
666
855
  this._trigger('done', null, options);
667
856
  },
668
857
 
669
858
  _onFail: function (jqXHR, textStatus, errorThrown, options) {
670
- options.jqXHR = jqXHR;
671
- options.textStatus = textStatus;
672
- options.errorThrown = errorThrown;
673
- this._trigger('fail', null, options);
859
+ var response = options._response;
674
860
  if (options.recalculateProgress) {
675
861
  // Remove the failed (error or abort) file upload from
676
862
  // the global progress calculation:
677
- this._loaded -= options.loaded || options.uploadedBytes || 0;
678
- this._total -= options.total || this._getTotal(options.files);
863
+ this._progress.loaded -= options._progress.loaded;
864
+ this._progress.total -= options._progress.total;
679
865
  }
866
+ response.jqXHR = options.jqXHR = jqXHR;
867
+ response.textStatus = options.textStatus = textStatus;
868
+ response.errorThrown = options.errorThrown = errorThrown;
869
+ this._trigger('fail', null, options);
680
870
  },
681
871
 
682
872
  _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
683
873
  // jqXHRorResult, textStatus and jqXHRorError are added to the
684
874
  // options object via done and fail callbacks
685
- this._active -= 1;
686
875
  this._trigger('always', null, options);
687
- if (this._active === 0) {
688
- // The stop callback is triggered when all uploads have
689
- // been completed, equivalent to the global ajaxStop event:
690
- this._trigger('stop');
691
- // Reset the global progress values:
692
- this._loaded = this._total = 0;
693
- this._bitrateTimer = null;
694
- }
695
876
  },
696
877
 
697
878
  _onSend: function (e, data) {
879
+ if (!data.submit) {
880
+ this._addConvenienceMethods(e, data);
881
+ }
698
882
  var that = this,
699
883
  jqXHR,
700
884
  aborted,
@@ -706,7 +890,11 @@
706
890
  // Set timer for bitrate progress calculation:
707
891
  options._bitrateTimer = new that._BitrateTimer();
708
892
  jqXHR = jqXHR || (
709
- ((aborted || that._trigger('send', e, options) === false) &&
893
+ ((aborted || that._trigger(
894
+ 'send',
895
+ $.Event('send', {delegatedEvent: e}),
896
+ options
897
+ ) === false) &&
710
898
  that._getXHRPromise(false, options.context, aborted)) ||
711
899
  that._chunkedUpload(options) || $.ajax(options)
712
900
  ).done(function (result, textStatus, jqXHR) {
@@ -714,32 +902,32 @@
714
902
  }).fail(function (jqXHR, textStatus, errorThrown) {
715
903
  that._onFail(jqXHR, textStatus, errorThrown, options);
716
904
  }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
717
- that._sending -= 1;
718
905
  that._onAlways(
719
906
  jqXHRorResult,
720
907
  textStatus,
721
908
  jqXHRorError,
722
909
  options
723
910
  );
911
+ that._sending -= 1;
912
+ that._active -= 1;
724
913
  if (options.limitConcurrentUploads &&
725
914
  options.limitConcurrentUploads > that._sending) {
726
915
  // Start the next queued upload,
727
916
  // that has not been aborted:
728
- var nextSlot = that._slots.shift(),
729
- isPending;
917
+ var nextSlot = that._slots.shift();
730
918
  while (nextSlot) {
731
- // jQuery 1.6 doesn't provide .state(),
732
- // while jQuery 1.8+ removed .isRejected():
733
- isPending = nextSlot.state ?
734
- nextSlot.state() === 'pending' :
735
- !nextSlot.isRejected();
736
- if (isPending) {
919
+ if (that._getDeferredState(nextSlot) === 'pending') {
737
920
  nextSlot.resolve();
738
921
  break;
739
922
  }
740
923
  nextSlot = that._slots.shift();
741
924
  }
742
925
  }
926
+ if (that._active === 0) {
927
+ // The stop callback is triggered when all uploads have
928
+ // been completed, equivalent to the global ajaxStop event:
929
+ that._trigger('stop');
930
+ }
743
931
  });
744
932
  return jqXHR;
745
933
  };
@@ -752,7 +940,8 @@
752
940
  this._slots.push(slot);
753
941
  pipe = slot.pipe(send);
754
942
  } else {
755
- pipe = (this._sequence = this._sequence.pipe(send, send));
943
+ this._sequence = this._sequence.pipe(send, send);
944
+ pipe = this._sequence;
756
945
  }
757
946
  // Return the piped Promise object, enhanced with an abort method,
758
947
  // which is delegated to the jqXHR object of the current upload,
@@ -776,49 +965,80 @@
776
965
  var that = this,
777
966
  result = true,
778
967
  options = $.extend({}, this.options, data),
968
+ files = data.files,
969
+ filesLength = files.length,
779
970
  limit = options.limitMultiFileUploads,
971
+ limitSize = options.limitMultiFileUploadSize,
972
+ overhead = options.limitMultiFileUploadSizeOverhead,
973
+ batchSize = 0,
780
974
  paramName = this._getParamName(options),
781
975
  paramNameSet,
782
976
  paramNameSlice,
783
977
  fileSet,
784
- i;
785
- if (!(options.singleFileUploads || limit) ||
978
+ i,
979
+ j = 0;
980
+ if (limitSize && (!filesLength || files[0].size === undefined)) {
981
+ limitSize = undefined;
982
+ }
983
+ if (!(options.singleFileUploads || limit || limitSize) ||
786
984
  !this._isXHRUpload(options)) {
787
- fileSet = [data.files];
985
+ fileSet = [files];
788
986
  paramNameSet = [paramName];
789
- } else if (!options.singleFileUploads && limit) {
987
+ } else if (!(options.singleFileUploads || limitSize) && limit) {
790
988
  fileSet = [];
791
989
  paramNameSet = [];
792
- for (i = 0; i < data.files.length; i += limit) {
793
- fileSet.push(data.files.slice(i, i + limit));
990
+ for (i = 0; i < filesLength; i += limit) {
991
+ fileSet.push(files.slice(i, i + limit));
794
992
  paramNameSlice = paramName.slice(i, i + limit);
795
993
  if (!paramNameSlice.length) {
796
994
  paramNameSlice = paramName;
797
995
  }
798
996
  paramNameSet.push(paramNameSlice);
799
997
  }
998
+ } else if (!options.singleFileUploads && limitSize) {
999
+ fileSet = [];
1000
+ paramNameSet = [];
1001
+ for (i = 0; i < filesLength; i = i + 1) {
1002
+ batchSize += files[i].size + overhead;
1003
+ if (i + 1 === filesLength ||
1004
+ ((batchSize + files[i + 1].size + overhead) > limitSize) ||
1005
+ (limit && i + 1 - j >= limit)) {
1006
+ fileSet.push(files.slice(j, i + 1));
1007
+ paramNameSlice = paramName.slice(j, i + 1);
1008
+ if (!paramNameSlice.length) {
1009
+ paramNameSlice = paramName;
1010
+ }
1011
+ paramNameSet.push(paramNameSlice);
1012
+ j = i + 1;
1013
+ batchSize = 0;
1014
+ }
1015
+ }
800
1016
  } else {
801
1017
  paramNameSet = paramName;
802
1018
  }
803
- data.originalFiles = data.files;
804
- $.each(fileSet || data.files, function (index, element) {
1019
+ data.originalFiles = files;
1020
+ $.each(fileSet || files, function (index, element) {
805
1021
  var newData = $.extend({}, data);
806
1022
  newData.files = fileSet ? element : [element];
807
1023
  newData.paramName = paramNameSet[index];
808
- newData.submit = function () {
809
- newData.jqXHR = this.jqXHR =
810
- (that._trigger('submit', e, this) !== false) &&
811
- that._onSend(e, this);
812
- return this.jqXHR;
813
- };
814
- result = that._trigger('add', e, newData);
1024
+ that._initResponseObject(newData);
1025
+ that._initProgressObject(newData);
1026
+ that._addConvenienceMethods(e, newData);
1027
+ result = that._trigger(
1028
+ 'add',
1029
+ $.Event('add', {delegatedEvent: e}),
1030
+ newData
1031
+ );
815
1032
  return result;
816
1033
  });
817
1034
  return result;
818
1035
  },
819
1036
 
820
- _replaceFileInput: function (input) {
821
- var inputClone = input.clone(true);
1037
+ _replaceFileInput: function (data) {
1038
+ var input = data.fileInput,
1039
+ inputClone = input.clone(true);
1040
+ // Add a reference for the new cloned file input to the data argument:
1041
+ data.fileInputClone = inputClone;
822
1042
  $('<form></form>').append(inputClone)[0].reset();
823
1043
  // Detaching allows to insert the fileInput on another form
824
1044
  // without loosing the file input value:
@@ -854,7 +1074,25 @@
854
1074
  // to be returned together in one set:
855
1075
  dfd.resolve([e]);
856
1076
  },
857
- dirReader;
1077
+ successHandler = function (entries) {
1078
+ that._handleFileTreeEntries(
1079
+ entries,
1080
+ path + entry.name + '/'
1081
+ ).done(function (files) {
1082
+ dfd.resolve(files);
1083
+ }).fail(errorHandler);
1084
+ },
1085
+ readEntries = function () {
1086
+ dirReader.readEntries(function (results) {
1087
+ if (!results.length) {
1088
+ successHandler(entries);
1089
+ } else {
1090
+ entries = entries.concat(results);
1091
+ readEntries();
1092
+ }
1093
+ }, errorHandler);
1094
+ },
1095
+ dirReader, entries = [];
858
1096
  path = path || '';
859
1097
  if (entry.isFile) {
860
1098
  if (entry._file) {
@@ -869,14 +1107,7 @@
869
1107
  }
870
1108
  } else if (entry.isDirectory) {
871
1109
  dirReader = entry.createReader();
872
- dirReader.readEntries(function (entries) {
873
- that._handleFileTreeEntries(
874
- entries,
875
- path + entry.name + '/'
876
- ).done(function (files) {
877
- dfd.resolve(files);
878
- }).fail(errorHandler);
879
- }, errorHandler);
1110
+ readEntries();
880
1111
  } else {
881
1112
  // Return an empy list for file system items
882
1113
  // other than files or directories:
@@ -978,84 +1209,99 @@
978
1209
  this._getFileInputFiles(data.fileInput).always(function (files) {
979
1210
  data.files = files;
980
1211
  if (that.options.replaceFileInput) {
981
- that._replaceFileInput(data.fileInput);
1212
+ that._replaceFileInput(data);
982
1213
  }
983
- if (that._trigger('change', e, data) !== false) {
1214
+ if (that._trigger(
1215
+ 'change',
1216
+ $.Event('change', {delegatedEvent: e}),
1217
+ data
1218
+ ) !== false) {
984
1219
  that._onAdd(e, data);
985
1220
  }
986
1221
  });
987
1222
  },
988
1223
 
989
1224
  _onPaste: function (e) {
990
- var cbd = e.originalEvent.clipboardData,
991
- items = (cbd && cbd.items) || [],
1225
+ var items = e.originalEvent && e.originalEvent.clipboardData &&
1226
+ e.originalEvent.clipboardData.items,
992
1227
  data = {files: []};
993
- $.each(items, function (index, item) {
994
- var file = item.getAsFile && item.getAsFile();
995
- if (file) {
996
- data.files.push(file);
1228
+ if (items && items.length) {
1229
+ $.each(items, function (index, item) {
1230
+ var file = item.getAsFile && item.getAsFile();
1231
+ if (file) {
1232
+ data.files.push(file);
1233
+ }
1234
+ });
1235
+ if (this._trigger(
1236
+ 'paste',
1237
+ $.Event('paste', {delegatedEvent: e}),
1238
+ data
1239
+ ) !== false) {
1240
+ this._onAdd(e, data);
997
1241
  }
998
- });
999
- if (this._trigger('paste', e, data) === false ||
1000
- this._onAdd(e, data) === false) {
1001
- return false;
1002
1242
  }
1003
1243
  },
1004
1244
 
1005
1245
  _onDrop: function (e) {
1246
+ e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
1006
1247
  var that = this,
1007
- dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
1248
+ dataTransfer = e.dataTransfer,
1008
1249
  data = {};
1009
1250
  if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1010
1251
  e.preventDefault();
1252
+ this._getDroppedFiles(dataTransfer).always(function (files) {
1253
+ data.files = files;
1254
+ if (that._trigger(
1255
+ 'drop',
1256
+ $.Event('drop', {delegatedEvent: e}),
1257
+ data
1258
+ ) !== false) {
1259
+ that._onAdd(e, data);
1260
+ }
1261
+ });
1011
1262
  }
1012
- this._getDroppedFiles(dataTransfer).always(function (files) {
1013
- data.files = files;
1014
- if (that._trigger('drop', e, data) !== false) {
1015
- that._onAdd(e, data);
1016
- }
1017
- });
1018
1263
  },
1019
1264
 
1020
- _onDragOver: function (e) {
1021
- var dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
1022
- if (this._trigger('dragover', e) === false) {
1023
- return false;
1024
- }
1025
- if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1) {
1026
- dataTransfer.dropEffect = 'copy';
1027
- e.preventDefault();
1028
- }
1029
- },
1265
+ _onDragOver: getDragHandler('dragover'),
1266
+
1267
+ _onDragEnter: getDragHandler('dragenter'),
1268
+
1269
+ _onDragLeave: getDragHandler('dragleave'),
1030
1270
 
1031
1271
  _initEventHandlers: function () {
1032
1272
  if (this._isXHRUpload(this.options)) {
1033
1273
  this._on(this.options.dropZone, {
1034
1274
  dragover: this._onDragOver,
1035
- drop: this._onDrop
1275
+ drop: this._onDrop,
1276
+ // event.preventDefault() on dragenter is required for IE10+:
1277
+ dragenter: this._onDragEnter,
1278
+ // dragleave is not required, but added for completeness:
1279
+ dragleave: this._onDragLeave
1036
1280
  });
1037
1281
  this._on(this.options.pasteZone, {
1038
1282
  paste: this._onPaste
1039
1283
  });
1040
1284
  }
1041
- this._on(this.options.fileInput, {
1042
- change: this._onChange
1043
- });
1285
+ if ($.support.fileInput) {
1286
+ this._on(this.options.fileInput, {
1287
+ change: this._onChange
1288
+ });
1289
+ }
1044
1290
  },
1045
1291
 
1046
1292
  _destroyEventHandlers: function () {
1047
- this._off(this.options.dropZone, 'dragover drop');
1293
+ this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
1048
1294
  this._off(this.options.pasteZone, 'paste');
1049
1295
  this._off(this.options.fileInput, 'change');
1050
1296
  },
1051
1297
 
1052
1298
  _setOption: function (key, value) {
1053
- var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
1054
- if (refresh) {
1299
+ var reinit = $.inArray(key, this._specialOptions) !== -1;
1300
+ if (reinit) {
1055
1301
  this._destroyEventHandlers();
1056
1302
  }
1057
1303
  this._super(key, value);
1058
- if (refresh) {
1304
+ if (reinit) {
1059
1305
  this._initSpecialOptions();
1060
1306
  this._initEventHandlers();
1061
1307
  }
@@ -1077,19 +1323,61 @@
1077
1323
  }
1078
1324
  },
1079
1325
 
1080
- _create: function () {
1081
- var options = this.options;
1326
+ _getRegExp: function (str) {
1327
+ var parts = str.split('/'),
1328
+ modifiers = parts.pop();
1329
+ parts.shift();
1330
+ return new RegExp(parts.join('/'), modifiers);
1331
+ },
1332
+
1333
+ _isRegExpOption: function (key, value) {
1334
+ return key !== 'url' && $.type(value) === 'string' &&
1335
+ /^\/.*\/[igm]{0,3}$/.test(value);
1336
+ },
1337
+
1338
+ _initDataAttributes: function () {
1339
+ var that = this,
1340
+ options = this.options,
1341
+ clone = $(this.element[0].cloneNode(false));
1082
1342
  // Initialize options set via HTML5 data-attributes:
1083
- $.extend(options, $(this.element[0].cloneNode(false)).data());
1343
+ $.each(
1344
+ clone.data(),
1345
+ function (key, value) {
1346
+ var dataAttributeName = 'data-' +
1347
+ // Convert camelCase to hyphen-ated key:
1348
+ key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
1349
+ if (clone.attr(dataAttributeName)) {
1350
+ if (that._isRegExpOption(key, value)) {
1351
+ value = that._getRegExp(value);
1352
+ }
1353
+ options[key] = value;
1354
+ }
1355
+ }
1356
+ );
1357
+ },
1358
+
1359
+ _create: function () {
1360
+ this._initDataAttributes();
1084
1361
  this._initSpecialOptions();
1085
1362
  this._slots = [];
1086
1363
  this._sequence = this._getXHRPromise(true);
1087
- this._sending = this._active = this._loaded = this._total = 0;
1364
+ this._sending = this._active = 0;
1365
+ this._initProgressObject(this);
1088
1366
  this._initEventHandlers();
1089
1367
  },
1090
1368
 
1091
- _destroy: function () {
1092
- this._destroyEventHandlers();
1369
+ // This method is exposed to the widget API and allows to query
1370
+ // the number of active uploads:
1371
+ active: function () {
1372
+ return this._active;
1373
+ },
1374
+
1375
+ // This method is exposed to the widget API and allows to query
1376
+ // the widget upload progress.
1377
+ // It returns an object with loaded, total and bitrate properties
1378
+ // for the running uploads:
1379
+ progress: function () {
1380
+ return this._progress;
1093
1381
  },
1094
1382
 
1095
1383
  // This method is exposed to the widget API and allows adding files
@@ -1138,8 +1426,13 @@
1138
1426
  if (aborted) {
1139
1427
  return;
1140
1428
  }
1429
+ if (!files.length) {
1430
+ dfd.reject();
1431
+ return;
1432
+ }
1141
1433
  data.files = files;
1142
- jqXHR = that._onSend(null, data).then(
1434
+ jqXHR = that._onSend(null, data);
1435
+ jqXHR.then(
1143
1436
  function (result, textStatus, jqXHR) {
1144
1437
  dfd.resolve(result, textStatus, jqXHR);
1145
1438
  },