Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions ts/WoltLabSuite/Core/Component/Inline/Actions/Disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Inline editor action to disable an object.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
*/

import { InlineEditor } from "WoltLabSuite/Core/Component/Inline/Editor";
import { Simple } from "WoltLabSuite/Core/Component/Inline/Actions/Simple";

export class Disable extends Simple {
constructor(inlineEditor: InlineEditor, endpoint: string | URL) {
super(inlineEditor, "wcf.global.button.disable", endpoint);
}

responseOk(): void {
this.inlineEditor.update({
isDisabled: 1,
});
}

isVisible(): boolean {
return (
this.inlineEditor.getPermissions()["canEnable"] &&
!this.inlineEditor.getState("isDeleted") &&
!this.inlineEditor.getState("isDisabled")
);
}
}
70 changes: 70 additions & 0 deletions ts/WoltLabSuite/Core/Component/Inline/Actions/Enable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Inline editor action to enable an object.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
*/

import { Action, InlineEditor } from "WoltLabSuite/Core/Component/Inline/Editor";
import { DropdownBuilderItemData } from "WoltLabSuite/Core/Ui/Dropdown/Builder";
import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result";
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
import { getPhrase } from "WoltLabSuite/Core/Language";

export class Enable implements Action {
protected readonly inlineEditor: InlineEditor;
protected readonly endpoint: string | URL;
protected readonly useFormBuilder: boolean;

constructor(inlineEditor: InlineEditor, endpoint: string | URL, useFormBuilder: boolean = false) {
this.inlineEditor = inlineEditor;
this.endpoint = endpoint;
this.useFormBuilder = useFormBuilder;
}

get item(): DropdownBuilderItemData {
return {
label: getPhrase("wcf.global.button.enable"),
callback: async () => {
if (this.useFormBuilder) {
const response = await dialogFactory()
.usingFormBuilder()
.fromEndpoint<{ isDisabled: boolean }>(this.endpoint.toString());

if (response.ok) {
this.inlineEditor.update({
isDisabled: response.result.isDisabled ? 1 : 0,
});
}
} else {
if ((await enable(this.endpoint)).ok) {
this.inlineEditor.update({
isDisabled: 0,
});
}
}
},
};
}

isVisible(): boolean {
return (
this.inlineEditor.getPermissions()["canEnable"] &&
!this.inlineEditor.getState("isDeleted") &&
this.inlineEditor.getState("isDisabled")
);
}
}

