Add draft for global `WCF.Action.(Delete|Toggle)` replacement modules
authorMatthias Schmidt <gravatronics@live.com>
Sun, 14 Mar 2021 12:26:02 +0000 (13:26 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 21 Mar 2021 09:21:39 +0000 (10:21 +0100)
17 files changed:
ts/WoltLabSuite/Core/Bootstrap.ts
ts/WoltLabSuite/Core/Controller/Clipboard.ts
ts/WoltLabSuite/Core/Controller/ClipboardData.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/Object/Action.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/Object/Action/Delete.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/Object/Action/Handler.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/Object/Action/Toogle.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/Object/Data.ts [new file with mode: 0644]
wcfsetup/install/files/acp/templates/adList.tpl
wcfsetup/install/files/acp/templates/tagList.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/ClipboardData.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Delete.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Handler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Toogle.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Data.js [new file with mode: 0644]

index dd137132f27e64e48b12d850b1f866a74f5c0ca2..27ce26f39e416da7862026d65710ac34fcb340f5 100644 (file)
@@ -27,6 +27,9 @@ import * as UiTooltip from "./Ui/Tooltip";
 import * as UiPageJumpTo from "./Ui/Page/JumpTo";
 import * as UiPassword from "./Ui/Password";
 import * as UiEmpty from "./Ui/Empty";
+import * as UiObjectAction from "./Ui/Object/Action";
+import * as UiObjectActionDelete from "./Ui/Object/Action/Delete";
+import * as UiObjectActionToggle from "./Ui/Object/Action/Toogle";
 
 // perfectScrollbar does not need to be bound anywhere, it just has to be loaded for WCF.js
 import "perfect-scrollbar";
@@ -92,6 +95,9 @@ export function setup(options: BoostrapOptions): void {
   UiTooltip.setup();
   UiPassword.setup();
   UiEmpty.setup();
+  UiObjectAction.setup();
+  UiObjectActionDelete.setup();
+  UiObjectActionToggle.setup();
 
   // Convert forms with `method="get"` into `method="post"`
   document.querySelectorAll("form[method=get]").forEach((form: HTMLFormElement) => {
index 6df0aeeb37590cc537075c158bd37d0d6cfac6e5..123b3f381dc5dac137492b2adeb55532bbfc57ce 100644 (file)
@@ -18,55 +18,7 @@ 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;
-  };
-}
+import { ClipboardOptions, ContainerData, ClipboardActionData, AjaxResponse } from "./ClipboardData";
 
 const _specialCheckboxSelector =
   '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]';
diff --git a/ts/WoltLabSuite/Core/Controller/ClipboardData.ts b/ts/WoltLabSuite/Core/Controller/ClipboardData.ts
new file mode 100644 (file)
index 0000000..3fa82d5
--- /dev/null
@@ -0,0 +1,49 @@
+import { DatabaseObjectActionResponse } from "../Ajax/Data";
+
+export interface ClipboardOptions {
+  hasMarkedItems: boolean;
+  pageClassName: string;
+  pageObjectId?: number;
+}
+
+export interface ContainerData {
+  checkboxes: HTMLCollectionOf<HTMLInputElement>;
+  element: HTMLElement;
+  markAll: HTMLInputElement | null;
+  markedObjectIds: Set<number>;
+}
+
+export interface ClipboardItemData {
+  items: { [key: string]: ClipboardActionData };
+  label: string;
+  reloadPageOnSuccess: string[];
+}
+
+export interface ClipboardActionData {
+  actionName: string;
+  internalData: ArbitraryObject;
+  label: string;
+  parameters: {
+    actionName?: string;
+    className?: string;
+    objectIDs: number[];
+    template: string;
+  };
+  url: string;
+}
+
+export interface AjaxResponseMarkedItems {
+  [key: string]: number[];
+}
+
+export interface AjaxResponse extends DatabaseObjectActionResponse {
+  returnValues: {
+    action: string;
+    items?: {
+      // They key is the `typeName`
+      [key: string]: ClipboardItemData;
+    };
+    markedItems?: AjaxResponseMarkedItems;
+    objectType: string;
+  };
+}
diff --git a/ts/WoltLabSuite/Core/Ui/Object/Action.ts b/ts/WoltLabSuite/Core/Ui/Object/Action.ts
new file mode 100644 (file)
index 0000000..7932840
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Handles actions that can be executed on (database) objects by clicking on specific action buttons.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action
+ */
+
+import * as Ajax from "../../Ajax";
+import * as EventHandler from "../../Event/Handler";
+import { DatabaseObjectActionResponse, ResponseData } from "../../Ajax/Data";
+import { ObjectActionData } from "./Data";
+import * as UiConfirmation from "../Confirmation";
+import * as Language from "../../Language";
+import * as StringUtil from "../../StringUtil";
+
+const containerSelector = ".jsObjectActionContainer[data-object-action-class-name]";
+const objectSelector = ".jsObjectActionObject[data-object-id]";
+const actionSelector = ".jsObjectAction[data-object-action]";
+
+function executeAction(event: Event): void {
+  const actionElement = event.currentTarget as HTMLElement;
+  const objectAction = actionElement.dataset.objectAction!;
+
+  // To support additional actions added by plugins, action elements can override the default object
+  // action class name and object id.
+  let objectActionClassName = (actionElement.closest(containerSelector) as HTMLElement).dataset.objectActionClassName;
+  if (actionElement.dataset.objectActionClassName) {
+    objectActionClassName = actionElement.dataset.objectActionClassName;
+  }
+
+  let objectId = (actionElement.closest(objectSelector) as HTMLElement).dataset.objectId;
+  if (actionElement.dataset.objectId) {
+    objectId = actionElement.dataset.objectId;
+  }
+
+  // Collect additional request parameters.
+  // TODO: Is still untested.
+  const parameters = {};
+  Object.entries(actionElement.dataset).forEach(([key, value]) => {
+    if (/^objectActionParameterData.+/.exec(key)) {
+      if (!("data" in parameters)) {
+        parameters["data"] = {};
+      }
+      parameters[StringUtil.lcfirst(key.replace(/^objectActionParameterData/, ""))] = value;
+    } else if (/^objectActionParameter.+/.exec(key)) {
+      parameters[StringUtil.lcfirst(key.replace(/^objectActionParameter/, ""))] = value;
+    }
+  });
+
+  function sendRequest(): void {
+    Ajax.apiOnce({
+      data: {
+        actionName: objectAction,
+        className: objectActionClassName,
+        objectIDs: [objectId],
+        parameters: parameters,
+      },
+      success: (data) => processAction(actionElement, data),
+    });
+  }
+
+  if (actionElement.dataset.confirmMessage) {
+    UiConfirmation.show({
+      confirm: sendRequest,
+      message: Language.get(actionElement.dataset.confirmMessage),
+      messageIsHtml: true,
+    });
+  } else {
+    sendRequest();
+  }
+}
+
+function processAction(actionElement: HTMLElement, data: ResponseData | DatabaseObjectActionResponse): void {
+  EventHandler.fire("WoltLabSuite/Core/Ui/Object/Action", actionElement.dataset.objectAction!, {
+    data,
+    objectElement: actionElement.closest(objectSelector),
+  } as ObjectActionData);
+}
+
+export function setup(): void {
+  document
+    .querySelectorAll(`${containerSelector} ${objectSelector} ${actionSelector}`)
+    .forEach((action: HTMLElement) => {
+      action.addEventListener("click", (ev) => executeAction(ev));
+    });
+
+  // TODO: handle elements added later on
+}
diff --git a/ts/WoltLabSuite/Core/Ui/Object/Action/Delete.ts b/ts/WoltLabSuite/Core/Ui/Object/Action/Delete.ts
new file mode 100644 (file)
index 0000000..b69a132
--- /dev/null
@@ -0,0 +1,19 @@
+/**
+ * Reacts to objects being deleted.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action/Delete
+ */
+
+import UiObjectActionHandler from "./Handler";
+import { DatabaseObjectActionResponse } from "../../../Ajax/Data";
+
+function deleteObject(data: DatabaseObjectActionResponse, objectElement: HTMLElement): void {
+  objectElement.remove();
+}
+
+export function setup(): void {
+  new UiObjectActionHandler("delete", ["delete"], deleteObject);
+}
diff --git a/ts/WoltLabSuite/Core/Ui/Object/Action/Handler.ts b/ts/WoltLabSuite/Core/Ui/Object/Action/Handler.ts
new file mode 100644 (file)
index 0000000..738b12c
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Default handler to react to a specific object action.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action/Handler
+ */
+
+import * as EventHandler from "../../../Event/Handler";
+import { ClipboardData, ObjectActionData } from "../Data";
+import * as ControllerClipboard from "../../../Controller/Clipboard";
+import { DatabaseObjectActionResponse } from "../../../Ajax/Data";
+
+export type ObjectAction = (data: DatabaseObjectActionResponse, objectElement: HTMLElement) => void;
+
+export default class UiObjectActionHandler {
+  protected readonly objectAction: ObjectAction;
+
+  constructor(actionName: string, clipboardActionNames: string[], objectAction: ObjectAction) {
+    this.objectAction = objectAction;
+
+    EventHandler.add("WoltLabSuite/Core/Ui/Object/Action", actionName, (data: ObjectActionData) =>
+      this.handleObjectAction(data),
+    );
+
+    document.querySelectorAll(".jsClipboardContainer[data-type]").forEach((container: HTMLElement) => {
+      EventHandler.add("com.woltlab.wcf.clipboard", container.dataset.type!, (data: ClipboardData) => {
+        // Only consider events if the action has actually been executed.
+        if (data.responseData === null) {
+          return;
+        }
+
+        if (clipboardActionNames.indexOf(data.responseData.actionName) !== -1) {
+          this.handleClipboardAction(data);
+        }
+      });
+    });
+  }
+
+  protected handleClipboardAction(data: ClipboardData): void {
+    const clipboardObjectType = data.listItem.dataset.type!;
+
+    document
+      .querySelectorAll(`.jsClipboardContainer[data-type="${clipboardObjectType}"] .jsClipboardObject`)
+      .forEach((clipboardObject: HTMLElement) => {
+        const objectId = clipboardObject.dataset.objectId!;
+
+        data.responseData.objectIDs.forEach((deletedObjectId) => {
+          if (~~deletedObjectId === ~~objectId) {
+            this.objectAction(data.responseData, clipboardObject);
+          }
+        });
+      });
+  }
+
+  protected handleObjectAction(data: ObjectActionData): void {
+    this.objectAction(data.data, data.objectElement);
+
+    ControllerClipboard.reload();
+  }
+}
diff --git a/ts/WoltLabSuite/Core/Ui/Object/Action/Toogle.ts b/ts/WoltLabSuite/Core/Ui/Object/Action/Toogle.ts
new file mode 100644 (file)
index 0000000..54fb91c
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Reacts to objects being toggled.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action/Toggle
+ */
+
+import * as Language from "../../../Language";
+import UiObjectActionHandler from "./Handler";
+import { DatabaseObjectActionResponse } from "../../../Ajax/Data";
+
+function toggleObject(data: DatabaseObjectActionResponse, objectElement: HTMLElement): void {
+  const toggleButton = objectElement.querySelector('.jsObjectAction[data-object-action="toggle"]') as HTMLElement;
+
+  if (toggleButton.classList.contains("fa-square-o")) {
+    toggleButton.classList.replace("fa-square-o", "fa-check-square-o");
+
+    const newTitle = toggleButton.dataset.disableTitle
+      ? toggleButton.dataset.disableTitle
+      : Language.get("wcf.global.button.disable");
+    toggleButton.title = newTitle;
+  } else {
+    toggleButton.classList.replace("fa-check-square-o", "fa-square-o");
+
+    const newTitle = toggleButton.dataset.enableTitle
+      ? toggleButton.dataset.enableTitle
+      : Language.get("wcf.global.button.enable");
+    toggleButton.title = newTitle;
+  }
+}
+
+export function setup(): void {
+  new UiObjectActionHandler("toggle", ["enable", "disable"], toggleObject);
+}
diff --git a/ts/WoltLabSuite/Core/Ui/Object/Data.ts b/ts/WoltLabSuite/Core/Ui/Object/Data.ts
new file mode 100644 (file)
index 0000000..a130777
--- /dev/null
@@ -0,0 +1,13 @@
+import { DatabaseObjectActionResponse } from "../../Ajax/Data";
+import { ClipboardActionData } from "../../Controller/ClipboardData";
+
+export interface ObjectActionData {
+  data: DatabaseObjectActionResponse;
+  objectElement: HTMLElement;
+}
+
+export interface ClipboardData {
+  data: ClipboardActionData;
+  listItem: HTMLLIElement;
+  responseData: DatabaseObjectActionResponse;
+}
index 05d0c92d1565bed2a63f4596b55d35d1947e5ab0..0b6febe125469d51502a2d5973dfb1305afcea20 100644 (file)
@@ -8,11 +8,6 @@
                        offset: {@$startIndex}
                });
        });
