From 46aa074c2aba3d4aa55bb9f3fdf720d24bf664f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Tue, 16 Sep 2025 13:06:57 +0200 Subject: [PATCH 1/2] IBX-10302: Add "Suggest Taxonomy Entries" Action Type (UI) --- .../Resources/encore/ibexa.js.config.js | 1 + .../js/scripts/core/multistep.selector.js | 159 ++++++++++++++++++ .../public/scss/_multistep-selector.scss | 32 ++++ src/bundle/Resources/public/scss/ibexa.scss | 1 + .../ui/component/dropdown/dropdown.html.twig | 86 ++++++---- .../step_selector.html.twig | 37 ++++ .../multistep_selector/widget.html.twig | 17 ++ 7 files changed, 297 insertions(+), 36 deletions(-) create mode 100644 src/bundle/Resources/public/js/scripts/core/multistep.selector.js create mode 100644 src/bundle/Resources/public/scss/_multistep-selector.scss create mode 100644 src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig create mode 100644 src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/widget.html.twig diff --git a/src/bundle/Resources/encore/ibexa.js.config.js b/src/bundle/Resources/encore/ibexa.js.config.js index 0539b9fe80..68b5f2b562 100644 --- a/src/bundle/Resources/encore/ibexa.js.config.js +++ b/src/bundle/Resources/encore/ibexa.js.config.js @@ -12,6 +12,7 @@ const layout = [ path.resolve(__dirname, '../public/js/scripts/core/base.chart.js'), path.resolve(__dirname, '../public/js/scripts/core/line.chart.js'), path.resolve(__dirname, '../public/js/scripts/core/multilevel.popup.menu.js'), + path.resolve(__dirname, '../public/js/scripts/core/multistep.selector.js'), path.resolve(__dirname, '../public/js/scripts/core/split.btn.js'), path.resolve(__dirname, '../public/js/scripts/core/pie.chart.js'), path.resolve(__dirname, '../public/js/scripts/core/bar.chart.js'), diff --git a/src/bundle/Resources/public/js/scripts/core/multistep.selector.js b/src/bundle/Resources/public/js/scripts/core/multistep.selector.js new file mode 100644 index 0000000000..38003bf20e --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/core/multistep.selector.js @@ -0,0 +1,159 @@ +(function (global, doc, ibexa) { + class StepSelector { + constructor(container, apiUrl) { + this.container = container; + this.apiUrl = apiUrl; + this.dropdownInitialContainer = this.container.querySelector('.ibexa-multistep-selector__dropdown-initial'); + this.dropdownContainer = this.container.querySelector('.ibexa-multistep-selector__dropdown'); + this.dropdownTemplate = this.container.querySelector('template'); + this.filledTemplate = null; + + this.createDropdown = this.createDropdown.bind(this); + this.loadData = this.loadData.bind(this); + } + + fillSourceOptions(options) { + const { escapeHTML } = ibexa.helpers.text; + const sourceInput = this.filledTemplate.querySelector('.ibexa-dropdown__source .ibexa-input'); + + options.forEach(({ id, name }) => { + const nameHtmlEscaped = escapeHTML(name); + const idHtmlEscaped = escapeHTML(id); + const optionNode = doc.createElement('option'); + + optionNode.value = idHtmlEscaped; + optionNode.textContent = nameHtmlEscaped; + + sourceInput.appendChild(optionNode); + }); + } + + fillListOptions(options) { + const { escapeHTML } = ibexa.helpers.text; + const { dangerouslyInsertAdjacentHTML } = ibexa.helpers.dom; + const itemsList = this.filledTemplate.querySelector('.ibexa-dropdown__items-list'); + const itemsListFragment = doc.createDocumentFragment(); + const { template: itemTemplate } = itemsList.dataset; + + options.forEach(({ id, name }) => { + const nameHtmlEscaped = escapeHTML(name); + const idHtmlEscaped = escapeHTML(id); + const itemsContainer = doc.createElement('ul'); + const itemRendered = itemTemplate.replace('{{ value }}', idHtmlEscaped).replaceAll('{{ label }}', nameHtmlEscaped); + + dangerouslyInsertAdjacentHTML(itemsContainer, 'beforeend', itemRendered); + itemsListFragment.append(itemsContainer.querySelector('li')); + }); + + itemsList.append(itemsListFragment); + } + + createDropdown(options = []) { + this.filledTemplate = this.dropdownTemplate.content.cloneNode(true); + + this.fillSourceOptions(options); + this.fillListOptions(options); + this.toggleDropdown(true); + + this.dropdownContainer.innerHTML = ''; + this.dropdownContainer.appendChild(this.filledTemplate); + this.filledTemplate = null; + this.dropdownInstance = new ibexa.core.Dropdown({ + container: this.dropdownContainer.querySelector('.ibexa-dropdown'), + }); + + this.dropdownInstance.init(); + this.bindOnChangeListener(); + } + + toggleDropdown(showFinal) { + const initialDropdown = this.container.querySelector('.ibexa-multistep-selector__dropdown-initial'); + const finalDropdown = this.container.querySelector('.ibexa-multistep-selector__dropdown'); + + if (showFinal) { + initialDropdown.setAttribute('hidden', true); + finalDropdown.removeAttribute('hidden'); + } else { + finalDropdown.setAttribute('hidden', true); + initialDropdown.removeAttribute('hidden'); + } + } + + toggleLoader(showLoader) { + const placeholder = this.dropdownInitialContainer.querySelector('.ibexa-dropdown__selected-placeholder'); + const loader = this.dropdownInitialContainer.querySelector('.ibexa-dropdown__loader-wrapper'); + + if (showLoader) { + placeholder.setAttribute('hidden', true); + loader.removeAttribute('hidden'); + } else { + loader.setAttribute('hidden', true); + placeholder.removeAttribute('hidden'); + } + } + + loadData(requestPromise) { + this.toggleDropdown(false); + this.toggleLoader(true); + + requestPromise().then((response) => { + this.toggleLoader(false); + this.createDropdown(response); + }); + } + + addOnChangeListener(callback) { + this.bindOnChangeListener = () => { + this.dropdownInstance.sourceInput.addEventListener('change', (event) => { + const selectedValues = [...event.target.selectedOptions].map((option) => option.value); + callback({ selectedValues }); + }); + }; + } + + reset() { + this.toggleDropdown(false); + this.toggleLoader(false); + } + + init() {} + } + + class MultistepSelector { + constructor(container, steps) { + this.container = container; + + this.steps = steps.map((step) => { + const stepContainer = this.container.querySelector(`.ibexa-multistep-selector__step[data-step-id="${step.id}"]`); + + return { + ...step, + instance: new StepSelector(stepContainer), + }; + }); + } + + init() { + this.steps.forEach((step, key) => { + const nextStep = this.steps[key + 1]; + const futureSteps = this.steps.slice(key + 2); + + step.instance.init(); + + step.instance.addOnChangeListener((params) => { + if (nextStep) { + nextStep.instance.loadData(() => nextStep.loadData(params)); + } + + futureSteps.forEach((futureStep) => futureStep.instance.reset()); + }); + }); + + if (this.steps[0]) { + this.steps[0].instance.loadData(() => this.steps[0].loadData()); + } + } + } + + ibexa.addConfig('core.MultistepSelector', MultistepSelector); +})(window, window.document, window.ibexa); diff --git a/src/bundle/Resources/public/scss/_multistep-selector.scss b/src/bundle/Resources/public/scss/_multistep-selector.scss new file mode 100644 index 0000000000..814886860b --- /dev/null +++ b/src/bundle/Resources/public/scss/_multistep-selector.scss @@ -0,0 +1,32 @@ +.ibexa-multistep-selector { + margin-top: calculateRem(24px); + + .ibexa-alert { + margin: calculateRem(24px) 0; + } + + .ibexa-dropdown { + &__loader-wrapper { + display: flex; + justify-content: center; + width: 100%; + } + + &__loader { + @include spinner(calculateRem(24px), calculateRem(3px), $ibexa-color-primary); + } + } + + &__steps { + display: flex; + gap: calculateRem(16px); + } + + &__step { + flex-grow: 1; + } + + & + & { + margin-top: calculateRem(40px); + } +} diff --git a/src/bundle/Resources/public/scss/ibexa.scss b/src/bundle/Resources/public/scss/ibexa.scss index 14ec41919c..9b0eb70e95 100644 --- a/src/bundle/Resources/public/scss/ibexa.scss +++ b/src/bundle/Resources/public/scss/ibexa.scss @@ -135,3 +135,4 @@ @import 'additional-actions'; @import 'user-mode-badge'; @import 'taggify'; +@import 'multistep-selector'; diff --git a/src/bundle/Resources/views/themes/admin/ui/component/dropdown/dropdown.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/dropdown/dropdown.html.twig index 82ca32d3f9..a13d52a38e 100644 --- a/src/bundle/Resources/views/themes/admin/ui/component/dropdown/dropdown.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/component/dropdown/dropdown.html.twig @@ -68,45 +68,59 @@ })|e('html_attr') }}" data-placeholder-template="{{ placeholder_list_item|e('html_attr') }}" > - {% if no_items %} - {% if not is_dynamic %} - {{ placeholder_list_item }} - {% endif %} - {% else %} - {% if value is empty %} - {% if not multiple %} - {% if placeholder is defined and placeholder is not none %} - {% set default_label = 'dropdown.placeholder.all'|trans()|desc('All') %} - - {% include selected_item_template_path with { - value: '', - label: _self.get_translated_label(placeholder, translation_domain)|trim|default(default_label), - } %} - {% else %} - {% set first_choice = choices_flat|first %} - - {% include selected_item_template_path with { - value: first_choice.value, - label: _self.get_translated_label(first_choice.label, translation_domain), - icon: first_choice.icon is defined ? first_choice.icon, - } %} - {% endif %} + {% block selection_info_content %} + {% if no_items %} + {% if not is_dynamic %} + {{ placeholder_list_item }} {% endif %} {% else %} - {% for choice in choices_flat %} - {% if custom_form ? choice.value == value : choice is selectedchoice(value) %} - {% set label = selected_item_label is defined - ? selected_item_label - : _self.get_translated_label(choice.label, translation_domain) - %} + {% if value is empty %} + {% if not multiple %} + {% if placeholder is defined and placeholder is not none %} + {% set default_label = 'dropdown.placeholder.all'|trans()|desc('All') %} - {% include selected_item_template_path with { - label, - value: choice.value, - icon: choice.icon is defined ? choice.icon, - } %} + {% include selected_item_template_path with { + value: '', + label: _self.get_translated_label(placeholder, translation_domain)|trim|default(default_label), + } %} + {% else %} + {% set first_choice = choices_flat|first %} + + {% include selected_item_template_path with { + value: first_choice.value, + label: _self.get_translated_label(first_choice.label, translation_domain), + icon: first_choice.icon is defined ? first_choice.icon, + } %} + {% endif %} {% endif %} - {% endfor %} + {% else %} + {% for choice in choices_flat %} + {% if custom_form ? choice.value == value : choice is selectedchoice(value) %} + {% set label = selected_item_label is defined + ? selected_item_label + : _self.get_translated_label(choice.label, translation_domain) + %} + + {% include selected_item_template_path with { + label, + value: choice.value, + icon: choice.icon is defined ? choice.icon, + } %} + {% endif %} + {% endfor %} + {% endif %} + {% if multiple %} +
  • + {% if placeholder is defined and placeholder is not none %} + {{ _self.get_translated_label(placeholder, translation_domain )}} + {% else %} + {{ 'dropdown.placeholder'|trans|desc("Choose an option") }} + {% endif %} +
  • + {% endif %} {% endif %} {% if multiple %}
  • {% endif %} - {% endif %} + {% endblock selection_info_content %} diff --git a/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig new file mode 100644 index 0000000000..9aaf59603b --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig @@ -0,0 +1,37 @@ +{% set source %} + +{% endset %} +
    + + +
    + {% embed '@ibexadesign/ui/component/dropdown/dropdown.html.twig' with { + source, + choices: [], + is_disabled: true, + class: 'ibexa-dropdown--' ~ id, + } %} + {% block selection_info_content %} +
  • + {{ 'multistep_selector.step.dropdown.placeholder'|trans|desc('Select') }} +
  • + + {% endblock %} + {% endembed %} +
    +
    +
    +
    diff --git a/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/widget.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/widget.html.twig new file mode 100644 index 0000000000..b3107f282a --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/widget.html.twig @@ -0,0 +1,17 @@ +
    +

    {{ title }}

    + + {% include '@ibexadesign/ui/component/alert/alert.html.twig' with { + type: 'info', + title: info, + } only %} + +
    + {% for step in steps %} + {% include '@ibexadesign/ui/component/multistep_selector/step_selector.html.twig' with { + id: step.id, + label: step.label, + } %} + {% endfor %} +
    +
    \ No newline at end of file From 3643243cc73388dbb56b84c8b568dac4bc830d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Fri, 7 Nov 2025 13:45:47 +0100 Subject: [PATCH 2/2] IBX-10302: Added multistep selector and fields selector --- .../js/scripts/admin.fields.selector.js | 281 ++++++++++++++++++ .../public/js/scripts/core/dropdown.js | 55 +++- .../js/scripts/core/multistep.selector.js | 55 +++- .../Resources/translations/forms.en.xliff | 30 ++ .../step_selector.html.twig | 2 +- .../multistep_selector/widget.html.twig | 23 +- .../themes/admin/ui/form_fields.html.twig | 34 +++ 7 files changed, 454 insertions(+), 26 deletions(-) create mode 100644 src/bundle/Resources/public/js/scripts/admin.fields.selector.js diff --git a/src/bundle/Resources/public/js/scripts/admin.fields.selector.js b/src/bundle/Resources/public/js/scripts/admin.fields.selector.js new file mode 100644 index 0000000000..861177a535 --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/admin.fields.selector.js @@ -0,0 +1,281 @@ +(function (global, doc, ibexa, Translator) { + const token = doc.querySelector('meta[name="CSRF-Token"]').content; + const siteaccess = doc.querySelector('meta[name="SiteAccess"]').content; + const currentLanguageCode = ibexa.adminUiConfig.languages.priority[0]; + const fieldsSelectorNodes = doc.querySelectorAll('.ibexa-fields-selector'); + const DROPDOWN_ROUTINE = { + CHANGE_ANY_ITEM: 'changeAnyItem', + CHANGE_ITEM: 'changeItem', + }; + const ANY_ITEM = { + id: '*', + name: Translator.trans(/*@Desc("Any")*/ 'ibexa.fields_selector.any_item', {}, 'forms'), + }; + let contentTypeGroups = []; + const getNameForContentType = (names) => { + const currentLanguageNames = names.value.find(({ _languageCode }) => _languageCode === currentLanguageCode); + + return currentLanguageNames ? currentLanguageNames['#text'] : ''; + }; + const getGroupPattern = (items) => { + if (items[0] === '*') { + return '*'; + } + + if (items.length === 1) { + return items[0]; + } + + return `{${items.join(',')}}`; + }; + const parsePattern = (pattern) => { + const output = pattern.split('/').map((part) => { + if (part === '') { + return null; + } + + if (part.startsWith('{') && part.endsWith('}')) { + const trimmedPart = part.slice(1, -1); + + return trimmedPart.split(','); + } + + return [part]; + }); + + return output.filter(Boolean); + }; + const loadContentTypeGroups = () => { + const request = new Request('/api/ibexa/v2/content/typegroups', { + method: 'GET', + mode: 'same-origin', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + }, + }); + + return fetch(request) + .then(ibexa.helpers.request.getJsonFromResponse) + .then((response) => { + contentTypeGroups = response.ContentTypeGroupList.ContentTypeGroup.map(({ id, identifier }) => ({ + id, + name: identifier, + })); + + contentTypeGroups.unshift(ANY_ITEM); + + return contentTypeGroups; + }); + }; + const loadContentTypes = (params) => { + const bodyRequest = { + ViewInput: { + identifier: 'ContentTypeView', + ContentTypeQuery: { + Query: {}, + }, + }, + }; + + if (params.selectedValues.length === 0) { + return Promise.resolve([]); + } + + if (params.selectedValues[0] !== '*') { + bodyRequest.ViewInput.ContentTypeQuery.Query.ContentTypeGroupIdCriterion = params.selectedValues; + } + + const request = new Request('/api/ibexa/v2/content/types/view', { + method: 'POST', + mode: 'same-origin', + credentials: 'same-origin', + headers: { + Accept: 'application/vnd.ibexa.api.ContentTypeView+json', + 'Content-Type': 'application/vnd.ibexa.api.ContentTypeViewInput+json', + 'X-Siteaccess': siteaccess, + 'X-CSRF-Token': token, + }, + body: JSON.stringify(bodyRequest), + }); + + return fetch(request) + .then(ibexa.helpers.request.getJsonFromResponse) + .then((response) => { + const contentTypes = response.ContentTypeList.ContentType.map(({ identifier, names }) => ({ + id: identifier, + name: getNameForContentType(names), + })); + + contentTypes.unshift(ANY_ITEM); + + return contentTypes; + }); + }; + const loadFields = (params) => { + if (params.selectedValues.length === 0) { + return Promise.resolve([]); + } + + return Promise.resolve([ + ANY_ITEM, + { id: 'name', name: 'Name' }, + { id: 'description', name: 'Description' }, + { id: 'price', name: 'Price' }, + ]); + }; + + class DropdownWithAllItem extends ibexa.core.Dropdown { + getAllItems() { + return [...this.itemsListContainer.querySelectorAll('.ibexa-dropdown__item')].slice(1); + } + + isAnyItem(element) { + const value = this.getValueFromElement(element, false); + + return value === ANY_ITEM.id; + } + + fitItems() { + if (this.dropdownRoutine === DROPDOWN_ROUTINE.CHANGE_ITEM) { + return; + } + + super.fitItems(); + } + + onSelectSetSelectionInfoState(element, ...restArgs) { + if (this.isAnyItem(element)) { + return; + } + + super.onSelectSetSelectionInfoState(element, ...restArgs); + } + + onSelectSetCurrentSelectedValueState(element, selected, ...restArgs) { + if (this.dropdownRoutine === DROPDOWN_ROUTINE.CHANGE_ITEM) { + return; + } + + super.onSelectSetCurrentSelectedValueState(element, selected, ...restArgs); + } + + getNumberOfSelectedItems() { + let numberOfSelectedItems = this.getSelectedItems().length; + + if (this.getSelectedItems()[0]?.value === ANY_ITEM.id) { + numberOfSelectedItems -= 1; + } + + return numberOfSelectedItems; + } + + updateAnyItemState() { + const anyItemElement = this.itemsListContainer.querySelector(`.ibexa-dropdown__item[data-value="${ANY_ITEM.id}"]`); + const anyItemLabel = anyItemElement.querySelector('.ibexa-dropdown__item-label'); + const anyItemCheckbox = anyItemElement.querySelector('.ibexa-input--checkbox'); + const numberOfSelectedItems = this.getNumberOfSelectedItems(); + + this.dropdownRoutine = DROPDOWN_ROUTINE.CHANGE_ANY_ITEM; + + if (numberOfSelectedItems === 0) { + anyItemLabel.textContent = ANY_ITEM.name; + } else { + anyItemLabel.textContent = Translator.trans( + /*@Desc("Any (%count% selected)")*/ 'ibexa.fields_selector.any_item_selected', + { + count: numberOfSelectedItems, + }, + 'forms', + ); + } + + if (numberOfSelectedItems === 0) { + anyItemCheckbox.indeterminate = false; + } else if (numberOfSelectedItems === this.getAllItems().length) { + anyItemCheckbox.indeterminate = false; + this.onSelect(anyItemElement, true); + } else { + this.onSelect(anyItemElement, false); + anyItemCheckbox.indeterminate = true; + } + + this.dropdownRoutine = null; + } + + deselectOption(...args) { + super.deselectOption(...args); + + this.updateAnyItemState(); + } + + onSelect(element, selected, ...restArgs) { + super.onSelect(element, selected, ...restArgs); + + if (this.isAnyItem(element)) { + if (this.dropdownRoutine === DROPDOWN_ROUTINE.CHANGE_ANY_ITEM) { + return; + } + + const allItems = this.getAllItems(); + + this.dropdownRoutine = DROPDOWN_ROUTINE.CHANGE_ITEM; + + allItems.forEach((item) => { + const value = this.getValueFromElement(item); + const { selected: itemSelected } = this.sourceInput.querySelector(`[value=${value}]`); + + if (selected && !itemSelected) { + this.onSelect(item, true, ...restArgs); + } else if (!selected && itemSelected) { + this.onSelect(item, false, ...restArgs); + } + }); + + this.dropdownRoutine = null; + this.fitItems(); + this.fireValueChangedEvent(); + + return; + } + + this.updateAnyItemState(); + } + } + + const initFieldsSelector = (node) => { + const sourceInput = node.querySelector('.ibexa-multistep-selector__source .ibexa-input'); + const multistepSelectorInstance = new ibexa.core.MultistepSelector( + node, + [ + { + id: 'ct-group', + loadData: loadContentTypeGroups, + }, + { + id: 'ct', + loadData: loadContentTypes, + }, + { + id: 'field', + loadData: loadFields, + }, + ], + { + customDropdown: DropdownWithAllItem, + initialValue: parsePattern(sourceInput.value), + callback: (values) => { + const output = values.map((item) => getGroupPattern(item)).join('/'); + + sourceInput.value = output; + }, + }, + ); + + multistepSelectorInstance.init(); + }; + + fieldsSelectorNodes.forEach((node) => { + initFieldsSelector(node); + }); +})(window, window.document, window.ibexa, window.Translator); diff --git a/src/bundle/Resources/public/js/scripts/core/dropdown.js b/src/bundle/Resources/public/js/scripts/core/dropdown.js index 1f560176c1..17ea6646bd 100644 --- a/src/bundle/Resources/public/js/scripts/core/dropdown.js +++ b/src/bundle/Resources/public/js/scripts/core/dropdown.js @@ -182,25 +182,37 @@ return this.onSelect(optionToSelect, true); } - onSelect(element, selected) { - const { choiceIcon } = element.dataset; - const value = JSON.stringify(String(element.dataset.value)); + getValueFromElement(element, isJSONValue = true) { + const value = String(element.dataset.value); - if (this.canSelectOnlyOne && selected) { - this.hideOptions(); - this.clearCurrentSelection(false); + if (!isJSONValue) { + return value; } + return JSON.stringify(String(element.dataset.value)); + } + + onSelectSetSourceInputState(element, selected) { + const value = this.getValueFromElement(element); + if (value) { this.sourceInput.querySelector(`[value=${value}]`).selected = selected; + } + } - if (!this.canSelectOnlyOne) { - element.querySelector('.ibexa-input').checked = selected; - } + onSelectSetItemsListState(element, selected) { + const value = this.getValueFromElement(element); + + if (value && !this.canSelectOnlyOne) { + element.querySelector('.ibexa-input').checked = selected; } this.itemsListContainer.querySelector(`[data-value=${value}]`).classList.toggle('ibexa-dropdown__item--selected', selected); + } + onSelectSetSelectionInfoState(element, selected) { + const { choiceIcon } = element.dataset; + const value = this.getValueFromElement(element); const selectedItemsList = this.container.querySelector('.ibexa-dropdown__selection-info'); if (selected) { @@ -218,6 +230,10 @@ } this.fitItems(); + } + + onSelectSetCurrentSelectedValueState(element) { + const value = this.getValueFromElement(element); if (this.currentSelectedValue !== value || !this.canSelectOnlyOne) { this.fireValueChangedEvent(); @@ -226,6 +242,18 @@ } } + onSelect(element, selected) { + if (this.canSelectOnlyOne && selected) { + this.hideOptions(); + this.clearCurrentSelection(false); + } + + this.onSelectSetSourceInputState(element, selected); + this.onSelectSetItemsListState(element, selected); + this.onSelectSetSelectionInfoState(element, selected); + this.onSelectSetCurrentSelectedValueState(element, selected); + } + onInteractionOutside(event) { if (this.itemsPopover.tip.contains(event.target)) { return; @@ -293,6 +321,13 @@ this.fireValueChangedEvent(); } + deselectOptionByValue(value) { + const stringifiedValue = JSON.stringify(String(value)); + const optionToDeselect = this.itemsListContainer.querySelector(`.ibexa-dropdown__item[data-value=${stringifiedValue}]`); + + return this.deselectOption(optionToDeselect); + } + fitItems() { if (this.canSelectOnlyOne) { return; @@ -526,7 +561,7 @@ ); this.itemsPopover._element.removeAttribute('title'); - if (this.isDynamic) { + if (this.isDynamic && this.canSelectOnlyOne) { this.selectFirstOption(); } diff --git a/src/bundle/Resources/public/js/scripts/core/multistep.selector.js b/src/bundle/Resources/public/js/scripts/core/multistep.selector.js index 38003bf20e..58b9db419d 100644 --- a/src/bundle/Resources/public/js/scripts/core/multistep.selector.js +++ b/src/bundle/Resources/public/js/scripts/core/multistep.selector.js @@ -1,12 +1,13 @@ (function (global, doc, ibexa) { class StepSelector { - constructor(container, apiUrl) { + constructor(container, { customDropdown } = {}) { this.container = container; - this.apiUrl = apiUrl; this.dropdownInitialContainer = this.container.querySelector('.ibexa-multistep-selector__dropdown-initial'); this.dropdownContainer = this.container.querySelector('.ibexa-multistep-selector__dropdown'); this.dropdownTemplate = this.container.querySelector('template'); this.filledTemplate = null; + this.value = []; + this.DropdownClass = customDropdown ?? ibexa.core.Dropdown; this.createDropdown = this.createDropdown.bind(this); this.loadData = this.loadData.bind(this); @@ -48,7 +49,7 @@ itemsList.append(itemsListFragment); } - createDropdown(options = []) { + createDropdown(options = [], values = []) { this.filledTemplate = this.dropdownTemplate.content.cloneNode(true); this.fillSourceOptions(options); @@ -58,11 +59,18 @@ this.dropdownContainer.innerHTML = ''; this.dropdownContainer.appendChild(this.filledTemplate); this.filledTemplate = null; - this.dropdownInstance = new ibexa.core.Dropdown({ + this.dropdownInstance = new this.DropdownClass({ container: this.dropdownContainer.querySelector('.ibexa-dropdown'), }); this.dropdownInstance.init(); + + values.forEach((value) => { + const element = this.dropdownInstance.itemsContainer.querySelector(`.ibexa-dropdown__item[data-value="${value}"]`); + + this.dropdownInstance.onSelect(element, true); + }); + this.bindOnChangeListener(); } @@ -92,13 +100,18 @@ } } - loadData(requestPromise) { + loadData(requestPromise, values = []) { + this.reset(); this.toggleDropdown(false); this.toggleLoader(true); requestPromise().then((response) => { + if (response.length === 0) { + return; + } + this.toggleLoader(false); - this.createDropdown(response); + this.createDropdown(response, values); }); } @@ -106,12 +119,15 @@ this.bindOnChangeListener = () => { this.dropdownInstance.sourceInput.addEventListener('change', (event) => { const selectedValues = [...event.target.selectedOptions].map((option) => option.value); + + this.value = selectedValues; callback({ selectedValues }); }); }; } reset() { + this.value = []; this.toggleDropdown(false); this.toggleLoader(false); } @@ -120,15 +136,17 @@ } class MultistepSelector { - constructor(container, steps) { + constructor(container, steps, { customDropdown, initialValue, callback } = {}) { this.container = container; + this.callback = callback; + this.initialValue = initialValue ?? []; this.steps = steps.map((step) => { const stepContainer = this.container.querySelector(`.ibexa-multistep-selector__step[data-step-id="${step.id}"]`); return { ...step, - instance: new StepSelector(stepContainer), + instance: new StepSelector(stepContainer, { customDropdown }), }; }); } @@ -146,12 +164,31 @@ } futureSteps.forEach((futureStep) => futureStep.instance.reset()); + + if (this.callback) { + const output = this.steps.map(({ instance }) => instance.value); + + this.callback(output); + } }); }); if (this.steps[0]) { - this.steps[0].instance.loadData(() => this.steps[0].loadData()); + const firstStepInitialValue = this.initialValue[0] || []; + + this.steps[0].instance.loadData(() => this.steps[0].loadData(), firstStepInitialValue); } + + this.initialValue.forEach((payloadValues, key) => { + const step = this.steps[key + 1]; + const stepValues = this.initialValue[key + 1]; + + if (!step) { + return; + } + + step.instance.loadData(() => step.loadData({ selectedValues: payloadValues }), stepValues); + }); } } diff --git a/src/bundle/Resources/translations/forms.en.xliff b/src/bundle/Resources/translations/forms.en.xliff index 9499b1a36a..0272f6347b 100644 --- a/src/bundle/Resources/translations/forms.en.xliff +++ b/src/bundle/Resources/translations/forms.en.xliff @@ -111,6 +111,31 @@ Asset Fields(s) key: form.trash_assets_non_unique.label + + Any + Any + key: ibexa.fields_selector.any_item + + + Any (%count% selected + Any (%count% selected + key: ibexa.fields_selector.any_item_selected + + + Content type + Content type + key: ibexa.fields_selector.ct + + + Content type group + Content type group + key: ibexa.fields_selector.ct_group + + + Field + Field + key: ibexa.fields_selector.field + Save Save @@ -156,6 +181,11 @@ Send the Content item and its related assets to trash key: location_trash_form.trash_with_asset + + Select + Select + key: multistep_selector.step.dropdown.placeholder + Delete Policies Delete Policies diff --git a/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig index 9aaf59603b..34ed6bf7d2 100644 --- a/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/component/multistep_selector/step_selector.html.twig @@ -24,7 +24,7 @@ } %} {% block selection_info_content %}
  • - {{ 'multistep_selector.step.dropdown.placeholder'|trans|desc('Select') }} + {{ 'multistep_selector.step.dropdown.placeholder'|trans({}, 'forms')|desc('Select') }}