rails-uploader 0.2.8 → 0.3.0

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +162 -86
  4. data/app/assets/javascripts/uploader/application.js +6 -4
  5. data/app/assets/javascripts/uploader/jquery.uploader.js.coffee +58 -0
  6. data/app/controllers/uploader/attachments_controller.rb +63 -39
  7. data/app/views/uploader/default/_container.html.erb +19 -45
  8. data/app/views/uploader/default/_download.html.erb +4 -5
  9. data/app/views/uploader/default/_upload.html.erb +0 -1
  10. data/config/locales/en.yml +2 -0
  11. data/config/locales/ru.yml +3 -0
  12. data/config/locales/uk.yml +3 -0
  13. data/config/routes.rb +1 -1
  14. data/lib/uploader/asset.rb +63 -84
  15. data/lib/uploader/authorization.rb +52 -0
  16. data/lib/uploader/authorization_adapter.rb +24 -0
  17. data/lib/uploader/chunked_uploads.rb +15 -0
  18. data/lib/uploader/engine.rb +6 -0
  19. data/lib/uploader/file_part.rb +18 -0
  20. data/lib/uploader/fileuploads.rb +42 -65
  21. data/lib/uploader/helpers/field_tag.rb +10 -5
  22. data/lib/uploader/hooks/formtastic.rb +0 -13
  23. data/lib/uploader/upload_request.rb +72 -0
  24. data/lib/uploader/version.rb +1 -1
  25. data/lib/uploader.rb +41 -8
  26. data/spec/dummy/app/models/asset.rb +12 -0
  27. data/spec/dummy/app/models/picture.rb +6 -0
  28. data/spec/dummy/db/test.sqlite3 +0 -0
  29. data/spec/dummy/log/test.log +325 -0
  30. data/spec/dummy/public/uploads/picture/data/1/thumb_rails.png +0 -0
  31. data/spec/dummy/public/uploads/picture/data/3/thumb_rails.png +0 -0
  32. data/spec/fileuploads_spec.rb +4 -4
  33. data/spec/requests/attachments_controller_spec.rb +11 -12
  34. data/vendor/assets/javascripts/uploader/jquery.fileupload-process.js +175 -0
  35. data/vendor/assets/javascripts/uploader/jquery.fileupload-ui.js +164 -261
  36. data/vendor/assets/javascripts/uploader/jquery.fileupload-validate.js +122 -0
  37. data/vendor/assets/javascripts/uploader/jquery.fileupload.js +335 -101
  38. data/vendor/assets/javascripts/uploader/jquery.iframe-transport.js +47 -15
  39. data/vendor/assets/javascripts/uploader/vendor/jquery.ui.widget.js +572 -0
  40. data/vendor/assets/javascripts/uploader/vendor/tmpl.min.js +1 -0
  41. data/vendor/assets/stylesheets/uploader/default.css +26 -19
  42. metadata +12 -9
  43. data/vendor/assets/javascripts/uploader/jquery.fileupload-fp.js +0 -227
  44. data/vendor/assets/javascripts/uploader/jquery.ui.widget.js +0 -530
  45. data/vendor/assets/javascripts/uploader/load-image.min.js +0 -1
  46. data/vendor/assets/javascripts/uploader/locales/en.js +0 -27
  47. data/vendor/assets/javascripts/uploader/locales/ru.js +0 -27
  48. data/vendor/assets/javascripts/uploader/locales/uk.js +0 -27
  49. data/vendor/assets/javascripts/uploader/tmpl.min.js +0 -1