-       
-       $(function() {
-               new WCF.Action.Delete('wcf\\data\\ad\\AdAction', '.jsAd');
-               new WCF.Action.Toggle('wcf\\data\\ad\\AdAction', '.jsAd');
-       });
 </script>
 
 <header class="contentHeader">
 
 {if $objects|count}
        <div class="section sortableListContainer" id="adList">
-               <ol class="sortableList jsReloadPageWhenEmpty" data-object-id="0" start="{@($pageNo - 1) * $itemsPerPage + 1}">
+               <ol class="sortableList jsObjectActionContainer jsReloadPageWhenEmpty" data-object-id="0" start="{@($pageNo - 1) * $itemsPerPage + 1}" data-object-action-class-name="wcf\data\ad\AdAction">
                        {foreach from=$objects item='ad'}
-                               <li class="sortableNode sortableNoNesting jsAd" data-object-id="{@$ad->adID}">
+                               <li class="sortableNode sortableNoNesting jsAd jsObjectActionObject" data-object-id="{@$ad->adID}">
                                        <span class="sortableNodeLabel">
                                                <a href="{link controller='AdEdit' object=$ad}{/link}">{$ad->adName}</a>
                                                
                                                <span class="statusDisplay sortableButtonContainer">
                                                        <span class="icon icon16 fa-arrows sortableNodeHandle"></span>
