Skip to content

Commit 449d048

Browse files
committed
bug #1825 [Autocomplete] Fix grouped options order (zavarock, smnandre)
This PR was merged into the 2.x branch. Discussion ---------- [Autocomplete] Fix grouped options order | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Issues | Fix #1616 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT The bug is being caused by TomSelect not preserving the order of the option elements in the select as we select the options in the dropdown. In this case, the MutationObserver callback uses the optgroup element as a parameter to "store" the group (if any) to which the option belongs. However, once the option is selected, it no longer has an optgroup as its parentElement (a problem caused by the aforementioned bug). As I see it, there is no need to "store" the option's group since, in any case, all <option> elements usually have a unique [value], and even if not, it will still work as expected. A callback was added for options added through [addOption](https://github.com/orchidjs/tom-select/blob/69180fa9e79060060f1824fbde85537579d0005d/src/tom-select.ts#L1636), but the caveat is that it needs the parameter user_created=true to trigger the 'option_add' event. Commits ------- 4344a3c Yarn cleanup 07b3eeb Build up to date packages 6424bd4 [Autocomplete] Fix grouped options order
2 parents fd7609f + 4344a3c commit 449d048

File tree

2 files changed

+84
-12
lines changed

2 files changed

+84
-12
lines changed

src/Autocomplete/assets/dist/controller.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,9 @@ class default_1 extends Controller {
195195
}
196196
createOptionsDataStructure(selectElement) {
197197
return Array.from(selectElement.options).map((option) => {
198-
const optgroup = option.closest('optgroup');
199198
return {
200199
value: option.value,
201200
text: option.text,
202-
group: optgroup ? optgroup.label : null,
203201
};
204202
});
205203
}
@@ -216,7 +214,7 @@ class default_1 extends Controller {
216214
if (filteredOriginalOptions.length !== filteredNewOptions.length) {
217215
return false;
218216
}
219-
const normalizeOption = (option) => `${option.value}-${option.text}-${option.group}`;
217+
const normalizeOption = (option) => `${option.value}-${option.text}`;
220218
const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
221219
const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
222220
return (originalOptionsSet.size === newOptionsSet.size &&
@@ -250,6 +248,40 @@ _default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _def
250248
this.tomSelect.setTextboxValue('');
251249
},
252250
closeAfterSelect: true,
251+
onOptionAdd: (value, data) => {
252+
let parentElement = this.tomSelect.input;
253+
let optgroupData = null;
254+
const optgroup = data[this.tomSelect.settings.optgroupField];
255+
if (optgroup && this.tomSelect.optgroups) {
256+
optgroupData = this.tomSelect.optgroups[optgroup];
257+
if (optgroupData) {
258+
const optgroupElement = parentElement.querySelector(`optgroup[label="${optgroupData.label}"]`);
259+
if (optgroupElement) {
260+
parentElement = optgroupElement;
261+
}
262+
}
263+
}
264+
const optionElement = document.createElement('option');
265+
optionElement.value = value;
266+
optionElement.text = data[this.tomSelect.settings.labelField];
267+
const optionOrder = data.$order;
268+
let orderedOption = null;
269+
for (const [, tomSelectOption] of Object.entries(this.tomSelect.options)) {
270+
if (tomSelectOption.$order === optionOrder) {
271+
orderedOption = parentElement.querySelector(`:scope > option[value="${tomSelectOption[this.tomSelect.settings.valueField]}"]`);
272+
break;
273+
}
274+
}
275+
if (orderedOption) {
276+
orderedOption.insertAdjacentElement('afterend', optionElement);
277+
}
278+
else if (optionOrder >= 0) {
279+
parentElement.append(optionElement);
280+
}
281+
else {
282+
parentElement.prepend(optionElement);
283+
}
284+
},
253285
};
254286
if (!this.selectElement && !this.urlValue) {
255287
config.shouldLoad = () => false;

src/Autocomplete/assets/src/controller.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export interface AutocompleteConnectOptions {
1717
tomSelect: TomSelect;
1818
options: any;
1919
}
20+
interface OptionDataStructure {
21+
value: string;
22+
text: string;
23+
}
2024

2125
export default class extends Controller {
2226
static values = {
@@ -47,7 +51,7 @@ export default class extends Controller {
4751
private mutationObserver: MutationObserver;
4852
private isObserving = false;
4953
private hasLoadedChoicesPreviously = false;
50-
private originalOptions: Array<{ value: string; text: string; group: string | null }> = [];
54+
private originalOptions: Array<OptionDataStructure> = [];
5155

5256
initialize() {
5357
if (!this.mutationObserver) {
@@ -158,6 +162,47 @@ export default class extends Controller {
158162
this.tomSelect.setTextboxValue('');
159163
},
160164
closeAfterSelect: true,
165+
// fix positioning (in the dropdown) of options added through addOption()
166+
onOptionAdd: (value: string, data: { [key: string]: any }) => {
167+
let parentElement = this.tomSelect.input as Element;
168+
let optgroupData = null;
169+
170+
const optgroup = data[this.tomSelect.settings.optgroupField];
171+
if (optgroup && this.tomSelect.optgroups) {
172+
optgroupData = this.tomSelect.optgroups[optgroup];
173+
if (optgroupData) {
174+
const optgroupElement = parentElement.querySelector(`optgroup[label="${optgroupData.label}"]`);
175+
if (optgroupElement) {
176+
parentElement = optgroupElement;
177+
}
178+
}
179+
}
180+
181+
const optionElement = document.createElement('option');
182+
optionElement.value = value;
183+
optionElement.text = data[this.tomSelect.settings.labelField];
184+
185+
const optionOrder = data.$order;
186+
let orderedOption = null;
187+
188+
for (const [, tomSelectOption] of Object.entries(this.tomSelect.options)) {
189+
if (tomSelectOption.$order === optionOrder) {
190+
orderedOption = parentElement.querySelector(
191+
`:scope > option[value="${tomSelectOption[this.tomSelect.settings.valueField]}"]`
192+
);
193+
194+
break;
195+
}
196+
}
197+
198+
if (orderedOption) {
199+
orderedOption.insertAdjacentElement('afterend', optionElement);
200+
} else if (optionOrder >= 0) {
201+
parentElement.append(optionElement);
202+
} else {
203+
parentElement.prepend(optionElement);
204+
}
205+
},
161206
};
162207

163208
// for non-autocompleting input elements, avoid the "No results" message that always shows
@@ -420,20 +465,16 @@ export default class extends Controller {
420465
}
421466
}
422467

423-
private createOptionsDataStructure(
424-
selectElement: HTMLSelectElement
425-
): Array<{ value: string; text: string; group: string | null }> {
468+
private createOptionsDataStructure(selectElement: HTMLSelectElement): Array<OptionDataStructure> {
426469
return Array.from(selectElement.options).map((option) => {
427-
const optgroup = option.closest('optgroup');
428470
return {
429471
value: option.value,
430472
text: option.text,
431-
group: optgroup ? optgroup.label : null,
432473
};
433474
});
434475
}
435476

436-
private areOptionsEquivalent(newOptions: Array<{ value: string; text: string; group: string | null }>): boolean {
477+
private areOptionsEquivalent(newOptions: Array<OptionDataStructure>): boolean {
437478
// remove the empty option, which is added by TomSelect so may be missing from new options
438479
const filteredOriginalOptions = this.originalOptions.filter((option) => option.value !== '');
439480
const filteredNewOptions = newOptions.filter((option) => option.value !== '');
@@ -453,8 +494,7 @@ export default class extends Controller {
453494
return false;
454495
}
455496

456-
const normalizeOption = (option: { value: string; text: string; group: string | null }) =>
457-
`${option.value}-${option.text}-${option.group}`;
497+
const normalizeOption = (option: OptionDataStructure) => `${option.value}-${option.text}`;
458498
const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
459499
const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
460500

0 commit comments

Comments
 (0)