Skip to content

Commit 3643243

Browse files
committed
IBX-10302: Added multistep selector and fields selector
1 parent 46aa074 commit 3643243

File tree

7 files changed

+454
-26
lines changed

7 files changed

+454
-26
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
(function (global, doc, ibexa, Translator) {
2+
const token = doc.querySelector('meta[name="CSRF-Token"]').content;
3+
const siteaccess = doc.querySelector('meta[name="SiteAccess"]').content;
4+
const currentLanguageCode = ibexa.adminUiConfig.languages.priority[0];
5+
const fieldsSelectorNodes = doc.querySelectorAll('.ibexa-fields-selector');
6+
const DROPDOWN_ROUTINE = {
7+
CHANGE_ANY_ITEM: 'changeAnyItem',
8+
CHANGE_ITEM: 'changeItem',
9+
};
10+
const ANY_ITEM = {
11+
id: '*',
12+
name: Translator.trans(/*@Desc("Any")*/ 'ibexa.fields_selector.any_item', {}, 'forms'),
13+
};
14+
let contentTypeGroups = [];
15+
const getNameForContentType = (names) => {
16+
const currentLanguageNames = names.value.find(({ _languageCode }) => _languageCode === currentLanguageCode);
17+
18+
return currentLanguageNames ? currentLanguageNames['#text'] : '';
19+
};
20+
const getGroupPattern = (items) => {
21+
if (items[0] === '*') {
22+
return '*';
23+
}
24+
25+
if (items.length === 1) {
26+
return items[0];
27+
}
28+
29+
return `{${items.join(',')}}`;
30+
};
31+
const parsePattern = (pattern) => {
32+
const output = pattern.split('/').map((part) => {
33+
if (part === '') {
34+
return null;
35+
}
36+
37+
if (part.startsWith('{') && part.endsWith('}')) {
38+
const trimmedPart = part.slice(1, -1);
39+
40+
return trimmedPart.split(',');
41+
}
42+
43+
return [part];
44+
});
45+
46+
return output.filter(Boolean);
47+
};
48+
const loadContentTypeGroups = () => {
49+
const request = new Request('/api/ibexa/v2/content/typegroups', {
50+
method: 'GET',
51+
mode: 'same-origin',
52+
credentials: 'same-origin',
53+
headers: {
54+
Accept: 'application/json',
55+
},
56+
});
57+
58+
return fetch(request)
59+
.then(ibexa.helpers.request.getJsonFromResponse)
60+
.then((response) => {
61+
contentTypeGroups = response.ContentTypeGroupList.ContentTypeGroup.map(({ id, identifier }) => ({
62+
id,
63+
name: identifier,
64+
}));
65+
66+
contentTypeGroups.unshift(ANY_ITEM);
67+
68+
return contentTypeGroups;
69+
});
70+
};
71+
const loadContentTypes = (params) => {
72+
const bodyRequest = {
73+
ViewInput: {
74+
identifier: 'ContentTypeView',
75+
ContentTypeQuery: {
76+
Query: {},
77+
},
78+
},
79+
};
80+
81+
if (params.selectedValues.length === 0) {
82+
return Promise.resolve([]);
83+
}
84+
85+
if (params.selectedValues[0] !== '*') {
86+
bodyRequest.ViewInput.ContentTypeQuery.Query.ContentTypeGroupIdCriterion = params.selectedValues;
87+
}
88+
89+
const request = new Request('/api/ibexa/v2/content/types/view', {
90+
method: 'POST',
91+
mode: 'same-origin',
92+
credentials: 'same-origin',
93+
headers: {
94+
Accept: 'application/vnd.ibexa.api.ContentTypeView+json',
95+
'Content-Type': 'application/vnd.ibexa.api.ContentTypeViewInput+json',
96+
'X-Siteaccess': siteaccess,
97+
'X-CSRF-Token': token,
98+
},
99+
body: JSON.stringify(bodyRequest),
100+
});
101+
102+
return fetch(request)
103+
.then(ibexa.helpers.request.getJsonFromResponse)
104+
.then((response) => {
105+
const contentTypes = response.ContentTypeList.ContentType.map(({ identifier, names }) => ({
106+
id: identifier,
107+
name: getNameForContentType(names),
108+
}));
109+
110+
contentTypes.unshift(ANY_ITEM);
111+
112+
return contentTypes;
113+
});
114+
};
115+
const loadFields = (params) => {
116+
if (params.selectedValues.length === 0) {
117+
return Promise.resolve([]);
118+
}
119+
120+
return Promise.resolve([
121+
ANY_ITEM,
122+
{ id: 'name', name: 'Name' },
123+
{ id: 'description', name: 'Description' },
124+
{ id: 'price', name: 'Price' },
125+
]);
126+
};
127+
128+
class DropdownWithAllItem extends ibexa.core.Dropdown {
129+
getAllItems() {
130+
return [...this.itemsListContainer.querySelectorAll('.ibexa-dropdown__item')].slice(1);
131+
}
132+
133+
isAnyItem(element) {
134+
const value = this.getValueFromElement(element, false);
135+
136+
return value === ANY_ITEM.id;
137+
}
138+
139+
fitItems() {
140+
if (this.dropdownRoutine === DROPDOWN_ROUTINE.CHANGE_ITEM) {
141+
return;
142+
}
143+
144+
super.fitItems();
145+
}
146+
147+
onSelectSetSelectionInfoState(element, ...restArgs) {
148+
if (this.isAnyItem(element)) {
149+
return;
150+
}
151+
152+
super.onSelectSetSelectionInfoState(element, ...restArgs);
153+
}
154+
155+
onSelectSetCurrentSelectedValueState(element, selected, ...restArgs) {
156+
if (this.dropdownRoutine === DROPDOWN_ROUTINE.CHANGE_ITEM) {
157+
return;
158+
}
159+
160+
super.onSelectSetCurrentSelectedValueState(element, selected, ...restArgs);
161+
}
162+
163+
getNumberOfSelectedItems() {
164+
let numberOfSelectedItems = this.getSelectedItems().length;
165+
166+
if (this.getSelectedItems()[0]?.value === ANY_ITEM.id) {
167+
numberOfSelectedItems -= 1;
168+
}
169+
170+
return numberOfSelectedItems;
171+
}
172+
173+
updateAnyItemState() {
174+
const anyItemElement = this.itemsListContainer.querySelector(`.ibexa-dropdown__item[data-value="${ANY_ITEM.id}"]`);
175+
const anyItemLabel = anyItemElement.querySelector('.ibexa-dropdown__item-label');
176+
const anyItemCheckbox = anyItemElement.querySelector('.ibexa-input--checkbox');
177+
const numberOfSelectedItems = this.getNumberOfSelectedItems();
178+
179+
this.dropdownRoutine = DROPDOWN_ROUTINE.CHANGE_ANY_ITEM;
180+
181+
if (numberOfSelectedItems === 0) {
182+
anyItemLabel.textContent = ANY_ITEM.name;
183+
} else {
184+
anyItemLabel.textContent = Translator.trans(
185+
/*@Desc("Any (%count% selected)")*/ 'ibexa.fields_selector.any_item_selected',
186+
{
187+
count: numberOfSelectedItems,
188+
},
189+
'forms',
190+
);
191+
}
192+
193+
if (numberOfSelectedItems === 0) {
194+
anyItemCheckbox.indeterminate = false;
195+
} else if (numberOfSelectedItems === this.getAllItems().length) {
196+
anyItemCheckbox.indeterminate = false;
197+
this.onSelect(anyItemElement, true);
198+
} else {
199+
this.onSelect(anyItemElement, false);
200+
anyItemCheckbox.indeterminate = true;
201+
}
202+
203+
this.dropdownRoutine = null;
204+
}
205+
206+
deselectOption(...args) {
207+
super.deselectOption(...args);
208+
209+
this.updateAnyItemState();
210+
}
211+
212+
onSelect(element, selected, ...restArgs) {
213+
super.onSelect(element, selected, ...restArgs);
214+
215+
if (this.isAnyItem(element)) {
216+
if (this.dropdownRoutine === DROPDOWN_ROUTINE.CHANGE_ANY_ITEM) {
217+
return;
218+
}
219+
220+
const allItems = this.getAllItems();
221+
222+
this.dropdownRoutine = DROPDOWN_ROUTINE.CHANGE_ITEM;
223+
224+
allItems.forEach((item) => {
225+
const value = this.getValueFromElement(item);
226+
const { selected: itemSelected } = this.sourceInput.querySelector(`[value=${value}]`);
227+
228+
if (selected && !itemSelected) {
229+
this.onSelect(item, true, ...restArgs);
230+
} else if (!selected && itemSelected) {
231+
this.onSelect(item, false, ...restArgs);
232+
}
233+
});
234+
235+
this.dropdownRoutine = null;
236+
this.fitItems();
237+
this.fireValueChangedEvent();
238+
239+
return;
240+
}
241+
242+
this.updateAnyItemState();
243+
}
244+
}
245+
246+
const initFieldsSelector = (node) => {
247+
const sourceInput = node.querySelector('.ibexa-multistep-selector__source .ibexa-input');
248+
const multistepSelectorInstance = new ibexa.core.MultistepSelector(
249+
node,
250+
[
251+
{
252+
id: 'ct-group',
253+
loadData: loadContentTypeGroups,
254+
},
255+
{
256+
id: 'ct',
257+
loadData: loadContentTypes,
258+
},
259+
{
260+
id: 'field',
261+
loadData: loadFields,
262+
},
263+
],
264+
{
265+
customDropdown: DropdownWithAllItem,
266+
initialValue: parsePattern(sourceInput.value),
267+
callback: (values) => {
268+
const output = values.map((item) => getGroupPattern(item)).join('/');
269+
270+
sourceInput.value = output;
271+
},
272+
},
273+
);
274+
275+
multistepSelectorInstance.init();
276+
};
277+
278+
fieldsSelectorNodes.forEach((node) => {
279+
initFieldsSelector(node);
280+
});
281+
})(window, window.document, window.ibexa, window.Translator);

