Convert `Ui/Message/InlineEditor` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Thu, 5 Nov 2020 15:09:06 +0000 (16:09 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 5 Nov 2020 15:09:06 +0000 (16:09 +0100)
global.d.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.ts [new file with mode: 0644]

index 23265d7316591fc4e5ef2d7ac8f8ec23aafcde38..466c32a2ae6d4b297c77f418f1ae880a3443f96e 100644 (file)
@@ -29,4 +29,6 @@ declare global {
   interface String {
     hashCode: () => string;
   }
+
+  type ArbitraryObject = Record<string, unknown>;
 }
index 1fc2489ce397a43375961732ed0ed36081b3d16f..4617cf60edde92dc1c44e992152d39fd0ea41bea 100644 (file)
 /**
  * Flexible message inline editor.
  *
- * @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/Message/InlineEditor
+ * @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/Message/InlineEditor
  */
-define([
-    'Ajax', 'Core', 'Dictionary', 'Environment',
-    'EventHandler', 'Language', 'ObjectMap', 'Dom/ChangeListener', 'Dom/Traverse',
-    'Dom/Util', 'Ui/Notification', 'Ui/ReusableDropdown', 'WoltLabSuite/Core/Ui/Scroll'
-], function (Ajax, Core, Dictionary, Environment, EventHandler, Language, ObjectMap, DomChangeListener, DomTraverse, DomUtil, UiNotification, UiReusableDropdown, UiScroll) {
+define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Dom/Change/Listener", "../../Dom/Util", "../../Environment", "../../Event/Handler", "../../Language", "../Dropdown/Reusable", "../Notification", "../Scroll"], function (require, exports, tslib_1, Ajax, Core, Listener_1, Util_1, Environment, EventHandler, Language, UiDropdownReusable, UiNotification, UiScroll) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            init: function () { },
-            rebuild: function () { },
-            _click: function () { },
-            _clickDropdown: function () { },
-            _dropdownBuild: function () { },
-            _dropdownToggle: function () { },
-            _dropdownGetItems: function () { },
-            _dropdownOpen: function () { },
-            _dropdownSelect: function () { },
-            _clickDropdownItem: function () { },
-            _prepare: function () { },
-            _showEditor: function () { },
-            _restoreMessage: function () { },
-            _save: function () { },
-            _validate: function () { },
-            throwError: function () { },
-            _showMessage: function () { },
-            _hideEditor: function () { },
-            _restoreEditor: function () { },
-            _destroyEditor: function () { },
-            _getHash: function () { },
-            _updateHistory: function () { },
-            _getEditorId: function () { },
-            _getObjectId: function () { },
-            _ajaxFailure: function () { },
-            _ajaxSuccess: function () { },
-            _ajaxSetup: function () { },
-            legacyEdit: function () { }
-        };
-        return Fake;
-    }
-    /**
-     * @constructor
-     */
-    function UiMessageInlineEditor(options) { this.init(options); }
-    UiMessageInlineEditor.prototype = {
+    Ajax = tslib_1.__importStar(Ajax);
+    Core = tslib_1.__importStar(Core);
+    Listener_1 = tslib_1.__importDefault(Listener_1);
+    Util_1 = tslib_1.__importDefault(Util_1);
+    Environment = tslib_1.__importStar(Environment);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    Language = tslib_1.__importStar(Language);
+    UiDropdownReusable = tslib_1.__importStar(UiDropdownReusable);
+    UiNotification = tslib_1.__importStar(UiNotification);
+    UiScroll = tslib_1.__importStar(UiScroll);
+    class UiMessageInlineEditor {
         /**
          * Initializes the message inline editor.
-         *
-         * @param      {Object}        options         list of configuration options
          */
-        init: function (options) {
+        constructor(opts) {
             this._activeDropdownElement = null;
             this._activeElement = null;
             this._dropdownMenu = null;
-            this._elements = new ObjectMap();
+            this._elements = new WeakMap();
             this._options = Core.extend({
                 canEditInline: false,
-                className: '',
+                className: "",
                 containerId: 0,
-                dropdownIdentifier: '',
-                editorPrefix: 'messageEditor',
-                messageSelector: '.jsMessage',
-                quoteManager: null
-            }, options);
+                dropdownIdentifier: "",
+                editorPrefix: "messageEditor",
+                messageSelector: ".jsMessage",
+                quoteManager: null,
+            }, opts);
             this.rebuild();
-            DomChangeListener.add('Ui/Message/InlineEdit_' + this._options.className, this.rebuild.bind(this));
-        },
+            Listener_1.default.add(`Ui/Message/InlineEdit_${this._options.className}`, () => this.rebuild());
+        }
         /**
          * Initializes each applicable message, should be called whenever new
          * messages are being displayed.
          */
-        rebuild: function () {
-            var button, canEdit, element, elements = elBySelAll(this._options.messageSelector);
-            for (var i = 0, length = elements.length; i < length; i++) {
-                element = elements[i];
+        rebuild() {
+            document.querySelectorAll(this._options.messageSelector).forEach((element) => {
                 if (this._elements.has(element)) {
-                    continue;
+                    return;
                 }
-                button = elBySel('.jsMessageEditButton', element);
+                const button = element.querySelector(".jsMessageEditButton");
                 if (button !== null) {
-                    canEdit = elDataBool(element, 'can-edit');
-                    if (this._options.canEditInline || elDataBool(element, 'can-edit-inline')) {
-                        button.addEventListener('click', this._clickDropdown.bind(this, element));
-                        button.classList.add('jsDropdownEnabled');
+                    const canEdit = Core.stringToBool(element.dataset.canEdit || "");
+                    const canEditInline = Core.stringToBool(element.dataset.canEditInline || "");
+                    if (this._options.canEditInline || canEditInline) {
+                        button.addEventListener("click", (ev) => this._clickDropdown(element, ev));
+                        button.classList.add("jsDropdownEnabled");
                         if (canEdit) {
-                            button.addEventListener('dblclick', this._click.bind(this, element));
+                            button.addEventListener("dblclick", (ev) => this._click(element, ev));
                         }
                     }
                     else if (canEdit) {
-                        button.addEventListener('click', this._click.bind(this, element));
+                        button.addEventListener("click", (ev) => this._click(element, ev));
                     }
                 }
-                var messageBody = elBySel('.messageBody', element);
-                var messageFooter = elBySel('.messageFooter', element);
-                var messageHeader = elBySel('.messageHeader', element);
+                const messageBody = element.querySelector(".messageBody");
+                const messageFooter = element.querySelector(".messageFooter");
+                const messageFooterButtons = messageFooter.querySelector(".messageFooterButtons");
+                const messageHeader = element.querySelector(".messageHeader");
+                const messageText = messageBody.querySelector(".messageText");
                 this._elements.set(element, {
-                    button: button,
-                    messageBody: messageBody,
+                    button,
+                    messageBody,
                     messageBodyEditor: null,
-                    messageFooter: messageFooter,
-                    messageFooterButtons: elBySel('.messageFooterButtons', messageFooter),
-                    messageHeader: messageHeader,
-                    messageText: elBySel('.messageText', messageBody)
+                    messageFooter,
+                    messageFooterButtons,
+                    messageHeader,
+                    messageText,
                 });
-            }
-        },
+            });
+        }
         /**
          * Handles clicks on the edit button or the edit dropdown item.
-         *
-         * @param      {Element}       element         message element
-         * @param      {?Event}        event           event object
-         * @protected
          */
-        _click: function (element, event) {
-            if (element === null)
+        _click(element, event) {
+            if (element === null) {
                 element = this._activeDropdownElement;
-            if (event)
+            }
+            if (event) {
                 event.preventDefault();
+            }
             if (this._activeElement === null) {
                 this._activeElement = element;
                 this._prepare();
                 Ajax.api(this, {
-                    actionName: 'beginEdit',
+                    actionName: "beginEdit",
                     parameters: {
                         containerID: this._options.containerId,
-                        objectID: this._getObjectId(element)
-                    }
+                        objectID: this._getObjectId(element),
+                    },
                 });
             }
             else {
-                UiNotification.show('wcf.message.error.editorAlreadyInUse', null, 'warning');
+                UiNotification.show("wcf.message.error.editorAlreadyInUse", undefined, "warning");
             }
-        },
+        }
         /**
          * Creates and opens the dropdown on first usage.
-         *
-         * @param      {Element}       element         message element
-         * @param      {Object}        event           event object
-         * @protected
          */
-        _clickDropdown: function (element, event) {
+        _clickDropdown(element, event) {
             event.preventDefault();
-            var button = event.currentTarget;
-            if (button.classList.contains('dropdownToggle')) {
+            const button = event.currentTarget;
+            if (button.classList.contains("dropdownToggle")) {
                 return;
             }
-            button.classList.add('dropdownToggle');
-            button.parentNode.classList.add('dropdown');
-            (function (button, element) {
-                button.addEventListener('click', (function (event) {
-                    event.preventDefault();
-                    event.stopPropagation();
-                    this._activeDropdownElement = element;
-                    UiReusableDropdown.toggleDropdown(this._options.dropdownIdentifier, button);
-                }).bind(this));
-            }).bind(this)(button, element);
+            button.classList.add("dropdownToggle");
+            button.parentElement.classList.add("dropdown");
+            button.addEventListener("click", (event) => {
+                event.preventDefault();
+                event.stopPropagation();
+                this._activeDropdownElement = element;
+                UiDropdownReusable.toggleDropdown(this._options.dropdownIdentifier, button);
+            });
             // build dropdown
             if (this._dropdownMenu === null) {
-                this._dropdownMenu = elCreate('ul');
-                this._dropdownMenu.className = 'dropdownMenu';
-                var items = this._dropdownGetItems();
-                EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownInit_' + this._options.dropdownIdentifier, {
-                    items: items
+                this._dropdownMenu = document.createElement("ul");
+                this._dropdownMenu.className = "dropdownMenu";
+                const items = this._dropdownGetItems();
+                EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownInit_${this._options.dropdownIdentifier}`, {
+                    items: items,
                 });
                 this._dropdownBuild(items);
-                UiReusableDropdown.init(this._options.dropdownIdentifier, this._dropdownMenu);
-                UiReusableDropdown.registerCallback(this._options.dropdownIdentifier, this._dropdownToggle.bind(this));
+                UiDropdownReusable.init(this._options.dropdownIdentifier, this._dropdownMenu);
+                UiDropdownReusable.registerCallback(this._options.dropdownIdentifier, (containerId, action) => this._dropdownToggle(containerId, action));
             }
-            setTimeout(function () {
-                Core.triggerEvent(button, 'click');
-            }, 10);
-        },
+            setTimeout(() => button.click(), 10);
+        }
         /**
          * Creates the dropdown menu on first usage.
-         *
-         * @param      {Object}        items   list of dropdown items
-         * @protected
          */
-        _dropdownBuild: function (items) {
-            var item, label, listItem;
-            var callbackClick = this._clickDropdownItem.bind(this);
-            for (var i = 0, length = items.length; i < length; i++) {
-                item = items[i];
-                listItem = elCreate('li');
-                elData(listItem, 'item', item.item);
-                if (item.item === 'divider') {
-                    listItem.className = 'dropdownDivider';
+        _dropdownBuild(items) {
+            items.forEach((item) => {
+                const listItem = document.createElement("li");
+                listItem.dataset.item = item.item;
+                if (item.item === "divider") {
+                    listItem.className = "dropdownDivider";
                 }
                 else {
-                    label = elCreate('span');
+                    const label = document.createElement("span");
                     label.textContent = Language.get(item.label);
                     listItem.appendChild(label);
-                    if (item.item === 'editItem') {
-                        listItem.addEventListener('click', this._click.bind(this, null));
+                    if (item.item === "editItem") {
+                        listItem.addEventListener("click", (ev) => this._click(null, ev));
                     }
                     else {
-                        listItem.addEventListener('click', callbackClick);
+                        listItem.addEventListener("click", (ev) => this._clickDropdownItem(ev));
                     }
                 }
                 this._dropdownMenu.appendChild(listItem);
-            }
-        },
+            });
+        }
         /**
          * Callback for dropdown toggle.
-         *
-         * @param      {int}           containerId     container id
-         * @param      {string}        action          toggle action, either 'open' or 'close'
-         * @protected
          */
-        _dropdownToggle: function (containerId, action) {
-            var elementData = this._elements.get(this._activeDropdownElement);
-            elementData.button.parentNode.classList[(action === 'open' ? 'add' : 'remove')]('dropdownOpen');
-            elementData.messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible');
-            if (action === 'open') {
-                var visibility = this._dropdownOpen();
-                EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownOpen_' + this._options.dropdownIdentifier, {
-                    element: this._activeDropdownElement,
-                    visibility: visibility
-                });
-                var item, listItem, visiblePredecessor = false;
-                for (var i = 0; i < this._dropdownMenu.childElementCount; i++) {
-                    listItem = this._dropdownMenu.children[i];
-                    item = elData(listItem, 'item');
-                    if (item === 'divider') {
-                        if (visiblePredecessor) {
-                            elShow(listItem);
-                            visiblePredecessor = false;
-                        }
-                        else {
-                            elHide(listItem);
-                        }
+        _dropdownToggle(containerId, action) {
+            const elementData = this._elements.get(this._activeDropdownElement);
+            const buttonParent = elementData.button.parentElement;
+            if (action === "close") {
+                buttonParent.classList.remove("dropdownOpen");
+                elementData.messageFooterButtons.classList.remove("forceVisible");
+                return;
+            }
+            buttonParent.classList.add("dropdownOpen");
+            elementData.messageFooterButtons.classList.add("forceVisible");
+            const visibility = new Map(Object.entries(this._dropdownOpen()));
+            EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownOpen_${this._options.dropdownIdentifier}`, {
+                element: this._activeDropdownElement,
+                visibility,
+            });
+            const dropdownMenu = this._dropdownMenu;
+            let visiblePredecessor = false;
+            const children = Array.from(dropdownMenu.children);
+            children.forEach((listItem, index) => {
+                const item = listItem.dataset.item;
+                if (item === "divider") {
+                    if (visiblePredecessor) {
+                        Util_1.default.show(listItem);
+                        visiblePredecessor = false;
                     }
                     else {
-                        if (objOwns(visibility, item) && visibility[item] === false) {
-                            elHide(listItem);
-                            // check if previous item was a divider
-                            if (i > 0 && i + 1 === this._dropdownMenu.childElementCount) {
-                                if (elData(listItem.previousElementSibling, 'item') === 'divider') {
-                                    elHide(listItem.previousElementSibling);
-                                }
+                        Util_1.default.hide(listItem);
+                    }
+                }
+                else {
+                    if (visibility.get(item) === false) {
+                        Util_1.default.hide(listItem);
+                        // check if previous item was a divider
+                        if (index > 0 && index + 1 === children.length) {
+                            const previousElementSibling = listItem.previousElementSibling;
+                            if (previousElementSibling.dataset.item === "divider") {
+                                Util_1.default.hide(previousElementSibling);
                             }
                         }
-                        else {
-                            elShow(listItem);
-                            visiblePredecessor = true;
-                        }
+                    }
+                    else {
+                        Util_1.default.show(listItem);
+                        visiblePredecessor = true;
                     }
                 }
-            }
-        },
+            });
+        }
         /**
          * Returns the list of dropdown items for this type.
-         *
-         * @return      {Array<Object>}         list of objects containing the type name and label
-         * @protected
          */
-        _dropdownGetItems: function () { },
+        _dropdownGetItems() {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+            return [];
+        }
         /**
          * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value
          * to represent the visibility of each item. Items that do not appear in this list will be considered
          * visible.
-         *
-         * @return      {Object<string, boolean>}
-         * @protected
          */
-        _dropdownOpen: function () { },
+        _dropdownOpen() {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+            return {};
+        }
         /**
          * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument.
-         *
-         * @param       {string}        item    selected dropdown item
-         * @protected
          */
-        _dropdownSelect: function (item) { },
+        _dropdownSelect(_item) {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+        }
         /**
          * Handles clicks on a dropdown item.
-         *
-         * @param      {Event}         event   event object
-         * @protected
          */
-        _clickDropdownItem: function (event) {
+        _clickDropdownItem(event) {
             event.preventDefault();
-            //noinspection JSCheckFunctionSignatures
-            var item = elData(event.currentTarget, 'item');
-            var data = {
+            const target = event.currentTarget;
+            const item = target.dataset.item;
+            const data = {
                 cancel: false,
                 element: this._activeDropdownElement,
-                item: item
+                item,
             };
-            EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownItemClick_' + this._options.dropdownIdentifier, data);
-            if (data.cancel === true) {
+            EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownItemClick_${this._options.dropdownIdentifier}`, data);
+            if (data.cancel) {
                 event.preventDefault();
             }
             else {
                 this._dropdownSelect(item);
             }
-        },
+        }
         /**
          * Prepares the message for editor display.
-         *
-         * @protected
          */
-        _prepare: function () {
-            var data = this._elements.get(this._activeElement);
-            var messageBodyEditor = elCreate('div');
-            messageBodyEditor.className = 'messageBody editor';
+        _prepare() {
+            const data = this._elements.get(this._activeElement);
+            const messageBodyEditor = document.createElement("div");
+            messageBodyEditor.className = "messageBody editor";
             data.messageBodyEditor = messageBodyEditor;
-            var icon = elCreate('span');
-            icon.className = 'icon icon48 fa-spinner';
+            const icon = document.createElement("span");
+            icon.className = "icon icon48 fa-spinner";
             messageBodyEditor.appendChild(icon);
-            DomUtil.insertAfter(messageBodyEditor, data.messageBody);
-            elHide(data.messageBody);
-        },
+            data.messageBody.insertAdjacentElement("afterend", messageBodyEditor);
+            Util_1.default.hide(data.messageBody);
+        }
         /**
          * Shows the message editor.
-         *
-         * @param      {Object}        data            ajax response data
-         * @protected
          */
-        _showEditor: function (data) {
-            var id = this._getEditorId();
-            var elementData = this._elements.get(this._activeElement);
-            this._activeElement.classList.add('jsInvalidQuoteTarget');
-            var icon = DomTraverse.childByClass(elementData.messageBodyEditor, 'icon');
-            elRemove(icon);
-            var messageBody = elementData.messageBodyEditor;
-            var editor = elCreate('div');
-            editor.className = 'editorContainer';
-            //noinspection JSUnresolvedVariable
-            DomUtil.setInnerHtml(editor, data.returnValues.template);
+        _showEditor(data) {
+            const id = this._getEditorId();
+            const activeElement = this._activeElement;
+            const elementData = this._elements.get(activeElement);
+            activeElement.classList.add("jsInvalidQuoteTarget");
+            const icon = elementData.messageBodyEditor.querySelector(".icon");
+            icon.remove();
+            const messageBody = elementData.messageBodyEditor;
+            const editor = document.createElement("div");
+            editor.className = "editorContainer";
+            Util_1.default.setInnerHtml(editor, data.returnValues.template);
             messageBody.appendChild(editor);
             // bind buttons
-            var formSubmit = elBySel('.formSubmit', editor);
-            var buttonSave = elBySel('button[data-type="save"]', formSubmit);
-            buttonSave.addEventListener('click', this._save.bind(this));
-            var buttonCancel = elBySel('button[data-type="cancel"]', formSubmit);
-            buttonCancel.addEventListener('click', this._restoreMessage.bind(this));
-            EventHandler.add('com.woltlab.wcf.redactor', 'submitEditor_' + id, (function (data) {
+            const formSubmit = editor.querySelector(".formSubmit");
+            const buttonSave = formSubmit.querySelector('button[data-type="save"]');
+            buttonSave.addEventListener("click", () => this._save());
+            const buttonCancel = formSubmit.querySelector('button[data-type="cancel"]');
+            buttonCancel.addEventListener("click", () => this._restoreMessage());
+            EventHandler.add("com.woltlab.wcf.redactor", `submitEditor_${id}`, (data) => {
                 data.cancel = true;
                 this._save();
-            }).bind(this));
+            });
             // hide message header and footer
-            elHide(elementData.messageHeader);
-            elHide(elementData.messageFooter);
-            var editorElement = elById(id);
-            if (Environment.editor() === 'redactor') {
-                window.setTimeout((function () {
+            Util_1.default.hide(elementData.messageHeader);
+            Util_1.default.hide(elementData.messageFooter);
+            if (Environment.editor() === "redactor") {
+                window.setTimeout(() => {
                     if (this._options.quoteManager) {
                         this._options.quoteManager.setAlternativeEditor(id);
                     }
-                    UiScroll.element(this._activeElement);
-                }).bind(this), 250);
+                    UiScroll.element(activeElement);
+                }, 250);
             }
             else {
+                const editorElement = document.getElementById(id);
                 editorElement.focus();
             }
-        },
+        }
         /**
          * Restores the message view.
-         *
-         * @protected
          */
-        _restoreMessage: function () {
-            var elementData = this._elements.get(this._activeElement);
+        _restoreMessage() {
+            const activeElement = this._activeElement;
+            const elementData = this._elements.get(activeElement);
             this._destroyEditor();
-            elRemove(elementData.messageBodyEditor);
+            elementData.messageBodyEditor.remove();
             elementData.messageBodyEditor = null;
-            elShow(elementData.messageBody);
-            elShow(elementData.messageFooter);
-            elShow(elementData.messageHeader);
-            this._activeElement.classList.remove('jsInvalidQuoteTarget');
+            Util_1.default.show(elementData.messageBody);
+            Util_1.default.show(elementData.messageFooter);
+            Util_1.default.show(elementData.messageHeader);
+            activeElement.classList.remove("jsInvalidQuoteTarget");
             this._activeElement = null;
             if (this._options.quoteManager) {
                 this._options.quoteManager.clearAlternativeEditor();
             }
-        },
+        }
         /**
          * Saves the editor message.
-         *
-         * @protected
          */
-        _save: function () {
-            var parameters = {
+        _save() {
+            const parameters = {
                 containerID: this._options.containerId,
                 data: {
-                    message: ''
+                    message: "",
                 },
                 objectID: this._getObjectId(this._activeElement),
-                removeQuoteIDs: (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : []
+                removeQuoteIDs: this._options.quoteManager ? this._options.quoteManager.getQuotesMarkedForRemoval() : [],
             };
-            var id = this._getEditorId();
+            const id = this._getEditorId();
             // add any available settings
-            var settingsContainer = elById('settings_' + id);
+            const settingsContainer = document.getElementById(`settings_${id}`);
             if (settingsContainer) {
-                elBySelAll('input, select, textarea', settingsContainer, function (element) {
-                    if (element.nodeName === 'INPUT' && (element.type === 'checkbox' || element.type === 'radio')) {
+                settingsContainer
+                    .querySelectorAll("input, select, textarea")
+                    .forEach((element) => {
+                    if (element.nodeName === "INPUT" && (element.type === "checkbox" || element.type === "radio")) {
                         if (!element.checked) {
                             return;
                         }
                     }
-                    var name = element.name;
-                    if (parameters.hasOwnProperty(name)) {
-                        throw new Error("Variable overshadowing, key '" + name + "' is already present.");
+                    const name = element.name;
+                    if (Object.prototype.hasOwnProperty.call(parameters, name)) {
+                        throw new Error(`Variable overshadowing, key '${name}' is already present.`);
                     }
                     parameters[name] = element.value.trim();
                 });
             }
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'getText_' + id, parameters.data);
-            var validateResult = this._validate(parameters);
+            EventHandler.fire("com.woltlab.wcf.redactor2", `getText_${id}`, parameters.data);
+            let validateResult = this._validate(parameters);
+            // Legacy validation methods returned a plain boolean.
             if (!(validateResult instanceof Promise)) {
                 if (validateResult === false) {
                     validateResult = Promise.reject();
@@ -422,212 +366,182 @@ define([
                     validateResult = Promise.resolve();
                 }
             }
-            validateResult.then(function () {
-                EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_' + id, parameters);
+            validateResult.then(() => {
+                EventHandler.fire("com.woltlab.wcf.redactor2", `submit_${id}`, parameters);
                 Ajax.api(this, {
-                    actionName: 'save',
-                    parameters: parameters
+                    actionName: "save",
+                    parameters: parameters,
                 });
                 this._hideEditor();
-            }.bind(this), function (e) {
-                console.log('Validation of post edit failed: ' + e);
+            }, (e) => {
+                const errorMessage = e.message;
+                console.log(`Validation of post edit failed: ${errorMessage}`);
             });
-        },
+        }
         /**
          * Validates the message and invokes listeners to perform additional validation.
-         *
-         * @param       {Object}        parameters      request parameters
-         * @return      {boolean}       validation result
-         * @protected
          */
-        _validate: function (parameters) {
+        _validate(parameters) {
             // remove all existing error elements
-            elBySelAll('.innerError', this._activeElement, elRemove);
-            var data = {
+            this._activeElement.querySelectorAll(".innerError").forEach((el) => el.remove());
+            const data = {
                 api: this,
                 parameters: parameters,
                 valid: true,
-                promises: []
+                promises: [],
             };
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_' + this._getEditorId(), data);
-            data.promises.push(Promise[data.valid ? 'resolve' : 'reject']());
+            EventHandler.fire("com.woltlab.wcf.redactor2", `validate_${this._getEditorId()}`, data);
+            if (data.valid) {
+                data.promises.push(Promise.resolve());
+            }
+            else {
+                data.promises.push(Promise.reject());
+            }
             return Promise.all(data.promises);
-        },
+        }
         /**
-         * Throws an error by adding an inline error to target element.
-         *
-         * @param       {Element}       element         erroneous element
-         * @param       {string}        message         error message
+         * Throws an error by showing an inline error for the target element.
          */
-        throwError: function (element, message) {
-            elInnerError(element, message);
-        },
+        throwError(element, message) {
+            Util_1.default.innerError(element, message);
+        }
         /**
          * Shows the update message.
-         *
-         * @param      {Object}        data            ajax response data
-         * @protected
          */
-        _showMessage: function (data) {
-            var activeElement = this._activeElement;
-            var editorId = this._getEditorId();
-            var elementData = this._elements.get(activeElement);
-            var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageFooter);
+        _showMessage(data) {
+            const activeElement = this._activeElement;
+            const editorId = this._getEditorId();
+            const elementData = this._elements.get(activeElement);
             // set new content
-            //noinspection JSUnresolvedVariable
-            DomUtil.setInnerHtml(DomTraverse.childByClass(elementData.messageBody, 'messageText'), data.returnValues.message);
+            Util_1.default.setInnerHtml(elementData.messageBody.querySelector(".messageText"), data.returnValues.message);
             // handle attachment list
-            //noinspection JSUnresolvedVariable
-            if (typeof data.returnValues.attachmentList === 'string') {
-                for (var i = 0, length = attachmentLists.length; i < length; i++) {
-                    elRemove(attachmentLists[i]);
-                }
-                var element = elCreate('div');
-                //noinspection JSUnresolvedVariable
-                DomUtil.setInnerHtml(element, data.returnValues.attachmentList);
-                var node;
+            if (typeof data.returnValues.attachmentList === "string") {
+                elementData.messageFooter
+                    .querySelectorAll(".attachmentThumbnailList, .attachmentFileList")
+                    .forEach((el) => el.remove());
+                const element = document.createElement("div");
+                Util_1.default.setInnerHtml(element, data.returnValues.attachmentList);
+                let node;
                 while (element.childNodes.length) {
                     node = element.childNodes[element.childNodes.length - 1];
                     elementData.messageFooter.insertBefore(node, elementData.messageFooter.firstChild);
                 }
             }
-            // handle poll
-            //noinspection JSUnresolvedVariable
-            if (typeof data.returnValues.poll === 'string') {
-                // find current poll
-                var poll = elBySel('.pollContainer', elementData.messageBody);
+            if (typeof data.returnValues.poll === "string") {
+                const poll = elementData.messageBody.querySelector(".pollContainer");
                 if (poll !== null) {
-                    // poll contain is wrapped inside `.jsInlineEditorHideContent`
-                    elRemove(poll.parentNode);
+                    // The poll container is wrapped inside `.jsInlineEditorHideContent`.
+                    poll.parentElement.remove();
                 }
-                var pollContainer = elCreate('div');
-                pollContainer.className = 'jsInlineEditorHideContent';
-                //noinspection JSUnresolvedVariable
-                DomUtil.setInnerHtml(pollContainer, data.returnValues.poll);
-                DomUtil.prepend(pollContainer, elementData.messageBody);
+                const pollContainer = document.createElement("div");
+                pollContainer.className = "jsInlineEditorHideContent";
+                Util_1.default.setInnerHtml(pollContainer, data.returnValues.poll);
+                elementData.messageBody.insertAdjacentElement("afterbegin", pollContainer);
             }
             this._restoreMessage();
             this._updateHistory(this._getHash(this._getObjectId(activeElement)));
-            EventHandler.fire('com.woltlab.wcf.redactor', 'autosaveDestroy_' + editorId);
+            EventHandler.fire("com.woltlab.wcf.redactor", `autosaveDestroy_${editorId}`);
             UiNotification.show();
             if (this._options.quoteManager) {
                 this._options.quoteManager.clearAlternativeEditor();
                 this._options.quoteManager.countQuotes();
             }
-        },
+        }
         /**
          * Hides the editor from view.
-         *
-         * @protected
          */
-        _hideEditor: function () {
-            var elementData = this._elements.get(this._activeElement);
-            elHide(DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer'));
-            var icon = elCreate('span');
-            icon.className = 'icon icon48 fa-spinner';
+        _hideEditor() {
+            const elementData = this._elements.get(this._activeElement);
+            const editorContainer = elementData.messageBodyEditor.querySelector(".editorContainer");
+            Util_1.default.hide(editorContainer);
+            const icon = document.createElement("span");
+            icon.className = "icon icon48 fa-spinner";
             elementData.messageBodyEditor.appendChild(icon);
-        },
+        }
         /**
          * Restores the previously hidden editor.
-         *
-         * @protected
          */
-        _restoreEditor: function () {
-            var elementData = this._elements.get(this._activeElement);
-            var icon = elBySel('.fa-spinner', elementData.messageBodyEditor);
-            elRemove(icon);
-            var editorContainer = DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer');
-            if (editorContainer !== null)
-                elShow(editorContainer);
-        },
+        _restoreEditor() {
+            const elementData = this._elements.get(this._activeElement);
+            const messageBodyEditor = elementData.messageBodyEditor;
+            const icon = messageBodyEditor.querySelector(".fa-spinner");
+            icon.remove();
+            const editorContainer = messageBodyEditor.querySelector(".editorContainer");
+            if (editorContainer !== null) {
+                Util_1.default.show(editorContainer);
+            }
+        }
         /**
          * Destroys the editor instance.
-         *
-         * @protected
          */
-        _destroyEditor: function () {
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this._getEditorId());
-            EventHandler.fire('com.woltlab.wcf.redactor2', 'destroy_' + this._getEditorId());
-        },
+        _destroyEditor() {
+            EventHandler.fire("com.woltlab.wcf.redactor2", `autosaveDestroy_${this._getEditorId()}`);
+            EventHandler.fire("com.woltlab.wcf.redactor2", `destroy_${this._getEditorId()}`);
+        }
         /**
          * Returns the hash added to the url after successfully editing a message.
-         *
-         * @param      {int}   objectId        message object id
-         * @return     string
-         * @protected
          */
-        _getHash: function (objectId) {
-            return '#message' + objectId;
-        },
+        _getHash(objectId) {
+            return `#message${objectId}`;
+        }
         /**
          * Updates the history to avoid old content when going back in the browser
          * history.
-         *
-         * @param      {string}        hash    location hash
-         * @protected
          */
-        _updateHistory: function (hash) {
+        _updateHistory(hash) {
             window.location.hash = hash;
-        },
+        }
         /**
          * Returns the unique editor id.
-         *
-         * @return     {string}        editor id
-         * @protected
          */
-        _getEditorId: function () {
-            return this._options.editorPrefix + this._getObjectId(this._activeElement);
-        },
+        _getEditorId() {
+            return this._options.editorPrefix + this._getObjectId(this._activeElement).toString();
+        }
         /**
          * Returns the element's `data-object-id` value.
-         *
-         * @param      {Element}       element         target element
-         * @return     {int}
-         * @protected
          */
-        _getObjectId: function (element) {
-            return ~~elData(element, 'object-id');
-        },
-        _ajaxFailure: function (data) {
-            var elementData = this._elements.get(this._activeElement);
-            var editor = elBySel('.redactor-layer', elementData.messageBodyEditor);
+        _getObjectId(element) {
+            return ~~(element.dataset.objectId || "");
+        }
+        _ajaxFailure(data) {
+            const elementData = this._elements.get(this._activeElement);
+            const editor = elementData.messageBodyEditor.querySelector(".redactor-layer");
             // handle errors occurring on editor load
             if (editor === null) {
                 this._restoreMessage();
                 return true;
             }
             this._restoreEditor();
-            //noinspection JSUnresolvedVariable
             if (!data || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) {
                 return true;
             }
-            //noinspection JSUnresolvedVariable
-            elInnerError(editor, data.returnValues.realErrorMessage);
+            Util_1.default.innerError(editor, data.returnValues.realErrorMessage);
             return false;
-        },
-        _ajaxSuccess: function (data) {
+        }
+        _ajaxSuccess(data) {
             switch (data.actionName) {
-                case 'beginEdit':
+                case "beginEdit":
                     this._showEditor(data);
                     break;
-                case 'save':
+                case "save":
                     this._showMessage(data);
                     break;
             }
-        },
-        _ajaxSetup: function () {
+        }
+        _ajaxSetup() {
             return {
                 data: {
                     className: this._options.className,
-                    interfaceName: 'wcf\\data\\IMessageInlineEditorAction'
+                    interfaceName: "wcf\\data\\IMessageInlineEditorAction",
                 },
-                silent: true
+                silent: true,
             };
-        },
-        /** @deprecated        3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
-        legacyEdit: function (containerId) {
-            this._click(elById(containerId), null);
         }
-    };
+        /** @deprecated  3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
+        legacyEdit(containerId) {
+            this._click(document.getElementById(containerId), null);
+        }
+    }
+    Core.enableLegacyInheritance(UiMessageInlineEditor);
     return UiMessageInlineEditor;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.js
deleted file mode 100644 (file)
index 017e49a..0000000
+++ /dev/null
@@ -1,757 +0,0 @@
-/**
- * Flexible message inline editor.
- * 
- * @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/Message/InlineEditor
- */
-define(
-       [
-               'Ajax',         'Core',            'Dictionary',          'Environment',
-               'EventHandler', 'Language',        'ObjectMap',           'Dom/ChangeListener', 'Dom/Traverse',
-               'Dom/Util',     'Ui/Notification', 'Ui/ReusableDropdown', 'WoltLabSuite/Core/Ui/Scroll'
-       ],
-       function(
-               Ajax,            Core,              Dictionary,            Environment,
-               EventHandler,    Language,          ObjectMap,             DomChangeListener,    DomTraverse,
-               DomUtil,         UiNotification,    UiReusableDropdown,    UiScroll
-       )
-{
-       "use strict";
-       
-       if (!COMPILER_TARGET_DEFAULT) {
-               var Fake = function() {};
-               Fake.prototype = {
-                       init: function() {},
-                       rebuild: function() {},
-                       _click: function() {},
-                       _clickDropdown: function() {},
-                       _dropdownBuild: function() {},
-                       _dropdownToggle: function() {},
-                       _dropdownGetItems: function() {},
-                       _dropdownOpen: function() {},
-                       _dropdownSelect: function() {},
-                       _clickDropdownItem: function() {},
-                       _prepare: function() {},
-                       _showEditor: function() {},
-                       _restoreMessage: function() {},
-                       _save: function() {},
-                       _validate: function() {},
-                       throwError: function() {},
-                       _showMessage: function() {},
-                       _hideEditor: function() {},
-                       _restoreEditor: function() {},
-                       _destroyEditor: function() {},
-                       _getHash: function() {},
-                       _updateHistory: function() {},
-                       _getEditorId: function() {},
-                       _getObjectId: function() {},
-                       _ajaxFailure: function() {},
-                       _ajaxSuccess: function() {},
-                       _ajaxSetup: function() {},
-                       legacyEdit: function() {}
-               };
-               return Fake;
-       }
-       
-       /**
-        * @constructor
-        */
-       function UiMessageInlineEditor(options) { this.init(options); }
-       UiMessageInlineEditor.prototype = {
-               /**
-                * Initializes the message inline editor.
-                * 
-                * @param       {Object}        options         list of configuration options
-                */
-               init: function(options) {
-                       this._activeDropdownElement = null;
-                       this._activeElement = null;
-                       this._dropdownMenu = null;
-                       this._elements = new ObjectMap();
-                       this._options = Core.extend({
-                               canEditInline: false,
-                               
-                               className: '',
-                               containerId: 0,
-                               dropdownIdentifier: '',
-                               editorPrefix: 'messageEditor',
-                               
-                               messageSelector: '.jsMessage',
-                               
-                               quoteManager: null
-                       }, options);
-                       
-                       this.rebuild();
-                       
-                       DomChangeListener.add('Ui/Message/InlineEdit_' + this._options.className, this.rebuild.bind(this));
-               },
-               
-               /**
-                * Initializes each applicable message, should be called whenever new
-                * messages are being displayed.
-                */
-               rebuild: function() {
-                       var button, canEdit, element, elements = elBySelAll(this._options.messageSelector);
-                       
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               element = elements[i];
-                               if (this._elements.has(element)) {
-                                       continue;
-                               }
-                               
-                               button = elBySel('.jsMessageEditButton', element);
-                               if (button !== null) {
-                                       canEdit = elDataBool(element, 'can-edit');
-                                       
-                                       if (this._options.canEditInline || elDataBool(element, 'can-edit-inline')) {
-                                               button.addEventListener('click', this._clickDropdown.bind(this, element));
-                                               button.classList.add('jsDropdownEnabled');
-                                               
-                                               if (canEdit) {
-                                                       button.addEventListener('dblclick', this._click.bind(this, element));
-                                               }
-                                       }
-                                       else if (canEdit) {
-                                               button.addEventListener('click', this._click.bind(this, element));
-                                       }
-                               }
-                               
-                               var messageBody = elBySel('.messageBody', element);
-                               var messageFooter = elBySel('.messageFooter', element);
-                               var messageHeader = elBySel('.messageHeader', element);
-                               
-                               this._elements.set(element, {
-                                       button: button,
-                                       messageBody: messageBody,
-                                       messageBodyEditor: null,
-                                       messageFooter: messageFooter,
-                                       messageFooterButtons: elBySel('.messageFooterButtons', messageFooter),
-                                       messageHeader: messageHeader,
-                                       messageText: elBySel('.messageText', messageBody)
-                               });
-                       }
-               },
-               
-               /**
-                * Handles clicks on the edit button or the edit dropdown item.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {?Event}        event           event object
-                * @protected
-                */
-               _click: function(element, event) {
-                       if (element === null) element = this._activeDropdownElement;
-                       if (event) event.preventDefault();
-                       
-                       if (this._activeElement === null) {
-                               this._activeElement = element;
-                               
-                               this._prepare();
-                               
-                               Ajax.api(this, {
-                                       actionName: 'beginEdit',
-                                       parameters: {
-                                               containerID: this._options.containerId,
-                                               objectID: this._getObjectId(element)
-                                       }
-                               });
-                       }
-                       else {
-                               UiNotification.show('wcf.message.error.editorAlreadyInUse', null, 'warning');
-                       }
-               },
-               
-               /**
-                * Creates and opens the dropdown on first usage.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {Object}        event           event object
-                * @protected
-                */
-               _clickDropdown: function(element, event) {
-                       event.preventDefault();
-                       
-                       var button = event.currentTarget;
-                       if (button.classList.contains('dropdownToggle')) {
-                               return;
-                       }
-                       
-                       button.classList.add('dropdownToggle');
-                       button.parentNode.classList.add('dropdown');
-                       (function(button, element) {
-                               button.addEventListener('click', (function(event) {
-                                       event.preventDefault();
-                                       event.stopPropagation();
-                                       
-                                       this._activeDropdownElement = element;
-                                       UiReusableDropdown.toggleDropdown(this._options.dropdownIdentifier, button);
-                               }).bind(this));
-                       }).bind(this)(button, element);
-                       
-                       // build dropdown
-                       if (this._dropdownMenu === null) {
-                               this._dropdownMenu = elCreate('ul');
-                               this._dropdownMenu.className = 'dropdownMenu';
-                               
-                               var items = this._dropdownGetItems();
-                               
-                               EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownInit_' + this._options.dropdownIdentifier, {
-                                       items: items
-                               });
-                               
-                               this._dropdownBuild(items);
-                               
-                               UiReusableDropdown.init(this._options.dropdownIdentifier, this._dropdownMenu);
-                               UiReusableDropdown.registerCallback(this._options.dropdownIdentifier, this._dropdownToggle.bind(this));
-                       }
-                       
-                       setTimeout(function() {
-                               Core.triggerEvent(button, 'click');
-                       }, 10);
-               },
-               
-               /**
-                * Creates the dropdown menu on first usage.
-                * 
-                * @param       {Object}        items   list of dropdown items
-                * @protected
-                */
-               _dropdownBuild: function(items) {
-                       var item, label, listItem;
-                       var callbackClick = this._clickDropdownItem.bind(this);
-                       
-                       for (var i = 0, length = items.length; i < length; i++) {
-                               item = items[i];
-                               listItem = elCreate('li');
-                               elData(listItem, 'item', item.item);
-                               
-                               if (item.item === 'divider') {
-                                       listItem.className = 'dropdownDivider';
-                               }
-                               else {
-                                       label = elCreate('span');
-                                       label.textContent = Language.get(item.label);
-                                       listItem.appendChild(label);
-                                       
-                                       if (item.item === 'editItem') {
-                                               listItem.addEventListener('click', this._click.bind(this, null));
-                                       }
-                                       else {
-                                               listItem.addEventListener('click', callbackClick);
-                                       }
-                               }
-                               
-                               this._dropdownMenu.appendChild(listItem);
-                       }
-               },
-               
-               /**
-                * Callback for dropdown toggle.
-                * 
-                * @param       {int}           containerId     container id
-                * @param       {string}        action          toggle action, either 'open' or 'close'
-                * @protected
-                */
-               _dropdownToggle: function(containerId, action) {
-                       var elementData = this._elements.get(this._activeDropdownElement);
-                       elementData.button.parentNode.classList[(action === 'open' ? 'add' : 'remove')]('dropdownOpen');
-                       elementData.messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible');
-                       
-                       if (action === 'open') {
-                               var visibility = this._dropdownOpen();
-                               
-                               EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownOpen_' + this._options.dropdownIdentifier, {
-                                       element: this._activeDropdownElement,
-                                       visibility: visibility
-                               });
-                               
-                               var item, listItem, visiblePredecessor = false;
-                               for (var i = 0; i < this._dropdownMenu.childElementCount; i++) {
-                                       listItem = this._dropdownMenu.children[i];
-                                       item = elData(listItem, 'item');
-                                       
-                                       if (item === 'divider') {
-                                               if (visiblePredecessor) {
-                                                       elShow(listItem);
-                                                       
-                                                       visiblePredecessor = false;
-                                               }
-                                               else {
-                                                       elHide(listItem);
-                                               }
-                                       }
-                                       else {
-                                               if (objOwns(visibility, item) && visibility[item] === false) {
-                                                       elHide(listItem);
-                                                       
-                                                       // check if previous item was a divider
-                                                       if (i > 0 && i + 1 === this._dropdownMenu.childElementCount) {
-                                                               if (elData(listItem.previousElementSibling, 'item') === 'divider') {
-                                                                       elHide(listItem.previousElementSibling);
-                                                               }
-                                                       }
-                                               }
-                                               else {
-                                                       elShow(listItem);
-                                                       
-                                                       visiblePredecessor = true;
-                                               }
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Returns the list of dropdown items for this type.
-                * 
-                * @return      {Array<Object>}         list of objects containing the type name and label
-                * @protected
-                */
-               _dropdownGetItems: function() {},
-               
-               /**
-                * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value
-                * to represent the visibility of each item. Items that do not appear in this list will be considered
-                * visible.
-                * 
-                * @return      {Object<string, boolean>}
-                * @protected
-                */
-               _dropdownOpen: function() {},
-               
-               /**
-                * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument.
-                * 
-                * @param       {string}        item    selected dropdown item
-                * @protected
-                */
-               _dropdownSelect: function(item) {},
-               
-               /**
-                * Handles clicks on a dropdown item.
-                * 
-                * @param       {Event}         event   event object
-                * @protected
-                */
-               _clickDropdownItem: function(event) {
-                       event.preventDefault();
-                       
-                       //noinspection JSCheckFunctionSignatures
-                       var item = elData(event.currentTarget, 'item');
-                       var data = {
-                               cancel: false,
-                               element: this._activeDropdownElement,
-                               item: item
-                       };
-                       EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownItemClick_' + this._options.dropdownIdentifier, data);
-                       
-                       if (data.cancel === true) {
-                               event.preventDefault();
-                       }
-                       else {
-                               this._dropdownSelect(item);
-                       }
-               },
-               
-               /**
-                * Prepares the message for editor display.
-                * 
-                * @protected
-                */
-               _prepare: function() {
-                       var data = this._elements.get(this._activeElement);
-                       
-                       var messageBodyEditor = elCreate('div');
-                       messageBodyEditor.className = 'messageBody editor';
-                       data.messageBodyEditor = messageBodyEditor;
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon48 fa-spinner';
-                       messageBodyEditor.appendChild(icon);
-                       
-                       DomUtil.insertAfter(messageBodyEditor, data.messageBody);
-                       
-                       elHide(data.messageBody);
-               },
-               
-               /**
-                * Shows the message editor.
-                * 
-                * @param       {Object}        data            ajax response data
-                * @protected
-                */
-               _showEditor: function(data) {
-                       var id = this._getEditorId();
-                       var elementData = this._elements.get(this._activeElement);
-                       
-                       this._activeElement.classList.add('jsInvalidQuoteTarget');
-                       var icon = DomTraverse.childByClass(elementData.messageBodyEditor, 'icon');
-                       elRemove(icon);
-                       
-                       var messageBody = elementData.messageBodyEditor;
-                       var editor = elCreate('div');
-                       editor.className = 'editorContainer';
-                       //noinspection JSUnresolvedVariable
-                       DomUtil.setInnerHtml(editor, data.returnValues.template);
-                       messageBody.appendChild(editor);
-                       
-                       // bind buttons
-                       var formSubmit = elBySel('.formSubmit', editor);
-                       
-                       var buttonSave = elBySel('button[data-type="save"]', formSubmit);
-                       buttonSave.addEventListener('click', this._save.bind(this));
-                       
-                       var buttonCancel = elBySel('button[data-type="cancel"]', formSubmit);
-                       buttonCancel.addEventListener('click', this._restoreMessage.bind(this));
-                       
-                       EventHandler.add('com.woltlab.wcf.redactor', 'submitEditor_' + id, (function(data) {
-                               data.cancel = true;
-                               
-                               this._save();
-                       }).bind(this));
-                       
-                       // hide message header and footer
-                       elHide(elementData.messageHeader);
-                       elHide(elementData.messageFooter);
-                       
-                       var editorElement = elById(id);
-                       if (Environment.editor() === 'redactor') {
-                               window.setTimeout((function() {
-                                       if (this._options.quoteManager) {
-                                               this._options.quoteManager.setAlternativeEditor(id);
-                                       }
-                                       
-                                       UiScroll.element(this._activeElement);
-                               }).bind(this), 250);
-                       }
-                       else {
-                               editorElement.focus();
-                       }
-               },
-               
-               /**
-                * Restores the message view.
-                * 
-                * @protected
-                */
-               _restoreMessage: function() {
-                       var elementData = this._elements.get(this._activeElement);
-                       
-                       this._destroyEditor();
-                       
-                       elRemove(elementData.messageBodyEditor);
-                       elementData.messageBodyEditor = null;
-                       
-                       elShow(elementData.messageBody);
-                       elShow(elementData.messageFooter);
-                       elShow(elementData.messageHeader);
-                       this._activeElement.classList.remove('jsInvalidQuoteTarget');
-                       
-                       this._activeElement = null;
-                       
-                       if (this._options.quoteManager) {
-                               this._options.quoteManager.clearAlternativeEditor();
-                       }
-               },
-               
-               /**
-                * Saves the editor message.
-                * 
-                * @protected
-                */
-               _save: function() {
-                       var parameters = {
-                               containerID: this._options.containerId,
-                               data: {
-                                       message: ''
-                               },
-                               objectID: this._getObjectId(this._activeElement),
-                               removeQuoteIDs: (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : []
-                       };
-                       
-                       var id = this._getEditorId();
-                       
-                       // add any available settings
-                       var settingsContainer = elById('settings_' + id);
-                       if (settingsContainer) {
-                               elBySelAll('input, select, textarea', settingsContainer, function (element) {
-                                       if (element.nodeName === 'INPUT' && (element.type === 'checkbox' || element.type === 'radio')) {
-                                               if (!element.checked) {
-                                                       return;
-                                               }
-                                       }
-                                       
-                                       var name = element.name;
-                                       if (parameters.hasOwnProperty(name)) {
-                                               throw new Error("Variable overshadowing, key '" + name + "' is already present.");
-                                       }
-                                       
-                                       parameters[name] = element.value.trim();
-                               });
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'getText_' + id, parameters.data);
-                       
-                       var validateResult = this._validate(parameters);
-                       
-                       if (!(validateResult instanceof Promise)) {
-                               if (validateResult === false) {
-                                       validateResult = Promise.reject();
-                               }
-                               else {
-                                       validateResult = Promise.resolve();
-                               }
-                       }
-                       
-                       validateResult.then(function () {
-                               EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_' + id, parameters);
-                               
-                               Ajax.api(this, {
-                                       actionName: 'save',
-                                       parameters: parameters
-                               });
-                               
-                               this._hideEditor();
-                       }.bind(this), function(e) {
-                               console.log('Validation of post edit failed: '+ e);
-                       });
-               },
-               
-               /**
-                * Validates the message and invokes listeners to perform additional validation.
-                *
-                * @param       {Object}        parameters      request parameters
-                * @return      {boolean}       validation result
-                * @protected
-                */
-               _validate: function(parameters) {
-                       // remove all existing error elements
-                       elBySelAll('.innerError', this._activeElement, elRemove);
-                       
-                       var data = {
-                               api: this,
-                               parameters: parameters,
-                               valid: true,
-                               promises: []
-                       };
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_' + this._getEditorId(), data);
-                       
-                       data.promises.push(Promise[data.valid ? 'resolve' : 'reject']());
-                       
-                       return Promise.all(data.promises);
-               },
-               
-               /**
-                * 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);
-               },
-               
-               /**
-                * Shows the update message.
-                * 
-                * @param       {Object}        data            ajax response data
-                * @protected
-                */
-               _showMessage: function(data) {
-                       var activeElement = this._activeElement;
-                       var editorId = this._getEditorId();
-                       var elementData = this._elements.get(activeElement);
-                       var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageFooter);
-                       
-                       // set new content
-                       //noinspection JSUnresolvedVariable
-                       DomUtil.setInnerHtml(DomTraverse.childByClass(elementData.messageBody, 'messageText'), data.returnValues.message);
-                       
-                       // handle attachment list
-                       //noinspection JSUnresolvedVariable
-                       if (typeof data.returnValues.attachmentList === 'string') {
-                               for (var i = 0, length = attachmentLists.length; i < length; i++) {
-                                       elRemove(attachmentLists[i]);
-                               }
-                               
-                               var element = elCreate('div');
-                               //noinspection JSUnresolvedVariable
-                               DomUtil.setInnerHtml(element, data.returnValues.attachmentList);
-                               
-                               var node;
-                               while (element.childNodes.length) {
-                                       node = element.childNodes[element.childNodes.length - 1];
-                                       elementData.messageFooter.insertBefore(node, elementData.messageFooter.firstChild);
-                               }
-                       }
-                       
-                       // handle poll
-                       //noinspection JSUnresolvedVariable
-                       if (typeof data.returnValues.poll === 'string') {
-                               // find current poll
-                               var poll = elBySel('.pollContainer', elementData.messageBody);
-                               if (poll !== null) {
-                                       // poll contain is wrapped inside `.jsInlineEditorHideContent`
-                                       elRemove(poll.parentNode);
-                               }
-                               
-                               var pollContainer = elCreate('div');
-                               pollContainer.className = 'jsInlineEditorHideContent';
-                               //noinspection JSUnresolvedVariable
-                               DomUtil.setInnerHtml(pollContainer, data.returnValues.poll);
-                               
-                               DomUtil.prepend(pollContainer, elementData.messageBody);
-                       }
-                       
-                       this._restoreMessage();
-                       
-                       this._updateHistory(this._getHash(this._getObjectId(activeElement)));
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor', 'autosaveDestroy_' + editorId);
-                       
-                       UiNotification.show();
-                       
-                       if (this._options.quoteManager) {
-                               this._options.quoteManager.clearAlternativeEditor();
-                               this._options.quoteManager.countQuotes();
-                       }
-               },
-               
-               /**
-                * Hides the editor from view.
-                * 
-                * @protected
-                */
-               _hideEditor: function() {
-                       var elementData = this._elements.get(this._activeElement);
-                       elHide(DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer'));
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon48 fa-spinner';
-                       elementData.messageBodyEditor.appendChild(icon);
-               },
-               
-               /**
-                * Restores the previously hidden editor.
-                * 
-                * @protected
-                */
-               _restoreEditor: function() {
-                       var elementData = this._elements.get(this._activeElement);
-                       var icon = elBySel('.fa-spinner', elementData.messageBodyEditor);
-                       elRemove(icon);
-                       
-                       var editorContainer = DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer');
-                       if (editorContainer !== null) elShow(editorContainer);
-               },
-               
-               /**
-                * Destroys the editor instance.
-                * 
-                * @protected
-                */
-               _destroyEditor: function() {
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this._getEditorId());
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'destroy_' + this._getEditorId());
-               },
-               
-               /**
-                * Returns the hash added to the url after successfully editing a message.
-                * 
-                * @param       {int}   objectId        message object id
-                * @return      string
-                * @protected
-                */
-               _getHash: function(objectId) {
-                       return '#message' + objectId;
-               },
-               
-               /**
-                * Updates the history to avoid old content when going back in the browser
-                * history.
-                * 
-                * @param       {string}        hash    location hash
-                * @protected
-                */
-               _updateHistory: function(hash) {
-                       window.location.hash = hash;
-               },
-               
-               /**
-                * Returns the unique editor id.
-                * 
-                * @return      {string}        editor id
-                * @protected
-                */
-               _getEditorId: function() {
-                       return this._options.editorPrefix + this._getObjectId(this._activeElement);
-               },
-               
-               /**
-                * Returns the element's `data-object-id` value.
-                * 
-                * @param       {Element}       element         target element
-                * @return      {int}
-                * @protected
-                */
-               _getObjectId: function(element) {
-                       return ~~elData(element, 'object-id');
-               },
-               
-               _ajaxFailure: function(data) {
-                       var elementData = this._elements.get(this._activeElement);
-                       var editor = elBySel('.redactor-layer', elementData.messageBodyEditor);
-                       
-                       // handle errors occurring on editor load
-                       if (editor === null) {
-                               this._restoreMessage();
-                               
-                               return true;
-                       }
-                       
-                       this._restoreEditor();
-                       
-                       //noinspection JSUnresolvedVariable
-                       if (!data || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) {
-                               return true;
-                       }
-                       
-                       //noinspection JSUnresolvedVariable
-                       elInnerError(editor, data.returnValues.realErrorMessage);
-                       
-                       return false;
-               },
-               
-               _ajaxSuccess: function(data) {
-                       switch (data.actionName) {
-                               case 'beginEdit':
-                                       this._showEditor(data);
-                                       break;
-                                       
-                               case 'save':
-                                       this._showMessage(data);
-                                       break;
-                       }
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: this._options.className,
-                                       interfaceName: 'wcf\\data\\IMessageInlineEditorAction'
-                               },
-                               silent: true
-                       };
-               },
-               
-               /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
-               legacyEdit: function(containerId) {
-                       this._click(elById(containerId), null);
-               }
-       };
-       
-       return UiMessageInlineEditor;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.ts
new file mode 100644 (file)
index 0000000..af75d13
--- /dev/null
@@ -0,0 +1,723 @@
+/**
+ * Flexible message inline editor.
+ *
+ * @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/Message/InlineEditor
+ */
+
+import * as Ajax from "../../Ajax";
+import { AjaxCallbackObject, AjaxCallbackSetup, ResponseData } from "../../Ajax/Data";
+import * as Core from "../../Core";
+import DomChangeListener from "../../Dom/Change/Listener";
+import DomUtil from "../../Dom/Util";
+import * as Environment from "../../Environment";
+import * as EventHandler from "../../Event/Handler";
+import * as Language from "../../Language";
+import { NotificationAction } from "../Dropdown/Data";
+import * as UiDropdownReusable from "../Dropdown/Reusable";
+import * as UiNotification from "../Notification";
+import * as UiScroll from "../Scroll";
+
+interface MessageInlineEditorOptions {
+  canEditInline: boolean;
+
+  className: string;
+  containerId: number;
+  dropdownIdentifier: string;
+  editorPrefix: string;
+
+  messageSelector: string;
+
+  // This is the legacy jQuery based class.
+  quoteManager: any;
+}
+
+interface ElementData {
+  button: HTMLAnchorElement;
+  messageBody: HTMLElement;
+  messageBodyEditor: HTMLElement | null;
+  messageFooter: HTMLElement;
+  messageFooterButtons: HTMLUListElement;
+  messageHeader: HTMLElement;
+  messageText: HTMLElement;
+}
+
+interface ItemData {
+  item: "divider" | "editItem" | string;
+  label?: string;
+}
+
+interface ElementVisibility {
+  [key: string]: boolean;
+}
+
+interface ValidationData {
+  api: UiMessageInlineEditor;
+  parameters: ArbitraryObject;
+  valid: boolean;
+  promises: Promise<void>[];
+}
+
+interface AjaxResponseEditor extends ResponseData {
+  returnValues: {
+    template: string;
+  };
+}
+
+interface AjaxResponseMessage extends ResponseData {
+  returnValues: {
+    attachmentList?: string;
+    message: string;
+    poll?: string;
+  };
+}
+
+class UiMessageInlineEditor implements AjaxCallbackObject {
+  protected _activeDropdownElement: HTMLElement | null = null;
+  protected _activeElement: HTMLElement | null = null;
+  protected _dropdownMenu: HTMLUListElement | null = null;
+  protected readonly _elements = new WeakMap<HTMLElement, ElementData>();
+  protected readonly _options: MessageInlineEditorOptions;
+
+  /**
+   * Initializes the message inline editor.
+   */
+  constructor(opts: Partial<MessageInlineEditorOptions>) {
+    this._options = Core.extend(
+      {
+        canEditInline: false,
+
+        className: "",
+        containerId: 0,
+        dropdownIdentifier: "",
+        editorPrefix: "messageEditor",
+
+        messageSelector: ".jsMessage",
+
+        quoteManager: null,
+      },
+      opts,
+    ) as MessageInlineEditorOptions;
+
+    this.rebuild();
+
+    DomChangeListener.add(`Ui/Message/InlineEdit_${this._options.className}`, () => this.rebuild());
+  }
+
+  /**
+   * Initializes each applicable message, should be called whenever new
+   * messages are being displayed.
+   */
+  rebuild(): void {
+    document.querySelectorAll(this._options.messageSelector).forEach((element: HTMLElement) => {
+      if (this._elements.has(element)) {
+        return;
+      }
+
+      const button = element.querySelector(".jsMessageEditButton") as HTMLAnchorElement;
+      if (button !== null) {
+        const canEdit = Core.stringToBool(element.dataset.canEdit || "");
+        const canEditInline = Core.stringToBool(element.dataset.canEditInline || "");
+
+        if (this._options.canEditInline || canEditInline) {
+          button.addEventListener("click", (ev) => this._clickDropdown(element, ev));
+          button.classList.add("jsDropdownEnabled");
+
+          if (canEdit) {
+            button.addEventListener("dblclick", (ev) => this._click(element, ev));
+          }
+        } else if (canEdit) {
+          button.addEventListener("click", (ev) => this._click(element, ev));
+        }
+      }
+
+      const messageBody = element.querySelector(".messageBody") as HTMLElement;
+      const messageFooter = element.querySelector(".messageFooter") as HTMLElement;
+      const messageFooterButtons = messageFooter.querySelector(".messageFooterButtons") as HTMLUListElement;
+      const messageHeader = element.querySelector(".messageHeader") as HTMLElement;
+      const messageText = messageBody.querySelector(".messageText") as HTMLElement;
+
+      this._elements.set(element, {
+        button,
+        messageBody,
+        messageBodyEditor: null,
+        messageFooter,
+        messageFooterButtons,
+        messageHeader,
+        messageText,
+      });
+    });
+  }
+
+  /**
+   * Handles clicks on the edit button or the edit dropdown item.
+   */
+  protected _click(element: HTMLElement | null, event: MouseEvent | null): void {
+    if (element === null) {
+      element = this._activeDropdownElement;
+    }
+    if (event) {
+      event.preventDefault();
+    }
+
+    if (this._activeElement === null) {
+      this._activeElement = element;
+
+      this._prepare();
+
+      Ajax.api(this, {
+        actionName: "beginEdit",
+        parameters: {
+          containerID: this._options.containerId,
+          objectID: this._getObjectId(element!),
+        },
+      });
+    } else {
+      UiNotification.show("wcf.message.error.editorAlreadyInUse", undefined, "warning");
+    }
+  }
+
+  /**
+   * Creates and opens the dropdown on first usage.
+   */
+  protected _clickDropdown(element: HTMLElement, event: MouseEvent): void {
+    event.preventDefault();
+
+    const button = event.currentTarget as HTMLElement;
+    if (button.classList.contains("dropdownToggle")) {
+      return;
+    }
+
+    button.classList.add("dropdownToggle");
+    button.parentElement!.classList.add("dropdown");
+    button.addEventListener("click", (event) => {
+      event.preventDefault();
+      event.stopPropagation();
+
+      this._activeDropdownElement = element;
+      UiDropdownReusable.toggleDropdown(this._options.dropdownIdentifier, button);
+    });
+
+    // build dropdown
+    if (this._dropdownMenu === null) {
+      this._dropdownMenu = document.createElement("ul");
+      this._dropdownMenu.className = "dropdownMenu";
+
+      const items = this._dropdownGetItems();
+
+      EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownInit_${this._options.dropdownIdentifier}`, {
+        items: items,
+      });
+
+      this._dropdownBuild(items);
+
+      UiDropdownReusable.init(this._options.dropdownIdentifier, this._dropdownMenu);
+      UiDropdownReusable.registerCallback(this._options.dropdownIdentifier, (containerId, action) =>
+        this._dropdownToggle(containerId, action),
+      );
+    }
+
+    setTimeout(() => button.click(), 10);
+  }
+
+  /**
+   * Creates the dropdown menu on first usage.
+   */
+  protected _dropdownBuild(items: ItemData[]): void {
+    items.forEach((item) => {
+      const listItem = document.createElement("li");
+      listItem.dataset.item = item.item;
+
+      if (item.item === "divider") {
+        listItem.className = "dropdownDivider";
+      } else {
+        const label = document.createElement("span");
+        label.textContent = Language.get(item.label!);
+        listItem.appendChild(label);
+
+        if (item.item === "editItem") {
+          listItem.addEventListener("click", (ev) => this._click(null, ev));
+        } else {
+          listItem.addEventListener("click", (ev) => this._clickDropdownItem(ev));
+        }
+      }
+
+      this._dropdownMenu!.appendChild(listItem);
+    });
+  }
+
+  /**
+   * Callback for dropdown toggle.
+   */
+  protected _dropdownToggle(containerId: string, action: NotificationAction): void {
+    const elementData = this._elements.get(this._activeDropdownElement!)!;
+    const buttonParent = elementData.button.parentElement!;
+
+    if (action === "close") {
+      buttonParent.classList.remove("dropdownOpen");
+      elementData.messageFooterButtons.classList.remove("forceVisible");
+
+      return;
+    }
+
+    buttonParent.classList.add("dropdownOpen");
+    elementData.messageFooterButtons.classList.add("forceVisible");
+
+    const visibility = new Map<string, boolean>(Object.entries(this._dropdownOpen()));
+
+    EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownOpen_${this._options.dropdownIdentifier}`, {
+      element: this._activeDropdownElement,
+      visibility,
+    });
+
+    const dropdownMenu = this._dropdownMenu!;
+
+    let visiblePredecessor = false;
+    const children = Array.from(dropdownMenu.children);
+    children.forEach((listItem: HTMLElement, index) => {
+      const item = listItem.dataset.item!;
+
+      if (item === "divider") {
+        if (visiblePredecessor) {
+          DomUtil.show(listItem);
+
+          visiblePredecessor = false;
+        } else {
+          DomUtil.hide(listItem);
+        }
+      } else {
+        if (visibility.get(item) === false) {
+          DomUtil.hide(listItem);
+
+          // check if previous item was a divider
+          if (index > 0 && index + 1 === children.length) {
+            const previousElementSibling = listItem.previousElementSibling as HTMLElement;
+            if (previousElementSibling.dataset.item === "divider") {
+              DomUtil.hide(previousElementSibling);
+            }
+          }
+        } else {
+          DomUtil.show(listItem);
+
+          visiblePredecessor = true;
+        }
+      }
+    });
+  }
+
+  /**
+   * Returns the list of dropdown items for this type.
+   */
+  protected _dropdownGetItems(): ItemData[] {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+    return [];
+  }
+
+  /**
+   * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value
+   * to represent the visibility of each item. Items that do not appear in this list will be considered
+   * visible.
+   */
+  protected _dropdownOpen(): ElementVisibility {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+    return {};
+  }
+
+  /**
+   * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument.
+   */
+  protected _dropdownSelect(_item: string): void {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+  }
+
+  /**
+   * Handles clicks on a dropdown item.
+   */
+  protected _clickDropdownItem(event: MouseEvent): void {
+    event.preventDefault();
+
+    const target = event.currentTarget as HTMLElement;
+    const item = target.dataset.item!;
+    const data = {
+      cancel: false,
+      element: this._activeDropdownElement,
+      item,
+    };
+    EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownItemClick_${this._options.dropdownIdentifier}`, data);
+
+    if (data.cancel) {
+      event.preventDefault();
+    } else {
+      this._dropdownSelect(item);
+    }
+  }
+
+  /**
+   * Prepares the message for editor display.
+   */
+  protected _prepare(): void {
+    const data = this._elements.get(this._activeElement!)!;
+
+    const messageBodyEditor = document.createElement("div");
+    messageBodyEditor.className = "messageBody editor";
+    data.messageBodyEditor = messageBodyEditor;
+
+    const icon = document.createElement("span");
+    icon.className = "icon icon48 fa-spinner";
+    messageBodyEditor.appendChild(icon);
+
+    data.messageBody.insertAdjacentElement("afterend", messageBodyEditor);
+
+    DomUtil.hide(data.messageBody);
+  }
+
+  /**
+   * Shows the message editor.
+   */
+  protected _showEditor(data: AjaxResponseEditor): void {
+    const id = this._getEditorId();
+    const activeElement = this._activeElement!;
+    const elementData = this._elements.get(activeElement)!;
+
+    activeElement.classList.add("jsInvalidQuoteTarget");
+    const icon = elementData.messageBodyEditor!.querySelector(".icon") as HTMLElement;
+    icon.remove();
+
+    const messageBody = elementData.messageBodyEditor!;
+    const editor = document.createElement("div");
+    editor.className = "editorContainer";
+    DomUtil.setInnerHtml(editor, data.returnValues.template);
+    messageBody.appendChild(editor);
+
+    // bind buttons
+    const formSubmit = editor.querySelector(".formSubmit") as HTMLElement;
+
+    const buttonSave = formSubmit.querySelector('button[data-type="save"]') as HTMLButtonElement;
+    buttonSave.addEventListener("click", () => this._save());
+
+    const buttonCancel = formSubmit.querySelector('button[data-type="cancel"]') as HTMLButtonElement;
+    buttonCancel.addEventListener("click", () => this._restoreMessage());
+
+    EventHandler.add("com.woltlab.wcf.redactor", `submitEditor_${id}`, (data: { cancel: boolean }) => {
+      data.cancel = true;
+
+      this._save();
+    });
+
+    // hide message header and footer
+    DomUtil.hide(elementData.messageHeader);
+    DomUtil.hide(elementData.messageFooter);
+
+    if (Environment.editor() === "redactor") {
+      window.setTimeout(() => {
+        if (this._options.quoteManager) {
+          this._options.quoteManager.setAlternativeEditor(id);
+        }
+
+        UiScroll.element(activeElement);
+      }, 250);
+    } else {
+      const editorElement = document.getElementById(id) as HTMLElement;
+      editorElement.focus();
+    }
+  }
+
+  /**
+   * Restores the message view.
+   */
+  protected _restoreMessage(): void {
+    const activeElement = this._activeElement!;
+    const elementData = this._elements.get(activeElement)!;
+
+    this._destroyEditor();
+
+    elementData.messageBodyEditor!.remove();
+    elementData.messageBodyEditor = null;
+
+    DomUtil.show(elementData.messageBody);
+    DomUtil.show(elementData.messageFooter);
+    DomUtil.show(elementData.messageHeader);
+    activeElement.classList.remove("jsInvalidQuoteTarget");
+
+    this._activeElement = null;
+
+    if (this._options.quoteManager) {
+      this._options.quoteManager.clearAlternativeEditor();
+    }
+  }
+
+  /**
+   * Saves the editor message.
+   */
+  protected _save(): void {
+    const parameters = {
+      containerID: this._options.containerId,
+      data: {
+        message: "",
+      },
+      objectID: this._getObjectId(this._activeElement!),
+      removeQuoteIDs: this._options.quoteManager ? this._options.quoteManager.getQuotesMarkedForRemoval() : [],
+    };
+
+    const id = this._getEditorId();
+
+    // add any available settings
+    const settingsContainer = document.getElementById(`settings_${id}`);
+    if (settingsContainer) {
+      settingsContainer
+        .querySelectorAll("input, select, textarea")
+        .forEach((element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement) => {
+          if (element.nodeName === "INPUT" && (element.type === "checkbox" || element.type === "radio")) {
+            if (!(element as HTMLInputElement).checked) {
+              return;
+            }
+          }
+
+          const name = element.name;
+          if (Object.prototype.hasOwnProperty.call(parameters, name)) {
+            throw new Error(`Variable overshadowing, key '${name}' is already present.`);
+          }
+
+          parameters[name] = element.value.trim();
+        });
+    }
+
+    EventHandler.fire("com.woltlab.wcf.redactor2", `getText_${id}`, parameters.data);
+
+    let validateResult: unknown = this._validate(parameters);
+
+    // Legacy validation methods returned a plain boolean.
+    if (!(validateResult instanceof Promise)) {
+      if (validateResult === false) {
+        validateResult = Promise.reject();
+      } else {
+        validateResult = Promise.resolve();
+      }
+    }
+
+    (validateResult as Promise<void[]>).then(
+      () => {
+        EventHandler.fire("com.woltlab.wcf.redactor2", `submit_${id}`, parameters);
+
+        Ajax.api(this, {
+          actionName: "save",
+          parameters: parameters,
+        });
+
+        this._hideEditor();
+      },
+      (e) => {
+        const errorMessage = (e as Error).message;
+        console.log(`Validation of post edit failed: ${errorMessage}`);
+      },
+    );
+  }
+
+  /**
+   * Validates the message and invokes listeners to perform additional validation.
+   */
+  protected _validate(parameters: ArbitraryObject): Promise<void[]> {
+    // remove all existing error elements
+    this._activeElement!.querySelectorAll(".innerError").forEach((el) => el.remove());
+
+    const data: ValidationData = {
+      api: this,
+      parameters: parameters,
+      valid: true,
+      promises: [],
+    };
+
+    EventHandler.fire("com.woltlab.wcf.redactor2", `validate_${this._getEditorId()}`, data);
+
+    if (data.valid) {
+      data.promises.push(Promise.resolve());
+    } else {
+      data.promises.push(Promise.reject());
+    }
+
+    return Promise.all(data.promises);
+  }
+
+  /**
+   * Throws an error by showing an inline error for the target element.
+   */
+  throwError(element: HTMLElement, message: string): void {
+    DomUtil.innerError(element, message);
+  }
+
+  /**
+   * Shows the update message.
+   */
+  protected _showMessage(data: AjaxResponseMessage): void {
+    const activeElement = this._activeElement!;
+    const editorId = this._getEditorId();
+    const elementData = this._elements.get(activeElement)!;
+
+    // set new content
+    DomUtil.setInnerHtml(elementData.messageBody.querySelector(".messageText")!, data.returnValues.message);
+
+    // handle attachment list
+    if (typeof data.returnValues.attachmentList === "string") {
+      elementData.messageFooter
+        .querySelectorAll(".attachmentThumbnailList, .attachmentFileList")
+        .forEach((el) => el.remove());
+
+      const element = document.createElement("div");
+      DomUtil.setInnerHtml(element, data.returnValues.attachmentList);
+
+      let node;
+      while (element.childNodes.length) {
+        node = element.childNodes[element.childNodes.length - 1];
+        elementData.messageFooter.insertBefore(node, elementData.messageFooter.firstChild);
+      }
+    }
+
+    if (typeof data.returnValues.poll === "string") {
+      const poll = elementData.messageBody.querySelector(".pollContainer");
+      if (poll !== null) {
+        // The poll container is wrapped inside `.jsInlineEditorHideContent`.
+        poll.parentElement!.remove();
+      }
+
+      const pollContainer = document.createElement("div");
+      pollContainer.className = "jsInlineEditorHideContent";
+      DomUtil.setInnerHtml(pollContainer, data.returnValues.poll);
+
+      elementData.messageBody.insertAdjacentElement("afterbegin", pollContainer);
+    }
+
+    this._restoreMessage();
+
+    this._updateHistory(this._getHash(this._getObjectId(activeElement)));
+
+    EventHandler.fire("com.woltlab.wcf.redactor", `autosaveDestroy_${editorId}`);
+
+    UiNotification.show();
+
+    if (this._options.quoteManager) {
+      this._options.quoteManager.clearAlternativeEditor();
+      this._options.quoteManager.countQuotes();
+    }
+  }
+
+  /**
+   * Hides the editor from view.
+   */
+  protected _hideEditor(): void {
+    const elementData = this._elements.get(this._activeElement!)!;
+    const editorContainer = elementData.messageBodyEditor!.querySelector(".editorContainer") as HTMLElement;
+    DomUtil.hide(editorContainer);
+
+    const icon = document.createElement("span");
+    icon.className = "icon icon48 fa-spinner";
+    elementData.messageBodyEditor!.appendChild(icon);
+  }
+
+  /**
+   * Restores the previously hidden editor.
+   */
+  protected _restoreEditor(): void {
+    const elementData = this._elements.get(this._activeElement!)!;
+    const messageBodyEditor = elementData.messageBodyEditor!;
+
+    const icon = messageBodyEditor.querySelector(".fa-spinner") as HTMLElement;
+    icon.remove();
+
+    const editorContainer = messageBodyEditor.querySelector(".editorContainer") as HTMLElement;
+    if (editorContainer !== null) {
+      DomUtil.show(editorContainer);
+    }
+  }
+
+  /**
+   * Destroys the editor instance.
+   */
+  protected _destroyEditor(): void {
+    EventHandler.fire("com.woltlab.wcf.redactor2", `autosaveDestroy_${this._getEditorId()}`);
+    EventHandler.fire("com.woltlab.wcf.redactor2", `destroy_${this._getEditorId()}`);
+  }
+
+  /**
+   * Returns the hash added to the url after successfully editing a message.
+   */
+  protected _getHash(objectId: number): string {
+    return `#message${objectId}`;
+  }
+
+  /**
+   * Updates the history to avoid old content when going back in the browser
+   * history.
+   */
+  protected _updateHistory(hash: string): void {
+    window.location.hash = hash;
+  }
+
+  /**
+   * Returns the unique editor id.
+   */
+  protected _getEditorId(): string {
+    return this._options.editorPrefix + this._getObjectId(this._activeElement!).toString();
+  }
+
+  /**
+   * Returns the element's `data-object-id` value.
+   */
+  protected _getObjectId(element: HTMLElement): number {
+    return ~~(element.dataset.objectId || "");
+  }
+
+  _ajaxFailure(data: ResponseData): boolean {
+    const elementData = this._elements.get(this._activeElement!)!;
+    const editor = elementData.messageBodyEditor!.querySelector(".redactor-layer") as HTMLElement;
+
+    // handle errors occurring on editor load
+    if (editor === null) {
+      this._restoreMessage();
+
+      return true;
+    }
+
+    this._restoreEditor();
+
+    if (!data || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) {
+      return true;
+    }
+
+    DomUtil.innerError(editor, data.returnValues.realErrorMessage);
+
+    return false;
+  }
+
+  _ajaxSuccess(data: ResponseData): void {
+    switch (data.actionName) {
+      case "beginEdit":
+        this._showEditor(data as AjaxResponseEditor);
+        break;
+
+      case "save":
+        this._showMessage(data as AjaxResponseMessage);
+        break;
+    }
+  }
+
+  _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
+    return {
+      data: {
+        className: this._options.className,
+        interfaceName: "wcf\\data\\IMessageInlineEditorAction",
+      },
+      silent: true,
+    };
+  }
+
+  /** @deprecated  3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
+  legacyEdit(containerId: string): void {
+    this._click(document.getElementById(containerId), null);
+  }
+}
+
+Core.enableLegacyInheritance(UiMessageInlineEditor);
+
+export = UiMessageInlineEditor;