Skip to content
Merged
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
32 changes: 7 additions & 25 deletions com.woltlab.wcf/templates/__labelSelection.tpl
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
{foreach from=$labelGroups item=labelGroup}
{if $labelGroup|count}
<dt><label>{$labelGroup->getTitle()}</label></dt>
{assign var='__labelPickerID' value='labelGroup'|concat:$labelGroup->groupID}
{assign var='__labelPickerName' value='labelIDs['|concat:$labelGroup->groupID:']'}
<dt><label for="{$__labelPickerID}">{$labelGroup->getTitle()}</label></dt>
<dd>
<ul class="labelList jsOnly">
<li class="dropdown labelChooser" id="labelGroup{$labelGroup->groupID}" data-group-id="{$labelGroup->groupID}">
<div class="dropdownToggle" data-toggle="labelGroup{$labelGroup->groupID}"><span class="badge label">{lang}wcf.label.none{/lang}</span></div>
<div class="dropdownMenu">
<ul class="scrollableDropdownMenu">
{foreach from=$labelGroup item=label}
<li data-label-id="{$label->labelID}"><span>{unsafe:$label->render()}</span></li>
{/foreach}
</ul>
</div>
</li>
</ul>
{if $noLabelSelectionNoScript|empty}
<noscript>
{foreach from=$labelGroups item=labelGroup}
<select name="labelIDs[{$labelGroup->groupID}]">
<option value="0">{lang}wcf.label.none{/lang}</option>
<option value="-1">{lang}wcf.label.withoutSelection{/lang}</option>
{foreach from=$labelGroup item=label}
<option value="{$label->labelID}"{if $labelIDs[$labelGroup->groupID]|isset && $labelIDs[$labelGroup->groupID] == $label->labelID} selected{/if}>{$label->getTitle()}</option>
{/foreach}
</select>
{/foreach}
</noscript>
{if $labelIDs[$labelGroup->groupID]|isset}
{unsafe:$labelGroup->renderPicker($__labelPickerName, $labelIDs[$labelGroup->groupID], false, true, $__labelPickerID)}
{else}
{unsafe:$labelGroup->renderPicker($__labelPickerName, 0, false, true, $__labelPickerID)}
{/if}
</dd>
{/if}
Expand Down
40 changes: 1 addition & 39 deletions com.woltlab.wcf/templates/shared_labelFormField.tpl
Original file line number Diff line number Diff line change
@@ -1,39 +1 @@
<ul class="labelList jsOnly" data-object-id="{$field->getLabelGroup()->groupID}">
<li class="dropdown labelChooser" data-group-id="{$field->getLabelGroup()->groupID}">
<div class="dropdownToggle" data-toggle="labelGroup{$field->getLabelGroup()->groupID}">
<span class="badge label">{lang}wcf.label.none{/lang}</span>
</div>
<div class="dropdownMenu">
<ul class="scrollableDropdownMenu">
{foreach from=$field->getLabelGroup() item=label}
<li data-label-id="{$label->labelID}">
<span>{unsafe:$label->render()}</span>
</li>
{/foreach}
</ul>
</div>
</li>
</ul>

<noscript>
<select name="{$field->getPrefixedId()}[{$field->getLabelGroup()->groupID}]">
{foreach from=$field->getLabelGroup() item=label}
<option value="{$label->labelID}">{$label->getTitle()}</option>
{/foreach}
</select>
</noscript>

<script data-relocate="true">
require(['Dom/Util', 'WoltLabSuite/Core/Form/Builder/Field/Controller/Label'], function(DomUtil, FormBuilderFieldLabel) {
{jsphrase name='wcf.label.none'}
{jsphrase name='wcf.label.withoutSelection'}

new FormBuilderFieldLabel(
'{unsafe:$field->getPrefixedId()|encodeJS}',
{if $field->getValue()}'{unsafe:$field->getValue()|encodeJS}'{else}null{/if},
{
forceSelection: {if $field->getLabelGroup()->forceSelection}true{else}false{/if}
}
);
});
</script>
{unsafe:$field->getLabelGroup()->renderPicker($field->getPrefixedId(), $field->getValue(), $field->isRequired(), $field->isInvertible(), $field->getPrefixedId())}
3 changes: 3 additions & 0 deletions ts/WoltLabSuite/Core/Form/Builder/Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export interface FormBuilderDialogOptions {
usesDboAction: boolean;
}

