jquery-fileupload-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,851 @@
1
+ /*
2
+ * jQuery File Upload Plugin 5.8.1
3
+ * https://github.com/blueimp/jQuery-File-Upload
4
+ *
5
+ * Copyright 2010, 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, document, XMLHttpRequestUpload, Blob, File, FormData, location */
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.ui.widget'
22
+ ], factory);
23
+ } else {
24
+ // Browser globals:
25
+ factory(window.jQuery);
26
+ }
27
+ }(function ($) {
28
+ 'use strict';
29
+
30
+ // The fileupload widget listens for change events on file input fields defined
31
+ // via fileInput setting and paste or drop events of the given dropZone.
32
+ // In addition to the default jQuery Widget methods, the fileupload widget
33
+ // exposes the "add" and "send" methods, to add or directly send files using
34
+ // the fileupload API.
35
+ // By default, files added via file input selection, paste, drag & drop or
36
+ // "add" method are uploaded immediately, but it is possible to override
37
+ // the "add" callback option to queue file uploads.
38
+ $.widget('blueimp.fileupload', {
39
+
40
+ options: {
41
+ // The namespace used for event handler binding on the dropZone and
42
+ // fileInput collections.
43
+ // If not set, the name of the widget ("fileupload") is used.
44
+ namespace: undefined,
45
+ // The drop target collection, by the default the complete document.
46
+ // Set to null or an empty collection to disable drag & drop support:
47
+ dropZone: $(document),
48
+ // The file input field collection, that is listened for change events.
49
+ // If undefined, it is set to the file input fields inside
50
+ // of the widget element on plugin initialization.
51
+ // Set to null or an empty collection to disable the change listener.
52
+ fileInput: undefined,
53
+ // By default, the file input field is replaced with a clone after
54
+ // each input field change event. This is required for iframe transport
55
+ // queues and allows change events to be fired for the same file
56
+ // selection, but can be disabled by setting the following option to false:
57
+ replaceFileInput: true,
58
+ // The parameter name for the file form data (the request argument name).
59
+ // If undefined or empty, the name property of the file input field is
60
+ // used, or "files[]" if the file input name property is also empty:
61
+ paramName: undefined,
62
+ // By default, each file of a selection is uploaded using an individual
63
+ // request for XHR type uploads. Set to false to upload file
64
+ // selections in one request each:
65
+ singleFileUploads: true,
66
+ // To limit the number of files uploaded with one XHR request,
67
+ // set the following option to an integer greater than 0:
68
+ limitMultiFileUploads: undefined,
69
+ // Set the following option to true to issue all file upload requests
70
+ // in a sequential order:
71
+ sequentialUploads: false,
72
+ // To limit the number of concurrent uploads,
73
+ // set the following option to an integer greater than 0:
74
+ limitConcurrentUploads: undefined,
75
+ // Set the following option to true to force iframe transport uploads:
76
+ forceIframeTransport: false,
77
+ // Set the following option to the location of a redirect url on the
78
+ // origin server, for cross-domain iframe transport uploads:
79
+ redirect: undefined,
80
+ // The parameter name for the redirect url, sent as part of the form
81
+ // data and set to 'redirect' if this option is empty:
82
+ redirectParamName: undefined,
83
+ // Set the following option to the location of a postMessage window,
84
+ // to enable postMessage transport uploads:
85
+ postMessage: undefined,
86
+ // By default, XHR file uploads are sent as multipart/form-data.
87
+ // The iframe transport is always using multipart/form-data.
88
+ // Set to false to enable non-multipart XHR uploads:
89
+ multipart: true,
90
+ // To upload large files in smaller chunks, set the following option
91
+ // to a preferred maximum chunk size. If set to 0, null or undefined,
92
+ // or the browser does not support the required Blob API, files will
93
+ // be uploaded as a whole.
94
+ maxChunkSize: undefined,
95
+ // When a non-multipart upload or a chunked multipart upload has been
96
+ // aborted, this option can be used to resume the upload by setting
97
+ // it to the size of the already uploaded bytes. This option is most
98
+ // useful when modifying the options object inside of the "add" or
99
+ // "send" callbacks, as the options are cloned for each file upload.
100
+ uploadedBytes: undefined,
101
+ // By default, failed (abort or error) file uploads are removed from the
102
+ // global progress calculation. Set the following option to false to
103
+ // prevent recalculating the global progress data:
104
+ recalculateProgress: true,
105
+
106
+ // Additional form data to be sent along with the file uploads can be set
107
+ // using this option, which accepts an array of objects with name and
108
+ // value properties, a function returning such an array, a FormData
109
+ // object (for XHR file uploads), or a simple object.
110
+ // The form of the first fileInput is given as parameter to the function:
111
+ formData: function (form) {
112
+ return form.serializeArray();
113
+ },
114
+
115
+ // The add callback is invoked as soon as files are added to the fileupload
116
+ // widget (via file input selection, drag & drop, paste or add API call).
117
+ // If the singleFileUploads option is enabled, this callback will be
118
+ // called once for each file in the selection for XHR file uplaods, else
119
+ // once for each file selection.
120
+ // The upload starts when the submit method is invoked on the data parameter.
121
+ // The data object contains a files property holding the added files
122
+ // and allows to override plugin options as well as define ajax settings.
123
+ // Listeners for this callback can also be bound the following way:
124
+ // .bind('fileuploadadd', func);
125
+ // data.submit() returns a Promise object and allows to attach additional
126
+ // handlers using jQuery's Deferred callbacks:
127
+ // data.submit().done(func).fail(func).always(func);
128
+ add: function (e, data) {
129
+ data.submit();
130
+ },
131
+
132
+ // Other callbacks:
133
+ // Callback for the submit event of each file upload:
134
+ // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
135
+ // Callback for the start of each file upload request:
136
+ // send: function (e, data) {}, // .bind('fileuploadsend', func);
137
+ // Callback for successful uploads:
138
+ // done: function (e, data) {}, // .bind('fileuploaddone', func);
139
+ // Callback for failed (abort or error) uploads:
140
+ // fail: function (e, data) {}, // .bind('fileuploadfail', func);
141
+ // Callback for completed (success, abort or error) requests:
142
+ // always: function (e, data) {}, // .bind('fileuploadalways', func);
143
+ // Callback for upload progress events:
144
+ // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
145
+ // Callback for global upload progress events:
146
+ // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
147
+ // Callback for uploads start, equivalent to the global ajaxStart event:
148
+ // start: function (e) {}, // .bind('fileuploadstart', func);
149
+ // Callback for uploads stop, equivalent to the global ajaxStop event:
150
+ // stop: function (e) {}, // .bind('fileuploadstop', func);
151
+ // Callback for change events of the fileInput collection:
152
+ // change: function (e, data) {}, // .bind('fileuploadchange', func);
153
+ // Callback for paste events to the dropZone collection:
154
+ // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
155
+ // Callback for drop events of the dropZone collection:
156
+ // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
157
+ // Callback for dragover events of the dropZone collection:
158
+ // dragover: function (e) {}, // .bind('fileuploaddragover', func);
159
+
160
+ // The plugin options are used as settings object for the ajax calls.
161
+ // The following are jQuery ajax settings required for the file uploads:
162
+ processData: false,
163
+ contentType: false,
164
+ cache: false
165
+ },
166
+
167
+ // A list of options that require a refresh after assigning a new value:
168
+ _refreshOptionsList: ['namespace', 'dropZone', 'fileInput'],
169
+
170
+ _isXHRUpload: function (options) {
171
+ var undef = 'undefined';
172
+ return !options.forceIframeTransport &&
173
+ typeof XMLHttpRequestUpload !== undef && typeof File !== undef &&
174
+ (!options.multipart || typeof FormData !== undef);
175
+ },
176
+
177
+ _getFormData: function (options) {
178
+ var formData;
179
+ if (typeof options.formData === 'function') {
180
+ return options.formData(options.form);
181
+ } else if ($.isArray(options.formData)) {
182
+ return options.formData;
183
+ } else if (options.formData) {
184
+ formData = [];
185
+ $.each(options.formData, function (name, value) {
186
+ formData.push({name: name, value: value});
187
+ });
188
+ return formData;
189
+ }
190
+ return [];
191
+ },
192
+
193
+ _getTotal: function (files) {
194
+ var total = 0;
195
+ $.each(files, function (index, file) {
196
+ total += file.size || 1;
197
+ });
198
+ return total;
199
+ },
200
+
201
+ _onProgress: function (e, data) {
202
+ if (e.lengthComputable) {
203
+ var total = data.total || this._getTotal(data.files),
204
+ loaded = parseInt(
205
+ e.loaded / e.total * (data.chunkSize || total),
206
+ 10
207
+ ) + (data.uploadedBytes || 0);
208
+ this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
209
+ data.lengthComputable = true;
210
+ data.loaded = loaded;
211
+ data.total = total;
212
+ // Trigger a custom progress event with a total data property set
213
+ // to the file size(s) of the current upload and a loaded data
214
+ // property calculated accordingly:
215
+ this._trigger('progress', e, data);
216
+ // Trigger a global progress event for all current file uploads,
217
+ // including ajax calls queued for sequential file uploads:
218
+ this._trigger('progressall', e, {
219
+ lengthComputable: true,
220
+ loaded: this._loaded,
221
+ total: this._total
222
+ });
223
+ }
224
+ },
225
+
226
+ _initProgressListener: function (options) {
227
+ var that = this,
228
+ xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
229
+ // Accesss to the native XHR object is required to add event listeners
230
+ // for the upload progress event:
231
+ if (xhr.upload) {
232
+ $(xhr.upload).bind('progress', function (e) {
233
+ var oe = e.originalEvent;
234
+ // Make sure the progress event properties get copied over:
235
+ e.lengthComputable = oe.lengthComputable;
236
+ e.loaded = oe.loaded;
237
+ e.total = oe.total;
238
+ that._onProgress(e, options);
239
+ });
240
+ options.xhr = function () {
241
+ return xhr;
242
+ };
243
+ }
244
+ },
245
+
246
+ _initXHRData: function (options) {
247
+ var formData,
248
+ file = options.files[0];
249
+ if (!options.multipart || options.blob) {
250
+ // For non-multipart uploads and chunked uploads,
251
+ // file meta data is not part of the request body,
252
+ // so we transmit this data as part of the HTTP headers.
253
+ // For cross domain requests, these headers must be allowed
254
+ // via Access-Control-Allow-Headers or removed using
255
+ // the beforeSend callback:
256
+ options.headers = $.extend(options.headers, {
257
+ 'X-File-Name': file.name,
258
+ 'X-File-Type': file.type,
259
+ 'X-File-Size': file.size
260
+ });
261
+ if (!options.blob) {
262
+ // Non-chunked non-multipart upload:
263
+ options.contentType = file.type;
264
+ options.data = file;
265
+ } else if (!options.multipart) {
266
+ // Chunked non-multipart upload:
267
+ options.contentType = 'application/octet-stream';
268
+ options.data = options.blob;
269
+ }
270
+ }
271
+ if (options.multipart && typeof FormData !== 'undefined') {
272
+ if (options.postMessage) {
273
+ // window.postMessage does not allow sending FormData
274
+ // objects, so we just add the File/Blob objects to
275
+ // the formData array and let the postMessage window
276
+ // create the FormData object out of this array:
277
+ formData = this._getFormData(options);
278
+ if (options.blob) {
279
+ formData.push({
280
+ name: options.paramName,
281
+ value: options.blob
282
+ });
283
+ } else {
284
+ $.each(options.files, function (index, file) {
285
+ formData.push({
286
+ name: options.paramName,
287
+ value: file
288
+ });
289
+ });
290
+ }
291
+ } else {
292
+ if (options.formData instanceof FormData) {
293
+ formData = options.formData;
294
+ } else {
295
+ formData = new FormData();
296
+ $.each(this._getFormData(options), function (index, field) {
297
+ formData.append(field.name, field.value);
298
+ });
299
+ }
300
+ if (options.blob) {
301
+ formData.append(options.paramName, options.blob, file.name);
302
+ } else {
303
+ $.each(options.files, function (index, file) {
304
+ // File objects are also Blob instances.
305
+ // This check allows the tests to run with
306
+ // dummy objects:
307
+ if (file instanceof Blob) {
308
+ formData.append(options.paramName, file, file.name);
309
+ }
310
+ });
311
+ }
312
+ }
313
+ options.data = formData;
314
+ }
315
+ // Blob reference is not needed anymore, free memory:
316
+ options.blob = null;
317
+ },
318
+
319
+ _initIframeSettings: function (options) {
320
+ // Setting the dataType to iframe enables the iframe transport:
321
+ options.dataType = 'iframe ' + (options.dataType || '');
322
+ // The iframe transport accepts a serialized array as form data:
323
+ options.formData = this._getFormData(options);
324
+ // Add redirect url to form data on cross-domain uploads:
325
+ if (options.redirect && $('<a></a>').prop('href', options.url)
326
+ .prop('host') !== location.host) {
327
+ options.formData.push({
328
+ name: options.redirectParamName || 'redirect',
329
+ value: options.redirect
330
+ });
331
+ }
332
+ },
333
+
334
+ _initDataSettings: function (options) {
335
+ if (this._isXHRUpload(options)) {
336
+ if (!this._chunkedUpload(options, true)) {
337
+ if (!options.data) {
338
+ this._initXHRData(options);
339
+ }
340
+ this._initProgressListener(options);
341
+ }
342
+ if (options.postMessage) {
343
+ // Setting the dataType to postmessage enables the
344
+ // postMessage transport:
345
+ options.dataType = 'postmessage ' + (options.dataType || '');
346
+ }
347
+ } else {
348
+ this._initIframeSettings(options, 'iframe');
349
+ }
350
+ },
351
+
352
+ _initFormSettings: function (options) {
353
+ // Retrieve missing options from the input field and the
354
+ // associated form, if available:
355
+ if (!options.form || !options.form.length) {
356
+ options.form = $(options.fileInput.prop('form'));
357
+ }
358
+ if (!options.paramName) {
359
+ options.paramName = options.fileInput.prop('name') ||
360
+ 'files[]';
361
+ }
362
+ if (!options.url) {
363
+ options.url = options.form.prop('action') || location.href;
364
+ }
365
+ // The HTTP request method must be "POST" or "PUT":
366
+ options.type = (options.type || options.form.prop('method') || '')
367
+ .toUpperCase();
368
+ if (options.type !== 'POST' && options.type !== 'PUT') {
369
+ options.type = 'POST';
370
+ }
371
+ },
372
+
373
+ _getAJAXSettings: function (data) {
374
+ var options = $.extend({}, this.options, data);
375
+ this._initFormSettings(options);
376
+ this._initDataSettings(options);
377
+ return options;
378
+ },
379
+
380
+ // Maps jqXHR callbacks to the equivalent
381
+ // methods of the given Promise object:
382
+ _enhancePromise: function (promise) {
383
+ promise.success = promise.done;
384
+ promise.error = promise.fail;
385
+ promise.complete = promise.always;
386
+ return promise;
387
+ },
388
+
389
+ // Creates and returns a Promise object enhanced with
390
+ // the jqXHR methods abort, success, error and complete:
391
+ _getXHRPromise: function (resolveOrReject, context, args) {
392
+ var dfd = $.Deferred(),
393
+ promise = dfd.promise();
394
+ context = context || this.options.context || promise;
395
+ if (resolveOrReject === true) {
396
+ dfd.resolveWith(context, args);
397
+ } else if (resolveOrReject === false) {
398
+ dfd.rejectWith(context, args);
399
+ }
400
+ promise.abort = dfd.promise;
401
+ return this._enhancePromise(promise);
402
+ },
403
+
404
+ // Uploads a file in multiple, sequential requests
405
+ // by splitting the file up in multiple blob chunks.
406
+ // If the second parameter is true, only tests if the file
407
+ // should be uploaded in chunks, but does not invoke any
408
+ // upload requests:
409
+ _chunkedUpload: function (options, testOnly) {
410
+ var that = this,
411
+ file = options.files[0],
412
+ fs = file.size,
413
+ ub = options.uploadedBytes = options.uploadedBytes || 0,
414
+ mcs = options.maxChunkSize || fs,
415
+ // Use the Blob methods with the slice implementation
416
+ // according to the W3C Blob API specification:
417
+ slice = file.webkitSlice || file.mozSlice || file.slice,
418
+ upload,
419
+ n,
420
+ jqXHR,
421
+ pipe;
422
+ if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
423
+ options.data) {
424
+ return false;
425
+ }
426
+ if (testOnly) {
427
+ return true;
428
+ }
429
+ if (ub >= fs) {
430
+ file.error = 'uploadedBytes';
431
+ return this._getXHRPromise(
432
+ false,
433
+ options.context,
434
+ [null, 'error', file.error]
435
+ );
436
+ }
437
+ // n is the number of blobs to upload,
438
+ // calculated via filesize, uploaded bytes and max chunk size:
439
+ n = Math.ceil((fs - ub) / mcs);
440
+ // The chunk upload method accepting the chunk number as parameter:
441
+ upload = function (i) {
442
+ if (!i) {
443
+ return that._getXHRPromise(true, options.context);
444
+ }
445
+ // Upload the blobs in sequential order:
446
+ return upload(i -= 1).pipe(function () {
447
+ // Clone the options object for each chunk upload:
448
+ var o = $.extend({}, options);
449
+ o.blob = slice.call(
450
+ file,
451
+ ub + i * mcs,
452
+ ub + (i + 1) * mcs
453
+ );
454
+ // Store the current chunk size, as the blob itself
455
+ // will be dereferenced after data processing:
456
+ o.chunkSize = o.blob.size;
457
+ // Process the upload data (the blob and potential form data):
458
+ that._initXHRData(o);
459
+ // Add progress listeners for this chunk upload:
460
+ that._initProgressListener(o);
461
+ jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context))
462
+ .done(function () {
463
+ // Create a progress event if upload is done and
464
+ // no progress event has been invoked for this chunk:
465
+ if (!o.loaded) {
466
+ that._onProgress($.Event('progress', {
467
+ lengthComputable: true,
468
+ loaded: o.chunkSize,
469
+ total: o.chunkSize
470
+ }), o);
471
+ }
472
+ options.uploadedBytes = o.uploadedBytes +=
473
+ o.chunkSize;
474
+ });
475
+ return jqXHR;
476
+ });
477
+ };
478
+ // Return the piped Promise object, enhanced with an abort method,
479
+ // which is delegated to the jqXHR object of the current upload,
480
+ // and jqXHR callbacks mapped to the equivalent Promise methods:
481
+ pipe = upload(n);
482
+ pipe.abort = function () {
483
+ return jqXHR.abort();
484
+ };
485
+ return this._enhancePromise(pipe);
486
+ },
487
+
488
+ _beforeSend: function (e, data) {
489
+ if (this._active === 0) {
490
+ // the start callback is triggered when an upload starts
491
+ // and no other uploads are currently running,
492
+ // equivalent to the global ajaxStart event:
493
+ this._trigger('start');
494
+ }
495
+ this._active += 1;
496
+ // Initialize the global progress values:
497
+ this._loaded += data.uploadedBytes || 0;
498
+ this._total += this._getTotal(data.files);
499
+ },
500
+
501
+ _onDone: function (result, textStatus, jqXHR, options) {
502
+ if (!this._isXHRUpload(options)) {
503
+ // Create a progress event for each iframe load:
504
+ this._onProgress($.Event('progress', {
505
+ lengthComputable: true,
506
+ loaded: 1,
507
+ total: 1
508
+ }), options);
509
+ }
510
+ options.result = result;
511
+ options.textStatus = textStatus;
512
+ options.jqXHR = jqXHR;
513
+ this._trigger('done', null, options);
514
+ },
515
+
516
+ _onFail: function (jqXHR, textStatus, errorThrown, options) {
517
+ options.jqXHR = jqXHR;
518
+ options.textStatus = textStatus;
519
+ options.errorThrown = errorThrown;
520
+ this._trigger('fail', null, options);
521
+ if (options.recalculateProgress) {
522
+ // Remove the failed (error or abort) file upload from
523
+ // the global progress calculation:
524
+ this._loaded -= options.loaded || options.uploadedBytes || 0;
525
+ this._total -= options.total || this._getTotal(options.files);
526
+ }
527
+ },
528
+
529
+ _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
530
+ this._active -= 1;
531
+ options.textStatus = textStatus;
532
+ if (jqXHRorError && jqXHRorError.always) {
533
+ options.jqXHR = jqXHRorError;
534
+ options.result = jqXHRorResult;
535
+ } else {
536
+ options.jqXHR = jqXHRorResult;
537
+ options.errorThrown = jqXHRorError;
538
+ }
539
+ this._trigger('always', null, options);
540
+ if (this._active === 0) {
541
+ // The stop callback is triggered when all uploads have
542
+ // been completed, equivalent to the global ajaxStop event:
543
+ this._trigger('stop');
544
+ // Reset the global progress values:
545
+ this._loaded = this._total = 0;
546
+ }
547
+ },
548
+
549
+ _onSend: function (e, data) {
550
+ var that = this,
551
+ jqXHR,
552
+ slot,
553
+ pipe,
554
+ options = that._getAJAXSettings(data),
555
+ send = function (resolve, args) {
556
+ that._sending += 1;
557
+ jqXHR = jqXHR || (
558
+ (resolve !== false &&
559
+ that._trigger('send', e, options) !== false &&
560
+ (that._chunkedUpload(options) || $.ajax(options))) ||
561
+ that._getXHRPromise(false, options.context, args)
562
+ ).done(function (result, textStatus, jqXHR) {
563
+ that._onDone(result, textStatus, jqXHR, options);
564
+ }).fail(function (jqXHR, textStatus, errorThrown) {
565
+ that._onFail(jqXHR, textStatus, errorThrown, options);
566
+ }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
567
+ that._sending -= 1;
568
+ that._onAlways(
569
+ jqXHRorResult,
570
+ textStatus,
571
+ jqXHRorError,
572
+ options
573
+ );
574
+ if (options.limitConcurrentUploads &&
575
+ options.limitConcurrentUploads > that._sending) {
576
+ // Start the next queued upload,
577
+ // that has not been aborted:
578
+ var nextSlot = that._slots.shift();
579
+ while (nextSlot) {
580
+ if (!nextSlot.isRejected()) {
581
+ nextSlot.resolve();
582
+ break;
583
+ }
584
+ nextSlot = that._slots.shift();
585
+ }
586
+ }
587
+ });
588
+ return jqXHR;
589
+ };
590
+ this._beforeSend(e, options);
591
+ if (this.options.sequentialUploads ||
592
+ (this.options.limitConcurrentUploads &&
593
+ this.options.limitConcurrentUploads <= this._sending)) {
594
+ if (this.options.limitConcurrentUploads > 1) {
595
+ slot = $.Deferred();
596
+ this._slots.push(slot);
597
+ pipe = slot.pipe(send);
598
+ } else {
599
+ pipe = (this._sequence = this._sequence.pipe(send, send));
600
+ }
601
+ // Return the piped Promise object, enhanced with an abort method,
602
+ // which is delegated to the jqXHR object of the current upload,
603
+ // and jqXHR callbacks mapped to the equivalent Promise methods:
604
+ pipe.abort = function () {
605
+ var args = [undefined, 'abort', 'abort'];
606
+ if (!jqXHR) {
607
+ if (slot) {
608
+ slot.rejectWith(args);
609
+ }
610
+ return send(false, args);
611
+ }
612
+ return jqXHR.abort();
613
+ };
614
+ return this._enhancePromise(pipe);
615
+ }
616
+ return send();
617
+ },
618
+
619
+ _onAdd: function (e, data) {
620
+ var that = this,
621
+ result = true,
622
+ options = $.extend({}, this.options, data),
623
+ limit = options.limitMultiFileUploads,
624
+ fileSet,
625
+ i;
626
+ if (!(options.singleFileUploads || limit) ||
627
+ !this._isXHRUpload(options)) {
628
+ fileSet = [data.files];
629
+ } else if (!options.singleFileUploads && limit) {
630
+ fileSet = [];
631
+ for (i = 0; i < data.files.length; i += limit) {
632
+ fileSet.push(data.files.slice(i, i + limit));
633
+ }
634
+ }
635
+ data.originalFiles = data.files;
636
+ $.each(fileSet || data.files, function (index, element) {
637
+ var files = fileSet ? element : [element],
638
+ newData = $.extend({}, data, {files: files});
639
+ newData.submit = function () {
640
+ newData.jqXHR = this.jqXHR =
641
+ (that._trigger('submit', e, this) !== false) &&
642
+ that._onSend(e, this);
643
+ return this.jqXHR;
644
+ };
645
+ return (result = that._trigger('add', e, newData));
646
+ });
647
+ return result;
648
+ },
649
+
650
+ // File Normalization for Gecko 1.9.1 (Firefox 3.5) support:
651
+ _normalizeFile: function (index, file) {
652
+ if (file.name === undefined && file.size === undefined) {
653
+ file.name = file.fileName;
654
+ file.size = file.fileSize;
655
+ }
656
+ },
657
+
658
+ _replaceFileInput: function (input) {
659
+ var inputClone = input.clone(true);
660
+ $('<form></form>').append(inputClone)[0].reset();
661
+ // Detaching allows to insert the fileInput on another form
662
+ // without loosing the file input value:
663
+ input.after(inputClone).detach();
664
+ // Avoid memory leaks with the detached file input:
665
+ $.cleanData(input.unbind('remove'));
666
+ // Replace the original file input element in the fileInput
667
+ // collection with the clone, which has been copied including
668
+ // event handlers:
669
+ this.options.fileInput = this.options.fileInput.map(function (i, el) {
670
+ if (el === input[0]) {
671
+ return inputClone[0];
672
+ }
673
+ return el;
674
+ });
675
+ // If the widget has been initialized on the file input itself,
676
+ // override this.element with the file input clone:
677
+ if (input[0] === this.element[0]) {
678
+ this.element = inputClone;
679
+ }
680
+ },
681
+
682
+ _onChange: function (e) {
683
+ var that = e.data.fileupload,
684
+ data = {
685
+ files: $.each($.makeArray(e.target.files), that._normalizeFile),
686
+ fileInput: $(e.target),
687
+ form: $(e.target.form)
688
+ };
689
+ if (!data.files.length) {
690
+ // If the files property is not available, the browser does not
691
+ // support the File API and we add a pseudo File object with
692
+ // the input value as name with path information removed:
693
+ data.files = [{name: e.target.value.replace(/^.*\\/, '')}];
694
+ }
695
+ if (that.options.replaceFileInput) {
696
+ that._replaceFileInput(data.fileInput);
697
+ }
698
+ if (that._trigger('change', e, data) === false ||
699
+ that._onAdd(e, data) === false) {
700
+ return false;
701
+ }
702
+ },
703
+
704
+ _onPaste: function (e) {
705
+ var that = e.data.fileupload,
706
+ cbd = e.originalEvent.clipboardData,
707
+ items = (cbd && cbd.items) || [],
708
+ data = {files: []};
709
+ $.each(items, function (index, item) {
710
+ var file = item.getAsFile && item.getAsFile();
711
+ if (file) {
712
+ data.files.push(file);
713
+ }
714
+ });
715
+ if (that._trigger('paste', e, data) === false ||
716
+ that._onAdd(e, data) === false) {
717
+ return false;
718
+ }
719
+ },
720
+
721
+ _onDrop: function (e) {
722
+ var that = e.data.fileupload,
723
+ dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
724
+ data = {
725
+ files: $.each(
726
+ $.makeArray(dataTransfer && dataTransfer.files),
727
+ that._normalizeFile
728
+ )
729
+ };
730
+ if (that._trigger('drop', e, data) === false ||
731
+ that._onAdd(e, data) === false) {
732
+ return false;
733
+ }
734
+ e.preventDefault();
735
+ },
736
+
737
+ _onDragOver: function (e) {
738
+ var that = e.data.fileupload,
739
+ dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
740
+ if (that._trigger('dragover', e) === false) {
741
+ return false;
742
+ }
743
+ if (dataTransfer) {
744
+ dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy';
745
+ }
746
+ e.preventDefault();
747
+ },
748
+
749
+ _initEventHandlers: function () {
750
+ var ns = this.options.namespace;
751
+ this.options.dropZone
752
+ .bind('dragover.' + ns, {fileupload: this}, this._onDragOver)
753
+ .bind('drop.' + ns, {fileupload: this}, this._onDrop)
754
+ .bind('paste.' + ns, {fileupload: this}, this._onPaste);
755
+ this.options.fileInput
756
+ .bind('change.' + ns, {fileupload: this}, this._onChange);
757
+ },
758
+
759
+ _destroyEventHandlers: function () {
760
+ var ns = this.options.namespace;
761
+ this.options.dropZone
762
+ .unbind('dragover.' + ns, this._onDragOver)
763
+ .unbind('drop.' + ns, this._onDrop)
764
+ .unbind('paste.' + ns, this._onPaste);
765
+ this.options.fileInput
766
+ .unbind('change.' + ns, this._onChange);
767
+ },
768
+
769
+ _setOption: function (key, value) {
770
+ var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
771
+ if (refresh) {
772
+ this._destroyEventHandlers();
773
+ }
774
+ $.Widget.prototype._setOption.call(this, key, value);
775
+ if (refresh) {
776
+ this._initSpecialOptions();
777
+ this._initEventHandlers();
778
+ }
779
+ },
780
+
781
+ _initSpecialOptions: function () {
782
+ var options = this.options;
783
+ if (options.fileInput === undefined) {
784
+ options.fileInput = this.element.is('input:file') ?
785
+ this.element : this.element.find('input:file');
786
+ } else if (!(options.fileInput instanceof $)) {
787
+ options.fileInput = $(options.fileInput);
788
+ }
789
+ if (!(options.dropZone instanceof $)) {
790
+ options.dropZone = $(options.dropZone);
791
+ }
792
+ },
793
+
794
+ _create: function () {
795
+ var options = this.options,
796
+ dataOpts = $.extend({}, this.element.data());
797
+ dataOpts[this.widgetName] = undefined;
798
+ $.extend(options, dataOpts);
799
+ options.namespace = options.namespace || this.widgetName;
800
+ this._initSpecialOptions();
801
+ this._slots = [];
802
+ this._sequence = this._getXHRPromise(true);
803
+ this._sending = this._active = this._loaded = this._total = 0;
804
+ this._initEventHandlers();
805
+ },
806
+
807
+ destroy: function () {
808
+ this._destroyEventHandlers();
809
+ $.Widget.prototype.destroy.call(this);
810
+ },
811
+
812
+ enable: function () {
813
+ $.Widget.prototype.enable.call(this);
814
+ this._initEventHandlers();
815
+ },
816
+
817
+ disable: function () {
818
+ this._destroyEventHandlers();
819
+ $.Widget.prototype.disable.call(this);
820
+ },
821
+
822
+ // This method is exposed to the widget API and allows adding files
823
+ // using the fileupload API. The data parameter accepts an object which
824
+ // must have a files property and can contain additional options:
825
+ // .fileupload('add', {files: filesList});
826
+ add: function (data) {
827
+ if (!data || this.options.disabled) {
828
+ return;
829
+ }
830
+ data.files = $.each($.makeArray(data.files), this._normalizeFile);
831
+ this._onAdd(null, data);
832
+ },
833
+
834
+ // This method is exposed to the widget API and allows sending files
835
+ // using the fileupload API. The data parameter accepts an object which
836
+ // must have a files property and can contain additional options:
837
+ // .fileupload('send', {files: filesList});
838
+ // The method returns a Promise object for the file upload call.
839
+ send: function (data) {
840
+ if (data && !this.options.disabled) {
841
+ data.files = $.each($.makeArray(data.files), this._normalizeFile);
842
+ if (data.files.length) {
843
+ return this._onSend(null, data);
844
+ }
845
+ }
846
+ return this._getXHRPromise(false, data && data.context);
847
+ }
848
+
849
+ });
850
+
851
+ }));