async function enable(endpoint: string | URL): Promise<ApiResult<[]>> {
try {
await prepareRequest(endpoint).post().fetchAsJson();
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue([]);
}
27 changes: 27 additions & 0 deletions ts/WoltLabSuite/Core/Component/Inline/Actions/Restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Inline editor action to restore an object.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
*/

import { InlineEditor } from "WoltLabSuite/Core/Component/Inline/Editor";
import { Simple } from "WoltLabSuite/Core/Component/Inline/Actions/Simple";

export class Restore extends Simple {
constructor(inlineEditor: InlineEditor, endpoint: string | URL) {
super(inlineEditor, "wcf.global.button.restore", endpoint);
}

responseOk(): void {
this.inlineEditor.update({
isDeleted: 0,
});
}

isVisible(): boolean {
return this.inlineEditor.getPermissions()["canRestore"] && this.inlineEditor.getState("isDeleted");
}
}
50 changes: 50 additions & 0 deletions ts/WoltLabSuite/Core/Component/Inline/Actions/Simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Inline editor action to restore an object.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
*/

import { Action, InlineEditor } from "WoltLabSuite/Core/Component/Inline/Editor";
import { DropdownBuilderItemData } from "WoltLabSuite/Core/Ui/Dropdown/Builder";
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { apiResultFromError, apiResultFromValue, ApiResult } from "WoltLabSuite/Core/Api/Result";
import { getPhrase } from "WoltLabSuite/Core/Language";

export abstract class Simple implements Action {
protected readonly inlineEditor: InlineEditor;
protected readonly endpoint: string | URL;
protected readonly label: string;

protected constructor(inlineEditor: InlineEditor, label: string, endpoint: string | URL) {
this.inlineEditor = inlineEditor;
this.endpoint = endpoint;
this.label = label;
}

get item(): DropdownBuilderItemData {
return {
label: getPhrase(this.label),
callback: async () => {
const response = await request(this.endpoint);
if (response.ok) {
this.responseOk();
}
},
};
}

abstract responseOk(): void;
}

async function request(endpoint: string | URL): Promise<ApiResult<[]>> {
try {
await prepareRequest(endpoint).post().fetchAsJson();
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue([]);
}
70 changes: 70 additions & 0 deletions ts/WoltLabSuite/Core/Component/Inline/Actions/Trash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Inline editor action to trash an object.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
*/

import { Action, InlineEditor } from "WoltLabSuite/Core/Component/Inline/Editor";
import { DropdownBuilderItemData } from "WoltLabSuite/Core/Ui/Dropdown/Builder";
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { apiResultFromError, apiResultFromValue, ApiResult } from "WoltLabSuite/Core/Api/Result";
import { confirmationFactory } from "WoltLabSuite/Core/Component/Confirmation";
import { getPhrase } from "WoltLabSuite/Core/Language";

export class Trash implements Action {
protected readonly inlineEditor: InlineEditor;
protected readonly endpoint: string | URL;
protected readonly title: string;

constructor(inlineEditor: InlineEditor, title: string, endpoint: string | URL) {
this.inlineEditor = inlineEditor;
this.endpoint = endpoint;
this.title = title;
}

get item(): DropdownBuilderItemData {
return {
label: getPhrase("wcf.global.button.trash"),
callback: async () => {
const result = await confirmationFactory().softDelete(this.title, true);
if (!result.result) {
return;
}

const response = await trash(this.endpoint, result.reason);
if (response.ok) {
this.inlineEditor.update({
isDeleted: 1,
deleteNote: response.value,
});
}
},
};
}

isVisible(): boolean {
return this.inlineEditor.getPermissions()["canTrash"] && !this.inlineEditor.getState("isDeleted");
}
}

type Response = {
deleteNote: string;
};

async function trash(endpoint: string | URL, reason: string): Promise<ApiResult<string>> {
let response: Response;
try {
response = (await prepareRequest(endpoint)
.post({
reason,
})
.fetchAsJson()) as Response;
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue(response.deleteNote);
}
131 changes: 131 additions & 0 deletions ts/WoltLabSuite/Core/Component/Inline/Editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Provides an inline editor for objects using a drop-down menu.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
*/

import {
DropdownBuilderItemData,
create as createDropdownMenu,
setItems as setDropdownItems,
} from "WoltLabSuite/Core/Ui/Dropdown/Builder";
import { show as showNotification } from "WoltLabSuite/Core/Ui/Notification";
import { stringToBool } from "WoltLabSuite/Core/Core";
import UiDropdownSimple from "WoltLabSuite/Core/Ui/Dropdown/Simple";

export interface Action {
isVisible?: () => boolean;

get item(): DropdownBuilderItemData;
}

export type State = string | boolean | number;

const inlineEditors = new Map<HTMLElement, InlineEditor>();

export class InlineEditor {
protected readonly element: HTMLElement;
readonly #dropdownToggle: HTMLElement;
protected permissions: Record<string, boolean> = {};
readonly #dropdownMenu: HTMLUListElement;
readonly #actions: Action[] = [];

public constructor(element: HTMLElement, dropdownToggleSelector: string) {
this.element = element;
this.#dropdownToggle = this.element.querySelector(dropdownToggleSelector) as HTMLElement;
this.#dropdownMenu = createDropdownMenu([]);

// @see WoltLabSuite/Core/Ui/Dropdown/Builder::attach()
UiDropdownSimple.initFragment(this.#dropdownToggle, this.#dropdownMenu);

this.#dropdownToggle.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();

// Rebuild the menu to ensure the menu items are displayed correctly,
// as states may change externally and cannot be detected automatically
this.#rebuildDropdownMenu();

UiDropdownSimple.toggleDropdown(this.#dropdownToggle.id);
});

inlineEditors.set(this.element, this);
}

/**
* Gets the state of a property from the element's dataset as a boolean.
*/
public getState(propertyName: string): boolean {
if (!Object.prototype.hasOwnProperty.call(this.element.dataset, propertyName)) {
return false;
}

return stringToBool(this.element.dataset[propertyName]!);
}

/**
* Updates the element's dataset with the provided data.
*/
public update(data: Record<string, State>): void {
showNotification();

Object.entries(data).forEach(([key, value]) => {
this.element.dataset[key] = typeof value === "boolean" ? (value ? "1" : "0") : value.toString();
});
}

/**
* Merge the current permissions with the provided permissions.
*/
public addPermissions(permissions: Record<string, boolean>): void {
this.permissions = { ...this.permissions, ...permissions };
}

/**
* Gets the permissions for the inline editor.
*/
public getPermissions(): Record<string, boolean> {
return this.permissions;
}

/**
* Adds an action to the inline editor.
*/
public addAction(action: Action): void {
this.#actions.push(action);
}

/**
* Adds multiple actions to the inline editor.
*/
public addActions(actions: Action[]): void {
this.#actions.push(...actions);
}

/**
* Rebuilds the dropdown menu based on the current menu items and their visibility.
*/
#rebuildDropdownMenu(): void {
const dropdownMenuItems = this.#actions
.filter((item) => {
return item.isVisible === undefined || item.isVisible();
})
.map((item) => item.item);

if (dropdownMenuItems.length === 0) {
this.#dropdownMenu.innerHTML = "";
} else {
setDropdownItems(this.#dropdownMenu, dropdownMenuItems);
}
}
}

/**
* Gets the inline editor instance associated with the given element.
*/
export function getInlineEditor(element: HTMLElement): InlineEditor | undefined {
return inlineEditors.get(element);
}
Loading
Loading