Skip to content

Commit 64aad83

Browse files
committed
feature #1290 [Autocomplete] Attempting a simpler "reset" of items (weaverryan)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Autocomplete] Attempting a simpler "reset" of items | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Issues | Possibly #1290 (need to check and remove data-live-ignore), fixes #1261 possibly #1241 possibly #909 | License | MIT Tomselect is a real pain in the butt. When you select an option, it re-orders the `option` elements and uses the elements themselves internally to remember which one is selected. It plays poorly with LiveComponents, which is why we added the MutationObserver logic. However, at least in one straightforward case that I had today, when you selected a new item, the new value was being lost. This will only get worse when Turbo 8 adds morphing. This PR attempts to simplify: when needed, completely destroy TomSelect and recreate it. It seems to work well, but 2 tests are failing. And I'm pulling my hair out - so it may not even be fixable. No matter what I try, when I reinitialize TomSelect on a set of HTML that has been reordered, it gets confused. It's as if it has a static cache somewhere that I can't find... Commits ------- af20ad8 [Autocomplete] Attempting a simpler "reset" of items
2 parents b73eac8 + af20ad8 commit 64aad83

File tree

9 files changed

+304
-307
lines changed

9 files changed

+304
-307
lines changed

src/Autocomplete/assets/dist/controller.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ export default class extends Controller {
3333
private mutationObserver;
3434
private isObserving;
3535
private hasLoadedChoicesPreviously;
36+
private originalOptions;
3637
initialize(): void;
3738
connect(): void;
39+
initializeTomSelect(): void;
3840
disconnect(): void;
3941
private getMaxOptions;
4042
get selectElement(): HTMLSelectElement | null;
@@ -43,9 +45,9 @@ export default class extends Controller {
4345
get preload(): string | boolean;
4446
private resetTomSelect;
4547
private changeTomSelectDisabledState;
46-
private updateTomSelectPlaceholder;
4748
private startMutationObserver;
4849
private stopMutationObserver;
4950
private onMutations;
50-
private requiresLiveIgnore;
51+
private createOptionsDataStructure;
52+
private areOptionsEquivalent;
5153
}

src/Autocomplete/assets/dist/controller.js

Lines changed: 48 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,25 @@ class default_1 extends Controller {
2929
_default_1_instances.add(this);
3030
this.isObserving = false;
3131
this.hasLoadedChoicesPreviously = false;
32+
this.originalOptions = [];
3233
}
3334
initialize() {
34-
if (this.requiresLiveIgnore()) {
35-
this.element.setAttribute('data-live-ignore', '');
36-
if (this.element.id) {
37-
const label = document.querySelector(`label[for="${this.element.id}"]`);
38-
if (label) {
39-
label.setAttribute('data-live-ignore', '');
40-
}
41-
}
42-
}
43-
else {
44-
if (!this.mutationObserver) {
45-
this.mutationObserver = new MutationObserver((mutations) => {
46-
this.onMutations(mutations);
47-
});
48-
}
35+
if (!this.mutationObserver) {
36+
this.mutationObserver = new MutationObserver((mutations) => {
37+
this.onMutations(mutations);
38+
});
4939
}
5040
}
5141
connect() {
42+
if (this.selectElement) {
43+
this.originalOptions = this.createOptionsDataStructure(this.selectElement);
44+
}
45+
this.initializeTomSelect();
46+
}
47+
initializeTomSelect() {
48+
if (this.selectElement) {
49+
this.selectElement.setAttribute('data-skip-morph', '');
50+
}
5251
if (this.urlValue) {
5352
this.tomSelect = __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_createAutocompleteWithRemoteData).call(this, this.urlValue, this.hasMinCharactersValue ? this.minCharactersValue : null);
5453
return;
@@ -97,9 +96,12 @@ class default_1 extends Controller {
9796
resetTomSelect() {
9897
if (this.tomSelect) {
9998
this.stopMutationObserver();
100-
this.tomSelect.clearOptions();
101-
this.tomSelect.settings.maxOptions = this.getMaxOptions();
102-
this.tomSelect.sync();
99+
const currentHtml = this.element.innerHTML;
100+
const currentValue = this.tomSelect.getValue();
101+
this.tomSelect.destroy();
102+
this.element.innerHTML = currentHtml;
103+
this.initializeTomSelect();
104+
this.tomSelect.setValue(currentValue);
103105
this.startMutationObserver();
104106
}
105107
}
@@ -113,22 +115,6 @@ class default_1 extends Controller {
113115
}
114116
this.startMutationObserver();
115117
}
116-
updateTomSelectPlaceholder() {
117-
const input = this.element;
118-
let placeholder = input.getAttribute('placeholder') || input.getAttribute('data-placeholder');
119-
if (!placeholder && !this.tomSelect.allowEmptyOption) {
120-
const option = input.querySelector('option[value=""]');
121-
if (option) {
122-
placeholder = option.textContent;
123-
}
124-
}
125-
if (placeholder) {
126-
this.stopMutationObserver();
127-
this.tomSelect.settings.placeholder = placeholder;
128-
this.tomSelect.control_input.setAttribute('placeholder', placeholder);
129-
this.startMutationObserver();
130-
}
131-
}
132118
startMutationObserver() {
133119
if (!this.isObserving && this.mutationObserver) {
134120
this.mutationObserver.observe(this.element, {
@@ -147,73 +133,51 @@ class default_1 extends Controller {
147133
}
148134
}
149135
onMutations(mutations) {
150-
const addedOptionElements = [];
151-
const removedOptionElements = [];
152-
let hasAnOptionChanged = false;
153136
let changeDisabledState = false;
154-
let changePlaceholder = false;
137+
let requireReset = false;
155138
mutations.forEach((mutation) => {
156139
switch (mutation.type) {
157-
case 'childList':
158-
if (mutation.target instanceof HTMLOptionElement) {
159-
if (mutation.target.value === '') {
160-
changePlaceholder = true;
161-
break;
162-
}
163-
hasAnOptionChanged = true;
164-
break;
165-
}
166-
mutation.addedNodes.forEach((node) => {
167-
if (node instanceof HTMLOptionElement) {
168-
if (removedOptionElements.includes(node)) {
169-
removedOptionElements.splice(removedOptionElements.indexOf(node), 1);
170-
return;
171-
}
172-
addedOptionElements.push(node);
173-
}
174-
});
175-
mutation.removedNodes.forEach((node) => {
176-
if (node instanceof HTMLOptionElement) {
177-
if (addedOptionElements.includes(node)) {
178-
addedOptionElements.splice(addedOptionElements.indexOf(node), 1);
179-
return;
180-
}
181-
removedOptionElements.push(node);
182-
}
183-
});
184-
break;
185140
case 'attributes':
186-
if (mutation.target instanceof HTMLOptionElement) {
187-
hasAnOptionChanged = true;
188-
break;
189-
}
190141
if (mutation.target === this.element && mutation.attributeName === 'disabled') {
191142
changeDisabledState = true;
192143
break;
193144
}
194-
break;
195-
case 'characterData':
196-
if (mutation.target instanceof Text && mutation.target.parentElement instanceof HTMLOptionElement) {
197-
if (mutation.target.parentElement.value === '') {
198-
changePlaceholder = true;
199-
break;
200-
}
201-
hasAnOptionChanged = true;
145+
if (mutation.target === this.element && mutation.attributeName === 'multiple') {
146+
requireReset = true;
147+
break;
202148
}
149+
break;
203150
}
204151
});
205-
if (hasAnOptionChanged || addedOptionElements.length > 0 || removedOptionElements.length > 0) {
152+
const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : [];
153+
const areOptionsEquivalent = this.areOptionsEquivalent(newOptions);
154+
if (!areOptionsEquivalent || requireReset) {
155+
this.originalOptions = newOptions;
206156
this.resetTomSelect();
207157
}
208158
if (changeDisabledState) {
209159
this.changeTomSelectDisabledState(this.formElement.disabled);
210160
}
211-
if (changePlaceholder) {
212-
this.updateTomSelectPlaceholder();
213-
}
214161
}
215-
requiresLiveIgnore() {
216-
return this.element instanceof HTMLSelectElement && this.element.multiple;
162+
createOptionsDataStructure(selectElement) {
163+
return Array.from(selectElement.options).map((option) => {
164+
const optgroup = option.closest('optgroup');
165+
return {
166+
value: option.value,
167+
text: option.text,
168+
group: optgroup ? optgroup.label : null,
169+
};
170+
});
171+
}
172+
areOptionsEquivalent(newOptions) {
173+
if (this.originalOptions.length !== newOptions.length) {
174+
return false;
175+
}
176+
const normalizeOption = (option) => `${option.value}-${option.text}-${option.group}`;
177+
const originalOptionsSet = new Set(this.originalOptions.map(normalizeOption));
178+
const newOptionsSet = new Set(newOptions.map(normalizeOption));
179+
return (originalOptionsSet.size === newOptionsSet.size &&
180+
[...originalOptionsSet].every((option) => newOptionsSet.has(option)));
217181
}
218182
}
219183
_default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _default_1_getCommonConfig() {
@@ -233,19 +197,12 @@ _default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _def
233197
return `<div class="no-results">${this.noResultsFoundTextValue}</div>`;
234198
},
235199
};
236-
const requiresLiveIgnore = this.requiresLiveIgnore();
237200
const config = {
238201
render,
239202
plugins,
240203
onItemAdd: () => {
241204
this.tomSelect.setTextboxValue('');
242205
},
243-
onInitialize: function () {
244-
if (requiresLiveIgnore) {
245-
const tomSelect = this;
246-
tomSelect.wrapper.setAttribute('data-live-ignore', '');
247-
}
248-
},
249206
closeAfterSelect: true,
250207
};
251208
if (!this.selectElement && !this.urlValue) {

0 commit comments

Comments
 (0)