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
40 changes: 11 additions & 29 deletions com.woltlab.wcf/templates/shared_booleanFormField.tpl
Original file line number Diff line number Diff line change
@@ -1,29 +1,11 @@
<ol class="flexibleButtonGroup">
<li>
<input type="radio" {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="1" {*
*}data-no-input-id="{$field->getPrefixedId()}_no"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->isAutofocused()} autofocus{/if}{*
*}{if $field->isRequired()} required{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{if $field->getValue()} checked{/if}{*
*}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}>
<label for="{$field->getPrefixedId()}" class="green">{icon name='check'} {lang}wcf.global.form.boolean.yes{/lang}</label>
</li>
<li>
<input type="radio" {*
*}id="{$field->getPrefixedId()}_no" {*
*}name="{$field->getPrefixedId()}" {*
*}value="0"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{if !$field->getValue()} checked{/if}{*
*}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}>
<label for="{$field->getPrefixedId()}_no" class="red">{icon name='xmark'} {lang}wcf.global.form.boolean.no{/lang}</label>
</li>
</ol>
<woltlab-core-toggle-button {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="1"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->isAutofocused()} autofocus{/if}{*
*}{if $field->isRequired()} required{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{if $field->getValue()} checked{/if}{*
*}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}></woltlab-core-toggle-button>
33 changes: 10 additions & 23 deletions com.woltlab.wcf/templates/shared_checkboxFormField.tpl
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
<dl id="{$field->getPrefixedId()}Container" {if !$field->getClasses()|empty} class="{implode from=$field->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{foreach from=$field->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{if !$field->checkDependencies()} style="display: none;"{/if}>
<dt></dt>
<dd>
<label>
<input type="checkbox" {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="1"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->isRequired()} required{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{if $field->getValue()} checked{/if}{*
*}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}>
{unsafe:$field->getLabel()}{if $field->isRequired()} <span class="formFieldRequired">*</span>{/if}
</label>

{include file='shared_formFieldDescription'}
{include file='shared_formFieldErrors'}
{include file='shared_formFieldDependencies'}
{include file='shared_formFieldDataHandler'}
</dd>
</dl>
<input type="checkbox" {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="1"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->isRequired()} required{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{if $field->getValue()} checked{/if}{*
*}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}>
18 changes: 18 additions & 0 deletions com.woltlab.wcf/templates/shared_inlineLabelFormField.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<dl id="{$field->getPrefixedId()}Container"{*
*}{if !$field->getClasses()|empty} class="{implode from=$field->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{foreach from=$field->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}{if !$field->checkDependencies()} style="display: none;"{/if}{*
*}>
<dt></dt>
<dd>
<label>
{unsafe:$field->getFieldHtml()}
{unsafe:$field->getLabel()}{if $field->isRequired() && $form->marksRequiredFields()} <span class="formFieldRequired">*</span>{/if}
</label>

{include file='shared_formFieldErrors'}
{include file='shared_formFieldDescription'}
{include file='shared_formFieldDependencies'}
{include file='shared_formFieldDataHandler'}
</dd>
</dl>
13 changes: 8 additions & 5 deletions ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ function _show(node: HTMLElement): void {
});
}