src/bundle/Resources/public/js/scripts/core/dropdown.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,25 +182,37 @@
182182
return this.onSelect(optionToSelect, true);
183183
}
184184

185-
onSelect(element, selected) {
186-
const { choiceIcon } = element.dataset;
187-
const value = JSON.stringify(String(element.dataset.value));
185+
getValueFromElement(element, isJSONValue = true) {
186+
const value = String(element.dataset.value);
188187

189-
if (this.canSelectOnlyOne && selected) {
190-
this.hideOptions();
191-
this.clearCurrentSelection(false);
188+
if (!isJSONValue) {
189+
return value;
192190
}
193191

192+
return JSON.stringify(String(element.dataset.value));
193+
}
194+
195+
onSelectSetSourceInputState(element, selected) {
196+
const value = this.getValueFromElement(element);
197+
194198
if (value) {
195199
this.sourceInput.querySelector(`[value=${value}]`).selected = selected;
200+
}
201+
}
196202

197-
if (!this.canSelectOnlyOne) {
198-
element.querySelector('.ibexa-input').checked = selected;
199-
}
203+
onSelectSetItemsListState(element, selected) {
204+
const value = this.getValueFromElement(element);
205+
206+
if (value && !this.canSelectOnlyOne) {
207+
element.querySelector('.ibexa-input').checked = selected;
200208
}
201209

202210
this.itemsListContainer.querySelector(`[data-value=${value}]`).classList.toggle('ibexa-dropdown__item--selected', selected);
211+
}
203212

213+
onSelectSetSelectionInfoState(element, selected) {
214+
const { choiceIcon } = element.dataset;
215+
const value = this.getValueFromElement(element);
204216
const selectedItemsList = this.container.querySelector('.ibexa-dropdown__selection-info');
205217

206218
if (selected) {
@@ -218,6 +230,10 @@
218230
}
219231

220232
this.fitItems();
233+
}
234+
235+
onSelectSetCurrentSelectedValueState(element) {
236+
const value = this.getValueFromElement(element);
221237

222238
if (this.currentSelectedValue !== value || !this.canSelectOnlyOne) {
223239
this.fireValueChangedEvent();
@@ -226,6 +242,18 @@
226242
}
227243
}
228244

245+
onSelect(element, selected) {
246+
if (this.canSelectOnlyOne && selected) {
247+
this.hideOptions();
248+
this.clearCurrentSelection(false);
249+
}
250+
251+
this.onSelectSetSourceInputState(element, selected);
252+
this.onSelectSetItemsListState(element, selected);
253+
this.onSelectSetSelectionInfoState(element, selected);
254+
this.onSelectSetCurrentSelectedValueState(element, selected);
255+
}
256+
229257
onInteractionOutside(event) {
230258
if (this.itemsPopover.tip.contains(event.target)) {
231259
return;
@@ -293,6 +321,13 @@
293321
this.fireValueChangedEvent();
294322
}
295323

324+
deselectOptionByValue(value) {
325+
const stringifiedValue = JSON.stringify(String(value));
326+
const optionToDeselect = this.itemsListContainer.querySelector(`.ibexa-dropdown__item[data-value=${stringifiedValue}]`);
327+
328+
return this.deselectOption(optionToDeselect);
329+
}
330+
296331
fitItems() {
297332
if (this.canSelectOnlyOne) {
298333
return;
@@ -526,7 +561,7 @@
526561
);
527562
this.itemsPopover._element.removeAttribute('title');
528563

529-
if (this.isDynamic) {
564+
if (this.isDynamic && this.canSelectOnlyOne) {
530565
this.selectFirstOption();
531566
}
532567

0 commit comments

Comments
 (0)