rails-uploader 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  /*
2
- * jQuery File Upload Plugin 5.10.1
2
+ * jQuery File Upload Plugin 5.21.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';
@@ -33,6 +33,21 @@
33
33
  $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
34
34
  $.support.xhrFormDataFileUpload = !!window.FormData;
35
35
 
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
+ });
46
+ }
47
+ return null;
48
+ }
49
+ };
50
+
36
51
  // The fileupload widget listens for change events on file input fields defined
37
52
  // via fileInput setting and paste or drop events of the given dropZone.
38
53
  // In addition to the default jQuery Widget methods, the fileupload widget
@@ -44,17 +59,16 @@
44
59
  $.widget('blueimp.fileupload', {
45
60
 
46
61
  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:
62
+ // The drop target element(s), by the default the complete document.
63
+ // Set to null to disable drag & drop support:
53
64
  dropZone: $(document),
54
- // The file input field collection, that is listened for change events.
65
+ // The paste target element(s), by the default the complete document.
66
+ // Set to null to disable paste support:
67
+ pasteZone: $(document),
68
+ // The file input field(s), that are listened to for change events.
55
69
  // If undefined, it is set to the file input fields inside
56
70
  // of the widget element on plugin initialization.
57
- // Set to null or an empty collection to disable the change listener.
71
+ // Set to null to disable the change listener.
58
72
  fileInput: undefined,
59
73
  // By default, the file input field is replaced with a clone after
60
74
  // each input field change event. This is required for iframe transport
@@ -109,6 +123,10 @@
109
123
  // global progress calculation. Set the following option to false to
110
124
  // prevent recalculating the global progress data:
111
125
  recalculateProgress: true,
126
+ // Interval in milliseconds to calculate and trigger progress events:
127
+ progressInterval: 100,
128
+ // Interval in milliseconds to calculate progress bitrate:
129
+ bitrateInterval: 500,
112
130
 
113
131
  // Additional form data to be sent along with the file uploads can be set
114
132
  // using this option, which accepts an array of objects with name and
@@ -137,33 +155,58 @@
137
155
  },
138
156
 
139
157
  // Other callbacks:
158
+
140
159
  // Callback for the submit event of each file upload:
141
160
  // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
161
+
142
162
  // Callback for the start of each file upload request:
143
163
  // send: function (e, data) {}, // .bind('fileuploadsend', func);
164
+
144
165
  // Callback for successful uploads:
145
166
  // done: function (e, data) {}, // .bind('fileuploaddone', func);
167
+
146
168
  // Callback for failed (abort or error) uploads:
147
169
  // fail: function (e, data) {}, // .bind('fileuploadfail', func);
170
+
148
171
  // Callback for completed (success, abort or error) requests:
149
172
  // always: function (e, data) {}, // .bind('fileuploadalways', func);
173
+
150
174
  // Callback for upload progress events:
151
175
  // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
176
+
152
177
  // Callback for global upload progress events:
153
178
  // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
179
+
154
180
  // Callback for uploads start, equivalent to the global ajaxStart event:
155
181
  // start: function (e) {}, // .bind('fileuploadstart', func);
182
+
156
183
  // Callback for uploads stop, equivalent to the global ajaxStop event:
157
184
  // stop: function (e) {}, // .bind('fileuploadstop', func);
158
- // Callback for change events of the fileInput collection:
185
+
186
+ // Callback for change events of the fileInput(s):
159
187
  // change: function (e, data) {}, // .bind('fileuploadchange', func);
160
- // Callback for paste events to the dropZone collection:
188
+
189
+ // Callback for paste events to the pasteZone(s):
161
190
  // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
162
- // Callback for drop events of the dropZone collection:
191
+
192
+ // Callback for drop events of the dropZone(s):
163
193
  // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
164
- // Callback for dragover events of the dropZone collection:
194
+
195
+ // Callback for dragover events of the dropZone(s):
165
196
  // dragover: function (e) {}, // .bind('fileuploaddragover', func);
166
197
 
198
+ // Callback for the start of each chunk upload request:
199
+ // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
200
+
201
+ // Callback for successful chunk uploads:
202
+ // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
203
+
204
+ // Callback for failed (abort or error) chunk uploads:
205
+ // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
206
+
207
+ // Callback for completed (success, abort or error) chunk upload requests:
208
+ // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
209
+
167
210
  // The plugin options are used as settings object for the ajax calls.
168
211
  // The following are jQuery ajax settings required for the file uploads:
169
212
  processData: false,
@@ -173,13 +216,28 @@
173
216
 
174
217
  // A list of options that require a refresh after assigning a new value:
175
218
  _refreshOptionsList: [
176
- 'namespace',
177
- 'dropZone',
178
219
  'fileInput',
220
+ 'dropZone',
221
+ 'pasteZone',
179
222
  'multipart',
180
223
  'forceIframeTransport'
181
224
  ],
182
225
 
226
+ _BitrateTimer: function () {
227
+ this.timestamp = +(new Date());
228
+ this.loaded = 0;
229
+ this.bitrate = 0;
230
+ this.getBitrate = function (now, loaded, interval) {
231
+ var timeDiff = now - this.timestamp;
232
+ if (!this.bitrate || !interval || timeDiff > interval) {
233
+ this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
234
+ this.loaded = loaded;
235
+ this.timestamp = now;
236
+ }
237
+ return this.bitrate;
238
+ };
239
+ },
240
+
183
241
  _isXHRUpload: function (options) {
184
242
  return !options.forceIframeTransport &&
185
243
  ((!options.multipart && $.support.xhrFileUpload) ||
@@ -190,9 +248,11 @@
190
248
  var formData;
191
249
  if (typeof options.formData === 'function') {
192
250
  return options.formData(options.form);
193
- } else if ($.isArray(options.formData)) {
251
+ }
252
+ if ($.isArray(options.formData)) {
194
253
  return options.formData;
195
- } else if (options.formData) {
254
+ }
255
+ if (options.formData) {
196
256
  formData = [];
197
257
  $.each(options.formData, function (name, value) {
198
258
  formData.push({name: name, value: value});
@@ -212,15 +272,29 @@
212
272
 
213
273
  _onProgress: function (e, data) {
214
274
  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);
275
+ var now = +(new Date()),
276
+ total,
277
+ loaded;
278
+ if (data._time && data.progressInterval &&
279
+ (now - data._time < data.progressInterval) &&
280
+ e.loaded !== e.total) {
281
+ return;
282
+ }
283
+ data._time = now;
284
+ total = data.total || this._getTotal(data.files);
285
+ loaded = parseInt(
286
+ e.loaded / e.total * (data.chunkSize || total),
287
+ 10
288
+ ) + (data.uploadedBytes || 0);
220
289
  this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
221
290
  data.lengthComputable = true;
222
291
  data.loaded = loaded;
223
292
  data.total = total;
293
+ data.bitrate = data._bitrateTimer.getBitrate(
294
+ now,
295
+ loaded,
296
+ data.bitrateInterval
297
+ );
224
298
  // Trigger a custom progress event with a total data property set
225
299
  // to the file size(s) of the current upload and a loaded data
226
300
  // property calculated accordingly:
@@ -230,7 +304,12 @@
230
304
  this._trigger('progressall', e, {
231
305
  lengthComputable: true,
232
306
  loaded: this._loaded,
233
- total: this._total
307
+ total: this._total,
308
+ bitrate: this._bitrateTimer.getBitrate(
309
+ now,
310
+ this._loaded,
311
+ data.bitrateInterval
312
+ )
234
313
  });
235
314
  }
236
315
  },
@@ -261,29 +340,16 @@
261
340
  // Ignore non-multipart setting if not supported:
262
341
  multipart = options.multipart || !$.support.xhrFileUpload,
263
342
  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
- }
343
+ options.headers = options.headers || {};
344
+ if (options.contentRange) {
345
+ options.headers['Content-Range'] = options.contentRange;
285
346
  }
286
- if (multipart && $.support.xhrFormDataFileUpload) {
347
+ if (!multipart) {
348
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
349
+ encodeURI(file.name) + '"';
350
+ options.contentType = file.type;
351
+ options.data = options.blob || file;
352
+ } else if ($.support.xhrFormDataFileUpload) {
287
353
  if (options.postMessage) {
288
354
  // window.postMessage does not allow sending FormData
289
355
  // objects, so we just add the File/Blob objects to
@@ -313,13 +379,17 @@
313
379
  });
314
380
  }
315
381
  if (options.blob) {
382
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
383
+ encodeURI(file.name) + '"';
316
384
  formData.append(paramName, options.blob, file.name);
317
385
  } else {
318
386
  $.each(options.files, function (index, file) {
319
- // File objects are also Blob instances.
387
+ // Files are also Blob instances, but some browsers
388
+ // (Firefox 3.6) support the File API but not Blobs.
320
389
  // This check allows the tests to run with
321
390
  // dummy objects:
322
- if (file instanceof Blob) {
391
+ if ((window.Blob && file instanceof Blob) ||
392
+ (window.File && file instanceof File)) {
323
393
  formData.append(
324
394
  options.paramName[index] || paramName,
325
395
  file,
@@ -396,6 +466,11 @@
396
466
  // associated form, if available:
397
467
  if (!options.form || !options.form.length) {
398
468
  options.form = $(options.fileInput.prop('form'));
469
+ // If the given file input doesn't have an associated form,
470
+ // use the default widget file input's form:
471
+ if (!options.form.length) {
472
+ options.form = $(this.options.fileInput.prop('form'));
473
+ }
399
474
  }
400
475
  options.paramName = this._getParamName(options);
401
476
  if (!options.url) {
@@ -404,9 +479,13 @@
404
479
  // The HTTP request method must be "POST" or "PUT":
405
480
  options.type = (options.type || options.form.prop('method') || '')
406
481
  .toUpperCase();
407
- if (options.type !== 'POST' && options.type !== 'PUT') {
482
+ if (options.type !== 'POST' && options.type !== 'PUT' &&
483
+ options.type !== 'PATCH') {
408
484
  options.type = 'POST';
409
485
  }
486
+ if (!options.formAcceptCharset) {
487
+ options.formAcceptCharset = options.form.attr('accept-charset');
488
+ }
410
489
  },
411
490
 
412
491
  _getAJAXSettings: function (data) {
@@ -440,6 +519,16 @@
440
519
  return this._enhancePromise(promise);
441
520
  },
442
521
 
522
+ // Parses the Range header from the server response
523
+ // and returns the uploaded bytes:
524
+ _getUploadedBytes: function (jqXHR) {
525
+ var range = jqXHR.getResponseHeader('Range'),
526
+ parts = range && range.split('-'),
527
+ upperBytesPos = parts && parts.length > 1 &&
528
+ parseInt(parts[1], 10);
529
+ return upperBytesPos && upperBytesPos + 1;
530
+ },
531
+
443
532
  // Uploads a file in multiple, sequential requests
444
533
  // by splitting the file up in multiple blob chunks.
445
534
  // If the second parameter is true, only tests if the file
@@ -451,13 +540,11 @@
451
540
  fs = file.size,
452
541
  ub = options.uploadedBytes = options.uploadedBytes || 0,
453
542
  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,
543
+ slice = file.slice || file.webkitSlice || file.mozSlice,
544
+ dfd = $.Deferred(),
545
+ promise = dfd.promise(),
459
546
  jqXHR,
460
- pipe;
547
+ upload;
461
548
  if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
462
549
  options.data) {
463
550
  return false;
@@ -466,62 +553,83 @@
466
553
  return true;
467
554
  }
468
555
  if (ub >= fs) {
469
- file.error = 'uploadedBytes';
556
+ file.error = 'Uploaded bytes exceed file size';
470
557
  return this._getXHRPromise(
471
558
  false,
472
559
  options.context,
473
560
  [null, 'error', file.error]
474
561
  );
475
562
  }
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
- });
563
+ // The chunk upload method:
564
+ upload = function () {
565
+ // Clone the options object for each chunk upload:
566
+ var o = $.extend({}, options);
567
+ o.blob = slice.call(
568
+ file,
569
+ ub,
570
+ ub + mcs,
571
+ file.type
572
+ );
573
+ // Store the current chunk size, as the blob itself
574
+ // will be dereferenced after data processing:
575
+ o.chunkSize = o.blob.size;
576
+ // Expose the chunk bytes position range:
577
+ o.contentRange = 'bytes ' + ub + '-' +
578
+ (ub + o.chunkSize - 1) + '/' + fs;
579
+ // Process the upload data (the blob and potential form data):
580
+ that._initXHRData(o);
581
+ // Add progress listeners for this chunk upload:
582
+ that._initProgressListener(o);
583
+ jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
584
+ that._getXHRPromise(false, o.context))
585
+ .done(function (result, textStatus, jqXHR) {
586
+ ub = that._getUploadedBytes(jqXHR) ||
587
+ (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) {
592
+ that._onProgress($.Event('progress', {
593
+ lengthComputable: true,
594
+ loaded: ub - o.uploadedBytes,
595
+ total: ub - o.uploadedBytes
596
+ }), o);
597
+ }
598
+ options.uploadedBytes = o.uploadedBytes = ub;
599
+ o.result = result;
600
+ o.textStatus = textStatus;
601
+ o.jqXHR = jqXHR;
602
+ that._trigger('chunkdone', null, o);
603
+ that._trigger('chunkalways', null, o);
604
+ if (ub < fs) {
605
+ // File upload not yet complete,
606
+ // continue with the next chunk:
607
+ upload();
608
+ } else {
609
+ dfd.resolveWith(
610
+ o.context,
611
+ [result, textStatus, jqXHR]
612
+ );
613
+ }
614
+ })
615
+ .fail(function (jqXHR, textStatus, errorThrown) {
616
+ o.jqXHR = jqXHR;
617
+ o.textStatus = textStatus;
618
+ o.errorThrown = errorThrown;
619
+ that._trigger('chunkfail', null, o);
620
+ that._trigger('chunkalways', null, o);
621
+ dfd.rejectWith(
622
+ o.context,
623
+ [jqXHR, textStatus, errorThrown]
624
+ );
625
+ });
516
626
  };
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 () {
627
+ this._enhancePromise(promise);
628
+ promise.abort = function () {
522
629
  return jqXHR.abort();
523
630
  };
524
- return this._enhancePromise(pipe);
631
+ upload();
632
+ return promise;
525
633
  },
526
634
 
527
635
  _beforeSend: function (e, data) {
@@ -530,6 +638,8 @@
530
638
  // and no other uploads are currently running,
531
639
  // equivalent to the global ajaxStart event:
532
640
  this._trigger('start');
641
+ // Set timer for global bitrate progress calculation:
642
+ this._bitrateTimer = new this._BitrateTimer();
533
643
  }
534
644
  this._active += 1;
535
645
  // Initialize the global progress values:
@@ -538,12 +648,16 @@
538
648
  },
539
649
 
540
650
  _onDone: function (result, textStatus, jqXHR, options) {
541
- if (!this._isXHRUpload(options)) {
542
- // Create a progress event for each iframe load:
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:
543
657
  this._onProgress($.Event('progress', {
544
658
  lengthComputable: true,
545
- loaded: 1,
546
- total: 1
659
+ loaded: total,
660
+ total: total
547
661
  }), options);
548
662
  }
549
663
  options.result = result;
@@ -566,15 +680,9 @@
566
680
  },
567
681
 
568
682
  _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
683
+ // jqXHRorResult, textStatus and jqXHRorError are added to the
684
+ // options object via done and fail callbacks
569
685
  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
- }
578
686
  this._trigger('always', null, options);
579
687
  if (this._active === 0) {
580
688
  // The stop callback is triggered when all uploads have
@@ -582,22 +690,25 @@
582
690
  this._trigger('stop');
583
691
  // Reset the global progress values:
584
692
  this._loaded = this._total = 0;
693
+ this._bitrateTimer = null;
585
694
  }
586
695
  },
587
696
 
588
697
  _onSend: function (e, data) {
589
698
  var that = this,
590
699
  jqXHR,
700
+ aborted,
591
701
  slot,
592
702
  pipe,
593
703
  options = that._getAJAXSettings(data),
594
- send = function (resolve, args) {
704
+ send = function () {
595
705
  that._sending += 1;
706
+ // Set timer for bitrate progress calculation:
707
+ options._bitrateTimer = new that._BitrateTimer();
596
708
  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)
709
+ ((aborted || that._trigger('send', e, options) === false) &&
710
+ that._getXHRPromise(false, options.context, aborted)) ||
711
+ that._chunkedUpload(options) || $.ajax(options)
601
712
  ).done(function (result, textStatus, jqXHR) {
602
713
  that._onDone(result, textStatus, jqXHR, options);
603
714
  }).fail(function (jqXHR, textStatus, errorThrown) {
@@ -614,9 +725,15 @@
614
725
  options.limitConcurrentUploads > that._sending) {
615
726
  // Start the next queued upload,
616
727
  // that has not been aborted:
617
- var nextSlot = that._slots.shift();
728
+ var nextSlot = that._slots.shift(),
729
+ isPending;
618
730
  while (nextSlot) {
619
- if (!nextSlot.isRejected()) {
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) {
620
737
  nextSlot.resolve();
621
738
  break;
622
739
  }
@@ -641,12 +758,12 @@
641
758
  // which is delegated to the jqXHR object of the current upload,
642
759
  // and jqXHR callbacks mapped to the equivalent Promise methods:
643
760
  pipe.abort = function () {
644
- var args = [undefined, 'abort', 'abort'];
761
+ aborted = [undefined, 'abort', 'abort'];
645
762
  if (!jqXHR) {
646
763
  if (slot) {
647
- slot.rejectWith(args);
764
+ slot.rejectWith(options.context, aborted);
648
765
  }
649
- return send(false, args);
766
+ return send();
650
767
  }
651
768
  return jqXHR.abort();
652
769
  };
@@ -694,19 +811,12 @@
694
811
  that._onSend(e, this);
695
812
  return this.jqXHR;
696
813
  };
697
- return (result = that._trigger('add', e, newData));
814
+ result = that._trigger('add', e, newData);
815
+ return result;
698
816
  });
699
817
  return result;
700
818
  },
701
819
 
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
820
  _replaceFileInput: function (input) {
711
821
  var inputClone = input.clone(true);
712
822
  $('<form></form>').append(inputClone)[0].reset();
@@ -716,7 +826,7 @@
716
826
  // Avoid memory leaks with the detached file input:
717
827
  $.cleanData(input.unbind('remove'));
718
828
  // Replace the original file input element in the fileInput
719
- // collection with the clone, which has been copied including
829
+ // elements set with the clone, which has been copied including
720
830
  // event handlers:
721
831
  this.options.fileInput = this.options.fileInput.map(function (i, el) {
722
832
  if (el === input[0]) {
@@ -731,31 +841,153 @@
731
841
  }
732
842
  },
733
843
 
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) {
844
+ _handleFileTreeEntry: function (entry, path) {
845
+ var that = this,
846
+ dfd = $.Deferred(),
847
+ errorHandler = function (e) {
848
+ if (e && !e.entry) {
849
+ e.entry = entry;
850
+ }
851
+ // Since $.when returns immediately if one
852
+ // Deferred is rejected, we use resolve instead.
853
+ // This allows valid files and invalid items
854
+ // to be returned together in one set:
855
+ dfd.resolve([e]);
856
+ },
857
+ dirReader;
858
+ path = path || '';
859
+ if (entry.isFile) {
860
+ if (entry._file) {
861
+ // Workaround for Chrome bug #149735
862
+ entry._file.relativePath = path;
863
+ dfd.resolve(entry._file);
864
+ } else {
865
+ entry.file(function (file) {
866
+ file.relativePath = path;
867
+ dfd.resolve(file);
868
+ }, errorHandler);
869
+ }
870
+ } else if (entry.isDirectory) {
871
+ 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);
880
+ } else {
881
+ // Return an empy list for file system items
882
+ // other than files or directories:
883
+ dfd.resolve([]);
884
+ }
885
+ return dfd.promise();
886
+ },
887
+
888
+ _handleFileTreeEntries: function (entries, path) {
889
+ var that = this;
890
+ return $.when.apply(
891
+ $,
892
+ $.map(entries, function (entry) {
893
+ return that._handleFileTreeEntry(entry, path);
894
+ })
895
+ ).pipe(function () {
896
+ return Array.prototype.concat.apply(
897
+ [],
898
+ arguments
899
+ );
900
+ });
901
+ },
902
+
903
+ _getDroppedFiles: function (dataTransfer) {
904
+ dataTransfer = dataTransfer || {};
905
+ var items = dataTransfer.items;
906
+ if (items && items.length && (items[0].webkitGetAsEntry ||
907
+ items[0].getAsEntry)) {
908
+ return this._handleFileTreeEntries(
909
+ $.map(items, function (item) {
910
+ var entry;
911
+ if (item.webkitGetAsEntry) {
912
+ entry = item.webkitGetAsEntry();
913
+ if (entry) {
914
+ // Workaround for Chrome bug #149735:
915
+ entry._file = item.getAsFile();
916
+ }
917
+ return entry;
918
+ }
919
+ return item.getAsEntry();
920
+ })
921
+ );
922
+ }
923
+ return $.Deferred().resolve(
924
+ $.makeArray(dataTransfer.files)
925
+ ).promise();
926
+ },
927
+
928
+ _getSingleFileInputFiles: function (fileInput) {
929
+ fileInput = $(fileInput);
930
+ var entries = fileInput.prop('webkitEntries') ||
931
+ fileInput.prop('entries'),
932
+ files,
933
+ value;
934
+ if (entries && entries.length) {
935
+ return this._handleFileTreeEntries(entries);
936
+ }
937
+ files = $.makeArray(fileInput.prop('files'));
938
+ if (!files.length) {
939
+ value = fileInput.prop('value');
940
+ if (!value) {
941
+ return $.Deferred().resolve([]).promise();
942
+ }
742
943
  // If the files property is not available, the browser does not
743
944
  // support the File API and we add a pseudo File object with
744
945
  // 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);
946
+ files = [{name: value.replace(/^.*\\/, '')}];
947
+ } else if (files[0].name === undefined && files[0].fileName) {
948
+ // File normalization for Safari 4 and Firefox 3:
949
+ $.each(files, function (index, file) {
950
+ file.name = file.fileName;
951
+ file.size = file.fileSize;
952
+ });
749
953
  }
750
- if (that._trigger('change', e, data) === false ||
751
- that._onAdd(e, data) === false) {
752
- return false;
954
+ return $.Deferred().resolve(files).promise();
955
+ },
956
+
957
+ _getFileInputFiles: function (fileInput) {
958
+ if (!(fileInput instanceof $) || fileInput.length === 1) {
959
+ return this._getSingleFileInputFiles(fileInput);
753
960
  }
961
+ return $.when.apply(
962
+ $,
963
+ $.map(fileInput, this._getSingleFileInputFiles)
964
+ ).pipe(function () {
965
+ return Array.prototype.concat.apply(
966
+ [],
967
+ arguments
968
+ );
969
+ });
970
+ },
971
+
972
+ _onChange: function (e) {
973
+ var that = this,
974
+ data = {
975
+ fileInput: $(e.target),
976
+ form: $(e.target.form)
977
+ };
978
+ this._getFileInputFiles(data.fileInput).always(function (files) {
979
+ data.files = files;
980
+ if (that.options.replaceFileInput) {
981
+ that._replaceFileInput(data.fileInput);
982
+ }
983
+ if (that._trigger('change', e, data) !== false) {
984
+ that._onAdd(e, data);
985
+ }
986
+ });
754
987
  },
755
988
 
756
989
  _onPaste: function (e) {
757
- var that = e.data.fileupload,
758
- cbd = e.originalEvent.clipboardData,
990
+ var cbd = e.originalEvent.clipboardData,
759
991
  items = (cbd && cbd.items) || [],
760
992
  data = {files: []};
761
993
  $.each(items, function (index, item) {
@@ -764,60 +996,57 @@
764
996
  data.files.push(file);
765
997
  }
766
998
  });
767
- if (that._trigger('paste', e, data) === false ||
768
- that._onAdd(e, data) === false) {
999
+ if (this._trigger('paste', e, data) === false ||
1000
+ this._onAdd(e, data) === false) {
769
1001
  return false;
770
1002
  }
771
1003
  },
772
1004
 
773
1005
  _onDrop: function (e) {
774
- var that = e.data.fileupload,
1006
+ var that = this,
775
1007
  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;
1008
+ data = {};
1009
+ if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1010
+ e.preventDefault();
785
1011
  }
786
- e.preventDefault();
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
+ });
787
1018
  },
788
1019
 
789
1020
  _onDragOver: function (e) {
790
- var that = e.data.fileupload,
791
- dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
792
- if (that._trigger('dragover', e) === false) {
1021
+ var dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
1022
+ if (this._trigger('dragover', e) === false) {
793
1023
  return false;
794
1024
  }
795
- if (dataTransfer) {
796
- dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy';
1025
+ if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1) {
1026
+ dataTransfer.dropEffect = 'copy';
1027
+ e.preventDefault();
797
1028
  }
798
- e.preventDefault();
799
1029
  },
800
1030
 
801
1031
  _initEventHandlers: function () {
802
- var ns = this.options.namespace;
803
1032
  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);
1033
+ this._on(this.options.dropZone, {
1034
+ dragover: this._onDragOver,
1035
+ drop: this._onDrop
1036
+ });
1037
+ this._on(this.options.pasteZone, {
1038
+ paste: this._onPaste
1039
+ });
808
1040
  }
809
- this.options.fileInput
810
- .bind('change.' + ns, {fileupload: this}, this._onChange);
1041
+ this._on(this.options.fileInput, {
1042
+ change: this._onChange
1043
+ });
811
1044
  },
812
1045
 
813
1046
  _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);
1047
+ this._off(this.options.dropZone, 'dragover drop');
1048
+ this._off(this.options.pasteZone, 'paste');
1049
+ this._off(this.options.fileInput, 'change');
821
1050
  },
822
1051
 
823
1052
  _setOption: function (key, value) {
@@ -825,7 +1054,7 @@
825
1054
  if (refresh) {
826
1055
  this._destroyEventHandlers();
827
1056
  }
828
- $.Widget.prototype._setOption.call(this, key, value);
1057
+ this._super(key, value);
829
1058
  if (refresh) {
830
1059
  this._initSpecialOptions();
831
1060
  this._initEventHandlers();
@@ -835,21 +1064,23 @@
835
1064
  _initSpecialOptions: function () {
836
1065
  var options = this.options;
837
1066
  if (options.fileInput === undefined) {
838
- options.fileInput = this.element.is('input:file') ?
839
- this.element : this.element.find('input:file');
1067
+ options.fileInput = this.element.is('input[type="file"]') ?
1068
+ this.element : this.element.find('input[type="file"]');
840
1069
  } else if (!(options.fileInput instanceof $)) {
841
1070
  options.fileInput = $(options.fileInput);
842
1071
  }
843
1072
  if (!(options.dropZone instanceof $)) {
844
1073
  options.dropZone = $(options.dropZone);
845
1074
  }
1075
+ if (!(options.pasteZone instanceof $)) {
1076
+ options.pasteZone = $(options.pasteZone);
1077
+ }
846
1078
  },
847
1079
 
848
1080
  _create: function () {
849
1081
  var options = this.options;
850
1082
  // Initialize options set via HTML5 data-attributes:
851
1083
  $.extend(options, $(this.element[0].cloneNode(false)).data());
852
- options.namespace = options.namespace || this.widgetName;
853
1084
  this._initSpecialOptions();
854
1085
  this._slots = [];
855
1086
  this._sequence = this._getXHRPromise(true);
@@ -857,19 +1088,8 @@
857
1088
  this._initEventHandlers();
858
1089
  },
859
1090
 
860
- destroy: function () {
1091
+ _destroy: function () {
861
1092
  this._destroyEventHandlers();
862
- $.Widget.prototype.destroy.call(this);
863
- },
864
-
865
- enable: function () {
866
- $.Widget.prototype.enable.call(this);
867
- this._initEventHandlers();
868
- },
869
-
870
- disable: function () {
871
- this._destroyEventHandlers();
872
- $.Widget.prototype.disable.call(this);
873
1093
  },
874
1094
 
875
1095
  // This method is exposed to the widget API and allows adding files
@@ -877,21 +1097,61 @@
877
1097
  // must have a files property and can contain additional options:
878
1098
  // .fileupload('add', {files: filesList});
879
1099
  add: function (data) {
1100
+ var that = this;
880
1101
  if (!data || this.options.disabled) {
881
1102
  return;
882
1103
  }
883
- data.files = $.each($.makeArray(data.files), this._normalizeFile);
884
- this._onAdd(null, data);
1104
+ if (data.fileInput && !data.files) {
1105
+ this._getFileInputFiles(data.fileInput).always(function (files) {
1106
+ data.files = files;
1107
+ that._onAdd(null, data);
1108
+ });
1109
+ } else {
1110
+ data.files = $.makeArray(data.files);
1111
+ this._onAdd(null, data);
1112
+ }
885
1113
  },
886
1114
 
887
1115
  // This method is exposed to the widget API and allows sending files
888
1116
  // using the fileupload API. The data parameter accepts an object which
889
- // must have a files property and can contain additional options:
1117
+ // must have a files or fileInput property and can contain additional options:
890
1118
  // .fileupload('send', {files: filesList});
891
1119
  // The method returns a Promise object for the file upload call.
892
1120
  send: function (data) {
893
1121
  if (data && !this.options.disabled) {
894
- data.files = $.each($.makeArray(data.files), this._normalizeFile);
1122
+ if (data.fileInput && !data.files) {
1123
+ var that = this,
1124
+ dfd = $.Deferred(),
1125
+ promise = dfd.promise(),
1126
+ jqXHR,
1127
+ aborted;
1128
+ promise.abort = function () {
1129
+ aborted = true;
1130
+ if (jqXHR) {
1131
+ return jqXHR.abort();
1132
+ }
1133
+ dfd.reject(null, 'abort', 'abort');
1134
+ return promise;
1135
+ };
1136
+ this._getFileInputFiles(data.fileInput).always(
1137
+ function (files) {
1138
+ if (aborted) {
1139
+ return;
1140
+ }
1141
+ data.files = files;
1142
+ jqXHR = that._onSend(null, data).then(
1143
+ function (result, textStatus, jqXHR) {
1144
+ dfd.resolve(result, textStatus, jqXHR);
1145
+ },
1146
+ function (jqXHR, textStatus, errorThrown) {
1147
+ dfd.reject(jqXHR, textStatus, errorThrown);
1148
+ }
1149
+ );
1150
+ }
1151
+ );
1152
+ return this._enhancePromise(promise);
1153
+ }
1154
+ data.files = $.makeArray(data.files);
895
1155
  if (data.files.length) {
896
1156
  return this._onSend(null, data);
897
1157
  }