diff --git a/src/bundle/Resources/encore/ibexa.js.config.js b/src/bundle/Resources/encore/ibexa.js.config.js index c7c09b3e1f..010fbc316e 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 6ed7168765..42955c4c1e 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 @@ -66,59 +66,61 @@ })|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, - } %} - {% 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") }} + {% 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 %} -
  • + {% 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 %} - {% 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