@@ -0,0 +1,122 @@
1
+ /*
2
+ * jQuery File Upload Validation Plugin
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, require, 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 if (typeof exports === 'object') {
23
+ // Node/CommonJS:
24
+ factory(require('jquery'));
25
+ } else {
26
+ // Browser globals:
27
+ factory(
28
+ window.jQuery
29
+ );
30
+ }
31
+ }(function ($) {
32
+ 'use strict';
33
+
34
+ // Append to the default processQueue:
35
+ $.blueimp.fileupload.prototype.options.processQueue.push(
36
+ {
37
+ action: 'validate',
38
+ // Always trigger this action,
39
+ // even if the previous action was rejected:
40
+ always: true,
41
+ // Options taken from the global options map:
42
+ acceptFileTypes: '@',
43
+ maxFileSize: '@',
44
+ minFileSize: '@',
45
+ maxNumberOfFiles: '@',
46
+ disabled: '@disableValidation'
47
+ }
48
+ );
49
+
50
+ // The File Upload Validation plugin extends the fileupload widget
51
+ // with file validation functionality:
52
+ $.widget('blueimp.fileupload', $.blueimp.fileupload, {
53
+
54
+ options: {
55
+ /*
56
+ // The regular expression for allowed file types, matches
57
+ // against either file type or file name:
58
+ acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
59
+ // The maximum allowed file size in bytes:
60
+ maxFileSize: 10000000, // 10 MB
61
+ // The minimum allowed file size in bytes:
62
+ minFileSize: undefined, // No minimal file size
63
+ // The limit of files to be uploaded:
64
+ maxNumberOfFiles: 10,
65
+ */
66
+
67
+ // Function returning the current number of files,
68
+ // has to be overriden for maxNumberOfFiles validation:
69
+ getNumberOfFiles: $.noop,
70
+
71
+ // Error and info messages:
72
+ messages: {
73
+ maxNumberOfFiles: 'Maximum number of files exceeded',
74
+ acceptFileTypes: 'File type not allowed',
75
+ maxFileSize: 'File is too large',
76
+ minFileSize: 'File is too small'
77
+ }
78
+ },
79
+
80
+ processActions: {
81
+
82
+ validate: function (data, options) {
83
+ if (options.disabled) {
84
+ return data;
85
+ }
86
+ var dfd = $.Deferred(),
87
+ settings = this.options,
88
+ file = data.files[data.index],
89
+ fileSize;
90
+ if (options.minFileSize || options.maxFileSize) {
91
+ fileSize = file.size;
92
+ }
93
+ if ($.type(options.maxNumberOfFiles) === 'number' &&
94
+ (settings.getNumberOfFiles() || 0) + data.files.length >
95
+ options.maxNumberOfFiles) {
96
+ file.error = settings.i18n('maxNumberOfFiles');
97
+ } else if (options.acceptFileTypes &&
98
+ !(options.acceptFileTypes.test(file.type) ||
99
+ options.acceptFileTypes.test(file.name))) {
100
+ file.error = settings.i18n('acceptFileTypes');
101
+ } else if (fileSize > options.maxFileSize) {
102
+ file.error = settings.i18n('maxFileSize');
103
+ } else if ($.type(fileSize) === 'number' &&
104
+ fileSize < options.minFileSize) {
105
+ file.error = settings.i18n('minFileSize');
106
+ } else {
107
+ delete file.error;
108
+ }
109
+ if (file.error || data.files.error) {
110
+ data.files.error = true;
111
+ dfd.rejectWith(this, [data]);
112
+ } else {
113
+ dfd.resolveWith(this, [data]);
114
+ }
115
+ return dfd.promise();
116
+ }
117
+
118
+ }
119
+
120
+ });
121
+
122
+ }));
@@ -1,5 +1,5 @@
1
1
  /*
2
- * jQuery File Upload Plugin 5.28.8
2
+ * jQuery File Upload Plugin
3
3
  * https://github.com/blueimp/jQuery-File-Upload
4
4
  *
5
5
  * Copyright 2010, Sebastian Tschan
@@ -9,10 +9,10 @@
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, require, window, document, location, Blob, FormData */
14
14
 