-                                                       <span class="icon icon16 fa-{if !$ad->isDisabled}check-{/if}square-o jsToggleButton jsTooltip pointer" title="{lang}wcf.global.button.{if $ad->isDisabled}enable{else}disable{/if}{/lang}" data-object-id="{@$ad->adID}"></span>
+                                                       <span class="icon icon16 fa-{if !$ad->isDisabled}check-{/if}square-o jsObjectAction jsTooltip pointer" title="{lang}wcf.global.button.{if $ad->isDisabled}enable{else}disable{/if}{/lang}" data-object-action="toggle"></span>
                                                        <a href="{link controller='AdEdit' object=$ad}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
-                                                       <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$ad->adID}" data-confirm-message-html="{lang __encode=true}wcf.acp.ad.delete.confirmMessage{/lang}"></span>
+                                                       <span class="icon icon16 fa-times jsObjectAction jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-action="delete" data-confirm-message="{lang __encode=true}wcf.acp.ad.delete.confirmMessage{/lang}"></span>
                                                        
                                                        {event name='itemButtons'}
                                                </span>
index c42c54772be90044fe67b3dc3a716e98edd636ed..c7618d9cfce999ce7fe66682f0bfbfea4298155f 100644 (file)
@@ -4,14 +4,7 @@
        require(['WoltLabSuite/Core/Controller/Clipboard', 'Language'], function(ControllerClipboard, Language) {
                Language.add('wcf.acp.tag.setAsSynonyms', '{jslang}wcf.acp.tag.setAsSynonyms{/jslang}');
                
-               var deleteAction = new WCF.Action.Delete('wcf\\data\\tag\\TagAction', '.jsTagRow');
-               deleteAction.setCallback(ControllerClipboard.reload.bind(ControllerClipboard));
-               
-               WCF.Clipboard.init('wcf\\acp\\page\\TagListPage', {@$hasMarkedItems}, {
-                       'com.woltlab.wcf.tag': {
-                               'delete': deleteAction
-                       }
-               });
+               WCF.Clipboard.init('wcf\\acp\\page\\TagListPage', {@$hasMarkedItems});
                
                new WCF.ACP.Tag.SetAsSynonymsHandler();
        });
