jquery-fileupload-rails 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  },