From 08a8f9dca1fe9462f22fe831cb7e9a607fb783dc Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 7 Nov 2020 19:32:48 +0100 Subject: [PATCH] Convert `Ui/File/Upload` to TypeScript --- .../js/WoltLabSuite/Core/Ui/File/Upload.js | 262 ++++++++---------- .../files/js/WoltLabSuite/Core/Upload.js | 1 + .../files/js/WoltLabSuite/Core/Upload/Data.js | 4 + .../ts/WoltLabSuite/Core/Ui/File/Upload.js | 247 ----------------- .../ts/WoltLabSuite/Core/Ui/File/Upload.ts | 259 +++++++++++++++++ .../Core/Ui/User/CoverPhoto/Upload.ts | 2 +- .../files/ts/WoltLabSuite/Core/Upload.ts | 41 +-- .../files/ts/WoltLabSuite/Core/Upload/Data.ts | 23 ++ 8 files changed, 421 insertions(+), 418 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Upload/Data.js delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.ts create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Upload/Data.ts 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 9eb5cd2e76..97f1a129ee 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/File/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/File/Upload.js @@ -1,205 +1,189 @@ /** * Uploads file via AJAX. * - * @author Joshua Ruesweg, Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/File/Upload - * @since 5.2 + * @author Joshua Ruesweg, Matthias Schmidt + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/File/Upload + * @since 5.2 */ -define(['Core', 'Language', 'Dom/Util', 'WoltLabSuite/Core/Ui/File/Delete', 'Upload'], function (Core, Language, DomUtil, DeleteHandler, CoreUpload) { +define(["require", "exports", "tslib", "../../Core", "./Delete", "../../Dom/Util", "../../Language", "../../Upload"], function (require, exports, tslib_1, Core, Delete_1, Util_1, Language, Upload_1) { "use strict"; - /** - * @constructor - */ - function Upload(buttonContainerId, targetId, options) { - options = options || {}; - if (options.internalId === undefined) { - throw new Error("Missing internal id."); - } - // set default options - this._options = Core.extend({ - // name if the upload field - name: '__files[]', - // is true if every file from a multi-file selection is uploaded in its own request - singleFileRequests: false, - // url for uploading file - url: 'index.php?ajax-file-upload/&t=' + SECURITY_TOKEN, - // image preview - imagePreview: false, - // max files - maxFiles: null, - // array of acceptable file types, null if any file type is acceptable - acceptableFiles: null, - }, options); - this._options.multiple = this._options.maxFiles === null || this._options.maxFiles > 1; - if (this._options.url.indexOf('index.php') === 0) { - this._options.url = WSC_API_URL + this._options.url; - } - this._buttonContainer = elById(buttonContainerId); - if (this._buttonContainer === null) { - throw new Error("Element id '" + buttonContainerId + "' is unknown."); - } - this._target = elById(targetId); - if (targetId === null) { - throw new Error("Element id '" + targetId + "' is unknown."); - } - if (options.multiple && this._target.nodeName !== 'UL' && this._target.nodeName !== 'OL') { - throw new Error("Target element has to be list or table body if uploading multiple files is supported."); + Core = tslib_1.__importStar(Core); + Delete_1 = tslib_1.__importDefault(Delete_1); + Util_1 = tslib_1.__importDefault(Util_1); + Language = tslib_1.__importStar(Language); + Upload_1 = tslib_1.__importDefault(Upload_1); + class FileUpload extends Upload_1.default { + constructor(buttonContainerId, targetId, options) { + options = options || {}; + if (options.internalId === undefined) { + throw new Error("Missing internal id."); + } + // set default options + options = Core.extend({ + // image preview + imagePreview: false, + // max files + maxFiles: null, + }, options); + options.multiple = options.maxFiles === null || options.maxFiles > 1; + super(buttonContainerId, targetId, options); + this.checkMaxFiles(); + this._deleteHandler = new Delete_1.default(buttonContainerId, targetId, this._options.imagePreview, this); } - this._fileElements = []; - this._internalFileId = 0; - // upload ids that belong to an upload of multiple files at once - this._multiFileUploadIds = []; - this._createButton(); - this.checkMaxFiles(); - this._deleteHandler = new DeleteHandler(buttonContainerId, targetId, this._options.imagePreview, this); - } - Core.inherit(Upload, CoreUpload, { - _createFileElement: function (file) { - var element = Upload._super.prototype._createFileElement.call(this, file); - element.classList.add('box64', 'uploadedFile'); - var progress = elBySel('progress', element); - var icon = elCreate('span'); - icon.className = 'icon icon64 fa-spinner'; - var fileName = element.textContent; + _createFileElement(file) { + const element = super._createFileElement(file); + element.classList.add("box64", "uploadedFile"); + const progress = element.querySelector("progress"); + const icon = document.createElement("span"); + icon.className = "icon icon64 fa-spinner"; + const fileName = element.textContent; element.textContent = ""; element.append(icon); - var innerDiv = elCreate('div'); - var fileNameP = elCreate('p'); + const innerDiv = document.createElement("div"); + const fileNameP = document.createElement("p"); fileNameP.textContent = fileName; // file.name - var smallProgress = elCreate('small'); + const smallProgress = document.createElement("small"); smallProgress.appendChild(progress); innerDiv.appendChild(fileNameP); innerDiv.appendChild(smallProgress); - var div = elCreate('div'); + const div = document.createElement("div"); div.appendChild(innerDiv); - var ul = elCreate('ul'); - ul.className = 'buttonGroup'; + const ul = document.createElement("ul"); + ul.className = "buttonGroup"; div.appendChild(ul); // reset element textContent and replace with own element style element.append(div); return element; - }, - _failure: function (uploadId, data, responseText, xhr, requestOptions) { - for (var i = 0, length = this._fileElements[uploadId].length; i < length; i++) { - this._fileElements[uploadId][i].classList.add('uploadFailed'); - elBySel('small', this._fileElements[uploadId][i]).innerHTML = ''; - var icon = elBySel('.icon', this._fileElements[uploadId][i]); - icon.classList.remove('fa-spinner'); - icon.classList.add('fa-ban'); - var innerError = elCreate('span'); - innerError.className = 'innerError'; - innerError.textContent = Language.get('wcf.upload.error.uploadFailed'); - DomUtil.insertAfter(innerError, elBySel('small', this._fileElements[uploadId][i])); + } + _failure(uploadId, data) { + this._fileElements[uploadId].forEach((fileElement) => { + fileElement.classList.add("uploadFailed"); + const small = fileElement.querySelector("small"); + small.innerHTML = ""; + const icon = fileElement.querySelector(".icon"); + icon.classList.remove("fa-spinner"); + icon.classList.add("fa-ban"); + const innerError = document.createElement("span"); + innerError.className = "innerError"; + innerError.textContent = Language.get("wcf.upload.error.uploadFailed"); + small.insertAdjacentElement("afterend", innerError); + }); + throw new Error(`Upload failed: ${data.message}`); + } + _upload(event, file, blob) { + const parent = this._buttonContainer.parentElement; + const innerError = parent.querySelector("small.innerError:not(.innerFileError)"); + if (innerError) { + innerError.remove(); } - throw new Error("Upload failed: " + data.message); - }, - _upload: function (event, file, blob) { - var innerError = elBySel('small.innerError:not(.innerFileError)', this._buttonContainer.parentNode); - if (innerError) - elRemove(innerError); - return Upload._super.prototype._upload.call(this, event, file, blob); - }, - _success: function (uploadId, data, responseText, xhr, requestOptions) { - for (var i = 0, length = this._fileElements[uploadId].length; i < length; i++) { - if (data['files'][i] !== undefined) { + return super._upload(event, file, blob); + } + _success(uploadId, data) { + this._fileElements[uploadId].forEach((fileElement, index) => { + if (data.files[index] !== undefined) { + const fileData = data.files[index]; if (this._options.imagePreview) { - if (data['files'][i].image === null) { + if (fileData.image === null) { throw new Error("Expect image for uploaded file. None given."); } - elRemove(this._fileElements[uploadId][i]); - if (elBySel('img.previewImage', this._target) !== null) { - elBySel('img.previewImage', this._target).setAttribute('src', data['files'][i].image); + fileElement.remove(); + const previewImage = this._target.querySelector("img.previewImage"); + if (previewImage !== null) { + previewImage.src = fileData.image; } else { - var image = elCreate('img'); - image.classList.add('previewImage'); - image.setAttribute('src', data['files'][i].image); - image.setAttribute('style', "max-width: 100%;"); - elData(image, 'unique-file-id', data['files'][i].uniqueFileId); + const image = document.createElement("img"); + image.classList.add("previewImage"); + image.src = fileData.image; + image.style.setProperty("max-width", "100%", ""); + image.dataset.uniqueFileId = fileData.uniqueFileId; this._target.appendChild(image); } } else { - elData(this._fileElements[uploadId][i], 'unique-file-id', data['files'][i].uniqueFileId); - elBySel('small', this._fileElements[uploadId][i]).textContent = data['files'][i].filesize; - var icon = elBySel('.icon', this._fileElements[uploadId][i]); - icon.classList.remove('fa-spinner'); - icon.classList.add('fa-' + data['files'][i].icon); + fileElement.dataset.uniqueFileId = fileData.uniqueFileId; + fileElement.querySelector("small").textContent = fileData.filesize.toString(); + const icon = fileElement.querySelector(".icon"); + icon.classList.remove("fa-spinner"); + icon.classList.add(`fa-${fileData.icon}`); } } - else if (data['error'][i] !== undefined) { - this._fileElements[uploadId][i].classList.add('uploadFailed'); - elBySel('small', this._fileElements[uploadId][i]).innerHTML = ''; - 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'); - innerError.className = 'innerError'; - innerError.textContent = data['error'][i].errorMessage; - DomUtil.insertAfter(innerError, elBySel('small', this._fileElements[uploadId][i])); + else if (data.error[index] !== undefined) { + const errorData = data["error"][index]; + fileElement.classList.add("uploadFailed"); + const small = fileElement.querySelector("small"); + small.innerHTML = ""; + const icon = fileElement.querySelector(".icon"); + icon.classList.remove("fa-spinner"); + icon.classList.add("fa-ban"); + let innerError = fileElement.querySelector(".innerError"); + if (innerError === null) { + innerError = document.createElement("span"); + innerError.className = "innerError"; + innerError.textContent = errorData.errorMessage; + small.insertAdjacentElement("afterend", innerError); } else { - elBySel('.innerError', this._fileElements[uploadId][i]).textContent = data['error'][i].errorMessage; + innerError.textContent = errorData.errorMessage; } } else { - throw new Error('Unknown uploaded file for uploadId ' + uploadId + '.'); + throw new Error(`Unknown uploaded file for uploadId ${uploadId}.`); } - } + }); // create delete buttons this._deleteHandler.rebuild(); this.checkMaxFiles(); - Core.triggerEvent(this._target, 'change'); - }, - _getFormData: function () { + Core.triggerEvent(this._target, "change"); + } + _getFormData() { return { - internalId: this._options.internalId + internalId: this._options.internalId, }; - }, - validateUpload: function (files) { + } + validateUpload(files) { if (this._options.maxFiles === null || files.length + this.countFiles() <= this._options.maxFiles) { return true; } else { - var innerError = elBySel('small.innerError:not(.innerFileError)', this._buttonContainer.parentNode); + const parent = this._buttonContainer.parentElement; + let innerError = parent.querySelector("small.innerError:not(.innerFileError)"); if (innerError === null) { - innerError = elCreate('small'); - innerError.className = 'innerError'; - DomUtil.insertAfter(innerError, this._buttonContainer); + innerError = document.createElement("small"); + innerError.className = "innerError"; + this._buttonContainer.insertAdjacentElement("afterend", innerError); } - innerError.textContent = Language.get('wcf.upload.error.reachedRemainingLimit', { - maxFiles: this._options.maxFiles - this.countFiles() + innerError.textContent = Language.get("wcf.upload.error.reachedRemainingLimit", { + maxFiles: this._options.maxFiles - this.countFiles(), }); return false; } - }, + } /** * Returns the count of the uploaded images. - * - * @return {int} */ - countFiles: function () { + countFiles() { if (this._options.imagePreview) { - return elBySel('img', this._target) !== null ? 1 : 0; + return this._target.querySelector("img") !== null ? 1 : 0; } else { return this._target.childElementCount; } - }, + } /** * Checks the maximum number of files and enables or disables the upload button. */ - checkMaxFiles: function () { + checkMaxFiles() { if (this._options.maxFiles !== null && this.countFiles() >= this._options.maxFiles) { - elHide(this._button); + Util_1.default.hide(this._button); } else { - elShow(this._button); + Util_1.default.show(this._button); } } - }); - return Upload; + } + Core.enableLegacyInheritance(FileUpload); + return FileUpload; }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js index 69140314e9..e4d9e134d9 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js @@ -192,6 +192,7 @@ define(["require", "exports", "tslib", "./Ajax/Request", "./Core", "./Dom/Change _success(_uploadId, _data, _responseText, _xhr, _requestOptions) { // This should be an abstract method, but cannot be marked as such for backwards compatibility. } + // This duplication is on purpose, the signature below is implementation private. _upload(event, file, blob) { // remove failed upload elements first this._target.querySelectorAll(".uploadFailed").forEach((el) => el.remove()); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Upload/Data.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Upload/Data.js new file mode 100644 index 0000000000..2ae92b6a8b --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Upload/Data.js @@ -0,0 +1,4 @@ +define(["require", "exports"], function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); +}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.js deleted file mode 100644 index 59b48a95dd..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.js +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Uploads file via AJAX. - * - * @author Joshua Ruesweg, Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/File/Upload - * @since 5.2 - */ -define(['Core', 'Language', 'Dom/Util', 'WoltLabSuite/Core/Ui/File/Delete', 'Upload'], function(Core, Language, DomUtil, DeleteHandler, CoreUpload) { - "use strict"; - - /** - * @constructor - */ - function Upload(buttonContainerId, targetId, options) { - options = options || {}; - - if (options.internalId === undefined) { - throw new Error("Missing internal id."); - } - - // set default options - this._options = Core.extend({ - // name if the upload field - name: '__files[]', - // is true if every file from a multi-file selection is uploaded in its own request - singleFileRequests: false, - // url for uploading file - url: 'index.php?ajax-file-upload/&t=' + SECURITY_TOKEN, - // image preview - imagePreview: false, - // max files - maxFiles: null, - // array of acceptable file types, null if any file type is acceptable - acceptableFiles: null, - }, options); - - this._options.multiple = this._options.maxFiles === null || this._options.maxFiles > 1; - - if (this._options.url.indexOf('index.php') === 0) { - this._options.url = WSC_API_URL + this._options.url; - } - - this._buttonContainer = elById(buttonContainerId); - if (this._buttonContainer === null) { - throw new Error("Element id '" + buttonContainerId + "' is unknown."); - } - - this._target = elById(targetId); - if (targetId === null) { - throw new Error("Element id '" + targetId + "' is unknown."); - } - - if (options.multiple && this._target.nodeName !== 'UL' && this._target.nodeName !== 'OL') { - throw new Error("Target element has to be list or table body if uploading multiple files is supported."); - } - - this._fileElements = []; - this._internalFileId = 0; - - // upload ids that belong to an upload of multiple files at once - this._multiFileUploadIds = []; - - this._createButton(); - this.checkMaxFiles(); - - this._deleteHandler = new DeleteHandler(buttonContainerId, targetId, this._options.imagePreview, this); - } - - Core.inherit(Upload, CoreUpload, { - _createFileElement: function(file) { - var element = Upload._super.prototype._createFileElement.call(this, file); - element.classList.add('box64', 'uploadedFile'); - - var progress = elBySel('progress', element); - - var icon = elCreate('span'); - icon.className = 'icon icon64 fa-spinner'; - - var fileName = element.textContent; - element.textContent = ""; - element.append(icon); - - var innerDiv = elCreate('div'); - var fileNameP = elCreate('p'); - fileNameP.textContent = fileName; // file.name - - var smallProgress = elCreate('small'); - smallProgress.appendChild(progress); - - innerDiv.appendChild(fileNameP); - innerDiv.appendChild(smallProgress); - - var div = elCreate('div'); - div.appendChild(innerDiv); - - var ul = elCreate('ul'); - ul.className = 'buttonGroup'; - div.appendChild(ul); - - // reset element textContent and replace with own element style - element.append(div); - - return element; - }, - - _failure: function(uploadId, data, responseText, xhr, requestOptions) { - for (var i = 0, length = this._fileElements[uploadId].length; i < length; i++) { - this._fileElements[uploadId][i].classList.add('uploadFailed'); - - elBySel('small', this._fileElements[uploadId][i]).innerHTML = ''; - var icon = elBySel('.icon', this._fileElements[uploadId][i]); - icon.classList.remove('fa-spinner'); - icon.classList.add('fa-ban'); - - var innerError = elCreate('span'); - innerError.className = 'innerError'; - innerError.textContent = Language.get('wcf.upload.error.uploadFailed'); - DomUtil.insertAfter(innerError, elBySel('small', this._fileElements[uploadId][i])); - } - - throw new Error("Upload failed: " + data.message); - }, - - _upload: function(event, file, blob) { - var innerError = elBySel('small.innerError:not(.innerFileError)', this._buttonContainer.parentNode); - if (innerError) elRemove(innerError); - - return Upload._super.prototype._upload.call(this, event, file, blob); - }, - - _success: function(uploadId, data, responseText, xhr, requestOptions) { - for (var i = 0, length = this._fileElements[uploadId].length; i < length; i++) { - if (data['files'][i] !== undefined) { - if (this._options.imagePreview) { - if (data['files'][i].image === null) { - throw new Error("Expect image for uploaded file. None given."); - } - - elRemove(this._fileElements[uploadId][i]); - - if (elBySel('img.previewImage', this._target) !== null) { - elBySel('img.previewImage', this._target).setAttribute('src', data['files'][i].image); - } - else { - var image = elCreate('img'); - image.classList.add('previewImage'); - image.setAttribute('src', data['files'][i].image); - image.setAttribute('style', "max-width: 100%;"); - elData(image, 'unique-file-id', data['files'][i].uniqueFileId); - this._target.appendChild(image); - } - } - else { - elData(this._fileElements[uploadId][i], 'unique-file-id', data['files'][i].uniqueFileId); - elBySel('small', this._fileElements[uploadId][i]).textContent = data['files'][i].filesize; - 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 = ''; - 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'); - innerError.className = 'innerError'; - innerError.textContent = data['error'][i].errorMessage; - DomUtil.insertAfter(innerError, elBySel('small', this._fileElements[uploadId][i])); - } - else { - elBySel('.innerError', this._fileElements[uploadId][i]).textContent = data['error'][i].errorMessage; - } - } - else { - throw new Error('Unknown uploaded file for uploadId ' + uploadId + '.'); - } - } - - // create delete buttons - this._deleteHandler.rebuild(); - this.checkMaxFiles(); - Core.triggerEvent(this._target, 'change'); - }, - - _getFormData: function() { - return { - internalId: this._options.internalId - }; - }, - - validateUpload: function(files) { - if (this._options.maxFiles === null || files.length + this.countFiles() <= this._options.maxFiles) { - return true; - } - else { - var innerError = elBySel('small.innerError:not(.innerFileError)', this._buttonContainer.parentNode); - - if (innerError === null) { - innerError = elCreate('small'); - innerError.className = 'innerError'; - DomUtil.insertAfter(innerError, this._buttonContainer); - } - - innerError.textContent = Language.get('wcf.upload.error.reachedRemainingLimit', { - maxFiles: this._options.maxFiles - this.countFiles() - }); - - return false; - } - }, - - /** - * Returns the count of the uploaded images. - * - * @return {int} - */ - countFiles: function() { - if (this._options.imagePreview) { - return elBySel('img', this._target) !== null ? 1 : 0; - } - else { - return this._target.childElementCount; - } - }, - - /** - * Checks the maximum number of files and enables or disables the upload button. - */ - checkMaxFiles: function() { - if (this._options.maxFiles !== null && this.countFiles() >= this._options.maxFiles) { - elHide(this._button); - } - else { - elShow(this._button); - } - } - }); - - return Upload; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.ts new file mode 100644 index 0000000000..dd814439fb --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.ts @@ -0,0 +1,259 @@ +/** + * Uploads file via AJAX. + * + * @author Joshua Ruesweg, Matthias Schmidt + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/File/Upload + * @since 5.2 + */ + +import { ResponseData } from "../../Ajax/Data"; +import * as Core from "../../Core"; +import { FileCollection, FileLikeObject, UploadId, UploadOptions } from "../../Upload/Data"; +import { default as DeleteHandler } from "./Delete"; +import DomUtil from "../../Dom/Util"; +import * as Language from "../../Language"; +import Upload from "../../Upload"; + +interface FileUploadOptions extends UploadOptions { + // image preview + imagePreview: boolean; + // max files + maxFiles: number | null; + + internalId: string; +} + +interface FileData { + filesize: number; + icon: string; + image: string | null; + uniqueFileId: string; +} + +interface ErrorData { + errorMessage: string; +} + +interface AjaxResponse { + error: ErrorData[]; + files: FileData[]; +} + +class FileUpload extends Upload { + protected readonly _deleteHandler: DeleteHandler; + + constructor(buttonContainerId: string, targetId: string, options: Partial) { + options = options || {}; + + if (options.internalId === undefined) { + throw new Error("Missing internal id."); + } + + // set default options + options = Core.extend( + { + // image preview + imagePreview: false, + // max files + maxFiles: null, + }, + options, + ); + + options.multiple = options.maxFiles === null || (options.maxFiles as number) > 1; + + super(buttonContainerId, targetId, options); + + this.checkMaxFiles(); + + this._deleteHandler = new DeleteHandler(buttonContainerId, targetId, this._options.imagePreview, this); + } + + protected _createFileElement(file: File | FileLikeObject): HTMLElement { + const element = super._createFileElement(file); + element.classList.add("box64", "uploadedFile"); + + const progress = element.querySelector("progress") as HTMLProgressElement; + + const icon = document.createElement("span"); + icon.className = "icon icon64 fa-spinner"; + + const fileName = element.textContent; + element.textContent = ""; + element.append(icon); + + const innerDiv = document.createElement("div"); + const fileNameP = document.createElement("p"); + fileNameP.textContent = fileName; // file.name + + const smallProgress = document.createElement("small"); + smallProgress.appendChild(progress); + + innerDiv.appendChild(fileNameP); + innerDiv.appendChild(smallProgress); + + const div = document.createElement("div"); + div.appendChild(innerDiv); + + const ul = document.createElement("ul"); + ul.className = "buttonGroup"; + div.appendChild(ul); + + // reset element textContent and replace with own element style + element.append(div); + + return element; + } + + protected _failure(uploadId: number, data: ResponseData): boolean { + this._fileElements[uploadId].forEach((fileElement) => { + fileElement.classList.add("uploadFailed"); + + const small = fileElement.querySelector("small") as HTMLElement; + small.innerHTML = ""; + + const icon = fileElement.querySelector(".icon") as HTMLElement; + icon.classList.remove("fa-spinner"); + icon.classList.add("fa-ban"); + + const innerError = document.createElement("span"); + innerError.className = "innerError"; + innerError.textContent = Language.get("wcf.upload.error.uploadFailed"); + small.insertAdjacentElement("afterend", innerError); + }); + + throw new Error(`Upload failed: ${data.message as string}`); + } + + protected _upload(event: Event): UploadId; + protected _upload(event: null, file: File): UploadId; + protected _upload(event: null, file: null, blob: Blob): UploadId; + protected _upload(event: Event | null, file?: File | null, blob?: Blob | null): UploadId { + const parent = this._buttonContainer.parentElement!; + const innerError = parent.querySelector("small.innerError:not(.innerFileError)"); + if (innerError) { + innerError.remove(); + } + + return super._upload(event, file, blob); + } + + protected _success(uploadId: number, data: AjaxResponse): void { + this._fileElements[uploadId].forEach((fileElement, index) => { + if (data.files[index] !== undefined) { + const fileData = data.files[index]; + + if (this._options.imagePreview) { + if (fileData.image === null) { + throw new Error("Expect image for uploaded file. None given."); + } + + fileElement.remove(); + + const previewImage = this._target.querySelector("img.previewImage") as HTMLImageElement; + if (previewImage !== null) { + previewImage.src = fileData.image; + } else { + const image = document.createElement("img"); + image.classList.add("previewImage"); + image.src = fileData.image; + image.style.setProperty("max-width", "100%", ""); + image.dataset.uniqueFileId = fileData.uniqueFileId; + this._target.appendChild(image); + } + } else { + fileElement.dataset.uniqueFileId = fileData.uniqueFileId; + fileElement.querySelector("small")!.textContent = fileData.filesize.toString(); + + const icon = fileElement.querySelector(".icon") as HTMLElement; + icon.classList.remove("fa-spinner"); + icon.classList.add(`fa-${fileData.icon}`); + } + } else if (data.error[index] !== undefined) { + const errorData = data["error"][index]; + + fileElement.classList.add("uploadFailed"); + + const small = fileElement.querySelector("small") as HTMLElement; + small.innerHTML = ""; + + const icon = fileElement.querySelector(".icon") as HTMLElement; + icon.classList.remove("fa-spinner"); + icon.classList.add("fa-ban"); + + let innerError = fileElement.querySelector(".innerError") as HTMLElement; + if (innerError === null) { + innerError = document.createElement("span"); + innerError.className = "innerError"; + innerError.textContent = errorData.errorMessage; + + small.insertAdjacentElement("afterend", innerError); + } else { + innerError.textContent = errorData.errorMessage; + } + } else { + throw new Error(`Unknown uploaded file for uploadId ${uploadId}.`); + } + }); + + // create delete buttons + this._deleteHandler.rebuild(); + this.checkMaxFiles(); + Core.triggerEvent(this._target, "change"); + } + + protected _getFormData(): ArbitraryObject { + return { + internalId: this._options.internalId, + }; + } + + validateUpload(files: FileCollection): boolean { + if (this._options.maxFiles === null || files.length + this.countFiles() <= this._options.maxFiles) { + return true; + } else { + const parent = this._buttonContainer.parentElement!; + + let innerError = parent.querySelector("small.innerError:not(.innerFileError)"); + if (innerError === null) { + innerError = document.createElement("small"); + innerError.className = "innerError"; + this._buttonContainer.insertAdjacentElement("afterend", innerError); + } + + innerError.textContent = Language.get("wcf.upload.error.reachedRemainingLimit", { + maxFiles: this._options.maxFiles - this.countFiles(), + }); + + return false; + } + } + + /** + * Returns the count of the uploaded images. + */ + countFiles(): number { + if (this._options.imagePreview) { + return this._target.querySelector("img") !== null ? 1 : 0; + } else { + return this._target.childElementCount; + } + } + + /** + * Checks the maximum number of files and enables or disables the upload button. + */ + checkMaxFiles(): void { + if (this._options.maxFiles !== null && this.countFiles() >= this._options.maxFiles) { + DomUtil.hide(this._button); + } else { + DomUtil.show(this._button); + } + } +} + +Core.enableLegacyInheritance(FileUpload); + +export = FileUpload; diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/CoverPhoto/Upload.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/CoverPhoto/Upload.ts index fec98afd92..af4319efb5 100644 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/CoverPhoto/Upload.ts +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/CoverPhoto/Upload.ts @@ -37,7 +37,7 @@ class UiUserCoverPhotoUpload extends Upload { this.userId = userId; } - protected _getParameters(): object { + protected _getParameters(): ArbitraryObject { return { userID: this.userId, }; diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts index 26fedee0a9..a0ae2c7f68 100644 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts @@ -13,42 +13,19 @@ import AjaxRequest from "./Ajax/Request"; import * as Core from "./Core"; import DomChangeListener from "./Dom/Change/Listener"; import * as Language from "./Language"; +import { FileCollection, FileElements, FileLikeObject, UploadId, UploadOptions } from "./Upload/Data"; -interface UploadOptions { - // name of the PHP action - action: string; - className: string; - // is true if multiple files can be uploaded at once - multiple: boolean; - // array of acceptable file types, null if any file type is acceptable - acceptableFiles: string[] | null; - // name of the upload field - name: string; - // is true if every file from a multi-file selection is uploaded in its own request - singleFileRequests: boolean; - // url for uploading file - url: string; -} - -type FileElements = HTMLElement[]; - -type FileLikeObject = { name: string }; - -type FileCollection = File[] | FileLikeObject[] | FileList; - -type UploadId = number | number[] | null; - -abstract class Upload { +abstract class Upload { protected readonly _button = document.createElement("p"); protected readonly _buttonContainer: HTMLElement; protected readonly _fileElements: FileElements[] = []; protected readonly _fileUpload = document.createElement("input"); protected _internalFileId = 0; protected readonly _multiFileUploadIds: unknown[] = []; - protected readonly _options: UploadOptions; + protected readonly _options: TOptions; protected readonly _target: HTMLElement; - protected constructor(buttonContainerId: string, targetId: string, options: Partial) { + protected constructor(buttonContainerId: string, targetId: string, options: Partial) { options = options || {}; if (!options.className) { throw new Error("Missing class name."); @@ -71,7 +48,7 @@ abstract class Upload { url: `index.php?ajax-upload/&t=${window.SECURITY_TOKEN}`, }, options, - ) as UploadOptions; + ) as TOptions; this._options.url = Core.convertLegacyUrl(this._options.url); if (this._options.url.indexOf("index.php") === 0) { @@ -217,7 +194,7 @@ abstract class Upload { /** * Return additional parameters for upload requests. */ - protected _getParameters(): object { + protected _getParameters(): ArbitraryObject { return {}; } @@ -226,7 +203,7 @@ abstract class Upload { * * @since 5.2 */ - protected _getFormData(): object { + protected _getFormData(): ArbitraryObject { return {}; } @@ -277,6 +254,8 @@ abstract class Upload { protected _upload(event: Event): UploadId; protected _upload(event: null, file: File): UploadId; protected _upload(event: null, file: null, blob: Blob): UploadId; + protected _upload(event: Event | null, file?: File | null, blob?: Blob | null): UploadId; + // This duplication is on purpose, the signature below is implementation private. protected _upload(event: Event | null, file?: File | null, blob?: Blob | null): UploadId { // remove failed upload elements first this._target.querySelectorAll(".uploadFailed").forEach((el) => el.remove()); @@ -333,7 +312,7 @@ abstract class Upload { * * @since 5.2 */ - protected validateUpload(_files: FileCollection): boolean { + validateUpload(_files: FileCollection): boolean { // This should be an abstract method, but cannot be marked as such for backwards compatibility. return true; diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload/Data.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload/Data.ts new file mode 100644 index 0000000000..7a641a64fb --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload/Data.ts @@ -0,0 +1,23 @@ +export interface UploadOptions { + // name of the PHP action + action: string; + className: string; + // is true if multiple files can be uploaded at once + multiple: boolean; + // array of acceptable file types, null if any file type is acceptable + acceptableFiles: string[] | null; + // name of the upload field + name: string; + // is true if every file from a multi-file selection is uploaded in its own request + singleFileRequests: boolean; + // url for uploading file + url: string; +} + +export type FileElements = HTMLElement[]; + +export type FileLikeObject = { name: string }; + +export type FileCollection = File[] | FileLikeObject[] | FileList; + +export type UploadId = number | number[] | null; -- 2.20.1