Skip to content

Commit 56a3de7

Browse files
committed
temp
1 parent adf4477 commit 56a3de7

File tree

9 files changed

+275
-10
lines changed

9 files changed

+275
-10
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { BaseDropdown, BaseDropdownItem } from '../../partials';
2+
import { createNodesFromTemplate } from '../../utils/dom';
3+
4+
export enum DropdownMultiInputAction {
5+
Check = 'check',
6+
Uncheck = 'uncheck',
7+
}
8+
9+
export class DropdownMultiInput extends BaseDropdown {
10+
private _sourceInputNode: HTMLSelectElement;
11+
private _value: string[];
12+
13+
constructor(container: HTMLDivElement) {
14+
super(container);
15+
16+
const _sourceInputNode = this._sourceNode.querySelector<HTMLSelectElement>('select');
17+
18+
if (!_sourceInputNode) {
19+
throw new Error('DropdownSingleInput: Required elements are missing in the container.');
20+
}
21+
22+
this._sourceInputNode = _sourceInputNode;
23+
this._value = this.getSelectedValuesFromSource();
24+
25+
this.onItemClick = this.onItemClick.bind(this);
26+
}
27+
28+
protected getSelectedValuesFromSource(): string[] {
29+
const selectedValues = Array.from(this._sourceInputNode.selectedOptions).map((option) => option.value);
30+
31+
return selectedValues;
32+
}
33+
34+
protected isSelected(id: string): boolean {
35+
return this._value.includes(id);
36+
}
37+
38+
protected setSource() {
39+
this._sourceInputNode.innerHTML = '';
40+
41+
this._itemsMap.forEach((item) => {
42+
const option = document.createElement('option');
43+
44+
option.value = item.id;
45+
option.textContent = item.label;
46+
47+
if (this._value.includes(item.id)) {
48+
option.selected = true;
49+
}
50+
51+
this._sourceInputNode.appendChild(option);
52+
});
53+
54+
this.setValue(this._sourceInputNode.value);
55+
}
56+
57+
protected setSourceValue(id: string, actionPerformed: DropdownMultiInputAction) {
58+
const optionNode = this._sourceInputNode.querySelector<HTMLOptionElement>(`option[value="${id}"]`);
59+
60+
if (!optionNode) {
61+
return;
62+
}
63+
64+
optionNode.selected = actionPerformed === DropdownMultiInputAction.Check;
65+
}
66+
67+
protected setSelectedItem(id: string, actionPerformed: DropdownMultiInputAction) {
68+
const listItemNode = this._itemsContainerNode.querySelector<HTMLLIElement>(`.ids-dropdown__item[data-id="${id}"]`);
69+
const checkboxNode = listItemNode?.querySelector<HTMLInputElement>('.ids-input--checkbox');
70+
71+
if (!checkboxNode) {
72+
return;
73+
}
74+
75+
checkboxNode.checked = actionPerformed === DropdownMultiInputAction.Check;
76+
}
77+
78+
protected setSelectionInfo(values: string[]) {
79+
const items = values.map((value) => this.getItemById(value)).filter((item): item is BaseDropdownItem => item !== undefined);
80+
81+
if (items.length) {
82+
// TODO: implement OverflowList when merged
83+
this._selectionInfoItemsNode.textContent = items.map(({ label }) => label).join(', ');
84+
this._selectionInfoItemsNode.removeAttribute('hidden');
85+
this._placeholderNode.setAttribute('hidden', '');
86+
} else {
87+
this._selectionInfoItemsNode.textContent = '';
88+
this._selectionInfoItemsNode.setAttribute('hidden', '');
89+
this._placeholderNode.removeAttribute('hidden');
90+
}
91+
}
92+
93+
public getItemContent(item: BaseDropdownItem, listItem: HTMLLIElement): NodeListOf<ChildNode> | string {
94+
const placeholders = {
95+
'{{ id }}': item.id,
96+
'{{ label }}': item.label,
97+
};
98+
99+
const itemContent = createNodesFromTemplate(listItem.innerHTML, placeholders);
100+
101+
return itemContent instanceof NodeList ? itemContent : item.label;
102+
}
103+
104+
public setItems(items: BaseDropdownItem[]) {
105+
super.setItems(items);
106+
107+
const tempValue = this._value;
108+
109+
this._value = [];
110+
111+
tempValue.forEach((value) => {
112+
this.setValue(value);
113+
});
114+
}
115+
116+
public setValue(value: string) {
117+
const isSelected = this.isSelected(value);
118+
const nextValue = isSelected ? this._value.filter((iteratedValue) => iteratedValue !== value) : [...this._value, value];
119+
const actionPerformed = isSelected ? DropdownMultiInputAction.Uncheck : DropdownMultiInputAction.Check;
120+
121+
this.setSourceValue(value, actionPerformed);
122+
this.setSelectedItem(value, actionPerformed);
123+
this.setSelectionInfo(nextValue);
124+
125+
this._value = nextValue;
126+
}
127+
128+
public onItemClick = (event: MouseEvent) => {
129+
if (event.currentTarget instanceof HTMLLIElement) {
130+
const { id } = event.currentTarget.dataset;
131+
132+
if (!id) {
133+
return;
134+
}
135+
136+
this.setValue(id);
137+
}
138+
};
139+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './dropdown_multi_input';
12
export * from './dropdown_single_input';

src/bundle/Resources/public/ts/init_components.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { CheckboxInput, CheckboxesListField } from './components/checkbox';
2+
import { DropdownMultiInput, DropdownSingleInput } from './components/dropdown';
23
import { InputTextField, InputTextInput } from './components/input_text';
34
import { Accordion } from './components/accordion';
45
import { AltRadioInput } from './components/alt_radio/alt_radio_input';
5-
import { DropdownSingleInput } from './components/dropdown/dropdown_single_input';
66

77
const accordionContainers = document.querySelectorAll<HTMLDivElement>('.ids-accordion:not([data-ids-custom-init])');
88

@@ -36,9 +36,17 @@ checkboxesFieldContainers.forEach((checkboxesFieldContainer: HTMLDivElement) =>
3636
checkboxesFieldInstance.init();
3737
});
3838

