Skip to content

Commit 030b50d

Browse files
committed
IBX-10302: Add "Suggest Taxonomy Entries" Action Type (UI)
1 parent 0879a75 commit 030b50d

File tree

7 files changed

+297
-48
lines changed

7 files changed

+297
-48
lines changed

src/bundle/Resources/encore/ibexa.js.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const layout = [
1212
path.resolve(__dirname, '../public/js/scripts/core/base.chart.js'),
1313
path.resolve(__dirname, '../public/js/scripts/core/line.chart.js'),
1414
path.resolve(__dirname, '../public/js/scripts/core/multilevel.popup.menu.js'),
15+
path.resolve(__dirname, '../public/js/scripts/core/multistep.selector.js'),
1516
path.resolve(__dirname, '../public/js/scripts/core/split.btn.js'),
1617
path.resolve(__dirname, '../public/js/scripts/core/pie.chart.js'),
1718
path.resolve(__dirname, '../public/js/scripts/core/bar.chart.js'),
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
(function (global, doc, ibexa) {
2+
class StepSelector {
3+
constructor(container, apiUrl) {
4+
this.container = container;
5+
this.apiUrl = apiUrl;
6+
this.dropdownInitialContainer = this.container.querySelector('.ibexa-multistep-selector__dropdown-initial');
7+
this.dropdownContainer = this.container.querySelector('.ibexa-multistep-selector__dropdown');
8+
this.dropdownTemplate = this.container.querySelector('template');
9+
this.filledTemplate = null;
10+
11+
this.createDropdown = this.createDropdown.bind(this);
12+
this.loadData = this.loadData.bind(this);
13+
}
14+
15+
fillSourceOptions(options) {
16+
const { escapeHTML } = ibexa.helpers.text;
17+
const sourceInput = this.filledTemplate.querySelector('.ibexa-dropdown__source .ibexa-input');
18+
19+
options.forEach(({ id, name }) => {
20+
const nameHtmlEscaped = escapeHTML(name);
21+
const idHtmlEscaped = escapeHTML(id);
22+
const optionNode = doc.createElement('option');
23+
24+
optionNode.value = idHtmlEscaped;
25+
optionNode.textContent = nameHtmlEscaped;
26+
27+
sourceInput.appendChild(optionNode);
28+
});
29+
}
30+
31+
fillListOptions(options) {
32+
const { escapeHTML } = ibexa.helpers.text;
33+
const { dangerouslyInsertAdjacentHTML } = ibexa.helpers.dom;
34+
const itemsList = this.filledTemplate.querySelector('.ibexa-dropdown__items-list');
35+
const itemsListFragment = doc.createDocumentFragment();
36+
const { template: itemTemplate } = itemsList.dataset;
37+
38+
options.forEach(({ id, name }) => {
39+
const nameHtmlEscaped = escapeHTML(name);
40+
const idHtmlEscaped = escapeHTML(id);
41+
const itemsContainer = doc.createElement('ul');
42+
const itemRendered = itemTemplate.replace('{{ value }}', idHtmlEscaped).replaceAll('{{ label }}', nameHtmlEscaped);
43+
44+
dangerouslyInsertAdjacentHTML(itemsContainer, 'beforeend', itemRendered);
45+
itemsListFragment.append(itemsContainer.querySelector('li'));
46+
});
47+
48+
itemsList.append(itemsListFragment);
49+
}
50+
51+
createDropdown(options = []) {
52+
this.filledTemplate = this.dropdownTemplate.content.cloneNode(true);
53+
54+
this.fillSourceOptions(options);
55+
this.fillListOptions(options);
56+
this.toggleDropdown(true);
57+
58+
this.dropdownContainer.innerHTML = '';
59+
this.dropdownContainer.appendChild(this.filledTemplate);
60+
this.filledTemplate = null;
61+
this.dropdownInstance = new ibexa.core.Dropdown({
62+
container: this.dropdownContainer.querySelector('.ibexa-dropdown'),
63+
});
64+
65+
this.dropdownInstance.init();
66+
this.bindOnChangeListener();
67+
}
68+
69+
toggleDropdown(showFinal) {
70+
const initialDropdown = this.container.querySelector('.ibexa-multistep-selector__dropdown-initial');
71+
const finalDropdown = this.container.querySelector('.ibexa-multistep-selector__dropdown');
72+
73+
if (showFinal) {
74+
initialDropdown.setAttribute('hidden', true);
75+
finalDropdown.removeAttribute('hidden');
76+
} else {
77+
finalDropdown.setAttribute('hidden', true);
78+
initialDropdown.removeAttribute('hidden');
79+
}
80+
}
81+
82+
toggleLoader(showLoader) {
83+
const placeholder = this.dropdownInitialContainer.querySelector('.ibexa-dropdown__selected-placeholder');
84+
const loader = this.dropdownInitialContainer.querySelector('.ibexa-dropdown__loader-wrapper');
85+
86+
if (showLoader) {
87+
placeholder.setAttribute('hidden', true);
88+
loader.removeAttribute('hidden');
89+
} else {
90+
loader.setAttribute('hidden', true);
91+
placeholder.removeAttribute('hidden');
92+
}
93+
}
94+
95+
loadData(requestPromise) {
96+
this.toggleDropdown(false);
97+
this.toggleLoader(true);
98+
99+
requestPromise().then((response) => {
100+
this.toggleLoader(false);
101+
this.createDropdown(response);
102+
});
103+
}
104+
105+
addOnChangeListener(callback) {
106+
this.bindOnChangeListener = () => {
107+
this.dropdownInstance.sourceInput.addEventListener('change', (event) => {
108+
const selectedValues = [...event.target.selectedOptions].map((option) => option.value);
109+
callback({ selectedValues });
110+
});
111+
};
112+
}
113+
114+
reset() {
115+
this.toggleDropdown(false);
116+
this.toggleLoader(false);
117+
}
118+
119+
init() {}
120+
}
121+
122+
class MultistepSelector {
123+
constructor(container, steps) {
124+
this.container = container;
125+
126+
this.steps = steps.map((step) => {
127+
const stepContainer = this.container.querySelector(`.ibexa-multistep-selector__step[data-step-id="${step.id}"]`);
128+
129+
return {
130+
...step,
131+
instance: new StepSelector(stepContainer),
132+
};
133+
});
134+
}
135+
136+
init() {
137+
this.steps.forEach((step, key) => {
138+
const nextStep = this.steps[key + 1];
139+
const futureSteps = this.steps.slice(key + 2);
140+
141+
step.instance.init();
142+
143+
step.instance.addOnChangeListener((params) => {
144+
if (nextStep) {
145+
nextStep.instance.loadData(() => nextStep.loadData(params));
146+
}
147+
148+
futureSteps.forEach((futureStep) => futureStep.instance.reset());
149+
});
150+
});
151+
152+
if (this.steps[0]) {
153+
this.steps[0].instance.loadData(() => this.steps[0].loadData());
154+
}
155+
}
156+
}
157+
158+
ibexa.addConfig('core.MultistepSelector', MultistepSelector);
159+
})(window, window.document, window.ibexa);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.ibexa-multistep-selector {
2+
margin-top: calculateRem(24px);
3+
4+
.ibexa-alert {
5+
margin: calculateRem(24px) 0;
6+
}
7+
8+
.ibexa-dropdown {
9+
&__loader-wrapper {
10+
display: flex;
11+
justify-content: center;
12+
width: 100%;
13+
}
14+
15+
&__loader {
16+
@include spinner(calculateRem(24px), calculateRem(3px), $ibexa-color-primary);
17+
}
18+
}
19+
20+
&__steps {
21+
display: flex;
22+
gap: calculateRem(16px);
23+
}
24+
25+
&__step {
26+
flex-grow: 1;
27+
}
28+
29+
& + & {
30+
margin-top: calculateRem(40px);
31+
}
32+
}

src/bundle/Resources/public/scss/ibexa.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,4 @@
135135
@import 'additional-actions';
136136
@import 'user-mode-badge';
137137
@import 'taggify';
138+
@import 'multistep-selector';

src/bundle/Resources/views/themes/admin/ui/component/dropdown/dropdown.html.twig

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -66,59 +66,61 @@
6666
})|e('html_attr') }}"
6767
data-placeholder-template="{{ placeholder_list_item|e('html_attr') }}"
6868
>
69-
{% if no_items %}
70-
{% if not is_dynamic %}
71-
{{ placeholder_list_item }}
72-
{% endif %}
73-
{% else %}
74-
{% if value is empty %}
75-
{% if not multiple %}
76-
{% if placeholder is defined and placeholder is not none %}
77-
{% set default_label = 'dropdown.placeholder.all'|trans()|desc('All') %}
78-
79-
{% include selected_item_template_path with {
80-
value: '',
81-
label: _self.get_translated_label(placeholder, translation_domain)|trim|default(default_label),
82-
} %}
83-
{% else %}
84-
{% set first_choice = choices_flat|first %}
85-
86-
{% include selected_item_template_path with {
87-
value: first_choice.value,
88-
label: _self.get_translated_label(first_choice.label, translation_domain),
89-
icon: first_choice.icon is defined ? first_choice.icon,
90-
} %}
91-
{% endif %}
69+
{% block selection_info_content %}
70+
{% if no_items %}
71+
{% if not is_dynamic %}
72+
{{ placeholder_list_item }}
9273
{% endif %}
9374
{% else %}
94-
{% for choice in choices_flat %}
95-
{% if custom_form ? choice.value == value : choice is selectedchoice(value) %}
96-
{% set label = selected_item_label is defined
97-
? selected_item_label
98-
: _self.get_translated_label(choice.label, translation_domain)
99-
%}
75+
{% if value is empty %}
76+
{% if not multiple %}
77+
{% if placeholder is defined and placeholder is not none %}
78+
{% set default_label = 'dropdown.placeholder.all'|trans()|desc('All') %}
10079

