glebtv-rails-uploader 0.1.3 → 0.2.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +17 -10
  3. data/{vendor → app}/assets/images/uploader/but_del_tag2.png +0 -0
  4. data/{vendor → app}/assets/images/uploader/ico_attach.png +0 -0
  5. data/{vendor → app}/assets/images/uploader/preloader.gif +0 -0
  6. data/{vendor → app}/assets/images/uploader/progressBarFillBg.png +0 -0
  7. data/app/assets/javascripts/uploader/application.js +2 -2
  8. data/app/assets/javascripts/uploader/canvas-to-blob.js +95 -0
  9. data/app/assets/javascripts/uploader/jquery.fileupload-angular.js +348 -0
  10. data/app/assets/javascripts/uploader/jquery.fileupload-process.js +158 -0
  11. data/app/assets/javascripts/uploader/jquery.fileupload-resize.js +212 -0
  12. data/{vendor → app}/assets/javascripts/uploader/jquery.fileupload-ui.js +265 -269
  13. data/app/assets/javascripts/uploader/jquery.fileupload-validate.js +116 -0
  14. data/{vendor → app}/assets/javascripts/uploader/jquery.fileupload.js +655 -258
  15. data/{vendor → app}/assets/javascripts/uploader/jquery.iframe-transport.js +29 -9
  16. data/app/assets/javascripts/uploader/jquery.ui.widget.js +530 -0
  17. data/app/assets/javascripts/uploader/load-image.js +381 -0
  18. data/{vendor → app}/assets/javascripts/uploader/locales/en.js +0 -0
  19. data/{vendor → app}/assets/javascripts/uploader/locales/ru.js +0 -0
  20. data/{vendor → app}/assets/javascripts/uploader/locales/uk.js +0 -0
  21. data/app/assets/javascripts/uploader/rails_admin.js +26 -24
  22. data/app/assets/javascripts/uploader/tmpl.js +86 -0
  23. data/{vendor → app}/assets/stylesheets/uploader/default.css +0 -0
  24. data/{vendor → app}/assets/stylesheets/uploader/jquery.fileupload-ui.css +0 -0
  25. data/app/controllers/uploader/attachments_controller.rb +13 -10
  26. data/app/views/rails_admin/main/_form_rails_uploader.haml +9 -9
  27. data/app/views/uploader/default/_container.html.erb +1 -2
  28. data/app/views/uploader/default/_download.html.erb +1 -1
  29. data/lib/uploader/asset.rb +2 -2
  30. data/lib/uploader/engine.rb +4 -4
  31. data/lib/uploader/fileuploads.rb +18 -18
  32. data/lib/uploader/helpers/field_tag.rb +16 -17
  33. data/lib/uploader/helpers/form_builder.rb +1 -1
  34. data/lib/uploader/helpers/form_tag_helper.rb +1 -1
  35. data/lib/uploader/hooks/formtastic.rb +1 -2
  36. data/lib/uploader/rails_admin/field.rb +27 -27
  37. data/lib/uploader/version.rb +1 -1
  38. metadata +115 -131
  39. data/vendor/assets/javascripts/uploader/jquery.fileupload-ip.js +0 -160
  40. data/vendor/assets/javascripts/uploader/jquery.ui.widget.js +0 -282
  41. data/vendor/assets/javascripts/uploader/load-image.min.js +0 -1
  42. data/vendor/assets/javascripts/uploader/tmpl.min.js +0 -1
@@ -0,0 +1,116 @@
1
+ /*
2
+ * jQuery File Upload Validation Plugin 1.0.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
+ /*jslint nomen: true, unparam: true, regexp: true */
13
+ /*global define, window */
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
+ './jquery.fileupload-process'
22
+ ], factory);
23
+ } else {
24
+ // Browser globals:
25
+ factory(
26
+ window.jQuery
27
+ );
28
+ }
29
+ }(function ($) {
30
+ 'use strict';
31
+
32
+ // Append to the default processQueue:
33
+ $.blueimp.fileupload.prototype.options.processQueue.push(
34
+ {
35
+ action: 'validate',
36
+ // Always trigger this action,
37
+ // even if the previous action was rejected:
38
+ always: true,
39
+ // Options taken from the global options map:
40
+ acceptFileTypes: '@acceptFileTypes',
41
+ maxFileSize: '@maxFileSize',
42
+ minFileSize: '@minFileSize',
43
+ maxNumberOfFiles: '@maxNumberOfFiles',
44
+ disabled: '@disableValidation'
45
+ }
46
+ );
47
+
48
+ // The File Upload Validation plugin extends the fileupload widget
49
+ // with file validation functionality:
50
+ $.widget('blueimp.fileupload', $.blueimp.fileupload, {
51
+
52
+ options: {
53
+ /*
54
+ // The regular expression for allowed file types, matches
55
+ // against either file type or file name:
56
+ acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
57
+ // The maximum allowed file size in bytes:
58
+ maxFileSize: 10000000, // 10 MB
59
+ // The minimum allowed file size in bytes:
60
+ minFileSize: undefined, // No minimal file size
61
+ // The limit of files to be uploaded:
62
+ maxNumberOfFiles: 10,
63
+ */
64
+
65
+ // Function returning the current number of files,
66
+ // has to be overriden for maxNumberOfFiles validation:
67
+ getNumberOfFiles: $.noop,
68
+
69
+ // Error and info messages:
70
+ messages: {
71
+ maxNumberOfFiles: 'Maximum number of files exceeded',
72
+ acceptFileTypes: 'File type not allowed',
73
+ maxFileSize: 'File is too large',
74
+ minFileSize: 'File is too small'
75
+ }
76
+ },
77
+
78
+ processActions: {
79
+
80
+ validate: function (data, options) {
81
+ if (options.disabled) {
82
+ return data;
83
+ }
84
+ var dfd = $.Deferred(),
85
+ settings = this.options,
86
+ file = data.files[data.index],
87
+ numberOfFiles = settings.getNumberOfFiles();
88
+ if (numberOfFiles && $.type(options.maxNumberOfFiles) === 'number' &&
89
+ numberOfFiles + data.files.length > options.maxNumberOfFiles) {
90
+ file.error = settings.i18n('maxNumberOfFiles');
91
+ } else if (options.acceptFileTypes &&
92
+ !(options.acceptFileTypes.test(file.type) ||
93
+ options.acceptFileTypes.test(file.name))) {
94
+ file.error = settings.i18n('acceptFileTypes');
95
+ } else if (options.maxFileSize && file.size > options.maxFileSize) {
96
+ file.error = settings.i18n('maxFileSize');
97
+ } else if ($.type(file.size) === 'number' &&
98
+ file.size < options.minFileSize) {
99
+ file.error = settings.i18n('minFileSize');
100
+ } else {
101
+ delete file.error;
102
+ }
103
+ if (file.error || data.files.error) {
104
+ data.files.error = true;
105
+ dfd.rejectWith(this, [data]);
106
+ } else {
107
+ dfd.resolveWith(this, [data]);
108
+ }
109
+ return dfd.promise();
110
+ }
111
+
112
+ }
113
+
114
+ });
115
+
116
+ }));
@@ -1,5 +1,5 @@
1
1
  /*
2
- * jQuery File Upload Plugin 5.10.1
2
+ * jQuery File Upload Plugin 5.31.1
3
3
  * https://github.com/blueimp/jQuery-File-Upload
4
4
  *
5
5
  * Copyright 2010, Sebastian Tschan
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  /*jslint nomen: true, unparam: true, regexp: true */
13
- /*global define, window, document, Blob, FormData, location */
13
+ /*global define, window, document, File, Blob, FormData, location */
14
14
 
