diff --git a/com.woltlab.wcf/templates/__labelFormField.tpl b/com.woltlab.wcf/templates/__labelFormField.tpl index fe67833766f..3f1037818b9 100644 --- a/com.woltlab.wcf/templates/__labelFormField.tpl +++ b/com.woltlab.wcf/templates/__labelFormField.tpl @@ -1,41 +1 @@ - - - - - +{@$field->getLabelPicker()->toHtml()} diff --git a/com.woltlab.wcf/templates/__labelPickerGroup.tpl b/com.woltlab.wcf/templates/__labelPickerGroup.tpl new file mode 100644 index 00000000000..6fb1c003aca --- /dev/null +++ b/com.woltlab.wcf/templates/__labelPickerGroup.tpl @@ -0,0 +1,6 @@ +{foreach from=$labelPickerGroup item=labelPicker} +
+
+ {@$labelPicker->toHtml()} +
+{/foreach} diff --git a/com.woltlab.wcf/templates/__labelSelection.tpl b/com.woltlab.wcf/templates/__labelSelection.tpl index 89fbb841d2e..6e06afe13ad 100644 --- a/com.woltlab.wcf/templates/__labelSelection.tpl +++ b/com.woltlab.wcf/templates/__labelSelection.tpl @@ -1,3 +1,4 @@ +{* @deprecated 6.1 Use the new `__labelPickerGroup` template instead. *} {foreach from=$labelGroups item=labelGroup} {if $labelGroup|count}
diff --git a/com.woltlab.wcf/templates/articleAdd.tpl b/com.woltlab.wcf/templates/articleAdd.tpl index 79394c919e6..4c0d0f10632 100644 --- a/com.woltlab.wcf/templates/articleAdd.tpl +++ b/com.woltlab.wcf/templates/articleAdd.tpl @@ -87,7 +87,7 @@ {/if} @@ -161,46 +167,24 @@ {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}
@@ -613,14 +597,9 @@ {/if} {/capture} diff --git a/com.woltlab.wcf/templates/categoryArticleList.tpl b/com.woltlab.wcf/templates/categoryArticleList.tpl index 12411e94f4a..f41a7f9ca76 100644 --- a/com.woltlab.wcf/templates/categoryArticleList.tpl +++ b/com.woltlab.wcf/templates/categoryArticleList.tpl @@ -36,7 +36,7 @@
- {include file='__labelSelection'} + {include file='__labelPickerGroup'}
@@ -44,17 +44,6 @@
- - {/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 @@