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

index 924cfb3c5e7eb3d89425f247c481e574c1142ab9..ef6e6f0ac801c4c11b1c991fad65aecb3fbb800c 100644 (file)
 /**
  * Provides access and editing of message properties.
  *
- * @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/Manager
+ * @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/Manager
  */
-define(['Ajax', 'Core', 'Dictionary', 'Language', 'Dom/ChangeListener', 'Dom/Util'], function (Ajax, Core, Dictionary, Language, DomChangeListener, DomUtil) {
+define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Dom/Change/Listener", "../../Language"], function (require, exports, tslib_1, Ajax, Core, Listener_1, Language) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        var Fake = function () { };
-        Fake.prototype = {
-            init: function () { },
-            rebuild: function () { },
-            getPermission: function () { },
-            getPropertyValue: function () { },
-            update: function () { },
-            updateItems: function () { },
-            updateAllItems: function () { },
-            setNote: function () { },
-            _update: function () { },
-            _updateState: function () { },
-            _toggleMessageStatus: function () { },
-            _getAttributeName: function () { },
-            _ajaxSuccess: function () { },
-            _ajaxSetup: function () { }
-        };
-        return Fake;
-    }
-    /**
-     * @param       {Object}        options         initialization options
-     * @constructor
-     */
-    function UiMessageManager(options) { this.init(options); }
-    UiMessageManager.prototype = {
+    Ajax = tslib_1.__importStar(Ajax);
+    Core = tslib_1.__importStar(Core);
+    Listener_1 = tslib_1.__importDefault(Listener_1);
+    Language = tslib_1.__importStar(Language);
+    class UiMessageManager {
         /**
          * Initializes a new manager instance.
-         *
-         * @param       {Object}        options         initialization options
          */
-        init: function (options) {
-            this._elements = null;
+        constructor(options) {
+            this._elements = new Map();
             this._options = Core.extend({
-                className: '',
-                selector: ''
+                className: "",
+                selector: "",
             }, options);
             this.rebuild();
-            DomChangeListener.add('Ui/Message/Manager' + this._options.className, this.rebuild.bind(this));
-        },
+            Listener_1.default.add(`Ui/Message/Manager${this._options.className}`, this.rebuild.bind(this));
+        }
         /**
          * Rebuilds the list of observed messages. You should call this method whenever a
          * message has been either added or removed from the document.
          */
-        rebuild: function () {
-            this._elements = new Dictionary();
-            var element, elements = elBySelAll(this._options.selector);
-            for (var i = 0, length = elements.length; i < length; i++) {
-                element = elements[i];
-                this._elements.set(elData(element, 'object-id'), element);
-            }
-        },
+        rebuild() {
+            this._elements.clear();
+            document.querySelectorAll(this._options.selector).forEach((element) => {
+                const objectId = ~~(element.dataset.objectId || "0");
+                this._elements.set(objectId, element);
+            });
+        }
         /**
          * Returns a boolean value for the given permission. The permission should not start
          * with "can" or "can-" as this is automatically assumed by this method.
-         *
-         * @param       {int}           objectId        message object id
-         * @param       {string}        permission      permission name without a leading "can" or "can-"
-         * @return      {boolean}       true if permission was set and is either 'true' or '1'
          */
-        getPermission: function (objectId, permission) {
-            permission = 'can-' + this._getAttributeName(permission);
-            var element = this._elements.get(objectId);
+        getPermission(objectId, permission) {
+            permission = "can-" + this._getAttributeName(permission);
+            const element = this._elements.get(objectId);
             if (element === undefined) {
-                throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
+                throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
             }
-            return elDataBool(element, permission);
-        },
+            return Core.stringToBool(element.dataset[permission] || "");
+        }
         /**
          * Returns the given property value from a message, optionally supporting a boolean return value.
-         *
-         * @param       {int}           objectId        message object id
-         * @param       {string}        propertyName    attribute name
-         * @param       {boolean}       asBool          attempt to interpret property value as boolean
-         * @return      {(boolean|string)}      raw property value or boolean if requested
          */
-        getPropertyValue: function (objectId, propertyName, asBool) {
-            var element = this._elements.get(objectId);
+        getPropertyValue(objectId, propertyName, asBool) {
+            const element = this._elements.get(objectId);
             if (element === undefined) {
-                throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
+                throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
             }
-            return window[(asBool ? 'elDataBool' : 'elData')](element, this._getAttributeName(propertyName));
-        },
+            const attributeName = this._getAttributeName(propertyName);
+            const value = element.dataset[attributeName] || "";
+            if (asBool) {
+                return Core.stringToBool(value);
+            }
+            return value;
+        }
         /**
          * Invokes a method for given message object id in order to alter its state or properties.
-         *
-         * @param       {int}           objectId        message object id
-         * @param       {string}        actionName      action name used for the ajax api
-         * @param       {Object=}       parameters      optional list of parameters included with the ajax request
          */
-        update: function (objectId, actionName, parameters) {
+        update(objectId, actionName, parameters) {
             Ajax.api(this, {
                 actionName: actionName,
                 parameters: parameters || {},
-                objectIDs: [objectId]
+                objectIDs: [objectId],
             });
-        },
+        }
         /**
          * Updates properties and states for given object ids. Keep in mind that this method does
          * not support setting individual properties per message, instead all property changes
          * are applied to all matching message objects.
-         *
-         * @param       {Array<int>}    objectIds       list of message object ids
-         * @param       {Object}        data            list of updated properties
          */
-        updateItems: function (objectIds, data) {
+        updateItems(objectIds, data) {
             if (!Array.isArray(objectIds)) {
                 objectIds = [objectIds];
             }
-            var element;
-            for (var i = 0, length = objectIds.length; i < length; i++) {
-                element = this._elements.get(objectIds[i]);
+            objectIds.forEach((objectId) => {
+                const element = this._elements.get(objectId);
                 if (element === undefined) {
-                    continue;
-                }
-                for (var key in data) {
-                    if (data.hasOwnProperty(key)) {
-                        this._update(element, key, data[key]);
-                    }
+                    return;
                 }
-            }
-        },
+                Object.entries(data).forEach(([key, value]) => {
+                    this._update(element, key, value);
+                });
+            });
+        }
         /**
          * Bulk updates the properties and states for all observed messages at once.
-         *
-         * @param       {Object}        data            list of updated properties
          */
-        updateAllItems: function (data) {
-            var objectIds = [];
-            this._elements.forEach((function (element, objectId) {
-                objectIds.push(objectId);
-            }).bind(this));
+        updateAllItems(data) {
+            const objectIds = Array.from(this._elements.keys());
             this.updateItems(objectIds, data);
-        },
+        }
         /**
          * Sets or removes a message note identified by its unique CSS class.
-         *
-         * @param       {int}           objectId        message object id
-         * @param       {string}        className       unique CSS class
-         * @param       {string}        htmlContent     HTML content
          */
-        setNote: function (objectId, className, htmlContent) {
-            var element = this._elements.get(objectId);
+        setNote(objectId, className, htmlContent) {
+            const element = this._elements.get(objectId);
             if (element === undefined) {
-                throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
+                throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
             }
-            var messageFooterNotes = elBySel('.messageFooterNotes', element);
-            var note = elBySel('.' + className, messageFooterNotes);
+            const messageFooterNotes = element.querySelector(".messageFooterNotes");
+            let note = messageFooterNotes.querySelector(`.${className}`);
             if (htmlContent) {
                 if (note === null) {
-                    note = elCreate('p');
-                    note.className = 'messageFooterNote ' + className;
+                    note = document.createElement("p");
+                    note.className = "messageFooterNote " + className;
                     messageFooterNotes.appendChild(note);
                 }
                 note.innerHTML = htmlContent;
             }
             else if (note !== null) {
-                elRemove(note);
+                note.remove();
             }
-        },
+        }
         /**
          * Updates a single property of a message element.
-         *
-         * @param       {Element}       element         message element
-         * @param       {string}        propertyName    property name
-         * @param       {?}             propertyValue   property value, will be implicitly converted to string
-         * @protected
          */
-        _update: function (element, propertyName, propertyValue) {
-            elData(element, this._getAttributeName(propertyName), propertyValue);
+        _update(element, propertyName, propertyValue) {
+            const attributeName = this._getAttributeName(propertyName);
+            element.dataset[attributeName] = propertyValue.toString();
             // handle special properties
-            var propertyValueBoolean = (propertyValue == 1 || propertyValue === true || propertyValue === 'true');
+            const propertyValueBoolean = propertyValue == 1 || propertyValue === true || propertyValue === "true";
             this._updateState(element, propertyName, propertyValue, propertyValueBoolean);
-        },
+        }
         /**
          * Updates the message element's state based upon a property change.
-         *
-         * @param       {Element}       element                 message element
-         * @param       {string}        propertyName            property name
-         * @param       {?}             propertyValue           property value
-         * @param       {boolean}       propertyValueBoolean    true if `propertyValue` equals either 'true' or '1'
-         * @protected
          */
-        _updateState: function (element, propertyName, propertyValue, propertyValueBoolean) {
+        _updateState(element, propertyName, propertyValue, propertyValueBoolean) {
             switch (propertyName) {
-                case 'isDeleted':
-                    element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDeleted');
-                    this._toggleMessageStatus(element, 'jsIconDeleted', 'wcf.message.status.deleted', 'red', propertyValueBoolean);
+                case "isDeleted":
+                    if (propertyValueBoolean) {
+                        element.classList.add("messageDeleted");
+                    }
+                    else {
+                        element.classList.remove("messageDeleted");
+                    }
+                    this._toggleMessageStatus(element, "jsIconDeleted", "wcf.message.status.deleted", "red", propertyValueBoolean);
                     break;
-                case 'isDisabled':
-                    element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDisabled');
-                    this._toggleMessageStatus(element, 'jsIconDisabled', 'wcf.message.status.disabled', 'green', propertyValueBoolean);
+                case "isDisabled":
+                    if (propertyValueBoolean) {
+                        element.classList.add("messageDisabled");
+                    }
+                    else {
+                        element.classList.remove("messageDisabled");
+                    }
+                    this._toggleMessageStatus(element, "jsIconDisabled", "wcf.message.status.disabled", "green", propertyValueBoolean);
                     break;
             }
-        },
+        }
         /**
          * Toggles the message status bade for provided element.
-         *
-         * @param       {Element}       element         message element
-         * @param       {string}        className       badge class name
-         * @param       {string}        phrase          language phrase
-         * @param       {string}        badgeColor      color css class
-         * @param       {boolean}       addBadge        add or remove badge
-         * @protected
          */
-        _toggleMessageStatus: function (element, className, phrase, badgeColor, addBadge) {
-            var messageStatus = elBySel('.messageStatus', element);
+        _toggleMessageStatus(element, className, phrase, badgeColor, addBadge) {
+            let messageStatus = element.querySelector(".messageStatus");
             if (messageStatus === null) {
-                var messageHeaderMetaData = elBySel('.messageHeaderMetaData', element);
+                const messageHeaderMetaData = element.querySelector(".messageHeaderMetaData");
                 if (messageHeaderMetaData === null) {
                     // can't find appropriate location to insert badge
                     return;
                 }
-                messageStatus = elCreate('ul');
-                messageStatus.className = 'messageStatus';
-                DomUtil.insertAfter(messageStatus, messageHeaderMetaData);
+                messageStatus = document.createElement("ul");
+                messageStatus.className = "messageStatus";
+                messageHeaderMetaData.insertAdjacentElement("afterend", messageStatus);
             }
-            var badge = elBySel('.' + className, messageStatus);
+            let badge = messageStatus.querySelector(`.${className}`);
             if (addBadge) {
                 if (badge !== null) {
                     // badge already exists
                     return;
                 }
-                badge = elCreate('span');
-                badge.className = 'badge label ' + badgeColor + ' ' + className;
+                badge = document.createElement("span");
+                badge.className = `badge label ${badgeColor} ${className}`;
                 badge.textContent = Language.get(phrase);
-                var listItem = elCreate('li');
+                const listItem = document.createElement("li");
                 listItem.appendChild(badge);
                 messageStatus.appendChild(listItem);
             }
@@ -243,42 +189,34 @@ define(['Ajax', 'Core', 'Dictionary', 'Language', 'Dom/ChangeListener', 'Dom/Uti
                     // badge does not exist
                     return;
                 }
-                elRemove(badge.parentNode);
+                badge.parentElement.remove();
             }
-        },
+        }
         /**
          * Transforms camel-cased property names into their attribute equivalent.
-         *
-         * @param       {string}        propertyName    camel-cased property name
-         * @return      {string}        equivalent attribute name
-         * @protected
          */
-        _getAttributeName: function (propertyName) {
-            if (propertyName.indexOf('-') !== -1) {
+        _getAttributeName(propertyName) {
+            if (propertyName.indexOf("-") !== -1) {
                 return propertyName;
             }
-            var attributeName = '';
-            var str, tmp = propertyName.split(/([A-Z][a-z]+)/);
-            for (var i = 0, length = tmp.length; i < length; i++) {
-                str = tmp[i];
-                if (str.length) {
-                    if (attributeName.length)
-                        attributeName += '-';
-                    attributeName += str.toLowerCase();
-                }
-            }
-            return attributeName;
-        },
-        _ajaxSuccess: function () {
+            return propertyName
+                .split(/([A-Z][a-z]+)/)
+                .map((s) => s.trim().toLowerCase())
+                .filter((s) => s.length > 0)
+                .join("-");
+        }
+        _ajaxSuccess(_data) {
+            // This should be an abstract method, but cannot be marked as such for backwards compatibility.
             throw new Error("Method _ajaxSuccess() must be implemented by deriving functions.");
-        },
-        _ajaxSetup: function () {
+        }
+        _ajaxSetup() {
             return {
                 data: {
-                    className: this._options.className
-                }
+                    className: this._options.className,
+                },
             };
         }
-    };
+    }
+    Core.enableLegacyInheritance(UiMessageManager);
     return UiMessageManager;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Manager.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Manager.js
deleted file mode 100644 (file)
index 170dcc9..0000000
+++ /dev/null
@@ -1,323 +0,0 @@
-/**
- * Provides access and editing of message properties.
- * 
- * @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/Manager
- */
-define(['Ajax', 'Core', 'Dictionary', 'Language', 'Dom/ChangeListener', 'Dom/Util'], function(Ajax, Core, Dictionary, Language, DomChangeListener, DomUtil) {
-       "use strict";
-       
-       if (!COMPILER_TARGET_DEFAULT) {
-               var Fake = function() {};
-               Fake.prototype = {
-                       init: function() {},
-                       rebuild: function() {},
-                       getPermission: function() {},
-                       getPropertyValue: function() {},
-                       update: function() {},
-                       updateItems: function() {},
-                       updateAllItems: function() {},
-                       setNote: function() {},
-                       _update: function() {},
-                       _updateState: function() {},
-                       _toggleMessageStatus: function() {},
-                       _getAttributeName: function() {},
-                       _ajaxSuccess: function() {},
-                       _ajaxSetup: function() {}
-               };
-               return Fake;
-       }
-       
-       /**
-        * @param       {Object}        options         initialization options
-        * @constructor
-        */
-       function UiMessageManager(options) { this.init(options); }
-       UiMessageManager.prototype = {
-               /**
-                * Initializes a new manager instance.
-                * 
-                * @param       {Object}        options         initialization options
-                */
-               init: function(options) {
-                       this._elements = null;
-                       this._options = Core.extend({
-                               className: '',
-                               selector: ''
-                       }, options);
-                       
-                       this.rebuild();
-                       
-                       DomChangeListener.add('Ui/Message/Manager' + this._options.className, this.rebuild.bind(this));
-               },
-               
-               /**
-                * Rebuilds the list of observed messages. You should call this method whenever a
-                * message has been either added or removed from the document.
-                */
-               rebuild: function() {
-                       this._elements = new Dictionary();
-                       
-                       var element, elements = elBySelAll(this._options.selector);
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               element = elements[i];
-                               
-                               this._elements.set(elData(element, 'object-id'), element);
-                       }
-               },
-               
-               /**
-                * Returns a boolean value for the given permission. The permission should not start
-                * with "can" or "can-" as this is automatically assumed by this method.
-                * 
-                * @param       {int}           objectId        message object id 
-                * @param       {string}        permission      permission name without a leading "can" or "can-"
-                * @return      {boolean}       true if permission was set and is either 'true' or '1'
-                */
-               getPermission: function(objectId, permission) {
-                       permission = 'can-' + this._getAttributeName(permission);
-                       var element = this._elements.get(objectId);
-                       if (element === undefined) {
-                               throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
-                       }
-                       
-                       return elDataBool(element, permission);
-               },
-               
-               /**
-                * Returns the given property value from a message, optionally supporting a boolean return value.
-                * 
-                * @param       {int}           objectId        message object id
-                * @param       {string}        propertyName    attribute name
-                * @param       {boolean}       asBool          attempt to interpret property value as boolean
-                * @return      {(boolean|string)}      raw property value or boolean if requested
-                */
-               getPropertyValue: function(objectId, propertyName, asBool) {
-                       var element = this._elements.get(objectId);
-                       if (element === undefined) {
-                               throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
-                       }
-                       
-                       return window[(asBool ? 'elDataBool' : 'elData')](element, this._getAttributeName(propertyName));
-               },
-               
-               /**
-                * Invokes a method for given message object id in order to alter its state or properties.
-                * 
-                * @param       {int}           objectId        message object id
-                * @param       {string}        actionName      action name used for the ajax api
-                * @param       {Object=}       parameters      optional list of parameters included with the ajax request
-                */
-               update: function(objectId, actionName, parameters) {
-                       Ajax.api(this, {
-                               actionName: actionName,
-                               parameters: parameters || {},
-                               objectIDs: [objectId]
-                       });
-               },
-               
-               /**
-                * Updates properties and states for given object ids. Keep in mind that this method does
-                * not support setting individual properties per message, instead all property changes
-                * are applied to all matching message objects.
-                * 
-                * @param       {Array<int>}    objectIds       list of message object ids
-                * @param       {Object}        data            list of updated properties
-                */
-               updateItems: function(objectIds, data) {
-                       if (!Array.isArray(objectIds)) {
-                               objectIds = [objectIds];
-                       }
-                       
-                       var element;
-                       for (var i = 0, length = objectIds.length; i < length; i++) {
-                               element = this._elements.get(objectIds[i]);
-                               if (element === undefined) {
-                                       continue;
-                               }
-                               
-                               for (var key in data) {
-                                       if (data.hasOwnProperty(key)) {
-                                               this._update(element, key, data[key]);
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Bulk updates the properties and states for all observed messages at once.
-                * 
-                * @param       {Object}        data            list of updated properties
-                */
-               updateAllItems: function(data) {
-                       var objectIds = [];
-                       this._elements.forEach((function(element, objectId) {
-                               objectIds.push(objectId);
-                       }).bind(this));
-                       
-                       this.updateItems(objectIds, data);
-               },
-               
-               /**
-                * Sets or removes a message note identified by its unique CSS class.
-                * 
-                * @param       {int}           objectId        message object id
-                * @param       {string}        className       unique CSS class
-                * @param       {string}        htmlContent     HTML content
-                */
-               setNote: function (objectId, className, htmlContent) {
-                       var element = this._elements.get(objectId);
-                       if (element === undefined) {
-                               throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
-                       }
-                       
-                       var messageFooterNotes = elBySel('.messageFooterNotes', element);
-                       var note = elBySel('.' + className, messageFooterNotes);
-                       if (htmlContent) {
-                               if (note === null) {
-                                       note = elCreate('p');
-                                       note.className = 'messageFooterNote ' + className;
-                                       
-                                       messageFooterNotes.appendChild(note);
-                               }
-                               
-                               note.innerHTML = htmlContent;
-                       }
-                       else if (note !== null) {
-                               elRemove(note);
-                       }
-               },
-               
-               /**
-                * Updates a single property of a message element.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {string}        propertyName    property name
-                * @param       {?}             propertyValue   property value, will be implicitly converted to string
-                * @protected
-                */
-               _update: function(element, propertyName, propertyValue) {
-                       elData(element, this._getAttributeName(propertyName), propertyValue);
-                       
-                       // handle special properties
-                       var propertyValueBoolean = (propertyValue == 1 || propertyValue === true || propertyValue === 'true');
-                       this._updateState(element, propertyName, propertyValue, propertyValueBoolean);
-               },
-               
-               /**
-                * Updates the message element's state based upon a property change.
-                * 
-                * @param       {Element}       element                 message element
-                * @param       {string}        propertyName            property name
-                * @param       {?}             propertyValue           property value
-                * @param       {boolean}       propertyValueBoolean    true if `propertyValue` equals either 'true' or '1'
-                * @protected
-                */
-               _updateState: function(element, propertyName, propertyValue, propertyValueBoolean) {
-                       switch (propertyName) {
-                               case 'isDeleted':
-                                       element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDeleted');
-                                       this._toggleMessageStatus(element, 'jsIconDeleted', 'wcf.message.status.deleted', 'red', propertyValueBoolean);
-                                       
-                                       break;
-                               
-                               case 'isDisabled':
-                                       element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDisabled');
-                                       this._toggleMessageStatus(element, 'jsIconDisabled', 'wcf.message.status.disabled', 'green', propertyValueBoolean);
-                                       
-                                       break;
-                       }
-               },
-               
-               /**
-                * Toggles the message status bade for provided element.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {string}        className       badge class name
-                * @param       {string}        phrase          language phrase
-                * @param       {string}        badgeColor      color css class
-                * @param       {boolean}       addBadge        add or remove badge
-                * @protected
-                */
-               _toggleMessageStatus: function(element, className, phrase, badgeColor, addBadge) {
-                       var messageStatus = elBySel('.messageStatus', element);
-                       if (messageStatus === null) {
-                               var messageHeaderMetaData = elBySel('.messageHeaderMetaData', element);
-                               if (messageHeaderMetaData === null) {
-                                       // can't find appropriate location to insert badge
-                                       return;
-                               }
-                               
-                               messageStatus = elCreate('ul');
-                               messageStatus.className = 'messageStatus';
-                               DomUtil.insertAfter(messageStatus, messageHeaderMetaData);
-                       }
-                       
-                       var badge = elBySel('.' + className, messageStatus);
-                       
-                       if (addBadge) {
-                               if (badge !== null) {
-                                       // badge already exists
-                                       return;
-                               }
-                               
-                               badge = elCreate('span');
-                               badge.className = 'badge label ' + badgeColor + ' ' + className;
-                               badge.textContent = Language.get(phrase);
-                               
-                               var listItem = elCreate('li');
-                               listItem.appendChild(badge);
-                               messageStatus.appendChild(listItem);
-                       }
-                       else {
-                               if (badge === null) {
-                                       // badge does not exist
-                                       return;
-                               }
-                               
-                               elRemove(badge.parentNode);
-                       }
-               },
-               
-               /**
-                * Transforms camel-cased property names into their attribute equivalent.
-                * 
-                * @param       {string}        propertyName    camel-cased property name
-                * @return      {string}        equivalent attribute name
-                * @protected
-                */
-               _getAttributeName: function(propertyName) {
-                       if (propertyName.indexOf('-') !== -1) {
-                               return propertyName;
-                       }
-                       
-                       var attributeName = '';
-                       var str, tmp = propertyName.split(/([A-Z][a-z]+)/);
-                       for (var i = 0, length = tmp.length; i < length; i++) {
-                               str = tmp[i];
-                               if (str.length) {
-                                       if (attributeName.length) attributeName += '-';
-                                       attributeName += str.toLowerCase();
-                               }
-                       }
-                       
-                       return attributeName;
-               },
-               
-               _ajaxSuccess: function() {
-                       throw new Error("Method _ajaxSuccess() must be implemented by deriving functions.");
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: this._options.className
-                               }
-                       };
-               }
-       };
-       
-       return UiMessageManager;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Manager.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Manager.ts
new file mode 100644 (file)
index 0000000..5d38424
--- /dev/null
@@ -0,0 +1,287 @@
+/**
+ * Provides access and editing of message properties.
+ *
+ * @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/Manager
+ */
+
+import * as Ajax from "../../Ajax";
+import { AjaxCallbackObject, AjaxCallbackSetup, ResponseData } from "../../Ajax/Data";
+import * as Core from "../../Core";
+import DomChangeListener from "../../Dom/Change/Listener";
+import * as Language from "../../Language";
+
+interface MessageManagerOptions {
+  className: string;
+  selector: string;
+}
+
+type StringableValue = boolean | number | string;
+
+class UiMessageManager implements AjaxCallbackObject {
+  protected readonly _elements = new Map<number, HTMLElement>();
+  protected readonly _options: MessageManagerOptions;
+
+  /**
+   * Initializes a new manager instance.
+   */
+  constructor(options: MessageManagerOptions) {
+    this._options = Core.extend(
+      {
+        className: "",
+        selector: "",
+      },
+      options,
+    ) as MessageManagerOptions;
+
+    this.rebuild();
+
+    DomChangeListener.add(`Ui/Message/Manager${this._options.className}`, this.rebuild.bind(this));
+  }
+
+  /**
+   * Rebuilds the list of observed messages. You should call this method whenever a
+   * message has been either added or removed from the document.
+   */
+  rebuild(): void {
+    this._elements.clear();
+
+    document.querySelectorAll(this._options.selector).forEach((element: HTMLElement) => {
+      const objectId = ~~(element.dataset.objectId || "0");
+      this._elements.set(objectId, element);
+    });
+  }
+
+  /**
+   * Returns a boolean value for the given permission. The permission should not start
+   * with "can" or "can-" as this is automatically assumed by this method.
+   */
+  getPermission(objectId: number, permission: string): boolean {
+    permission = "can-" + this._getAttributeName(permission);
+    const element = this._elements.get(objectId);
+    if (element === undefined) {
+      throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
+    }
+
+    return Core.stringToBool(element.dataset[permission] || "");
+  }
+
+  /**
+   * Returns the given property value from a message, optionally supporting a boolean return value.
+   */
+  getPropertyValue(objectId: number, propertyName: string, asBool: boolean): boolean | string {
+    const element = this._elements.get(objectId);
+    if (element === undefined) {
+      throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
+    }
+
+    const attributeName = this._getAttributeName(propertyName);
+    const value = element.dataset[attributeName] || "";
+
+    if (asBool) {
+      return Core.stringToBool(value);
+    }
+
+    return value;
+  }
+
+  /**
+   * Invokes a method for given message object id in order to alter its state or properties.
+   */
+  update(objectId: number, actionName: string, parameters?: ArbitraryObject): void {
+    Ajax.api(this, {
+      actionName: actionName,
+      parameters: parameters || {},
+      objectIDs: [objectId],
+    });
+  }
+
+  /**
+   * Updates properties and states for given object ids. Keep in mind that this method does
+   * not support setting individual properties per message, instead all property changes
+   * are applied to all matching message objects.
+   */
+  updateItems(objectIds: number | number[], data: ArbitraryObject): void {
+    if (!Array.isArray(objectIds)) {
+      objectIds = [objectIds];
+    }
+
+    objectIds.forEach((objectId) => {
+      const element = this._elements.get(objectId);
+      if (element === undefined) {
+        return;
+      }
+
+      Object.entries(data).forEach(([key, value]) => {
+        this._update(element, key, value as StringableValue);
+      });
+    });
+  }
+
+  /**
+   * Bulk updates the properties and states for all observed messages at once.
+   */
+  updateAllItems(data: ArbitraryObject): void {
+    const objectIds = Array.from(this._elements.keys());
+
+    this.updateItems(objectIds, data);
+  }
+
+  /**
+   * Sets or removes a message note identified by its unique CSS class.
+   */
+  setNote(objectId: number, className: string, htmlContent: string): void {
+    const element = this._elements.get(objectId);
+    if (element === undefined) {
+      throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
+    }
+
+    const messageFooterNotes = element.querySelector(".messageFooterNotes") as HTMLElement;
+    let note = messageFooterNotes.querySelector(`.${className}`);
+    if (htmlContent) {
+      if (note === null) {
+        note = document.createElement("p");
+        note.className = "messageFooterNote " + className;
+
+        messageFooterNotes.appendChild(note);
+      }
+
+      note.innerHTML = htmlContent;
+    } else if (note !== null) {
+      note.remove();
+    }
+  }
+
+  /**
+   * Updates a single property of a message element.
+   */
+  protected _update(element: HTMLElement, propertyName: string, propertyValue: StringableValue): void {
+    const attributeName = this._getAttributeName(propertyName);
+    element.dataset[attributeName] = propertyValue.toString();
+
+    // handle special properties
+    const propertyValueBoolean = propertyValue == 1 || propertyValue === true || propertyValue === "true";
+    this._updateState(element, propertyName, propertyValue, propertyValueBoolean);
+  }
+
+  /**
+   * Updates the message element's state based upon a property change.
+   */
+  protected _updateState(
+    element: HTMLElement,
+    propertyName: string,
+    propertyValue: StringableValue,
+    propertyValueBoolean: boolean,
+  ): void {
+    switch (propertyName) {
+      case "isDeleted":
+        if (propertyValueBoolean) {
+          element.classList.add("messageDeleted");
+        } else {
+          element.classList.remove("messageDeleted");
+        }
+
+        this._toggleMessageStatus(element, "jsIconDeleted", "wcf.message.status.deleted", "red", propertyValueBoolean);
+
+        break;
+
+      case "isDisabled":
+        if (propertyValueBoolean) {
+          element.classList.add("messageDisabled");
+        } else {
+          element.classList.remove("messageDisabled");
+        }
+
+        this._toggleMessageStatus(
+          element,
+          "jsIconDisabled",
+          "wcf.message.status.disabled",
+          "green",
+          propertyValueBoolean,
+        );
+
+        break;
+    }
+  }
+
+  /**
+   * Toggles the message status bade for provided element.
+   */
+  protected _toggleMessageStatus(
+    element: HTMLElement,
+    className: string,
+    phrase: string,
+    badgeColor: string,
+    addBadge: boolean,
+  ): void {
+    let messageStatus = element.querySelector(".messageStatus");
+    if (messageStatus === null) {
+      const messageHeaderMetaData = element.querySelector(".messageHeaderMetaData");
+      if (messageHeaderMetaData === null) {
+        // can't find appropriate location to insert badge
+        return;
+      }
+
+      messageStatus = document.createElement("ul");
+      messageStatus.className = "messageStatus";
+      messageHeaderMetaData.insertAdjacentElement("afterend", messageStatus);
+    }
+
+    let badge = messageStatus.querySelector(`.${className}`);
+    if (addBadge) {
+      if (badge !== null) {
+        // badge already exists
+        return;
+      }
+
+      badge = document.createElement("span");
+      badge.className = `badge label ${badgeColor} ${className}`;
+      badge.textContent = Language.get(phrase);
+
+      const listItem = document.createElement("li");
+      listItem.appendChild(badge);
+      messageStatus.appendChild(listItem);
+    } else {
+      if (badge === null) {
+        // badge does not exist
+        return;
+      }
+
+      badge.parentElement!.remove();
+    }
+  }
+
+  /**
+   * Transforms camel-cased property names into their attribute equivalent.
+   */
+  protected _getAttributeName(propertyName: string): string {
+    if (propertyName.indexOf("-") !== -1) {
+      return propertyName;
+    }
+
+    return propertyName
+      .split(/([A-Z][a-z]+)/)
+      .map((s) => s.trim().toLowerCase())
+      .filter((s) => s.length > 0)
+      .join("-");
+  }
+
+  _ajaxSuccess(_data: ResponseData): void {
+    // This should be an abstract method, but cannot be marked as such for backwards compatibility.
+    throw new Error("Method _ajaxSuccess() must be implemented by deriving functions.");
+  }
+
+  _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
+    return {
+      data: {
+        className: this._options.className,
+      },
+    };
+  }
+}
+
+Core.enableLegacyInheritance(UiMessageManager);
+
+export = UiMessageManager;