From 2abd663419d9caf265b66518c8066dc3f566460d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 13 Jun 2016 22:26:49 +0200 Subject: [PATCH] update jQuery File Upload Plugin (9.12.5) to support dragleave --- apps/files/js/jquery.fileupload.js | 312 +++++++++++++++++++++-------- 1 file changed, 228 insertions(+), 84 deletions(-) diff --git a/apps/files/js/jquery.fileupload.js b/apps/files/js/jquery.fileupload.js index d7f19b1755..622161ede9 100644 --- a/apps/files/js/jquery.fileupload.js +++ b/apps/files/js/jquery.fileupload.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload Plugin 5.32.2 + * jQuery File Upload Plugin 9.12.5 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan @@ -9,10 +9,10 @@ * http://www.opensource.org/licenses/MIT */ -/*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, location, File, Blob, FormData */ +/* jshint nomen:false */ +/* global define, require, window, document, location, Blob, FormData */ -(function (factory) { +;(function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: @@ -20,6 +20,12 @@ 'jquery', 'jquery.ui.widget' ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./vendor/jquery.ui.widget') + ); } else { // Browser globals: factory(window.jQuery); @@ -40,15 +46,36 @@ $('').prop('disabled')); // The FileReader API is not actually used, but works as feature detection, - // as e.g. Safari supports XHR file uploads via the FormData API, - // but not non-multipart XHR file uploads: - $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); $.support.xhrFormDataFileUpload = !!window.FormData; // Detect support for Blob slicing (required for chunked uploads): $.support.blobSlice = window.Blob && (Blob.prototype.slice || Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + // Helper function to create drag handlers for dragover/dragenter/dragleave: + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + type, + $.Event(type, {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + // The fileupload widget listens for change events on file input fields defined // via fileInput setting and paste or drop events of the given dropZone. // In addition to the default jQuery Widget methods, the fileupload widget @@ -63,9 +90,9 @@ // The drop target element(s), by the default the complete document. // Set to null to disable drag & drop support: dropZone: $(document), - // The paste target element(s), by the default the complete document. - // Set to null to disable paste support: - pasteZone: $(document), + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, // The file input field(s), that are listened to for change events. // If undefined, it is set to the file input fields inside // of the widget element on plugin initialization. @@ -88,6 +115,14 @@ // To limit the number of files uploaded with one XHR request, // set the following option to an integer greater than 0: limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, // Set the following option to true to issue all file upload requests // in a sequential order: sequentialUploads: false, @@ -174,6 +209,9 @@ // handlers using jQuery's Deferred callbacks: // data.submit().done(func).fail(func).always(func); add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } if (data.autoUpload || (data.autoUpload !== false && $(this).fileupload('option', 'autoUpload'))) { data.process().done(function () { @@ -239,7 +277,8 @@ // The following are jQuery ajax settings required for the file uploads: processData: false, contentType: false, - cache: false + cache: false, + timeout: 0 }, // A list of options that require reinitializing event listeners and/or @@ -280,7 +319,7 @@ _getFormData: function (options) { var formData; - if (typeof options.formData === 'function') { + if ($.type(options.formData) === 'function') { return options.formData(options.form); } if ($.isArray(options.formData)) { @@ -360,10 +399,18 @@ // 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); + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); // Trigger a global progress event for all current file uploads, // including ajax calls queued for sequential file uploads: - this._trigger('progressall', e, this._progress); + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); } }, @@ -398,8 +445,9 @@ file = options.files[0], // Ignore non-multipart setting if not supported: multipart = options.multipart || !$.support.xhrFileUpload, - paramName = options.paramName[0]; - options.headers = options.headers || {}; + paramName = $.type(options.paramName) === 'array' ? + options.paramName[0] : options.paramName; + options.headers = $.extend({}, options.headers); if (options.contentRange) { options.headers['Content-Range'] = options.contentRange; } @@ -408,7 +456,7 @@ encodeURI(file.name) + '"'; } if (!multipart) { - options.contentType = file.type; + options.contentType = file.type || 'application/octet-stream'; options.data = options.blob || file; } else if ($.support.xhrFormDataFileUpload) { if (options.postMessage) { @@ -425,7 +473,8 @@ } else { $.each(options.files, function (index, file) { formData.push({ - name: options.paramName[index] || paramName, + name: ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, value: file }); }); @@ -448,9 +497,10 @@ if (that._isInstanceOf('File', file) || that._isInstanceOf('Blob', file)) { formData.append( - options.paramName[index] || paramName, + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, file, - file.name + file.uploadName || file.name ); } }); @@ -534,8 +584,10 @@ 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(); + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); if (options.type !== 'POST' && options.type !== 'PUT' && options.type !== 'PATCH') { options.type = 'POST'; @@ -594,22 +646,32 @@ // Adds convenience methods to the data callback argument: _addConvenienceMethods: function (e, data) { var that = this, - getPromise = function (data) { - return $.Deferred().resolveWith(that, [data]).promise(); + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); }; data.process = function (resolveFunc, rejectFunc) { if (resolveFunc || rejectFunc) { data._processQueue = this._processQueue = - (this._processQueue || getPromise(this)) - .pipe(resolveFunc, rejectFunc); + (this._processQueue || getPromise([this])).then( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).then(resolveFunc, rejectFunc); } - return this._processQueue || getPromise(this); + return this._processQueue || getPromise([this]); }; data.submit = function () { if (this.state() !== 'pending') { data.jqXHR = this.jqXHR = - (that._trigger('submit', e, this) !== false) && - that._onSend(e, this); + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); } return this.jqXHR || that._getXHRPromise(); }; @@ -617,7 +679,9 @@ if (this.jqXHR) { return this.jqXHR.abort(); } - return that._getXHRPromise(); + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); }; data.state = function () { if (this.jqXHR) { @@ -627,6 +691,10 @@ return that._getDeferredState(this._processQueue); } }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; data.progress = function () { return this._progress; }; @@ -829,7 +897,11 @@ // Set timer for bitrate progress calculation: options._bitrateTimer = new that._BitrateTimer(); jqXHR = jqXHR || ( - ((aborted || that._trigger('send', e, options) === false) && + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && that._getXHRPromise(false, options.context, aborted)) || that._chunkedUpload(options) || $.ajax(options) ).done(function (result, textStatus, jqXHR) { @@ -873,9 +945,9 @@ if (this.options.limitConcurrentUploads > 1) { slot = $.Deferred(); this._slots.push(slot); - pipe = slot.pipe(send); + pipe = slot.then(send); } else { - this._sequence = this._sequence.pipe(send, send); + this._sequence = this._sequence.then(send, send); pipe = this._sequence; } // Return the piped Promise object, enhanced with an abort method, @@ -900,50 +972,93 @@ var that = this, result = true, options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, paramName = this._getParamName(options), paramNameSet, paramNameSlice, fileSet, - i; - if (!(options.singleFileUploads || limit) || + i, + j = 0; + if (!filesLength) { + return false; + } + if (limitSize && files[0].size === undefined) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || !this._isXHRUpload(options)) { - fileSet = [data.files]; + fileSet = [files]; paramNameSet = [paramName]; - } else if (!options.singleFileUploads && limit) { + } else if (!(options.singleFileUploads || limitSize) && limit) { fileSet = []; paramNameSet = []; - for (i = 0; i < data.files.length; i += limit) { - fileSet.push(data.files.slice(i, i + limit)); + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); paramNameSlice = paramName.slice(i, i + limit); if (!paramNameSlice.length) { paramNameSlice = paramName; } paramNameSet.push(paramNameSlice); } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + ((batchSize + files[i + 1].size + overhead) > limitSize) || + (limit && i + 1 - j >= limit)) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } } else { paramNameSet = paramName; } - data.originalFiles = data.files; - $.each(fileSet || data.files, function (index, element) { + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { var newData = $.extend({}, data); newData.files = fileSet ? element : [element]; newData.paramName = paramNameSet[index]; that._initResponseObject(newData); that._initProgressObject(newData); that._addConvenienceMethods(e, newData); - result = that._trigger('add', e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); return result; }); return result; }, - _replaceFileInput: function (input) { - var inputClone = input.clone(true); + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true), + restoreFocus = input.is(document.activeElement); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; $('
').append(inputClone)[0].reset(); // Detaching allows to insert the fileInput on another form - // without losing the file input value: + // without loosing the file input value: input.after(inputClone).detach(); + // If the fileInput had focus before it was detached, + // restore focus to the inputClone. + if (restoreFocus) { + inputClone.focus(); + } // Avoid memory leaks with the detached file input: $.cleanData(input.unbind('remove')); // Replace the original file input element in the fileInput @@ -975,7 +1090,25 @@ // to be returned together in one set: dfd.resolve([e]); }, - dirReader; + successHandler = function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, errorHandler); + }, + dirReader, entries = []; path = path || ''; if (entry.isFile) { if (entry._file) { @@ -990,14 +1123,7 @@ } } else if (entry.isDirectory) { dirReader = entry.createReader(); - dirReader.readEntries(function (entries) { - that._handleFileTreeEntries( - entries, - path + entry.name + '/' - ).done(function (files) { - dfd.resolve(files); - }).fail(errorHandler); - }, errorHandler); + readEntries(); } else { // Return an empy list for file system items // other than files or directories: @@ -1013,7 +1139,7 @@ $.map(entries, function (entry) { return that._handleFileTreeEntry(entry, path); }) - ).pipe(function () { + ).then(function () { return Array.prototype.concat.apply( [], arguments @@ -1082,7 +1208,7 @@ return $.when.apply( $, $.map(fileInput, this._getSingleFileInputFiles) - ).pipe(function () { + ).then(function () { return Array.prototype.concat.apply( [], arguments @@ -1099,9 +1225,13 @@ this._getFileInputFiles(data.fileInput).always(function (files) { data.files = files; if (that.options.replaceFileInput) { - that._replaceFileInput(data.fileInput); + that._replaceFileInput(data); } - if (that._trigger('change', e, data) !== false) { + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { that._onAdd(e, data); } }); @@ -1118,9 +1248,12 @@ data.files.push(file); } }); - if (this._trigger('paste', e, data) === false || - this._onAdd(e, data) === false) { - return false; + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); } } }, @@ -1134,32 +1267,32 @@ e.preventDefault(); this._getDroppedFiles(dataTransfer).always(function (files) { data.files = files; - if (that._trigger('drop', e, data) !== false) { + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { that._onAdd(e, data); } }); } }, - _onDragOver: function (e) { - e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; - var dataTransfer = e.dataTransfer; - if (dataTransfer) { - if (this._trigger('dragover', e) === false) { - return false; - } - if ($.inArray('Files', dataTransfer.types) !== -1) { - dataTransfer.dropEffect = 'copy'; - e.preventDefault(); - } - } - }, + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), _initEventHandlers: function () { if (this._isXHRUpload(this.options)) { this._on(this.options.dropZone, { dragover: this._onDragOver, - drop: this._onDrop + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave }); this._on(this.options.pasteZone, { paste: this._onPaste @@ -1173,7 +1306,7 @@ }, _destroyEventHandlers: function () { - this._off(this.options.dropZone, 'dragover drop'); + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); this._off(this.options.pasteZone, 'paste'); this._off(this.options.fileInput, 'change'); }, @@ -1220,15 +1353,25 @@ _initDataAttributes: function () { var that = this, - options = this.options; + options = this.options, + data = this.element.data(); // Initialize options set via HTML5 data-attributes: $.each( - $(this.element[0].cloneNode(false)).data(), - function (key, value) { - if (that._isRegExpOption(key, value)) { - value = that._getRegExp(value); + this.element[0].attributes, + function (index, attr) { + var key = attr.name.toLowerCase(), + value; + if (/^data-/.test(key)) { + // Convert hyphen-ated key to camelCase: + key = key.slice(5).replace(/-[a-z]/g, function (str) { + return str.charAt(1).toUpperCase(); + }); + value = data[key]; + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; } - options[key] = value; } ); }, @@ -1308,7 +1451,8 @@ return; } data.files = files; - jqXHR = that._onSend(null, data).then( + jqXHR = that._onSend(null, data); + jqXHR.then( function (result, textStatus, jqXHR) { dfd.resolve(result, textStatus, jqXHR); },