Convert `Controller/Clipboard` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Thu, 26 Nov 2020 17:08:17 +0000 (18:08 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 26 Nov 2020 17:08:17 +0000 (18:08 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.ts [new file with mode: 0644]

index 7710ccc175e3b24b238ad4e2963f31e5e960737d..ed443bf0c2292a9e02f89fc346b8dffa7e63fbb8 100644 (file)
 /**
  * Clipboard API 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/Controller/Clipboard
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Controller/Clipboard
  */
-define([
-    'Ajax', 'Core', 'Dictionary', 'EventHandler',
-    'Language', 'List', 'ObjectMap', 'Dom/ChangeListener',
-    'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown',
-    'WoltLabSuite/Core/Ui/Page/Action', 'Ui/Screen'
-], function (Ajax, Core, Dictionary, EventHandler, Language, List, ObjectMap, DomChangeListener, DomTraverse, DomUtil, UiConfirmation, UiSimpleDropdown, UiPageAction, UiScreen) {
+define(["require", "exports", "tslib", "../Ajax", "../Core", "../Dom/Change/Listener", "../Dom/Util", "../Event/Handler", "../Language", "../Ui/Confirmation", "../Ui/Dropdown/Simple", "../Ui/Page/Action", "../Ui/Screen"], function (require, exports, tslib_1, Ajax, Core, Listener_1, Util_1, EventHandler, Language, UiConfirmation, Simple_1, UiPageAction, UiScreen) {
     "use strict";
-    if (!COMPILER_TARGET_DEFAULT) {
-        return {
-            setup: function () { },
-            reload: function () { },
-            _initContainers: function () { },
-            _loadMarkedItems: function () { },
-            _markAll: function () { },
-            _mark: function () { },
-            _saveState: function () { },
-            _executeAction: function () { },
-            _executeProxyAction: function () { },
-            _unmarkAll: function () { },
-            _ajaxSetup: function () { },
-            _ajaxSuccess: function () { },
-            _rebuildMarkings: function () { },
-            hideEditor: function () { },
-            showEditor: function () { },
-            unmark: function () { }
-        };
-    }
-    var _containers = new Dictionary();
-    var _editors = new Dictionary();
-    var _editorDropdowns = new Dictionary();
-    var _elements = elByClass('jsClipboardContainer');
-    var _itemData = new ObjectMap();
-    var _knownCheckboxes = new List();
-    var _options = {};
-    var _reloadPageOnSuccess = new Dictionary();
-    var _callbackCheckbox = null;
-    var _callbackItem = null;
-    var _callbackUnmarkAll = null;
-    var _specialCheckboxSelector = '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]';
-    /**
-     * Clipboard API
-     *
-     * @exports        WoltLabSuite/Core/Controller/Clipboard
-     */
-    return {
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.unmark = exports.showEditor = exports.hideEditor = exports.reload = exports.setup = void 0;
+    Ajax = tslib_1.__importStar(Ajax);
+    Core = tslib_1.__importStar(Core);
+    Listener_1 = tslib_1.__importDefault(Listener_1);
+    Util_1 = tslib_1.__importDefault(Util_1);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    Language = tslib_1.__importStar(Language);
+    UiConfirmation = tslib_1.__importStar(UiConfirmation);
+    Simple_1 = tslib_1.__importDefault(Simple_1);
+    UiPageAction = tslib_1.__importStar(UiPageAction);
+    UiScreen = tslib_1.__importStar(UiScreen);
+    const _specialCheckboxSelector = '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]';
+    class ControllerClipboard {
+        constructor() {
+            this.containers = new Map();
+            this.editors = new Map();
+            this.editorDropdowns = new Map();
+            this.itemData = new WeakMap();
+            this.knownCheckboxes = new WeakSet();
+            this.pageClassNames = [];
+            this.pageObjectId = 0;
+            this.reloadPageOnSuccess = new Map();
+        }
         /**
          * Initializes the clipboard API handler.
-         *
-         * @param      {Object}        options         initialization options
          */
-        setup: function (options) {
+        setup(options) {
             if (!options.pageClassName) {
                 throw new Error("Expected a non-empty string for parameter 'pageClassName'.");
             }
-            if (_callbackCheckbox === null) {
-                _callbackCheckbox = this._mark.bind(this);
-                _callbackItem = this._executeAction.bind(this);
-                _callbackUnmarkAll = this._unmarkAll.bind(this);
-                _options = Core.extend({
-                    hasMarkedItems: false,
-                    pageClassNames: [options.pageClassName],
-                    pageObjectId: 0
-                }, options);
-                delete _options.pageClassName;
-            }
-            else {
-                if (options.pageObjectId) {
-                    throw new Error("Cannot load secondary clipboard with page object id set.");
-                }
-                _options.pageClassNames.push(options.pageClassName);
-            }
-            if (!Element.prototype.matches) {
-                Element.prototype.matches = Element.prototype.msMatchesSelector;
+            let hasMarkedItems = false;
+            if (this.pageClassNames.length === 0) {
+                hasMarkedItems = options.hasMarkedItems;
+                this.pageObjectId = options.pageObjectId;
             }
-            this._initContainers();
-            if (_options.hasMarkedItems && _elements.length) {
-                this._loadMarkedItems();
+            this.pageClassNames.push(options.pageClassName);
+            this.initContainers();
+            if (hasMarkedItems && this.containers.size) {
+                this.loadMarkedItems();
             }
-            DomChangeListener.add('WoltLabSuite/Core/Controller/Clipboard', this._initContainers.bind(this));
-        },
+            Listener_1.default.add("WoltLabSuite/Core/Controller/Clipboard", () => this.initContainers());
+        }
         /**
          * Reloads the clipboard data.
          */
-        reload: function () {
-            if (_containers.size) {
-                this._loadMarkedItems();
+        reload() {
+            if (this.containers.size) {
+                this.loadMarkedItems();
             }
-        },
+        }
         /**
          * Initializes clipboard containers.
          */
-        _initContainers: function () {
-            for (var i = 0, length = _elements.length; i < length; i++) {
-                var container = _elements[i];
-                var containerId = DomUtil.identify(container);
-                var containerData = _containers.get(containerId);
+        initContainers() {
+            document.querySelectorAll(".jsClipboardContainer").forEach((container) => {
+                const containerId = Util_1.default.identify(container);
+                let containerData = this.containers.get(containerId);
                 if (containerData === undefined) {
-                    var markAll = elBySel('.jsClipboardMarkAll', container);
+                    const markAll = container.querySelector(".jsClipboardMarkAll");
                     if (markAll !== null) {
                         if (markAll.matches(_specialCheckboxSelector)) {
-                            var label = markAll.closest('label');
-                            elAttr(label, 'role', 'checkbox');
-                            elAttr(label, 'tabindex', '0');
-                            elAttr(label, 'aria-checked', false);
-                            elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.markAll'));
-                            label.addEventListener('keyup', function (event) {
-                                if (event.keyCode === 13 || event.keyCode === 32) {
-                                    checkbox.click();
+                            const label = markAll.closest("label");
+                            label.setAttribute("role", "checkbox");
+                            label.tabIndex = 0;
+                            label.setAttribute("aria-checked", "false");
+                            label.setAttribute("aria-label", Language.get("wcf.clipboard.item.markAll"));
+                            label.addEventListener("keyup", (event) => {
+                                if (event.key === "Enter" || event.key === "Space") {
+                                    markAll.click();
                                 }
                             });
                         }
-                        elData(markAll, 'container-id', containerId);
-                        markAll.addEventListener('click', this._markAll.bind(this));
+                        markAll.dataset.containerId = containerId;
+                        markAll.addEventListener("click", (ev) => this.markAll(ev));
                     }
                     containerData = {
-                        checkboxes: elByClass('jsClipboardItem', container),
+                        checkboxes: container.getElementsByClassName("jsClipboardItem"),
                         element: container,
                         markAll: markAll,
-                        markedObjectIds: new List()
+                        markedObjectIds: new Set(),
                     };
-                    _containers.set(containerId, containerData);
+                    this.containers.set(containerId, containerData);
                 }
-                for (var j = 0, innerLength = containerData.checkboxes.length; j < innerLength; j++) {
-                    var checkbox = containerData.checkboxes[j];
-                    if (!_knownCheckboxes.has(checkbox)) {
-                        elData(checkbox, 'container-id', containerId);
-                        (function (checkbox) {
-                            if (checkbox.matches(_specialCheckboxSelector)) {
-                                var label = checkbox.closest('label');
-                                elAttr(label, 'role', 'checkbox');
-                                elAttr(label, 'tabindex', '0');
-                                elAttr(label, 'aria-checked', false);
-                                elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.mark'));
-                                label.addEventListener('keyup', function (event) {
-                                    if (event.keyCode === 13 || event.keyCode === 32) {
-                                        checkbox.click();
-                                    }
-                                });
-                            }
-                            var link = checkbox.closest('a');
-                            if (link === null) {
-                                checkbox.addEventListener('click', _callbackCheckbox);
-                            }
-                            else {
-                                // Firefox will always trigger the link if the checkbox is
-                                // inside of one. Since 2000. Thanks Firefox. 
-                                checkbox.addEventListener('click', function (event) {
-                                    event.preventDefault();
-                                    window.setTimeout(function () {
-                                        checkbox.checked = !checkbox.checked;
-                                        _callbackCheckbox(null, checkbox);
-                                    }, 10);
-                                });
+                Array.from(containerData.checkboxes).forEach((checkbox) => {
+                    if (this.knownCheckboxes.has(checkbox)) {
+                        return;
+                    }
+                    checkbox.dataset.containerId = containerId;
+                    if (checkbox.matches(_specialCheckboxSelector)) {
+                        const label = checkbox.closest("label");
+                        label.setAttribute("role", "checkbox");
+                        label.tabIndex = 0;
+                        label.setAttribute("aria-checked", "false");
+                        label.setAttribute("aria-label", Language.get("wcf.clipboard.item.mark"));
+                        label.addEventListener("keyup", (event) => {
+                            if (event.key === "Enter" || event.key === "Space") {
+                                checkbox.click();
                             }
-                        })(checkbox);
-                        _knownCheckboxes.add(checkbox);
+                        });
                     }
-                }
-            }
-        },
+                    const link = checkbox.closest("a");
+                    if (link === null) {
+                        checkbox.addEventListener("click", (ev) => this.mark(ev));
+                    }
+                    else {
+                        // Firefox will always trigger the link if the checkbox is
+                        // inside of one. Since 2000. Thanks Firefox.
+                        checkbox.addEventListener("click", (event) => {
+                            event.preventDefault();
+                            window.setTimeout(() => {
+                                checkbox.checked = !checkbox.checked;
+                                this.mark(checkbox);
+                            }, 10);
+                        });
+                    }
+                    this.knownCheckboxes.add(checkbox);
+                });
+            });
+        }
         /**
          * Loads marked items from clipboard.
          */
-        _loadMarkedItems: function () {
+        loadMarkedItems() {
             Ajax.api(this, {
-                actionName: 'getMarkedItems',
+                actionName: "getMarkedItems",
                 parameters: {
-                    pageClassNames: _options.pageClassNames,
-                    pageObjectID: _options.pageObjectId
-                }
+                    pageClassNames: this.pageClassNames,
+                    pageObjectID: this.pageObjectId,
+                },
             });
-        },
+        }
         /**
          * Marks or unmarks all visible items at once.
-         *
-         * @param      {object}        event   event object
          */
-        _markAll: function (event) {
-            var checkbox = event.currentTarget;
-            var isMarked = (checkbox.nodeName !== 'INPUT' || checkbox.checked);
-            if (elAttr(checkbox.parentNode, 'role') === 'checkbox') {
-                elAttr(checkbox.parentNode, 'aria-checked', isMarked);
-            }
-            var objectIds = [];
-            var containerId = elData(checkbox, 'container-id');
-            var data = _containers.get(containerId);
-            var type = elData(data.element, 'type');
-            for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                var item = data.checkboxes[i];
-                var objectId = ~~elData(item, 'object-id');
+        markAll(event) {
+            const checkbox = event.currentTarget;
+            const isMarked = checkbox.nodeName !== "INPUT" || checkbox.checked;
+            this.setParentAsMarked(checkbox, isMarked);
+            const objectIds = [];
+            const containerId = checkbox.dataset.containerId;
+            const data = this.containers.get(containerId);
+            const type = data.element.dataset.type;
+            Array.from(data.checkboxes).forEach((item) => {
+                const objectId = ~~item.dataset.objectId;
                 if (isMarked) {
                     if (!item.checked) {
                         item.checked = true;
@@ -206,128 +163,115 @@ define([
                 else {
                     if (item.checked) {
                         item.checked = false;
-                        data.markedObjectIds['delete'](objectId);
+                        data.markedObjectIds["delete"](objectId);
                         objectIds.push(objectId);
                     }
                 }
-                if (elAttr(item.parentNode, 'role') === 'checkbox') {
-                    elAttr(item.parentNode, 'aria-checked', isMarked);
-                }
-                var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
+                this.setParentAsMarked(item, isMarked);
+                const clipboardObject = checkbox.closest(".jsClipboardObject");
                 if (clipboardObject !== null) {
-                    clipboardObject.classList[(isMarked ? 'addClass' : 'removeClass')]('jsMarked');
+                    if (isMarked) {
+                        clipboardObject.classList.add("jsMarked");
+                    }
+                    else {
+                        clipboardObject.classList.remove("jsMarked");
+                    }
                 }
-            }
-            this._saveState(type, objectIds, isMarked);
-        },
+            });
+            this.saveState(type, objectIds, isMarked);
+        }
         /**
          * Marks or unmarks an individual item.
          *
-         * @param      {object}        event           event object
-         * @param       {Element=}      checkbox        checkbox element
          */
-        _mark: function (event, checkbox) {
-            checkbox = (event instanceof Event) ? event.currentTarget : checkbox;
-            var objectId = ~~elData(checkbox, 'object-id');
-            var isMarked = checkbox.checked;
-            var containerId = elData(checkbox, 'container-id');
-            var data = _containers.get(containerId);
-            var type = elData(data.element, 'type');
-            var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-            data.markedObjectIds[(isMarked ? 'add' : 'delete')](objectId);
-            clipboardObject.classList[(isMarked) ? 'add' : 'remove']('jsMarked');
-            if (data.markAll !== null) {
-                var markedAll = true;
-                for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                    if (!data.checkboxes[i].checked) {
-                        markedAll = false;
-                        break;
-                    }
-                }
-                data.markAll.checked = markedAll;
-                if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') {
-                    elAttr(data.markAll.parentNode, 'aria-checked', isMarked);
-                }
+        mark(event) {
+            const checkbox = event instanceof Event ? event.currentTarget : event;
+            const objectId = ~~checkbox.dataset.objectId;
+            const isMarked = checkbox.checked;
+            const containerId = checkbox.dataset.containerId;
+            const data = this.containers.get(containerId);
+            const type = data.element.dataset.type;
+            const clipboardObject = checkbox.closest(".jsClipboardObject");
+            if (isMarked) {
+                data.markedObjectIds.add(objectId);
+                clipboardObject.classList.add("jsMarked");
+            }
+            else {
+                data.markedObjectIds.delete(objectId);
+                clipboardObject.classList.remove("jsMarked");
             }
-            if (elAttr(checkbox.parentNode, 'role') === 'checkbox') {
-                elAttr(checkbox.parentNode, 'aria-checked', checkbox.checked);
+            if (data.markAll !== null) {
+                data.markAll.checked = Array.from(data.checkboxes).some((item) => !item.checked);
+                this.setParentAsMarked(data.markAll, isMarked);
             }
-            this._saveState(type, [objectId], isMarked);
-        },
+            this.setParentAsMarked(checkbox, checkbox.checked);
+            this.saveState(type, [objectId], isMarked);
+        }
         /**
          * Saves the state for given item object ids.
-         *
-         * @param      {string}        type            object type
-         * @param      {int[]}         objectIds       item object ids
-         * @param      {boolean}       isMarked        true if marked
          */
-        _saveState: function (type, objectIds, isMarked) {
+        saveState(objectType, objectIds, isMarked) {
             Ajax.api(this, {
-                actionName: (isMarked ? 'mark' : 'unmark'),
+                actionName: isMarked ? "mark" : "unmark",
                 parameters: {
-                    pageClassNames: _options.pageClassNames,
-                    pageObjectID: _options.pageObjectId,
+                    pageClassNames: this.pageClassNames,
+                    pageObjectID: this.pageObjectId,
                     objectIDs: objectIds,
-                    objectType: type
-                }
+                    objectType,
+                },
             });
-        },
+        }
         /**
          * Executes an editor action.
-         *
-         * @param      {object}        event           event object
          */
-        _executeAction: function (event) {
-            var listItem = event.currentTarget;
-            var data = _itemData.get(listItem);
+        executeAction(event) {
+            const listItem = event.currentTarget;
+            const data = this.itemData.get(listItem);
             if (data.url) {
                 window.location.href = data.url;
                 return;
             }
-            var triggerEvent = function () {
-                var type = elData(listItem, 'type');
-                EventHandler.fire('com.woltlab.wcf.clipboard', type, {
-                    data: data,
-                    listItem: listItem,
-                    responseData: null
+            function triggerEvent() {
+                const type = listItem.dataset.type;
+                EventHandler.fire("com.woltlab.wcf.clipboard", type, {
+                    data,
+                    listItem,
+                    responseData: null,
                 });
-            };
-            //noinspection JSUnresolvedVariable
-            var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : '';
-            var fireEvent = true;
-            if (typeof data.parameters === 'object' && data.parameters.actionName && data.parameters.className) {
-                if (data.parameters.actionName === 'unmarkAll' || Array.isArray(data.parameters.objectIDs)) {
-                    if (confirmMessage.length) {
-                        //noinspection JSUnresolvedVariable
-                        var template = (typeof data.internalData.template === 'string') ? data.internalData.template : '';
+            }
+            const message = typeof data.internalData.confirmMessage === "string" ? data.internalData.confirmMessage : "";
+            let fireEvent = true;
+            if (Core.isPlainObject(data.parameters) && data.parameters.actionName && data.parameters.className) {
+                if (data.parameters.actionName === "unmarkAll" || Array.isArray(data.parameters.objectIDs)) {
+                    if (message.length) {
+                        const template = typeof data.internalData.template === "string" ? data.internalData.template : "";
                         UiConfirmation.show({
-                            confirm: (function () {
-                                var formData = {};
+                            confirm: () => {
+                                const formData = {};
                                 if (template.length) {
-                                    var items = elBySelAll('input, select, textarea', UiConfirmation.getContentElement());
-                                    for (var i = 0, length = items.length; i < length; i++) {
-                                        var item = items[i];
-                                        var name = elAttr(item, 'name');
+                                    UiConfirmation.getContentElement()
+                                        .querySelectorAll("input, select, textarea")
+                                        .forEach((item) => {
+                                        const name = item.name;
                                         switch (item.nodeName) {
-                                            case 'INPUT':
+                                            case "INPUT":
                                                 if ((item.type !== "checkbox" && item.type !== "radio") || item.checked) {
-                                                    formData[name] = elAttr(item, 'value');
+                                                    formData[name] = item.value;
                                                 }
                                                 break;
-                                            case 'SELECT':
+                                            case "SELECT":
                                                 formData[name] = item.value;
                                                 break;
-                                            case 'TEXTAREA':
+                                            case "TEXTAREA":
                                                 formData[name] = item.value.trim();
                                                 break;
                                         }
-                                    }
+                                    });
                                 }
-                                //noinspection JSUnresolvedFunction
                                 this._executeProxyAction(listItem, data, formData);
-                            }).bind(this),
-                            message: confirmMessage,
-                            template: template
+                            },
+                            message,
+                            template,
                         });
                     }
                     else {
@@ -335,199 +279,160 @@ define([
                     }
                 }
             }
-            else if (confirmMessage.length) {
+            else if (message.length) {
                 fireEvent = false;
                 UiConfirmation.show({
                     confirm: triggerEvent,
-                    message: confirmMessage
+                    message,
                 });
             }
             if (fireEvent) {
                 triggerEvent();
             }
-        },
+        }
         /**
          * Forwards clipboard actions to an individual handler.
-         *
-         * @param      {Element}       listItem        dropdown item element
-         * @param      {Object}        data            action data
-         * @param      {Object?}       formData        form data
          */
-        _executeProxyAction: function (listItem, data, formData) {
-            formData = formData || {};
-            var objectIds = (data.parameters.actionName !== 'unmarkAll') ? data.parameters.objectIDs : [];
-            var parameters = { data: formData };
-            //noinspection JSUnresolvedVariable
-            if (typeof data.internalData.parameters === 'object') {
-                //noinspection JSUnresolvedVariable
-                for (var key in data.internalData.parameters) {
-                    //noinspection JSUnresolvedVariable
-                    if (data.internalData.parameters.hasOwnProperty(key)) {
-                        //noinspection JSUnresolvedVariable
-                        parameters[key] = data.internalData.parameters[key];
-                    }
-                }
+        _executeProxyAction(listItem, data, formData = {}) {
+            const objectIds = data.parameters.actionName !== "unmarkAll" ? data.parameters.objectIDs : [];
+            const parameters = { data: formData };
+            if (Core.isPlainObject(data.internalData.parameters)) {
+                Object.entries(data.internalData.parameters).forEach(([key, value]) => {
+                    parameters[key] = value;
+                });
             }
             Ajax.api(this, {
                 actionName: data.parameters.actionName,
                 className: data.parameters.className,
                 objectIDs: objectIds,
-                parameters: parameters
-            }, (function (responseData) {
-                if (data.actionName !== 'unmarkAll') {
-                    var type = elData(listItem, 'type');
-                    EventHandler.fire('com.woltlab.wcf.clipboard', type, {
-                        data: data,
-                        listItem: listItem,
-                        responseData: responseData
+                parameters,
+            }, (responseData) => {
+                if (data.actionName !== "unmarkAll") {
+                    const type = listItem.dataset.type;
+                    EventHandler.fire("com.woltlab.wcf.clipboard", type, {
+                        data,
+                        listItem,
+                        responseData,
                     });
-                    if (_reloadPageOnSuccess.has(type) && _reloadPageOnSuccess.get(type).indexOf(responseData.actionName) !== -1) {
+                    const reloadPageOnSuccess = this.reloadPageOnSuccess.get(type);
+                    if (reloadPageOnSuccess && reloadPageOnSuccess.includes(responseData.actionName)) {
                         window.location.reload();
                         return;
                     }
                 }
-                this._loadMarkedItems();
-            }).bind(this));
-        },
+                this.loadMarkedItems();
+            });
+        }
         /**
          * Unmarks all clipboard items for an object type.
-         *
-         * @param      {object}        event           event object
          */
-        _unmarkAll: function (event) {
-            var type = elData(event.currentTarget, 'type');
+        unmarkAll(event) {
+            const listItem = event.currentTarget;
             Ajax.api(this, {
-                actionName: 'unmarkAll',
+                actionName: "unmarkAll",
                 parameters: {
-                    objectType: type
-                }
+                    objectType: listItem.dataset.type,
+                },
             });
-        },
+        }
         /**
          * Sets up ajax request object.
-         *
-         * @return     {object}        request options
          */
-        _ajaxSetup: function () {
+        _ajaxSetup() {
             return {
                 data: {
-                    className: 'wcf\\data\\clipboard\\item\\ClipboardItemAction'
-                }
+                    className: "wcf\\data\\clipboard\\item\\ClipboardItemAction",
+                },
             };
-        },
+        }
         /**
          * Handles successful AJAX requests.
-         *
-         * @param      {object}        data    response data
          */
-        _ajaxSuccess: function (data) {
-            if (data.actionName === 'unmarkAll') {
-                _containers.forEach((function (containerData) {
-                    if (elData(containerData.element, 'type') === data.returnValues.objectType) {
-                        var clipboardObjects = elByClass('jsMarked', containerData.element);
-                        while (clipboardObjects.length) {
-                            clipboardObjects[0].classList.remove('jsMarked');
-                        }
-                        if (containerData.markAll !== null) {
-                            containerData.markAll.checked = false;
-                            if (elAttr(containerData.markAll.parentNode, 'role') === 'checkbox') {
-                                elAttr(containerData.markAll.parentNode, 'aria-checked', false);
-                            }
-                        }
-                        for (var i = 0, length = containerData.checkboxes.length; i < length; i++) {
-                            containerData.checkboxes[i].checked = false;
-                            if (elAttr(containerData.checkboxes[i].parentNode, 'role') === 'checkbox') {
-                                elAttr(containerData.checkboxes[i].parentNode, 'aria-checked', false);
-                            }
-                        }
-                        UiPageAction.remove('wcfClipboard-' + data.returnValues.objectType);
+        _ajaxSuccess(data) {
+            if (data.actionName === "unmarkAll") {
+                const objectType = data.returnValues.objectType;
+                this.containers.forEach((containerData) => {
+                    if (containerData.element.dataset.type !== objectType) {
+                        return;
+                    }
+                    containerData.element.querySelectorAll(".jsMarked").forEach((element) => element.classList.remove("jsMarked"));
+                    if (containerData.markAll !== null) {
+                        containerData.markAll.checked = false;
+                        this.setParentAsMarked(containerData.markAll, false);
                     }
-                }).bind(this));
+                    Array.from(containerData.checkboxes).forEach((checkbox) => {
+                        checkbox.checked = false;
+                        this.setParentAsMarked(checkbox, false);
+                    });
+                    UiPageAction.remove(`wcfClipboard-${objectType}`);
+                });
                 return;
             }
-            _itemData = new ObjectMap();
-            _reloadPageOnSuccess = new Dictionary();
+            this.itemData = new WeakMap();
+            this.reloadPageOnSuccess.clear();
             // rebuild markings
-            _containers.forEach((function (containerData) {
-                var typeName = elData(containerData.element, 'type');
-                //noinspection JSUnresolvedVariable
-                var objectIds = (data.returnValues.markedItems && data.returnValues.markedItems.hasOwnProperty(typeName)) ? data.returnValues.markedItems[typeName] : [];
-                this._rebuildMarkings(containerData, objectIds);
-            }).bind(this));
-            var keepEditors = [], typeName;
-            if (data.returnValues && data.returnValues.items) {
-                for (typeName in data.returnValues.items) {
-                    if (data.returnValues.items.hasOwnProperty(typeName)) {
-                        keepEditors.push(typeName);
-                    }
-                }
-            }
+            const markings = Core.isPlainObject(data.returnValues.markedItems) ? data.returnValues.markedItems : {};
+            this.containers.forEach((containerData) => {
+                const typeName = containerData.element.dataset.type;
+                const objectIds = Array.isArray(markings[typeName]) ? markings[typeName] : [];
+                this.rebuildMarkings(containerData, objectIds);
+            });
+            const keepEditors = Object.keys(data.returnValues.items || {});
             // clear editors
-            _editors.forEach(function (editor, typeName) {
-                if (keepEditors.indexOf(typeName) === -1) {
-                    UiPageAction.remove('wcfClipboard-' + typeName);
-                    _editorDropdowns.get(typeName).innerHTML = '';
+            this.editors.forEach((editor, typeName) => {
+                if (keepEditors.includes(typeName)) {
+                    UiPageAction.remove(`wcfClipboard-${typeName}`);
+                    this.editorDropdowns.get(typeName).innerHTML = "";
                 }
             });
             // no items
-            if (!data.returnValues || !data.returnValues.items) {
+            if (!data.returnValues.items) {
                 return;
             }
             // rebuild editors
-            var actionName, created, dropdown, editor, typeData;
-            var divider, item, itemData, itemIndex, label, unmarkAll;
-            for (typeName in data.returnValues.items) {
-                if (!data.returnValues.items.hasOwnProperty(typeName)) {
-                    continue;
-                }
-                typeData = data.returnValues.items[typeName];
-                //noinspection JSUnresolvedVariable
-                _reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess);
-                created = false;
-                editor = _editors.get(typeName);
-                dropdown = _editorDropdowns.get(typeName);
+            Object.entries(data.returnValues.items).forEach(([typeName, typeData]) => {
+                this.reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess);
+                let created = false;
+                let editor = this.editors.get(typeName);
+                let dropdown = this.editorDropdowns.get(typeName);
                 if (editor === undefined) {
                     created = true;
-                    editor = elCreate('a');
-                    editor.className = 'dropdownToggle';
+                    editor = document.createElement("a");
+                    editor.className = "dropdownToggle";
                     editor.textContent = typeData.label;
-                    _editors.set(typeName, editor);
-                    dropdown = elCreate('ol');
-                    dropdown.className = 'dropdownMenu';
-                    _editorDropdowns.set(typeName, dropdown);
+                    this.editors.set(typeName, editor);
+                    dropdown = document.createElement("ol");
+                    dropdown.className = "dropdownMenu";
+                    this.editorDropdowns.set(typeName, dropdown);
                 }
                 else {
                     editor.textContent = typeData.label;
-                    dropdown.innerHTML = '';
+                    dropdown.innerHTML = "";
                 }
                 // create editor items
-                for (itemIndex in typeData.items) {
-                    if (!typeData.items.hasOwnProperty(itemIndex)) {
-                        continue;
-                    }
-                    itemData = typeData.items[itemIndex];
-                    item = elCreate('li');
-                    label = elCreate('span');
+                Object.values(typeData.items).forEach((itemData) => {
+                    const item = document.createElement("li");
+                    const label = document.createElement("span");
                     label.textContent = itemData.label;
                     item.appendChild(label);
                     dropdown.appendChild(item);
-                    elData(item, 'type', typeName);
-                    item.addEventListener('click', _callbackItem);
-                    _itemData.set(item, itemData);
-                }
-                divider = elCreate('li');
-                divider.classList.add('dropdownDivider');
+                    item.dataset.type = typeName;
+                    item.addEventListener("click", (ev) => this.executeAction(ev));
+                    this.itemData.set(item, itemData);
+                });
+                const divider = document.createElement("li");
+                divider.classList.add("dropdownDivider");
                 dropdown.appendChild(divider);
                 // add 'unmark all'
-                unmarkAll = elCreate('li');
-                elData(unmarkAll, 'type', typeName);
-                label = elCreate('span');
-                label.textContent = Language.get('wcf.clipboard.item.unmarkAll');
+                const unmarkAll = document.createElement("li");
+                unmarkAll.dataset.type = typeName;
+                const label = document.createElement("span");
+                label.textContent = Language.get("wcf.clipboard.item.unmarkAll");
                 unmarkAll.appendChild(label);
-                unmarkAll.addEventListener('click', _callbackUnmarkAll);
+                unmarkAll.addEventListener("click", (ev) => this.unmarkAll(ev));
                 dropdown.appendChild(unmarkAll);
                 if (keepEditors.indexOf(typeName) !== -1) {
-                    actionName = 'wcfClipboard-' + typeName;
+                    const actionName = `wcfClipboard-${typeName}`;
                     if (UiPageAction.has(actionName)) {
                         UiPageAction.show(actionName);
                     }
@@ -536,73 +441,114 @@ define([
                     }
                 }
                 if (created) {
-                    editor.parentNode.classList.add('dropdown');
-                    editor.parentNode.appendChild(dropdown);
-                    UiSimpleDropdown.init(editor);
+                    const parent = editor.parentElement;
+                    parent.classList.add("dropdown");
+                    parent.appendChild(dropdown);
+                    Simple_1.default.init(editor);
                 }
-            }
-        },
+            });
+        }
         /**
          * Rebuilds the mark state for each item.
-         *
-         * @param      {Object}        data            container data
-         * @param      {int[]}         objectIds       item object ids
          */
-        _rebuildMarkings: function (data, objectIds) {
-            var markAll = true;
-            for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                var checkbox = data.checkboxes[i];
-                var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-                var isMarked = (objectIds.indexOf(~~elData(checkbox, 'object-id')) !== -1);
-                if (!isMarked)
+        rebuildMarkings(data, objectIds) {
+            let markAll = true;
+            Array.from(data.checkboxes).forEach((checkbox) => {
+                const clipboardObject = checkbox.closest(".jsClipboardObject");
+                const isMarked = objectIds.includes(~~checkbox.dataset.objectId);
+                if (!isMarked) {
                     markAll = false;
+                }
                 checkbox.checked = isMarked;
-                clipboardObject.classList[(isMarked ? 'add' : 'remove')]('jsMarked');
-                if (elAttr(checkbox.parentNode, 'role') === 'checkbox') {
-                    elAttr(checkbox.parentNode, 'aria-checked', isMarked);
+                if (isMarked) {
+                    clipboardObject.classList.add("jsMarked");
                 }
-            }
+                else {
+                    clipboardObject.classList.remove("jsMarked");
+                }
+                this.setParentAsMarked(checkbox, isMarked);
+            });
             if (data.markAll !== null) {
                 data.markAll.checked = markAll;
-                if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') {
-                    elAttr(data.markAll.parentNode, 'aria-checked', markAll);
-                }
-                var parent = data.markAll;
-                while (parent = parent.parentNode) {
-                    if (parent instanceof Element && parent.classList.contains('columnMark')) {
-                        parent = parent.parentNode;
-                        break;
-                    }
-                }
+                this.setParentAsMarked(data.markAll, markAll);
+                const parent = data.markAll.closest(".columnMark");
                 if (parent) {
-                    parent.classList[(markAll ? 'add' : 'remove')]('jsMarked');
+                    if (markAll) {
+                        parent.classList.add("jsMarked");
+                    }
+                    else {
+                        parent.classList.remove("jsMarked");
+                    }
                 }
             }
-        },
+        }
+        setParentAsMarked(element, isMarked) {
+            const parent = element.parentElement;
+            if (parent.getAttribute("role") === "checkbox") {
+                parent.setAttribute("aria-checked", isMarked ? "true" : "false");
+            }
+        }
         /**
          * Hides the clipboard editor for the given object type.
-         *
-         * @param      {string}        objectType
          */
-        hideEditor: function (objectType) {
-            UiPageAction.remove('wcfClipboard-' + objectType);
+        hideEditor(objectType) {
+            UiPageAction.remove("wcfClipboard-" + objectType);
             UiScreen.pageOverlayOpen();
-        },
+        }
         /**
          * Shows the clipboard editor.
          */
-        showEditor: function () {
-            this._loadMarkedItems();
+        showEditor() {
+            this.loadMarkedItems();
             UiScreen.pageOverlayClose();
-        },
+        }
         /**
          * Unmarks the objects with given clipboard object type and ids.
-         *
-         * @param      {string}        objectType
-         * @param      {int[]}         objectIds
          */
-        unmark: function (objectType, objectIds) {
-            this._saveState(objectType, objectIds, false);
+        unmark(objectType, objectIds) {
+            this.saveState(objectType, objectIds, false);
         }
-    };
+    }
+    let controllerClipboard;
+    function getControllerClipboard() {
+        if (!controllerClipboard) {
+            controllerClipboard = new ControllerClipboard();
+        }
+        return controllerClipboard;
+    }
+    /**
+     * Initializes the clipboard API handler.
+     */
+    function setup(options) {
+        getControllerClipboard().setup(options);
+    }
+    exports.setup = setup;
+    /**
+     * Reloads the clipboard data.
+     */
+    function reload() {
+        getControllerClipboard().reload();
+    }
+    exports.reload = reload;
+    /**
+     * Hides the clipboard editor for the given object type.
+     */
+    function hideEditor(objectType) {
+        getControllerClipboard().hideEditor(objectType);
+    }
+    exports.hideEditor = hideEditor;
+    /**
+     * Shows the clipboard editor.
+     */
+    function showEditor() {
+        getControllerClipboard().showEditor();
+    }
+    exports.showEditor = showEditor;
+    /**
+     * Unmarks the objects with given clipboard object type and ids.
+     */
+    function unmark(objectType, objectIds) {
+        getControllerClipboard().unmark(objectType, objectIds);
+    }
+    exports.unmark = unmark;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.js
deleted file mode 100644 (file)
index 25c0568..0000000
+++ /dev/null
@@ -1,732 +0,0 @@
-/**
- * Clipboard API 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/Controller/Clipboard
- */
-define(
-       [
-               'Ajax',         'Core',     'Dictionary',      'EventHandler',
-               'Language',     'List',     'ObjectMap',       'Dom/ChangeListener',
-               'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown',
-               'WoltLabSuite/Core/Ui/Page/Action', 'Ui/Screen'
-       ],
-       function(
-               Ajax,            Core,       Dictionary,        EventHandler,
-               Language,        List,       ObjectMap,         DomChangeListener,
-               DomTraverse,     DomUtil,    UiConfirmation,    UiSimpleDropdown,
-               UiPageAction,    UiScreen
-       )
-{
-       "use strict";
-       
-       if (!COMPILER_TARGET_DEFAULT) {
-               return {
-                       setup: function() {},
-                       reload: function() {},
-                       _initContainers: function() {},
-                       _loadMarkedItems: function() {},
-                       _markAll: function() {},
-                       _mark: function() {},
-                       _saveState: function() {},
-                       _executeAction: function() {},
-                       _executeProxyAction: function() {},
-                       _unmarkAll: function() {},
-                       _ajaxSetup: function() {},
-                       _ajaxSuccess: function() {},
-                       _rebuildMarkings: function() {},
-                       hideEditor: function() {},
-                       showEditor: function() {},
-                       unmark: function() {}
-               };
-       }
-       
-       var _containers = new Dictionary();
-       var _editors = new Dictionary();
-       var _editorDropdowns = new Dictionary();
-       var _elements = elByClass('jsClipboardContainer');
-       var _itemData = new ObjectMap();
-       var _knownCheckboxes = new List();
-       var _options = {};
-       var _reloadPageOnSuccess = new Dictionary();
-       
-       var _callbackCheckbox = null;
-       var _callbackItem = null;
-       var _callbackUnmarkAll = null;
-       
-       var _specialCheckboxSelector = '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]';
-       
-       /**
-        * Clipboard API
-        * 
-        * @exports     WoltLabSuite/Core/Controller/Clipboard
-        */
-       return {
-               /**
-                * Initializes the clipboard API handler.
-                * 
-                * @param       {Object}        options         initialization options
-                */
-               setup: function(options) {
-                       if (!options.pageClassName) {
-                               throw new Error("Expected a non-empty string for parameter 'pageClassName'.");
-                       }
-                       
-                       if (_callbackCheckbox === null) {
-                               _callbackCheckbox = this._mark.bind(this);
-                               _callbackItem = this._executeAction.bind(this);
-                               _callbackUnmarkAll = this._unmarkAll.bind(this);
-                               
-                               _options = Core.extend({
-                                       hasMarkedItems: false,
-                                       pageClassNames: [options.pageClassName],
-                                       pageObjectId: 0
-                               }, options);
-                               
-                               delete _options.pageClassName;
-                       }
-                       else {
-                               if (options.pageObjectId) {
-                                       throw new Error("Cannot load secondary clipboard with page object id set.");
-                               }
-                               
-                               _options.pageClassNames.push(options.pageClassName);
-                       }
-                       
-                       if (!Element.prototype.matches) {
-                               Element.prototype.matches = Element.prototype.msMatchesSelector;
-                       }
-                       
-                       this._initContainers();
-                       
-                       if (_options.hasMarkedItems && _elements.length) {
-                               this._loadMarkedItems();
-                       }
-                       
-                       DomChangeListener.add('WoltLabSuite/Core/Controller/Clipboard', this._initContainers.bind(this));
-               },
-               
-               /**
-                * Reloads the clipboard data.
-                */
-               reload: function() {
-                       if (_containers.size) {
-                               this._loadMarkedItems();
-                       }
-               },
-               
-               /**
-                * Initializes clipboard containers.
-                */
-               _initContainers: function() {
-                       for (var i = 0, length = _elements.length; i < length; i++) {
-                               var container = _elements[i];
-                               var containerId = DomUtil.identify(container);
-                               var containerData = _containers.get(containerId);
-                               
-                               if (containerData === undefined) {
-                                       var markAll = elBySel('.jsClipboardMarkAll', container);
-                                       
-                                       if (markAll !== null) {
-                                               if (markAll.matches(_specialCheckboxSelector)) {
-                                                       var label = markAll.closest('label');
-                                                       elAttr(label, 'role', 'checkbox');
-                                                       elAttr(label, 'tabindex', '0');
-                                                       elAttr(label, 'aria-checked', false);
-                                                       elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.markAll'));
-                                                       
-                                                       label.addEventListener('keyup', function (event) {
-                                                               if (event.keyCode === 13 || event.keyCode === 32) {
-                                                                       checkbox.click();
-                                                               }
-                                                       });
-                                               }
-                                               
-                                               elData(markAll, 'container-id', containerId);
-                                               markAll.addEventListener('click', this._markAll.bind(this));
-                                       }
-                                       
-                                       containerData = {
-                                               checkboxes: elByClass('jsClipboardItem', container),
-                                               element: container,
-                                               markAll: markAll,
-                                               markedObjectIds: new List()
-                                       };
-                                       _containers.set(containerId, containerData);
-                               }
-                               
-                               for (var j = 0, innerLength = containerData.checkboxes.length; j < innerLength; j++) {
-                                       var checkbox = containerData.checkboxes[j];
-                                       
-                                       if (!_knownCheckboxes.has(checkbox)) {
-                                               elData(checkbox, 'container-id', containerId);
-                                               
-                                               (function(checkbox) {
-                                                       if (checkbox.matches(_specialCheckboxSelector)) {
-                                                               var label = checkbox.closest('label');
-                                                               elAttr(label, 'role', 'checkbox');
-                                                               elAttr(label, 'tabindex', '0');
-                                                               elAttr(label, 'aria-checked', false);
-                                                               elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.mark'));
-                                                               
-                                                               label.addEventListener('keyup', function (event) {
-                                                                       if (event.keyCode === 13 || event.keyCode === 32) {
-                                                                               checkbox.click();
-                                                                       }
-                                                               });
-                                                       }
-                                                       
-                                                       var link = checkbox.closest('a');
-                                                       if (link === null) {
-                                                               checkbox.addEventListener('click', _callbackCheckbox);
-                                                       }
-                                                       else {
-                                                               // Firefox will always trigger the link if the checkbox is
-                                                               // inside of one. Since 2000. Thanks Firefox. 
-                                                               checkbox.addEventListener('click', function (event) {
-                                                                       event.preventDefault();
-                                                                       
-                                                                       window.setTimeout(function () {
-                                                                               checkbox.checked = !checkbox.checked;
-                                                                               
-                                                                               _callbackCheckbox(null, checkbox);
-                                                                       }, 10);
-                                                               });
-                                                       }
-                                               })(checkbox);
-                                               
-                                               _knownCheckboxes.add(checkbox);
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Loads marked items from clipboard.
-                */
-               _loadMarkedItems: function() {
-                       Ajax.api(this, {
-                               actionName: 'getMarkedItems',
-                               parameters: {
-                                       pageClassNames: _options.pageClassNames,
-                                       pageObjectID: _options.pageObjectId
-                               }
-                       });
-               },
-               
-               /**
-                * Marks or unmarks all visible items at once.
-                * 
-                * @param       {object}        event   event object
-                */
-               _markAll: function(event) {
-                       var checkbox = event.currentTarget;
-                       var isMarked = (checkbox.nodeName !== 'INPUT' || checkbox.checked);
-                       
-                       if (elAttr(checkbox.parentNode, 'role') === 'checkbox') {
-                               elAttr(checkbox.parentNode, 'aria-checked', isMarked);
-                       }
-                       
-                       var objectIds = [];
-                       
-                       var containerId = elData(checkbox, 'container-id');
-                       var data = _containers.get(containerId);
-                       var type = elData(data.element, 'type');
-                       
-                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                               var item = data.checkboxes[i];
-                               var objectId = ~~elData(item, 'object-id');
-                               
-                               if (isMarked) {
-                                       if (!item.checked) {
-                                               item.checked = true;
-                                               
-                                               data.markedObjectIds.add(objectId);
-                                               objectIds.push(objectId);
-                                       }
-                               }
-                               else {
-                                       if (item.checked) {
-                                               item.checked = false;
-                                               
-                                               data.markedObjectIds['delete'](objectId);
-                                               objectIds.push(objectId);
-                                       }
-                               }
-                               
-                               if (elAttr(item.parentNode, 'role') === 'checkbox') {
-                                       elAttr(item.parentNode, 'aria-checked', isMarked);
-                               }
-                               
-                               var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-                               if (clipboardObject !== null) {
-                                       clipboardObject.classList[(isMarked ? 'addClass' : 'removeClass')]('jsMarked');
-                               }
-                       }
-                       
-                       this._saveState(type, objectIds, isMarked);
-               },
-               
-               /**
-                * Marks or unmarks an individual item.
-                * 
-                * @param       {object}        event           event object
-                * @param       {Element=}      checkbox        checkbox element
-                */
-               _mark: function(event, checkbox) {
-                       checkbox = (event instanceof Event) ? event.currentTarget : checkbox;
-                       var objectId = ~~elData(checkbox, 'object-id');
-                       var isMarked = checkbox.checked;
-                       var containerId = elData(checkbox, 'container-id');
-                       var data = _containers.get(containerId);
-                       var type = elData(data.element, 'type');
-                       
-                       var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-                       data.markedObjectIds[(isMarked ? 'add' : 'delete')](objectId);
-                       clipboardObject.classList[(isMarked) ? 'add' : 'remove']('jsMarked');
-                       
-                       if (data.markAll !== null) {
-                               var markedAll = true;
-                               for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                                       if (!data.checkboxes[i].checked) {
-                                               markedAll = false;
-                                               
-                                               break;
-                                       }
-                               }
-                               
-                               data.markAll.checked = markedAll;
-                               
-                               if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') {
-                                       elAttr(data.markAll.parentNode, 'aria-checked', isMarked);
-                               }
-                       }
-                       
-                       if (elAttr(checkbox.parentNode, 'role') === 'checkbox') {
-                               elAttr(checkbox.parentNode, 'aria-checked', checkbox.checked);
-                       }
-                       
-                       this._saveState(type, [ objectId ], isMarked);
-               },
-               
-               /**
-                * Saves the state for given item object ids.
-                * 
-                * @param       {string}        type            object type
-                * @param       {int[]}         objectIds       item object ids
-                * @param       {boolean}       isMarked        true if marked
-                */
-               _saveState: function(type, objectIds, isMarked) {
-                       Ajax.api(this, {
-                               actionName: (isMarked ? 'mark' : 'unmark'),
-                               parameters: {
-                                       pageClassNames: _options.pageClassNames,
-                                       pageObjectID: _options.pageObjectId,
-                                       objectIDs: objectIds,
-                                       objectType: type
-                               }
-                       });
-               },
-               
-               /**
-                * Executes an editor action.
-                * 
-                * @param       {object}        event           event object
-                */
-               _executeAction: function(event) {
-                       var listItem = event.currentTarget;
-                       var data = _itemData.get(listItem);
-                       
-                       if (data.url) {
-                               window.location.href = data.url;
-                               return;
-                       }
-                       
-                       var triggerEvent = function() {
-                               var type = elData(listItem, 'type');
-                               
-                               EventHandler.fire('com.woltlab.wcf.clipboard', type, {
-                                       data: data,
-                                       listItem: listItem,
-                                       responseData: null
-                               });
-                       };
-                       
-                       //noinspection JSUnresolvedVariable
-                       var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : '';
-                       var fireEvent = true;
-                       
-                       if (typeof data.parameters === 'object' && data.parameters.actionName && data.parameters.className) {
-                               if (data.parameters.actionName === 'unmarkAll' || Array.isArray(data.parameters.objectIDs)) {
-                                       if (confirmMessage.length) {
-                                               //noinspection JSUnresolvedVariable
-                                               var template = (typeof data.internalData.template === 'string') ? data.internalData.template : '';
-                                               
-                                               UiConfirmation.show({
-                                                       confirm: (function() {
-                                                               var formData = {};
-                                                               
-                                                               if (template.length) {
-                                                                       var items = elBySelAll('input, select, textarea', UiConfirmation.getContentElement());
-                                                                       for (var i = 0, length = items.length; i < length; i++) {
-                                                                               var item = items[i];
-                                                                               var name = elAttr(item, 'name');
-                                                                               
-                                                                               switch (item.nodeName) {
-                                                                                       case 'INPUT':
-                                                                                               if ((item.type !== "checkbox" && item.type !== "radio") || item.checked) {
-                                                                                                       formData[name] = elAttr(item, 'value');
-                                                                                               }
-                                                                                               break;
-                                                                                       
-                                                                                       case 'SELECT':
-                                                                                               formData[name] = item.value;
-                                                                                               break;
-                                                                                       
-                                                                                       case 'TEXTAREA':
-                                                                                               formData[name] = item.value.trim();
-                                                                                               break;
-                                                                               }
-                                                                       }
-                                                               }
-                                                               
-                                                               //noinspection JSUnresolvedFunction
-                                                               this._executeProxyAction(listItem, data, formData);
-                                                       }).bind(this),
-                                                       message: confirmMessage,
-                                                       template: template
-                                               });
-                                       }
-                                       else {
-                                               this._executeProxyAction(listItem, data);
-                                       }
-                               }
-                       }
-                       else if (confirmMessage.length) {
-                               fireEvent = false;
-                               
-                               UiConfirmation.show({
-                                       confirm: triggerEvent,
-                                       message: confirmMessage
-                               });
-                       }
-                       
-                       if (fireEvent) {
-                               triggerEvent();
-                       }
-               },
-               
-               /**
-                * Forwards clipboard actions to an individual handler.
-                * 
-                * @param       {Element}       listItem        dropdown item element
-                * @param       {Object}        data            action data
-                * @param       {Object?}       formData        form data
-                */
-               _executeProxyAction: function(listItem, data, formData) {
-                       formData = formData || {};
-                       
-                       var objectIds = (data.parameters.actionName !== 'unmarkAll') ? data.parameters.objectIDs : [];
-                       var parameters = { data: formData };
-                       
-                       //noinspection JSUnresolvedVariable
-                       if (typeof data.internalData.parameters === 'object') {
-                               //noinspection JSUnresolvedVariable
-                               for (var key in data.internalData.parameters) {
-                                       //noinspection JSUnresolvedVariable
-                                       if (data.internalData.parameters.hasOwnProperty(key)) {
-                                               //noinspection JSUnresolvedVariable
-                                               parameters[key] = data.internalData.parameters[key];
-                                       }
-                               }
-                       }
-                       
-                       Ajax.api(this, {
-                               actionName: data.parameters.actionName,
-                               className: data.parameters.className,
-                               objectIDs: objectIds,
-                               parameters: parameters
-                       }, (function(responseData) {
-                               if (data.actionName !== 'unmarkAll') {
-                                       var type = elData(listItem, 'type');
-                                       
-                                       EventHandler.fire('com.woltlab.wcf.clipboard', type, {
-                                               data: data,
-                                               listItem: listItem,
-                                               responseData: responseData
-                                       });
-                                       
-                                       if (_reloadPageOnSuccess.has(type) && _reloadPageOnSuccess.get(type).indexOf(responseData.actionName) !== -1) {
-                                               window.location.reload();
-                                               return;
-                                       }
-                               }
-                               
-                               this._loadMarkedItems();
-                       }).bind(this));
-               },
-               
-               /**
-                * Unmarks all clipboard items for an object type.
-                * 
-                * @param       {object}        event           event object
-                */
-               _unmarkAll: function(event) {
-                       var type = elData(event.currentTarget, 'type');
-                       
-                       Ajax.api(this, {
-                               actionName: 'unmarkAll',
-                               parameters: {
-                                       objectType: type
-                               }
-                       });
-               },
-               
-               /**
-                * Sets up ajax request object.
-                * 
-                * @return      {object}        request options
-                */
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: 'wcf\\data\\clipboard\\item\\ClipboardItemAction'
-                               }
-                       };
-               },
-               
-               /**
-                * Handles successful AJAX requests.
-                * 
-                * @param       {object}        data    response data
-                */
-               _ajaxSuccess: function(data) {
-                       if (data.actionName === 'unmarkAll') {
-                               _containers.forEach((function(containerData) {
-                                       if (elData(containerData.element, 'type') === data.returnValues.objectType) {
-                                               var clipboardObjects = elByClass('jsMarked', containerData.element);
-                                               while (clipboardObjects.length) {
-                                                       clipboardObjects[0].classList.remove('jsMarked');
-                                               }
-                                               
-                                               if (containerData.markAll !== null) {
-                                                       containerData.markAll.checked = false;
-                                                       
-                                                       if (elAttr(containerData.markAll.parentNode, 'role') === 'checkbox') {
-                                                               elAttr(containerData.markAll.parentNode, 'aria-checked', false);
-                                                       }
-                                               }
-                                               for (var i = 0, length = containerData.checkboxes.length; i < length; i++) {
-                                                       containerData.checkboxes[i].checked = false;
-                                                       
-                                                       if (elAttr(containerData.checkboxes[i].parentNode, 'role') === 'checkbox') {
-                                                               elAttr(containerData.checkboxes[i].parentNode, 'aria-checked', false);
-                                                       }
-                                               }
-                                               
-                                               UiPageAction.remove('wcfClipboard-' + data.returnValues.objectType);
-                                       }
-                               }).bind(this));
-                               
-                               return;
-                       }
-                       
-                       _itemData = new ObjectMap();
-                       _reloadPageOnSuccess = new Dictionary();
-                       
-                       // rebuild markings
-                       _containers.forEach((function(containerData) {
-                               var typeName = elData(containerData.element, 'type');
-                               
-                               //noinspection JSUnresolvedVariable
-                               var objectIds = (data.returnValues.markedItems && data.returnValues.markedItems.hasOwnProperty(typeName)) ? data.returnValues.markedItems[typeName] : [];
-                               this._rebuildMarkings(containerData, objectIds);
-                       }).bind(this));
-                       
-                       var keepEditors = [], typeName;
-                       if (data.returnValues && data.returnValues.items) {
-                               for (typeName in data.returnValues.items) {
-                                       if (data.returnValues.items.hasOwnProperty(typeName)) {
-                                               keepEditors.push(typeName);
-                                       }
-                               }
-                       }
-                       
-                       // clear editors
-                       _editors.forEach(function(editor, typeName) {
-                               if (keepEditors.indexOf(typeName) === -1) {
-                                       UiPageAction.remove('wcfClipboard-' + typeName);
-                                       
-                                       _editorDropdowns.get(typeName).innerHTML = '';
-                               }
-                       });
-                       
-                       // no items
-                       if (!data.returnValues || !data.returnValues.items) {
-                               return;
-                       }
-                       
-                       // rebuild editors
-                       var actionName, created, dropdown, editor, typeData;
-                       var divider, item, itemData, itemIndex, label, unmarkAll;
-                       for (typeName in data.returnValues.items) {
-                               if (!data.returnValues.items.hasOwnProperty(typeName)) {
-                                       continue;
-                               }
-                               
-                               typeData = data.returnValues.items[typeName];
-                               //noinspection JSUnresolvedVariable
-                               _reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess);
-                               created = false;
-                               
-                               editor = _editors.get(typeName);
-                               dropdown = _editorDropdowns.get(typeName);
-                               if (editor === undefined) {
-                                       created = true;
-                                       
-                                       editor = elCreate('a');
-                                       editor.className = 'dropdownToggle';
-                                       editor.textContent = typeData.label;
-                                       
-                                       _editors.set(typeName, editor);
-                                       
-                                       dropdown = elCreate('ol');
-                                       dropdown.className = 'dropdownMenu';
-                                       
-                                       _editorDropdowns.set(typeName, dropdown);
-                               }
-                               else {
-                                       editor.textContent = typeData.label;
-                                       dropdown.innerHTML = '';
-                               }
-                               
-                               // create editor items
-                               for (itemIndex in typeData.items) {
-                                       if (!typeData.items.hasOwnProperty(itemIndex)) {
-                                               continue;
-                                       }
-                                       
-                                       itemData = typeData.items[itemIndex];
-                                       
-                                       item = elCreate('li');
-                                       label = elCreate('span');
-                                       label.textContent = itemData.label;
-                                       item.appendChild(label);
-                                       dropdown.appendChild(item);
-                                       
-                                       elData(item, 'type', typeName);
-                                       item.addEventListener('click', _callbackItem);
-                                       
-                                       _itemData.set(item, itemData);
-                               }
-                               
-                               divider = elCreate('li');
-                               divider.classList.add('dropdownDivider');
-                               dropdown.appendChild(divider);
-                               
-                               // add 'unmark all'
-                               unmarkAll = elCreate('li');
-                               elData(unmarkAll, 'type', typeName);
-                               label = elCreate('span');
-                               label.textContent = Language.get('wcf.clipboard.item.unmarkAll');
-                               unmarkAll.appendChild(label);
-                               unmarkAll.addEventListener('click', _callbackUnmarkAll);
-                               dropdown.appendChild(unmarkAll);
-                               
-                               if (keepEditors.indexOf(typeName) !== -1) {
-                                       actionName = 'wcfClipboard-' + typeName;
-                                       
-                                       if (UiPageAction.has(actionName)) {
-                                               UiPageAction.show(actionName);
-                                       }
-                                       else {
-                                               UiPageAction.add(actionName, editor);
-                                       }
-                               }
-                               
-                               if (created) {
-                                       editor.parentNode.classList.add('dropdown');
-                                       editor.parentNode.appendChild(dropdown);
-                                       UiSimpleDropdown.init(editor);
-                               }
-                       }
-               },
-               
-               /**
-                * Rebuilds the mark state for each item.
-                * 
-                * @param       {Object}        data            container data
-                * @param       {int[]}         objectIds       item object ids
-                */
-               _rebuildMarkings: function(data, objectIds) {
-                       var markAll = true;
-                       
-                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                               var checkbox = data.checkboxes[i];
-                               var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-                               
-                               var isMarked = (objectIds.indexOf(~~elData(checkbox, 'object-id')) !== -1);
-                               if (!isMarked) markAll = false;
-                               
-                               checkbox.checked = isMarked;
-                               clipboardObject.classList[(isMarked ? 'add' : 'remove')]('jsMarked');
-                               
-                               if (elAttr(checkbox.parentNode, 'role') === 'checkbox') {
-                                       elAttr(checkbox.parentNode, 'aria-checked', isMarked);
-                               }
-                       }
-                       
-                       if (data.markAll !== null) {
-                               data.markAll.checked = markAll;
-                               
-                               if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') {
-                                       elAttr(data.markAll.parentNode, 'aria-checked', markAll);
-                               }
-                               
-                               var parent = data.markAll;
-                               while (parent = parent.parentNode) {
-                                       if (parent instanceof Element && parent.classList.contains('columnMark')) {
-                                               parent = parent.parentNode;
-                                               break;
-                                       }
-                               }
-                               
-                               if (parent) {
-                                       parent.classList[(markAll ? 'add' : 'remove')]('jsMarked');
-                               }
-                       }
-               },
-               
-               /**
-                * Hides the clipboard editor for the given object type.
-                * 
-                * @param       {string}        objectType
-                */
-               hideEditor: function(objectType) {
-                       UiPageAction.remove('wcfClipboard-' + objectType);
-                       
-                       UiScreen.pageOverlayOpen();
-               },
-               
-               /**
-                * Shows the clipboard editor.
-                */
-               showEditor: function() {
-                       this._loadMarkedItems();
-                       
-                       UiScreen.pageOverlayClose();
-               },
-               
-               /**
-                * Unmarks the objects with given clipboard object type and ids.
-                * 
-                * @param       {string}        objectType
-                * @param       {int[]}         objectIds
-                */
-               unmark: function(objectType, objectIds) {
-                       this._saveState(objectType, objectIds, false);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.ts
new file mode 100644 (file)
index 0000000..8919e63
--- /dev/null
@@ -0,0 +1,710 @@
+/**
+ * Clipboard API 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/Controller/Clipboard
+ */
+
+import * as Ajax from "../Ajax";
+import { AjaxCallbackSetup } from "../Ajax/Data";
+import * as Core from "../Core";
+import DomChangeListener from "../Dom/Change/Listener";
+import DomUtil from "../Dom/Util";
+import * as EventHandler from "../Event/Handler";
+import * as Language from "../Language";
+import * as UiConfirmation from "../Ui/Confirmation";
+import UiDropdownSimple from "../Ui/Dropdown/Simple";
+import * as UiPageAction from "../Ui/Page/Action";
+import * as UiScreen from "../Ui/Screen";
+
+interface ClipboardOptions {
+  hasMarkedItems: boolean;
+  pageClassName: string;
+  pageObjectId: number;
+}
+
+interface ContainerData {
+  checkboxes: HTMLCollectionOf<HTMLInputElement>;
+  element: HTMLElement;
+  markAll: HTMLInputElement | null;
+  markedObjectIds: Set<number>;
+}
+
+interface ItemData {
+  items: { [key: string]: ClipboardActionData };
+  label: string;
+  reloadPageOnSuccess: string[];
+}
+
+interface ClipboardActionData {
+  actionName: string;
+  internalData: ArbitraryObject;
+  label: string;
+  parameters: {
+    actionName?: string;
+    className?: string;
+    objectIDs: number[];
+    template: string;
+  };
+  url: string;
+}
+
+interface AjaxResponseMarkedItems {
+  [key: string]: number[];
+}
+
+interface AjaxResponse {
+  actionName: string;
+  returnValues: {
+    action: string;
+    items?: {
+      // They key is the `typeName`
+      [key: string]: ItemData;
+    };
+    markedItems?: AjaxResponseMarkedItems;
+    objectType: string;
+  };
+}
+
+const _specialCheckboxSelector =
+  '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]';
+
+class ControllerClipboard {
+  private readonly containers = new Map<string, ContainerData>();
+  private readonly editors = new Map<string, HTMLAnchorElement>();
+  private readonly editorDropdowns = new Map<string, HTMLOListElement>();
+  private itemData = new WeakMap<HTMLLIElement, ClipboardActionData>();
+  private readonly knownCheckboxes = new WeakSet<HTMLInputElement>();
+  private readonly pageClassNames: string[] = [];
+  private pageObjectId = 0;
+  private readonly reloadPageOnSuccess = new Map<string, string[]>();
+
+  /**
+   * Initializes the clipboard API handler.
+   */
+  setup(options: ClipboardOptions) {
+    if (!options.pageClassName) {
+      throw new Error("Expected a non-empty string for parameter 'pageClassName'.");
+    }
+
+    let hasMarkedItems = false;
+    if (this.pageClassNames.length === 0) {
+      hasMarkedItems = options.hasMarkedItems;
+      this.pageObjectId = options.pageObjectId;
+    }
+
+    this.pageClassNames.push(options.pageClassName);
+
+    this.initContainers();
+
+    if (hasMarkedItems && this.containers.size) {
+      this.loadMarkedItems();
+    }
+
+    DomChangeListener.add("WoltLabSuite/Core/Controller/Clipboard", () => this.initContainers());
+  }
+
+  /**
+   * Reloads the clipboard data.
+   */
+  reload(): void {
+    if (this.containers.size) {
+      this.loadMarkedItems();
+    }
+  }
+
+  /**
+   * Initializes clipboard containers.
+   */
+  private initContainers(): void {
+    document.querySelectorAll(".jsClipboardContainer").forEach((container: HTMLElement) => {
+      const containerId = DomUtil.identify(container);
+
+      let containerData = this.containers.get(containerId);
+      if (containerData === undefined) {
+        const markAll = container.querySelector(".jsClipboardMarkAll") as HTMLInputElement;
+
+        if (markAll !== null) {
+          if (markAll.matches(_specialCheckboxSelector)) {
+            const label = markAll.closest("label") as HTMLLabelElement;
+            label.setAttribute("role", "checkbox");
+            label.tabIndex = 0;
+            label.setAttribute("aria-checked", "false");
+            label.setAttribute("aria-label", Language.get("wcf.clipboard.item.markAll"));
+
+            label.addEventListener("keyup", (event) => {
+              if (event.key === "Enter" || event.key === "Space") {
+                markAll.click();
+              }
+            });
+          }
+
+          markAll.dataset.containerId = containerId;
+          markAll.addEventListener("click", (ev) => this.markAll(ev));
+        }
+
+        containerData = {
+          checkboxes: container.getElementsByClassName("jsClipboardItem") as HTMLCollectionOf<HTMLInputElement>,
+          element: container,
+          markAll: markAll,
+          markedObjectIds: new Set<number>(),
+        };
+        this.containers.set(containerId, containerData);
+      }
+
+      Array.from(containerData.checkboxes).forEach((checkbox) => {
+        if (this.knownCheckboxes.has(checkbox)) {
+          return;
+        }
+
+        checkbox.dataset.containerId = containerId;
+
+        if (checkbox.matches(_specialCheckboxSelector)) {
+          const label = checkbox.closest("label") as HTMLLabelElement;
+          label.setAttribute("role", "checkbox");
+          label.tabIndex = 0;
+          label.setAttribute("aria-checked", "false");
+          label.setAttribute("aria-label", Language.get("wcf.clipboard.item.mark"));
+
+          label.addEventListener("keyup", (event) => {
+            if (event.key === "Enter" || event.key === "Space") {
+              checkbox.click();
+            }
+          });
+        }
+
+        const link = checkbox.closest("a");
+        if (link === null) {
+          checkbox.addEventListener("click", (ev) => this.mark(ev));
+        } else {
+          // Firefox will always trigger the link if the checkbox is
+          // inside of one. Since 2000. Thanks Firefox.
+          checkbox.addEventListener("click", (event) => {
+            event.preventDefault();
+
+            window.setTimeout(() => {
+              checkbox.checked = !checkbox.checked;
+
+              this.mark(checkbox);
+            }, 10);
+          });
+        }
+
+        this.knownCheckboxes.add(checkbox);
+      });
+    });
+  }
+
+  /**
+   * Loads marked items from clipboard.
+   */
+  private loadMarkedItems(): void {
+    Ajax.api(this, {
+      actionName: "getMarkedItems",
+      parameters: {
+        pageClassNames: this.pageClassNames,
+        pageObjectID: this.pageObjectId,
+      },
+    });
+  }
+
+  /**
+   * Marks or unmarks all visible items at once.
+   */
+  private markAll(event: MouseEvent): void {
+    const checkbox = event.currentTarget as HTMLInputElement;
+    const isMarked = checkbox.nodeName !== "INPUT" || checkbox.checked;
+
+    this.setParentAsMarked(checkbox, isMarked);
+
+    const objectIds: number[] = [];
+
+    const containerId = checkbox.dataset.containerId!;
+    const data = this.containers.get(containerId)!;
+    const type = data.element.dataset.type!;
+
+    Array.from(data.checkboxes).forEach((item) => {
+      const objectId = ~~item.dataset.objectId!;
+
+      if (isMarked) {
+        if (!item.checked) {
+          item.checked = true;
+
+          data.markedObjectIds.add(objectId);
+          objectIds.push(objectId);
+        }
+      } else {
+        if (item.checked) {
+          item.checked = false;
+
+          data.markedObjectIds["delete"](objectId);
+          objectIds.push(objectId);
+        }
+      }
+
+      this.setParentAsMarked(item, isMarked);
+
+      const clipboardObject = checkbox.closest(".jsClipboardObject");
+      if (clipboardObject !== null) {
+        if (isMarked) {
+          clipboardObject.classList.add("jsMarked");
+        } else {
+          clipboardObject.classList.remove("jsMarked");
+        }
+      }
+    });
+
+    this.saveState(type, objectIds, isMarked);
+  }
+
+  /**
+   * Marks or unmarks an individual item.
+   *
+   */
+  private mark(event: MouseEvent | HTMLInputElement): void {
+    const checkbox = event instanceof Event ? (event.currentTarget as HTMLInputElement) : event;
+
+    const objectId = ~~checkbox.dataset.objectId!;
+    const isMarked = checkbox.checked;
+    const containerId = checkbox.dataset.containerId!;
+    const data = this.containers.get(containerId)!;
+    const type = data.element.dataset.type!;
+
+    const clipboardObject = checkbox.closest(".jsClipboardObject") as HTMLElement;
+    if (isMarked) {
+      data.markedObjectIds.add(objectId);
+      clipboardObject.classList.add("jsMarked");
+    } else {
+      data.markedObjectIds.delete(objectId);
+      clipboardObject.classList.remove("jsMarked");
+    }
+
+    if (data.markAll !== null) {
+      data.markAll.checked = Array.from(data.checkboxes).some((item) => !item.checked);
+
+      this.setParentAsMarked(data.markAll, isMarked);
+    }
+
+    this.setParentAsMarked(checkbox, checkbox.checked);
+
+    this.saveState(type, [objectId], isMarked);
+  }
+
+  /**
+   * Saves the state for given item object ids.
+   */
+  private saveState(objectType: string, objectIds: number[], isMarked: boolean): void {
+    Ajax.api(this, {
+      actionName: isMarked ? "mark" : "unmark",
+      parameters: {
+        pageClassNames: this.pageClassNames,
+        pageObjectID: this.pageObjectId,
+        objectIDs: objectIds,
+        objectType,
+      },
+    });
+  }
+
+  /**
+   * Executes an editor action.
+   */
+  private executeAction(event: MouseEvent): void {
+    const listItem = event.currentTarget as HTMLLIElement;
+    const data = this.itemData.get(listItem)!;
+
+    if (data.url) {
+      window.location.href = data.url;
+      return;
+    }
+
+    function triggerEvent() {
+      const type = listItem.dataset.type!;
+
+      EventHandler.fire("com.woltlab.wcf.clipboard", type, {
+        data,
+        listItem,
+        responseData: null,
+      });
+    }
+
+    const message = typeof data.internalData.confirmMessage === "string" ? data.internalData.confirmMessage : "";
+    let fireEvent = true;
+
+    if (Core.isPlainObject(data.parameters) && data.parameters.actionName && data.parameters.className) {
+      if (data.parameters.actionName === "unmarkAll" || Array.isArray(data.parameters.objectIDs)) {
+        if (message.length) {
+          const template = typeof data.internalData.template === "string" ? data.internalData.template : "";
+
+          UiConfirmation.show({
+            confirm: () => {
+              const formData = {};
+
+              if (template.length) {
+                UiConfirmation.getContentElement()
+                  .querySelectorAll("input, select, textarea")
+                  .forEach((item: HTMLInputElement) => {
+                    const name = item.name;
+
+                    switch (item.nodeName) {
+                      case "INPUT":
+                        if ((item.type !== "checkbox" && item.type !== "radio") || item.checked) {
+                          formData[name] = item.value;
+                        }
+                        break;
+
+                      case "SELECT":
+                        formData[name] = item.value;
+                        break;
+
+                      case "TEXTAREA":
+                        formData[name] = item.value.trim();
+                        break;
+                    }
+                  });
+              }
+
+              this._executeProxyAction(listItem, data, formData);
+            },
+            message,
+            template,
+          });
+        } else {
+          this._executeProxyAction(listItem, data);
+        }
+      }
+    } else if (message.length) {
+      fireEvent = false;
+
+      UiConfirmation.show({
+        confirm: triggerEvent,
+        message,
+      });
+    }
+
+    if (fireEvent) {
+      triggerEvent();
+    }
+  }
+
+  /**
+   * Forwards clipboard actions to an individual handler.
+   */
+  private _executeProxyAction(
+    listItem: HTMLLIElement,
+    data: ClipboardActionData,
+    formData: ArbitraryObject = {},
+  ): void {
+    const objectIds = data.parameters.actionName !== "unmarkAll" ? data.parameters.objectIDs : [];
+    const parameters = { data: formData };
+
+    if (Core.isPlainObject(data.internalData.parameters)) {
+      Object.entries(data.internalData.parameters as ArbitraryObject).forEach(([key, value]) => {
+        parameters[key] = value;
+      });
+    }
+
+    Ajax.api(
+      this,
+      {
+        actionName: data.parameters.actionName,
+        className: data.parameters.className,
+        objectIDs: objectIds,
+        parameters,
+      },
+      (responseData: AjaxResponse) => {
+        if (data.actionName !== "unmarkAll") {
+          const type = listItem.dataset.type!;
+
+          EventHandler.fire("com.woltlab.wcf.clipboard", type, {
+            data,
+            listItem,
+            responseData,
+          });
+
+          const reloadPageOnSuccess = this.reloadPageOnSuccess.get(type);
+          if (reloadPageOnSuccess && reloadPageOnSuccess.includes(responseData.actionName)) {
+            window.location.reload();
+            return;
+          }
+        }
+
+        this.loadMarkedItems();
+      },
+    );
+  }
+
+  /**
+   * Unmarks all clipboard items for an object type.
+   */
+  private unmarkAll(event: MouseEvent): void {
+    const listItem = event.currentTarget as HTMLElement;
+
+    Ajax.api(this, {
+      actionName: "unmarkAll",
+      parameters: {
+        objectType: listItem.dataset.type!,
+      },
+    });
+  }
+
+  /**
+   * Sets up ajax request object.
+   */
+  _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
+    return {
+      data: {
+        className: "wcf\\data\\clipboard\\item\\ClipboardItemAction",
+      },
+    };
+  }
+
+  /**
+   * Handles successful AJAX requests.
+   */
+  _ajaxSuccess(data: AjaxResponse): void {
+    if (data.actionName === "unmarkAll") {
+      const objectType = data.returnValues.objectType;
+      this.containers.forEach((containerData) => {
+        if (containerData.element.dataset.type !== objectType) {
+          return;
+        }
+
+        containerData.element.querySelectorAll(".jsMarked").forEach((element) => element.classList.remove("jsMarked"));
+
+        if (containerData.markAll !== null) {
+          containerData.markAll.checked = false;
+
+          this.setParentAsMarked(containerData.markAll, false);
+        }
+
+        Array.from(containerData.checkboxes).forEach((checkbox) => {
+          checkbox.checked = false;
+
+          this.setParentAsMarked(checkbox, false);
+        });
+
+        UiPageAction.remove(`wcfClipboard-${objectType}`);
+      });
+
+      return;
+    }
+
+    this.itemData = new WeakMap<HTMLLIElement, ClipboardActionData>();
+    this.reloadPageOnSuccess.clear();
+
+    // rebuild markings
+    const markings = Core.isPlainObject(data.returnValues.markedItems) ? data.returnValues.markedItems! : {};
+    this.containers.forEach((containerData) => {
+      const typeName = containerData.element.dataset.type!;
+
+      const objectIds = Array.isArray(markings[typeName]) ? markings[typeName] : [];
+      this.rebuildMarkings(containerData, objectIds);
+    });
+
+    const keepEditors: string[] = Object.keys(data.returnValues.items || {});
+
+    // clear editors
+    this.editors.forEach((editor, typeName) => {
+      if (keepEditors.includes(typeName)) {
+        UiPageAction.remove(`wcfClipboard-${typeName}`);
+
+        this.editorDropdowns.get(typeName)!.innerHTML = "";
+      }
+    });
+
+    // no items
+    if (!data.returnValues.items) {
+      return;
+    }
+
+    // rebuild editors
+    Object.entries(data.returnValues.items).forEach(([typeName, typeData]) => {
+      this.reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess);
+
+      let created = false;
+
+      let editor = this.editors.get(typeName);
+      let dropdown = this.editorDropdowns.get(typeName)!;
+      if (editor === undefined) {
+        created = true;
+
+        editor = document.createElement("a");
+        editor.className = "dropdownToggle";
+        editor.textContent = typeData.label;
+
+        this.editors.set(typeName, editor);
+
+        dropdown = document.createElement("ol");
+        dropdown.className = "dropdownMenu";
+
+        this.editorDropdowns.set(typeName, dropdown);
+      } else {
+        editor.textContent = typeData.label;
+        dropdown.innerHTML = "";
+      }
+
+      // create editor items
+      Object.values(typeData.items).forEach((itemData) => {
+        const item = document.createElement("li");
+        const label = document.createElement("span");
+        label.textContent = itemData.label;
+        item.appendChild(label);
+        dropdown.appendChild(item);
+
+        item.dataset.type = typeName;
+        item.addEventListener("click", (ev) => this.executeAction(ev));
+
+        this.itemData.set(item, itemData);
+      });
+
+      const divider = document.createElement("li");
+      divider.classList.add("dropdownDivider");
+      dropdown.appendChild(divider);
+
+      // add 'unmark all'
+      const unmarkAll = document.createElement("li");
+      unmarkAll.dataset.type = typeName;
+      const label = document.createElement("span");
+      label.textContent = Language.get("wcf.clipboard.item.unmarkAll");
+      unmarkAll.appendChild(label);
+      unmarkAll.addEventListener("click", (ev) => this.unmarkAll(ev));
+      dropdown.appendChild(unmarkAll);
+
+      if (keepEditors.indexOf(typeName) !== -1) {
+        const actionName = `wcfClipboard-${typeName}`;
+
+        if (UiPageAction.has(actionName)) {
+          UiPageAction.show(actionName);
+        } else {
+          UiPageAction.add(actionName, editor);
+        }
+      }
+
+      if (created) {
+        const parent = editor.parentElement!;
+        parent.classList.add("dropdown");
+        parent.appendChild(dropdown);
+        UiDropdownSimple.init(editor);
+      }
+    });
+  }
+
+  /**
+   * Rebuilds the mark state for each item.
+   */
+  private rebuildMarkings(data: ContainerData, objectIds: number[]): void {
+    let markAll = true;
+
+    Array.from(data.checkboxes).forEach((checkbox) => {
+      const clipboardObject = checkbox.closest(".jsClipboardObject") as HTMLElement;
+
+      const isMarked = objectIds.includes(~~checkbox.dataset.objectId!);
+      if (!isMarked) {
+        markAll = false;
+      }
+
+      checkbox.checked = isMarked;
+      if (isMarked) {
+        clipboardObject.classList.add("jsMarked");
+      } else {
+        clipboardObject.classList.remove("jsMarked");
+      }
+
+      this.setParentAsMarked(checkbox, isMarked);
+    });
+
+    if (data.markAll !== null) {
+      data.markAll.checked = markAll;
+
+      this.setParentAsMarked(data.markAll, markAll);
+
+      const parent = data.markAll.closest(".columnMark");
+      if (parent) {
+        if (markAll) {
+          parent.classList.add("jsMarked");
+        } else {
+          parent.classList.remove("jsMarked");
+        }
+      }
+    }
+  }
+
+  private setParentAsMarked(element: HTMLElement, isMarked: boolean): void {
+    const parent = element.parentElement!;
+    if (parent.getAttribute("role") === "checkbox") {
+      parent.setAttribute("aria-checked", isMarked ? "true" : "false");
+    }
+  }
+
+  /**
+   * Hides the clipboard editor for the given object type.
+   */
+  hideEditor(objectType: string): void {
+    UiPageAction.remove("wcfClipboard-" + objectType);
+
+    UiScreen.pageOverlayOpen();
+  }
+
+  /**
+   * Shows the clipboard editor.
+   */
+  showEditor(): void {
+    this.loadMarkedItems();
+
+    UiScreen.pageOverlayClose();
+  }
+
+  /**
+   * Unmarks the objects with given clipboard object type and ids.
+   */
+  unmark(objectType: string, objectIds: number[]): void {
+    this.saveState(objectType, objectIds, false);
+  }
+}
+
+let controllerClipboard: ControllerClipboard;
+
+function getControllerClipboard(): ControllerClipboard {
+  if (!controllerClipboard) {
+    controllerClipboard = new ControllerClipboard();
+  }
+
+  return controllerClipboard;
+}
+
+/**
+ * Initializes the clipboard API handler.
+ */
+export function setup(options: ClipboardOptions): void {
+  getControllerClipboard().setup(options);
+}
+
+/**
+ * Reloads the clipboard data.
+ */
+export function reload(): void {
+  getControllerClipboard().reload();
+}
+
+/**
+ * Hides the clipboard editor for the given object type.
+ */
+export function hideEditor(objectType: string): void {
+  getControllerClipboard().hideEditor(objectType);
+}
+
+/**
+ * Shows the clipboard editor.
+ */
+export function showEditor(): void {
+  getControllerClipboard().showEditor();
+}
+
+/**
+ * Unmarks the objects with given clipboard object type and ids.
+ */
+export function unmark(objectType: string, objectIds: number[]): void {
+  getControllerClipboard().unmark(objectType, objectIds);
+}