diff --git a/src/bundle/Resources/public/ts/components/dropdown/dropdown_multi_input.ts b/src/bundle/Resources/public/ts/components/dropdown/dropdown_multi_input.ts new file mode 100644 index 00000000..c3e9ed51 --- /dev/null +++ b/src/bundle/Resources/public/ts/components/dropdown/dropdown_multi_input.ts @@ -0,0 +1,143 @@ +import { BaseDropdown, BaseDropdownItem } from '../../partials'; +import { createNodesFromTemplate } from '../../utils/dom'; + +export enum DropdownMultiInputAction { + Check = 'check', + Uncheck = 'uncheck', +} + +export class DropdownMultiInput extends BaseDropdown { + private _sourceInputNode: HTMLSelectElement; + private _value: string[]; + + constructor(container: HTMLDivElement) { + super(container); + + const _sourceInputNode = this._sourceNode.querySelector('select'); + + if (!_sourceInputNode) { + throw new Error('DropdownMultiInput: Required elements are missing in the container.'); + } + + this._sourceInputNode = _sourceInputNode; + this._value = this.getSelectedValuesFromSource(); + + this.onItemClick = this.onItemClick.bind(this); + } + + protected getSelectedValuesFromSource(): string[] { + const selectedValues = Array.from(this._sourceInputNode.selectedOptions).map((option) => option.value); + + return selectedValues; + } + + protected isSelected(id: string): boolean { + return this._value.includes(id); + } + + protected setSource() { + this._sourceInputNode.innerHTML = ''; + + this._itemsMap.forEach((item) => { + const option = document.createElement('option'); + + option.value = item.id; + option.textContent = item.label; + + if (this._value.includes(item.id)) { + option.selected = true; + } + + this._sourceInputNode.appendChild(option); + }); + + this.setValues(this.getSelectedValuesFromSource()); + } + + protected setSourceValue(id: string, actionPerformed: DropdownMultiInputAction) { + const optionNode = this._sourceInputNode.querySelector(`option[value="${id}"]`); + + if (!optionNode) { + return; + } + + optionNode.selected = actionPerformed === DropdownMultiInputAction.Check; + } + + protected setSelectedItem(id: string, actionPerformed: DropdownMultiInputAction) { + const listItemNode = this._itemsContainerNode.querySelector(`.ids-dropdown__item[data-id="${id}"]`); + const checkboxNode = listItemNode?.querySelector('.ids-input--checkbox'); + + if (!checkboxNode) { + return; + } + + checkboxNode.checked = actionPerformed === DropdownMultiInputAction.Check; + } + + protected setSelectionInfo(values: string[]) { + const items = values.map((value) => this.getItemById(value)).filter((item): item is BaseDropdownItem => item !== undefined); + + if (items.length) { + // TODO: implement OverflowList when merged + this._selectionInfoItemsNode.textContent = items.map(({ label }) => label).join(', '); + this._selectionInfoItemsNode.removeAttribute('hidden'); + this._placeholderNode.setAttribute('hidden', ''); + } else { + this._selectionInfoItemsNode.textContent = ''; + this._selectionInfoItemsNode.setAttribute('hidden', ''); + this._placeholderNode.removeAttribute('hidden'); + } + } + + public getItemContent(item: BaseDropdownItem, listItem: HTMLLIElement): NodeListOf | string { + const placeholders = { + '{{ id }}': item.id, + '{{ label }}': item.label, + }; + + const itemContent = createNodesFromTemplate(listItem.innerHTML, placeholders); + + return itemContent instanceof NodeList ? itemContent : item.label; + } + + public setItems(items: BaseDropdownItem[]) { + super.setItems(items); + + const tempValue = this._value; + + this._value = []; + + this.setValues(tempValue); + } + + public setValues(values: string[]) { + values.forEach((value) => { + this.setValue(value); + }); + } + + public setValue(value: string) { + const isSelected = this.isSelected(value); + const nextValue = isSelected ? this._value.filter((iteratedValue) => iteratedValue !== value) : [...this._value, value]; + const actionPerformed = isSelected ? DropdownMultiInputAction.Uncheck : DropdownMultiInputAction.Check; + + this.setSourceValue(value, actionPerformed); + this.setSelectedItem(value, actionPerformed); + this.setSelectionInfo(nextValue); + + this._value = nextValue; + } + + public onItemClick = (event: MouseEvent) => { + if (event.currentTarget instanceof HTMLLIElement) { + const { id } = event.currentTarget.dataset; + + if (!id) { + return; + } + + this.setValue(id); + } + }; +} diff --git a/src/bundle/Resources/public/ts/components/dropdown/index.ts b/src/bundle/Resources/public/ts/components/dropdown/index.ts index c94f8831..24f61dde 100644 --- a/src/bundle/Resources/public/ts/components/dropdown/index.ts +++ b/src/bundle/Resources/public/ts/components/dropdown/index.ts @@ -1 +1,2 @@ +export * from './dropdown_multi_input'; export * from './dropdown_single_input'; diff --git a/src/bundle/Resources/public/ts/init_components.ts b/src/bundle/Resources/public/ts/init_components.ts index 1371fb55..778544eb 100644 --- a/src/bundle/Resources/public/ts/init_components.ts +++ b/src/bundle/Resources/public/ts/init_components.ts @@ -1,8 +1,8 @@ import { CheckboxInput, CheckboxesListField } from './components/checkbox'; +import { DropdownMultiInput, DropdownSingleInput } from './components/dropdown'; import { InputTextField, InputTextInput } from './components/input_text'; import { Accordion } from './components/accordion'; import { AltRadioInput } from './components/alt_radio/alt_radio_input'; -import { DropdownSingleInput } from './components/dropdown/dropdown_single_input'; import { OverflowList } from './components/overflow_list'; const accordionContainers = document.querySelectorAll('.ids-accordion:not([data-ids-custom-init])'); @@ -37,9 +37,17 @@ checkboxesFieldContainers.forEach((checkboxesFieldContainer: HTMLDivElement) => checkboxesFieldInstance.init(); }); -const dropdownContainers = document.querySelectorAll('.ids-dropdown:not([data-ids-custom-init])'); +const dropdownMultiContainers = document.querySelectorAll('.ids-dropdown--multi:not([data-ids-custom-init])'); -dropdownContainers.forEach((dropdownContainer: HTMLDivElement) => { +dropdownMultiContainers.forEach((dropdownContainer: HTMLDivElement) => { + const dropdownInstance = new DropdownMultiInput(dropdownContainer); + + dropdownInstance.init(); +}); + +const dropdownSingleContainers = document.querySelectorAll('.ids-dropdown--single:not([data-ids-custom-init])'); + +dropdownSingleContainers.forEach((dropdownContainer: HTMLDivElement) => { const dropdownInstance = new DropdownSingleInput(dropdownContainer); dropdownInstance.init(); diff --git a/src/bundle/Resources/public/ts/partials/base_dropdown/base_dropdown.ts b/src/bundle/Resources/public/ts/partials/base_dropdown/base_dropdown.ts index 1d9ee6fc..4e263db0 100644 --- a/src/bundle/Resources/public/ts/partials/base_dropdown/base_dropdown.ts +++ b/src/bundle/Resources/public/ts/partials/base_dropdown/base_dropdown.ts @@ -112,7 +112,17 @@ export abstract class BaseDropdown extends Base { listItem.dataset.id = item.id; listItem.dataset.label = item.label; - listItem.textContent = this.getItemContent(item); + + const itemContent = this.getItemContent(item, listItem); + + if (itemContent instanceof NodeList) { + listItem.innerHTML = ''; + Array.from(itemContent).forEach((childNode) => { + listItem.appendChild(childNode); + }); + } else { + listItem.textContent = itemContent; + } this._itemsNode.appendChild(listItem); }); @@ -132,15 +142,12 @@ export abstract class BaseDropdown extends Base { } } - protected abstract setSourceValue(id: string): void; - - protected abstract setSelectedItem(id: string): void; - protected abstract setSource(): void; /******* Items management ********/ - public getItemContent(item: BaseDropdownItem) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public getItemContent(item: BaseDropdownItem, _listItem: HTMLLIElement): NodeListOf | string { return item.label; } diff --git a/src/bundle/Resources/public/ts/utils/dom.ts b/src/bundle/Resources/public/ts/utils/dom.ts new file mode 100644 index 00000000..82161d72 --- /dev/null +++ b/src/bundle/Resources/public/ts/utils/dom.ts @@ -0,0 +1,16 @@ +export const createNodesFromTemplate = (template: string, placeholders: Record): NodeListOf | null => { + const container = document.createElement('div'); + let result = template; + + Object.entries(placeholders).forEach(([placeholder, value]) => { + result = result.replaceAll(placeholder, value); + }); + + container.innerHTML = result; + + if (container instanceof HTMLElement && container.childNodes.length > 0) { + return container.childNodes; + } + + return null; +}; diff --git a/src/bundle/Resources/views/themes/standard/design_system/components/dropdown_multi/input.html.twig b/src/bundle/Resources/views/themes/standard/design_system/components/dropdown_multi/input.html.twig new file mode 100644 index 00000000..748f0c23 --- /dev/null +++ b/src/bundle/Resources/views/themes/standard/design_system/components/dropdown_multi/input.html.twig @@ -0,0 +1,25 @@ +{% extends '@IbexaDesignSystemTwig/themes/standard/design_system/partials/base_dropdown.html.twig' %} + +{% set class = html_classes('ids-dropdown--multi', attributes.render('class') ?? '') %} + +{% block source %} + +{% endblock source %} + +{% block item_content %} + + {{ item.label }} +{% endblock item_content %} + +{% block selected_items %} + {{ selected_items|map(item => item.label)|join(', ') }} +{% endblock selected_items %} diff --git a/src/bundle/Resources/views/themes/standard/design_system/partials/base_dropdown.html.twig b/src/bundle/Resources/views/themes/standard/design_system/partials/base_dropdown.html.twig index f7866625..0c1de990 100644 --- a/src/bundle/Resources/views/themes/standard/design_system/partials/base_dropdown.html.twig +++ b/src/bundle/Resources/views/themes/standard/design_system/partials/base_dropdown.html.twig @@ -68,7 +68,9 @@ {% block templates %} {% set item_content %} - {{ block('item_content') is defined ? block('item_content') : '' }} + {% with { item: item_template_props } %} + {{ block('item_content') is defined ? block('item_content') : '' }} + {% endwith %} {% endset %}