/**
* @deprecated 6.3
*/
export interface LabelFormFieldOptions {
forceSelection: boolean;
showWithoutSelection: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* @copyright 2001-2020 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 5.2
* @deprecated 6.3 `LabelFormField` uses `<woltlab-core-label-picker>` instead.
*/

import * as Core from "../../../../Core";
Expand Down
8 changes: 8 additions & 0 deletions ts/WoltLabSuite/Core/Ui/Dropdown/Simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,14 @@ const UiDropdownSimple = {
_menuContainer.appendChild(menu);

const containerId = DomUtil.identify(dropdown);

// A dropdown with the same id might have been removed from the DOM,
// e.g. when a dialog with a fixed set of ids is shown again.
const existingDropdown = _dropdowns.get(containerId);
if (existingDropdown !== undefined && existingDropdown !== dropdown && !existingDropdown.isConnected) {
UiDropdownSimple.destroy(containerId);
}

if (!_dropdowns.has(containerId)) {
button.classList.add("jsDropdownEnabled");
button.addEventListener("click", toggle);
Expand Down
1 change: 1 addition & 0 deletions ts/WoltLabSuite/WebComponent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import "./fa-brand.ts";
import "./fa-icon.ts";
import "./woltlab-core-date-time.ts";
import "./woltlab-core-file-upload.ts";
import "./woltlab-core-label-picker.ts";
import "./woltlab-core-loading-indicator.ts";
import "./woltlab-core-notice.ts";
import "./woltlab-core-pagination.ts";
Expand Down
230 changes: 230 additions & 0 deletions ts/WoltLabSuite/WebComponent/woltlab-core-label-picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/**
* `<woltlab-core-label-picker>` provides a dropdown to select a single label
* out of a label group. The element is form-associated and submits the id of
* the selected label, `0` for no selection or `-1` for objects without any
* label of this group (requires `invertible`).
*
* Usage: `<woltlab-core-label-picker name="labelIDs[1]" labels="[[1,&quot;…&quot;]]" value="1"></woltlab-core-label-picker>`
*
* @author Alexander Ebert, Marcel Werk
* @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.3
*/

{
class WoltlabCoreLabelPickerElement extends HTMLElement {
static formAssociated = true;

readonly #internals: ElementInternals;
#button: HTMLButtonElement | undefined = undefined;
#defaultValue = 0;
#items = new Map<number, HTMLLIElement>();
#labels = new Map<number, string>();

constructor() {
super();

this.#internals = this.attachInternals();
}

connectedCallback() {
// The dropdown menu is moved into a global container, building the
// element again when it is moved within the DOM would orphan it.
if (this.#button !== undefined) {
return;
}

if (this.hasAttribute("labels")) {
this.#labels = new Map(JSON.parse(this.getAttribute("labels")!));
this.removeAttribute("labels");
}

this.#defaultValue = this.value;

this.innerHTML = "";
this.classList.add("dropdown");

this.#button = document.createElement("button");
this.#button.type = "button";
this.#button.classList.add("dropdownToggle");
this.#button.disabled = this.matches(":disabled");

this.append(this.#button, this.#buildMenu());

// Clicks on an associated `<label>` are forwarded to the host element.
this.addEventListener("click", (event) => {
if (event.target === this) {
this.#button!.focus();
}
});

this.#render();
}

formDisabledCallback(disabled: boolean): void {
if (this.#button !== undefined) {
this.#button.disabled = disabled;
}
}

formResetCallback(): void {
this.setAttribute("value", this.#defaultValue.toString());
this.#render();
}

formStateRestoreCallback(state: string | File | FormData | null): void {
if (typeof state !== "string") {
return;
}

// The restored state might reference a label that no longer exists.
const value = parseInt(state);
if (this.#isValidValue(value)) {
this.setAttribute("value", value.toString());
this.#render();
}
}

#isValidValue(value: number): boolean {
if (value === -1) {
return this.invertible;
}

return value === 0 || this.#labels.has(value);
}

#buildMenu(): HTMLElement {
const labelList = document.createElement("ul");
labelList.classList.add("scrollableDropdownMenu");
for (const [labelId, html] of this.#labels) {
labelList.append(this.#createItem(labelId, html));
}

const dropdownMenu = document.createElement("div");
dropdownMenu.classList.add("dropdownMenu");
dropdownMenu.append(labelList);

if (this.invertible || !this.required) {
const divider = document.createElement("li");
divider.classList.add("dropdownDivider");

const optionList = document.createElement("ul");
optionList.append(divider);

if (this.invertible) {
optionList.append(this.#createItem(-1, this.#getBadge("wcf.label.withoutSelection")));
}

if (!this.required) {
optionList.append(this.#createItem(0, this.#getBadge("wcf.label.none")));
}

dropdownMenu.append(optionList);
}

return dropdownMenu;
}

#createItem(labelId: number, html: string): HTMLLIElement {
const button = document.createElement("button");
button.type = "button";
button.innerHTML = html;
button.addEventListener("click", () => {
if (this.value === labelId) {
return;
}

this.value = labelId;
this.dispatchEvent(new Event("input", { bubbles: true }));
this.dispatchEvent(new Event("change", { bubbles: true }));
});

const listItem = document.createElement("li");
listItem.append(button);
this.#items.set(labelId, listItem);

return listItem;
}

#getBadge(phrase: string): string {
const badge = document.createElement("span");
badge.classList.add("badge", "label");
badge.textContent = window.WoltLabLanguage.getPhrase(phrase);

return badge.outerHTML;
}

#render(): void {
const value = this.value;

if (this.#button !== undefined) {
if (value === -1) {
this.#button.innerHTML = this.#getBadge("wcf.label.withoutSelection");
} else {
this.#button.innerHTML = this.#labels.get(value) ?? this.#getBadge("wcf.label.none");
}
}

for (const [labelId, listItem] of this.#items) {
listItem.classList.toggle("active", labelId === value);
}

this.#internals.setFormValue(value.toString());
this.#updateValidity(value);
}

#updateValidity(value: number): void {
if (this.required && value === 0) {
this.#internals.setValidity(
{ valueMissing: true },
window.WoltLabLanguage.getPhrase("wcf.global.form.error.empty"),
this.#button,
);
} else {
this.#internals.setValidity({});
}
}

get value(): number {
const value = parseInt(this.getAttribute("value") || "0");

return Number.isNaN(value) ? 0 : value;
}

set value(value: number) {
if (!this.#isValidValue(value)) {
throw new Error(`There is no label with the id ${value}.`);
}

this.setAttribute("value", value.toString());
this.#render();
}

get name(): string {
return this.getAttribute("name") || "";
}

get form(): HTMLFormElement | null {
return this.#internals.form;
}

get disabled(): boolean {
return this.hasAttribute("disabled");
}

set disabled(disabled: boolean) {
this.toggleAttribute("disabled", disabled);
}

get required(): boolean {
return this.hasAttribute("required");
}

get invertible(): boolean {
return this.hasAttribute("invertible");
}
}

window.customElements.define("woltlab-core-label-picker", WoltlabCoreLabelPickerElement);
}
12 changes: 12 additions & 0 deletions ts/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ declare global {
setData: (data: Map<number, number>, selectedReaction?: number) => void;
}

interface WoltlabCoreLabelPickerElement extends HTMLElement {
get value(): number;
set value(value: number);
get name(): string;
get form(): HTMLFormElement | null;
get disabled(): boolean;
set disabled(disabled: boolean);
get required(): boolean;
get invertible(): boolean;
}

interface WoltlabCorePaginationElement extends HTMLElement {
getLinkUrl(page: number): string;
jumpToPage(page: number): void;
Expand Down Expand Up @@ -151,6 +162,7 @@ declare global {
"woltlab-core-file": WoltlabCoreFileElement;
"woltlab-core-file-upload": WoltlabCoreFileUploadElement;
"woltlab-core-google-maps": WoltlabCoreGoogleMapsElement;
"woltlab-core-label-picker": WoltlabCoreLabelPickerElement;
"woltlab-core-loading-indicator": WoltlabCoreLoadingIndicatorElement;
"woltlab-core-notice": WoltlabCoreNoticeElement;
"woltlab-core-pagination": WoltlabCorePaginationElement;
Expand Down
1 change: 1 addition & 0 deletions wcfsetup/install/files/js/WCF.Label.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ WCF.Label = {};
* @param string containerSelector
* @param string submitButtonSelector
* @param boolean showWithoutSelection
* @deprecated 6.3 Use `ViewableLabelGroup::renderPicker()` instead.
*/
WCF.Label.Chooser = Class.extend({
/**
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading