Blame | Letzte Änderung | Log anzeigen | RSS feed
/** jQuery File Upload Plugin 5.2* https://github.com/blueimp/jQuery-File-Upload** Copyright 2010, Sebastian Tschan* https://blueimp.net** Licensed under the MIT license:* http://creativecommons.org/licenses/MIT/*//*jslint nomen: true, unparam: true, regexp: true *//*global document, XMLHttpRequestUpload, Blob, File, FormData, location, jQuery */(function ($) {'use strict';// The fileupload widget listens for change events on file input fields// defined via fileInput setting and drop events of the given dropZone.// In addition to the default jQuery Widget methods, the fileupload widget// exposes the "add" and "send" methods, to add or directly send files// using the fileupload API.// By default, files added via file input selection, drag & drop or// "add" method are uploaded immediately, but it is possible to override// the "add" callback option to queue file uploads.$.widget('blueimp.fileupload', {options: {// The namespace used for event handler binding on the dropZone and// fileInput collections.// If not set, the name of the widget ("fileupload") is used.namespace: undefined,// The drop target collection, by the default the complete document.// Set to null or an empty collection to disable drag & drop support:dropZone: $(document),// The file input field collection, that is listened for change events.// If undefined, it is set to the file input fields inside// of the widget element on plugin initialization.// Set to null or an empty collection to disable the change listener.fileInput: undefined,// By default, the file input field is replaced with a clone after// each input field change event. This is required for iframe transport// queues and allows change events to be fired for the same file// selection, but can be disabled by setting the following option to false:replaceFileInput: true,// The parameter name for the file form data (the request argument name).// If undefined or empty, the name property of the file input field is// used, or "files[]" if the file input name property is also empty:paramName: undefined,// By default, each file of a selection is uploaded using an individual// request for XHR type uploads. Set to false to upload file// selections in one request each:singleFileUploads: true,// To limit the number of files uploaded with one XHR request,// set the following option to an integer greater than 0:limitMultiFileUploads: undefined,// Set the following option to true to issue all file upload requests// in a sequential order:sequentialUploads: false,// To limit the number of concurrent uploads,// set the following option to an integer greater than 0:limitConcurrentUploads: undefined,// Set the following option to true to force iframe transport uploads:forceIframeTransport: false,// By default, XHR file uploads are sent as multipart/form-data.// The iframe transport is always using multipart/form-data.// Set to false to enable non-multipart XHR uploads:multipart: true,// To upload large files in smaller chunks, set the following option// to a preferred maximum chunk size. If set to 0, null or undefined,// or the browser does not support the required Blob API, files will// be uploaded as a whole.maxChunkSize: undefined,// When a non-multipart upload or a chunked multipart upload has been// aborted, this option can be used to resume the upload by setting// it to the size of the already uploaded bytes. This option is most// useful when modifying the options object inside of the "add" or// "send" callbacks, as the options are cloned for each file upload.uploadedBytes: undefined,// By default, failed (abort or error) file uploads are removed from the// global progress calculation. Set the following option to false to// prevent recalculating the global progress data:recalculateProgress: true,// Additional form data to be sent along with the file uploads can be set// using this option, which accepts an array of objects with name and// value properties, a function returning such an array, a FormData// object (for XHR file uploads), or a simple object.// The form of the first fileInput is given as parameter to the function:formData: function (form) {return form.serializeArray();},// The add callback is invoked as soon as files are added to the fileupload// widget (via file input selection, drag & drop or add API call).// If the singleFileUploads option is enabled, this callback will be// called once for each file in the selection for XHR file uplaods, else// once for each file selection.// The upload starts when the submit method is invoked on the data parameter.// The data object contains a files property holding the added files// and allows to override plugin options as well as define ajax settings.// Listeners for this callback can also be bound the following way:// .bind('fileuploadadd', func);// data.submit() returns a Promise object and allows to attach additional// handlers using jQuery's Deferred callbacks:// data.submit().done(func).fail(func).always(func);add: function (e, data) {data.submit();},// Other callbacks:// Callback for the start of each file upload request:// send: function (e, data) {}, // .bind('fileuploadsend', func);// Callback for successful uploads:// done: function (e, data) {}, // .bind('fileuploaddone', func);// Callback for failed (abort or error) uploads:// fail: function (e, data) {}, // .bind('fileuploadfail', func);// Callback for completed (success, abort or error) requests:// always: function (e, data) {}, // .bind('fileuploadalways', func);// Callback for upload progress events:// progress: function (e, data) {}, // .bind('fileuploadprogress', func);// Callback for global upload progress events:// progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);// Callback for uploads start, equivalent to the global ajaxStart event:// start: function (e) {}, // .bind('fileuploadstart', func);// Callback for uploads stop, equivalent to the global ajaxStop event:// stop: function (e) {}, // .bind('fileuploadstop', func);// Callback for change events of the fileInput collection:// change: function (e, data) {}, // .bind('fileuploadchange', func);// Callback for drop events of the dropZone collection:// drop: function (e, data) {}, // .bind('fileuploaddrop', func);// Callback for dragover events of the dropZone collection:// dragover: function (e) {}, // .bind('fileuploaddragover', func);// The plugin options are used as settings object for the ajax calls.// The following are jQuery ajax settings required for the file uploads:processData: false,contentType: false,cache: false},// A list of options that require a refresh after assigning a new value:_refreshOptionsList: ['namespace', 'dropZone', 'fileInput'],_isXHRUpload: function (options) {var undef = 'undefined';return !options.forceIframeTransport &&typeof XMLHttpRequestUpload !== undef && typeof File !== undef &&(!options.multipart || typeof FormData !== undef);},_getFormData: function (options) {var formData;if (typeof options.formData === 'function') {return options.formData(options.form);} else if ($.isArray(options.formData)) {return options.formData;} else if (options.formData) {formData = [];$.each(options.formData, function (name, value) {formData.push({name: name, value: value});});return formData;}return [];},_getTotal: function (files) {var total = 0;$.each(files, function (index, file) {total += file.size || 1;});return total;},_onProgress: function (e, data) {if (e.lengthComputable) {var total = data.total || this._getTotal(data.files),loaded = parseInt(e.loaded / e.total * (data.chunkSize || total),10) + (data.uploadedBytes || 0);this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);data.lengthComputable = true;data.loaded = loaded;data.total = total;// Trigger a custom progress event with a total data property set// to the file size(s) of the current upload and a loaded data// property calculated accordingly:this._trigger('progress', e, data);// Trigger a global progress event for all current file uploads,// including ajax calls queued for sequential file uploads:this._trigger('progressall', e, {lengthComputable: true,loaded: this._loaded,total: this._total});}},_initProgressListener: function (options) {var that = this,xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();// Accesss to the native XHR object is required to add event listeners// for the upload progress event:if (xhr.upload && xhr.upload.addEventListener) {xhr.upload.addEventListener('progress', function (e) {that._onProgress(e, options);}, false);options.xhr = function () {return xhr;};}},_initXHRData: function (options) {var formData,file = options.files[0];if (!options.multipart || options.blob) {// For non-multipart uploads and chunked uploads,// file meta data is not part of the request body,// so we transmit this data as part of the HTTP headers.// For cross domain requests, these headers must be allowed// via Access-Control-Allow-Headers or removed using// the beforeSend callback:options.headers = $.extend(options.headers, {'X-File-Name': file.name,'X-File-Type': file.type,'X-File-Size': file.size});if (!options.blob) {// Non-chunked non-multipart upload:options.contentType = file.type;options.data = file;} else if (!options.multipart) {// Chunked non-multipart upload:options.contentType = 'application/octet-stream';options.data = options.blob;}}if (options.multipart && typeof FormData !== 'undefined') {if (options.formData instanceof FormData) {formData = options.formData;} else {formData = new FormData();$.each(this._getFormData(options), function (index, field) {formData.append(field.name, field.value);});}if (options.blob) {formData.append(options.paramName, options.blob);} else {$.each(options.files, function (index, file) {// File objects are also Blob instances.// This check allows the tests to run with// dummy objects:if (file instanceof Blob) {formData.append(options.paramName, file);}});}options.data = formData;}// Blob reference is not needed anymore, free memory:options.blob = null;},_initIframeSettings: function (options) {// Setting the dataType to iframe enables the iframe transport:options.dataType = 'iframe ' + (options.dataType || '');// The iframe transport accepts a serialized array as form data:options.formData = this._getFormData(options);},_initDataSettings: function (options) {if (this._isXHRUpload(options)) {if (!this._chunkedUpload(options, true)) {if (!options.data) {this._initXHRData(options);}this._initProgressListener(options);}} else {this._initIframeSettings(options);}},_initFormSettings: function (options) {// Retrieve missing options from the input field and the// associated form, if available:if (!options.form || !options.form.length) {options.form = $(options.fileInput.prop('form'));}if (!options.paramName) {options.paramName = options.fileInput.prop('name') ||'files[]';}if (!options.url) {options.url = options.form.prop('action') || location.href;}// The HTTP request method must be "POST" or "PUT":options.type = (options.type || options.form.prop('method') || '').toUpperCase();if (options.type !== 'POST' && options.type !== 'PUT') {options.type = 'POST';}},_getAJAXSettings: function (data) {var options = $.extend({}, this.options, data);this._initFormSettings(options);this._initDataSettings(options);return options;},// Maps jqXHR callbacks to the equivalent// methods of the given Promise object:_enhancePromise: function (promise) {promise.success = promise.done;promise.error = promise.fail;promise.complete = promise.always;return promise;},// Creates and returns a Promise object enhanced with// the jqXHR methods abort, success, error and complete:_getXHRPromise: function (resolveOrReject, context, args) {var dfd = $.Deferred(),promise = dfd.promise();context = context || this.options.context || promise;if (resolveOrReject === true) {dfd.resolveWith(context, args);} else if (resolveOrReject === false) {dfd.rejectWith(context, args);}promise.abort = dfd.promise;return this._enhancePromise(promise);},// Uploads a file in multiple, sequential requests// by splitting the file up in multiple blob chunks.// If the second parameter is true, only tests if the file// should be uploaded in chunks, but does not invoke any// upload requests:_chunkedUpload: function (options, testOnly) {var that = this,file = options.files[0],fs = file.size,ub = options.uploadedBytes = options.uploadedBytes || 0,mcs = options.maxChunkSize || fs,// Use the Blob methods with the slice implementation// according to the W3C Blob API specification:slice = file.webkitSlice || file.mozSlice || file.slice,upload,n,jqXHR,pipe;if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||options.data) {return false;}if (testOnly) {return true;}if (ub >= fs) {file.error = 'uploadedBytes';return this._getXHRPromise(false);}// n is the number of blobs to upload,// calculated via filesize, uploaded bytes and max chunk size:n = Math.ceil((fs - ub) / mcs);// The chunk upload method accepting the chunk number as parameter:upload = function (i) {if (!i) {return that._getXHRPromise(true);}// Upload the blobs in sequential order:return upload(i -= 1).pipe(function () {// Clone the options object for each chunk upload:var o = $.extend({}, options);o.blob = slice.call(file,ub + i * mcs,ub + (i + 1) * mcs);// Store the current chunk size, as the blob itself// will be dereferenced after data processing:o.chunkSize = o.blob.size;// Process the upload data (the blob and potential form data):that._initXHRData(o);// Add progress listeners for this chunk upload:that._initProgressListener(o);jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)).done(function () {// Create a progress event if upload is done and// no progress event has been invoked for this chunk:if (!o.loaded) {that._onProgress($.Event('progress', {lengthComputable: true,loaded: o.chunkSize,total: o.chunkSize}), o);}options.uploadedBytes = o.uploadedBytes+= o.chunkSize;});return jqXHR;});};// Return the piped Promise object, enhanced with an abort method,// which is delegated to the jqXHR object of the current upload,// and jqXHR callbacks mapped to the equivalent Promise methods:pipe = upload(n);pipe.abort = function () {return jqXHR.abort();};return this._enhancePromise(pipe);},_beforeSend: function (e, data) {if (this._active === 0) {// the start callback is triggered when an upload starts// and no other uploads are currently running,// equivalent to the global ajaxStart event:this._trigger('start');}this._active += 1;// Initialize the global progress values:this._loaded += data.uploadedBytes || 0;this._total += this._getTotal(data.files);},_onDone: function (result, textStatus, jqXHR, options) {if (!this._isXHRUpload(options)) {// Create a progress event for each iframe load:this._onProgress($.Event('progress', {lengthComputable: true,loaded: 1,total: 1}), options);}options.result = result;options.textStatus = textStatus;options.jqXHR = jqXHR;this._trigger('done', null, options);},_onFail: function (jqXHR, textStatus, errorThrown, options) {options.jqXHR = jqXHR;options.textStatus = textStatus;options.errorThrown = errorThrown;this._trigger('fail', null, options);if (options.recalculateProgress) {// Remove the failed (error or abort) file upload from// the global progress calculation:this._loaded -= options.loaded || options.uploadedBytes || 0;this._total -= options.total || this._getTotal(options.files);}},_onAlways: function (result, textStatus, jqXHR, errorThrown, options) {this._active -= 1;options.result = result;options.textStatus = textStatus;options.jqXHR = jqXHR;options.errorThrown = errorThrown;this._trigger('always', null, options);if (this._active === 0) {// The stop callback is triggered when all uploads have// been completed, equivalent to the global ajaxStop event:this._trigger('stop');// Reset the global progress values:this._loaded = this._total = 0;}},_onSend: function (e, data) {var that = this,jqXHR,slot,pipe,options = that._getAJAXSettings(data),send = function (resolve, args) {that._sending += 1;jqXHR = jqXHR || ((resolve !== false &&that._trigger('send', e, options) !== false &&(that._chunkedUpload(options) || $.ajax(options))) ||that._getXHRPromise(false, options.context, args)).done(function (result, textStatus, jqXHR) {that._onDone(result, textStatus, jqXHR, options);}).fail(function (jqXHR, textStatus, errorThrown) {that._onFail(jqXHR, textStatus, errorThrown, options);}).always(function (a1, a2, a3) {that._sending -= 1;if (a3 && a3.done) {that._onAlways(a1, a2, a3, undefined, options);} else {that._onAlways(undefined, a2, a1, a3, options);}if (options.limitConcurrentUploads &&options.limitConcurrentUploads > that._sending) {// Start the next queued upload,// that has not been aborted:var nextSlot = that._slots.shift();while (nextSlot) {if (!nextSlot.isRejected()) {nextSlot.resolve();break;}nextSlot = that._slots.shift();}}});return jqXHR;};this._beforeSend(e, options);if (this.options.sequentialUploads ||(this.options.limitConcurrentUploads &&this.options.limitConcurrentUploads <= this._sending)) {if (this.options.limitConcurrentUploads > 1) {slot = $.Deferred();this._slots.push(slot);pipe = slot.pipe(send);} else {pipe = (this._sequence = this._sequence.pipe(send, send));}// Return the piped Promise object, enhanced with an abort method,// which is delegated to the jqXHR object of the current upload,// and jqXHR callbacks mapped to the equivalent Promise methods:pipe.abort = function () {var args = [undefined, 'abort', 'abort'];if (!jqXHR) {if (slot) {slot.rejectWith(args);}return send(false, args);}return jqXHR.abort();};return this._enhancePromise(pipe);}return send();},_onAdd: function (e, data) {var that = this,result = true,options = $.extend({}, this.options, data),fileSet = data.files,limit = options.limitMultiFileUploads,i;if (!(options.singleFileUploads || limit) ||!this._isXHRUpload(options)) {fileSet = [fileSet];} else if (!options.singleFileUploads && limit) {fileSet = [];for (i = 0; i < data.files.length; i += limit) {fileSet.push(data.files.slice(i, i + limit));}}$.each(fileSet, function (index, file) {var files = $.isArray(file) ? file : [file],newData = $.extend({}, data, {files: files});newData.submit = function () {return that._onSend(e, newData);};return (result = that._trigger('add', e, newData));});return result;},// File Normalization for Gecko 1.9.1 (Firefox 3.5) support:_normalizeFile: function (index, file) {if (file.name === undefined && file.size === undefined) {file.name = file.fileName;file.size = file.fileSize;}},_replaceFileInput: function (input) {var inputClone = input.clone(true);$('<form></form>').append(inputClone)[0].reset();// Detaching allows to insert the fileInput on another form// without loosing the file input value:input.after(inputClone).detach();// Replace the original file input element in the fileInput// collection with the clone, which has been copied including// event handlers:this.options.fileInput = this.options.fileInput.map(function (i, el) {if (el === input[0]) {return inputClone[0];}return el;});},_onChange: function (e) {var that = e.data.fileupload,data = {files: $.each($.makeArray(e.target.files), that._normalizeFile),fileInput: $(e.target),form: $(e.target.form)};if (!data.files.length) {// If the files property is not available, the browser does not// support the File API and we add a pseudo File object with// the input value as name with path information removed:data.files = [{name: e.target.value.replace(/^.*\\/, '')}];}// Store the form reference as jQuery data for other event handlers,// as the form property is not available after replacing the file input:if (data.form.length) {data.fileInput.data('blueimp.fileupload.form', data.form);} else {data.form = data.fileInput.data('blueimp.fileupload.form');}if (that.options.replaceFileInput) {that._replaceFileInput(data.fileInput);}if (that._trigger('change', e, data) === false ||that._onAdd(e, data) === false) {return false;}},_onDrop: function (e) {var that = e.data.fileupload,dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,data = {files: $.each($.makeArray(dataTransfer && dataTransfer.files),that._normalizeFile)};if (that._trigger('drop', e, data) === false ||that._onAdd(e, data) === false) {return false;}e.preventDefault();},_onDragOver: function (e) {var that = e.data.fileupload,dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;if (that._trigger('dragover', e) === false) {return false;}if (dataTransfer) {dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy';}e.preventDefault();},_initEventHandlers: function () {var ns = this.options.namespace || this.name;this.options.dropZone.bind('dragover.' + ns, {fileupload: this}, this._onDragOver).bind('drop.' + ns, {fileupload: this}, this._onDrop);this.options.fileInput.bind('change.' + ns, {fileupload: this}, this._onChange);},_destroyEventHandlers: function () {var ns = this.options.namespace || this.name;this.options.dropZone.unbind('dragover.' + ns, this._onDragOver).unbind('drop.' + ns, this._onDrop);this.options.fileInput.unbind('change.' + ns, this._onChange);},_beforeSetOption: function (key, value) {this._destroyEventHandlers();},_afterSetOption: function (key, value) {var options = this.options;if (!options.fileInput) {options.fileInput = $();}if (!options.dropZone) {options.dropZone = $();}this._initEventHandlers();},_setOption: function (key, value) {var refresh = $.inArray(key, this._refreshOptionsList) !== -1;if (refresh) {this._beforeSetOption(key, value);}$.Widget.prototype._setOption.call(this, key, value);if (refresh) {this._afterSetOption(key, value);}},_create: function () {var options = this.options;if (options.fileInput === undefined) {options.fileInput = this.element.is('input:file') ?this.element : this.element.find('input:file');} else if (!options.fileInput) {options.fileInput = $();}if (!options.dropZone) {options.dropZone = $();}this._slots = [];this._sequence = this._getXHRPromise(true);this._sending = this._active = this._loaded = this._total = 0;this._initEventHandlers();},destroy: function () {this._destroyEventHandlers();$.Widget.prototype.destroy.call(this);},enable: function () {$.Widget.prototype.enable.call(this);this._initEventHandlers();},disable: function () {this._destroyEventHandlers();$.Widget.prototype.disable.call(this);},// This method is exposed to the widget API and allows adding files// using the fileupload API. The data parameter accepts an object which// must have a files property and can contain additional options:// .fileupload('add', {files: filesList});add: function (data) {if (!data || this.options.disabled) {return;}data.files = $.each($.makeArray(data.files), this._normalizeFile);this._onAdd(null, data);},// This method is exposed to the widget API and allows sending files// using the fileupload API. The data parameter accepts an object which// must have a files property and can contain additional options:// .fileupload('send', {files: filesList});// The method returns a Promise object for the file upload call.send: function (data) {if (data && !this.options.disabled) {data.files = $.each($.makeArray(data.files), this._normalizeFile);if (data.files.length) {return this._onSend(null, data);}}return this._getXHRPromise(false, data && data.context);}});}(jQuery));