Convert `Ui/Comment/Add` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Fri, 6 Nov 2020 12:02:10 +0000 (13:02 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 6 Nov 2020 12:02:10 +0000 (13:02 +0100)
global.d.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Comment/Add.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Comment/Add.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Comment/Add.ts [new file with mode: 0644]

index ac66b89c87140ef07ea943dec21bd0bbe784c1eb..4949d6bcec71fce7f6c4ba1592a6a66c0257ebb9 100644 (file)
@@ -31,5 +31,9 @@ declare global {
     hashCode: () => string;
   }
 
+  interface JQuery {
+    redactor(...args: any[]): JQuery;
+  }
+
   type ArbitraryObject = Record<string, unknown>;
 }
index af141a2181c7f0025d0a5e6c72ffe8f58dc462b2..2b15fd040afee5166f540639e269f03296fd33c7 100644 (file)
  *          modified version. Changes made to this class need to be verified
  *          against the response implementation.
  *
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/Comment/Add
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Comment/Add
  */
-define([
-    'Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', 'WoltLabSuite/Core/Ui/Scroll', 'EventKey', 'User', 'WoltLabSuite/Core/Controller/Captcha'
-], function (Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha) {
+define(["require", "exports", "tslib", "../../Ajax", "../../Controller/Captcha", "../../Core", "../../Dom/Change/Listener", "../../Dom/Util", "../../Event/Handler", "../../Language", "../Dialog", "../Scroll", "../../User", "../Notification"], function (require, exports, tslib_1, Ajax, Captcha_1, Core, Listener_1, Util_1, EventHandler, Language, Dialog_1, UiScroll, User_1, UiNotification) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            init: function () { },
-            _submitGuestDialog: function () { },
-            _submit: function () { },
-            _getParameters: function () { },
-            _validate: function () { },
-            throwError: function () { },
-            _showLoadingOverlay: function () { },
-            _hideLoadingOverlay: function () { },
-            _reset: function () { },
-            _handleError: function () { },
-            _getEditor: function () { },
-            _insertMessage: function () { },
-            _ajaxSuccess: function () { },
-            _ajaxFailure: function () { },
-            _ajaxSetup: function () { },
-            _cancelGuestDialog: function () { }
-        };
-        return Fake;
-    }
-    /**
-     * @constructor
-     */
-    function UiCommentAdd(container) { this.init(container); }
-    UiCommentAdd.prototype = {
+    Ajax = tslib_1.__importStar(Ajax);
+    Captcha_1 = tslib_1.__importDefault(Captcha_1);
+    Core = tslib_1.__importStar(Core);
+    Listener_1 = tslib_1.__importDefault(Listener_1);
+    Util_1 = tslib_1.__importDefault(Util_1);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    Language = tslib_1.__importStar(Language);
+    Dialog_1 = tslib_1.__importDefault(Dialog_1);
+    UiScroll = tslib_1.__importStar(UiScroll);
+    User_1 = tslib_1.__importDefault(User_1);
+    UiNotification = tslib_1.__importStar(UiNotification);
+    class UiCommentAdd {
         /**
          * Initializes a new quick reply field.
-         *
-         * @param       {Element}       container       container element
          */
-        init: function (container) {
-            this._container = container;
-            this._content = elBySel('.jsOuterEditorContainer', this._container);
-            this._textarea = elBySel('.wysiwygTextarea', this._container);
+        constructor(container) {
             this._editor = null;
             this._loadingOverlay = null;
-            this._content.addEventListener('click', (function (event) {
-                if (this._content.classList.contains('collapsed')) {
+            this._container = container;
+            this._content = this._container.querySelector(".jsOuterEditorContainer");
+            this._textarea = this._container.querySelector(".wysiwygTextarea");
+            this._content.addEventListener("click", (event) => {
+                if (this._content.classList.contains("collapsed")) {
                     event.preventDefault();
-                    this._content.classList.remove('collapsed');
+                    this._content.classList.remove("collapsed");
                     this._focusEditor();
                 }
-            }).bind(this));
+            });
             // handle submit button
-            var submitButton = elBySel('button[data-type="save"]', this._container);
-            submitButton.addEventListener('click', this._submit.bind(this));
-        },
+            const submitButton = this._container.querySelector('button[data-type="save"]');
+            submitButton.addEventListener("click", (ev) => this._submit(ev));
+        }
         /**
          * Scrolls the editor into view and sets the caret to the end of the editor.
-         *
-         * @protected
          */
-        _focusEditor: function () {
-            UiScroll.element(this._container, (function () {
-                window.jQuery(this._textarea).redactor('WoltLabCaret.endOfEditor');
-            }).bind(this));
-        },
+        _focusEditor() {
+            UiScroll.element(this._container, () => {
+                window.jQuery(this._textarea).redactor("WoltLabCaret.endOfEditor");
+            });
+        }
         /**
          * Submits the guest dialog.
-         *
-         * @param      {Event}         event
-         * @protected
          */
-        _submitGuestDialog: function (event) {
+        _submitGuestDialog(event) {
             // only submit when enter key is pressed
-            if (event.type === 'keypress' && !EventKey.Enter(event)) {
+            if (event instanceof KeyboardEvent && event.key !== "Enter") {
                 return;
             }
-            var usernameInput = elBySel('input[name=username]', event.currentTarget.closest('.dialogContent'));
-            if (usernameInput.value === '') {
-                elInnerError(usernameInput, Language.get('wcf.global.form.error.empty'));
-                usernameInput.closest('dl').classList.add('formError');
+            const target = event.currentTarget;
+            const dialogContent = target.closest(".dialogContent");
+            const usernameInput = dialogContent.querySelector("input[name=username]");
+            if (usernameInput.value === "") {
+                Util_1.default.innerError(usernameInput, Language.get("wcf.global.form.error.empty"));
+                usernameInput.closest("dl").classList.add("formError");
                 return;
             }
-            var parameters = {
+            let parameters = {
                 parameters: {
                     data: {
-                        username: usernameInput.value
-                    }
-                }
+                        username: usernameInput.value,
+                    },
+                },
             };
-            if (ControllerCaptcha.has('commentAdd')) {
-                var data = ControllerCaptcha.getData('commentAdd');
+            if (Captcha_1.default.has("commentAdd")) {
+                const data = Captcha_1.default.getData("commentAdd");
                 if (data instanceof Promise) {
-                    data.then((function (data) {
+                    void data.then((data) => {
                         parameters = Core.extend(parameters, data);
                         this._submit(undefined, parameters);
-                    }).bind(this));
+                    });
                 }
                 else {
                     parameters = Core.extend(parameters, data);
@@ -113,15 +91,11 @@ define([
             else {
                 this._submit(undefined, parameters);
             }
-        },
+        }
         /**
          * Validates the message and submits it to the server.
-         *
-         * @param      {Event?}        event                   event object
-         * @param      {Object?}       additionalParameters    additional parameters sent to the server
-         * @protected
          */
-        _submit: function (event, additionalParameters) {
+        _submit(event, additionalParameters) {
             if (event) {
                 event.preventDefault();
             }
@@ -131,201 +105,173 @@ define([
             }
             this._showLoadingOverlay();
             // build parameters
-            var parameters = this._getParameters();
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data);
-            if (!User.userId && !additionalParameters) {
+            const parameters = this._getParameters();
+            EventHandler.fire("com.woltlab.wcf.redactor2", "submit_text", parameters.data);
+            if (!User_1.default.userId && !additionalParameters) {
                 parameters.requireGuestDialog = true;
             }
             Ajax.api(this, Core.extend({
-                parameters: parameters
+                parameters: parameters,
             }, additionalParameters));
-        },
+        }
         /**
          * Returns the request parameters to add a comment.
-         *
-         * @return      {{data: {message: string, objectID: number, objectTypeID: number}}}
-         * @protected
          */
-        _getParameters: function () {
-            var commentList = this._container.closest('.commentList');
+        _getParameters() {
+            const commentList = this._container.closest(".commentList");
             return {
                 data: {
                     message: this._getEditor().code.get(),
-                    objectID: ~~elData(commentList, 'object-id'),
-                    objectTypeID: ~~elData(commentList, 'object-type-id')
-                }
+                    objectID: ~~commentList.dataset.objectId,
+                    objectTypeID: ~~commentList.dataset.objectTypeId,
+                },
             };
-        },
+        }
         /**
          * Validates the message and invokes listeners to perform additional validation.
-         *
-         * @return      {boolean}       validation result
-         * @protected
          */
-        _validate: function () {
+        _validate() {
             // remove all existing error elements
-            elBySelAll('.innerError', this._container, elRemove);
+            this._container.querySelectorAll(".innerError").forEach((el) => el.remove());
             // check if editor contains actual content
             if (this._getEditor().utils.isEmpty()) {
-                this.throwError(this._textarea, Language.get('wcf.global.form.error.empty'));
+                this.throwError(this._textarea, Language.get("wcf.global.form.error.empty"));
                 return false;
             }
-            var data = {
+            const data = {
                 api: this,
                 editor: this._getEditor(),
                 message: this._getEditor().code.get(),
-                valid: true
+                valid: true,
             };
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_text', data);
-            return (data.valid !== false);
-        },
+            EventHandler.fire("com.woltlab.wcf.redactor2", "validate_text", data);
+            return data.valid;
+        }
         /**
          * Throws an error by adding an inline error to target element.
-         *
-         * @param       {Element}       element         erroneous element
-         * @param       {string}        message         error message
          */
-        throwError: function (element, message) {
-            elInnerError(element, (message === 'empty' ? Language.get('wcf.global.form.error.empty') : message));
-        },
+        throwError(element, message) {
+            Util_1.default.innerError(element, message === "empty" ? Language.get("wcf.global.form.error.empty") : message);
+        }
         /**
          * Displays a loading spinner while the request is processed by the server.
-         *
-         * @protected
          */
-        _showLoadingOverlay: function () {
+        _showLoadingOverlay() {
             if (this._loadingOverlay === null) {
-                this._loadingOverlay = elCreate('div');
-                this._loadingOverlay.className = 'commentLoadingOverlay';
+                this._loadingOverlay = document.createElement("div");
+                this._loadingOverlay.className = "commentLoadingOverlay";
                 this._loadingOverlay.innerHTML = '<span class="icon icon96 fa-spinner"></span>';
             }
-            this._content.classList.add('loading');
+            this._content.classList.add("loading");
             this._content.appendChild(this._loadingOverlay);
-        },
+        }
         /**
          * Hides the loading spinner.
-         *
-         * @protected
          */
-        _hideLoadingOverlay: function () {
-            this._content.classList.remove('loading');
-            var loadingOverlay = elBySel('.commentLoadingOverlay', this._content);
+        _hideLoadingOverlay() {
+            this._content.classList.remove("loading");
+            const loadingOverlay = this._content.querySelector(".commentLoadingOverlay");
             if (loadingOverlay !== null) {
-                loadingOverlay.parentNode.removeChild(loadingOverlay);
+                loadingOverlay.remove();
             }
-        },
+        }
         /**
          * Resets the editor contents and notifies event listeners.
-         *
-         * @protected
          */
-        _reset: function () {
-            this._getEditor().code.set('<p>\u200b</p>');
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'reset_text');
-            if (document.activeElement) {
+        _reset() {
+            this._getEditor().code.set("<p>\u200b</p>");
+            EventHandler.fire("com.woltlab.wcf.redactor2", "reset_text");
+            if (document.activeElement instanceof HTMLElement) {
                 document.activeElement.blur();
             }
-            this._content.classList.add('collapsed');
-        },
+            this._content.classList.add("collapsed");
+        }
         /**
          * Handles errors occurred during server processing.
-         *
-         * @param       {Object}        data    response data
-         * @protected
          */
-        _handleError: function (data) {
-            //noinspection JSUnresolvedVariable
+        _handleError(data) {
             this.throwError(this._textarea, data.returnValues.errorType);
-        },
+        }
         /**
          * Returns the current editor instance.
-         *
-         * @return      {Object}       editor instance
-         * @protected
          */
-        _getEditor: function () {
+        _getEditor() {
             if (this._editor === null) {
-                if (typeof window.jQuery === 'function') {
-                    this._editor = window.jQuery(this._textarea).data('redactor');
+                if (typeof window.jQuery === "function") {
+                    this._editor = window.jQuery(this._textarea).data("redactor");
                 }
                 else {
                     throw new Error("Unable to access editor, jQuery has not been loaded yet.");
                 }
             }
             return this._editor;
-        },
+        }
         /**
          * Inserts the rendered message.
-         *
-         * @param       {Object}        data    response data
-         * @return      {Element}       scroll target
-         * @protected
          */
-        _insertMessage: function (data) {
+        _insertMessage(data) {
             // insert HTML
-            //noinspection JSCheckFunctionSignatures
-            DomUtil.insertHtml(data.returnValues.template, this._container, 'after');
-            UiNotification.show(Language.get('wcf.global.success.add'));
-            DomChangeListener.trigger();
+            Util_1.default.insertHtml(data.returnValues.template, this._container, "after");
+            UiNotification.show(Language.get("wcf.global.success.add"));
+            Listener_1.default.trigger();
             return this._container.nextElementSibling;
-        },
-        /**
-         * @param {{returnValues:{guestDialog:string}}} data
-         * @protected
-         */
-        _ajaxSuccess: function (data) {
-            if (!User.userId && data.returnValues.guestDialog) {
-                UiDialog.openStatic('jsDialogGuestComment', data.returnValues.guestDialog, {
+        }
+        _ajaxSuccess(data) {
+            if (!User_1.default.userId && data.returnValues.guestDialog) {
+                Dialog_1.default.openStatic("jsDialogGuestComment", data.returnValues.guestDialog, {
                     closable: false,
-                    onClose: function () {
-                        if (ControllerCaptcha.has('commentAdd')) {
-                            ControllerCaptcha.delete('commentAdd');
+                    onClose: () => {
+                        if (Captcha_1.default.has("commentAdd")) {
+                            Captcha_1.default.delete("commentAdd");
                         }
                     },
-                    title: Language.get('wcf.global.confirmation.title')
+                    title: Language.get("wcf.global.confirmation.title"),
                 });
-                var dialog = UiDialog.getDialog('jsDialogGuestComment');
-                elBySel('input[type=submit]', dialog.content).addEventListener('click', this._submitGuestDialog.bind(this));
-                elBySel('button[data-type="cancel"]', dialog.content).addEventListener('click', this._cancelGuestDialog.bind(this));
-                elBySel('input[type=text]', dialog.content).addEventListener('keypress', this._submitGuestDialog.bind(this));
+                const dialog = Dialog_1.default.getDialog("jsDialogGuestComment");
+                const submitButton = dialog.content.querySelector("input[type=submit]");
+                submitButton.addEventListener("click", (ev) => this._submitGuestDialog(ev));
+                const cancelButton = dialog.content.querySelector('button[data-type="cancel"]');
+                cancelButton.addEventListener("click", () => this._cancelGuestDialog());
+                const input = dialog.content.querySelector("input[type=text]");
+                input.addEventListener("keypress", (ev) => this._submitGuestDialog(ev));
             }
             else {
-                var scrollTarget = this._insertMessage(data);
-                if (!User.userId) {
-                    UiDialog.close('jsDialogGuestComment');
+                const scrollTarget = this._insertMessage(data);
+                if (!User_1.default.userId) {
+                    Dialog_1.default.close("jsDialogGuestComment");
                 }
                 this._reset();
                 this._hideLoadingOverlay();
-                window.setTimeout((function () {
+                window.setTimeout(() => {
                     UiScroll.element(scrollTarget);
-                }).bind(this), 100);
+                }, 100);
             }
-        },
-        _ajaxFailure: function (data) {
+        }
+        _ajaxFailure(data) {
             this._hideLoadingOverlay();
-            //noinspection JSUnresolvedVariable
             if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
                 return true;
             }
             this._handleError(data);
             return false;
-        },
-        _ajaxSetup: function () {
+        }
+        _ajaxSetup() {
             return {
                 data: {
-                    actionName: 'addComment',
-                    className: 'wcf\\data\\comment\\CommentAction'
+                    actionName: "addComment",
+                    className: "wcf\\data\\comment\\CommentAction",
                 },
-                silent: true
+                silent: true,
             };
-        },
+        }
         /**
          * Cancels the guest dialog and restores the comment editor.
          */
-        _cancelGuestDialog: function () {
-            UiDialog.close('jsDialogGuestComment');
+        _cancelGuestDialog() {
+            Dialog_1.default.close("jsDialogGuestComment");
             this._hideLoadingOverlay();
         }
-    };
+    }
+    Core.enableLegacyInheritance(UiCommentAdd);
     return UiCommentAdd;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Comment/Add.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Comment/Add.js
deleted file mode 100644 (file)
index a8cba69..0000000
+++ /dev/null
@@ -1,390 +0,0 @@
-/**
- * Handles the comment add feature.
- * 
- * Warning: This implementation is also used for responses, but in a slightly
- *          modified version. Changes made to this class need to be verified
- *          against the response implementation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/Comment/Add
- */
-define([
-       'Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', 'WoltLabSuite/Core/Ui/Scroll', 'EventKey', 'User', 'WoltLabSuite/Core/Controller/Captcha'
-],
-function(
-       Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha
-) {
-       "use strict";
-       
-       if (!COMPILER_TARGET_DEFAULT) {
-               var Fake = function() {};
-               Fake.prototype = {
-                       init: function() {},
-                       _submitGuestDialog: function() {},
-                       _submit: function() {},
-                       _getParameters: function () {},
-                       _validate: function() {},
-                       throwError: function() {},
-                       _showLoadingOverlay: function() {},
-                       _hideLoadingOverlay: function() {},
-                       _reset: function() {},
-                       _handleError: function() {},
-                       _getEditor: function() {},
-                       _insertMessage: function() {},
-                       _ajaxSuccess: function() {},
-                       _ajaxFailure: function() {},
-                       _ajaxSetup: function() {},
-                       _cancelGuestDialog: function() {}
-               };
-               return Fake;
-       }
-       
-       /**
-        * @constructor
-        */
-       function UiCommentAdd(container) { this.init(container); }
-       UiCommentAdd.prototype = {
-               /**
-                * Initializes a new quick reply field.
-                * 
-                * @param       {Element}       container       container element
-                */
-               init: function(container) {
-                       this._container = container;
-                       this._content = elBySel('.jsOuterEditorContainer', this._container);
-                       this._textarea = elBySel('.wysiwygTextarea', this._container);
-                       this._editor = null;
-                       this._loadingOverlay = null;
-                       
-                       this._content.addEventListener('click', (function (event) {
-                               if (this._content.classList.contains('collapsed')) {
-                                       event.preventDefault();
-                                       
-                                       this._content.classList.remove('collapsed');
-                                       
-                                       this._focusEditor();
-                               }
-                       }).bind(this));
-                       
-                       // handle submit button
-                       var submitButton = elBySel('button[data-type="save"]', this._container);
-                       submitButton.addEventListener('click', this._submit.bind(this));
-               },
-               
-               /**
-                * Scrolls the editor into view and sets the caret to the end of the editor.
-                * 
-                * @protected
-                */
-               _focusEditor: function () {
-                       UiScroll.element(this._container, (function () {
-                               window.jQuery(this._textarea).redactor('WoltLabCaret.endOfEditor');
-                       }).bind(this));
-               },
-               
-               /**
-                * Submits the guest dialog.
-                * 
-                * @param       {Event}         event
-                * @protected
-                */
-               _submitGuestDialog: function(event) {
-                       // only submit when enter key is pressed
-                       if (event.type === 'keypress' && !EventKey.Enter(event)) {
-                               return;
-                       }
-                       
-                       var usernameInput = elBySel('input[name=username]', event.currentTarget.closest('.dialogContent'));
-                       if (usernameInput.value === '') {
-                               elInnerError(usernameInput, Language.get('wcf.global.form.error.empty'));
-                               usernameInput.closest('dl').classList.add('formError');
-                               
-                               return;
-                       }
-                       
-                       var parameters = {
-                               parameters: {
-                                       data: {
-                                               username: usernameInput.value
-                                       }
-                               }
-                       };
-                       
-                       if (ControllerCaptcha.has('commentAdd')) {
-                               var data = ControllerCaptcha.getData('commentAdd');
-                               if (data instanceof Promise) {
-                                       data.then((function (data) {
-                                               parameters = Core.extend(parameters, data);
-                                               this._submit(undefined, parameters);
-                                       }).bind(this));
-                               }
-                               else {
-                                       parameters = Core.extend(parameters, data);
-                                       this._submit(undefined, parameters);
-                               }
-                       }
-                       else {
-                               this._submit(undefined, parameters);
-                       }
-               },
-               
-               /**
-                * Validates the message and submits it to the server.
-                * 
-                * @param       {Event?}        event                   event object
-                * @param       {Object?}       additionalParameters    additional parameters sent to the server
-                * @protected
-                */
-               _submit: function(event, additionalParameters) {
-                       if (event) {
-                               event.preventDefault();
-                       }
-                       
-                       if (!this._validate()) {
-                               // validation failed, bail out
-                               return;
-                       }
-                       
-                       this._showLoadingOverlay();
-                       
-                       // build parameters
-                       var parameters = this._getParameters();
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data);
-                       
-                       if (!User.userId && !additionalParameters) {
-                               parameters.requireGuestDialog = true;
-                       }
-                       
-                       Ajax.api(this, Core.extend({
-                               parameters: parameters
-                       }, additionalParameters));
-               },
-               
-               /**
-                * Returns the request parameters to add a comment.
-                * 
-                * @return      {{data: {message: string, objectID: number, objectTypeID: number}}}
-                * @protected
-                */
-               _getParameters: function () {
-                       var commentList = this._container.closest('.commentList');
-                       
-                       return {
-                               data: {
-                                       message: this._getEditor().code.get(),
-                                       objectID: ~~elData(commentList, 'object-id'),
-                                       objectTypeID: ~~elData(commentList, 'object-type-id')
-                               }
-                       };
-               },
-               
-               /**
-                * Validates the message and invokes listeners to perform additional validation.
-                * 
-                * @return      {boolean}       validation result
-                * @protected
-                */
-               _validate: function() {
-                       // remove all existing error elements
-                       elBySelAll('.innerError', this._container, elRemove);
-                       
-                       // check if editor contains actual content
-                       if (this._getEditor().utils.isEmpty()) {
-                               this.throwError(this._textarea, Language.get('wcf.global.form.error.empty'));
-                               return false;
-                       }
-                       
-                       var data = {
-                               api: this,
-                               editor: this._getEditor(),
-                               message: this._getEditor().code.get(),
-                               valid: true
-                       };
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_text', data);
-                       
-                       return (data.valid !== false);
-               },
-               
-               /**
-                * Throws an error by adding an inline error to target element.
-                * 
-                * @param       {Element}       element         erroneous element
-                * @param       {string}        message         error message
-                */
-               throwError: function(element, message) {
-                       elInnerError(element, (message === 'empty' ? Language.get('wcf.global.form.error.empty') : message));
-               },
-               
-               /**
-                * Displays a loading spinner while the request is processed by the server.
-                * 
-                * @protected
-                */
-               _showLoadingOverlay: function() {
-                       if (this._loadingOverlay === null) {
-                               this._loadingOverlay = elCreate('div');
-                               this._loadingOverlay.className = 'commentLoadingOverlay';
-                               this._loadingOverlay.innerHTML = '<span class="icon icon96 fa-spinner"></span>';
-                       }
-                       
-                       this._content.classList.add('loading');
-                       this._content.appendChild(this._loadingOverlay);
-               },
-               
-               /**
-                * Hides the loading spinner.
-                * 
-                * @protected
-                */
-               _hideLoadingOverlay: function() {
-                       this._content.classList.remove('loading');
-                       
-                       var loadingOverlay = elBySel('.commentLoadingOverlay', this._content);
-                       if (loadingOverlay !== null) {
-                               loadingOverlay.parentNode.removeChild(loadingOverlay);
-                       }
-               },
-               
-               /**
-                * Resets the editor contents and notifies event listeners.
-                * 
-                * @protected
-                */
-               _reset: function() {
-                       this._getEditor().code.set('<p>\u200b</p>');
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'reset_text');
-                       
-                       if (document.activeElement) {
-                               document.activeElement.blur();
-                       }
-                       
-                       this._content.classList.add('collapsed');
-               },
-               
-               /**
-                * Handles errors occurred during server processing.
-                * 
-                * @param       {Object}        data    response data
-                * @protected
-                */
-               _handleError: function(data) {
-                       //noinspection JSUnresolvedVariable
-                       this.throwError(this._textarea, data.returnValues.errorType);
-               },
-               
-               /**
-                * Returns the current editor instance.
-                * 
-                * @return      {Object}       editor instance
-                * @protected
-                */
-               _getEditor: function() {
-                       if (this._editor === null) {
-                               if (typeof window.jQuery === 'function') {
-                                       this._editor = window.jQuery(this._textarea).data('redactor');
-                               }
-                               else {
-                                       throw new Error("Unable to access editor, jQuery has not been loaded yet.");
-                               }
-                       }
-                       
-                       return this._editor;
-               },
-               
-               /**
-                * Inserts the rendered message.
-                * 
-                * @param       {Object}        data    response data
-                * @return      {Element}       scroll target
-                * @protected
-                */
-               _insertMessage: function(data) {
-                       // insert HTML
-                       //noinspection JSCheckFunctionSignatures
-                       DomUtil.insertHtml(data.returnValues.template, this._container, 'after');
-                       
-                       UiNotification.show(Language.get('wcf.global.success.add'));
-                       
-                       DomChangeListener.trigger();
-                       
-                       return this._container.nextElementSibling;
-               },
-               
-               /**
-                * @param {{returnValues:{guestDialog:string}}} data
-                * @protected
-                */
-               _ajaxSuccess: function(data) {
-                       if (!User.userId && data.returnValues.guestDialog) {
-                               UiDialog.openStatic('jsDialogGuestComment', data.returnValues.guestDialog, {
-                                       closable: false,
-                                       onClose: function() {
-                                               if (ControllerCaptcha.has('commentAdd')) {
-                                                       ControllerCaptcha.delete('commentAdd');
-                                               }
-                                       },
-                                       title: Language.get('wcf.global.confirmation.title')
-                               });
-                               
-                               var dialog = UiDialog.getDialog('jsDialogGuestComment');
-                               elBySel('input[type=submit]', dialog.content).addEventListener('click', this._submitGuestDialog.bind(this));
-                               elBySel('button[data-type="cancel"]', dialog.content).addEventListener('click', this._cancelGuestDialog.bind(this));
-                               elBySel('input[type=text]', dialog.content).addEventListener('keypress', this._submitGuestDialog.bind(this));
-                       }
-                       else {
-                               var scrollTarget = this._insertMessage(data);
-                               
-                               if (!User.userId) {
-                                       UiDialog.close('jsDialogGuestComment');
-                               }
-                               
-                               this._reset();
-                               
-                               this._hideLoadingOverlay();
-                               
-                               window.setTimeout((function () {
-                                       UiScroll.element(scrollTarget);
-                               }).bind(this), 100);
-                       }
-               },
-               
-               _ajaxFailure: function(data) {
-                       this._hideLoadingOverlay();
-                       
-                       //noinspection JSUnresolvedVariable
-                       if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
-                               return true;
-                       }
-                       
-                       this._handleError(data);
-                       
-                       return false;
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'addComment',
-                                       className: 'wcf\\data\\comment\\CommentAction'
-                               },
-                               silent: true
-                       };
-               },
-               
-               /**
-                * Cancels the guest dialog and restores the comment editor.
-                */
-               _cancelGuestDialog: function() {
-                       UiDialog.close('jsDialogGuestComment');
-                       
-                       this._hideLoadingOverlay();
-               }
-       };
-       
-       return UiCommentAdd;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Comment/Add.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Comment/Add.ts
new file mode 100644 (file)
index 0000000..aee0293
--- /dev/null
@@ -0,0 +1,348 @@
+/**
+ * Handles the comment add feature.
+ *
+ * Warning: This implementation is also used for responses, but in a slightly
+ *          modified version. Changes made to this class need to be verified
+ *          against the response implementation.
+ *
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Comment/Add
+ */
+
+import * as Ajax from "../../Ajax";
+import { AjaxCallbackSetup, ResponseData } from "../../Ajax/Data";
+import ControllerCaptcha from "../../Controller/Captcha";
+import * as Core from "../../Core";
+import DomChangeListener from "../../Dom/Change/Listener";
+import DomUtil from "../../Dom/Util";
+import * as EventHandler from "../../Event/Handler";
+import * as Language from "../../Language";
+import UiDialog from "../Dialog";
+import { RedactorEditor } from "../Redactor/Editor";
+import * as UiScroll from "../Scroll";
+import User from "../../User";
+import * as UiNotification from "../Notification";
+
+interface AjaxResponse {
+  returnValues: {
+    guestDialog?: string;
+    template: string;
+  };
+}
+
+class UiCommentAdd {
+  protected readonly _container: HTMLElement;
+  protected readonly _content: HTMLElement;
+  protected readonly _textarea: HTMLTextAreaElement;
+  protected _editor: RedactorEditor | null = null;
+  protected _loadingOverlay: HTMLElement | null = null;
+
+  /**
+   * Initializes a new quick reply field.
+   */
+  constructor(container: HTMLElement) {
+    this._container = container;
+    this._content = this._container.querySelector(".jsOuterEditorContainer") as HTMLElement;
+    this._textarea = this._container.querySelector(".wysiwygTextarea") as HTMLTextAreaElement;
+
+    this._content.addEventListener("click", (event) => {
+      if (this._content.classList.contains("collapsed")) {
+        event.preventDefault();
+
+        this._content.classList.remove("collapsed");
+
+        this._focusEditor();
+      }
+    });
+
+    // handle submit button
+    const submitButton = this._container.querySelector('button[data-type="save"]') as HTMLButtonElement;
+    submitButton.addEventListener("click", (ev) => this._submit(ev));
+  }
+
+  /**
+   * Scrolls the editor into view and sets the caret to the end of the editor.
+   */
+  protected _focusEditor(): void {
+    UiScroll.element(this._container, () => {
+      window.jQuery(this._textarea).redactor("WoltLabCaret.endOfEditor");
+    });
+  }
+
+  /**
+   * Submits the guest dialog.
+   */
+  protected _submitGuestDialog(event: MouseEvent | KeyboardEvent): void {
+    // only submit when enter key is pressed
+    if (event instanceof KeyboardEvent && event.key !== "Enter") {
+      return;
+    }
+
+    const target = event.currentTarget as HTMLInputElement;
+    const dialogContent = target.closest(".dialogContent") as HTMLElement;
+    const usernameInput = dialogContent.querySelector("input[name=username]") as HTMLInputElement;
+    if (usernameInput.value === "") {
+      DomUtil.innerError(usernameInput, Language.get("wcf.global.form.error.empty"));
+      usernameInput.closest("dl")!.classList.add("formError");
+
+      return;
+    }
+
+    let parameters: ArbitraryObject = {
+      parameters: {
+        data: {
+          username: usernameInput.value,
+        },
+      },
+    };
+
+    if (ControllerCaptcha.has("commentAdd")) {
+      const data = ControllerCaptcha.getData("commentAdd");
+      if (data instanceof Promise) {
+        void data.then((data) => {
+          parameters = Core.extend(parameters, data) as ArbitraryObject;
+          this._submit(undefined, parameters);
+        });
+      } else {
+        parameters = Core.extend(parameters, data as ArbitraryObject) as ArbitraryObject;
+        this._submit(undefined, parameters);
+      }
+    } else {
+      this._submit(undefined, parameters);
+    }
+  }
+
+  /**
+   * Validates the message and submits it to the server.
+   */
+  protected _submit(event: MouseEvent | undefined, additionalParameters?: ArbitraryObject): void {
+    if (event) {
+      event.preventDefault();
+    }
+
+    if (!this._validate()) {
+      // validation failed, bail out
+      return;
+    }
+
+    this._showLoadingOverlay();
+
+    // build parameters
+    const parameters = this._getParameters();
+
+    EventHandler.fire("com.woltlab.wcf.redactor2", "submit_text", parameters.data as any);
+
+    if (!User.userId && !additionalParameters) {
+      parameters.requireGuestDialog = true;
+    }
+
+    Ajax.api(
+      this,
+      Core.extend(
+        {
+          parameters: parameters,
+        },
+        additionalParameters as ArbitraryObject,
+      ),
+    );
+  }
+
+  /**
+   * Returns the request parameters to add a comment.
+   */
+  protected _getParameters(): ArbitraryObject {
+    const commentList = this._container.closest(".commentList") as HTMLElement;
+
+    return {
+      data: {
+        message: this._getEditor().code.get(),
+        objectID: ~~commentList.dataset.objectId!,
+        objectTypeID: ~~commentList.dataset.objectTypeId!,
+      },
+    };
+  }
+
+  /**
+   * Validates the message and invokes listeners to perform additional validation.
+   */
+  protected _validate(): boolean {
+    // remove all existing error elements
+    this._container.querySelectorAll(".innerError").forEach((el) => el.remove());
+
+    // check if editor contains actual content
+    if (this._getEditor().utils.isEmpty()) {
+      this.throwError(this._textarea, Language.get("wcf.global.form.error.empty"));
+      return false;
+    }
+
+    const data = {
+      api: this,
+      editor: this._getEditor(),
+      message: this._getEditor().code.get(),
+      valid: true,
+    };
+
+    EventHandler.fire("com.woltlab.wcf.redactor2", "validate_text", data);
+
+    return data.valid;
+  }
+
+  /**
+   * Throws an error by adding an inline error to target element.
+   */
+  throwError(element: HTMLElement, message: string): void {
+    DomUtil.innerError(element, message === "empty" ? Language.get("wcf.global.form.error.empty") : message);
+  }
+
+  /**
+   * Displays a loading spinner while the request is processed by the server.
+   */
+  protected _showLoadingOverlay(): void {
+    if (this._loadingOverlay === null) {
+      this._loadingOverlay = document.createElement("div");
+      this._loadingOverlay.className = "commentLoadingOverlay";
+      this._loadingOverlay.innerHTML = '<span class="icon icon96 fa-spinner"></span>';
+    }
+
+    this._content.classList.add("loading");
+    this._content.appendChild(this._loadingOverlay);
+  }
+
+  /**
+   * Hides the loading spinner.
+   */
+  protected _hideLoadingOverlay(): void {
+    this._content.classList.remove("loading");
+
+    const loadingOverlay = this._content.querySelector(".commentLoadingOverlay");
+    if (loadingOverlay !== null) {
+      loadingOverlay.remove();
+    }
+  }
+
+  /**
+   * Resets the editor contents and notifies event listeners.
+   */
+  protected _reset(): void {
+    this._getEditor().code.set("<p>\u200b</p>");
+
+    EventHandler.fire("com.woltlab.wcf.redactor2", "reset_text");
+
+    if (document.activeElement instanceof HTMLElement) {
+      document.activeElement.blur();
+    }
+
+    this._content.classList.add("collapsed");
+  }
+
+  /**
+   * Handles errors occurred during server processing.
+   */
+  protected _handleError(data: ResponseData): void {
+    this.throwError(this._textarea, data.returnValues.errorType);
+  }
+
+  /**
+   * Returns the current editor instance.
+   */
+  protected _getEditor(): RedactorEditor {
+    if (this._editor === null) {
+      if (typeof window.jQuery === "function") {
+        this._editor = window.jQuery(this._textarea).data("redactor") as RedactorEditor;
+      } else {
+        throw new Error("Unable to access editor, jQuery has not been loaded yet.");
+      }
+    }
+
+    return this._editor;
+  }
+
+  /**
+   * Inserts the rendered message.
+   */
+  protected _insertMessage(data: AjaxResponse): HTMLElement {
+    // insert HTML
+    DomUtil.insertHtml(data.returnValues.template, this._container, "after");
+
+    UiNotification.show(Language.get("wcf.global.success.add"));
+
+    DomChangeListener.trigger();
+
+    return this._container.nextElementSibling as HTMLElement;
+  }
+
+  _ajaxSuccess(data: AjaxResponse): void {
+    if (!User.userId && data.returnValues.guestDialog) {
+      UiDialog.openStatic("jsDialogGuestComment", data.returnValues.guestDialog, {
+        closable: false,
+        onClose: () => {
+          if (ControllerCaptcha.has("commentAdd")) {
+            ControllerCaptcha.delete("commentAdd");
+          }
+        },
+        title: Language.get("wcf.global.confirmation.title"),
+      });
+
+      const dialog = UiDialog.getDialog("jsDialogGuestComment")!;
+
+      const submitButton = dialog.content.querySelector("input[type=submit]") as HTMLButtonElement;
+      submitButton.addEventListener("click", (ev) => this._submitGuestDialog(ev));
+      const cancelButton = dialog.content.querySelector('button[data-type="cancel"]') as HTMLButtonElement;
+      cancelButton.addEventListener("click", () => this._cancelGuestDialog());
+
+      const input = dialog.content.querySelector("input[type=text]") as HTMLInputElement;
+      input.addEventListener("keypress", (ev) => this._submitGuestDialog(ev));
+    } else {
+      const scrollTarget = this._insertMessage(data);
+
+      if (!User.userId) {
+        UiDialog.close("jsDialogGuestComment");
+      }
+
+      this._reset();
+
+      this._hideLoadingOverlay();
+
+      window.setTimeout(() => {
+        UiScroll.element(scrollTarget);
+      }, 100);
+    }
+  }
+
+  _ajaxFailure(data: ResponseData): boolean {
+    this._hideLoadingOverlay();
+
+    if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
+      return true;
+    }
+
+    this._handleError(data);
+
+    return false;
+  }
+
+  _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
+    return {
+      data: {
+        actionName: "addComment",
+        className: "wcf\\data\\comment\\CommentAction",
+      },
+      silent: true,
+    };
+  }
+
+  /**
+   * Cancels the guest dialog and restores the comment editor.
+   */
+  protected _cancelGuestDialog(): void {
+    UiDialog.close("jsDialogGuestComment");
+
+    this._hideLoadingOverlay();
+  }
+}
+
+Core.enableLegacyInheritance(UiCommentAdd);
+
+export = UiCommentAdd;