@@ -61,7 +54,7 @@
 
 {if $objects|count}
        <div class="section tabularBox">
-               <table data-type="com.woltlab.wcf.tag" class="table jsClipboardContainer">
+               <table class="table jsClipboardContainer jsObjectActionContainer" data-type="com.woltlab.wcf.tag" data-object-action-class-name="wcf\data\tag\TagAction">
                        <thead>
                                <tr>
                                        <th class="columnMark"><label><input type="checkbox" class="jsClipboardMarkAll"></label></th>
                        
                        <tbody class="jsReloadPageWhenEmpty">
                                {foreach from=$objects item=tag}
-                                       <tr class="jsTagRow jsClipboardObject">
+                                       <tr class="jsTagRow jsClipboardObject jsObjectActionObject" data-object-id="{@$tag->tagID}">
                                                <td class="columnMark"><input type="checkbox" class="jsClipboardItem" data-object-id="{@$tag->tagID}"></td>
                                                <td class="columnIcon">
                                                        <a href="{link controller='TagEdit' object=$tag}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
-                                                       <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$tag->tagID}" data-confirm-message-html="{lang __encode=true}wcf.acp.tag.delete.sure{/lang}"></span>
+                                                       <span class="icon icon16 fa-times jsObjectAction jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-action="delete" data-confirm-message="{lang __encode=true}wcf.acp.tag.delete.sure{/lang}"></span>
                                                        
                                                        {event name='rowButtons'}
                                                </td>
