Convert `Upload` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Fri, 30 Oct 2020 17:09:54 +0000 (18:09 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 30 Oct 2020 17:09:54 +0000 (18:09 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Data.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts [new file with mode: 0644]

index efddceccfec7992888e4167cf5ba7acaf8132173..8edf1c4a675cfd210e4bbf393af85d89ff3ab197 100644 (file)
 /**
  * Uploads file via AJAX.
  *
- * @author     Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     Upload (alias)
- * @module     WoltLabSuite/Core/Upload
+ * @author  Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  Upload (alias)
+ * @module  WoltLabSuite/Core/Upload
  */
-define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse'], function (AjaxRequest, Core, DomChangeListener, Language, DomUtil, DomTraverse) {
+define(["require", "exports", "tslib", "./Ajax/Request", "./Core", "./Dom/Change/Listener", "./Language"], function (require, exports, tslib_1, Request_1, Core, Listener_1, Language) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            _createButton: function () { },
-            _createFileElement: function () { },
-            _createFileElements: function () { },
-            _failure: function () { },
-            _getParameters: function () { },
-            _insertButton: function () { },
-            _progress: function () { },
-            _removeButton: function () { },
-            _success: function () { },
-            _upload: function () { },
-            _uploadFiles: function () { }
-        };
-        return Fake;
-    }
-    /**
-     * @constructor
-     */
-    function Upload(buttonContainerId, targetId, options) {
-        options = options || {};
-        if (options.className === undefined) {
-            throw new Error("Missing class name.");
-        }
-        // set default options
-        this._options = Core.extend({
-            // name of the PHP action
-            action: 'upload',
-            // is true if multiple files can be uploaded at once
-            multiple: false,
-            // array of acceptable file types, null if any file type is acceptable
-            acceptableFiles: null,
-            // 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-upload/&t=' + SECURITY_TOKEN
-        }, options);
-        this._options.url = Core.convertLegacyUrl(this._options.url);
-        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' && this._target.nodeName !== 'TBODY') {
-            throw new Error("Target element has to be list or table body if uploading multiple files is supported.");
+    Request_1 = tslib_1.__importDefault(Request_1);
+    Core = tslib_1.__importStar(Core);
+    Listener_1 = tslib_1.__importDefault(Listener_1);
+    Language = tslib_1.__importStar(Language);
+    class Upload {
+        constructor(buttonContainerId, targetId, options) {
+            this._button = document.createElement("p");
+            this._fileElements = [];
+            this._fileUpload = document.createElement("input");
+            this._internalFileId = 0;
+            this._multiFileUploadIds = [];
+            options = options || {};
+            if (!options.className) {
+                throw new Error("Missing class name.");
+            }
+            // set default options
+            this._options = Core.extend({
+                // name of the PHP action
+                action: "upload",
+                // is true if multiple files can be uploaded at once
+                multiple: false,
+                // array of acceptable file types, null if any file type is acceptable
+                acceptableFiles: null,
+                // name of 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-upload/&t=${window.SECURITY_TOKEN}`,
+            }, options);
+            this._options.url = Core.convertLegacyUrl(this._options.url);
+            if (this._options.url.indexOf("index.php") === 0) {
+                this._options.url = window.WSC_API_URL + this._options.url;
+            }
+            const buttonContainer = document.getElementById(buttonContainerId);
+            if (buttonContainer === null) {
+                throw new Error(`Element id '${buttonContainerId}' is unknown.`);
+            }
+            this._buttonContainer = buttonContainer;
+            const target = document.getElementById(targetId);
+            if (target === null) {
+                throw new Error(`Element id '${targetId}' is unknown.`);
+            }
+            this._target = target;
+            if (options.multiple &&
+                this._target.nodeName !== "UL" &&
+                this._target.nodeName !== "OL" &&
+                this._target.nodeName !== "TBODY") {
+                throw new Error("Target element has to be list or table body if uploading multiple files is supported.");
+            }
+            this._createButton();
         }
-        this._fileElements = [];
-        this._internalFileId = 0;
-        // upload ids that belong to an upload of multiple files at once
-        this._multiFileUploadIds = [];
-        this._createButton();
-    }
-    Upload.prototype = {
         /**
          * Creates the upload button.
          */
-        _createButton: function () {
-            this._fileUpload = elCreate('input');
-            elAttr(this._fileUpload, 'type', 'file');
-            elAttr(this._fileUpload, 'name', this._options.name);
+        _createButton() {
+            this._fileUpload.type = "file";
+            this._fileUpload.name = this._options.name;
             if (this._options.multiple) {
-                elAttr(this._fileUpload, 'multiple', 'true');
+                this._fileUpload.multiple = true;
             }
             if (this._options.acceptableFiles !== null) {
-                elAttr(this._fileUpload, 'accept', this._options.acceptableFiles.join(','));
+                this._fileUpload.accept = this._options.acceptableFiles.join(",");
             }
-            this._fileUpload.addEventListener('change', this._upload.bind(this));
-            this._button = elCreate('p');
-            this._button.className = 'button uploadButton';
-            elAttr(this._button, 'role', 'button');
-            this._fileUpload.addEventListener('focus', (function () {
-                if (this._fileUpload.classList.contains('focus-visible')) {
-                    this._button.classList.add('active');
+            this._fileUpload.addEventListener("change", (ev) => this._upload(ev));
+            this._button.className = "button uploadButton";
+            this._button.setAttribute("role", "button");
+            this._fileUpload.addEventListener("focus", () => {
+                if (this._fileUpload.classList.contains("focus-visible")) {
+                    this._button.classList.add("active");
                 }
-            }).bind(this));
-            this._fileUpload.addEventListener('blur', (function () { this._button.classList.remove('active'); }).bind(this));
-            var span = elCreate('span');
-            span.textContent = Language.get('wcf.global.button.upload');
+            });
+            this._fileUpload.addEventListener("blur", () => {
+                this._button.classList.remove("active");
+            });
+            const span = document.createElement("span");
+            span.textContent = Language.get("wcf.global.button.upload");
             this._button.appendChild(span);
-            DomUtil.prepend(this._fileUpload, this._button);
+            this._buttonContainer.insertAdjacentElement("afterbegin", this._fileUpload);
             this._insertButton();
-            DomChangeListener.trigger();
-        },
+            Listener_1.default.trigger();
+        }
         /**
          * Creates the document element for an uploaded file.
-         *
-         * @param      {File}          file            uploaded file
-         * @return     {HTMLElement}
          */
-        _createFileElement: function (file) {
-            var progress = elCreate('progress');
-            elAttr(progress, 'max', 100);
-            if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
-                var li = elCreate('li');
-                li.innerText = file.name;
-                li.appendChild(progress);
-                this._target.appendChild(li);
-                return li;
+        _createFileElement(file) {
+            const progress = document.createElement("progress");
+            progress.max = 100;
+            let element;
+            switch (this._target.nodeName) {
+                case "OL":
+                case "UL":
+                    element = document.createElement("li");
+                    element.innerText = file.name;
+                    element.appendChild(progress);
+                    this._target.appendChild(element);
+                    return element;
+                case "TBODY":
+                    return this._createFileTableRow(file);
+                default:
+                    element = document.createElement("p");
+                    element.appendChild(progress);
+                    this._target.appendChild(element);
+                    return element;
             }
-            else if (this._target.nodeName === 'TBODY') {
-                return this._createFileTableRow(file);
-            }
-            else {
-                var p = elCreate('p');
-                p.appendChild(progress);
-                this._target.appendChild(p);
-                return p;
-            }
-        },
+        }
         /**
          * Creates the document elements for uploaded files.
-         *
-         * @param      {(FileList|Array.<File>)}       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;
+        _createFileElements(files) {
+            if (!files.length) {
+                return null;
             }
-            return null;
-        },
-        _createFileTableRow: function (file) {
+            const elements = [];
+            Array.from(files).forEach((file) => {
+                const fileElement = this._createFileElement(file);
+                if (!fileElement.classList.contains("uploadFailed")) {
+                    fileElement.dataset.filename = file.name;
+                    fileElement.dataset.internalFileId = (this._internalFileId++).toString();
+                    elements.push(fileElement);
+                }
+            });
+            const uploadId = this._fileElements.length;
+            this._fileElements.push(elements);
+            Listener_1.default.trigger();
+            return uploadId;
+        }
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        _createFileTableRow(file) {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
             throw new Error("Has to be implemented in subclass.");
-        },
+        }
         /**
          * Handles a failed file upload.
-         *
-         * @param      {int}                   uploadId        identifier of a file upload
-         * @param      {object<string, *>}     data            response data
-         * @param      {string}                responseText    response
-         * @param      {XMLHttpRequest}        xhr             request object
-         * @param      {object<string, *>}     requestOptions  options used to send AJAX request
-         * @return     {boolean}       true if the error message should be shown
          */
-        _failure: function (uploadId, data, responseText, xhr, requestOptions) {
-            // does nothing
+        _failure(
+        /* eslint-disable @typescript-eslint/no-unused-vars */
+        uploadId, data, responseText, xhr, requestOptions
+        /* eslint-enable @typescript-eslint/no-unused-vars */
+        ) {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
             return true;
-        },
+        }
         /**
          * Return additional parameters for upload requests.
-         *
-         * @return     {object<string, *>}     additional parameters
          */
-        _getParameters: function () {
+        _getParameters() {
             return {};
-        },
+        }
         /**
          * Return additional form data for upload requests.
          *
-         * @return     {object<string, *>}     additional form data
          * @since       5.2
          */
-        _getFormData: function () {
+        _getFormData() {
             return {};
-        },
+        }
         /**
          * Inserts the created button to upload files into the button container.
          */
-        _insertButton: function () {
-            DomUtil.prepend(this._button, this._buttonContainer);
-        },
+        _insertButton() {
+            this._buttonContainer.insertAdjacentElement("afterbegin", this._button);
+        }
         /**
          * 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);
+        _progress(uploadId, event) {
+            const percentComplete = Math.round((event.loaded / event.total) * 100);
+            this._fileElements[uploadId].forEach((element) => {
+                const progress = element.querySelector("progress");
+                if (progress) {
+                    progress.value = percentComplete;
                 }
-            }
-        },
+            });
+        }
         /**
          * Removes the button to upload files.
          */
-        _removeButton: function () {
-            elRemove(this._button);
-            DomChangeListener.trigger();
-        },
+        _removeButton() {
+            this._button.remove();
+            Listener_1.default.trigger();
+        }
         /**
          * Handles a successful file upload.
-         *
-         * @param      {int}                   uploadId        identifier of a file upload
-         * @param      {object<string, *>}     data            response data
-         * @param      {string}                responseText    response
-         * @param      {XMLHttpRequest}        xhr             request object
-         * @param      {object<string, *>}     requestOptions  options used to send AJAX request
-         */
-        _success: function (uploadId, data, responseText, xhr, requestOptions) {
-            // does nothing
-        },
-        /**
-         * 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.<int>|null)}        identifier(s) for the uploaded files
          */
-        _upload: function (event, file, blob) {
+        _success(
+        /* eslint-disable @typescript-eslint/no-unused-vars */
+        uploadId, data, responseText, xhr, requestOptions
+        /* eslint-enable @typescript-eslint/no-unused-vars */
+        ) {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+        }
+        _upload(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 = [];
+            this._target.querySelectorAll(".uploadFailed").forEach((el) => el.remove());
+            let uploadId = null;
+            let files = [];
             if (file) {
                 files.push(file);
             }
             else if (blob) {
-                var fileExtension = '';
+                let fileExtension = "";
                 switch (blob.type) {
-                    case 'image/jpeg':
-                        fileExtension = '.jpg';
+                    case "image/jpeg":
+                        fileExtension = "jpg";
                         break;
-                    case 'image/gif':
-                        fileExtension = '.gif';
+                    case "image/gif":
+                        fileExtension = "gif";
                         break;
-                    case 'image/png':
-                        fileExtension = '.png';
+                    case "image/png":
+                        fileExtension = "png";
                         break;
                 }
                 files.push({
-                    name: 'pasted-from-clipboard' + fileExtension
+                    name: `pasted-from-clipboard.${fileExtension}`,
                 });
             }
             else {
-                files = this._fileUpload.files;
+                files = Array.from(this._fileUpload.files);
             }
             if (files.length && this.validateUpload(files)) {
                 if (this._options.singleFileRequests) {
                     uploadId = [];
-                    for (var i = 0, length = files.length; i < length; i++) {
-                        var localUploadId = this._uploadFiles([files[i]], blob);
+                    files.forEach((file) => {
+                        const localUploadId = this._uploadFiles([file], blob);
                         if (files.length !== 1) {
                             this._multiFileUploadIds.push(localUploadId);
                         }
                         uploadId.push(localUploadId);
-                    }
+                    });
                 }
                 else {
                     uploadId = this._uploadFiles(files, blob);
@@ -283,64 +249,60 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do
             this._removeButton();
             this._createButton();
             return uploadId;
-        },
+        }
         /**
          * Validates the upload before uploading them.
          *
-         * @param       {(FileList|Array.<File>)}      files           uploaded files
-         * @return     {boolean}
          * @since       5.2
          */
-        validateUpload: function (files) {
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        validateUpload(files) {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
             return true;
-        },
+        }
         /**
          * Sends the request to upload files.
-         *
-         * @param      {(FileList|Array.<File>)}       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);
+        _uploadFiles(files, blob) {
+            const 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++) {
+            const formData = new FormData();
+            for (let i = 0, length = files.length; i < length; i++) {
                 if (this._fileElements[uploadId][i]) {
-                    var internalFileId = elData(this._fileElements[uploadId][i], 'internal-file-id');
+                    const internalFileId = this._fileElements[uploadId][i].dataset.internalFileId;
                     if (blob) {
-                        formData.append('__files[' + internalFileId + ']', blob, files[i].name);
+                        formData.append(`__files[${internalFileId}]`, blob, files[i].name);
                     }
                     else {
-                        formData.append('__files[' + internalFileId + ']', files[i]);
+                        formData.append(`__files[${internalFileId}]`, files[i]);
                     }
                 }
             }
-            formData.append('actionName', this._options.action);
-            formData.append('className', this._options.className);
-            if (this._options.action === 'upload') {
-                formData.append('interfaceName', 'wcf\\data\\IUploadAction');
+            formData.append("actionName", this._options.action);
+            formData.append("className", this._options.className);
+            if (this._options.action === "upload") {
+                formData.append("interfaceName", "wcf\\data\\IUploadAction");
             }
             // recursively append additional parameters to form data
-            var appendFormData = function (parameters, prefix) {
-                prefix = prefix || '';
-                for (var name in parameters) {
-                    if (typeof parameters[name] === 'object') {
-                        var newPrefix = prefix.length === 0 ? name : prefix + '[' + name + ']';
-                        appendFormData(parameters[name], newPrefix);
+            function appendFormData(parameters, prefix) {
+                prefix = prefix || "";
+                Object.entries(parameters).forEach(([key, value]) => {
+                    if (typeof value === "object") {
+                        const newPrefix = prefix.length === 0 ? key : `${prefix}[${key}]`;
+                        appendFormData(value, newPrefix);
                     }
                     else {
-                        var dataName = prefix.length === 0 ? name : prefix + '[' + name + ']';
-                        formData.append(dataName, parameters[name]);
+                        const dataName = prefix.length === 0 ? key : `${prefix}[${key}]`;
+                        formData.append(dataName, value);
                     }
-                }
-            };
-            appendFormData(this._getParameters(), 'parameters');
+                });
+            }
+            appendFormData(this._getParameters(), "parameters");
             appendFormData(this._getFormData());
-            var request = new AjaxRequest({
+            const request = new Request_1.default({
                 data: formData,
                 contentType: false,
                 failure: this._failure.bind(this, uploadId),
@@ -348,47 +310,34 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Do
                 success: this._success.bind(this, uploadId),
                 uploadProgress: this._progress.bind(this, uploadId),
                 url: this._options.url,
-                withCredentials: true
+                withCredentials: true,
             });
             request.sendRequest();
             return uploadId;
-        },
+        }
         /**
          * Returns true if there are any pending uploads handled by this
          * upload manager.
          *
-         * @return     {boolean}
-         * @since      5.2
+         * @since  5.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;
-        },
+        hasPendingUploads() {
+            return (this._fileElements.find((elements) => {
+                return elements.find((el) => el.querySelector("progress") !== null);
+            }) !== undefined);
+        }
         /**
          * Uploads the given file blob.
-         *
-         * @param      {Blob}          blob            file blob
-         * @return     {int}           identifier for the uploaded file
          */
-        uploadBlob: function (blob) {
+        uploadBlob(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) {
+        uploadFile(file) {
             return this._upload(null, file);
         }
-    };
+    }
     return Upload;
 });
index 05e717b84f8e00bd11bf53ecfcef1d7f37c875f1..4deefb2ed90e918dfc5f7090f2594f9e28e0f935 100644 (file)
@@ -57,7 +57,7 @@ export interface AjaxCallbackObject {
 export interface RequestOptions {
   // request data
   data?: RequestData;
-  contentType?: string;
+  contentType?: string | false;
   responseType?: string;
   type?: string;
   url?: string;
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.js
deleted file mode 100644 (file)
index efddcec..0000000
+++ /dev/null
@@ -1,394 +0,0 @@
-/**
- * Uploads file via AJAX.
- *
- * @author     Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     Upload (alias)
- * @module     WoltLabSuite/Core/Upload
- */
-define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse'], function (AjaxRequest, Core, DomChangeListener, Language, DomUtil, DomTraverse) {
-    "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            _createButton: function () { },
-            _createFileElement: function () { },
-            _createFileElements: function () { },
-            _failure: function () { },
-            _getParameters: function () { },
-            _insertButton: function () { },
-            _progress: function () { },
-            _removeButton: function () { },
-            _success: function () { },
-            _upload: function () { },
-            _uploadFiles: function () { }
-        };
-        return Fake;
-    }
-    /**
-     * @constructor
-     */
-    function Upload(buttonContainerId, targetId, options) {
-        options = options || {};
-        if (options.className === undefined) {
-            throw new Error("Missing class name.");
-        }
-        // set default options
-        this._options = Core.extend({
-            // name of the PHP action
-            action: 'upload',
-            // is true if multiple files can be uploaded at once
-            multiple: false,
-            // array of acceptable file types, null if any file type is acceptable
-            acceptableFiles: null,
-            // 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-upload/&t=' + SECURITY_TOKEN
-        }, options);
-        this._options.url = Core.convertLegacyUrl(this._options.url);
-        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' && this._target.nodeName !== 'TBODY') {
-            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();
-    }
-    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');
-            }
-            if (this._options.acceptableFiles !== null) {
-                elAttr(this._fileUpload, 'accept', this._options.acceptableFiles.join(','));
-            }
-            this._fileUpload.addEventListener('change', this._upload.bind(this));
-            this._button = elCreate('p');
-            this._button.className = 'button uploadButton';
-            elAttr(this._button, 'role', 'button');
-            this._fileUpload.addEventListener('focus', (function () {
-                if (this._fileUpload.classList.contains('focus-visible')) {
-                    this._button.classList.add('active');
-                }
-            }).bind(this));
-            this._fileUpload.addEventListener('blur', (function () { this._button.classList.remove('active'); }).bind(this));
-            var span = elCreate('span');
-            span.textContent = Language.get('wcf.global.button.upload');
-            this._button.appendChild(span);
-            DomUtil.prepend(this._fileUpload, this._button);
-            this._insertButton();
-            DomChangeListener.trigger();
-        },
-        /**
-         * Creates the document element for an uploaded file.
-         *
-         * @param      {File}          file            uploaded file
-         * @return     {HTMLElement}
-         */
-        _createFileElement: function (file) {
-            var progress = elCreate('progress');
-            elAttr(progress, 'max', 100);
-            if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
-                var li = elCreate('li');
-                li.innerText = file.name;
-                li.appendChild(progress);
-                this._target.appendChild(li);
-                return li;
-            }
-            else if (this._target.nodeName === 'TBODY') {
-                return this._createFileTableRow(file);
-            }
-            else {
-                var p = elCreate('p');
-                p.appendChild(progress);
-                this._target.appendChild(p);
-                return p;
-            }
-        },
-        /**
-         * Creates the document elements for uploaded files.
-         *
-         * @param      {(FileList|Array.<File>)}       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;
-        },
-        _createFileTableRow: function (file) {
-            throw new Error("Has to be implemented in subclass.");
-        },
-        /**
-         * Handles a failed file upload.
-         *
-         * @param      {int}                   uploadId        identifier of a file upload
-         * @param      {object<string, *>}     data            response data
-         * @param      {string}                responseText    response
-         * @param      {XMLHttpRequest}        xhr             request object
-         * @param      {object<string, *>}     requestOptions  options used to send AJAX request
-         * @return     {boolean}       true if the error message should be shown
-         */
-        _failure: function (uploadId, data, responseText, xhr, requestOptions) {
-            // does nothing
-            return true;
-        },
-        /**
-         * Return additional parameters for upload requests.
-         *
-         * @return     {object<string, *>}     additional parameters
-         */
-        _getParameters: function () {
-            return {};
-        },
-        /**
-         * Return additional form data for upload requests.
-         *
-         * @return     {object<string, *>}     additional form data
-         * @since       5.2
-         */
-        _getFormData: 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<string, *>}     data            response data
-         * @param      {string}                responseText    response
-         * @param      {XMLHttpRequest}        xhr             request object
-         * @param      {object<string, *>}     requestOptions  options used to send AJAX request
-         */
-        _success: function (uploadId, data, responseText, xhr, requestOptions) {
-            // does nothing
-        },
-        /**
-         * 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.<int>|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 && this.validateUpload(files)) {
-                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);
-                }
-            }
-            // re-create upload button to effectively reset the 'files'
-            // property of the input element
-            this._removeButton();
-            this._createButton();
-            return uploadId;
-        },
-        /**
-         * Validates the upload before uploading them.
-         *
-         * @param       {(FileList|Array.<File>)}      files           uploaded files
-         * @return     {boolean}
-         * @since       5.2
-         */
-        validateUpload: function (files) {
-            return true;
-        },
-        /**
-         * Sends the request to upload files.
-         *
-         * @param      {(FileList|Array.<File>)}       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('actionName', this._options.action);
-            formData.append('className', this._options.className);
-            if (this._options.action === 'upload') {
-                formData.append('interfaceName', 'wcf\\data\\IUploadAction');
-            }
-            // recursively append additional parameters to form data
-            var appendFormData = function (parameters, prefix) {
-                prefix = prefix || '';
-                for (var name in parameters) {
-                    if (typeof parameters[name] === 'object') {
-                        var newPrefix = prefix.length === 0 ? name : prefix + '[' + name + ']';
-                        appendFormData(parameters[name], newPrefix);
-                    }
-                    else {
-                        var dataName = prefix.length === 0 ? name : prefix + '[' + name + ']';
-                        formData.append(dataName, parameters[name]);
-                    }
-                }
-            };
-            appendFormData(this._getParameters(), 'parameters');
-            appendFormData(this._getFormData());
-            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      5.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;
-        },
-        /**
-         * 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);
-        }
-    };
-    return Upload;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Upload.ts
new file mode 100644 (file)
index 0000000..b186f57
--- /dev/null
@@ -0,0 +1,442 @@
+/**
+ * Uploads file via AJAX.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  Upload (alias)
+ * @module  WoltLabSuite/Core/Upload
+ */
+
+import { RequestOptions, ResponseData } from "./Ajax/Data";
+import AjaxRequest from "./Ajax/Request";
+import * as Core from "./Core";
+import DomChangeListener from "./Dom/Change/Listener";
+import * as Language from "./Language";
+
+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 {
+  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 _target: HTMLElement;
+
+  protected constructor(buttonContainerId: string, targetId: string, options: Partial<UploadOptions>) {
+    options = options || {};
+    if (!options.className) {
+      throw new Error("Missing class name.");
+    }
+
+    // set default options
+    this._options = Core.extend(
+      {
+        // name of the PHP action
+        action: "upload",
+        // is true if multiple files can be uploaded at once
+        multiple: false,
+        // array of acceptable file types, null if any file type is acceptable
+        acceptableFiles: null,
+        // name of 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-upload/&t=${window.SECURITY_TOKEN}`,
+      },
+      options
+    ) as UploadOptions;
+
+    this._options.url = Core.convertLegacyUrl(this._options.url);
+    if (this._options.url.indexOf("index.php") === 0) {
+      this._options.url = window.WSC_API_URL + this._options.url;
+    }
+
+    const buttonContainer = document.getElementById(buttonContainerId);
+    if (buttonContainer === null) {
+      throw new Error(`Element id '${buttonContainerId}' is unknown.`);
+    }
+    this._buttonContainer = buttonContainer;
+
+    const target = document.getElementById(targetId);
+    if (target === null) {
+      throw new Error(`Element id '${targetId}' is unknown.`);
+    }
+    this._target = target;
+
+    if (
+      options.multiple &&
+      this._target.nodeName !== "UL" &&
+      this._target.nodeName !== "OL" &&
+      this._target.nodeName !== "TBODY"
+    ) {
+      throw new Error("Target element has to be list or table body if uploading multiple files is supported.");
+    }
+
+    this._createButton();
+  }
+
+  /**
+   * Creates the upload button.
+   */
+  protected _createButton(): void {
+    this._fileUpload.type = "file";
+    this._fileUpload.name = this._options.name;
+    if (this._options.multiple) {
+      this._fileUpload.multiple = true;
+    }
+    if (this._options.acceptableFiles !== null) {
+      this._fileUpload.accept = this._options.acceptableFiles.join(",");
+    }
+    this._fileUpload.addEventListener("change", (ev) => this._upload(ev));
+
+    this._button.className = "button uploadButton";
+    this._button.setAttribute("role", "button");
+    this._fileUpload.addEventListener("focus", () => {
+      if (this._fileUpload.classList.contains("focus-visible")) {
+        this._button.classList.add("active");
+      }
+    });
+    this._fileUpload.addEventListener("blur", () => {
+      this._button.classList.remove("active");
+    });
+
+    const span = document.createElement("span");
+    span.textContent = Language.get("wcf.global.button.upload");
+    this._button.appendChild(span);
+
+    this._buttonContainer.insertAdjacentElement("afterbegin", this._fileUpload);
+
+    this._insertButton();
+
+    DomChangeListener.trigger();
+  }
+
+  /**
+   * Creates the document element for an uploaded file.
+   */
+  protected _createFileElement(file: File | FileLikeObject): HTMLElement {
+    const progress = document.createElement("progress");
+    progress.max = 100;
+
+    let element: HTMLElement;
+    switch (this._target.nodeName) {
+      case "OL":
+      case "UL":
+        element = document.createElement("li");
+        element.innerText = file.name;
+        element.appendChild(progress);
+        this._target.appendChild(element);
+
+        return element;
+
+      case "TBODY":
+        return this._createFileTableRow(file);
+
+      default:
+        element = document.createElement("p");
+        element.appendChild(progress);
+        this._target.appendChild(element);
+
+        return element;
+    }
+  }
+
+  /**
+   * Creates the document elements for uploaded files.
+   */
+  protected _createFileElements(files: FileCollection): number | null {
+    if (!files.length) {
+      return null;
+    }
+
+    const elements: FileElements = [];
+    Array.from(files).forEach((file) => {
+      const fileElement = this._createFileElement(file);
+      if (!fileElement.classList.contains("uploadFailed")) {
+        fileElement.dataset.filename = file.name;
+        fileElement.dataset.internalFileId = (this._internalFileId++).toString();
+        elements.push(fileElement);
+      }
+    });
+
+    const uploadId = this._fileElements.length;
+    this._fileElements.push(elements);
+
+    DomChangeListener.trigger();
+    return uploadId;
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  protected _createFileTableRow(file: File | FileLikeObject): HTMLTableRowElement {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+
+    throw new Error("Has to be implemented in subclass.");
+  }
+
+  /**
+   * Handles a failed file upload.
+   */
+  protected _failure(
+    /* eslint-disable @typescript-eslint/no-unused-vars */
+
+    uploadId: number,
+    data: ResponseData,
+    responseText: string,
+    xhr: XMLHttpRequest,
+    requestOptions: RequestOptions
+
+    /* eslint-enable @typescript-eslint/no-unused-vars */
+  ): boolean {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+
+    return true;
+  }
+
+  /**
+   * Return additional parameters for upload requests.
+   */
+  protected _getParameters(): object {
+    return {};
+  }
+
+  /**
+   * Return additional form data for upload requests.
+   *
+   * @since       5.2
+   */
+  protected _getFormData(): object {
+    return {};
+  }
+
+  /**
+   * Inserts the created button to upload files into the button container.
+   */
+  protected _insertButton(): void {
+    this._buttonContainer.insertAdjacentElement("afterbegin", this._button);
+  }
+
+  /**
+   * Updates the progress of an upload.
+   */
+  protected _progress(uploadId: number, event: ProgressEvent): void {
+    const percentComplete = Math.round((event.loaded / event.total) * 100);
+    this._fileElements[uploadId].forEach((element) => {
+      const progress = element.querySelector("progress");
+      if (progress) {
+        progress.value = percentComplete;
+      }
+    });
+  }
+
+  /**
+   * Removes the button to upload files.
+   */
+  protected _removeButton(): void {
+    this._button.remove();
+    DomChangeListener.trigger();
+  }
+
+  /**
+   * Handles a successful file upload.
+   */
+  protected _success(
+    /* eslint-disable @typescript-eslint/no-unused-vars */
+
+    uploadId: number,
+    data: ResponseData,
+    responseText: string,
+    xhr: XMLHttpRequestEventTarget,
+    requestOptions: RequestOptions
+
+    /* eslint-enable @typescript-eslint/no-unused-vars */
+  ): void {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+  }
+
+  /**
+   * File input change callback to upload files.
+   */
+  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 {
+    // remove failed upload elements first
+    this._target.querySelectorAll(".uploadFailed").forEach((el) => el.remove());
+
+    let uploadId: UploadId = null;
+    let files: (File | FileLikeObject)[] = [];
+    if (file) {
+      files.push(file);
+    } else if (blob) {
+      let 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 = Array.from(this._fileUpload.files!);
+    }
+
+    if (files.length && this.validateUpload(files)) {
+      if (this._options.singleFileRequests) {
+        uploadId = [];
+        files.forEach((file) => {
+          const localUploadId = this._uploadFiles([file], blob) as number;
+          if (files.length !== 1) {
+            this._multiFileUploadIds.push(localUploadId);
+          }
+
+          (uploadId as number[]).push(localUploadId);
+        });
+      } else {
+        uploadId = this._uploadFiles(files, blob);
+      }
+    }
+    // re-create upload button to effectively reset the 'files'
+    // property of the input element
+    this._removeButton();
+    this._createButton();
+
+    return uploadId;
+  }
+
+  /**
+   * Validates the upload before uploading them.
+   *
+   * @since       5.2
+   */
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  protected validateUpload(files: FileCollection): boolean {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+
+    return true;
+  }
+
+  /**
+   * Sends the request to upload files.
+   */
+  protected _uploadFiles(files: FileCollection, blob?: Blob | null): number | null {
+    const uploadId = this._createFileElements(files)!;
+
+    // no more files left, abort
+    if (!this._fileElements[uploadId].length) {
+      return null;
+    }
+
+    const formData = new FormData();
+    for (let i = 0, length = files.length; i < length; i++) {
+      if (this._fileElements[uploadId][i]) {
+        const internalFileId = this._fileElements[uploadId][i].dataset.internalFileId!;
+        if (blob) {
+          formData.append(`__files[${internalFileId}]`, blob, files[i].name);
+        } else {
+          formData.append(`__files[${internalFileId}]`, files[i] as File);
+        }
+      }
+    }
+    formData.append("actionName", this._options.action);
+    formData.append("className", this._options.className);
+    if (this._options.action === "upload") {
+      formData.append("interfaceName", "wcf\\data\\IUploadAction");
+    }
+
+    // recursively append additional parameters to form data
+    function appendFormData(parameters: object, prefix?: string): void {
+      prefix = prefix || "";
+
+      Object.entries(parameters).forEach(([key, value]) => {
+        if (typeof value === "object") {
+          const newPrefix = prefix!.length === 0 ? key : `${prefix!}[${key}]`;
+          appendFormData(value, newPrefix);
+        } else {
+          const dataName = prefix!.length === 0 ? key : `${prefix!}[${key}]`;
+          formData.append(dataName, value);
+        }
+      });
+    }
+
+    appendFormData(this._getParameters(), "parameters");
+    appendFormData(this._getFormData());
+
+    const 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.
+   *
+   * @since  5.2
+   */
+  protected hasPendingUploads(): boolean {
+    return (
+      this._fileElements.find((elements) => {
+        return elements.find((el) => el.querySelector("progress") !== null);
+      }) !== undefined
+    );
+  }
+
+  /**
+   * Uploads the given file blob.
+   */
+  uploadBlob(blob: Blob): number {
+    return this._upload(null, null, blob) as number;
+  }
+
+  /**
+   * Uploads the given file.
+   */
+  uploadFile(file: File): number {
+    return this._upload(null, file) as number;
+  }
+}
+
+export = Upload;