node.querySelectorAll("input, select, textarea").forEach((validatedField: HTMLInputElement | HTMLSelectElement) => {
const validatedFields = "input, select, textarea, woltlab-core-toggle-button";
node.querySelectorAll(validatedFields).forEach((validatedField: HTMLInputElement | HTMLSelectElement) => {
// if a container is shown, ignore all fields that
// have a hidden parent element within the container
let parentNode = validatedField.parentNode! as HTMLElement;
Expand Down Expand Up @@ -149,11 +150,13 @@ export function addDependency(dependency: FormBuilderFormFieldDependency): void
if (!_fields.has(id)) {
_fields.set(id, field);

// Toggle buttons fire `change` on every state change, including programmatic ones.
if (
field.tagName === "INPUT" &&
((field as HTMLInputElement).type === "checkbox" ||
(field as HTMLInputElement).type === "radio" ||
(field as HTMLInputElement).type === "hidden")
field.tagName === "WOLTLAB-CORE-TOGGLE-BUTTON" ||
(field.tagName === "INPUT" &&
((field as HTMLInputElement).type === "checkbox" ||
(field as HTMLInputElement).type === "radio" ||
(field as HTMLInputElement).type === "hidden"))
) {
field.addEventListener("change", () => checkDependencies());
} else {
Expand Down
8 changes: 7 additions & 1 deletion ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ class Value extends Abstract {
return false;
}

values.push((this._field as HTMLInputElement).value);
if (this._field.tagName === "WOLTLAB-CORE-TOGGLE-BUTTON") {
// The value of a toggle button is always set, regardless of its state.
const toggleButton = this._field as WoltlabCoreToggleButtonElement;
values.push(toggleButton.checked ? toggleButton.value : "0");
} else {
values.push((this._field as HTMLInputElement).value);
}
} else {
let hasCheckedField = true;
this._fields.forEach((field: HTMLInputElement) => {
Expand Down
136 changes: 106 additions & 30 deletions ts/WoltLabSuite/WebComponent/woltlab-core-toggle-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* `<woltlab-core-toggle-button>` creates a toggle button.
* Usage: `<woltlab-core-toggle-button name="foo" checked></woltlab-core-toggle-button>`
*
* The element is form-associated and submits its `value` (defaults to `1`)
* when checked, behaving like a regular checkbox.
*
* @author Marcel Werk
* @copyright 2001-2024 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
Expand All @@ -10,21 +13,32 @@

{
class WoltlabCoreToggleButtonElement extends HTMLElement {
static formAssociated = true;

static get observedAttributes() {
return ["checked"];
return ["checked", "required"];
}

readonly #internals: ElementInternals;
#defaultChecked: boolean | undefined = undefined;

constructor() {
super();

this.#internals = this.attachInternals();

const shadow = this.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = `
:host {
display: inline-flex;
vertical-align: middle;
}


:host(:disabled) {
opacity: 0.54;
}

[part="track"] {
position: relative;
border-radius: 14px;
Expand All @@ -35,6 +49,10 @@
transition: 0.4s;
}

:host(:disabled) [part="track"] {
cursor: default;
}

:host([checked]) [part="track"] {
background-color: var(--wcfToggleButtonBackgroundActive);
}
Expand Down Expand Up @@ -74,46 +92,49 @@
const iconSlot = document.createElement("slot");
iconSlot.name = "icon";
slider.append(iconSlot);
const checkboxSlot = document.createElement("slot");
checkboxSlot.name = "checkbox";
shadow.append(style, track, checkboxSlot);
shadow.append(style, track);

// Clicks on an associated `<label>` are forwarded to the host element.
this.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
this.toggle();

if (!this.matches(":disabled")) {
this.toggle();
}
});
this.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
this.toggle();

if (!this.matches(":disabled")) {
this.toggle();
}
}
});
}

connectedCallback() {
if (this.#defaultChecked === undefined) {
this.#defaultChecked = this.checked;
}

this.innerHTML = "";

this.#renderCheckbox();
this.#renderIcon();

this.setAttribute("role", "switch");
this.setAttribute("tabindex", "0");
this.setAttribute("aria-checked", this.checked.toString());
this.#updateDisabledState(this.matches(":disabled"));
this.#updateFormValue();
}

#renderCheckbox(): void {
if (!this.hasAttribute("name")) {
return;
}
formDisabledCallback(disabled: boolean): void {
this.#updateDisabledState(disabled);
}

const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.name = this.getAttribute("name")!;
checkbox.value = this.hasAttribute("value") ? this.getAttribute("value")! : "1";
checkbox.checked = this.checked;
checkbox.hidden = true;
checkbox.slot = "checkbox";
this.append(checkbox);
formResetCallback(): void {
this.checked = this.#defaultChecked === true;
}

#renderIcon(): void {
Expand All @@ -123,6 +144,29 @@
this.append(icon);
}

#updateDisabledState(disabled: boolean): void {
if (disabled) {
this.removeAttribute("tabindex");
this.setAttribute("aria-disabled", "true");
} else {
this.setAttribute("tabindex", "0");
this.removeAttribute("aria-disabled");
}
}

#updateFormValue(): void {
this.#internals.setFormValue(this.checked ? this.value : null);

if (this.required && !this.checked) {
this.#internals.setValidity(
{ valueMissing: true },
window.WoltLabLanguage.getPhrase("wcf.global.form.error.empty"),
);
} else {
this.#internals.setValidity({});
}
}

get checked(): boolean {
return this.hasAttribute("checked");
}
Expand All @@ -132,20 +176,52 @@
this.querySelector("fa-icon")?.setIcon(value ? "check" : "xmark");
}

get value(): string {
return this.getAttribute("value") ?? "1";
}

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");
}

toggle(): void {
this.checked = !this.checked;
this.dispatchEvent(new Event("input", { bubbles: true }));
}

attributeChangedCallback(name: string) {
if (name === "checked") {
this.setAttribute("aria-checked", this.checked.toString());
this.dispatchEvent(
new CustomEvent("change", {
detail: {
checked: this.checked,
},
}),
);
switch (name) {
case "checked":
this.setAttribute("aria-checked", this.checked.toString());
this.#updateFormValue();
this.dispatchEvent(
new CustomEvent("change", {
detail: {
checked: this.checked,
},
}),
);
break;

case "required":
this.#updateFormValue();
break;
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions ts/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ declare global {
toggle(): void;
get checked(): boolean;
set checked(checked: boolean);
get value(): string;
get name(): string;
get form(): HTMLFormElement | null;
get disabled(): boolean;
set disabled(disabled: boolean);
get required(): boolean;
}

interface HTMLElementTagNameMap {
Expand Down
1 change: 0 additions & 1 deletion wcfsetup/install/files/acp/templates/__boxPipGui.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
}

visibleEverywhere.addEventListener('change', updateVisibilityExceptions);
elById('visibleEverywhere_no').addEventListener('change', updateVisibilityExceptions);

updateVisibilityExceptions();
});
Expand Down
1 change: 0 additions & 1 deletion wcfsetup/install/files/acp/templates/__menuPipGui.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
}

boxVisibleEverywhere.addEventListener('change', updateVisibilityExceptions);
elById('boxVisibleEverywhere_no').addEventListener('change', updateVisibilityExceptions);

updateVisibilityExceptions();
});
Expand Down
Loading
Loading