index 88eed5c339b0b170399badb31cf87e0d50aee1b3..94204ea920d9fccb1cd6ed7c814eefb3e5ee88ea 100644 (file)
@@ -8,7 +8,7 @@
  * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module  WoltLabSuite/Core/Bootstrap
  */
-define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Date/Time/Relative", "./Devtools", "./Dom/Change/Listener", "./Environment", "./Event/Handler", "./Language", "./StringUtil", "./Ui/Dialog", "./Ui/Dropdown/Simple", "./Ui/Mobile", "./Ui/Page/Action", "./Ui/TabMenu", "./Ui/Tooltip", "./Ui/Page/JumpTo", "./Ui/Password", "./Ui/Empty", "perfect-scrollbar"], function (require, exports, tslib_1, Core, Picker_1, DateTimeRelative, Devtools_1, Listener_1, Environment, EventHandler, Language, StringUtil, Dialog_1, Simple_1, UiMobile, UiPageAction, UiTabMenu, UiTooltip, UiPageJumpTo, UiPassword, UiEmpty) {
+define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Date/Time/Relative", "./Devtools", "./Dom/Change/Listener", "./Environment", "./Event/Handler", "./Language", "./StringUtil", "./Ui/Dialog", "./Ui/Dropdown/Simple", "./Ui/Mobile", "./Ui/Page/Action", "./Ui/TabMenu", "./Ui/Tooltip", "./Ui/Page/JumpTo", "./Ui/Password", "./Ui/Empty", "./Ui/Object/Action", "./Ui/Object/Action/Delete", "./Ui/Object/Action/Toogle", "perfect-scrollbar"], function (require, exports, tslib_1, Core, Picker_1, DateTimeRelative, Devtools_1, Listener_1, Environment, EventHandler, Language, StringUtil, Dialog_1, Simple_1, UiMobile, UiPageAction, UiTabMenu, UiTooltip, UiPageJumpTo, UiPassword, UiEmpty, UiObjectAction, UiObjectActionDelete, UiObjectActionToggle) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.setup = void 0;
@@ -30,6 +30,9 @@ define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Date/Time/R
     UiPageJumpTo = tslib_1.__importStar(UiPageJumpTo);
     UiPassword = tslib_1.__importStar(UiPassword);
     UiEmpty = tslib_1.__importStar(UiEmpty);
