Convert `Ui/File/Upload` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Sat, 7 Nov 2020 18:32:48 +0000 (19:32 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 7 Nov 2020 18:32:48 +0000 (19:32 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/File/Upload.js
wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js
wcfsetup/install/files/js/WoltLabSuite/Core/Upload/Data.js [new file with mode: 0644]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/File/Upload.ts [new file with mode: 0644]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/CoverPhoto/Upload.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Upload/Data.ts [new file with mode: 0644]

index 9eb5cd2e76e41e3bf8f7b9d5abfce660c322098a..97f1a129ee77c1458770a1bbd901baae67d231cd 100644 (file)
 /**
  * Uploads file via AJAX.
  *
- * @author     Joshua Ruesweg, Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
 });
index 69140314e9456c5496573a2246412862aca097c6..e4d9e134d976799dc2a7b53e1837a351760a5787 100644 (file)
@@ -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 (file)
index 0000000..2ae92b6
--- /dev/null
@@ -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 (file)
index 59b48a9..0000000
+++ /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 <http://opensource.org/licenses/lgpl-license.php>
- * @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 (file)
index 0000000..dd81443
--- /dev/null
@@ -0,0 +1,259 @@
+/**
+ * Uploads file via AJAX.
+ *
+ * @author  Joshua Ruesweg, Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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<FileUploadOptions> {
+  protected readonly _deleteHandler: DeleteHandler;
+
+  constructor(buttonContainerId: string, targetId: string, options: Partial<FileUploadOptions>) {
+    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;
index fec98afd92c3556ffc6270ebbc0022fe922420f6..af4319efb54d588b72767d26c280e23e80b09e4a 100644 (file)
@@ -37,7 +37,7 @@ class UiUserCoverPhotoUpload extends Upload {
     this.userId = userId;
   }
 
-  protected _getParameters(): object {
+  protected _getParameters(): ArbitraryObject {
     return {
       userID: this.userId,
     };
index 26fedee0a9bee29fbf97951bba2adb9f79297fb9..a0ae2c7f68d30c6f8f3d93d7a893e21329e7b14c 100644 (file)
@@ -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<TOptions extends UploadOptions = UploadOptions> {
   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<UploadOptions>) {
+  protected constructor(buttonContainerId: string, targetId: string, options: Partial<TOptions>) {
     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 (file)
index 0000000..7a641a6
--- /dev/null
@@ -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;