jquery-fileupload-requirejs-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1156 @@
1
+ define(['jquery', 'jqueryui/widget'], function ($) {
2
+
3
+
4
+
5
+ /*
6
+ * jQuery File Upload Plugin 5.21
7
+ * https://github.com/blueimp/jQuery-File-Upload
8
+ *
9
+ * Copyright 2010, Sebastian Tschan
10
+ * https://blueimp.net
11
+ *
12
+ * Licensed under the MIT license:
13
+ * http://www.opensource.org/licenses/MIT
14
+ */
15
+
16
+ /*jslint nomen: true, unparam: true, regexp: true */
17
+ /*global define, window, document, File, Blob, FormData, location */
18
+
19
+
20
+ 'use strict';
21
+
22
+ // The FileReader API is not actually used, but works as feature detection,
23
+ // as e.g. Safari supports XHR file uploads via the FormData API,
24
+ // but not non-multipart XHR file uploads:
25
+ $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
26
+ $.support.xhrFormDataFileUpload = !!window.FormData;
27
+
28
+ // The form.elements propHook is added to filter serialized elements
29
+ // to not include file inputs in jQuery 1.9.0.
30
+ // This hooks directly into jQuery.fn.serializeArray.
31
+ // For more info, see http://bugs.jquery.com/ticket/13306
32
+ $.propHooks.elements = {
33
+ get: function (form) {
34
+ if ($.nodeName(form, 'form')) {
35
+ return $.grep(form.elements, function (elem) {
36
+ return !$.nodeName(elem, 'input') || elem.type !== 'file';
37
+ });
38
+ }
39
+ return null;
40
+ }
41
+ };
42
+
43
+ // The fileupload widget listens for change events on file input fields defined
44
+ // via fileInput setting and paste or drop events of the given dropZone.
45
+ // In addition to the default jQuery Widget methods, the fileupload widget
46
+ // exposes the "add" and "send" methods, to add or directly send files using
47
+ // the fileupload API.
48
+ // By default, files added via file input selection, paste, drag & drop or
49
+ // "add" method are uploaded immediately, but it is possible to override
50
+ // the "add" callback option to queue file uploads.
51
+ $.widget('blueimp.fileupload', {
52
+
53
+ options: {
54
+ // The drop target element(s), by the default the complete document.
55
+ // Set to null to disable drag & drop support:
56
+ dropZone: $(document),
57
+ // The paste target element(s), by the default the complete document.
58
+ // Set to null to disable paste support:
59
+ pasteZone: $(document),
60
+ // The file input field(s), that are listened to for change events.
61
+ // If undefined, it is set to the file input fields inside
62
+ // of the widget element on plugin initialization.
63
+ // Set to null to disable the change listener.
64
+ fileInput: undefined,
65
+ // By default, the file input field is replaced with a clone after
66
+ // each input field change event. This is required for iframe transport
67
+ // queues and allows change events to be fired for the same file
68
+ // selection, but can be disabled by setting the following option to false:
69
+ replaceFileInput: true,
70
+ // The parameter name for the file form data (the request argument name).
71
+ // If undefined or empty, the name property of the file input field is
72
+ // used, or "files[]" if the file input name property is also empty,
73
+ // can be a string or an array of strings:
74
+ paramName: undefined,
75
+ // By default, each file of a selection is uploaded using an individual
76
+ // request for XHR type uploads. Set to false to upload file
77
+ // selections in one request each:
78
+ singleFileUploads: true,
79
+ // To limit the number of files uploaded with one XHR request,
80
+ // set the following option to an integer greater than 0:
81
+ limitMultiFileUploads: undefined,
82
+ // Set the following option to true to issue all file upload requests
83
+ // in a sequential order:
84
+ sequentialUploads: false,
85
+ // To limit the number of concurrent uploads,
86
+ // set the following option to an integer greater than 0:
87
+ limitConcurrentUploads: undefined,
88
+ // Set the following option to true to force iframe transport uploads:
89
+ forceIframeTransport: false,
90
+ // Set the following option to the location of a redirect url on the
91
+ // origin server, for cross-domain iframe transport uploads:
92
+ redirect: undefined,
93
+ // The parameter name for the redirect url, sent as part of the form
94
+ // data and set to 'redirect' if this option is empty:
95
+ redirectParamName: undefined,
96
+ // Set the following option to the location of a postMessage window,
97
+ // to enable postMessage transport uploads:
98
+ postMessage: undefined,
99
+ // By default, XHR file uploads are sent as multipart/form-data.
100
+ // The iframe transport is always using multipart/form-data.
101
+ // Set to false to enable non-multipart XHR uploads:
102
+ multipart: true,
103
+ // To upload large files in smaller chunks, set the following option
104
+ // to a preferred maximum chunk size. If set to 0, null or undefined,
105
+ // or the browser does not support the required Blob API, files will
106
+ // be uploaded as a whole.
107
+ maxChunkSize: undefined,
108
+ // When a non-multipart upload or a chunked multipart upload has been
109
+ // aborted, this option can be used to resume the upload by setting
110
+ // it to the size of the already uploaded bytes. This option is most
111
+ // useful when modifying the options object inside of the "add" or
112
+ // "send" callbacks, as the options are cloned for each file upload.
113
+ uploadedBytes: undefined,
114
+ // By default, failed (abort or error) file uploads are removed from the
115
+ // global progress calculation. Set the following option to false to
116
+ // prevent recalculating the global progress data:
117
+ recalculateProgress: true,
118
+ // Interval in milliseconds to calculate and trigger progress events:
119
+ progressInterval: 100,
120
+ // Interval in milliseconds to calculate progress bitrate:
121
+ bitrateInterval: 500,
122
+
123
+ // Additional form data to be sent along with the file uploads can be set
124
+ // using this option, which accepts an array of objects with name and
125
+ // value properties, a function returning such an array, a FormData
126
+ // object (for XHR file uploads), or a simple object.
127
+ // The form of the first fileInput is given as parameter to the function:
128
+ formData: function (form) {
129
+ return form.serializeArray();
130
+ },
131
+
132
+ // The add callback is invoked as soon as files are added to the fileupload
133
+ // widget (via file input selection, drag & drop, paste or add API call).
134
+ // If the singleFileUploads option is enabled, this callback will be
135
+ // called once for each file in the selection for XHR file uplaods, else
136
+ // once for each file selection.
137
+ // The upload starts when the submit method is invoked on the data parameter.
138
+ // The data object contains a files property holding the added files
139
+ // and allows to override plugin options as well as define ajax settings.
140
+ // Listeners for this callback can also be bound the following way:
141
+ // .bind('fileuploadadd', func);
142
+ // data.submit() returns a Promise object and allows to attach additional
143
+ // handlers using jQuery's Deferred callbacks:
144
+ // data.submit().done(func).fail(func).always(func);
145
+ add: function (e, data) {
146
+ data.submit();
147
+ },
148
+
149
+ // Other callbacks:
150
+
151
+ // Callback for the submit event of each file upload:
152
+ // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
153
+
154
+ // Callback for the start of each file upload request:
155
+ // send: function (e, data) {}, // .bind('fileuploadsend', func);
156
+
157
+ // Callback for successful uploads:
158
+ // done: function (e, data) {}, // .bind('fileuploaddone', func);
159
+
160
+ // Callback for failed (abort or error) uploads:
161
+ // fail: function (e, data) {}, // .bind('fileuploadfail', func);
162
+
163
+ // Callback for completed (success, abort or error) requests:
164
+ // always: function (e, data) {}, // .bind('fileuploadalways', func);
165
+
166
+ // Callback for upload progress events:
167
+ // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
168
+
169
+ // Callback for global upload progress events:
170
+ // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
171
+
172
+ // Callback for uploads start, equivalent to the global ajaxStart event:
173
+ // start: function (e) {}, // .bind('fileuploadstart', func);
174
+
175
+ // Callback for uploads stop, equivalent to the global ajaxStop event:
176
+ // stop: function (e) {}, // .bind('fileuploadstop', func);
177
+
178
+ // Callback for change events of the fileInput(s):
179
+ // change: function (e, data) {}, // .bind('fileuploadchange', func);
180
+
181
+ // Callback for paste events to the pasteZone(s):
182
+ // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
183
+
184
+ // Callback for drop events of the dropZone(s):
185
+ // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
186
+
187
+ // Callback for dragover events of the dropZone(s):
188
+ // dragover: function (e) {}, // .bind('fileuploaddragover', func);
189
+
190
+ // Callback for the start of each chunk upload request:
191
+ // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
192
+
193
+ // Callback for successful chunk uploads:
194
+ // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
195
+
196
+ // Callback for failed (abort or error) chunk uploads:
197
+ // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
198
+
199
+ // Callback for completed (success, abort or error) chunk upload requests:
200
+ // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
201
+
202
+ // The plugin options are used as settings object for the ajax calls.
203
+ // The following are jQuery ajax settings required for the file uploads:
204
+ processData: false,
205
+ contentType: false,
206
+ cache: false
207
+ },
208
+
209
+ // A list of options that require a refresh after assigning a new value:
210
+ _refreshOptionsList: [
211
+ 'fileInput',
212
+ 'dropZone',
213
+ 'pasteZone',
214
+ 'multipart',
215
+ 'forceIframeTransport'
216
+ ],
217
+
218
+ _BitrateTimer: function () {
219
+ this.timestamp = +(new Date());
220
+ this.loaded = 0;
221
+ this.bitrate = 0;
222
+ this.getBitrate = function (now, loaded, interval) {
223
+ var timeDiff = now - this.timestamp;
224
+ if (!this.bitrate || !interval || timeDiff > interval) {
225
+ this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
226
+ this.loaded = loaded;
227
+ this.timestamp = now;
228
+ }
229
+ return this.bitrate;
230
+ };
231
+ },
232
+
233
+ _isXHRUpload: function (options) {
234
+ return !options.forceIframeTransport &&
235
+ ((!options.multipart && $.support.xhrFileUpload) ||
236
+ $.support.xhrFormDataFileUpload);
237
+ },
238
+
239
+ _getFormData: function (options) {
240
+ var formData;
241
+ if (typeof options.formData === 'function') {
242
+ return options.formData(options.form);
243
+ }
244
+ if ($.isArray(options.formData)) {
245
+ return options.formData;
246
+ }
247
+ if (options.formData) {
248
+ formData = [];
249
+ $.each(options.formData, function (name, value) {
250
+ formData.push({name: name, value: value});
251
+ });
252
+ return formData;
253
+ }
254
+ return [];
255
+ },
256
+
257
+ _getTotal: function (files) {
258
+ var total = 0;
259
+ $.each(files, function (index, file) {
260
+ total += file.size || 1;
261
+ });
262
+ return total;
263
+ },
264
+
265
+ _onProgress: function (e, data) {
266
+ if (e.lengthComputable) {
267
+ var now = +(new Date()),
268
+ total,
269
+ loaded;
270
+ if (data._time && data.progressInterval &&
271
+ (now - data._time < data.progressInterval) &&
272
+ e.loaded !== e.total) {
273
+ return;
274
+ }
275
+ data._time = now;
276
+ total = data.total || this._getTotal(data.files);
277
+ loaded = parseInt(
278
+ e.loaded / e.total * (data.chunkSize || total),
279
+ 10
280
+ ) + (data.uploadedBytes || 0);
281
+ this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
282
+ data.lengthComputable = true;
283
+ data.loaded = loaded;
284
+ data.total = total;
285
+ data.bitrate = data._bitrateTimer.getBitrate(
286
+ now,
287
+ loaded,
288
+ data.bitrateInterval
289
+ );
290
+ // Trigger a custom progress event with a total data property set
291
+ // to the file size(s) of the current upload and a loaded data
292
+ // property calculated accordingly:
293
+ this._trigger('progress', e, data);
294
+ // Trigger a global progress event for all current file uploads,
295
+ // including ajax calls queued for sequential file uploads:
296
+ this._trigger('progressall', e, {
297
+ lengthComputable: true,
298
+ loaded: this._loaded,
299
+ total: this._total,
300
+ bitrate: this._bitrateTimer.getBitrate(
301
+ now,
302
+ this._loaded,
303
+ data.bitrateInterval
304
+ )
305
+ });
306
+ }
307
+ },
308
+
309
+ _initProgressListener: function (options) {
310
+ var that = this,
311
+ xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
312
+ // Accesss to the native XHR object is required to add event listeners
313
+ // for the upload progress event:
314
+ if (xhr.upload) {
315
+ $(xhr.upload).bind('progress', function (e) {
316
+ var oe = e.originalEvent;
317
+ // Make sure the progress event properties get copied over:
318
+ e.lengthComputable = oe.lengthComputable;
319
+ e.loaded = oe.loaded;
320
+ e.total = oe.total;
321
+ that._onProgress(e, options);
322
+ });
323
+ options.xhr = function () {
324
+ return xhr;
325
+ };
326
+ }
327
+ },
328
+
329
+ _initXHRData: function (options) {
330
+ var formData,
331
+ file = options.files[0],
332
+ // Ignore non-multipart setting if not supported:
333
+ multipart = options.multipart || !$.support.xhrFileUpload,
334
+ paramName = options.paramName[0];
335
+ options.headers = options.headers || {};
336
+ if (options.contentRange) {
337
+ options.headers['Content-Range'] = options.contentRange;
338
+ }
339
+ if (!multipart) {
340
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
341
+ encodeURI(file.name) + '"';
342
+ options.contentType = file.type;
343
+ options.data = options.blob || file;
344
+ } else if ($.support.xhrFormDataFileUpload) {
345
+ if (options.postMessage) {
346
+ // window.postMessage does not allow sending FormData
347
+ // objects, so we just add the File/Blob objects to
348
+ // the formData array and let the postMessage window
349
+ // create the FormData object out of this array:
350
+ formData = this._getFormData(options);
351
+ if (options.blob) {
352
+ formData.push({
353
+ name: paramName,
354
+ value: options.blob
355
+ });
356
+ } else {
357
+ $.each(options.files, function (index, file) {
358
+ formData.push({
359
+ name: options.paramName[index] || paramName,
360
+ value: file
361
+ });
362
+ });
363
+ }
364
+ } else {
365
+ if (options.formData instanceof FormData) {
366
+ formData = options.formData;
367
+ } else {
368
+ formData = new FormData();
369
+ $.each(this._getFormData(options), function (index, field) {
370
+ formData.append(field.name, field.value);
371
+ });
372
+ }
373
+ if (options.blob) {
374
+ options.headers['Content-Disposition'] = 'attachment; filename="' +
375
+ encodeURI(file.name) + '"';
376
+ formData.append(paramName, options.blob, file.name);
377
+ } else {
378
+ $.each(options.files, function (index, file) {
379
+ // Files are also Blob instances, but some browsers
380
+ // (Firefox 3.6) support the File API but not Blobs.
381
+ // This check allows the tests to run with
382
+ // dummy objects:
383
+ if ((window.Blob && file instanceof Blob) ||
384
+ (window.File && file instanceof File)) {
385
+ formData.append(
386
+ options.paramName[index] || paramName,
387
+ file,
388
+ file.name
389
+ );
390
+ }
391
+ });
392
+ }
393
+ }
394
+ options.data = formData;
395
+ }
396
+ // Blob reference is not needed anymore, free memory:
397
+ options.blob = null;
398
+ },
399
+
400
+ _initIframeSettings: function (options) {
401
+ // Setting the dataType to iframe enables the iframe transport:
402
+ options.dataType = 'iframe ' + (options.dataType || '');
403
+ // The iframe transport accepts a serialized array as form data:
404
+ options.formData = this._getFormData(options);
405
+ // Add redirect url to form data on cross-domain uploads:
406
+ if (options.redirect && $('<a></a>').prop('href', options.url)
407
+ .prop('host') !== location.host) {
408
+ options.formData.push({
409
+ name: options.redirectParamName || 'redirect',
410
+ value: options.redirect
411
+ });
412
+ }
413
+ },
414
+
415
+ _initDataSettings: function (options) {
416
+ if (this._isXHRUpload(options)) {
417
+ if (!this._chunkedUpload(options, true)) {
418
+ if (!options.data) {
419
+ this._initXHRData(options);
420
+ }
421
+ this._initProgressListener(options);
422
+ }
423
+ if (options.postMessage) {
424
+ // Setting the dataType to postmessage enables the
425
+ // postMessage transport:
426
+ options.dataType = 'postmessage ' + (options.dataType || '');
427
+ }
428
+ } else {
429
+ this._initIframeSettings(options, 'iframe');
430
+ }
431
+ },
432
+
433
+ _getParamName: function (options) {
434
+ var fileInput = $(options.fileInput),
435
+ paramName = options.paramName;
436
+ if (!paramName) {
437
+ paramName = [];
438
+ fileInput.each(function () {
439
+ var input = $(this),
440
+ name = input.prop('name') || 'files[]',
441
+ i = (input.prop('files') || [1]).length;
442
+ while (i) {
443
+ paramName.push(name);
444
+ i -= 1;
445
+ }
446
+ });
447
+ if (!paramName.length) {
448
+ paramName = [fileInput.prop('name') || 'files[]'];
449
+ }
450
+ } else if (!$.isArray(paramName)) {
451
+ paramName = [paramName];
452
+ }
453
+ return paramName;
454
+ },
455
+
456
+ _initFormSettings: function (options) {
457
+ // Retrieve missing options from the input field and the
458
+ // associated form, if available:
459
+ if (!options.form || !options.form.length) {
460
+ options.form = $(options.fileInput.prop('form'));
461
+ // If the given file input doesn't have an associated form,
462
+ // use the default widget file input's form:
463
+ if (!options.form.length) {
464
+ options.form = $(this.options.fileInput.prop('form'));
465
+ }
466
+ }
467
+ options.paramName = this._getParamName(options);
468
+ if (!options.url) {
469
+ options.url = options.form.prop('action') || location.href;
470
+ }
471
+ // The HTTP request method must be "POST" or "PUT":
472
+ options.type = (options.type || options.form.prop('method') || '')
473
+ .toUpperCase();
474
+ if (options.type !== 'POST' && options.type !== 'PUT' &&
475
+ options.type !== 'PATCH') {
476
+ options.type = 'POST';
477
+ }
478
+ if (!options.formAcceptCharset) {
479
+ options.formAcceptCharset = options.form.attr('accept-charset');
480
+ }
481
+ },
482
+
483
+ _getAJAXSettings: function (data) {
484
+ var options = $.extend({}, this.options, data);
485
+ this._initFormSettings(options);
486
+ this._initDataSettings(options);
487
+ return options;
488
+ },
489
+
490
+ // Maps jqXHR callbacks to the equivalent
491
+ // methods of the given Promise object:
492
+ _enhancePromise: function (promise) {
493
+ promise.success = promise.done;
494
+ promise.error = promise.fail;
495
+ promise.complete = promise.always;
496
+ return promise;
497
+ },
498
+
499
+ // Creates and returns a Promise object enhanced with
500
+ // the jqXHR methods abort, success, error and complete:
501
+ _getXHRPromise: function (resolveOrReject, context, args) {
502
+ var dfd = $.Deferred(),
503
+ promise = dfd.promise();
504
+ context = context || this.options.context || promise;
505
+ if (resolveOrReject === true) {
506
+ dfd.resolveWith(context, args);
507
+ } else if (resolveOrReject === false) {
508
+ dfd.rejectWith(context, args);
509
+ }
510
+ promise.abort = dfd.promise;
511
+ return this._enhancePromise(promise);
512
+ },
513
+
514
+ // Parses the Range header from the server response
515
+ // and returns the uploaded bytes:
516
+ _getUploadedBytes: function (jqXHR) {
517
+ var range = jqXHR.getResponseHeader('Range'),
518
+ parts = range && range.split('-'),
519
+ upperBytesPos = parts && parts.length > 1 &&
520
+ parseInt(parts[1], 10);
521
+ return upperBytesPos && upperBytesPos + 1;
522
+ },
523
+
524
+ // Uploads a file in multiple, sequential requests
525
+ // by splitting the file up in multiple blob chunks.
526
+ // If the second parameter is true, only tests if the file
527
+ // should be uploaded in chunks, but does not invoke any
528
+ // upload requests:
529
+ _chunkedUpload: function (options, testOnly) {
530
+ var that = this,
531
+ file = options.files[0],
532
+ fs = file.size,
533
+ ub = options.uploadedBytes = options.uploadedBytes || 0,
534
+ mcs = options.maxChunkSize || fs,
535
+ slice = file.slice || file.webkitSlice || file.mozSlice,
536
+ dfd = $.Deferred(),
537
+ promise = dfd.promise(),
538
+ jqXHR,
539
+ upload;
540
+ if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
541
+ options.data) {
542
+ return false;
543
+ }
544
+ if (testOnly) {
545
+ return true;
546
+ }
547
+ if (ub >= fs) {
548
+ file.error = 'Uploaded bytes exceed file size';
549
+ return this._getXHRPromise(
550
+ false,
551
+ options.context,
552
+ [null, 'error', file.error]
553
+ );
554
+ }
555
+ // The chunk upload method:
556
+ upload = function (i) {
557
+ // Clone the options object for each chunk upload:
558
+ var o = $.extend({}, options);
559
+ o.blob = slice.call(
560
+ file,
561
+ ub,
562
+ ub + mcs,
563
+ file.type
564
+ );
565
+ // Store the current chunk size, as the blob itself
566
+ // will be dereferenced after data processing:
567
+ o.chunkSize = o.blob.size;
568
+ // Expose the chunk bytes position range:
569
+ o.contentRange = 'bytes ' + ub + '-' +
570
+ (ub + o.chunkSize - 1) + '/' + fs;
571
+ // Process the upload data (the blob and potential form data):
572
+ that._initXHRData(o);
573
+ // Add progress listeners for this chunk upload:
574
+ that._initProgressListener(o);
575
+ jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
576
+ that._getXHRPromise(false, o.context))
577
+ .done(function (result, textStatus, jqXHR) {
578
+ ub = that._getUploadedBytes(jqXHR) ||
579
+ (ub + o.chunkSize);
580
+ // Create a progress event if upload is done and no progress
581
+ // event has been invoked for this chunk, or there has been
582
+ // no progress event with loaded equaling total:
583
+ if (!o.loaded || o.loaded < o.total) {
584
+ that._onProgress($.Event('progress', {
585
+ lengthComputable: true,
586
+ loaded: ub - o.uploadedBytes,
587
+ total: ub - o.uploadedBytes
588
+ }), o);
589
+ }
590
+ options.uploadedBytes = o.uploadedBytes = ub;
591
+ o.result = result;
592
+ o.textStatus = textStatus;
593
+ o.jqXHR = jqXHR;
594
+ that._trigger('chunkdone', null, o);
595
+ that._trigger('chunkalways', null, o);
596
+ if (ub < fs) {
597
+ // File upload not yet complete,
598
+ // continue with the next chunk:
599
+ upload();
600
+ } else {
601
+ dfd.resolveWith(
602
+ o.context,
603
+ [result, textStatus, jqXHR]
604
+ );
605
+ }
606
+ })
607
+ .fail(function (jqXHR, textStatus, errorThrown) {
608
+ o.jqXHR = jqXHR;
609
+ o.textStatus = textStatus;
610
+ o.errorThrown = errorThrown;
611
+ that._trigger('chunkfail', null, o);
612
+ that._trigger('chunkalways', null, o);
613
+ dfd.rejectWith(
614
+ o.context,
615
+ [jqXHR, textStatus, errorThrown]
616
+ );
617
+ });
618
+ };
619
+ this._enhancePromise(promise);
620
+ promise.abort = function () {
621
+ return jqXHR.abort();
622
+ };
623
+ upload();
624
+ return promise;
625
+ },
626
+
627
+ _beforeSend: function (e, data) {
628
+ if (this._active === 0) {
629
+ // the start callback is triggered when an upload starts
630
+ // and no other uploads are currently running,
631
+ // equivalent to the global ajaxStart event:
632
+ this._trigger('start');
633
+ // Set timer for global bitrate progress calculation:
634
+ this._bitrateTimer = new this._BitrateTimer();
635
+ }
636
+ this._active += 1;
637
+ // Initialize the global progress values:
638
+ this._loaded += data.uploadedBytes || 0;
639
+ this._total += this._getTotal(data.files);
640
+ },
641
+
642
+ _onDone: function (result, textStatus, jqXHR, options) {
643
+ if (!this._isXHRUpload(options) || !options.loaded ||
644
+ options.loaded < options.total) {
645
+ var total = this._getTotal(options.files) || 1;
646
+ // Create a progress event for each iframe load,
647
+ // or if there has been no progress event with
648
+ // loaded equaling total for XHR uploads:
649
+ this._onProgress($.Event('progress', {
650
+ lengthComputable: true,
651
+ loaded: total,
652
+ total: total
653
+ }), options);
654
+ }
655
+ options.result = result;
656
+ options.textStatus = textStatus;
657
+ options.jqXHR = jqXHR;
658
+ this._trigger('done', null, options);
659
+ },
660
+
661
+ _onFail: function (jqXHR, textStatus, errorThrown, options) {
662
+ options.jqXHR = jqXHR;
663
+ options.textStatus = textStatus;
664
+ options.errorThrown = errorThrown;
665
+ this._trigger('fail', null, options);
666
+ if (options.recalculateProgress) {
667
+ // Remove the failed (error or abort) file upload from
668
+ // the global progress calculation:
669
+ this._loaded -= options.loaded || options.uploadedBytes || 0;
670
+ this._total -= options.total || this._getTotal(options.files);
671
+ }
672
+ },
673
+
674
+ _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
675
+ // jqXHRorResult, textStatus and jqXHRorError are added to the
676
+ // options object via done and fail callbacks
677
+ this._active -= 1;
678
+ this._trigger('always', null, options);
679
+ if (this._active === 0) {
680
+ // The stop callback is triggered when all uploads have
681
+ // been completed, equivalent to the global ajaxStop event:
682
+ this._trigger('stop');
683
+ // Reset the global progress values:
684
+ this._loaded = this._total = 0;
685
+ this._bitrateTimer = null;
686
+ }
687
+ },
688
+
689
+ _onSend: function (e, data) {
690
+ var that = this,
691
+ jqXHR,
692
+ aborted,
693
+ slot,
694
+ pipe,
695
+ options = that._getAJAXSettings(data),
696
+ send = function () {
697
+ that._sending += 1;
698
+ // Set timer for bitrate progress calculation:
699
+ options._bitrateTimer = new that._BitrateTimer();
700
+ jqXHR = jqXHR || (
701
+ ((aborted || that._trigger('send', e, options) === false) &&
702
+ that._getXHRPromise(false, options.context, aborted)) ||
703
+ that._chunkedUpload(options) || $.ajax(options)
704
+ ).done(function (result, textStatus, jqXHR) {
705
+ that._onDone(result, textStatus, jqXHR, options);
706
+ }).fail(function (jqXHR, textStatus, errorThrown) {
707
+ that._onFail(jqXHR, textStatus, errorThrown, options);
708
+ }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
709
+ that._sending -= 1;
710
+ that._onAlways(
711
+ jqXHRorResult,
712
+ textStatus,
713
+ jqXHRorError,
714
+ options
715
+ );
716
+ if (options.limitConcurrentUploads &&
717
+ options.limitConcurrentUploads > that._sending) {
718
+ // Start the next queued upload,
719
+ // that has not been aborted:
720
+ var nextSlot = that._slots.shift(),
721
+ isPending;
722
+ while (nextSlot) {
723
+ // jQuery 1.6 doesn't provide .state(),
724
+ // while jQuery 1.8+ removed .isRejected():
725
+ isPending = nextSlot.state ?
726
+ nextSlot.state() === 'pending' :
727
+ !nextSlot.isRejected();
728
+ if (isPending) {
729
+ nextSlot.resolve();
730
+ break;
731
+ }
732
+ nextSlot = that._slots.shift();
733
+ }
734
+ }
735
+ });
736
+ return jqXHR;
737
+ };
738
+ this._beforeSend(e, options);
739
+ if (this.options.sequentialUploads ||
740
+ (this.options.limitConcurrentUploads &&
741
+ this.options.limitConcurrentUploads <= this._sending)) {
742
+ if (this.options.limitConcurrentUploads > 1) {
743
+ slot = $.Deferred();
744
+ this._slots.push(slot);
745
+ pipe = slot.pipe(send);
746
+ } else {
747
+ pipe = (this._sequence = this._sequence.pipe(send, send));
748
+ }
749
+ // Return the piped Promise object, enhanced with an abort method,
750
+ // which is delegated to the jqXHR object of the current upload,
751
+ // and jqXHR callbacks mapped to the equivalent Promise methods:
752
+ pipe.abort = function () {
753
+ aborted = [undefined, 'abort', 'abort'];
754
+ if (!jqXHR) {
755
+ if (slot) {
756
+ slot.rejectWith(options.context, aborted);
757
+ }
758
+ return send();
759
+ }
760
+ return jqXHR.abort();
761
+ };
762
+ return this._enhancePromise(pipe);
763
+ }
764
+ return send();
765
+ },
766
+
767
+ _onAdd: function (e, data) {
768
+ var that = this,
769
+ result = true,
770
+ options = $.extend({}, this.options, data),
771
+ limit = options.limitMultiFileUploads,
772
+ paramName = this._getParamName(options),
773
+ paramNameSet,
774
+ paramNameSlice,
775
+ fileSet,
776
+ i;
777
+ if (!(options.singleFileUploads || limit) ||
778
+ !this._isXHRUpload(options)) {
779
+ fileSet = [data.files];
780
+ paramNameSet = [paramName];
781
+ } else if (!options.singleFileUploads && limit) {
782
+ fileSet = [];
783
+ paramNameSet = [];
784
+ for (i = 0; i < data.files.length; i += limit) {
785
+ fileSet.push(data.files.slice(i, i + limit));
786
+ paramNameSlice = paramName.slice(i, i + limit);
787
+ if (!paramNameSlice.length) {
788
+ paramNameSlice = paramName;
789
+ }
790
+ paramNameSet.push(paramNameSlice);
791
+ }
792
+ } else {
793
+ paramNameSet = paramName;
794
+ }
795
+ data.originalFiles = data.files;
796
+ $.each(fileSet || data.files, function (index, element) {
797
+ var newData = $.extend({}, data);
798
+ newData.files = fileSet ? element : [element];
799
+ newData.paramName = paramNameSet[index];
800
+ newData.submit = function () {
801
+ newData.jqXHR = this.jqXHR =
802
+ (that._trigger('submit', e, this) !== false) &&
803
+ that._onSend(e, this);
804
+ return this.jqXHR;
805
+ };
806
+ result = that._trigger('add', e, newData);
807
+ return result;
808
+ });
809
+ return result;
810
+ },
811
+
812
+ _replaceFileInput: function (input) {
813
+ var inputClone = input.clone(true);
814
+ $('<form></form>').append(inputClone)[0].reset();
815
+ // Detaching allows to insert the fileInput on another form
816
+ // without loosing the file input value:
817
+ input.after(inputClone).detach();
818
+ // Avoid memory leaks with the detached file input:
819
+ $.cleanData(input.unbind('remove'));
820
+ // Replace the original file input element in the fileInput
821
+ // elements set with the clone, which has been copied including
822
+ // event handlers:
823
+ this.options.fileInput = this.options.fileInput.map(function (i, el) {
824
+ if (el === input[0]) {
825
+ return inputClone[0];
826
+ }
827
+ return el;
828
+ });
829
+ // If the widget has been initialized on the file input itself,
830
+ // override this.element with the file input clone:
831
+ if (input[0] === this.element[0]) {
832
+ this.element = inputClone;
833
+ }
834
+ },
835
+
836
+ _handleFileTreeEntry: function (entry, path) {
837
+ var that = this,
838
+ dfd = $.Deferred(),
839
+ errorHandler = function (e) {
840
+ if (e && !e.entry) {
841
+ e.entry = entry;
842
+ }
843
+ // Since $.when returns immediately if one
844
+ // Deferred is rejected, we use resolve instead.
845
+ // This allows valid files and invalid items
846
+ // to be returned together in one set:
847
+ dfd.resolve([e]);
848
+ },
849
+ dirReader;
850
+ path = path || '';
851
+ if (entry.isFile) {
852
+ if (entry._file) {
853
+ // Workaround for Chrome bug #149735
854
+ entry._file.relativePath = path;
855
+ dfd.resolve(entry._file);
856
+ } else {
857
+ entry.file(function (file) {
858
+ file.relativePath = path;
859
+ dfd.resolve(file);
860
+ }, errorHandler);
861
+ }
862
+ } else if (entry.isDirectory) {
863
+ dirReader = entry.createReader();
864
+ dirReader.readEntries(function (entries) {
865
+ that._handleFileTreeEntries(
866
+ entries,
867
+ path + entry.name + '/'
868
+ ).done(function (files) {
869
+ dfd.resolve(files);
870
+ }).fail(errorHandler);
871
+ }, errorHandler);
872
+ } else {
873
+ // Return an empy list for file system items
874
+ // other than files or directories:
875
+ dfd.resolve([]);
876
+ }
877
+ return dfd.promise();
878
+ },
879
+
880
+ _handleFileTreeEntries: function (entries, path) {
881
+ var that = this;
882
+ return $.when.apply(
883
+ $,
884
+ $.map(entries, function (entry) {
885
+ return that._handleFileTreeEntry(entry, path);
886
+ })
887
+ ).pipe(function () {
888
+ return Array.prototype.concat.apply(
889
+ [],
890
+ arguments
891
+ );
892
+ });
893
+ },
894
+
895
+ _getDroppedFiles: function (dataTransfer) {
896
+ dataTransfer = dataTransfer || {};
897
+ var items = dataTransfer.items;
898
+ if (items && items.length && (items[0].webkitGetAsEntry ||
899
+ items[0].getAsEntry)) {
900
+ return this._handleFileTreeEntries(
901
+ $.map(items, function (item) {
902
+ var entry;
903
+ if (item.webkitGetAsEntry) {
904
+ entry = item.webkitGetAsEntry();
905
+ if (entry) {
906
+ // Workaround for Chrome bug #149735:
907
+ entry._file = item.getAsFile();
908
+ }
909
+ return entry;
910
+ }
911
+ return item.getAsEntry();
912
+ })
913
+ );
914
+ }
915
+ return $.Deferred().resolve(
916
+ $.makeArray(dataTransfer.files)
917
+ ).promise();
918
+ },
919
+
920
+ _getSingleFileInputFiles: function (fileInput) {
921
+ fileInput = $(fileInput);
922
+ var entries = fileInput.prop('webkitEntries') ||
923
+ fileInput.prop('entries'),
924
+ files,
925
+ value;
926
+ if (entries && entries.length) {
927
+ return this._handleFileTreeEntries(entries);
928
+ }
929
+ files = $.makeArray(fileInput.prop('files'));
930
+ if (!files.length) {
931
+ value = fileInput.prop('value');
932
+ if (!value) {
933
+ return $.Deferred().resolve([]).promise();
934
+ }
935
+ // If the files property is not available, the browser does not
936
+ // support the File API and we add a pseudo File object with
937
+ // the input value as name with path information removed:
938
+ files = [{name: value.replace(/^.*\\/, '')}];
939
+ } else if (files[0].name === undefined && files[0].fileName) {
940
+ // File normalization for Safari 4 and Firefox 3:
941
+ $.each(files, function (index, file) {
942
+ file.name = file.fileName;
943
+ file.size = file.fileSize;
944
+ });
945
+ }
946
+ return $.Deferred().resolve(files).promise();
947
+ },
948
+
949
+ _getFileInputFiles: function (fileInput) {
950
+ if (!(fileInput instanceof $) || fileInput.length === 1) {
951
+ return this._getSingleFileInputFiles(fileInput);
952
+ }
953
+ return $.when.apply(
954
+ $,
955
+ $.map(fileInput, this._getSingleFileInputFiles)
956
+ ).pipe(function () {
957
+ return Array.prototype.concat.apply(
958
+ [],
959
+ arguments
960
+ );
961
+ });
962
+ },
963
+
964
+ _onChange: function (e) {
965
+ var that = this,
966
+ data = {
967
+ fileInput: $(e.target),
968
+ form: $(e.target.form)
969
+ };
970
+ this._getFileInputFiles(data.fileInput).always(function (files) {
971
+ data.files = files;
972
+ if (that.options.replaceFileInput) {
973
+ that._replaceFileInput(data.fileInput);
974
+ }
975
+ if (that._trigger('change', e, data) !== false) {
976
+ that._onAdd(e, data);
977
+ }
978
+ });
979
+ },
980
+
981
+ _onPaste: function (e) {
982
+ var cbd = e.originalEvent.clipboardData,
983
+ items = (cbd && cbd.items) || [],
984
+ data = {files: []};
985
+ $.each(items, function (index, item) {
986
+ var file = item.getAsFile && item.getAsFile();
987
+ if (file) {
988
+ data.files.push(file);
989
+ }
990
+ });
991
+ if (this._trigger('paste', e, data) === false ||
992
+ this._onAdd(e, data) === false) {
993
+ return false;
994
+ }
995
+ },
996
+
997
+ _onDrop: function (e) {
998
+ var that = this,
999
+ dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
1000
+ data = {};
1001
+ if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1002
+ e.preventDefault();
1003
+ }
1004
+ this._getDroppedFiles(dataTransfer).always(function (files) {
1005
+ data.files = files;
1006
+ if (that._trigger('drop', e, data) !== false) {
1007
+ that._onAdd(e, data);
1008
+ }
1009
+ });
1010
+ },
1011
+
1012
+ _onDragOver: function (e) {
1013
+ var dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
1014
+ if (this._trigger('dragover', e) === false) {
1015
+ return false;
1016
+ }
1017
+ if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1) {
1018
+ dataTransfer.dropEffect = 'copy';
1019
+ e.preventDefault();
1020
+ }
1021
+ },
1022
+
1023
+ _initEventHandlers: function () {
1024
+ if (this._isXHRUpload(this.options)) {
1025
+ this._on(this.options.dropZone, {
1026
+ dragover: this._onDragOver,
1027
+ drop: this._onDrop
1028
+ });
1029
+ this._on(this.options.pasteZone, {
1030
+ paste: this._onPaste
1031
+ });
1032
+ }
1033
+ this._on(this.options.fileInput, {
1034
+ change: this._onChange
1035
+ });
1036
+ },
1037
+
1038
+ _destroyEventHandlers: function () {
1039
+ this._off(this.options.dropZone, 'dragover drop');
1040
+ this._off(this.options.pasteZone, 'paste');
1041
+ this._off(this.options.fileInput, 'change');
1042
+ },
1043
+
1044
+ _setOption: function (key, value) {
1045
+ var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
1046
+ if (refresh) {
1047
+ this._destroyEventHandlers();
1048
+ }
1049
+ this._super(key, value);
1050
+ if (refresh) {
1051
+ this._initSpecialOptions();
1052
+ this._initEventHandlers();
1053
+ }
1054
+ },
1055
+
1056
+ _initSpecialOptions: function () {
1057
+ var options = this.options;
1058
+ if (options.fileInput === undefined) {
1059
+ options.fileInput = this.element.is('input[type="file"]') ?
1060
+ this.element : this.element.find('input[type="file"]');
1061
+ } else if (!(options.fileInput instanceof $)) {
1062
+ options.fileInput = $(options.fileInput);
1063
+ }
1064
+ if (!(options.dropZone instanceof $)) {
1065
+ options.dropZone = $(options.dropZone);
1066
+ }
1067
+ if (!(options.pasteZone instanceof $)) {
1068
+ options.pasteZone = $(options.pasteZone);
1069
+ }
1070
+ },
1071
+
1072
+ _create: function () {
1073
+ var options = this.options;
1074
+ // Initialize options set via HTML5 data-attributes:
1075
+ $.extend(options, $(this.element[0].cloneNode(false)).data());
1076
+ this._initSpecialOptions();
1077
+ this._slots = [];
1078
+ this._sequence = this._getXHRPromise(true);
1079
+ this._sending = this._active = this._loaded = this._total = 0;
1080
+ this._initEventHandlers();
1081
+ },
1082
+
1083
+ _destroy: function () {
1084
+ this._destroyEventHandlers();
1085
+ },
1086
+
1087
+ // This method is exposed to the widget API and allows adding files
1088
+ // using the fileupload API. The data parameter accepts an object which
1089
+ // must have a files property and can contain additional options:
1090
+ // .fileupload('add', {files: filesList});
1091
+ add: function (data) {
1092
+ var that = this;
1093
+ if (!data || this.options.disabled) {
1094
+ return;
1095
+ }
1096
+ if (data.fileInput && !data.files) {
1097
+ this._getFileInputFiles(data.fileInput).always(function (files) {
1098
+ data.files = files;
1099
+ that._onAdd(null, data);
1100
+ });
1101
+ } else {
1102
+ data.files = $.makeArray(data.files);
1103
+ this._onAdd(null, data);
1104
+ }
1105
+ },
1106
+
1107
+ // This method is exposed to the widget API and allows sending files
1108
+ // using the fileupload API. The data parameter accepts an object which
1109
+ // must have a files or fileInput property and can contain additional options:
1110
+ // .fileupload('send', {files: filesList});
1111
+ // The method returns a Promise object for the file upload call.
1112
+ send: function (data) {
1113
+ if (data && !this.options.disabled) {
1114
+ if (data.fileInput && !data.files) {
1115
+ var that = this,
1116
+ dfd = $.Deferred(),
1117
+ promise = dfd.promise(),
1118
+ jqXHR,
1119
+ aborted;
1120
+ promise.abort = function () {
1121
+ aborted = true;
1122
+ if (jqXHR) {
1123
+ return jqXHR.abort();
1124
+ }
1125
+ dfd.reject(null, 'abort', 'abort');
1126
+ return promise;
1127
+ };
1128
+ this._getFileInputFiles(data.fileInput).always(
1129
+ function (files) {
1130
+ if (aborted) {
1131
+ return;
1132
+ }
1133
+ data.files = files;
1134
+ jqXHR = that._onSend(null, data).then(
1135
+ function (result, textStatus, jqXHR) {
1136
+ dfd.resolve(result, textStatus, jqXHR);
1137
+ },
1138
+ function (jqXHR, textStatus, errorThrown) {
1139
+ dfd.reject(jqXHR, textStatus, errorThrown);
1140
+ }
1141
+ );
1142
+ }
1143
+ );
1144
+ return this._enhancePromise(promise);
1145
+ }
1146
+ data.files = $.makeArray(data.files);
1147
+ if (data.files.length) {
1148
+ return this._onSend(null, data);
1149
+ }
1150
+ }
1151
+ return this._getXHRPromise(false, data && data.context);
1152
+ }
1153
+
1154
+ });
1155
+
1156
+ });