+    UiObjectAction = tslib_1.__importStar(UiObjectAction);
+    UiObjectActionDelete = tslib_1.__importStar(UiObjectActionDelete);
+    UiObjectActionToggle = tslib_1.__importStar(UiObjectActionToggle);
     // non strict equals by intent
     if (window.WCF == null) {
         window.WCF = {};
@@ -78,6 +81,9 @@ define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Date/Time/R
         UiTooltip.setup();
         UiPassword.setup();
         UiEmpty.setup();
+        UiObjectAction.setup();
+        UiObjectActionDelete.setup();
+        UiObjectActionToggle.setup();
         // Convert forms with `method="get"` into `method="post"`
         document.querySelectorAll("form[method=get]").forEach((form) => {
             form.method = "post";
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/ClipboardData.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/ClipboardData.js
new file mode 100644 (file)
index 0000000..2ae92b6
--- /dev/null
@@ -0,0 +1,4 @@
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action.js
new file mode 100644 (file)
index 0000000..eb19054
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Handles actions that can be executed on (database) objects by clicking on specific action buttons.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action
+ */
+define(["require", "exports", "tslib", "../../Ajax", "../../Event/Handler", "../Confirmation", "../../Language", "../../StringUtil"], function (require, exports, tslib_1, Ajax, EventHandler, UiConfirmation, Language, StringUtil) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = void 0;
+    Ajax = tslib_1.__importStar(Ajax);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    UiConfirmation = tslib_1.__importStar(UiConfirmation);
+    Language = tslib_1.__importStar(Language);
+    StringUtil = tslib_1.__importStar(StringUtil);
+    const containerSelector = ".jsObjectActionContainer[data-object-action-class-name]";
+    const objectSelector = ".jsObjectActionObject[data-object-id]";
+    const actionSelector = ".jsObjectAction[data-object-action]";
+    function executeAction(event) {
+        const actionElement = event.currentTarget;
+        const objectAction = actionElement.dataset.objectAction;
+        // To support additional actions added by plugins, action elements can override the default object
+        // action class name and object id.
+        let objectActionClassName = actionElement.closest(containerSelector).dataset.objectActionClassName;
+        if (actionElement.dataset.objectActionClassName) {
+            objectActionClassName = actionElement.dataset.objectActionClassName;
+        }
+        let objectId = actionElement.closest(objectSelector).dataset.objectId;
+        if (actionElement.dataset.objectId) {
+            objectId = actionElement.dataset.objectId;
+        }
+        // Collect additional request parameters.
+        // TODO: Is still untested.
+        const parameters = {};
+        Object.entries(actionElement.dataset).forEach(([key, value]) => {
+            if (/^objectActionParameterData.+/.exec(key)) {
+                if (!("data" in parameters)) {
+                    parameters["data"] = {};
+                }
+                parameters[StringUtil.lcfirst(key.replace(/^objectActionParameterData/, ""))] = value;
+            }
+            else if (/^objectActionParameter.+/.exec(key)) {
+                parameters[StringUtil.lcfirst(key.replace(/^objectActionParameter/, ""))] = value;
+            }
+        });
+        function sendRequest() {
+            Ajax.apiOnce({
+                data: {
+                    actionName: objectAction,
+                    className: objectActionClassName,
+                    objectIDs: [objectId],
+                    parameters: parameters,
+                },
+                success: (data) => processAction(actionElement, data),
+            });
+        }
+        if (actionElement.dataset.confirmMessage) {
+            UiConfirmation.show({
+                confirm: sendRequest,
+                message: Language.get(actionElement.dataset.confirmMessage),
+                messageIsHtml: true,
+            });
+        }
+        else {
+            sendRequest();
+        }
+    }
+    function processAction(actionElement, data) {
+        EventHandler.fire("WoltLabSuite/Core/Ui/Object/Action", actionElement.dataset.objectAction, {
+            data,
+            objectElement: actionElement.closest(objectSelector),
+        });
+    }
+    function setup() {
+        document
+            .querySelectorAll(`${containerSelector} ${objectSelector} ${actionSelector}`)
+            .forEach((action) => {
+            action.addEventListener("click", (ev) => executeAction(ev));
+        });
+        // TODO: handle elements added later on
+    }
+    exports.setup = setup;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Delete.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Delete.js
new file mode 100644 (file)
index 0000000..4261900
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * Reacts to objects being deleted.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action/Delete
+ */
+define(["require", "exports", "tslib", "./Handler"], function (require, exports, tslib_1, Handler_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = void 0;
+    Handler_1 = tslib_1.__importDefault(Handler_1);
+    function deleteObject(data, objectElement) {
+        objectElement.remove();
+    }
+    function setup() {
+        new Handler_1.default("delete", ["delete"], deleteObject);
+    }
+    exports.setup = setup;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Handler.js
new file mode 100644 (file)
index 0000000..e0b4ed6
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Default handler to react to a specific object action.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action/Handler
+ */
+define(["require", "exports", "tslib", "../../../Event/Handler", "../../../Controller/Clipboard"], function (require, exports, tslib_1, EventHandler, ControllerClipboard) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    EventHandler = tslib_1.__importStar(EventHandler);
+    ControllerClipboard = tslib_1.__importStar(ControllerClipboard);
+    class UiObjectActionHandler {
+        constructor(actionName, clipboardActionNames, objectAction) {
+            this.objectAction = objectAction;
+            EventHandler.add("WoltLabSuite/Core/Ui/Object/Action", actionName, (data) => this.handleObjectAction(data));
+            document.querySelectorAll(".jsClipboardContainer[data-type]").forEach((container) => {
+                EventHandler.add("com.woltlab.wcf.clipboard", container.dataset.type, (data) => {
+                    // Only consider events if the action has actually been executed.
+                    if (data.responseData === null) {
+                        return;
+                    }
+                    if (clipboardActionNames.indexOf(data.responseData.actionName) !== -1) {
+                        this.handleClipboardAction(data);
+                    }
+                });
+            });
+        }
+        handleClipboardAction(data) {
+            const clipboardObjectType = data.listItem.dataset.type;
+            document
+                .querySelectorAll(`.jsClipboardContainer[data-type="${clipboardObjectType}"] .jsClipboardObject`)
+                .forEach((clipboardObject) => {
+                const objectId = clipboardObject.dataset.objectId;
+                data.responseData.objectIDs.forEach((deletedObjectId) => {
+                    if (~~deletedObjectId === ~~objectId) {
+                        this.objectAction(data.responseData, clipboardObject);
+                    }
+                });
+            });
+        }
+        handleObjectAction(data) {
+            this.objectAction(data.data, data.objectElement);
+            ControllerClipboard.reload();
+        }
+    }
+    exports.default = UiObjectActionHandler;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Toogle.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Action/Toogle.js
new file mode 100644 (file)
index 0000000..b7bbf2a
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Reacts to objects being toggled.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2021 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Object/Action/Toggle
+ */
+define(["require", "exports", "tslib", "../../../Language", "./Handler"], function (require, exports, tslib_1, Language, Handler_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = void 0;
+    Language = tslib_1.__importStar(Language);
+    Handler_1 = tslib_1.__importDefault(Handler_1);
+    function toggleObject(data, objectElement) {
+        const toggleButton = objectElement.querySelector('.jsObjectAction[data-object-action="toggle"]');
+        if (toggleButton.classList.contains("fa-square-o")) {
+            toggleButton.classList.replace("fa-square-o", "fa-check-square-o");
+            const newTitle = toggleButton.dataset.disableTitle
+                ? toggleButton.dataset.disableTitle
+                : Language.get("wcf.global.button.disable");
+            toggleButton.title = newTitle;
+        }
+        else {
+            toggleButton.classList.replace("fa-check-square-o", "fa-square-o");
+            const newTitle = toggleButton.dataset.enableTitle
+                ? toggleButton.dataset.enableTitle
+                : Language.get("wcf.global.button.enable");
+            toggleButton.title = newTitle;
+        }
+    }
+    function setup() {
+        new Handler_1.default("toggle", ["enable", "disable"], toggleObject);
+    }
+    exports.setup = setup;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Data.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Object/Data.js
new file mode 100644 (file)
index 0000000..2ae92b6
--- /dev/null
@@ -0,0 +1,4 @@
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+});