Convert `Ui/Like/Handler` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Tue, 3 Nov 2020 11:02:05 +0000 (12:02 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 3 Nov 2020 11:02:05 +0000 (12:02 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Like/Handler.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Like/Handler.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Like/Handler.ts [new file with mode: 0644]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Reaction/Handler.ts

index daf360935f58cf1f724addaffdcd9d57b4164fce..551da42664ca6b1cb9321f77b554179de4d80c3c 100644 (file)
@@ -1,39 +1,33 @@
 /**
  * Provides interface elements to display and review likes.
  *
- * @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/Like/Handler
+ * @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/Like/Handler
  * @deprecated  5.2 use ReactionHandler instead
  */
-define([
-    'Ajax', 'Core', 'Dictionary', 'Language',
-    'ObjectMap', 'StringUtil', 'Dom/ChangeListener', 'Dom/Util',
-    'Ui/Dialog', 'WoltLabSuite/Core/Ui/User/List', 'User', 'WoltLabSuite/Core/Ui/Reaction/Handler'
-], function (Ajax, Core, Dictionary, Language, ObjectMap, StringUtil, DomChangeListener, DomUtil, UiDialog, UiUserList, User, UiReactionHandler) {
+define(["require", "exports", "tslib", "../../Core", "../../Dom/Change/Listener", "../../Language", "../../StringUtil", "../Reaction/Handler", "../../User"], function (require, exports, tslib_1, Core, Listener_1, Language, StringUtil, Handler_1, User_1) {
     "use strict";
-    /**
-     * @constructor
-     */
-    function UiLikeHandler(objectType, options) { this.init(objectType, options); }
-    UiLikeHandler.prototype = {
+    Core = tslib_1.__importStar(Core);
+    Listener_1 = tslib_1.__importDefault(Listener_1);
+    Language = tslib_1.__importStar(Language);
+    StringUtil = tslib_1.__importStar(StringUtil);
+    Handler_1 = tslib_1.__importDefault(Handler_1);
+    User_1 = tslib_1.__importDefault(User_1);
+    class UiLikeHandler {
         /**
          * Initializes the like handler.
-         *
-         * @param      {string}        objectType      object type
-         * @param      {object}        options         initialization options
          */
-        init: function (objectType, options) {
-            if (options.containerSelector === '') {
+        constructor(objectType, opts) {
+            this._containers = new Map();
+            if (!opts.containerSelector) {
                 throw new Error("[WoltLabSuite/Core/Ui/Like/Handler] Expected a non-empty string for option 'containerSelector'.");
             }
-            this._containers = new ObjectMap();
-            this._details = new ObjectMap();
             this._objectType = objectType;
             this._options = Core.extend({
                 // settings
-                badgeClassNames: '',
+                badgeClassNames: "",
                 isSingleItem: false,
                 markListItemAsActive: false,
                 renderAsButton: true,
@@ -45,98 +39,104 @@ define([
                 canLikeOwnContent: false,
                 canViewSummary: false,
                 // selectors
-                badgeContainerSelector: '.messageHeader .messageStatus',
-                buttonAppendToSelector: '.messageFooter .messageFooterButtons',
-                buttonBeforeSelector: '',
-                containerSelector: '',
-                summarySelector: '.messageFooterGroup'
-            }, options);
-            this.initContainers(options, objectType);
-            DomChangeListener.add('WoltLabSuite/Core/Ui/Like/Handler-' + objectType, this.initContainers.bind(this));
-            new UiReactionHandler(this._objectType, {
+                badgeContainerSelector: ".messageHeader .messageStatus",
+                buttonAppendToSelector: ".messageFooter .messageFooterButtons",
+                buttonBeforeSelector: "",
+                containerSelector: "",
+                summarySelector: ".messageFooterGroup",
+            }, opts);
+            this.initContainers();
+            Listener_1.default.add(`WoltLabSuite/Core/Ui/Like/Handler-${objectType}`, () => this.initContainers());
+            new Handler_1.default(this._objectType, {
                 containerSelector: this._options.containerSelector,
-                summaryListSelector: '.reactionSummaryList'
             });
-        },
+        }
         /**
          * Initializes all applicable containers.
          */
-        initContainers: function () {
-            var element, elements = elBySelAll(this._options.containerSelector), elementData, triggerChange = false;
-            for (var i = 0, length = elements.length; i < length; i++) {
-                element = elements[i];
+        initContainers() {
+            let triggerChange = false;
+            document.querySelectorAll(this._options.containerSelector).forEach((element) => {
                 if (this._containers.has(element)) {
-                    continue;
+                    return;
                 }
-                elementData = {
+                const elementData = {
                     badge: null,
                     dislikeButton: null,
                     likeButton: null,
                     summary: null,
-                    dislikes: ~~elData(element, 'like-dislikes'),
-                    liked: ~~elData(element, 'like-liked'),
-                    likes: ~~elData(element, 'like-likes'),
-                    objectId: ~~elData(element, 'object-id'),
-                    users: JSON.parse(elData(element, 'like-users'))
+                    dislikes: ~~element.dataset.likeDislikes,
+                    liked: ~~element.dataset.likeLiked,
+                    likes: ~~element.dataset.likeLikes,
+                    objectId: ~~element.dataset.objectId,
+                    users: JSON.parse(element.dataset.likeUsers),
                 };
                 this._containers.set(element, elementData);
                 this._buildWidget(element, elementData);
                 triggerChange = true;
-            }
+            });
             if (triggerChange) {
-                DomChangeListener.trigger();
+                Listener_1.default.trigger();
             }
-        },
+        }
         /**
          * Creates the interface elements.
-         *
-         * @param      {Element}       element         container element
-         * @param      {object}        elementData     like data
          */
-        _buildWidget: function (element, elementData) {
-            // build reaction summary list
-            var summaryList, listItem, badgeContainer, isSummaryPosition = true;
-            badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.summarySelector) : elBySel(this._options.summarySelector, element);
+        _buildWidget(element, elementData) {
+            let badgeContainer;
+            let isSummaryPosition = true;
+            if (this._options.isSingleItem) {
+                badgeContainer = document.querySelector(this._options.summarySelector);
+            }
+            else {
+                badgeContainer = element.querySelector(this._options.summarySelector);
+            }
             if (badgeContainer === null) {
-                badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.badgeContainerSelector) : elBySel(this._options.badgeContainerSelector, element);
+                if (this._options.isSingleItem) {
+                    badgeContainer = document.querySelector(this._options.badgeContainerSelector);
+                }
+                else {
+                    badgeContainer = element.querySelector(this._options.badgeContainerSelector);
+                }
                 isSummaryPosition = false;
             }
             if (badgeContainer !== null) {
-                summaryList = elCreate('ul');
-                summaryList.classList.add('reactionSummaryList');
+                const summaryList = document.createElement("ul");
+                summaryList.classList.add("reactionSummaryList");
                 if (isSummaryPosition) {
-                    summaryList.classList.add('likesSummary');
+                    summaryList.classList.add("likesSummary");
                 }
                 else {
-                    summaryList.classList.add('reactionSummaryListTiny');
+                    summaryList.classList.add("reactionSummaryListTiny");
                 }
-                for (var key in elementData.users) {
-                    if (key === "reactionTypeID")
-                        continue;
-                    if (!REACTION_TYPES.hasOwnProperty(key))
-                        continue;
-                    // create element 
-                    var createdElement = elCreate('li');
-                    createdElement.className = 'reactCountButton';
-                    elData(createdElement, 'reaction-type-id', key);
-                    var countSpan = elCreate('span');
-                    countSpan.className = 'reactionCount';
-                    countSpan.innerHTML = StringUtil.shortUnit(elementData.users[key]);
+                const availableReactions = new Map(Object.entries(window.REACTION_TYPES));
+                Object.entries(elementData.users).forEach(([reactionTypeId, count]) => {
+                    const reaction = availableReactions.get(reactionTypeId);
+                    if (reactionTypeId === "reactionTypeID" || !reaction) {
+                        return;
+                    }
+                    // create element
+                    const createdElement = document.createElement("li");
+                    createdElement.className = "reactCountButton";
+                    createdElement.setAttribute("reaction-type-id", reactionTypeId);
+                    const countSpan = document.createElement("span");
+                    countSpan.className = "reactionCount";
+                    countSpan.innerHTML = StringUtil.shortUnit(~~count);
                     createdElement.appendChild(countSpan);
-                    createdElement.innerHTML = REACTION_TYPES[key].renderedIcon + createdElement.innerHTML;
+                    createdElement.innerHTML = reaction.renderedIcon + createdElement.innerHTML;
                     summaryList.appendChild(createdElement);
-                }
+                });
                 if (isSummaryPosition) {
                     if (this._options.summaryPrepend) {
-                        DomUtil.prepend(summaryList, badgeContainer);
+                        badgeContainer.insertAdjacentElement("afterbegin", summaryList);
                     }
                     else {
-                        badgeContainer.appendChild(summaryList);
+                        badgeContainer.insertAdjacentElement("beforeend", summaryList);
                     }
                 }
                 else {
-                    if (badgeContainer.nodeName === 'OL' || badgeContainer.nodeName === 'UL') {
-                        listItem = elCreate('li');
+                    if (badgeContainer.nodeName === "OL" || badgeContainer.nodeName === "UL") {
+                        const listItem = document.createElement("li");
                         listItem.appendChild(summaryList);
                         badgeContainer.appendChild(listItem);
                     }
@@ -147,9 +147,25 @@ define([
                 elementData.badge = summaryList;
             }
             // build reaction button
-            if (this._options.canLike && (User.userId != elData(element, 'user-id') || this._options.canLikeOwnContent)) {
-                var appendTo = (this._options.buttonAppendToSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonAppendToSelector) : elBySel(this._options.buttonAppendToSelector, element)) : null;
-                var insertPosition = (this._options.buttonBeforeSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonBeforeSelector) : elBySel(this._options.buttonBeforeSelector, element)) : null;
+            if (this._options.canLike && (User_1.default.userId != ~~element.dataset.userId || this._options.canLikeOwnContent)) {
+                let appendTo = null;
+                if (this._options.buttonAppendToSelector) {
+                    if (this._options.isSingleItem) {
+                        appendTo = document.querySelector(this._options.buttonAppendToSelector);
+                    }
+                    else {
+                        appendTo = element.querySelector(this._options.buttonAppendToSelector);
+                    }
+                }
+                let insertPosition = null;
+                if (this._options.buttonBeforeSelector) {
+                    if (this._options.isSingleItem) {
+                        insertPosition = document.querySelector(this._options.buttonBeforeSelector);
+                    }
+                    else {
+                        insertPosition = element.querySelector(this._options.buttonBeforeSelector);
+                    }
+                }
                 if (insertPosition === null && appendTo === null) {
                     throw new Error("Unable to find insert location for like/dislike buttons.");
                 }
@@ -157,51 +173,46 @@ define([
                     elementData.likeButton = this._createButton(element, elementData.users.reactionTypeID, insertPosition, appendTo);
                 }
             }
-        },
+        }
         /**
          * Creates a reaction button.
-         *
-         * @param      {Element}       element                 container element
-         * @param      {int}           reactionTypeID          the reactionTypeID of the current state
-         * @param      {Element?}      insertBefore            insert button before given element
-         * @param       {Element?}      appendTo                append button to given element
-         * @return     {Element}       button element
          */
-        _createButton: function (element, reactionTypeID, insertBefore, appendTo) {
-            var title = Language.get('wcf.reactions.react');
-            var listItem = elCreate('li');
-            listItem.className = 'wcfReactButton';
-            var button = elCreate('a');
-            button.className = 'jsTooltip reactButton';
+        _createButton(element, reactionTypeID, insertBefore, appendTo) {
+            const title = Language.get("wcf.reactions.react");
+            const listItem = document.createElement("li");
+            listItem.className = "wcfReactButton";
+            const button = document.createElement("a");
+            button.className = "jsTooltip reactButton";
             if (this._options.renderAsButton) {
-                button.classList.add('button');
+                button.classList.add("button");
             }
-            button.href = '#';
+            button.href = "#";
             button.title = title;
-            var icon = elCreate('span');
-            icon.className = 'icon icon16 fa-smile-o';
+            const icon = document.createElement("span");
+            icon.className = "icon icon16 fa-smile-o";
             if (reactionTypeID === undefined || reactionTypeID == 0) {
-                elData(icon, 'reaction-type-id', 0);
+                icon.dataset.reactionTypeId = "0";
             }
             else {
-                elData(button, 'reaction-type-id', reactionTypeID);
+                button.dataset.reactionTypeId = reactionTypeID.toString();
                 button.classList.add("active");
             }
             button.appendChild(icon);
-            var invisibleText = elCreate("span");
+            const invisibleText = document.createElement("span");
             invisibleText.className = "invisible";
             invisibleText.innerHTML = title;
             button.appendChild(document.createTextNode(" "));
             button.appendChild(invisibleText);
             listItem.appendChild(button);
             if (insertBefore) {
-                insertBefore.parentNode.insertBefore(listItem, insertBefore);
+                insertBefore.insertAdjacentElement("beforebegin", listItem);
             }
             else {
-                appendTo.appendChild(listItem);
+                appendTo.insertAdjacentElement("beforeend", listItem);
             }
             return button;
         }
-    };
+    }
+    Core.enableLegacyInheritance(UiLikeHandler);
     return UiLikeHandler;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Like/Handler.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Like/Handler.js
deleted file mode 100644 (file)
index 4c21ef4..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-/**
- * Provides interface elements to display and review likes.
- * 
- * @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/Like/Handler
- * @deprecated  5.2 use ReactionHandler instead 
- */
-define(
-       [
-               'Ajax',      'Core',                     'Dictionary',         'Language',
-               'ObjectMap', 'StringUtil',               'Dom/ChangeListener', 'Dom/Util',
-               'Ui/Dialog', 'WoltLabSuite/Core/Ui/User/List', 'User',         'WoltLabSuite/Core/Ui/Reaction/Handler'
-       ],
-       function(
-               Ajax,        Core,                        Dictionary,           Language,
-               ObjectMap,   StringUtil,                  DomChangeListener,    DomUtil,
-               UiDialog,    UiUserList,                  User,                 UiReactionHandler
-       )
-{
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function UiLikeHandler(objectType, options) { this.init(objectType, options); }
-       UiLikeHandler.prototype = {
-               /**
-                * Initializes the like handler.
-                * 
-                * @param       {string}        objectType      object type
-                * @param       {object}        options         initialization options
-                */
-               init: function(objectType, options) {
-                       if (options.containerSelector === '') {
-                               throw new Error("[WoltLabSuite/Core/Ui/Like/Handler] Expected a non-empty string for option 'containerSelector'.");
-                       }
-                       
-                       this._containers = new ObjectMap();
-                       this._details = new ObjectMap();
-                       this._objectType = objectType;
-                       this._options = Core.extend({
-                               // settings
-                               badgeClassNames: '',
-                               isSingleItem: false,
-                               markListItemAsActive: false,
-                               renderAsButton: true,
-                               summaryPrepend: true,
-                               summaryUseIcon: true,
-                               
-                               // permissions
-                               canDislike: false,
-                               canLike: false,
-                               canLikeOwnContent: false,
-                               canViewSummary: false,
-                               
-                               // selectors
-                               badgeContainerSelector: '.messageHeader .messageStatus',
-                               buttonAppendToSelector: '.messageFooter .messageFooterButtons',
-                               buttonBeforeSelector: '',
-                               containerSelector: '',
-                               summarySelector: '.messageFooterGroup'
-                       }, options);
-                       
-                       this.initContainers(options, objectType);
-                       
-                       DomChangeListener.add('WoltLabSuite/Core/Ui/Like/Handler-' + objectType, this.initContainers.bind(this));
-                       
-                       new UiReactionHandler(this._objectType, {
-                               containerSelector: this._options.containerSelector,
-                               summaryListSelector: '.reactionSummaryList'
-                       });
-               },
-               
-               /**
-                * Initializes all applicable containers.
-                */
-               initContainers: function() {
-                       var element, elements = elBySelAll(this._options.containerSelector), elementData, triggerChange = false;
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               element = elements[i];
-                               if (this._containers.has(element)) {
-                                       continue;
-                               }
-                               
-                               elementData = {
-                                       badge: null,
-                                       dislikeButton: null,
-                                       likeButton: null,
-                                       summary: null,
-                                       
-                                       dislikes: ~~elData(element, 'like-dislikes'),
-                                       liked: ~~elData(element, 'like-liked'),
-                                       likes: ~~elData(element, 'like-likes'),
-                                       objectId: ~~elData(element, 'object-id'),
-                                       users: JSON.parse(elData(element, 'like-users'))
-                               };
-                               
-                               this._containers.set(element, elementData);
-                               this._buildWidget(element, elementData);
-                               
-                               triggerChange = true;
-                       }
-                       
-                       if (triggerChange) {
-                               DomChangeListener.trigger();
-                       }
-               },
-               
-               /**
-                * Creates the interface elements.
-                * 
-                * @param       {Element}       element         container element
-                * @param       {object}        elementData     like data
-                */
-               _buildWidget: function(element, elementData) {
-                       // build reaction summary list
-                       var summaryList, listItem, badgeContainer, isSummaryPosition = true;
-                       badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.summarySelector) : elBySel(this._options.summarySelector, element);
-                       if (badgeContainer === null) {
-                               badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.badgeContainerSelector) : elBySel(this._options.badgeContainerSelector, element);
-                               isSummaryPosition = false;
-                       }
-                       
-                       if (badgeContainer !== null) {
-                               summaryList = elCreate('ul');
-                               summaryList.classList.add('reactionSummaryList');
-                               if (isSummaryPosition) {
-                                       summaryList.classList.add('likesSummary');
-                               }
-                               else {
-                                       summaryList.classList.add('reactionSummaryListTiny');
-                               }
-                               
-                               for (var key in elementData.users) {
-                                       if (key === "reactionTypeID") continue;
-                                       if (!REACTION_TYPES.hasOwnProperty(key)) continue;
-                                       
-                                       // create element 
-                                       var createdElement = elCreate('li');
-                                       createdElement.className = 'reactCountButton';
-                                       elData(createdElement, 'reaction-type-id', key);
-                                       
-                                       var countSpan = elCreate('span');
-                                       countSpan.className = 'reactionCount';
-                                       countSpan.innerHTML = StringUtil.shortUnit(elementData.users[key]);
-                                       createdElement.appendChild(countSpan);
-                                       
-                                       createdElement.innerHTML = REACTION_TYPES[key].renderedIcon + createdElement.innerHTML;
-                                       
-                                       summaryList.appendChild(createdElement);
-                               }
-                               
-                               if (isSummaryPosition) {
-                                       if (this._options.summaryPrepend) {
-                                               DomUtil.prepend(summaryList, badgeContainer);
-                                       }
-                                       else {
-                                               badgeContainer.appendChild(summaryList);
-                                       }
-                               }
-                               else {
-                                       if (badgeContainer.nodeName === 'OL' || badgeContainer.nodeName === 'UL') {
-                                               listItem = elCreate('li');
-                                               listItem.appendChild(summaryList);
-                                               badgeContainer.appendChild(listItem);
-                                       }
-                                       else {
-                                               badgeContainer.appendChild(summaryList);
-                                       }
-                               }
-                               
-                               elementData.badge = summaryList;
-                       }
-                       
-                       // build reaction button
-                       if (this._options.canLike && (User.userId != elData(element, 'user-id') || this._options.canLikeOwnContent)) {
-                               var appendTo = (this._options.buttonAppendToSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonAppendToSelector) : elBySel(this._options.buttonAppendToSelector, element)) : null;
-                               var insertPosition = (this._options.buttonBeforeSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonBeforeSelector) : elBySel(this._options.buttonBeforeSelector, element)) : null;
-                               if (insertPosition === null && appendTo === null) {
-                                       throw new Error("Unable to find insert location for like/dislike buttons.");
-                               }
-                               else {
-                                       elementData.likeButton = this._createButton(element, elementData.users.reactionTypeID, insertPosition, appendTo);
-                               }
-                       }
-               },
-               
-               /**
-                * Creates a reaction button.
-                * 
-                * @param       {Element}       element                 container element
-                * @param       {int}           reactionTypeID          the reactionTypeID of the current state
-                * @param       {Element?}      insertBefore            insert button before given element
-                * @param       {Element?}      appendTo                append button to given element
-                * @return      {Element}       button element 
-                */
-               _createButton: function(element, reactionTypeID, insertBefore, appendTo) {
-                       var title = Language.get('wcf.reactions.react');
-                       
-                       var listItem = elCreate('li');
-                       listItem.className = 'wcfReactButton';
-                       
-                       var button = elCreate('a');
-                       button.className = 'jsTooltip reactButton';
-                       if (this._options.renderAsButton) {
-                               button.classList.add('button');
-                       }
-                       
-                       button.href = '#';
-                       button.title = title;
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon16 fa-smile-o';
-                       
-                       if (reactionTypeID === undefined || reactionTypeID == 0) {
-                               elData(icon, 'reaction-type-id', 0);
-                       }
-                       else {
-                               elData(button, 'reaction-type-id', reactionTypeID);
-                               button.classList.add("active");
-                       }
-                       
-                       button.appendChild(icon);
-                       
-                       var invisibleText = elCreate("span");
-                       invisibleText.className = "invisible";
-                       invisibleText.innerHTML = title;
-                       
-                       button.appendChild(document.createTextNode(" "));
-                       button.appendChild(invisibleText);
-                       
-                       listItem.appendChild(button);
-                       
-                       if (insertBefore) {
-                               insertBefore.parentNode.insertBefore(listItem, insertBefore);
-                       }
-                       else {
-                               appendTo.appendChild(listItem);
-                       }
-                       
-                       return button;
-               }
-       };
-       
-       return UiLikeHandler;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Like/Handler.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Like/Handler.ts
new file mode 100644 (file)
index 0000000..f53ea40
--- /dev/null
@@ -0,0 +1,306 @@
+/**
+ * Provides interface elements to display and review likes.
+ *
+ * @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/Like/Handler
+ * @deprecated  5.2 use ReactionHandler instead
+ */
+
+import * as Core from "../../Core";
+import DomChangeListener from "../../Dom/Change/Listener";
+import * as Language from "../../Language";
+import * as StringUtil from "../../StringUtil";
+import UiReactionHandler from "../Reaction/Handler";
+import User from "../../User";
+
+interface LikeHandlerOptions {
+  // settings
+  badgeClassNames: string;
+  isSingleItem: boolean;
+  markListItemAsActive: boolean;
+  renderAsButton: boolean;
+  summaryPrepend: boolean;
+  summaryUseIcon: boolean;
+
+  // permissions
+  canDislike: boolean;
+  canLike: boolean;
+  canLikeOwnContent: boolean;
+  canViewSummary: boolean;
+
+  // selectors
+  badgeContainerSelector: string;
+  buttonAppendToSelector: string;
+  buttonBeforeSelector: string;
+  containerSelector: string;
+  summarySelector: string;
+}
+
+interface LikeUsers {
+  [key: string]: number;
+}
+
+interface ElementData {
+  badge: HTMLUListElement | null;
+  dislikeButton: null;
+  likeButton: HTMLAnchorElement | null;
+  summary: null;
+
+  dislikes: number;
+  liked: number;
+  likes: number;
+  objectId: number;
+  users: LikeUsers;
+}
+
+class UiLikeHandler {
+  protected readonly _containers = new Map<HTMLElement, ElementData>();
+  protected readonly _objectType: string;
+  protected readonly _options: LikeHandlerOptions;
+
+  /**
+   * Initializes the like handler.
+   */
+  constructor(objectType: string, opts: Partial<LikeHandlerOptions>) {
+    if (!opts.containerSelector) {
+      throw new Error(
+        "[WoltLabSuite/Core/Ui/Like/Handler] Expected a non-empty string for option 'containerSelector'.",
+      );
+    }
+
+    this._objectType = objectType;
+    this._options = Core.extend(
+      {
+        // settings
+        badgeClassNames: "",
+        isSingleItem: false,
+        markListItemAsActive: false,
+        renderAsButton: true,
+        summaryPrepend: true,
+        summaryUseIcon: true,
+
+        // permissions
+        canDislike: false,
+        canLike: false,
+        canLikeOwnContent: false,
+        canViewSummary: false,
+
+        // selectors
+        badgeContainerSelector: ".messageHeader .messageStatus",
+        buttonAppendToSelector: ".messageFooter .messageFooterButtons",
+        buttonBeforeSelector: "",
+        containerSelector: "",
+        summarySelector: ".messageFooterGroup",
+      },
+      opts,
+    ) as LikeHandlerOptions;
+
+    this.initContainers();
+
+    DomChangeListener.add(`WoltLabSuite/Core/Ui/Like/Handler-${objectType}`, () => this.initContainers());
+
+    new UiReactionHandler(this._objectType, {
+      containerSelector: this._options.containerSelector,
+    });
+  }
+
+  /**
+   * Initializes all applicable containers.
+   */
+  initContainers(): void {
+    let triggerChange = false;
+
+    document.querySelectorAll(this._options.containerSelector).forEach((element: HTMLElement) => {
+      if (this._containers.has(element)) {
+        return;
+      }
+
+      const elementData = {
+        badge: null,
+        dislikeButton: null,
+        likeButton: null,
+        summary: null,
+
+        dislikes: ~~element.dataset.likeDislikes!,
+        liked: ~~element.dataset.likeLiked!,
+        likes: ~~element.dataset.likeLikes!,
+        objectId: ~~element.dataset.objectId!,
+        users: JSON.parse(element.dataset.likeUsers!),
+      };
+
+      this._containers.set(element, elementData);
+      this._buildWidget(element, elementData);
+
+      triggerChange = true;
+    });
+
+    if (triggerChange) {
+      DomChangeListener.trigger();
+    }
+  }
+
+  /**
+   * Creates the interface elements.
+   */
+  protected _buildWidget(element: HTMLElement, elementData: ElementData): void {
+    let badgeContainer: HTMLElement | null;
+    let isSummaryPosition = true;
+
+    if (this._options.isSingleItem) {
+      badgeContainer = document.querySelector(this._options.summarySelector);
+    } else {
+      badgeContainer = element.querySelector(this._options.summarySelector);
+    }
+
+    if (badgeContainer === null) {
+      if (this._options.isSingleItem) {
+        badgeContainer = document.querySelector(this._options.badgeContainerSelector);
+      } else {
+        badgeContainer = element.querySelector(this._options.badgeContainerSelector);
+      }
+
+      isSummaryPosition = false;
+    }
+
+    if (badgeContainer !== null) {
+      const summaryList = document.createElement("ul");
+      summaryList.classList.add("reactionSummaryList");
+      if (isSummaryPosition) {
+        summaryList.classList.add("likesSummary");
+      } else {
+        summaryList.classList.add("reactionSummaryListTiny");
+      }
+
+      const availableReactions = new Map(Object.entries(window.REACTION_TYPES));
+      Object.entries(elementData.users).forEach(([reactionTypeId, count]) => {
+        const reaction = availableReactions.get(reactionTypeId);
+        if (reactionTypeId === "reactionTypeID" || !reaction) {
+          return;
+        }
+
+        // create element
+        const createdElement = document.createElement("li");
+        createdElement.className = "reactCountButton";
+        createdElement.setAttribute("reaction-type-id", reactionTypeId);
+
+        const countSpan = document.createElement("span");
+        countSpan.className = "reactionCount";
+        countSpan.innerHTML = StringUtil.shortUnit(~~count);
+        createdElement.appendChild(countSpan);
+
+        createdElement.innerHTML = reaction.renderedIcon + createdElement.innerHTML;
+
+        summaryList.appendChild(createdElement);
+      });
+
+      if (isSummaryPosition) {
+        if (this._options.summaryPrepend) {
+          badgeContainer.insertAdjacentElement("afterbegin", summaryList);
+        } else {
+          badgeContainer.insertAdjacentElement("beforeend", summaryList);
+        }
+      } else {
+        if (badgeContainer.nodeName === "OL" || badgeContainer.nodeName === "UL") {
+          const listItem = document.createElement("li");
+          listItem.appendChild(summaryList);
+          badgeContainer.appendChild(listItem);
+        } else {
+          badgeContainer.appendChild(summaryList);
+        }
+      }
+
+      elementData.badge = summaryList;
+    }
+
+    // build reaction button
+    if (this._options.canLike && (User.userId != ~~element.dataset.userId! || this._options.canLikeOwnContent)) {
+      let appendTo: HTMLElement | null = null;
+      if (this._options.buttonAppendToSelector) {
+        if (this._options.isSingleItem) {
+          appendTo = document.querySelector(this._options.buttonAppendToSelector);
+        } else {
+          appendTo = element.querySelector(this._options.buttonAppendToSelector);
+        }
+      }
+
+      let insertPosition: HTMLElement | null = null;
+      if (this._options.buttonBeforeSelector) {
+        if (this._options.isSingleItem) {
+          insertPosition = document.querySelector(this._options.buttonBeforeSelector);
+        } else {
+          insertPosition = element.querySelector(this._options.buttonBeforeSelector);
+        }
+      }
+
+      if (insertPosition === null && appendTo === null) {
+        throw new Error("Unable to find insert location for like/dislike buttons.");
+      } else {
+        elementData.likeButton = this._createButton(
+          element,
+          elementData.users.reactionTypeID,
+          insertPosition,
+          appendTo,
+        );
+      }
+    }
+  }
+
+  /**
+   * Creates a reaction button.
+   */
+  protected _createButton(
+    element: HTMLElement,
+    reactionTypeID: number,
+    insertBefore: HTMLElement | null,
+    appendTo: HTMLElement | null,
+  ): HTMLAnchorElement {
+    const title = Language.get("wcf.reactions.react");
+
+    const listItem = document.createElement("li");
+    listItem.className = "wcfReactButton";
+
+    const button = document.createElement("a");
+    button.className = "jsTooltip reactButton";
+    if (this._options.renderAsButton) {
+      button.classList.add("button");
+    }
+
+    button.href = "#";
+    button.title = title;
+
+    const icon = document.createElement("span");
+    icon.className = "icon icon16 fa-smile-o";
+
+    if (reactionTypeID === undefined || reactionTypeID == 0) {
+      icon.dataset.reactionTypeId = "0";
+    } else {
+      button.dataset.reactionTypeId = reactionTypeID.toString();
+      button.classList.add("active");
+    }
+
+    button.appendChild(icon);
+
+    const invisibleText = document.createElement("span");
+    invisibleText.className = "invisible";
+    invisibleText.innerHTML = title;
+
+    button.appendChild(document.createTextNode(" "));
+    button.appendChild(invisibleText);
+
+    listItem.appendChild(button);
+
+    if (insertBefore) {
+      insertBefore.insertAdjacentElement("beforebegin", listItem);
+    } else {
+      appendTo!.insertAdjacentElement("beforeend", listItem);
+    }
+
+    return button;
+  }
+}
+
+Core.enableLegacyInheritance(UiLikeHandler);
+
+export = UiLikeHandler;
index a623878737a1c6c29d258bc6e45ab9e5b4f802b2..c5d0108b6453e733934abbad2001eb28a36d2c7f 100644 (file)
@@ -65,7 +65,7 @@ class UiReactionHandler {
   /**
    * Initializes the reaction handler.
    */
-  constructor(objectType: string, opts: ReactionHandlerOptions) {
+  constructor(objectType: string, opts: Partial<ReactionHandlerOptions>) {
     if (!opts.containerSelector) {
       throw new Error(
         "[WoltLabSuite/Core/Ui/Reaction/Handler] Expected a non-empty string for option 'containerSelector'.",