101-
{% include selected_item_template_path with {
102-
label,
103-
value: choice.value,
104-
icon: choice.icon is defined ? choice.icon,
105-
} %}
106-
{% endif %}
107-
{% endfor %}
108-
{% endif %}
109-
{% if multiple %}
110-
<li
111-
class="ibexa-dropdown__selected-item ibexa-dropdown__selected-item--predefined ibexa-dropdown__selected-placeholder"
112-
{% if value is empty %}hidden{% endif %}
113-
>
114-
{% if placeholder is defined and placeholder is not none %}
115-
{{ _self.get_translated_label(placeholder, translation_domain )}}
116-
{% else %}
117-
{{ 'dropdown.placeholder'|trans|desc("Choose an option") }}
80+
{% include selected_item_template_path with {
81+
value: '',
82+
label: _self.get_translated_label(placeholder, translation_domain)|trim|default(default_label),
83+
} %}
84+
{% else %}
85+
{% set first_choice = choices_flat|first %}
86+
87+
{% include selected_item_template_path with {
88+
value: first_choice.value,
89+
label: _self.get_translated_label(first_choice.label, translation_domain),
90+
icon: first_choice.icon is defined ? first_choice.icon,
91+
} %}
92+
{% endif %}
11893
{% endif %}
119-
</li>
94+
{% else %}
95+
{% for choice in choices_flat %}
96+
{% if custom_form ? choice.value == value : choice is selectedchoice(value) %}
97+
{% set label = selected_item_label is defined
98+
? selected_item_label
99+
: _self.get_translated_label(choice.label, translation_domain)
100+
%}
101+
102+
{% include selected_item_template_path with {
103+
label,
104+
value: choice.value,
105+
icon: choice.icon is defined ? choice.icon,
106+
} %}
107+
{% endif %}
108+
{% endfor %}
109+
{% endif %}
110+
{% if multiple %}
111+
<li
112+
class="ibexa-dropdown__selected-item ibexa-dropdown__selected-item--predefined ibexa-dropdown__selected-placeholder"
113+
{% if value is empty %}hidden{% endif %}
114+
>
115+
{% if placeholder is defined and placeholder is not none %}
116+
{{ _self.get_translated_label(placeholder, translation_domain )}}
117+
{% else %}
118+
{{ 'dropdown.placeholder'|trans|desc("Choose an option") }}
119+
{% endif %}
120+
</li>
121+
{% endif %}
120122
{% endif %}
121-
{% endif %}
123+
{% endblock selection_info_content %}
122124