15
- (function (factory) {
15
+ ;(function (factory) {
16
16
  'use strict';
17
17
  if (typeof define === 'function' && define.amd) {
18
18
  // Register as an anonymous AMD module:
@@ -20,6 +20,12 @@
20
20
  'jquery',
21
21
  'jquery.ui.widget'
22
22
  ], factory);
23
+ } else if (typeof exports === 'object') {
24
+ // Node/CommonJS:
25
+ factory(
26
+ require('jquery'),
27
+ require('./vendor/jquery.ui.widget')
28
+ );
23
29
  } else {
24
30
  // Browser globals:
25
31
  factory(window.jQuery);
@@ -27,12 +33,49 @@
27
33
  }(function ($) {
28
34
  'use strict';
29
35
 
36
+ // Detect file input support, based on
37
+ // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
38
+ $.support.fileInput = !(new RegExp(
39
+ // Handle devices which give false positives for the feature detection:
40
+ '(Android (1\\.[0156]|2\\.[01]))' +
41
+ '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
42
+ '|(w(eb)?OSBrowser)|(webOS)' +
43
+ '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
44
+ ).test(window.navigator.userAgent) ||
45
+ // Feature detection for all other devices:
46
+ $('<input type="file">').prop('disabled'));
47
+
30
48
  // 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);
49
+ // as some Safari versions (5?) support XHR file uploads via the FormData API,
50
+ // but not non-multipart XHR file uploads.
51
+ // window.XMLHttpRequestUpload is not available on IE10, so we check for
52
+ // window.ProgressEvent instead to detect XHR2 file upload capability:
53
+ $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
34
54
  $.support.xhrFormDataFileUpload = !!window.FormData;
35
55
 
56
+ // Detect support for Blob slicing (required for chunked uploads):
57
+ $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
58
+ Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
59
+
60
+ // Helper function to create drag handlers for dragover/dragenter/dragleave:
61
+ function getDragHandler(type) {
62
+ var isDragOver = type === 'dragover';
63
+ return function (e) {
64
+ e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
65
+ var dataTransfer = e.dataTransfer;
66
+ if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
67
+ this._trigger(
68
+ type,
69
+ $.Event(type, {delegatedEvent: e})
70
+ ) !== false) {
71
+ e.preventDefault();
72
+ if (isDragOver) {
73
+ dataTransfer.dropEffect = 'copy';
74
+ }
75
+ }
76
+ };
77
+ }
78
+
36
79
  // The fileupload widget listens for change events on file input fields defined
37
80
  // via fileInput setting and paste or drop events of the given dropZone.
38
81
  // In addition to the default jQuery Widget methods, the fileupload widget
@@ -47,9 +90,9 @@
47
90
  // The drop target element(s), by the default the complete document.
48
91
  // Set to null to disable drag & drop support:
49
92
  dropZone: $(document),
50
- // The paste target element(s), by the default the complete document.
51
- // Set to null to disable paste support:
52
- pasteZone: $(document),
93
+ // The paste target element(s), by the default undefined.
94
+ // Set to a DOM node or jQuery object to enable file pasting:
95
+ pasteZone: undefined,
53
96
  // The file input field(s), that are listened to for change events.
54
97
  // If undefined, it is set to the file input fields inside
55
98
  // of the widget element on plugin initialization.
@@ -72,6 +115,14 @@
72
115
  // To limit the number of files uploaded with one XHR request,
73
116
  // set the following option to an integer greater than 0:
74
117
  limitMultiFileUploads: undefined,
118
+ // The following option limits the number of files uploaded with one
119
+ // XHR request to keep the request size under or equal to the defined
120
+ // limit in bytes:
121
+ limitMultiFileUploadSize: undefined,
122
+ // Multipart file uploads add a number of bytes to each uploaded file,
123
+ // therefore the following option adds an overhead for each file used
124
+ // in the limitMultiFileUploadSize configuration:
125
+ limitMultiFileUploadSizeOverhead: 512,
75
126
  // Set the following option to true to issue all file upload requests
76
127
  // in a sequential order:
77
128
  sequentialUploads: false,
@@ -115,6 +166,23 @@
115
166
  // By default, uploads are started automatically when adding files:
116
167
  autoUpload: true,
117
168
 
169
+ // Error and info messages:
170
+ messages: {
171
+ uploadedBytes: 'Uploaded bytes exceed file size'
172
+ },
173
+
174
+ // Translation function, gets the message key to be translated
175
+ // and an object with context specific data as arguments:
176
+ i18n: function (message, context) {
177
+ message = this.messages[message] || message.toString();
178
+ if (context) {
179
+ $.each(context, function (key, value) {
180
+ message = message.replace('{' + key + '}', value);
181
+ });
182
+ }
183
+ return message;
184
+ },
185
+
118
186
  // Additional form data to be sent along with the file uploads can be set
119
187
  // using this option, which accepts an array of objects with name and
120
188
  // value properties, a function returning such an array, a FormData
@@ -127,21 +195,28 @@
127
195
  // The add callback is invoked as soon as files are added to the fileupload
128
196
  // widget (via file input selection, drag & drop, paste or add API call).
129
197
  // If the singleFileUploads option is enabled, this callback will be
130
- // called once for each file in the selection for XHR file uplaods, else
198
+ // called once for each file in the selection for XHR file uploads, else
131
199
  // once for each file selection.
200
+ //
132
201
  // The upload starts when the submit method is invoked on the data parameter.
133
202
  // The data object contains a files property holding the added files
134
- // and allows to override plugin options as well as define ajax settings.
203
+ // and allows you to override plugin options as well as define ajax settings.
204
+ //
135
205
  // Listeners for this callback can also be bound the following way:
136
206
  // .bind('fileuploadadd', func);
207
+ //
137
208
  // data.submit() returns a Promise object and allows to attach additional
138
209
  // handlers using jQuery's Deferred callbacks:
139
210
  // data.submit().done(func).fail(func).always(func);
140
211
  add: function (e, data) {
212
+ if (e.isDefaultPrevented()) {
213
+ return false;
214
+ }
141
215
  if (data.autoUpload || (data.autoUpload !== false &&
142
- ($(this).data('blueimp-fileupload') ||
143
- $(this).data('fileupload')).options.autoUpload)) {
144
- data.submit();
216
+ $(this).fileupload('option', 'autoUpload'))) {
217
+ data.process().done(function () {
218
+ data.submit();
219
+ });
145
220
  }
146
221
  },
147
222
 
@@ -202,11 +277,13 @@
202
277
  // The following are jQuery ajax settings required for the file uploads:
203
278
  processData: false,
204
279
  contentType: false,
205
- cache: false
280
+ cache: false,
281
+ timeout: 0
206
282
  },
207
283
 
208
- // A list of options that require a refresh after assigning a new value:
209
- _refreshOptionsList: [
284
+ // A list of options that require reinitializing event listeners and/or
285
+ // special initialization code:
286
+ _specialOptions: [
210
287
  'fileInput',
211
288
  'dropZone',
212
289
  'pasteZone',
@@ -214,6 +291,11 @@
214
291
  'forceIframeTransport'
215
292
  ],
216
293
 
294
+ _blobSlice: $.support.blobSlice && function () {
295
+ var slice = this.slice || this.webkitSlice || this.mozSlice;
296
+ return slice.apply(this, arguments);
297
+ },
298
+
217
299
  _BitrateTimer: function () {
218
300
  this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
219
301
  this.loaded = 0;
@@ -237,7 +319,7 @@
237
319
 
238
320
  _getFormData: function (options) {
239
321
  var formData;
240
- if (typeof options.formData === 'function') {
322
+ if ($.type(options.formData) === 'function') {
241
323
  return options.formData(options.form);
242
324
  }
243
325
  if ($.isArray(options.formData)) {
@@ -317,10 +399,18 @@
317
399
  // Trigger a custom progress event with a total data property set
318
400
  // to the file size(s) of the current upload and a loaded data
319
401
  // property calculated accordingly:
320
- this._trigger('progress', e, data);
402
+ this._trigger(
403
+ 'progress',
404
+ $.Event('progress', {delegatedEvent: e}),
405
+ data
406
+ );
321
407
  // Trigger a global progress event for all current file uploads,
322
408
  // including ajax calls queued for sequential file uploads:
323
- this._trigger('progressall', e, this._progress);
409
+ this._trigger(
410
+ 'progressall',
411
+ $.Event('progressall', {delegatedEvent: e}),
412
+ this._progress
413
+ );
324
414
  }
325
415
  },
326
416
 
@@ -355,15 +445,18 @@
355
445
  file = options.files[0],
356
446
  // Ignore non-multipart setting if not supported:
357
447
  multipart = options.multipart || !$.support.xhrFileUpload,
358
- paramName = options.paramName[0];
359
- options.headers = options.headers || {};
448
+ paramName = $.type(options.paramName) === 'array' ?
449
+ options.paramName[0] : options.paramName;
450
+ options.headers = $.extend({}, options.headers);
360
451
  if (options.contentRange) {
361
452
  options.headers['Content-Range'] = options.contentRange;
362
453
  }
363
- if (!multipart) {
454
+ if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
364
455
  options.headers['Content-Disposition'] = 'attachment; filename="' +
365
456
  encodeURI(file.name) + '"';
366
- options.contentType = file.type;
457
+ }
458
+ if (!multipart) {
459
+ options.contentType = file.type || 'application/octet-stream';
367
460
  options.data = options.blob || file;
368
461
  } else if ($.support.xhrFormDataFileUpload) {
369
462
  if (options.postMessage) {
@@ -380,7 +473,8 @@
380
473
  } else {
381
474
  $.each(options.files, function (index, file) {
382
475
  formData.push({
383
- name: options.paramName[index] || paramName,
476
+ name: ($.type(options.paramName) === 'array' &&
477
+ options.paramName[index]) || paramName,
384
478
  value: file
385
479
  });
386
480
  });
@@ -395,8 +489,6 @@
395
489
  });
396
490
  }
397
491
  if (options.blob) {
398
- options.headers['Content-Disposition'] = 'attachment; filename="' +
399
- encodeURI(file.name) + '"';
400
492
  formData.append(paramName, options.blob, file.name);
401
493
  } else {
402
494
  $.each(options.files, function (index, file) {
@@ -405,9 +497,10 @@
405
497
  if (that._isInstanceOf('File', file) ||
406
498
  that._isInstanceOf('Blob', file)) {
407
499
  formData.append(
408
- options.paramName[index] || paramName,
500
+ ($.type(options.paramName) === 'array' &&
501
+ options.paramName[index]) || paramName,
409
502
  file,
410
- file.name
503
+ file.uploadName || file.name
411
504
  );
412
505
  }
413
506
  });
@@ -420,13 +513,13 @@
420
513
  },
421
514
 
422
515
  _initIframeSettings: function (options) {
516
+ var targetHost = $('<a></a>').prop('href', options.url).prop('host');
423
517
  // Setting the dataType to iframe enables the iframe transport:
424
518
  options.dataType = 'iframe ' + (options.dataType || '');
425
519
  // The iframe transport accepts a serialized array as form data:
426
520
  options.formData = this._getFormData(options);
427
521
  // Add redirect url to form data on cross-domain uploads:
428
- if (options.redirect && $('<a></a>').prop('href', options.url)
429
- .prop('host') !== location.host) {
522
+ if (options.redirect && targetHost && targetHost !== location.host) {
430
523
  options.formData.push({
431
524
  name: options.redirectParamName || 'redirect',
432
525
  value: options.redirect
@@ -491,8 +584,10 @@
491
584
  options.url = options.form.prop('action') || location.href;
492
585
  }
493
586
  // The HTTP request method must be "POST" or "PUT":
494
- options.type = (options.type || options.form.prop('method') || '')
495
- .toUpperCase();
587
+ options.type = (options.type ||
588
+ ($.type(options.form.prop('method')) === 'string' &&
589
+ options.form.prop('method')) || ''
590
+ ).toUpperCase();
496
591
  if (options.type !== 'POST' && options.type !== 'PUT' &&
497
592
  options.type !== 'PATCH') {
498
593
  options.type = 'POST';
@@ -548,14 +643,35 @@
548
643
  return this._enhancePromise(promise);
549
644
  },
550
645
 
551
- // Adds convenience methods to the callback arguments:
646
+ // Adds convenience methods to the data callback argument:
552
647
  _addConvenienceMethods: function (e, data) {
553
- var that = this;
648
+ var that = this,
649
+ getPromise = function (args) {
650
+ return $.Deferred().resolveWith(that, args).promise();
651
+ };
652
+ data.process = function (resolveFunc, rejectFunc) {
653
+ if (resolveFunc || rejectFunc) {
654
+ data._processQueue = this._processQueue =
655
+ (this._processQueue || getPromise([this])).then(
656
+ function () {
657
+ if (data.errorThrown) {
658
+ return $.Deferred()
659
+ .rejectWith(that, [data]).promise();
660
+ }
661
+ return getPromise(arguments);
662
+ }
663
+ ).then(resolveFunc, rejectFunc);
664
+ }
665
+ return this._processQueue || getPromise([this]);
666
+ };
554
667
  data.submit = function () {
555
668
  if (this.state() !== 'pending') {
556
669
  data.jqXHR = this.jqXHR =
557
- (that._trigger('submit', e, this) !== false) &&
558
- that._onSend(e, this);
670
+ (that._trigger(
671
+ 'submit',
672
+ $.Event('submit', {delegatedEvent: e}),
673
+ this
674
+ ) !== false) && that._onSend(e, this);
559
675
  }
560
676
  return this.jqXHR || that._getXHRPromise();
561
677
  };
@@ -563,12 +679,21 @@
563
679
  if (this.jqXHR) {
564
680
  return this.jqXHR.abort();
565
681
  }
566
- return that._getXHRPromise();
682
+ this.errorThrown = 'abort';
683
+ that._trigger('fail', null, this);
684
+ return that._getXHRPromise(false);
567
685
  };
568
686
  data.state = function () {
569
687
  if (this.jqXHR) {
570
688
  return that._getDeferredState(this.jqXHR);
571
689
  }
690
+ if (this._processQueue) {
691
+ return that._getDeferredState(this._processQueue);
692
+ }
693
+ };
694
+ data.processing = function () {
695
+ return !this.jqXHR && this._processQueue && that
696
+ ._getDeferredState(this._processQueue) === 'pending';
572
697
  };
573
698
  data.progress = function () {
574
699
  return this._progress;
@@ -594,12 +719,13 @@
594
719
  // should be uploaded in chunks, but does not invoke any
595
720
  // upload requests:
596
721
  _chunkedUpload: function (options, testOnly) {
722
+ options.uploadedBytes = options.uploadedBytes || 0;
597
723
  var that = this,
598
724
  file = options.files[0],
599
725
  fs = file.size,
600
- ub = options.uploadedBytes = options.uploadedBytes || 0,
726
+ ub = options.uploadedBytes,
601
727
  mcs = options.maxChunkSize || fs,
602
- slice = file.slice || file.webkitSlice || file.mozSlice,
728
+ slice = this._blobSlice,
603
729
  dfd = $.Deferred(),
604
730
  promise = dfd.promise(),
605
731
  jqXHR,
@@ -612,7 +738,7 @@
612
738
  return true;
613
739
  }
614
740
  if (ub >= fs) {
615
- file.error = 'Uploaded bytes exceed file size';
741
+ file.error = options.i18n('uploadedBytes');
616
742
  return this._getXHRPromise(
617
743
  false,
618
744
  options.context,
@@ -648,7 +774,7 @@
648
774
  // Create a progress event if no final progress event
649
775
  // with loaded equaling total has been triggered
650
776
  // for this chunk:
651
- if (o._progress.loaded === currentLoaded) {
777
+ if (currentLoaded + o.chunkSize - o._progress.loaded) {
652
778
  that._onProgress($.Event('progress', {
653
779
  lengthComputable: true,
654
780
  loaded: ub - o.uploadedBytes,
@@ -771,7 +897,11 @@
771
897
  // Set timer for bitrate progress calculation:
772
898
  options._bitrateTimer = new that._BitrateTimer();
773
899
  jqXHR = jqXHR || (
774
- ((aborted || that._trigger('send', e, options) === false) &&
900
+ ((aborted || that._trigger(
901
+ 'send',
902
+ $.Event('send', {delegatedEvent: e}),
903
+ options
904
+ ) === false) &&
775
905
  that._getXHRPromise(false, options.context, aborted)) ||
776
906
  that._chunkedUpload(options) || $.ajax(options)
777
907
  ).done(function (result, textStatus, jqXHR) {
@@ -815,9 +945,10 @@
815
945
  if (this.options.limitConcurrentUploads > 1) {
816
946
  slot = $.Deferred();
817
947
  this._slots.push(slot);
818
- pipe = slot.pipe(send);
948
+ pipe = slot.then(send);
819
949
  } else {
820
- pipe = (this._sequence = this._sequence.pipe(send, send));
950
+ this._sequence = this._sequence.then(send, send);
951
+ pipe = this._sequence;
821
952
  }
822
953
  // Return the piped Promise object, enhanced with an abort method,
823
954
  // which is delegated to the jqXHR object of the current upload,
@@ -841,50 +972,93 @@
841
972
  var that = this,
842
973
  result = true,
843
974
  options = $.extend({}, this.options, data),
975
+ files = data.files,
976
+ filesLength = files.length,
844
977
  limit = options.limitMultiFileUploads,
978
+ limitSize = options.limitMultiFileUploadSize,
979
+ overhead = options.limitMultiFileUploadSizeOverhead,
980
+ batchSize = 0,
845
981
  paramName = this._getParamName(options),
846
982
  paramNameSet,
847
983
  paramNameSlice,
848
984
  fileSet,
849
- i;
850
- if (!(options.singleFileUploads || limit) ||
985
+ i,
986
+ j = 0;
987
+ if (!filesLength) {
988
+ return false;
989
+ }
990
+ if (limitSize && files[0].size === undefined) {
991
+ limitSize = undefined;
992
+ }
993
+ if (!(options.singleFileUploads || limit || limitSize) ||
851
994
  !this._isXHRUpload(options)) {
852
- fileSet = [data.files];
995
+ fileSet = [files];
853
996
  paramNameSet = [paramName];
854
- } else if (!options.singleFileUploads && limit) {
997
+ } else if (!(options.singleFileUploads || limitSize) && limit) {
855
998
  fileSet = [];
856
999
  paramNameSet = [];
857
- for (i = 0; i < data.files.length; i += limit) {
858
- fileSet.push(data.files.slice(i, i + limit));
1000
+ for (i = 0; i < filesLength; i += limit) {
1001
+ fileSet.push(files.slice(i, i + limit));
859
1002
  paramNameSlice = paramName.slice(i, i + limit);
860
1003
  if (!paramNameSlice.length) {
861
1004
  paramNameSlice = paramName;
862
1005
  }
863
1006
  paramNameSet.push(paramNameSlice);
864
1007
  }
1008
+ } else if (!options.singleFileUploads && limitSize) {
1009
+ fileSet = [];
1010
+ paramNameSet = [];
1011
+ for (i = 0; i < filesLength; i = i + 1) {
1012
+ batchSize += files[i].size + overhead;
1013
+ if (i + 1 === filesLength ||
1014
+ ((batchSize + files[i + 1].size + overhead) > limitSize) ||
1015
+ (limit && i + 1 - j >= limit)) {
1016
+ fileSet.push(files.slice(j, i + 1));
1017
+ paramNameSlice = paramName.slice(j, i + 1);
1018
+ if (!paramNameSlice.length) {
1019
+ paramNameSlice = paramName;
1020
+ }
1021
+ paramNameSet.push(paramNameSlice);
1022
+ j = i + 1;
1023
+ batchSize = 0;
1024
+ }
1025
+ }
865
1026
  } else {
866
1027
  paramNameSet = paramName;
867
1028
  }
868
- data.originalFiles = data.files;
869
- $.each(fileSet || data.files, function (index, element) {
1029
+ data.originalFiles = files;
1030
+ $.each(fileSet || files, function (index, element) {
870
1031
  var newData = $.extend({}, data);
871
1032
  newData.files = fileSet ? element : [element];
872
1033
  newData.paramName = paramNameSet[index];
873
1034
  that._initResponseObject(newData);
874
1035
  that._initProgressObject(newData);
875
1036
  that._addConvenienceMethods(e, newData);
876
- result = that._trigger('add', e, newData);
1037
+ result = that._trigger(
1038
+ 'add',
1039
+ $.Event('add', {delegatedEvent: e}),
1040
+ newData
1041
+ );
877
1042
  return result;
878
1043
  });
879
1044
  return result;
880
1045
  },
881
1046
 
882
- _replaceFileInput: function (input) {
883
- var inputClone = input.clone(true);
1047
+ _replaceFileInput: function (data) {
1048
+ var input = data.fileInput,
1049
+ inputClone = input.clone(true),
1050
+ restoreFocus = input.is(document.activeElement);
1051
+ // Add a reference for the new cloned file input to the data argument:
1052
+ data.fileInputClone = inputClone;
884
1053
  $('<form></form>').append(inputClone)[0].reset();
885
1054
  // Detaching allows to insert the fileInput on another form
886
1055
  // without loosing the file input value:
887
1056
  input.after(inputClone).detach();
1057
+ // If the fileInput had focus before it was detached,
1058
+ // restore focus to the inputClone.
1059
+ if (restoreFocus) {
1060
+ inputClone.focus();
1061
+ }
888
1062
  // Avoid memory leaks with the detached file input:
889
1063
  $.cleanData(input.unbind('remove'));
890
1064
  // Replace the original file input element in the fileInput
@@ -916,7 +1090,25 @@
916
1090
  // to be returned together in one set:
917
1091
  dfd.resolve([e]);
918
1092
  },
919
- dirReader;
1093
+ successHandler = function (entries) {
1094
+ that._handleFileTreeEntries(
1095
+ entries,
1096
+ path + entry.name + '/'
1097
+ ).done(function (files) {
1098
+ dfd.resolve(files);
1099
+ }).fail(errorHandler);
1100
+ },
1101
+ readEntries = function () {
1102
+ dirReader.readEntries(function (results) {
1103
+ if (!results.length) {
1104
+ successHandler(entries);
1105
+ } else {
1106
+ entries = entries.concat(results);
1107
+ readEntries();
1108
+ }
1109
+ }, errorHandler);
1110
+ },
1111
+ dirReader, entries = [];
920
1112
  path = path || '';
921
1113
  if (entry.isFile) {
922
1114
  if (entry._file) {
@@ -931,14 +1123,7 @@
931
1123
  }
932
1124
  } else if (entry.isDirectory) {
933
1125
  dirReader = entry.createReader();
934
- dirReader.readEntries(function (entries) {
935
- that._handleFileTreeEntries(
936
- entries,
937
- path + entry.name + '/'
938
- ).done(function (files) {
939
- dfd.resolve(files);
940
- }).fail(errorHandler);
941
- }, errorHandler);
1126
+ readEntries();
942
1127
  } else {
943
1128
  // Return an empy list for file system items
944
1129
  // other than files or directories:
@@ -954,7 +1139,7 @@
954
1139
  $.map(entries, function (entry) {
955
1140
  return that._handleFileTreeEntry(entry, path);
956
1141
  })
957
- ).pipe(function () {
1142
+ ).then(function () {
958
1143
  return Array.prototype.concat.apply(
959
1144
  [],
960
1145
  arguments
@@ -1023,7 +1208,7 @@
1023
1208
  return $.when.apply(
1024
1209
  $,
1025
1210
  $.map(fileInput, this._getSingleFileInputFiles)
1026
- ).pipe(function () {
1211
+ ).then(function () {
1027
1212
  return Array.prototype.concat.apply(
1028
1213
  [],
1029
1214
  arguments
@@ -1040,9 +1225,13 @@
1040
1225
  this._getFileInputFiles(data.fileInput).always(function (files) {
1041
1226
  data.files = files;
1042
1227
  if (that.options.replaceFileInput) {
1043
- that._replaceFileInput(data.fileInput);
1228
+ that._replaceFileInput(data);
1044
1229
  }
1045
- if (that._trigger('change', e, data) !== false) {
1230
+ if (that._trigger(
1231
+ 'change',
1232
+ $.Event('change', {delegatedEvent: e}),
1233
+ data
1234
+ ) !== false) {
1046
1235
  that._onAdd(e, data);
1047
1236
  }
1048
1237
  });
@@ -1059,71 +1248,76 @@
1059
1248
  data.files.push(file);
1060
1249
  }
1061
1250
  });
1062
- if (this._trigger('paste', e, data) === false ||
1063
- this._onAdd(e, data) === false) {
1064
- return false;
1251
+ if (this._trigger(
1252
+ 'paste',
1253
+ $.Event('paste', {delegatedEvent: e}),
1254
+ data
1255
+ ) !== false) {
1256
+ this._onAdd(e, data);
1065
1257
  }
1066
1258
  }
1067
1259
  },
1068
1260
 
1069
1261
  _onDrop: function (e) {
1262
+ e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
1070
1263
  var that = this,
1071
- dataTransfer = e.dataTransfer = e.originalEvent &&
1072
- e.originalEvent.dataTransfer,
1264
+ dataTransfer = e.dataTransfer,
1073
1265
  data = {};
1074
1266
  if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1075
1267
  e.preventDefault();
1076
1268
  this._getDroppedFiles(dataTransfer).always(function (files) {
1077
1269
  data.files = files;
1078
- if (that._trigger('drop', e, data) !== false) {
1270
+ if (that._trigger(
1271
+ 'drop',
1272
+ $.Event('drop', {delegatedEvent: e}),
1273
+ data
1274
+ ) !== false) {
1079
1275
  that._onAdd(e, data);
1080
1276
  }
1081
1277
  });
1082
1278
  }
1083
1279
  },
1084
1280
 
1085
- _onDragOver: function (e) {
1086
- var dataTransfer = e.dataTransfer = e.originalEvent &&
1087
- e.originalEvent.dataTransfer;
1088
- if (dataTransfer) {
1089
- if (this._trigger('dragover', e) === false) {
1090
- return false;
1091
- }
1092
- if ($.inArray('Files', dataTransfer.types) !== -1) {
1093
- dataTransfer.dropEffect = 'copy';
1094
- e.preventDefault();
1095
- }
1096
- }
1097
- },
1281
+ _onDragOver: getDragHandler('dragover'),
1282
+
1283
+ _onDragEnter: getDragHandler('dragenter'),
1284
+
1285
+ _onDragLeave: getDragHandler('dragleave'),
1098
1286
 
1099
1287
  _initEventHandlers: function () {
1100
1288
  if (this._isXHRUpload(this.options)) {
1101
1289
  this._on(this.options.dropZone, {
1102
1290
  dragover: this._onDragOver,
1103
- drop: this._onDrop
1291
+ drop: this._onDrop,
1292
+ // event.preventDefault() on dragenter is required for IE10+:
1293
+ dragenter: this._onDragEnter,
1294
+ // dragleave is not required, but added for completeness:
1295
+ dragleave: this._onDragLeave
1104
1296
  });
1105
1297
  this._on(this.options.pasteZone, {
1106
1298
  paste: this._onPaste
1107
1299
  });
1108
1300
  }
1109
- this._on(this.options.fileInput, {
1110
- change: this._onChange
1111
- });
1301
+ if ($.support.fileInput) {
1302
+ this._on(this.options.fileInput, {
1303
+ change: this._onChange
1304
+ });
1305
+ }
1112
1306
  },
1113
1307
 
1114
1308
  _destroyEventHandlers: function () {
1115
- this._off(this.options.dropZone, 'dragover drop');
1309
+ this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
1116
1310
  this._off(this.options.pasteZone, 'paste');
1117
1311
  this._off(this.options.fileInput, 'change');
1118
1312
  },
1119
1313
 
1120
1314
  _setOption: function (key, value) {
1121
- var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
1122
- if (refresh) {
1315
+ var reinit = $.inArray(key, this._specialOptions) !== -1;
1316
+ if (reinit) {
1123
1317
  this._destroyEventHandlers();
1124
1318
  }
1125
1319
  this._super(key, value);
1126
- if (refresh) {
1320
+ if (reinit) {
1127
1321
  this._initSpecialOptions();
1128
1322
  this._initEventHandlers();
1129
1323
  }
@@ -1145,10 +1339,45 @@
1145
1339
  }
1146
1340
  },
1147
1341
 
1148
- _create: function () {
1149
- var options = this.options;
1342
+ _getRegExp: function (str) {
1343
+ var parts = str.split('/'),
1344
+ modifiers = parts.pop();
1345
+ parts.shift();
1346
+ return new RegExp(parts.join('/'), modifiers);
1347
+ },
1348
+
1349
+ _isRegExpOption: function (key, value) {
1350
+ return key !== 'url' && $.type(value) === 'string' &&
1351
+ /^\/.*\/[igm]{0,3}$/.test(value);
1352
+ },
1353
+
1354
+ _initDataAttributes: function () {
1355
+ var that = this,
1356
+ options = this.options,
1357
+ data = this.element.data();
1150
1358
  // Initialize options set via HTML5 data-attributes:
1151
- $.extend(options, $(this.element[0].cloneNode(false)).data());
1359
+ $.each(
1360
+ this.element[0].attributes,
1361
+ function (index, attr) {
1362
+ var key = attr.name.toLowerCase(),
1363
+ value;
1364
+ if (/^data-/.test(key)) {
1365
+ // Convert hyphen-ated key to camelCase:
1366
+ key = key.slice(5).replace(/-[a-z]/g, function (str) {
1367
+ return str.charAt(1).toUpperCase();
1368
+ });
1369
+ value = data[key];
1370
+ if (that._isRegExpOption(key, value)) {
1371
+ value = that._getRegExp(value);
1372
+ }
1373
+ options[key] = value;
1374
+ }
1375
+ }
1376
+ );
1377
+ },
1378
+
1379
+ _create: function () {
1380
+ this._initDataAttributes();
1152
1381
  this._initSpecialOptions();
1153
1382
  this._slots = [];
1154
1383
  this._sequence = this._getXHRPromise(true);
@@ -1217,8 +1446,13 @@
1217
1446
  if (aborted) {
1218
1447
  return;
1219
1448
  }
1449
+ if (!files.length) {
1450
+ dfd.reject();
1451
+ return;
1452
+ }
1220
1453
  data.files = files;
1221
- jqXHR = that._onSend(null, data).then(
1454
+ jqXHR = that._onSend(null, data);
1455
+ jqXHR.then(
1222
1456
  function (result, textStatus, jqXHR) {
1223
1457
  dfd.resolve(result, textStatus, jqXHR);
1224
1458
  },