Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"symfony/event-dispatcher": "^7.2",
"symfony/http-foundation": "^7.2",
"symfony/http-kernel": "^7.2",
"symfony/uid": "^7.3",
"symfony/ux-twig-component": "^2.27",
"symfony/yaml": "^7.2",
"twig/html-extra": "^3.20"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './toggle_button_field';
export * from './toggle_button_input';
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Base } from '../../partials';
import { HelperText } from '../helper_text';
import { Label } from '../label';
import { ToggleButtonInput } from './toggle_button_input';

export class ToggleButtonField extends Base {
private helperTextInstance: HelperText | null = null;
private inputInstance: ToggleButtonInput;
private labelInstance: Label | null = null;

constructor(container: HTMLDivElement) {
super(container);

const inputContainer = container.querySelector<HTMLDivElement>('.ids-toggle');

if (!inputContainer) {
throw new Error('ToggleButtonField: Input container is missing in the container.');
}

const labelContainer = container.querySelector<HTMLDivElement>('.ids-label');

if (labelContainer) {
this.labelInstance = new Label(labelContainer);
}

const helperTextContainer = container.querySelector<HTMLDivElement>('.ids-helper-text');

if (helperTextContainer) {
this.helperTextInstance = new HelperText(helperTextContainer);
}

this.inputInstance = new ToggleButtonInput(inputContainer);
}

initChildren(): void {
this.labelInstance?.init();
this.inputInstance.init();
this.helperTextInstance?.init();
}

init(): void {
super.init();

this.initChildren();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { BaseChoiceInput } from '../../partials';

export class ToggleButtonInput extends BaseChoiceInput {
private labels: { on: string; off: string };
private widgetNode: HTMLDivElement;
private toggleLabelNode: HTMLLabelElement;

static EVENTS = {
...BaseChoiceInput.EVENTS,
CHANGE: 'ids:toggle-button-input:change',
};

constructor(container: HTMLDivElement) {
super(container);

const widgetNode = this._container.querySelector<HTMLDivElement>('.ids-toggle__widget');
const toggleLabelNode = this._container.querySelector<HTMLLabelElement>('.ids-toggle__label');

if (!widgetNode || !toggleLabelNode) {
throw new Error('ToggleButtonInput: Required elements are missing in the container.');
}

const labelOn = toggleLabelNode.getAttribute('data-ids-label-on');
const labelOff = toggleLabelNode.getAttribute('data-ids-label-off');

if (!labelOn || !labelOff) {
throw new Error('ToggleButtonInput: Toggle labels are missing in label attributes.');
}

this.labels = { off: labelOff, on: labelOn };
this.widgetNode = widgetNode;
this.toggleLabelNode = toggleLabelNode;
}

protected updateLabel(): void {
const isChecked = this._inputElement.checked;

this.toggleLabelNode.textContent = isChecked ? this.labels.on : this.labels.off;
}

protected initWidgets(): void {
this.widgetNode.addEventListener('click', () => {
this._inputElement.focus();
this._inputElement.checked = !this._inputElement.checked;
this._inputElement.dispatchEvent(new Event('change', { bubbles: true }));
});
}

protected initInputEvents(): void {
this._inputElement.addEventListener('focus', () => {
this._container.classList.add('ids-toggle--focused');
});

this._inputElement.addEventListener('blur', () => {
this._container.classList.remove('ids-toggle--focused');
});

this._inputElement.addEventListener('change', () => {
const changeEvent = new CustomEvent(ToggleButtonInput.EVENTS.CHANGE, {
bubbles: true,
detail: this._inputElement.checked,
});

this.updateLabel();
this._container.classList.toggle('ids-toggle--checked', this._inputElement.checked);
this._container.dispatchEvent(changeEvent);
});
}

public init() {
super.init();

this.initInputEvents();
this.initWidgets();
}
}
17 changes: 17 additions & 0 deletions src/bundle/Resources/public/ts/init_components.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CheckboxInput, CheckboxesListField } from './components/checkbox';
import { InputTextField, InputTextInput } from './components/input_text';
import { ToggleButtonField, ToggleButtonInput } from './components/toggle_button';
import { Accordion } from './components/accordion';
import { AltRadioInput } from './components/alt_radio/alt_radio_input';
import { DropdownSingleInput } from './components/dropdown/dropdown_single_input';
Expand Down Expand Up @@ -68,3 +69,19 @@ overflowListContainers.forEach((overflowListContainer: HTMLDivElement) => {

overflowListInstance.init();
});

const toggleButtonFieldContainers = document.querySelectorAll<HTMLDivElement>('.ids-toggle-field:not([data-ids-custom-init])');

toggleButtonFieldContainers.forEach((toggleButtonFieldContainer: HTMLDivElement) => {
const toggleButtonFieldInstance = new ToggleButtonField(toggleButtonFieldContainer);

toggleButtonFieldInstance.init();
});

const toggleButtonContainers = document.querySelectorAll<HTMLDivElement>('.ids-toggle:not([data-ids-custom-init])');

toggleButtonContainers.forEach((toggleButtonContainer: HTMLDivElement) => {
const toggleButtonInstance = new ToggleButtonInput(toggleButtonContainer);

toggleButtonInstance.init();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends '@IbexaDesignSystemTwig/themes/standard/design_system/partials/base_field.html.twig' %}

{% set class = html_classes('ids-toggle-field', attributes.render('class') ?? '') %}

{% block content %}
<twig:ibexa:toggle_button:input {{ ...input }} />
{% endblock content %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% set component_classes =
html_cva(
base: html_classes(
'ids-toggle',
{
'ids-toggle--checked': checked,
'ids-toggle--disabled': disabled,
}
),
variants: {
size: {
medium: 'ids-toggle--medium',
small: 'ids-toggle--small'
}
}
)
%}

<div class="{{ component_classes.apply({ size }, attributes.render('class')) }}" {{ attributes }}>
<div class="ids-toggle__source">
<twig:ibexa:checkbox:input
:id="id"
:name="name"
:value="value"
:checked="checked"
:disabled="disabled"
:required="required"
data-ids-custom-init="1"
/>
</div>
<div class="ids-toggle__widget" role="button">
<div class="ids-toggle__indicator"></div>
</div>
<twig:ibexa:choice_input_label
class="ids-toggle__label"
:for="id"
:data-ids-label-on="onLabel"
:data-ids-label-off="offLabel"
>
{{ checked ? onLabel : offLabel }}
</twig:ibexa:choice_input_label>
</div>
8 changes: 4 additions & 4 deletions src/lib/Twig/Components/AbstractField.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
use Symfony\UX\TwigComponent\Attribute\PreMount;

/**
* @phpstan-type AttrMap array<string, scalar>
* @phpstan-type AttributeMap array<string, scalar>
*/
abstract class AbstractField
{
/** @var non-empty-string */
public string $name;

/** @var AttrMap */
/** @var AttributeMap */
#[ExposeInTemplate(name: 'label_extra', getter: 'getLabelExtra')]
public array $labelExtra = [];

/** @var AttrMap */
/** @var AttributeMap */
#[ExposeInTemplate('helper_text_extra')]
public array $helperTextExtra = [];

Expand Down Expand Up @@ -66,7 +66,7 @@ public function validate(array $props): array
}

/**
* @return AttrMap
* @return AttributeMap
*/
public function getLabelExtra(): array
{
Expand Down
49 changes: 3 additions & 46 deletions src/lib/Twig/Components/InputText/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,20 @@
namespace Ibexa\DesignSystemTwig\Twig\Components\InputText;

use Ibexa\DesignSystemTwig\Twig\Components\AbstractField;
use Symfony\Component\OptionsResolver\Options;
use Ibexa\DesignSystemTwig\Twig\Components\SingleInputFieldTrait;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

/**
* @phpstan-type AttrMap array<string, scalar>
*/
#[AsTwigComponent('ibexa:input_text:field')]
final class Field extends AbstractField
{
/** @var non-empty-string */
public string $id;

/** @var AttrMap */
#[ExposeInTemplate(name: 'input', getter: 'getInput')]
public array $input = [];
use SingleInputFieldTrait;

public string $type = 'input-text';

public string $value = '';

/**
* @return AttrMap
*/
public function getLabelExtra(): array
{
return $this->labelExtra + ['for' => $this->id, 'required' => $this->required];
}

/**
* @return AttrMap
*/
public function getInput(): array
{
return $this->input + [
'id' => $this->id,
'name' => $this->name,
'required' => $this->required,
'value' => $this->value,
'data-ids-custom-init' => 'true',
];
}

protected function configurePropsResolver(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'id' => null,
'input' => [],
]);
$resolver->setAllowedTypes('input', 'array');
$resolver->setNormalizer('input', static function (Options $options, array $attributes) {
return self::assertForbidden($attributes, ['id', 'name', 'required', 'value'], 'input');
});
$this->configureSingleInputFieldOptions($resolver, null);
$resolver->setRequired(['name']);
$resolver->setAllowedTypes('id', ['null', 'string']);
$resolver->setDefaults(['value' => '']);
$resolver->setAllowedTypes('value', 'string');
}
}
Loading