Dotclear

source: admin/js/jsUpload/jquery.fileupload.js @ 1144:4af82896ca3d

Revision 1144:4af82896ca3d, 54.7 KB checked in by Nicolas <nikrou77@…>, 12 years ago (diff)

Remplacement de l'upload utilisant swfupload par le plugin jQuery-File-Upload

Todo:

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

Sites map