39-
const dropdownContainers = document.querySelectorAll<HTMLDivElement>('.ids-dropdown:not([data-ids-custom-init])');
39+
const dropdownMultiContainers = document.querySelectorAll<HTMLDivElement>('.ids-dropdown--multi:not([data-ids-custom-init])');
4040

41-
dropdownContainers.forEach((dropdownContainer: HTMLDivElement) => {
41+
dropdownMultiContainers.forEach((dropdownContainer: HTMLDivElement) => {
42+
const dropdownInstance = new DropdownMultiInput(dropdownContainer);
43+
44+
dropdownInstance.init();
45+
});
46+
47+
const dropdownSingleContainers = document.querySelectorAll<HTMLDivElement>('.ids-dropdown--single:not([data-ids-custom-init])');
48+
49+
dropdownSingleContainers.forEach((dropdownContainer: HTMLDivElement) => {
4250
const dropdownInstance = new DropdownSingleInput(dropdownContainer);
4351

4452
dropdownInstance.init();

src/bundle/Resources/public/ts/partials/base_dropdown/base_dropdown.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,17 @@ export abstract class BaseDropdown extends Base {
112112

113113
listItem.dataset.id = item.id;
114114
listItem.dataset.label = item.label;
115-
listItem.textContent = this.getItemContent(item);
115+
116+
const itemContent = this.getItemContent(item, listItem);
117+
118+
if (itemContent instanceof NodeList) {
119+
listItem.innerHTML = '';
120+
Array.from(itemContent).forEach((childNode) => {
121+
listItem.appendChild(childNode);
122+
});
123+
} else {
124+
listItem.textContent = itemContent;
125+
}
116126

117127
this._itemsNode.appendChild(listItem);
118128
});
@@ -132,15 +142,12 @@ export abstract class BaseDropdown extends Base {
132142
}
133143
}
134144

135-
protected abstract setSourceValue(id: string): void;
136-
137-
protected abstract setSelectedItem(id: string): void;
138-
139145
protected abstract setSource(): void;
140146

141147
/******* Items management ********/
142148

143-
public getItemContent(item: BaseDropdownItem) {
149+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
150+
public getItemContent(item: BaseDropdownItem, _listItem: HTMLLIElement): NodeListOf<ChildNode> | string {
144151
return item.label;
145152
}
146153

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const createNodesFromTemplate = (template: string, placeholders: Record<string, string>): NodeListOf<ChildNode> | null => {
2+
const container = document.createElement('div');
3+
let result = template;
4+
5+
Object.entries(placeholders).forEach(([placeholder, value]) => {
6+
result = result.replaceAll(placeholder, value);
7+
});
8+
9+
container.innerHTML = result;
10+
11+
if (container instanceof HTMLElement && container.childNodes.length > 0) {
12+
return container.childNodes;
13+
}
14+
15+
return null;
16+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% extends '@IbexaDesignSystemTwig/themes/standard/design_system/partials/base_dropdown.html.twig' %}
2+
3+
{% set class = html_classes('ids-dropdown--multi', attributes.render('class') ?? '') %}
4+
5+
{% block source %}
6+
<select multiple name="{{ name }}"{% if disabled %} disabled{% endif %}>
7+
{% for item in items %}
8+
<option value="{{ item.id }}"{% if item.id in value %} selected{% endif %}>{{ item.label }}</option>
9+
{% endfor %}
10+
</select>
11+
{% endblock source %}
12+
13+
{% block item_content %}
14+
<twig:ibexa:checkbox:input
15+
name="{{ name }}-checkbox"
16+
:checked="item.id in value"
17+
:value="item.id"
18+
data-ids-custom-init="true"
19+
/>
20+
{{ item.label }}
21+
{% endblock item_content %}
22+
23+
{% block selected_items %}
24+
{{ selected_items|map(item => item.label)|join(', ') }}
25+
{% endblock selected_items %}

src/bundle/Resources/views/themes/standard/design_system/partials/base_dropdown.html.twig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@
6666
</div>
6767
{% block templates %}
6868
{% set item_content %}
69-
{{ block('item_content') is defined ? block('item_content') : '' }}
69+
{% with { item: item_template_props } %}
70+
{{ block('item_content') is defined ? block('item_content') : '' }}
71+
{% endwith %}
7072
{% endset %}
7173

7274
<template class="ids-dropdown__template" data-id="item">

src/lib/Twig/Components/AbstractDropdown.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ abstract class AbstractDropdown
2929
/** @var array<DropdownItem> */
3030
public array $items = [];
3131

32+
/** @var array<string> */
33+
public array $itemTemplateProps = ['id', 'label'];
34+
3235
public string $placeholder;
3336

3437
#[ExposeInTemplate('max_visible_items')]
@@ -77,5 +80,19 @@ public function getIsSearchVisible(): bool
7780
return count($this->items) > $this->maxVisibleItems;
7881
}
7982

83+
/**
84+
* @return array<string, string>
85+
*/
86+
#[ExposeInTemplate('item_template_props')]
87+
public function getItemTemplateProps(): array
88+
{
89+
$itemPropsPatterns = array_map(
90+
fn (string $name): string => '{{ ' . $name . ' }}',
91+
$this->itemTemplateProps
92+
);
93+
94+
return array_combine($this->itemTemplateProps, $itemPropsPatterns);
95+
}
96+
8097
abstract protected function configurePropsResolver(OptionsResolver $resolver): void;
8198
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\DesignSystemTwig\Twig\Components\DropdownMulti;
10+
11+
use Ibexa\DesignSystemTwig\Twig\Components\AbstractDropdown;
12+
use Symfony\Component\OptionsResolver\OptionsResolver;
13+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
14+
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
15+
use Symfony\UX\TwigComponent\Attribute\PostMount;
16+
17+
#[AsTwigComponent('ibexa:dropdown_multi:input')]
18+
final class Input extends AbstractDropdown
19+
{
20+
/** @var array<string> */
21+
public array $value = [];
22+
23+
#[ExposeInTemplate('selected_items')]
24+
public function getSelectedItems(): array
25+
{
26+
$items = $this->items;
27+
return array_map(
28+
static function (string $id) use ($items) {
29+
return array_find($items, static function (array $item) use ($id) {
30+
return $item['id'] === $id;
31+
});
32+
},
33+
$this->value
34+
);
35+
}
36+
37+
#[ExposeInTemplate('is_empty')]
38+
public function isEmpty(): bool
39+
{
40+
return count($this->value) === 0;
41+
}
42+
43+
protected function configurePropsResolver(OptionsResolver $resolver): void
44+
{
45+
$resolver
46+
->define('value')
47+
->allowedTypes('array')
48+
->default([]);
49+
}
50+
}

0 commit comments

Comments
 (0)