Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
21627 kshitij.so 1
/*
2
 * jQuery File Upload Plugin 5.21.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, File, Blob, 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 FileReader API is not actually used, but works as feature detection,
31
    // as e.g. Safari supports XHR file uploads via the FormData API,
32
    // but not non-multipart XHR file uploads:
33
    $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
34
    $.support.xhrFormDataFileUpload = !!window.FormData;
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
 
51
    // The fileupload widget listens for change events on file input fields defined
52
    // via fileInput setting and paste or drop events of the given dropZone.
53
    // In addition to the default jQuery Widget methods, the fileupload widget
54
    // exposes the "add" and "send" methods, to add or directly send files using
55
    // the fileupload API.
56
    // By default, files added via file input selection, paste, drag & drop or
57
    // "add" method are uploaded immediately, but it is possible to override
58
    // the "add" callback option to queue file uploads.
59
    $.widget('blueimp.fileupload', {
60
 
61
        options: {
62
            // The drop target element(s), by the default the complete document.
63
            // Set to null to disable drag & drop support:
64
            dropZone: $(document),
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.
69
            // If undefined, it is set to the file input fields inside
70
            // of the widget element on plugin initialization.
71
            // Set to null to disable the change listener.
72
            fileInput: undefined,
73
            // By default, the file input field is replaced with a clone after
74
            // each input field change event. This is required for iframe transport
75
            // queues and allows change events to be fired for the same file
76
            // selection, but can be disabled by setting the following option to false:
77
            replaceFileInput: true,
78
            // The parameter name for the file form data (the request argument name).
79
            // If undefined or empty, the name property of the file input field is
80
            // used, or "files[]" if the file input name property is also empty,
81
            // can be a string or an array of strings:
82
            paramName: undefined,
83
            // By default, each file of a selection is uploaded using an individual
84
            // request for XHR type uploads. Set to false to upload file
85
            // selections in one request each:
86
            singleFileUploads: true,
87
            // To limit the number of files uploaded with one XHR request,
88
            // set the following option to an integer greater than 0:
89
            limitMultiFileUploads: undefined,
90
            // Set the following option to true to issue all file upload requests
91
            // in a sequential order:
92
            sequentialUploads: false,
93
            // To limit the number of concurrent uploads,
94
            // set the following option to an integer greater than 0:
95
            limitConcurrentUploads: undefined,
96
            // Set the following option to true to force iframe transport uploads:
97
            forceIframeTransport: false,
98
            // Set the following option to the location of a redirect url on the
99
            // origin server, for cross-domain iframe transport uploads:
100
            redirect: undefined,
101
            // The parameter name for the redirect url, sent as part of the form
102
            // data and set to 'redirect' if this option is empty:
103
            redirectParamName: undefined,
104
            // Set the following option to the location of a postMessage window,
105
            // to enable postMessage transport uploads:
106
            postMessage: undefined,
107
            // By default, XHR file uploads are sent as multipart/form-data.
108
            // The iframe transport is always using multipart/form-data.
109
            // Set to false to enable non-multipart XHR uploads:
110
            multipart: true,
111
            // To upload large files in smaller chunks, set the following option
112
            // to a preferred maximum chunk size. If set to 0, null or undefined,
113
            // or the browser does not support the required Blob API, files will
114
            // be uploaded as a whole.
115
            maxChunkSize: undefined,
116
            // When a non-multipart upload or a chunked multipart upload has been
117
            // aborted, this option can be used to resume the upload by setting
118
            // it to the size of the already uploaded bytes. This option is most
119
            // useful when modifying the options object inside of the "add" or
120
            // "send" callbacks, as the options are cloned for each file upload.
121
            uploadedBytes: undefined,
122
            // By default, failed (abort or error) file uploads are removed from the
123
            // global progress calculation. Set the following option to false to
124
            // prevent recalculating the global progress data:
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,
130
 
131
            // Additional form data to be sent along with the file uploads can be set
132
            // using this option, which accepts an array of objects with name and
133
            // value properties, a function returning such an array, a FormData
134
            // object (for XHR file uploads), or a simple object.
135
            // The form of the first fileInput is given as parameter to the function:
136
            formData: function (form) {
137
                return form.serializeArray();
138
            },
139
 
140
            // The add callback is invoked as soon as files are added to the fileupload
141
            // widget (via file input selection, drag & drop, paste or add API call).
142
            // If the singleFileUploads option is enabled, this callback will be
143
            // called once for each file in the selection for XHR file uplaods, else
144
            // once for each file selection.
145
            // The upload starts when the submit method is invoked on the data parameter.
146
            // The data object contains a files property holding the added files
147
            // and allows to override plugin options as well as define ajax settings.
148
            // Listeners for this callback can also be bound the following way:
149
            // .bind('fileuploadadd', func);
150
            // data.submit() returns a Promise object and allows to attach additional
151
            // handlers using jQuery's Deferred callbacks:
152
            // data.submit().done(func).fail(func).always(func);
153
            add: function (e, data) {
154
                data.submit();
155
            },
156
 
157
            // Other callbacks:
158
 
159
            // Callback for the submit event of each file upload:
160
            // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
161
 
162
            // Callback for the start of each file upload request:
163
            // send: function (e, data) {}, // .bind('fileuploadsend', func);
164
 
165
            // Callback for successful uploads:
166
            // done: function (e, data) {}, // .bind('fileuploaddone', func);
167
 
168
            // Callback for failed (abort or error) uploads:
169
            // fail: function (e, data) {}, // .bind('fileuploadfail', func);
170
 
171
            // Callback for completed (success, abort or error) requests:
172
            // always: function (e, data) {}, // .bind('fileuploadalways', func);
173
 
174
            // Callback for upload progress events:
175
            // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
176
 
177
            // Callback for global upload progress events:
178
            // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
179
 
180
            // Callback for uploads start, equivalent to the global ajaxStart event:
181
            // start: function (e) {}, // .bind('fileuploadstart', func);
182
 
183
            // Callback for uploads stop, equivalent to the global ajaxStop event:
184
            // stop: function (e) {}, // .bind('fileuploadstop', func);
185
 
186
            // Callback for change events of the fileInput(s):
187
            // change: function (e, data) {}, // .bind('fileuploadchange', func);
188
 
189
            // Callback for paste events to the pasteZone(s):
190
            // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
191
 
192
            // Callback for drop events of the dropZone(s):
193
            // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
194
 
195
            // Callback for dragover events of the dropZone(s):
196
            // dragover: function (e) {}, // .bind('fileuploaddragover', func);
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
 
210
            // The plugin options are used as settings object for the ajax calls.
211
            // The following are jQuery ajax settings required for the file uploads:
212
            processData: false,
213
            contentType: false,
214
            cache: false
215
        },
216
 
217
        // A list of options that require a refresh after assigning a new value:
218
        _refreshOptionsList: [
219
            'fileInput',
220
            'dropZone',
221
            'pasteZone',
222
            'multipart',
223
            'forceIframeTransport'
224
        ],
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
 
241
        _isXHRUpload: function (options) {
242
            return !options.forceIframeTransport &&
243
                ((!options.multipart && $.support.xhrFileUpload) ||
244
                $.support.xhrFormDataFileUpload);
245
        },
246
 
247
        _getFormData: function (options) {
248
            var formData;
249
            if (typeof options.formData === 'function') {
250
                return options.formData(options.form);
251
            }
252
            if ($.isArray(options.formData)) {
253
                return options.formData;
254
            }
255
            if (options.formData) {
256
                formData = [];
257
                $.each(options.formData, function (name, value) {
258
                    formData.push({name: name, value: value});
259
                });
260
                return formData;
261
            }
262
            return [];
263
        },
264
 
265
        _getTotal: function (files) {
266
            var total = 0;
267
            $.each(files, function (index, file) {
268
                total += file.size || 1;
269
            });
270
            return total;
271
        },
272
 
273
        _onProgress: function (e, data) {
274
            if (e.lengthComputable) {
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);
289
                this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
290
                data.lengthComputable = true;
291
                data.loaded = loaded;
292
                data.total = total;
293
                data.bitrate = data._bitrateTimer.getBitrate(
294
                    now,
295
                    loaded,
296
                    data.bitrateInterval
297
                );
298
                // Trigger a custom progress event with a total data property set
299
                // to the file size(s) of the current upload and a loaded data
300
                // property calculated accordingly:
301
                this._trigger('progress', e, data);
302
                // Trigger a global progress event for all current file uploads,
303
                // including ajax calls queued for sequential file uploads:
304
                this._trigger('progressall', e, {
305
                    lengthComputable: true,
306
                    loaded: this._loaded,
307
                    total: this._total,
308
                    bitrate: this._bitrateTimer.getBitrate(
309
                        now,
310
                        this._loaded,
311
                        data.bitrateInterval
312
                    )
313
                });
314
            }
315
        },
316
 
317
        _initProgressListener: function (options) {
318
            var that = this,
319
                xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
320
            // Accesss to the native XHR object is required to add event listeners
321
            // for the upload progress event:
322
            if (xhr.upload) {
323
                $(xhr.upload).bind('progress', function (e) {
324
                    var oe = e.originalEvent;
325
                    // Make sure the progress event properties get copied over:
326
                    e.lengthComputable = oe.lengthComputable;
327
                    e.loaded = oe.loaded;
328
                    e.total = oe.total;
329
                    that._onProgress(e, options);
330
                });
331
                options.xhr = function () {
332
                    return xhr;
333
                };
334
            }
335
        },
336
 
337
        _initXHRData: function (options) {
338
            var formData,
339
                file = options.files[0],
340
                // Ignore non-multipart setting if not supported:
341
                multipart = options.multipart || !$.support.xhrFileUpload,
342
                paramName = options.paramName[0];
343
            options.headers = options.headers || {};
344
            if (options.contentRange) {
345
                options.headers['Content-Range'] = options.contentRange;
346
            }
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) {
353
                if (options.postMessage) {
354
                    // window.postMessage does not allow sending FormData
355
                    // objects, so we just add the File/Blob objects to
356
                    // the formData array and let the postMessage window
357
                    // create the FormData object out of this array:
358
                    formData = this._getFormData(options);
359
                    if (options.blob) {
360
                        formData.push({
361
                            name: paramName,
362
                            value: options.blob
363
                        });
364
                    } else {
365
                        $.each(options.files, function (index, file) {
366
                            formData.push({
367
                                name: options.paramName[index] || paramName,
368
                                value: file
369
                            });
370
                        });
371
                    }
372
                } else {
373
                    if (options.formData instanceof FormData) {
374
                        formData = options.formData;
375
                    } else {
376
                        formData = new FormData();
377
                        $.each(this._getFormData(options), function (index, field) {
378
                            formData.append(field.name, field.value);
379
                        });
380
                    }
381
                    if (options.blob) {
382
                        options.headers['Content-Disposition'] = 'attachment; filename="' +
383
                            encodeURI(file.name) + '"';
384
                        formData.append(paramName, options.blob, file.name);
385
                    } else {
386
                        $.each(options.files, function (index, file) {
387
                            // Files are also Blob instances, but some browsers
388
                            // (Firefox 3.6) support the File API but not Blobs.
389
                            // This check allows the tests to run with
390
                            // dummy objects:
391
                            if ((window.Blob && file instanceof Blob) ||
392
                                    (window.File && file instanceof File)) {
393
                                formData.append(
394
                                    options.paramName[index] || paramName,
395
                                    file,
396
                                    file.name
397
                                );
398
                            }
399
                        });
400
                    }
401
                }
402
                options.data = formData;
403
            }
404
            // Blob reference is not needed anymore, free memory:
405
            options.blob = null;
406
        },
407
 
408
        _initIframeSettings: function (options) {
409
            // Setting the dataType to iframe enables the iframe transport:
410
            options.dataType = 'iframe ' + (options.dataType || '');
411
            // The iframe transport accepts a serialized array as form data:
412
            options.formData = this._getFormData(options);
413
            // Add redirect url to form data on cross-domain uploads:
414
            if (options.redirect && $('<a></a>').prop('href', options.url)
415
                    .prop('host') !== location.host) {
416
                options.formData.push({
417
                    name: options.redirectParamName || 'redirect',
418
                    value: options.redirect
419
                });
420
            }
421
        },
422
 
423
        _initDataSettings: function (options) {
424
            if (this._isXHRUpload(options)) {
425
                if (!this._chunkedUpload(options, true)) {
426
                    if (!options.data) {
427
                        this._initXHRData(options);
428
                    }
429
                    this._initProgressListener(options);
430
                }
431
                if (options.postMessage) {
432
                    // Setting the dataType to postmessage enables the
433
                    // postMessage transport:
434
                    options.dataType = 'postmessage ' + (options.dataType || '');
435
                }
436
            } else {
437
                this._initIframeSettings(options, 'iframe');
438
            }
439
        },
440
 
441
        _getParamName: function (options) {
442
            var fileInput = $(options.fileInput),
443
                paramName = options.paramName;
444
            if (!paramName) {
445
                paramName = [];
446
                fileInput.each(function () {
447
                    var input = $(this),
448
                        name = input.prop('name') || 'files[]',
449
                        i = (input.prop('files') || [1]).length;
450
                    while (i) {
451
                        paramName.push(name);
452
                        i -= 1;
453
                    }
454
                });
455
                if (!paramName.length) {
456
                    paramName = [fileInput.prop('name') || 'files[]'];
457
                }
458
            } else if (!$.isArray(paramName)) {
459
                paramName = [paramName];
460
            }
461
            return paramName;
462
        },
463
 
464
        _initFormSettings: function (options) {
465
            // Retrieve missing options from the input field and the
466
            // associated form, if available:
467
            if (!options.form || !options.form.length) {
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
                }
474
            }
475
            options.paramName = this._getParamName(options);
476
            if (!options.url) {
477
                options.url = options.form.prop('action') || location.href;
478
            }
479
            // The HTTP request method must be "POST" or "PUT":
480
            options.type = (options.type || options.form.prop('method') || '')
481
                .toUpperCase();
482
            if (options.type !== 'POST' && options.type !== 'PUT' &&
483
                    options.type !== 'PATCH') {
484
                options.type = 'POST';
485
            }
486
            if (!options.formAcceptCharset) {
487
                options.formAcceptCharset = options.form.attr('accept-charset');
488
            }
489
        },
490
 
491
        _getAJAXSettings: function (data) {
492
            var options = $.extend({}, this.options, data);
493
            this._initFormSettings(options);
494
            this._initDataSettings(options);
495
            return options;
496
        },
497
 
498
        // Maps jqXHR callbacks to the equivalent
499
        // methods of the given Promise object:
500
        _enhancePromise: function (promise) {
501
            promise.success = promise.done;
502
            promise.error = promise.fail;
503
            promise.complete = promise.always;
504
            return promise;
505
        },
506
 
507
        // Creates and returns a Promise object enhanced with
508
        // the jqXHR methods abort, success, error and complete:
509
        _getXHRPromise: function (resolveOrReject, context, args) {
510
            var dfd = $.Deferred(),
511
                promise = dfd.promise();
512
            context = context || this.options.context || promise;
513
            if (resolveOrReject === true) {
514
                dfd.resolveWith(context, args);
515
            } else if (resolveOrReject === false) {
516
                dfd.rejectWith(context, args);
517
            }
518
            promise.abort = dfd.promise;
519
            return this._enhancePromise(promise);
520
        },
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
 
532
        // Uploads a file in multiple, sequential requests
533
        // by splitting the file up in multiple blob chunks.
534
        // If the second parameter is true, only tests if the file
535
        // should be uploaded in chunks, but does not invoke any
536
        // upload requests:
537
        _chunkedUpload: function (options, testOnly) {
538
            var that = this,
539
                file = options.files[0],
540
                fs = file.size,
541
                ub = options.uploadedBytes = options.uploadedBytes || 0,
542
                mcs = options.maxChunkSize || fs,
543
                slice = file.slice || file.webkitSlice || file.mozSlice,
544
                dfd = $.Deferred(),
545
                promise = dfd.promise(),
546
                jqXHR,
547
                upload;
548
            if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
549
                    options.data) {
550
                return false;
551
            }
552
            if (testOnly) {
553
                return true;
554
            }
555
            if (ub >= fs) {
556
                file.error = 'Uploaded bytes exceed file size';
557
                return this._getXHRPromise(
558
                    false,
559
                    options.context,
560
                    [null, 'error', file.error]
561
                );
562
            }
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
                    });
626
            };
627
            this._enhancePromise(promise);
628
            promise.abort = function () {
629
                return jqXHR.abort();
630
            };
631
            upload();
632
            return promise;
633
        },
634
 
635
        _beforeSend: function (e, data) {
636
            if (this._active === 0) {
637
                // the start callback is triggered when an upload starts
638
                // and no other uploads are currently running,
639
                // equivalent to the global ajaxStart event:
640
                this._trigger('start');
641
                // Set timer for global bitrate progress calculation:
642
                this._bitrateTimer = new this._BitrateTimer();
643
            }
644
            this._active += 1;
645
            // Initialize the global progress values:
646
            this._loaded += data.uploadedBytes || 0;
647
            this._total += this._getTotal(data.files);
648
        },
649
 
650
        _onDone: function (result, textStatus, jqXHR, options) {
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:
657
                this._onProgress($.Event('progress', {
658
                    lengthComputable: true,
659
                    loaded: total,
660
                    total: total
661
                }), options);
662
            }
663
            options.result = result;
664
            options.textStatus = textStatus;
665
            options.jqXHR = jqXHR;
666
            this._trigger('done', null, options);
667
        },
668
 
669
        _onFail: function (jqXHR, textStatus, errorThrown, options) {
670
            options.jqXHR = jqXHR;
671
            options.textStatus = textStatus;
672
            options.errorThrown = errorThrown;
673
            this._trigger('fail', null, options);
674
            if (options.recalculateProgress) {
675
                // Remove the failed (error or abort) file upload from
676
                // the global progress calculation:
677
                this._loaded -= options.loaded || options.uploadedBytes || 0;
678
                this._total -= options.total || this._getTotal(options.files);
679
            }
680
        },
681
 
682
        _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
683
            // jqXHRorResult, textStatus and jqXHRorError are added to the
684
            // options object via done and fail callbacks
685
            this._active -= 1;
686
            this._trigger('always', null, options);
687
            if (this._active === 0) {
688
                // The stop callback is triggered when all uploads have
689
                // been completed, equivalent to the global ajaxStop event:
690
                this._trigger('stop');
691
                // Reset the global progress values:
692
                this._loaded = this._total = 0;
693
                this._bitrateTimer = null;
694
            }
695
        },
696
 
697
        _onSend: function (e, data) {
698
            var that = this,
699
                jqXHR,
700
                aborted,
701
                slot,
702
                pipe,
703
                options = that._getAJAXSettings(data),
704
                send = function () {
705
                    that._sending += 1;
706
                    // Set timer for bitrate progress calculation:
707
                    options._bitrateTimer = new that._BitrateTimer();
708
                    jqXHR = jqXHR || (
709
                        ((aborted || that._trigger('send', e, options) === false) &&
710
                        that._getXHRPromise(false, options.context, aborted)) ||
711
                        that._chunkedUpload(options) || $.ajax(options)
712
                    ).done(function (result, textStatus, jqXHR) {
713
                        that._onDone(result, textStatus, jqXHR, options);
714
                    }).fail(function (jqXHR, textStatus, errorThrown) {
715
                        that._onFail(jqXHR, textStatus, errorThrown, options);
716
                    }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
717
                        that._sending -= 1;
718
                        that._onAlways(
719
                            jqXHRorResult,
720
                            textStatus,
721
                            jqXHRorError,
722
                            options
723
                        );
724
                        if (options.limitConcurrentUploads &&
725
                                options.limitConcurrentUploads > that._sending) {
726
                            // Start the next queued upload,
727
                            // that has not been aborted:
728
                            var nextSlot = that._slots.shift(),
729
                                isPending;
730
                            while (nextSlot) {
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) {
737
                                    nextSlot.resolve();
738
                                    break;
739
                                }
740
                                nextSlot = that._slots.shift();
741
                            }
742
                        }
743
                    });
744
                    return jqXHR;
745
                };
746
            this._beforeSend(e, options);
747
            if (this.options.sequentialUploads ||
748
                    (this.options.limitConcurrentUploads &&
749
                    this.options.limitConcurrentUploads <= this._sending)) {
750
                if (this.options.limitConcurrentUploads > 1) {
751
                    slot = $.Deferred();
752
                    this._slots.push(slot);
753
                    pipe = slot.pipe(send);
754
                } else {
755
                    pipe = (this._sequence = this._sequence.pipe(send, send));
756
                }
757
                // Return the piped Promise object, enhanced with an abort method,
758
                // which is delegated to the jqXHR object of the current upload,
759
                // and jqXHR callbacks mapped to the equivalent Promise methods:
760
                pipe.abort = function () {
761
                    aborted = [undefined, 'abort', 'abort'];
762
                    if (!jqXHR) {
763
                        if (slot) {
764
                            slot.rejectWith(options.context, aborted);
765
                        }
766
                        return send();
767
                    }
768
                    return jqXHR.abort();
769
                };
770
                return this._enhancePromise(pipe);
771
            }
772
            return send();
773
        },
774
 
775
        _onAdd: function (e, data) {
776
            var that = this,
777
                result = true,
778
                options = $.extend({}, this.options, data),
779
                limit = options.limitMultiFileUploads,
780
                paramName = this._getParamName(options),
781
                paramNameSet,
782
                paramNameSlice,
783
                fileSet,
784
                i;
785
            if (!(options.singleFileUploads || limit) ||
786
                    !this._isXHRUpload(options)) {
787
                fileSet = [data.files];
788
                paramNameSet = [paramName];
789
            } else if (!options.singleFileUploads && limit) {
790
                fileSet = [];
791
                paramNameSet = [];
792
                for (i = 0; i < data.files.length; i += limit) {
793
                    fileSet.push(data.files.slice(i, i + limit));
794
                    paramNameSlice = paramName.slice(i, i + limit);
795
                    if (!paramNameSlice.length) {
796
                        paramNameSlice = paramName;
797
                    }
798
                    paramNameSet.push(paramNameSlice);
799
                }
800
            } else {
801
                paramNameSet = paramName;
802
            }
803
            data.originalFiles = data.files;
804
            $.each(fileSet || data.files, function (index, element) {
805
                var newData = $.extend({}, data);
806
                newData.files = fileSet ? element : [element];
807
                newData.paramName = paramNameSet[index];
808
                newData.submit = function () {
809
                    newData.jqXHR = this.jqXHR =
810
                        (that._trigger('submit', e, this) !== false) &&
811
                        that._onSend(e, this);
812
                    return this.jqXHR;
813
                };
814
                result = that._trigger('add', e, newData);
815
                return result;
816
            });
817
            return result;
818
        },
819
 
820
        _replaceFileInput: function (input) {
821
            var inputClone = input.clone(true);
822
            $('<form></form>').append(inputClone)[0].reset();
823
            // Detaching allows to insert the fileInput on another form
824
            // without loosing the file input value:
825
            input.after(inputClone).detach();
826
            // Avoid memory leaks with the detached file input:
827
            $.cleanData(input.unbind('remove'));
828
            // Replace the original file input element in the fileInput
829
            // elements set with the clone, which has been copied including
830
            // event handlers:
831
            this.options.fileInput = this.options.fileInput.map(function (i, el) {
832
                if (el === input[0]) {
833
                    return inputClone[0];
834
                }
835
                return el;
836
            });
837
            // If the widget has been initialized on the file input itself,
838
            // override this.element with the file input clone:
839
            if (input[0] === this.element[0]) {
840
                this.element = inputClone;
841
            }
842
        },
843
 
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
                }
943
                // If the files property is not available, the browser does not
944
                // support the File API and we add a pseudo File object with
945
                // the input value as name with path information removed:
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
                });
953
            }
954
            return $.Deferred().resolve(files).promise();
955
        },
956
 
957
        _getFileInputFiles: function (fileInput) {
958
            if (!(fileInput instanceof $) || fileInput.length === 1) {
959
                return this._getSingleFileInputFiles(fileInput);
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
            });
987
        },
988
 
989
        _onPaste: function (e) {
990
            var cbd = e.originalEvent.clipboardData,
991
                items = (cbd && cbd.items) || [],
992
                data = {files: []};
993
            $.each(items, function (index, item) {
994
                var file = item.getAsFile && item.getAsFile();
995
                if (file) {
996
                    data.files.push(file);
997
                }
998
            });
999
            if (this._trigger('paste', e, data) === false ||
1000
                    this._onAdd(e, data) === false) {
1001
                return false;
1002
            }
1003
        },
1004
 
1005
        _onDrop: function (e) {
1006
            var that = this,
1007
                dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
1008
                data = {};
1009
            if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
1010
                e.preventDefault();
1011
            }
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
            });
1018
        },
1019
 
1020
        _onDragOver: function (e) {
1021
            var dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
1022
            if (this._trigger('dragover', e) === false) {
1023
                return false;
1024
            }
1025
            if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1) {
1026
                dataTransfer.dropEffect = 'copy';
1027
                e.preventDefault();
1028
            }
1029
        },
1030
 
1031
        _initEventHandlers: function () {
1032
            if (this._isXHRUpload(this.options)) {
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
                });
1040
            }
1041
            this._on(this.options.fileInput, {
1042
                change: this._onChange
1043
            });
1044
        },
1045
 
1046
        _destroyEventHandlers: function () {
1047
            this._off(this.options.dropZone, 'dragover drop');
1048
            this._off(this.options.pasteZone, 'paste');
1049
            this._off(this.options.fileInput, 'change');
1050
        },
1051
 
1052
        _setOption: function (key, value) {
1053
            var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
1054
            if (refresh) {
1055
                this._destroyEventHandlers();
1056
            }
1057
            this._super(key, value);
1058
            if (refresh) {
1059
                this._initSpecialOptions();
1060
                this._initEventHandlers();
1061
            }
1062
        },
1063
 
1064
        _initSpecialOptions: function () {
1065
            var options = this.options;
1066
            if (options.fileInput === undefined) {
1067
                options.fileInput = this.element.is('input[type="file"]') ?
1068
                        this.element : this.element.find('input[type="file"]');
1069
            } else if (!(options.fileInput instanceof $)) {
1070
                options.fileInput = $(options.fileInput);
1071
            }
1072
            if (!(options.dropZone instanceof $)) {
1073
                options.dropZone = $(options.dropZone);
1074
            }
1075
            if (!(options.pasteZone instanceof $)) {
1076
                options.pasteZone = $(options.pasteZone);
1077
            }
1078
        },
1079
 
1080
        _create: function () {
1081
            var options = this.options;
1082
            // Initialize options set via HTML5 data-attributes:
1083
            $.extend(options, $(this.element[0].cloneNode(false)).data());
1084
            this._initSpecialOptions();
1085
            this._slots = [];
1086
            this._sequence = this._getXHRPromise(true);
1087
            this._sending = this._active = this._loaded = this._total = 0;
1088
            this._initEventHandlers();
1089
        },
1090
 
1091
        _destroy: function () {
1092
            this._destroyEventHandlers();
1093
        },
1094
 
1095
        // This method is exposed to the widget API and allows adding files
1096
        // using the fileupload API. The data parameter accepts an object which
1097
        // must have a files property and can contain additional options:
1098
        // .fileupload('add', {files: filesList});
1099
        add: function (data) {
1100
            var that = this;
1101
            if (!data || this.options.disabled) {
1102
                return;
1103
            }
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
            }
1113
        },
1114
 
1115
        // This method is exposed to the widget API and allows sending files
1116
        // using the fileupload API. The data parameter accepts an object which
1117
        // must have a files or fileInput property and can contain additional options:
1118
        // .fileupload('send', {files: filesList});
1119
        // The method returns a Promise object for the file upload call.
1120
        send: function (data) {
1121
            if (data && !this.options.disabled) {
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);
1155
                if (data.files.length) {
1156
                    return this._onSend(null, data);
1157
                }
1158
            }
1159
            return this._getXHRPromise(false, data && data.context);
1160
        }
1161
 
1162
    });
1163
 
1164
}));