123125
<li class="ibexa-dropdown__selected-item ibexa-dropdown__selected-item--predefined ibexa-dropdown__selected-overflow-number" hidden></li>
124126
</ul>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{% set source %}
2+
<select name="selector-{{ id }}" class="ibexa-input ibexa-input--select" multiple>
3+
</select>
4+
{% endset %}
5+
<div class="ibexa-multistep-selector__step ibexa-multistep-selector__step--{{ id }}" data-step-id="{{ id }}">
6+
<template class="ibexa-multistep-selector__dropdown-template">
7+
{% include '@ibexadesign/ui/component/dropdown/dropdown.html.twig' with {
8+
source,
9+
choices: [],
10+
is_dynamic: true,
11+
multiple: true,
12+
class: 'ibexa-dropdown--' ~ id,
13+
} only %}
14+
</template>
15+
<label class="ibexa-label form-label">
16+
{{ label }}
17+
</label>
18+
<div class="ibexa-multistep-selector__dropdown-initial">
19+
{% embed '@ibexadesign/ui/component/dropdown/dropdown.html.twig' with {
20+
source,
21+
choices: [],
22+
is_disabled: true,
23+
class: 'ibexa-dropdown--' ~ id,
24+
} %}
25+
{% block selection_info_content %}
26+
<li class="ibexa-dropdown__selected-item ibexa-dropdown__selected-item--predefined ibexa-dropdown__selected-placeholder">
27+
{{ 'multistep_selector.step.dropdown.placeholder'|trans|desc('Select') }}
28+
</li>
29+
<li class="ibexa-dropdown__selected-item ibexa-dropdown__loader-wrapper" hidden>
30+
<div class="ibexa-dropdown__loader"></div>
31+
</li>
32+
{% endblock %}
33+
{% endembed %}
34+
</div>
35+
<div class="ibexa-multistep-selector__dropdown">
36+
</div>
37+
</div>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="ibexa-multistep-selector ibexa-multistep-selector--{{ id }}">
2+
<h3>{{ title }}</h3>
3+
4+
{% include '@ibexadesign/ui/component/alert/alert.html.twig' with {
5+
type: 'info',
6+
title: info,
7+
} only %}
8+
9+
<div class="ibexa-multistep-selector__steps">
10+
{% for step in steps %}
11+
{% include '@ibexadesign/ui/component/multistep_selector/step_selector.html.twig' with {
12+
id: step.id,
13+
label: step.label,
14+
} %}
15+
{% endfor %}
16+
</div>
17+
</div>

0 commit comments

Comments
 (0)