-
-
{/if}
{/capture}
diff --git a/com.woltlab.wcf/templates/unreadArticleList.tpl b/com.woltlab.wcf/templates/unreadArticleList.tpl
index 140321095b1..0377ded7824 100644
--- a/com.woltlab.wcf/templates/unreadArticleList.tpl
+++ b/com.woltlab.wcf/templates/unreadArticleList.tpl
@@ -17,7 +17,7 @@
- {include file='__labelSelection'}
+ {include file='__labelPickerGroup'}
@@ -25,17 +25,6 @@
-
-
{/if}
{/capture}
diff --git a/com.woltlab.wcf/templates/watchedArticleList.tpl b/com.woltlab.wcf/templates/watchedArticleList.tpl
index 179cf697587..4c622eb302e 100644
--- a/com.woltlab.wcf/templates/watchedArticleList.tpl
+++ b/com.woltlab.wcf/templates/watchedArticleList.tpl
@@ -15,7 +15,7 @@
- {include file='__labelSelection'}
+ {include file='__labelPickerGroup'}
@@ -23,17 +23,6 @@
-
-
{/if}
{/capture}
diff --git a/ts/WoltLabSuite/Core/Component/Article/LabelPicker.ts b/ts/WoltLabSuite/Core/Component/Article/LabelPicker.ts
new file mode 100644
index 00000000000..95cad6e70bf
--- /dev/null
+++ b/ts/WoltLabSuite/Core/Component/Article/LabelPicker.ts
@@ -0,0 +1,46 @@
+/**
+ * Toggles the visibility of label groups based on the selected category.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @woltlabExcludeBundle all
+ */
+
+type CategoryId = number;
+type LabelGroupId = number;
+
+function toggleVisibility(showLabelGroupIds: LabelGroupId[] | undefined): void {
+ if (showLabelGroupIds === undefined) {
+ showLabelGroupIds = [];
+ }
+
+ document.querySelectorAll("woltlab-core-label-picker").forEach((labelPicker) => {
+ const groupId = parseInt(labelPicker.dataset.groupId!);
+ if (showLabelGroupIds!.includes(groupId)) {
+ labelPicker.disabled = false;
+ labelPicker.closest("dl")!.hidden = false;
+ } else {
+ labelPicker.disabled = true;
+ labelPicker.closest("dl")!.hidden = true;
+ }
+ });
+}
+
+export function setup(categoryMapping: Map): void {
+ if (categoryMapping.size === 0) {
+ return;
+ }
+
+ const categoryId = document.getElementById("categoryID") as HTMLSelectElement;
+ function updateVisibility() {
+ const value = parseInt(categoryId.value);
+ toggleVisibility(categoryMapping.get(value));
+ }
+
+ categoryId.addEventListener("change", () => {
+ updateVisibility();
+ });
+
+ updateVisibility();
+}
diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.ts
deleted file mode 100644
index 17ae26ab315..00000000000
--- a/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Label.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * Handles the JavaScript part of the label form field.
- *
- * @author Alexander Ebert, Matthias Schmidt
- * @copyright 2001-2020 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @since 5.2
- */
-
-import * as Core from "../../../../Core";
-import * as DomUtil from "../../../../Dom/Util";
-import * as Language from "../../../../Language";
-import UiDropdownSimple from "../../../../Ui/Dropdown/Simple";
-import { LabelFormFieldOptions } from "../../Data";
-
-class Label {
- protected readonly _formFieldContainer: HTMLElement;
- protected readonly _input: HTMLInputElement;
- protected readonly _labelChooser: HTMLElement;
- protected readonly _options: LabelFormFieldOptions;
-
- constructor(fieldId: string, labelId: string, options: Partial) {
- this._formFieldContainer = document.getElementById(fieldId + "Container")!;
- this._labelChooser = this._formFieldContainer.getElementsByClassName("labelChooser")[0] as HTMLElement;
- this._options = Core.extend(
- {
- forceSelection: false,
- showWithoutSelection: false,
- },
- options,
- ) as LabelFormFieldOptions;
-
- this._input = document.createElement("input");
- this._input.type = "hidden";
- this._input.id = fieldId;
- this._input.name = fieldId;
- this._input.value = labelId;
- this._formFieldContainer.appendChild(this._input);
-
- const labelChooserId = DomUtil.identify(this._labelChooser);
-
- // init dropdown
- let dropdownMenu = UiDropdownSimple.getDropdownMenu(labelChooserId)!;
- if (dropdownMenu === null) {
- UiDropdownSimple.init(this._labelChooser.getElementsByClassName("dropdownToggle")[0] as HTMLElement);
- dropdownMenu = UiDropdownSimple.getDropdownMenu(labelChooserId)!;
- }
-
- let additionalOptionList: HTMLUListElement | null = null;
- if (this._options.showWithoutSelection || !this._options.forceSelection) {
- additionalOptionList = document.createElement("ul");
- dropdownMenu.appendChild(additionalOptionList);
-
- const dropdownDivider = document.createElement("li");
- dropdownDivider.classList.add("dropdownDivider");
- additionalOptionList.appendChild(dropdownDivider);
- }
-
- if (this._options.showWithoutSelection) {
- const listItem = document.createElement("li");
- listItem.dataset.labelId = "-1";
- this._blockScroll(listItem);
- additionalOptionList!.appendChild(listItem);
-
- const span = document.createElement("span");
- listItem.appendChild(span);
-
- const label = document.createElement("span");
- label.classList.add("badge", "label");
- label.innerHTML = Language.get("wcf.label.withoutSelection");
- span.appendChild(label);
- }
-
- if (!this._options.forceSelection) {
- const listItem = document.createElement("li");
- listItem.dataset.labelId = "0";
- this._blockScroll(listItem);
- additionalOptionList!.appendChild(listItem);
-
- const span = document.createElement("span");
- listItem.appendChild(span);
-
- const label = document.createElement("span");
- label.classList.add("badge", "label");
- label.innerHTML = Language.get("wcf.label.none");
- span.appendChild(label);
- }
-
- dropdownMenu.querySelectorAll("li:not(.dropdownDivider)").forEach((listItem: HTMLElement) => {
- listItem.addEventListener("click", (ev) => this._click(ev));
-
- if (labelId) {
- if (listItem.dataset.labelId === labelId) {
- this._selectLabel(listItem);
- }
- }
- });
- }
-
- _blockScroll(element: HTMLElement): void {
- element.addEventListener("wheel", (ev) => ev.preventDefault(), {
- passive: false,
- });
- }
-
- _click(event: Event): void {
- event.preventDefault();
-
- this._selectLabel(event.currentTarget as HTMLElement);
- }
-
- _selectLabel(label: HTMLElement): void {
- // save label
- let labelId = label.dataset.labelId;
- if (!labelId) {
- labelId = "0";
- }
-
- // replace button with currently selected label
- const displayLabel = label.querySelector("span > span")!;
- const button = this._labelChooser.querySelector(".dropdownToggle > span")!;
- button.className = displayLabel.className;
- button.textContent = displayLabel.textContent;
-
- this._input.value = labelId;
- }
-}
-
-export = Label;
diff --git a/ts/WoltLabSuite/Core/Ui/Dropdown/Simple.ts b/ts/WoltLabSuite/Core/Ui/Dropdown/Simple.ts
index f35975303bd..7b48ff9ced2 100644
--- a/ts/WoltLabSuite/Core/Ui/Dropdown/Simple.ts
+++ b/ts/WoltLabSuite/Core/Ui/Dropdown/Simple.ts
@@ -378,8 +378,11 @@ const UiDropdownSimple = {
init(button: HTMLElement, isLazyInitialization?: boolean | MouseEvent): boolean {
UiDropdownSimple.setup();
- button.setAttribute("role", "button");
- button.tabIndex = 0;
+ if (!(button instanceof HTMLButtonElement)) {
+ button.setAttribute("role", "button");
+ button.tabIndex = 0;
+ }
+
button.setAttribute("aria-haspopup", "true");
button.setAttribute("aria-expanded", "false");
diff --git a/ts/WoltLabSuite/WebComponent/index.ts b/ts/WoltLabSuite/WebComponent/index.ts
index b44b02355a6..21cd146e720 100644
--- a/ts/WoltLabSuite/WebComponent/index.ts
+++ b/ts/WoltLabSuite/WebComponent/index.ts
@@ -12,6 +12,7 @@ import "./fa-metadata.js";
import "./fa-brand.ts";
import "./fa-icon.ts";
import "./woltlab-core-date-time.ts";
+import "./woltlab-core-label-picker.ts";
import "./woltlab-core-loading-indicator";
import "./woltlab-core-pagination.ts";
import "./woltlab-core-reaction-summary.ts";
diff --git a/ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts b/ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts
new file mode 100644
index 00000000000..f65813a5b72
--- /dev/null
+++ b/ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts
@@ -0,0 +1,188 @@
+/**
+ * The `` provides an interactive widget to select a
+ * label out of a label group.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @woltlabExcludeBundle all
+ */
+
+{
+ class WoltlabCoreLabelPickerElement extends HTMLElement {
+ readonly #button: HTMLButtonElement;
+ #formValue: HTMLInputElement | undefined;
+ #labels = new Map();
+
+ constructor() {
+ super();
+
+ this.#button = document.createElement("button");
+ }
+
+ connectedCallback() {
+ if (this.hasAttribute("labels")) {
+ this.#labels = new Map(JSON.parse(this.getAttribute("labels")!));
+ this.removeAttribute("labels");
+ }
+
+ if (this.#labels.size === 0) {
+ throw new Error("Expected a non empty list of labels.");
+ }
+
+ const emptyLabel = this.#getHtmlForNoneLabel();
+
+ this.#button.type = "button";
+ this.#button.classList.add("dropdownToggle");
+ this.#button.innerHTML = emptyLabel;
+ this.#button.addEventListener("click", (event) => {
+ event.preventDefault();
+
+ const evt = new CustomEvent("showPicker");
+ this.dispatchEvent(evt);
+ });
+
+ // Moving the ID to the button allows clicks on the label to be forwarded.
+ this.#button.id = this.id;
+ this.removeAttribute("id");
+
+ this.append(this.#button);
+
+ const scrollableDropdownMenu = document.createElement("ul");
+ scrollableDropdownMenu.classList.add("scrollableDropdownMenu");
+ for (const [labelId, html] of this.#labels) {
+ scrollableDropdownMenu.append(this.#createLabelItem(labelId, html));
+ }
+
+ if (!this.required) {
+ const divider = document.createElement("li");
+ divider.classList.add("dropdownDivider");
+
+ scrollableDropdownMenu.append(divider);
+
+ if (this.invertible) {
+ const invertSelection = this.#createLabelItem(-1, this.#getHtmlForInvertedSelection());
+ scrollableDropdownMenu.append(invertSelection);
+ }
+
+ scrollableDropdownMenu.append(this.#createLabelItem(0, emptyLabel));
+ }
+
+ const dropdownMenu = document.createElement("ul");
+ dropdownMenu.classList.add("dropdownMenu");
+ dropdownMenu.append(scrollableDropdownMenu);
+
+ this.append(dropdownMenu);
+
+ this.classList.add("dropdown");
+
+ if (this.closest("form") !== null) {
+ if (this.#formValue === undefined) {
+ this.#formValue = document.createElement("input");
+ this.#formValue.type = "hidden";
+ this.#formValue.name = `${this.name}[${this.dataset.groupId}]`;
+ this.append(this.#formValue);
+ }
+
+ this.#formValue.value = (this.selected || 0).toString();
+ } else {
+ this.#formValue?.remove();
+ }
+
+ if (this.selected) {
+ this.#updateValue(this.selected);
+ }
+ }
+
+ #createLabelItem(labelId: number, html: string): HTMLLIElement {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.dataset.labelId = labelId.toString();
+ button.innerHTML = html;
+ button.addEventListener("click", () => {
+ this.selected = labelId;
+ });
+
+ const listItem = document.createElement("li");
+ listItem.append(button);
+
+ return listItem;
+ }
+
+ set selected(selected: number) {
+ this.setAttribute("selected", selected.toString());
+
+ this.#updateValue(selected);
+ }
+
+ get selected(): number | undefined {
+ const selected = parseInt(this.getAttribute("selected")!);
+ if (Number.isNaN(selected)) {
+ return undefined;
+ }
+
+ return selected;
+ }
+
+ set disabled(disabled: boolean) {
+ if (disabled) {
+ this.setAttribute("disabled", "");
+ } else {
+ this.removeAttribute("disabled");
+ }
+
+ this.#button.disabled = disabled;
+ if (this.#formValue) {
+ this.#formValue.disabled = disabled;
+ }
+ }
+
+ get disabled(): boolean {
+ return this.hasAttribute("disabled");
+ }
+
+ set required(required: boolean) {
+ if (required) {
+ this.setAttribute("required", "");
+ } else {
+ this.removeAttribute("required");
+ }
+ }
+
+ get required(): boolean {
+ return this.hasAttribute("required");
+ }
+
+ get invertible(): boolean {
+ return this.hasAttribute("invertible");
+ }
+
+ get name(): string {
+ return this.getAttribute("name")!;
+ }
+
+ #getHtmlForNoneLabel(): string {
+ return `${window.WoltLabLanguage.getPhrase("wcf.label.none")}`;
+ }
+
+ #getHtmlForInvertedSelection(): string {
+ return `${window.WoltLabLanguage.getPhrase("wcf.label.withoutSelection")}`;
+ }
+
+ #updateValue(labelId: number): void {
+ let html = "";
+ if (this.#labels.has(labelId)) {
+ html = this.#labels.get(labelId)!;
+ } else if (labelId === -1 && this.invertible) {
+ html = this.#getHtmlForInvertedSelection();
+ }
+
+ this.#button.innerHTML = html || this.#getHtmlForNoneLabel();
+ if (this.#formValue !== undefined) {
+ this.#formValue.value = labelId.toString();
+ }
+ }
+ }
+
+ window.customElements.define("woltlab-core-label-picker", WoltlabCoreLabelPickerElement);
+}
diff --git a/ts/global.d.ts b/ts/global.d.ts
index 29daeeab06e..c7c781eeb60 100644
--- a/ts/global.d.ts
+++ b/ts/global.d.ts
@@ -91,6 +91,15 @@ declare global {
set date(date: Date);
}
+ interface WoltlabCoreLabelPickerElement extends HTMLElement {
+ set disabled(disabled: boolean);
+ get disabled(): boolean;
+ set selected(selected: number);
+ get selected(): number | undefined;
+ set required(required: boolean);
+ get required(): boolean;
+ }
+
interface WoltlabCoreLoadingIndicatorElement extends HTMLElement {
get size(): LoadingIndicatorIconSize;
set size(size: LoadingIndicatorIconSize);
@@ -121,6 +130,7 @@ declare global {
"woltlab-core-dialog": WoltlabCoreDialogElement;
"woltlab-core-dialog-control": WoltlabCoreDialogControlElement;
"woltlab-core-date-time": WoltlabCoreDateTime;
+ "woltlab-core-label-picker": WoltlabCoreLabelPickerElement;
"woltlab-core-loading-indicator": WoltlabCoreLoadingIndicatorElement;
"woltlab-core-pagination": WoltlabCorePaginationElement;
"woltlab-core-google-maps": WoltlabCoreGoogleMapsElement;
diff --git a/wcfsetup/install/files/acp/templates/__labelFormField.tpl b/wcfsetup/install/files/acp/templates/__labelFormField.tpl
index fe67833766f..3f1037818b9 100644
--- a/wcfsetup/install/files/acp/templates/__labelFormField.tpl
+++ b/wcfsetup/install/files/acp/templates/__labelFormField.tpl
@@ -1,41 +1 @@
-
-
-
-
-
+{@$field->getLabelPicker()->toHtml()}
diff --git a/wcfsetup/install/files/acp/templates/articleAdd.tpl b/wcfsetup/install/files/acp/templates/articleAdd.tpl
index a718b081746..bc9ee08cea7 100644
--- a/wcfsetup/install/files/acp/templates/articleAdd.tpl
+++ b/wcfsetup/install/files/acp/templates/articleAdd.tpl
@@ -88,7 +88,7 @@
{/if}
@@ -163,45 +169,23 @@
{event name='categoryFields'}
- {if $labelGroups|count}
- {foreach from=$labelGroups item=labelGroup}
- {if $labelGroup|count}
- groupID]|isset} class="formError"{/if}>
-
- -
-
-
- {if $errorField == 'label' && $errorType[$labelGroup->groupID]|isset}
-
- {if $errorType[$labelGroup->groupID] == 'missing'}
- {lang}wcf.label.error.missing{/lang}
- {else}
- {lang}wcf.label.error.invalid{/lang}
- {/if}
-
+ {foreach from=$labelPickerGroup item=labelPicker}
+
+
+ -
+ {@$labelPicker->toHtml()}
+ {if $errorField == 'label' && $errorType[$labelPicker->labelGroup->groupID]|isset}
+
+ {if $errorType[$labelPicker->labelGroup->groupID] == 'missing'}
+ {lang}wcf.label.error.missing{/lang}
+ {else}
+ {lang}wcf.label.error.invalid{/lang}
{/if}
-
-
- {/if}
- {/foreach}
- {/if}
+
+ {/if}
+
+
+ {/foreach}
@@ -614,14 +598,9 @@