15
15
  (function (factory) {
16
16
  'use strict';
@@ -44,17 +44,16 @@
44
44
  $.widget('blueimp.fileupload', {
45
45
 
46
46
  options: {
47
- // The namespace used for event handler binding on the dropZone and
48
- // fileInput collections.
49
- // If not set, the name of the widget ("fileupload") is used.
50
- namespace: undefined,
51
- // The drop target collection, by the default the complete document.
52
- // Set to null or an empty collection to disable drag & drop support:
47
+ // The drop target element(s), by the default the complete document.
48
+ // Set to null to disable drag & drop support:
53
49
  dropZone: $(document),
54
- // The file input field collection, that is listened for change events.
50
+ // The paste target element(s), by the default the complete document.
51
+ // Set to null to disable paste support:
52
+ pasteZone: $(document),
53
+ // The file input field(s), that are listened to for change events.
55
54
  // If undefined, it is set to the file input fields inside
56
55
  // of the widget element on plugin initialization.
57
- // Set to null or an empty collection to disable the change listener.
56
+ // Set to null to disable the change listener.
58
57
  fileInput: undefined,
59
58
  // By default, the file input field is replaced with a clone after
60
59
  // each input field change event. This is required for iframe transport
@@ -109,6 +108,29 @@
109
108
  // global progress calculation. Set the following option to false to
110
109
  // prevent recalculating the global progress data:
111
110
  recalculateProgress: true,
111
+ // Interval in milliseconds to calculate and trigger progress events:
112
+ progressInterval: 100,
113
+ // Interval in milliseconds to calculate progress bitrate:
114
+ bitrateInterval: 500,
115
+ // By default, uploads are started automatically when adding files:
116
+ autoUpload: true,
117
+
118
+ // Error and info messages:
119
+ messages: {
120
+ uploadedBytes: 'Uploaded bytes exceed file size'
121
+ },
122
+
123
+ // Translation function, gets the message key to be translated
124
+ // and an object with context specific data as arguments:
125
+ i18n: function (message, context) {
126
+ message = this.messages[message] || message.toString();
127
+ if (context) {
128
+ $.each(context, function (key, value) {
129
+ message = message.replace('{' + key + '}', value);
130
+ });
131
+ }
132
+ return message;
133
+ },
112
134
 
113
135
  // Additional form data to be sent along with the file uploads can be set
114
136
  // using this option, which accepts an array of objects with name and
@@ -133,37 +155,67 @@
133
155
  // handlers using jQuery's Deferred callbacks:
134
156
  // data.submit().done(func).fail(func).always(func);
135
157
  add: function (e, data) {
136
- data.submit();
158
+ if (data.autoUpload || (data.autoUpload !== false &&
159
+ $(this).fileupload('option', 'autoUpload'))) {
160
+ data.process().done(function () {
161
+ data.submit();
162
+ });
163
+ }
137
164
  },
138
165
 
139
166
  // Other callbacks:
167
+
140
168
  // Callback for the submit event of each file upload:
141
169
  // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
170
+
142
171
  // Callback for the start of each file upload request:
143
172
  // send: function (e, data) {}, // .bind('fileuploadsend', func);
173
+
144
174
  // Callback for successful uploads:
145
175
  // done: function (e, data) {}, // .bind('fileuploaddone', func);
176
+
146
177
  // Callback for failed (abort or error) uploads:
147
178
  // fail: function (e, data) {}, // .bind('fileuploadfail', func);
179
+
148
180
  // Callback for completed (success, abort or error) requests:
149
181
  // always: function (e, data) {}, // .bind('fileuploadalways', func);
182
+
150
183
  // Callback for upload progress events:
151
184
  // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
185
+
152
186
  // Callback for global upload progress events:
153
187
  // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
188
+
154
189
  // Callback for uploads start, equivalent to the global ajaxStart event:
155
190
  // start: function (e) {}, // .bind('fileuploadstart', func);
191
+
156
192
  // Callback for uploads stop, equivalent to the global ajaxStop event:
157
193
  // stop: function (e) {}, // .bind('fileuploadstop', func);
158
- // Callback for change events of the fileInput collection:
194
+
195
+ // Callback for change events of the fileInput(s):
159
196
  // change: function (e, data) {}, // .bind('fileuploadchange', func);
160
- // Callback for paste events to the dropZone collection:
197
+
198
+ // Callback for paste events to the pasteZone(s):
161
199
  // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
162
- // Callback for drop events of the dropZone collection:
200
+
201
+ // Callback for drop events of the dropZone(s):
163
202
  // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
164
- // Callback for dragover events of the dropZone collection:
203
+
204
+ // Callback for dragover events of the dropZone(s):
165
205
  // dragover: function (e) {}, // .bind('fileuploaddragover', func);
166
206
 
207
+ // Callback for the start of each chunk upload request:
208
+ // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
209
+
210
+ // Callback for successful chunk uploads:
211
+ // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
212
+
213
+ // Callback for failed (abort or error) chunk uploads:
214
+ // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
215
+
216
+ // Callback for completed (success, abort or error) chunk upload requests:
217
+ // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
218
+
167
219
  // The plugin options are used as settings object for the ajax calls.
168
220
  // The following are jQuery ajax settings required for the file uploads:
169
221
  processData: false,
@@ -171,15 +223,31 @@
171
223
  cache: false
172
224
  },
173
225
 
174
- // A list of options that require a refresh after assigning a new value:
175
- _refreshOptionsList: [
176
- 'namespace',
177
- 'dropZone',
226
+ // A list of options that require reinitializing event listeners and/or
227
+ // special initialization code:
228
+ _specialOptions: [
178
229
  'fileInput',
230
+ 'dropZone',
231
+ 'pasteZone',
179
232
  'multipart',
180
233
  'forceIframeTransport'
181
234
  ],
182
235
 
236
+ _BitrateTimer: function () {
237
+ this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
238
+ this.loaded = 0;
239
+ this.bitrate = 0;
240
+ this.getBitrate = function (now, loaded, interval) {
241
+ var timeDiff = now - this.timestamp;
242
+ if (!this.bitrate || !interval || timeDiff > interval) {
243
+ this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
244
+ this.loaded = loaded;
245
+ this.timestamp = now;
246
+ }
247
+ return this.bitrate;
248
+ };
249
+ },
250
+
183
251
  _isXHRUpload: function (options) {
184
252
  return !options.forceIframeTransport &&
185
253
  ((!options.multipart && $.support.xhrFileUpload) ||
@@ -190,9 +258,11 @@
190
258
  var formData;
191
259
  if (typeof options.formData === 'function') {
192
260
  return options.formData(options.form);
193
- } else if ($.isArray(options.formData)) {
261
+ }
262
+ if ($.isArray(options.formData)) {
194
263
  return options.formData;
195
- } else if (options.formData) {
264
+ }
265
+ if ($.type(options.formData) === 'object') {
196
266
  formData = [];
197
267
  $.each(options.formData, function (name, value) {
198
268
  formData.push({name: name, value: value});
@@ -210,28 +280,66 @@
210
280
  return total;
211
281
  },
212
282
 
283
+ _initProgressObject: function (obj) {
284
+ var progress = {
285
+ loaded: 0,
286
+ total: 0,
287
+ bitrate: 0
288
+ };
289
+ if (obj._progress) {
290
+ $.extend(obj._progress, progress);
291
+ } else {
292
+ obj._progress = progress;
293
+ }
294
+ },
295
+
296
+ _initResponseObject: function (obj) {
297
+ var prop;
298
+ if (obj._response) {
299
+ for (prop in obj._response) {
300
+ if (obj._response.hasOwnProperty(prop)) {
301
+ delete obj._response[prop];
302
+ }
303
+ }
304
+ } else {
305
+ obj._response = {};
306
+ }
307
+ },
308
+
213
309
  _onProgress: function (e, data) {
214
310
  if (e.lengthComputable) {
215
- var total = data.total || this._getTotal(data.files),
216
- loaded = parseInt(
217
- e.loaded / e.total * (data.chunkSize || total),
218
- 10
219
- ) + (data.uploadedBytes || 0);
220
- this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
221
- data.lengthComputable = true;
222
- data.loaded = loaded;
223
- data.total = total;
311
+ var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
312
+ loaded;
313
+ if (data._time && data.progressInterval &&
314
+ (now - data._time < data.progressInterval) &&
315
+ e.loaded !== e.total) {
316
+ return;
317
+ }
318
+ data._time = now;
319
+ loaded = Math.floor(
320
+ e.loaded / e.total * (data.chunkSize || data._progress.total)
321
+ ) + (data.uploadedBytes || 0);
322
+ // Add the difference from the previously loaded state
323
+ // to the global loaded counter:
324
+ this._progress.loaded += (loaded - data._progress.loaded);
325
+ this._progress.bitrate = this._bitrateTimer.getBitrate(
326
+ now,
327
+ this._progress.loaded,
328
+ data.bitrateInterval
329
+ );
330
+ data._progress.loaded = data.loaded = loaded;
331
+ data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
332
+ now,
333
+ loaded,
334
+ data.bitrateInterval
335
+ );
224
336
  // Trigger a custom progress event with a total data property set
225
337
  // to the file size(s) of the current upload and a loaded data
226
338
  // property calculated accordingly:
227
339
  this._trigger('progress', e, data);
228
340
  // Trigger a global progress event for all current file uploads,
229
341
  // including ajax calls queued for sequential file uploads:
230
- this._trigger('progressall', e, {
231
- lengthComputable: true,
232
- loaded: this._loaded,
233
- total: this._total
234
- });
342
+ this._trigger('progressall', e, this._progress);
235
343
  }
236
344
  },
237
345
 
@@ -255,35 +363,28 @@
255
363
  }
256
364
  },
257
365
 
366
+ _isInstanceOf: function (type, obj) {
367
+ // Cross-frame instanceof check
368
+ return Object.prototype.toString.call(obj) === '[object ' + type + ']';
369
+ },
370
+
258
371
  _initXHRData: function (options) {
259
- var formData,
372
+ var that = this,
373
+ formData,
260
374
  file = options.files[0],
261
375
  // Ignore non-multipart setting if not supported:
262
376
  multipart = options.multipart || !$.support.xhrFileUpload,
263
377
  paramName = options.paramName[0];
264
- if (!multipart || options.blob) {
265
- // For non-multipart uploads and chunked uploads,
266
- // file meta data is not part of the request body,
267
- // so we transmit this data as part of the HTTP headers.
268
- // For cross domain requests, these headers must be allowed
269
- // via Access-Control-Allow-Headers or removed using
270
- // the beforeSend callback:
271
- options.headers = $.extend(options.headers, {
272
- 'X-File-Name': file.name,
273
- 'X-File-Type': file.type,
274
- 'X-File-Size': file.size
275
- });
276
- if (!options.blob) {
277
- // Non-chunked non-multipart upload:
278
- options.contentType = file.type;
279
- options.data = file;
280
- } else if (!multipart) {
281
- // Chunked non-multipart upload:
282
- options.contentType = 'application/octet-stream';
283
- options.data = options.blob;
284
- }
378
+ options.headers = options.headers || {};
379
+ if (options.contentRange) {
380
+ options.headers['Content-Range'] = options.contentRange;
285
381
  }
286
- if (multipart && $.support.xhrFormDataFileUpload) {
382
+ if (!multipart) {
383
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
384
+ encodeURI(file.name) + '"';
385
+ options.contentType = file.type;
386
+ options.data = options.blob || file;
387
+ } else if ($.support.xhrFormDataFileUpload) {
287
388
  if (options.postMessage) {
288
389
  // window.postMessage does not allow sending FormData
289
390
  // objects, so we just add the File/Blob objects to
@@ -304,7 +405,7 @@
304
405
  });
305
406
  }
306
407
  } else {
307
- if (options.formData instanceof FormData) {
408
+ if (that._isInstanceOf('FormData', options.formData)) {
308
409
  formData = options.formData;
309
410
  } else {
310
411
  formData = new FormData();
@@ -313,13 +414,15 @@
313
414
  });
314
415
  }
315
416
  if (options.blob) {
417
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
418
+ encodeURI(file.name) + '"';
316
419
  formData.append(paramName, options.blob, file.name);
317
420
  } else {
318
421
  $.each(options.files, function (index, file) {
319
- // File objects are also Blob instances.
320
422
  // This check allows the tests to run with
321
423
  // dummy objects:
322
- if (file instanceof Blob) {
424
+ if (that._isInstanceOf('File', file) ||
425
+ that._isInstanceOf('Blob', file)) {
323
426
  formData.append(
324
427
  options.paramName[index] || paramName,
325
428
  file,
@@ -364,7 +467,7 @@
364
467
  options.dataType = 'postmessage ' + (options.dataType || '');
365
468
  }
366
469
  } else {
367
- this._initIframeSettings(options, 'iframe');
470
+ this._initIframeSettings(options);
368
471
  }
369
472
  },
370
473
 
@@ -396,6 +499,11 @@
396
499
  // associated form, if available:
397
500
  if (!options.form || !options.form.length) {
398
501
  options.form = $(options.fileInput.prop('form'));
502
+ // If the given file input doesn't have an associated form,
503
+ // use the default widget file input's form:
504
+ if (!options.form.length) {
505
+ options.form = $(this.options.fileInput.prop('form'));
506
+ }
399
507
  }
400
508
  options.paramName = this._getParamName(options);
401
509
  if (!options.url) {
@@ -404,9 +512,13 @@
404
512
  // The HTTP request method must be "POST" or "PUT":
405
513
  options.type = (options.type || options.form.prop('method') || '')
406
514
  .toUpperCase();
407
- if (options.type !== 'POST' && options.type !== 'PUT') {
515
+ if (options.type !== 'POST' && options.type !== 'PUT' &&
516
+ options.type !== 'PATCH') {
408
517
  options.type = 'POST';
409
518
  }
519
+ if (!options.formAcceptCharset) {
520
+ options.formAcceptCharset = options.form.attr('accept-charset');
521
+ }
410
522
  },
411
523
 
412
524
  _getAJAXSettings: function (data) {
@@ -416,6 +528,21 @@
416
528
  return options;
417
529
  },
418
530
 
531
+ // jQuery 1.6 doesn't provide .state(),
532
+ // while jQuery 1.8+ removed .isRejected() and .isResolved():
533
+ _getDeferredState: function (deferred) {
534
+ if (deferred.state) {
535
+ return deferred.state();
536
+ }
537
+ if (deferred.isResolved()) {
538
+ return 'resolved';
539
+ }
540
+ if (deferred.isRejected()) {
541
+ return 'rejected';
542
+ }
543
+ return 'pending';
544
+ },
545
+
419
546
  // Maps jqXHR callbacks to the equivalent
420
547
  // methods of the given Promise object:
421
548
  _enhancePromise: function (promise) {
@@ -440,6 +567,60 @@
440
567
  return this._enhancePromise(promise);
441
568
  },
442
569
 
570
+ // Adds convenience methods to the data callback argument:
571
+ _addConvenienceMethods: function (e, data) {
572
+ var that = this,
573
+ getPromise = function (data) {
574
+ return $.Deferred().resolveWith(that, [data]).promise();
575
+ };
576
+ data.process = function (resolveFunc, rejectFunc) {
577
+ if (resolveFunc || rejectFunc) {
578
+ data._processQueue = this._processQueue =
579
+ (this._processQueue || getPromise(this))
580
+ .pipe(resolveFunc, rejectFunc);
581
+ }
582
+ return this._processQueue || getPromise(this);
583
+ };
584
+ data.submit = function () {
585
+ if (this.state() !== 'pending') {
586
+ data.jqXHR = this.jqXHR =
587
+ (that._trigger('submit', e, this) !== false) &&
588
+ that._onSend(e, this);
589
+ }
590
+ return this.jqXHR || that._getXHRPromise();
591
+ };
592
+ data.abort = function () {
593
+ if (this.jqXHR) {
594
+ return this.jqXHR.abort();
595
+ }
596
+ return that._getXHRPromise();
597
+ };
598
+ data.state = function () {
599
+ if (this.jqXHR) {
600
+ return that._getDeferredState(this.jqXHR);
601
+ }
602
+ if (this._processQueue) {
603
+ return that._getDeferredState(this._processQueue);
604
+ }
605
+ };
606
+ data.progress = function () {
607
+ return this._progress;
608
+ };
609
+ data.response = function () {
610
+ return this._response;
611
+ };
612
+ },
613
+
614
+ // Parses the Range header from the server response
615
+ // and returns the uploaded bytes:
616
+ _getUploadedBytes: function (jqXHR) {
617
+ var range = jqXHR.getResponseHeader('Range'),
618
+ parts = range && range.split('-'),
619
+ upperBytesPos = parts && parts.length > 1 &&
620
+ parseInt(parts[1], 10);
621
+ return upperBytesPos && upperBytesPos + 1;
622
+ },
623
+
443
624
  // Uploads a file in multiple, sequential requests
444
625
  // by splitting the file up in multiple blob chunks.
445
626
  // If the second parameter is true, only tests if the file
@@ -451,13 +632,11 @@
451
632
  fs = file.size,
452
633
  ub = options.uploadedBytes = options.uploadedBytes || 0,
453
634
  mcs = options.maxChunkSize || fs,
454
- // Use the Blob methods with the slice implementation
455
- // according to the W3C Blob API specification:
456
- slice = file.webkitSlice || file.mozSlice || file.slice,
457
- upload,
458
- n,
635
+ slice = file.slice || file.webkitSlice || file.mozSlice,
636
+ dfd = $.Deferred(),
637
+ promise = dfd.promise(),
459
638
  jqXHR,
460
- pipe;
639
+ upload;
461
640
  if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
462
641
  options.data) {
463
642
  return false;
@@ -466,62 +645,84 @@
466
645
  return true;
467
646
  }
468
647
  if (ub >= fs) {
469
- file.error = 'uploadedBytes';
648
+ file.error = options.i18n('uploadedBytes');
470
649
  return this._getXHRPromise(
471
650
  false,
472
651
  options.context,
473
652
  [null, 'error', file.error]
474
653
  );
475
654
  }
476
- // n is the number of blobs to upload,
477
- // calculated via filesize, uploaded bytes and max chunk size:
478
- n = Math.ceil((fs - ub) / mcs);
479
- // The chunk upload method accepting the chunk number as parameter:
480
- upload = function (i) {
481
- if (!i) {
482
- return that._getXHRPromise(true, options.context);
483
- }
484
- // Upload the blobs in sequential order:
485
- return upload(i -= 1).pipe(function () {
486
- // Clone the options object for each chunk upload:
487
- var o = $.extend({}, options);
488
- o.blob = slice.call(
489
- file,
490
- ub + i * mcs,
491
- ub + (i + 1) * mcs
492
- );
493
- // Store the current chunk size, as the blob itself
494
- // will be dereferenced after data processing:
495
- o.chunkSize = o.blob.size;
496
- // Process the upload data (the blob and potential form data):
497
- that._initXHRData(o);
498
- // Add progress listeners for this chunk upload:
499
- that._initProgressListener(o);
500
- jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context))
501
- .done(function () {
502
- // Create a progress event if upload is done and
503
- // no progress event has been invoked for this chunk:
504
- if (!o.loaded) {
505
- that._onProgress($.Event('progress', {
506
- lengthComputable: true,
507
- loaded: o.chunkSize,
508
- total: o.chunkSize
509
- }), o);
510
- }
511
- options.uploadedBytes = o.uploadedBytes +=
512
- o.chunkSize;
513
- });
514
- return jqXHR;
515
- });
655
+ // The chunk upload method:
656
+ upload = function () {
657
+ // Clone the options object for each chunk upload:
658
+ var o = $.extend({}, options),
659
+ currentLoaded = o._progress.loaded;
660
+ o.blob = slice.call(
661
+ file,
662
+ ub,
663
+ ub + mcs,
664
+ file.type
665
+ );
666
+ // Store the current chunk size, as the blob itself
667
+ // will be dereferenced after data processing:
668
+ o.chunkSize = o.blob.size;
669
+ // Expose the chunk bytes position range:
670
+ o.contentRange = 'bytes ' + ub + '-' +
671
+ (ub + o.chunkSize - 1) + '/' + fs;
672
+ // Process the upload data (the blob and potential form data):
673
+ that._initXHRData(o);
674
+ // Add progress listeners for this chunk upload:
675
+ that._initProgressListener(o);
676
+ jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
677
+ that._getXHRPromise(false, o.context))
678
+ .done(function (result, textStatus, jqXHR) {
679
+ ub = that._getUploadedBytes(jqXHR) ||
680
+ (ub + o.chunkSize);
681
+ // Create a progress event if no final progress event
682
+ // with loaded equaling total has been triggered
683
+ // for this chunk:
684
+ if (currentLoaded + o.chunkSize - o._progress.loaded) {
685
+ that._onProgress($.Event('progress', {
686
+ lengthComputable: true,
687
+ loaded: ub - o.uploadedBytes,
688
+ total: ub - o.uploadedBytes
689
+ }), o);
690
+ }
691
+ options.uploadedBytes = o.uploadedBytes = ub;
692
+ o.result = result;
693
+ o.textStatus = textStatus;
694
+ o.jqXHR = jqXHR;
695
+ that._trigger('chunkdone', null, o);
696
+ that._trigger('chunkalways', null, o);
697
+ if (ub < fs) {
698
+ // File upload not yet complete,
699
+ // continue with the next chunk:
700
+ upload();
701
+ } else {
702
+ dfd.resolveWith(
703
+ o.context,
704
+ [result, textStatus, jqXHR]
705
+ );
706
+ }
707
+ })
708
+ .fail(function (jqXHR, textStatus, errorThrown) {
709
+ o.jqXHR = jqXHR;
710
+ o.textStatus = textStatus;
711
+ o.errorThrown = errorThrown;
712
+ that._trigger('chunkfail', null, o);
713
+ that._trigger('chunkalways', null, o);
714
+ dfd.rejectWith(
715
+ o.context,
716
+ [jqXHR, textStatus, errorThrown]
717
+ );
718
+ });
516
719
  };
517
- // Return the piped Promise object, enhanced with an abort method,
518
- // which is delegated to the jqXHR object of the current upload,
519
- // and jqXHR callbacks mapped to the equivalent Promise methods:
520
- pipe = upload(n);
521
- pipe.abort = function () {
720
+ this._enhancePromise(promise);
721
+ promise.abort = function () {
522
722
  return jqXHR.abort();
523
723
  };
524
- return this._enhancePromise(pipe);
724
+ upload();
725
+ return promise;
525
726
  },
526
727
 
527
728
  _beforeSend: function (e, data) {
@@ -530,99 +731,113 @@
530
731
  // and no other uploads are currently running,
531
732
  // equivalent to the global ajaxStart event:
532
733
  this._trigger('start');
734
+ // Set timer for global bitrate progress calculation:
735
+ this._bitrateTimer = new this._BitrateTimer();
736
+ // Reset the global progress values:
737
+ this._progress.loaded = this._progress.total = 0;
738
+ this._progress.bitrate = 0;
533
739
  }
740
+ // Make sure the container objects for the .response() and
741
+ // .progress() methods on the data object are available
742
+ // and reset to their initial state:
743
+ this._initResponseObject(data);
744
+ this._initProgressObject(data);
745
+ data._progress.loaded = data.loaded = data.uploadedBytes || 0;
746
+ data._progress.total = data.total = this._getTotal(data.files) || 1;
747
+ data._progress.bitrate = data.bitrate = 0;
534
748
  this._active += 1;
535
749
  // Initialize the global progress values:
536
- this._loaded += data.uploadedBytes || 0;
537
- this._total += this._getTotal(data.files);
750
+ this._progress.loaded += data.loaded;
751
+ this._progress.total += data.total;
538
752
  },
539
753
 
540
754
  _onDone: function (result, textStatus, jqXHR, options) {
541
- if (!this._isXHRUpload(options)) {
542
- // Create a progress event for each iframe load:
755
+ var total = options._progress.total,
756
+ response = options._response;
757
+ if (options._progress.loaded < total) {
758
+ // Create a progress event if no final progress event
759
+ // with loaded equaling total has been triggered:
543
760
  this._onProgress($.Event('progress', {
544
761
  lengthComputable: true,
545
- loaded: 1,
546
- total: 1
762
+ loaded: total,
763
+ total: total
547
764
  }), options);
548
765
  }
549
- options.result = result;
550
- options.textStatus = textStatus;
551
- options.jqXHR = jqXHR;
766
+ response.result = options.result = result;
767
+ response.textStatus = options.textStatus = textStatus;
768
+ response.jqXHR = options.jqXHR = jqXHR;
552
769
  this._trigger('done', null, options);
553
770
  },
554
771
 
555
772
  _onFail: function (jqXHR, textStatus, errorThrown, options) {
556
- options.jqXHR = jqXHR;
557
- options.textStatus = textStatus;
558
- options.errorThrown = errorThrown;
559
- this._trigger('fail', null, options);
773
+ var response = options._response;
560
774
  if (options.recalculateProgress) {
561
775
  // Remove the failed (error or abort) file upload from
562
776
  // the global progress calculation:
563
- this._loaded -= options.loaded || options.uploadedBytes || 0;
564
- this._total -= options.total || this._getTotal(options.files);
777
+ this._progress.loaded -= options._progress.loaded;
778
+ this._progress.total -= options._progress.total;
565
779
  }
780
+ response.jqXHR = options.jqXHR = jqXHR;
781
+ response.textStatus = options.textStatus = textStatus;
782
+ response.errorThrown = options.errorThrown = errorThrown;
783
+ this._trigger('fail', null, options);
566
784
  },
567
785
 
568
786
  _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
569
- this._active -= 1;
570
- options.textStatus = textStatus;
571
- if (jqXHRorError && jqXHRorError.always) {
572
- options.jqXHR = jqXHRorError;
573
- options.result = jqXHRorResult;
574
- } else {
575
- options.jqXHR = jqXHRorResult;
576
- options.errorThrown = jqXHRorError;
577
- }
787
+ // jqXHRorResult, textStatus and jqXHRorError are added to the
788
+ // options object via done and fail callbacks
578
789
  this._trigger('always', null, options);
579
- if (this._active === 0) {
580
- // The stop callback is triggered when all uploads have
581
- // been completed, equivalent to the global ajaxStop event:
582
- this._trigger('stop');
583
- // Reset the global progress values:
584
- this._loaded = this._total = 0;
585
- }
586
790
  },
587
791
 
588
792
  _onSend: function (e, data) {
793
+ if (!data.submit) {
794
+ this._addConvenienceMethods(e, data);
795
+ }
589
796
  var that = this,
590
797
  jqXHR,
798
+ aborted,
591
799
  slot,
592
800
  pipe,
593
801
  options = that._getAJAXSettings(data),
594
- send = function (resolve, args) {
802
+ send = function () {
595
803
  that._sending += 1;
804
+ // Set timer for bitrate progress calculation:
805
+ options._bitrateTimer = new that._BitrateTimer();
596
806
  jqXHR = jqXHR || (
597
- (resolve !== false &&
598
- that._trigger('send', e, options) !== false &&
599
- (that._chunkedUpload(options) || $.ajax(options))) ||
600
- that._getXHRPromise(false, options.context, args)
807
+ ((aborted || that._trigger('send', e, options) === false) &&
808
+ that._getXHRPromise(false, options.context, aborted)) ||
809
+ that._chunkedUpload(options) || $.ajax(options)
601
810
  ).done(function (result, textStatus, jqXHR) {
602
811
  that._onDone(result, textStatus, jqXHR, options);
603
812
  }).fail(function (jqXHR, textStatus, errorThrown) {
604
813
  that._onFail(jqXHR, textStatus, errorThrown, options);
605
814
  }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
606
- that._sending -= 1;
607
815
  that._onAlways(
608
816
  jqXHRorResult,
609
817
  textStatus,
610
818
  jqXHRorError,
611
819
  options
612
820
  );
821
+ that._sending -= 1;
822
+ that._active -= 1;
613
823
  if (options.limitConcurrentUploads &&
614
824
  options.limitConcurrentUploads > that._sending) {
615
825
  // Start the next queued upload,
616
826
  // that has not been aborted:
617
827
  var nextSlot = that._slots.shift();
618
828
  while (nextSlot) {
619
- if (!nextSlot.isRejected()) {
829
+ if (that._getDeferredState(nextSlot) === 'pending') {
620
830
  nextSlot.resolve();
621
831
  break;
622
832
  }
623
833
  nextSlot = that._slots.shift();
624
834
  }
625
835
  }
836
+ if (that._active === 0) {
837
+ // The stop callback is triggered when all uploads have
838
+ // been completed, equivalent to the global ajaxStop event:
839
+ that._trigger('stop');
840
+ }
626
841
  });
627
842
  return jqXHR;
628
843
  };
@@ -641,12 +856,12 @@
641
856
  // which is delegated to the jqXHR object of the current upload,
642
857
  // and jqXHR callbacks mapped to the equivalent Promise methods:
643
858
  pipe.abort = function () {
644
- var args = [undefined, 'abort', 'abort'];
859
+ aborted = [undefined, 'abort', 'abort'];
645
860
  if (!jqXHR) {
646
861
  if (slot) {
647
- slot.rejectWith(args);
862
+ slot.rejectWith(options.context, aborted);
648
863
  }
649
- return send(false, args);
864
+ return send();
650
865
  }
651
866
  return jqXHR.abort();
652
867
  };
@@ -688,25 +903,15 @@
688
903
  var newData = $.extend({}, data);
689
904
  newData.files = fileSet ? element : [element];
690
905
  newData.paramName = paramNameSet[index];
691
- newData.submit = function () {
692
- newData.jqXHR = this.jqXHR =
693
- (that._trigger('submit', e, this) !== false) &&
694
- that._onSend(e, this);
695
- return this.jqXHR;
696
- };
697
- return (result = that._trigger('add', e, newData));
906
+ that._initResponseObject(newData);
907
+ that._initProgressObject(newData);
908
+ that._addConvenienceMethods(e, newData);
909
+ result = that._trigger('add', e, newData);
910
+ return result;
698
911
  });
699
912
  return result;
700
913
  },
701
914
 
702
- // File Normalization for Gecko 1.9.1 (Firefox 3.5) support:
703
- _normalizeFile: function (index, file) {
704
- if (file.name === undefined && file.size === undefined) {
705
- file.name = file.fileName;
706
- file.size = file.fileSize;
707
- }
708
- },
709
-
710
915
  _replaceFileInput: function (input) {
711
916
  var inputClone = input.clone(true);
712
917
  $('<form></form>').append(inputClone)[0].reset();
@@ -716,7 +921,7 @@
716
921
  // Avoid memory leaks with the detached file input:
717
922
  $.cleanData(input.unbind('remove'));
718
923
  // Replace the original file input element in the fileInput
719
- // collection with the clone, which has been copied including
924
+ // elements set with the clone, which has been copied including
720
925
  // event handlers:
721
926
  this.options.fileInput = this.options.fileInput.map(function (i, el) {
722
927
  if (el === input[0]) {
@@ -731,102 +936,227 @@
731
936
  }
732
937
  },
733
938
 
734
- _onChange: function (e) {
735
- var that = e.data.fileupload,
736
- data = {
737
- files: $.each($.makeArray(e.target.files), that._normalizeFile),
738
- fileInput: $(e.target),
739
- form: $(e.target.form)
740
- };
741
- if (!data.files.length) {
939
+ _handleFileTreeEntry: function (entry, path) {
940
+ var that = this,
941
+ dfd = $.Deferred(),
942
+ errorHandler = function (e) {
943
+ if (e && !e.entry) {
944
+ e.entry = entry;
945
+ }
946
+ // Since $.when returns immediately if one
947
+ // Deferred is rejected, we use resolve instead.
948
+ // This allows valid files and invalid items
949
+ // to be returned together in one set:
950
+ dfd.resolve([e]);
951
+ },
952
+ dirReader;
953
+ path = path || '';
954
+ if (entry.isFile) {
955
+ if (entry._file) {
956
+ // Workaround for Chrome bug #149735
957
+ entry._file.relativePath = path;
958
+ dfd.resolve(entry._file);
959
+ } else {
960
+ entry.file(function (file) {
961
+ file.relativePath = path;
962
+ dfd.resolve(file);
963
+ }, errorHandler);
964
+ }
965
+ } else if (entry.isDirectory) {
966
+ dirReader = entry.createReader();
967
+ dirReader.readEntries(function (entries) {
968
+ that._handleFileTreeEntries(
969
+ entries,
970
+ path + entry.name + '/'
971
+ ).done(function (files) {
972
+ dfd.resolve(files);
973
+ }).fail(errorHandler);
974
+ }, errorHandler);
975
+ } else {
976
+ // Return an empy list for file system items
977
+ // other than files or directories:
978
+ dfd.resolve([]);
979
+ }
980
+ return dfd.promise();
981
+ },
982
+
983
+ _handleFileTreeEntries: function (entries, path) {
984
+ var that = this;
985
+ return $.when.apply(
986
+ $,
987
+ $.map(entries, function (entry) {
988
+ return that._handleFileTreeEntry(entry, path);
989
+ })
990
+ ).pipe(function () {
991
+ return Array.prototype.concat.apply(
992
+ [],
993
+ arguments
994
+ );
995
+ });
996
+ },
997
+
998
+ _getDroppedFiles: function (dataTransfer) {
999
+ dataTransfer = dataTransfer || {};
1000
+ var items = dataTransfer.items;
1001
+ if (items && items.length && (items[0].webkitGetAsEntry ||
1002
+ items[0].getAsEntry)) {
1003
+ return this._handleFileTreeEntries(
1004
+ $.map(items, function (item) {
1005
+ var entry;
1006
+ if (item.webkitGetAsEntry) {
1007
+ entry = item.webkitGetAsEntry();
1008
+ if (entry) {
1009
+ // Workaround for Chrome bug #149735:
1010
+ entry._file = item.getAsFile();
1011
+ }
1012
+ return entry;
1013
+ }
1014
+ return item.getAsEntry();
1015
+ })
1016
+ );
1017
+ }
1018
+ return $.Deferred().resolve(
1019
+ $.makeArray(dataTransfer.files)
1020
+ ).promise();
1021
+ },
1022
+
1023
+ _getSingleFileInputFiles: function (fileInput) {
1024
+ fileInput = $(fileInput);
1025
+ var entries = fileInput.prop('webkitEntries') ||
1026
+ fileInput.prop('entries'),
1027
+ files,
1028
+ value;
1029
+ if (entries && entries.length) {
1030
+ return this._handleFileTreeEntries(entries);
1031
+ }
1032
+ files = $.makeArray(fileInput.prop('files'));
1033
+ if (!files.length) {
1034
+ value = fileInput.prop('value');
1035
+ if (!value) {
1036
+ return $.Deferred().resolve([]).promise();
1037
+ }
742
1038
  // If the files property is not available, the browser does not
743
1039
  // support the File API and we add a pseudo File object with
744
1040
  // the input value as name with path information removed:
745
- data.files = [{name: e.target.value.replace(/^.*\\/, '')}];
746
- }
747
- if (that.options.replaceFileInput) {
748
- that._replaceFileInput(data.fileInput);
1041
+ files = [{name: value.replace(/^.*\\/, '')}];
1042
+ } else if (files[0].name === undefined && files[0].fileName) {
1043
+ // File normalization for Safari 4 and Firefox 3:
1044
+ $.each(files, function (index, file) {
1045
+ file.name = file.fileName;
1046
+ file.size = file.fileSize;
1047
+ });
749
1048
  }
750
- if (that._trigger('change', e, data) === false ||
751
- that._onAdd(e, data) === false) {
752
- return false;
1049
+ return $.Deferred().resolve(files).promise();
1050
+ },
1051
+
1052
+ _getFileInputFiles: function (fileInput) {
1053
+ if (!(fileInput instanceof $) || fileInput.length === 1) {
1054
+ return this._getSingleFileInputFiles(fileInput);
753
1055
  }
1056
+ return $.when.apply(
1057
+ $,
1058
+ $.map(fileInput, this._getSingleFileInputFiles)
1059
+ ).pipe(function () {
1060
+ return Array.prototype.concat.apply(
1061
+ [],
1062
+ arguments
1063
+ );
1064
+ });
1065
+ },
1066
+
1067
+ _onChange: function (e) {
1068
+ var that = this,
1069
+ data = {
1070
+ fileInput: $(e.target),
1071
+ form: $(e.target.form)
1072
+ };
1073
+ this._getFileInputFiles(data.fileInput).always(function (files) {
1074
+ data.files = files;
1075
+ if (that.options.replaceFileInput) {
1076
+ that._replaceFileInput(data.fileInput);
1077
+ }
1078
+ if (that._trigger('change', e, data) !== false) {
1079
+ that._onAdd(e, data);
1080
+ }
1081
+ });
754
1082
  },
755
1083
 
756
1084
  _onPaste: function (e) {
757
- var that = e.data.fileupload,
758
- cbd = e.originalEvent.clipboardData,
759
- items = (cbd && cbd.items) || [],
1085
+ var items = e.originalEvent && e.originalEvent.clipboardData &&
1086
+ e.originalEvent.clipboardData.items,
760
1087
  data = {files: []};
761
- $.each(items, function (index, item) {
762
- var file = item.getAsFile && item.getAsFile();
763
- if (file) {
764
- data.files.push(file);
1088
+ if (items && items.length) {
1089
+ $.each(items, function (index, item) {
1090
+ var file = item.getAsFile && item.getAsFile();
1091
+ if (file) {
1092
+ data.files.push(file);
1093
+ }
1094
+ });
1095
+ if (this._trigger('paste', e, data) === false ||
1096
+ this._onAdd(e, data) === false) {
1097
+ return false;
765
1098
  }
766
- });
767
- if (that._trigger('paste', e, data) === false ||
768
- that._onAdd(e, data) === false) {
769
- return false;
770
1099
  }
771
1100
  },
772
1101
 
773
1102
  _onDrop: function (e) {
774
- var that = e.data.fileupload,
775
- dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
776
- data = {
777
- files: $.each(
778
- $.makeArray(dataTransfer && dataTransfer.files),
779
- that._normalizeFile
780
- )
781
- };
782
- if (that._trigger('drop', e, data) === false ||
783
- that._onAdd(e, data) === false) {
784
- return false;
1103
+ var that = this,
1104
+ dataTransfer = e.dataTransfer = e.originalEvent &&
1105
+ e.originalEvent.dataTransfer,
1106
+ data = {};
1107
+ if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1108
+ e.preventDefault();
1109
+ this._getDroppedFiles(dataTransfer).always(function (files) {
1110
+ data.files = files;
1111
+ if (that._trigger('drop', e, data) !== false) {
1112
+ that._onAdd(e, data);
1113
+ }
1114
+ });
785
1115
  }
786
- e.preventDefault();
787
1116
  },
788
1117
 
789
1118
  _onDragOver: function (e) {
790
- var that = e.data.fileupload,
791
- dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
792
- if (that._trigger('dragover', e) === false) {
793
- return false;
794
- }
1119
+ var dataTransfer = e.dataTransfer = e.originalEvent &&
1120
+ e.originalEvent.dataTransfer;
795
1121
  if (dataTransfer) {
796
- dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy';
1122
+ if (this._trigger('dragover', e) === false) {
1123
+ return false;
1124
+ }
1125
+ if ($.inArray('Files', dataTransfer.types) !== -1) {
1126
+ dataTransfer.dropEffect = 'copy';
1127
+ e.preventDefault();
1128
+ }
797
1129
  }
798
- e.preventDefault();
799
1130
  },
800
1131
 
801
1132
  _initEventHandlers: function () {
802
- var ns = this.options.namespace;
803
1133
  if (this._isXHRUpload(this.options)) {
804
- this.options.dropZone
805
- .bind('dragover.' + ns, {fileupload: this}, this._onDragOver)
806
- .bind('drop.' + ns, {fileupload: this}, this._onDrop)
807
- .bind('paste.' + ns, {fileupload: this}, this._onPaste);
1134
+ this._on(this.options.dropZone, {
1135
+ dragover: this._onDragOver,
1136
+ drop: this._onDrop
1137
+ });
1138
+ this._on(this.options.pasteZone, {
1139
+ paste: this._onPaste
1140
+ });
808
1141
  }
809
- this.options.fileInput
810
- .bind('change.' + ns, {fileupload: this}, this._onChange);
1142
+ this._on(this.options.fileInput, {
1143
+ change: this._onChange
1144
+ });
811
1145
  },
812
1146
 
813
1147
  _destroyEventHandlers: function () {
814
- var ns = this.options.namespace;
815
- this.options.dropZone
816
- .unbind('dragover.' + ns, this._onDragOver)
817
- .unbind('drop.' + ns, this._onDrop)
818
- .unbind('paste.' + ns, this._onPaste);
819
- this.options.fileInput
820
- .unbind('change.' + ns, this._onChange);
1148
+ this._off(this.options.dropZone, 'dragover drop');
1149
+ this._off(this.options.pasteZone, 'paste');
1150
+ this._off(this.options.fileInput, 'change');
821
1151
  },
822
1152
 
823
1153
  _setOption: function (key, value) {
824
- var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
825
- if (refresh) {
1154
+ var reinit = $.inArray(key, this._specialOptions) !== -1;
1155
+ if (reinit) {
826
1156
  this._destroyEventHandlers();
827
1157
  }
828
- $.Widget.prototype._setOption.call(this, key, value);
829
- if (refresh) {
1158
+ this._super(key, value);
1159
+ if (reinit) {
830
1160
  this._initSpecialOptions();
831
1161
  this._initEventHandlers();
832
1162
  }
@@ -835,41 +1165,68 @@
835
1165
  _initSpecialOptions: function () {
836
1166
  var options = this.options;
837
1167
  if (options.fileInput === undefined) {
838
- options.fileInput = this.element.is('input:file') ?
839
- this.element : this.element.find('input:file');
1168
+ options.fileInput = this.element.is('input[type="file"]') ?
1169
+ this.element : this.element.find('input[type="file"]');
840
1170
  } else if (!(options.fileInput instanceof $)) {
841
1171
  options.fileInput = $(options.fileInput);
842
1172
  }
843
1173
  if (!(options.dropZone instanceof $)) {
844
1174
  options.dropZone = $(options.dropZone);
845
1175
  }
1176
+ if (!(options.pasteZone instanceof $)) {
1177
+ options.pasteZone = $(options.pasteZone);
1178
+ }
846
1179
  },
847
1180
 
848
- _create: function () {
849
- var options = this.options;
1181
+ _getRegExp: function (str) {
1182
+ var parts = str.split('/'),
1183
+ modifiers = parts.pop();
1184
+ parts.shift();
1185
+ return new RegExp(parts.join('/'), modifiers);
1186
+ },
1187
+
1188
+ _isRegExpOption: function (key, value) {
1189
+ return key !== 'url' && $.type(value) === 'string' &&
1190
+ /^\/.*\/[igm]{0,3}$/.test(value);
1191
+ },
1192
+
1193
+ _initDataAttributes: function () {
1194
+ var that = this,
1195
+ options = this.options;
850
1196
  // Initialize options set via HTML5 data-attributes:
851
- $.extend(options, $(this.element[0].cloneNode(false)).data());
852
- options.namespace = options.namespace || this.widgetName;
1197
+ $.each(
1198
+ $(this.element[0].cloneNode(false)).data(),
1199
+ function (key, value) {
1200
+ if (that._isRegExpOption(key, value)) {
1201
+ value = that._getRegExp(value);
1202
+ }
1203
+ options[key] = value;
1204
+ }
1205
+ );
1206
+ },
1207
+
1208
+ _create: function () {
1209
+ this._initDataAttributes();
853
1210
  this._initSpecialOptions();
854
1211
  this._slots = [];
855
1212
  this._sequence = this._getXHRPromise(true);
856
- this._sending = this._active = this._loaded = this._total = 0;
1213
+ this._sending = this._active = 0;
1214
+ this._initProgressObject(this);
857
1215
  this._initEventHandlers();
858
1216
  },
859
1217
 
860
- destroy: function () {
861
- this._destroyEventHandlers();
862
- $.Widget.prototype.destroy.call(this);
863
- },
864
-
865
- enable: function () {
866
- $.Widget.prototype.enable.call(this);
867
- this._initEventHandlers();
1218
+ // This method is exposed to the widget API and allows to query
1219
+ // the number of active uploads:
1220
+ active: function () {
1221
+ return this._active;
868
1222
  },
869
1223
 
870
- disable: function () {
871
- this._destroyEventHandlers();
872
- $.Widget.prototype.disable.call(this);
1224
+ // This method is exposed to the widget API and allows to query
1225
+ // the widget upload progress.
1226
+ // It returns an object with loaded, total and bitrate properties
1227
+ // for the running uploads:
1228
+ progress: function () {
1229
+ return this._progress;
873
1230
  },
874
1231
 
875
1232
  // This method is exposed to the widget API and allows adding files
@@ -877,21 +1234,61 @@
877
1234
  // must have a files property and can contain additional options:
878
1235
  // .fileupload('add', {files: filesList});
879
1236
  add: function (data) {
1237
+ var that = this;
880
1238
  if (!data || this.options.disabled) {
881
1239
  return;
882
1240
  }
883
- data.files = $.each($.makeArray(data.files), this._normalizeFile);
884
- this._onAdd(null, data);
1241
+ if (data.fileInput && !data.files) {
1242
+ this._getFileInputFiles(data.fileInput).always(function (files) {
1243
+ data.files = files;
1244
+ that._onAdd(null, data);
1245
+ });
1246
+ } else {
1247
+ data.files = $.makeArray(data.files);
1248
+ this._onAdd(null, data);
1249
+ }
885
1250
  },
886
1251
 
887
1252
  // This method is exposed to the widget API and allows sending files
888
1253
  // using the fileupload API. The data parameter accepts an object which
889
- // must have a files property and can contain additional options:
1254
+ // must have a files or fileInput property and can contain additional options:
890
1255
  // .fileupload('send', {files: filesList});
891
1256
  // The method returns a Promise object for the file upload call.
892
1257
  send: function (data) {
893
1258
  if (data && !this.options.disabled) {
894
- data.files = $.each($.makeArray(data.files), this._normalizeFile);
1259
+ if (data.fileInput && !data.files) {
1260
+ var that = this,
1261
+ dfd = $.Deferred(),
1262
+ promise = dfd.promise(),
1263
+ jqXHR,
1264
+ aborted;
1265
+ promise.abort = function () {
1266
+ aborted = true;
1267
+ if (jqXHR) {
1268
+ return jqXHR.abort();
1269
+ }
1270
+ dfd.reject(null, 'abort', 'abort');
1271
+ return promise;
1272
+ };
1273
+ this._getFileInputFiles(data.fileInput).always(
1274
+ function (files) {
1275
+ if (aborted) {
1276
+ return;
1277
+ }
1278
+ data.files = files;
1279
+ jqXHR = that._onSend(null, data).then(
1280
+ function (result, textStatus, jqXHR) {
1281
+ dfd.resolve(result, textStatus, jqXHR);
1282
+ },
1283
+ function (jqXHR, textStatus, errorThrown) {
1284
+ dfd.reject(jqXHR, textStatus, errorThrown);
1285
+ }
1286
+ );
1287
+ }
1288
+ );
1289
+ return this._enhancePromise(promise);
1290
+ }
1291
+ data.files = $.makeArray(data.files);
895
1292
  if (data.files.length) {
896
1293
  return this._onSend(null, data);
897
1294
  }