From 6d0bc6fdab39c64d9a1151beea86e5b3afefe97c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Joshua=20R=C3=BCsweg?= Date: Sat, 12 Jan 2019 00:02:06 +0100 Subject: [PATCH] Rewrite JS API for uploads See #2825 --- .../js/WoltLabSuite/Core/Ui/File/Upload.js | 364 +++--------------- .../files/js/WoltLabSuite/Core/Upload.js | 32 +- 2 files changed, 71 insertions(+), 325 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/File/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/File/Upload.js index 53e334f3fe..52f4b2957d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/File/Upload.js @@ -5,9 +5,9 @@ * @copyright 2001-2018 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Ui/File/Upload - * @since 3.2 + * @since 5.2 */ -define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse', 'WoltLabSuite/Core/Ui/File/Delete'], function(AjaxRequest, Core, DomChangeListener, Language, DomUtil, DomTraverse, DeleteHandler) { +define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse', 'WoltLabSuite/Core/Ui/File/Delete', 'Upload'], function(AjaxRequest, Core, DomChangeListener, Language, DomUtil, DomTraverse, DeleteHandler, CoreUpload) { "use strict"; /** @@ -65,118 +65,51 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do this._deleteHandler = new DeleteHandler(buttonContainerId, targetId, this._options.imagePreview, this); } - Upload.prototype = { - /** - * Creates the upload button. - */ - _createButton: function() { - this._fileUpload = elCreate('input'); - elAttr(this._fileUpload, 'type', 'file'); - elAttr(this._fileUpload, 'name', this._options.name); - if (this._options.multiple) { - elAttr(this._fileUpload, 'multiple', 'true'); - } - this._fileUpload.addEventListener('change', this._upload.bind(this)); + Core.inherit(Upload, CoreUpload, { + _createFileElement: function(file) { + var element = Upload._super.prototype._createFileElement.call(this, file); + element.classList.add('box64', 'uploadedFile'); - this._button = elCreate('p'); - this._button.className = 'button uploadButton'; + var progress = elBySel('progress', element); - var span = elCreate('span'); - span.textContent = Language.get('wcf.global.button.upload'); - this._button.appendChild(span); + var icon = elCreate('span'); + icon.classList = 'icon icon64 fa-spinner'; - DomUtil.prepend(this._fileUpload, this._button); + var fileName = element.textContent; + element.textContent = ""; + element.append(icon); - this._insertButton(); + var innerDiv = elCreate('div'); + var fileNameP = elCreate('p'); + fileNameP.textContent = fileName; // file.name - DomChangeListener.trigger(); - }, - - /** - * Creates the document element for an uploaded file. - * - * @param {File} file uploaded file - * @return {HTMLElement} - */ - _createFileElement: function(file) { - var li = elCreate('li'); - li.classList = 'box64 uploadedFile'; + var smallProgress = elCreate('small'); + smallProgress.appendChild(progress); - var span = elCreate('span'); - span.classList = 'icon icon64 fa-spinner'; - li.appendChild(span); + innerDiv.appendChild(fileNameP); + innerDiv.appendChild(smallProgress); var div = elCreate('div'); - var innerDiv = elCreate('div'); - var p = elCreate('p'); - p.textContent = file.name; - - var small = elCreate('small'); - var progress = elCreate('progress'); - elAttr(progress, 'max', 100); - small.appendChild(progress); - - innerDiv.appendChild(p); - innerDiv.appendChild(small); + div.appendChild(innerDiv); var ul = elCreate('ul'); ul.classList = 'buttonGroup'; - - div.appendChild(innerDiv); div.appendChild(ul); - li.appendChild(div); - this._target.appendChild(li); + // reset element textContent and replace with own element style + element.append(div); - return li; + return element; }, - /** - * Creates the document elements for uploaded files. - * - * @param {(FileList|Array.)} files uploaded files - */ - _createFileElements: function(files) { - if (files.length) { - var uploadId = this._fileElements.length; - this._fileElements[uploadId] = []; - - for (var i = 0, length = files.length; i < length; i++) { - var file = files[i]; - var fileElement = this._createFileElement(file); - - if (!fileElement.classList.contains('uploadFailed')) { - elData(fileElement, 'filename', file.name); - elData(fileElement, 'internal-file-id', this._internalFileId++); - this._fileElements[uploadId][i] = fileElement; - } - } - - DomChangeListener.trigger(); - - return uploadId; - } - - return null; - }, - - /** - * Handles a failed file upload. - * - * @param {int} uploadId identifier of a file upload - * @param {object} data response data - * @param {string} responseText response - * @param {XMLHttpRequest} xhr request object - * @param {object} requestOptions options used to send AJAX request - * @return {boolean} true if the error message should be shown - */ _failure: function(uploadId, data, responseText, xhr, requestOptions) { for (var i in this._fileElements[uploadId]) { this._fileElements[uploadId][i].classList.add('uploadFailed'); elBySel('small', this._fileElements[uploadId][i]).innerHTML = ''; - elBySel('.icon', this._fileElements[uploadId][i]).classList.remove('fa-spinner'); - elBySel('.icon', this._fileElements[uploadId][i]).classList.add('fa-ban'); + var icon = elBySel('.icon', this._fileElements[uploadId][i]); + icon.classList.remove('fa-spinner'); + icon.classList.add('fa-ban'); var innerError = elCreate('span'); innerError.classList = 'innerError'; @@ -185,61 +118,8 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do } throw new Error("Upload failed: " + data.message); - - return false; - }, - - /** - * Return additional parameters for upload requests. - * - * @return {object} additional parameters - */ - _getParameters: function() { - return {}; - }, - - /** - * Inserts the created button to upload files into the button container. - */ - _insertButton: function() { - DomUtil.prepend(this._button, this._buttonContainer); - }, - - /** - * Updates the progress of an upload. - * - * @param {int} uploadId internal upload identifier - * @param {XMLHttpRequestProgressEvent} event progress event object - */ - _progress: function(uploadId, event) { - var percentComplete = Math.round(event.loaded / event.total * 100); - - for (var i in this._fileElements[uploadId]) { - var progress = elByTag('PROGRESS', this._fileElements[uploadId][i]); - if (progress.length === 1) { - elAttr(progress[0], 'value', percentComplete); - } - } - }, - - /** - * Removes the button to upload files. - */ - _removeButton: function() { - elRemove(this._button); - - DomChangeListener.trigger(); }, - /** - * Handles a successful file upload. - * - * @param {int} uploadId identifier of a file upload - * @param {object} data response data - * @param {string} responseText response - * @param {XMLHttpRequest} xhr request object - * @param {object} requestOptions options used to send AJAX request - */ _success: function(uploadId, data, responseText, xhr, requestOptions) { for (var i in this._fileElements[uploadId]) { if (data['files'][i] !== undefined) { @@ -264,18 +144,19 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do } else { elData(this._fileElements[uploadId][i], 'unique-file-id', data['files'][i].uniqueFileId); - elBySel('small', this._fileElements[uploadId][i]).innerHTML = ''; elBySel('small', this._fileElements[uploadId][i]).textContent = data['files'][i].filesize; - elBySel('.icon', this._fileElements[uploadId][i]).classList.remove('fa-spinner'); - elBySel('.icon', this._fileElements[uploadId][i]).classList.add('fa-' + data['files'][i].icon); + var icon = elBySel('.icon', this._fileElements[uploadId][i]); + icon.classList.remove('fa-spinner'); + icon.classList.add('fa-' + data['files'][i].icon); } } else if (data['error'][i] !== undefined) { this._fileElements[uploadId][i].classList.add('uploadFailed'); elBySel('small', this._fileElements[uploadId][i]).innerHTML = ''; - elBySel('.icon', this._fileElements[uploadId][i]).classList.remove('fa-spinner'); - elBySel('.icon', this._fileElements[uploadId][i]).classList.add('fa-ban'); + var icon = elBySel('.icon', this._fileElements[uploadId][i]); + icon.classList.remove('fa-spinner'); + icon.classList.add('fa-ban'); if (elBySel('.innerError', this._fileElements[uploadId][i]) === null) { var innerError = elCreate('span'); @@ -297,66 +178,15 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do this.checkMaxFiles(); }, - /** - * File input change callback to upload files. - * - * @param {Event} event input change event object - * @param {File} file uploaded file - * @param {Blob} blob file blob - * @return {(int|Array.|null)} identifier(s) for the uploaded files - */ - _upload: function(event, file, blob) { - // remove failed upload elements first - var failedUploads = DomTraverse.childrenByClass(this._target, 'uploadFailed'); - for (var i = 0, length = failedUploads.length; i < length; i++) { - elRemove(failedUploads[i]); - } - - var uploadId = null; - - var files = []; - if (file) { - files.push(file); - } - else if (blob) { - var fileExtension = ''; - switch (blob.type) { - case 'image/jpeg': - fileExtension = '.jpg'; - break; - - case 'image/gif': - fileExtension = '.gif'; - break; - - case 'image/png': - fileExtension = '.png'; - break; - } - - files.push({ - name: 'pasted-from-clipboard' + fileExtension - }); - } - else { - files = this._fileUpload.files; - } - - if (files.length && files.length + this.countFiles() <= this._options.maxFiles) { - if (this._options.singleFileRequests) { - uploadId = []; - for (var i = 0, length = files.length; i < length; i++) { - var localUploadId = this._uploadFiles([ files[i] ], blob); - - if (files.length !== 1) { - this._multiFileUploadIds.push(localUploadId) - } - uploadId.push(localUploadId); - } - } - else { - uploadId = this._uploadFiles(files, blob); - } + _getFormData: function() { + return { + internalId: this._options.internalId + }; + }, + + validateUpload: function(files) { + if (files.length + this.countFiles() <= this._options.maxFiles) { + return true; } else { var innerError = elBySel('small.innerError:not(.innerFileError)', this._buttonContainer.parentNode); @@ -368,117 +198,9 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do } innerError.textContent = WCF.Language.get('wcf.upload.error.reachedRemainingLimit').replace(/#remaining#/, this._options.maxFiles); - } - - // re-create upload button to effectively reset the 'files' - // property of the input element - this._removeButton(); - this._createButton(); - - return uploadId; - - }, - - /** - * Sends the request to upload files. - * - * @param {(FileList|Array.)} files uploaded files - * @param {Blob} blob file blob - * @return {(int|null)} identifier for the uploaded files - */ - _uploadFiles: function(files, blob) { - var uploadId = this._createFileElements(files); - - // no more files left, abort - if (!this._fileElements[uploadId].length) { - return null; - } - - var formData = new FormData(); - for (var i = 0, length = files.length; i < length; i++) { - if (this._fileElements[uploadId][i]) { - var internalFileId = elData(this._fileElements[uploadId][i], 'internal-file-id'); - - if (blob) { - formData.append('__files[' + internalFileId + ']', blob, files[i].name); - } - else { - formData.append('__files[' + internalFileId + ']', files[i]); - } - } - } - - formData.append('internalId', this._options.internalId); - - // recursively append additional parameters to form data - var appendFormData = function(parameters, prefix) { - prefix = prefix || ''; - for (var name in parameters) { - if (typeof parameters[name] === 'object') { - appendFormData(parameters[name], prefix + '[' + name + ']'); - } - else { - formData.append('parameters' + prefix + '[' + name + ']', parameters[name]); - } - } - }; - - appendFormData(this._getParameters()); - - var request = new AjaxRequest({ - data: formData, - contentType: false, - failure: this._failure.bind(this, uploadId), - silent: true, - success: this._success.bind(this, uploadId), - uploadProgress: this._progress.bind(this, uploadId), - url: this._options.url, - withCredentials: true - }); - request.sendRequest(); - - return uploadId; - }, - - /** - * Returns true if there are any pending uploads handled by this - * upload manager. - * - * @return {boolean} - * @since 3.2 - */ - hasPendingUploads: function() { - for (var uploadId in this._fileElements) { - for (var i in this._fileElements[uploadId]) { - var progress = elByTag('PROGRESS', this._fileElements[uploadId][i]); - if (progress.length === 1) { - return true; - } - } + return false; } - - return false; - }, - - /** - * Uploads the given file blob. - * - * @param {Blob} blob file blob - * @return {int} identifier for the uploaded file - */ - uploadBlob: function(blob) { - return this._upload(null, null, blob); - }, - - /** - * Uploads the given file. - * - * @param {File} file uploaded file - * @return {int} identifier(s) for the uploaded file - */ - uploadFile: function(file) { - return this._upload(null, file); }, /** @@ -506,7 +228,7 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do elShow(this._button); } } - }; + }); return Upload; }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js index b9cff77eff..da9d359f73 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js @@ -193,6 +193,16 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do return {}; }, + /** + * Return additional form data for upload requests. + * + * @return {object} additional form data + * @since 5.2 + */ + _getFormData: function() { + return {}; + }, + /** * Inserts the created button to upload files into the button container. */ @@ -284,7 +294,7 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do files = this._fileUpload.files; } - if (files.length) { + if (files.length && this.validateUpload(files)) { if (this._options.singleFileRequests) { uploadId = []; for (var i = 0, length = files.length; i < length; i++) { @@ -309,6 +319,17 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do return uploadId; }, + /** + * Validates the upload before uploading them. + * + * @param {(FileList|Array.)} files uploaded files + * @return {boolean} + * @since 5.2 + */ + validateUpload: function(files) { + return true; + }, + /** * Sends the request to upload files. * @@ -350,15 +371,18 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do for (var name in parameters) { if (typeof parameters[name] === 'object') { - appendFormData(parameters[name], prefix + '[' + name + ']'); + var newPrefix = prefix.length === 0 ? name : prefix + '[' + name + ']'; + appendFormData(parameters[name], newPrefix); } else { - formData.append('parameters' + prefix + '[' + name + ']', parameters[name]); + var dataName = prefix.length === 0 ? name : prefix + '[' + name + ']'; + formData.append(dataName, parameters[name]); } } }; - appendFormData(this._getParameters()); + appendFormData(this._getParameters(), '[parameters]'); + appendFormData(this._getFormData()); var request = new AjaxRequest({ data: formData, -- 2.20.1