From 52bec7354d21192bec88622b60dbfb31b3791460 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sat, 17 Feb 2024 07:00:32 -0500 Subject: [PATCH] WIP - playing with Autocomplete + morph --- src/Autocomplete/assets/dist/controller.d.ts | 2 + src/Autocomplete/assets/dist/controller.js | 53 ++++++++++--- src/Autocomplete/assets/src/controller.ts | 78 ++++++++++++++++--- ux.symfony.com/src/Form/TimeForAMealForm.php | 4 + ux.symfony.com/templates/base.html.twig | 2 + .../ux_packages/autocomplete.html.twig | 6 +- 6 files changed, 121 insertions(+), 24 deletions(-) diff --git a/src/Autocomplete/assets/dist/controller.d.ts b/src/Autocomplete/assets/dist/controller.d.ts index 091fcd4c25a..8d62db42dba 100644 --- a/src/Autocomplete/assets/dist/controller.d.ts +++ b/src/Autocomplete/assets/dist/controller.d.ts @@ -50,4 +50,6 @@ export default class extends Controller { private onMutations; private createOptionsDataStructure; private areOptionsEquivalent; + private beforeMorphElement; + private beforeMorphAttribute; } diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js index 5659d9bcc17..9b472b7119e 100644 --- a/src/Autocomplete/assets/dist/controller.js +++ b/src/Autocomplete/assets/dist/controller.js @@ -42,6 +42,12 @@ class default_1 extends Controller { if (this.selectElement) { this.originalOptions = this.createOptionsDataStructure(this.selectElement); } + const parentElement = this.element.parentElement; + if (!parentElement) { + return; + } + parentElement.addEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this)); + parentElement.addEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this)); this.initializeTomSelect(); } initializeTomSelect() { @@ -61,6 +67,12 @@ class default_1 extends Controller { } disconnect() { this.stopMutationObserver(); + const parentElement = this.element.parentElement; + if (!parentElement) { + return; + } + parentElement.removeEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this)); + parentElement.removeEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this)); let currentSelectedValues = []; if (this.selectElement) { if (this.selectElement.multiple) { @@ -156,15 +168,13 @@ class default_1 extends Controller { } } onMutations(mutations) { - let changeDisabledState = false; let requireReset = false; + if (this.tomSelect.isDisabled !== this.formElement.disabled) { + this.changeTomSelectDisabledState(this.formElement.disabled); + } mutations.forEach((mutation) => { switch (mutation.type) { case 'attributes': - if (mutation.target === this.element && mutation.attributeName === 'disabled') { - changeDisabledState = true; - break; - } if (mutation.target === this.element && mutation.attributeName === 'multiple') { const isNowMultiple = this.element.hasAttribute('multiple'); const wasMultiple = mutation.oldValue === 'multiple'; @@ -178,13 +188,12 @@ class default_1 extends Controller { }); const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : []; const areOptionsEquivalent = this.areOptionsEquivalent(newOptions); - if (!areOptionsEquivalent || requireReset) { + const value = this.selectElement ? Array.from(this.selectElement.options || []).map((option) => option.value) : this.formElement.value; + const didValueChange = value !== this.tomSelect.getValue(); + if (!areOptionsEquivalent || requireReset || didValueChange) { this.originalOptions = newOptions; this.resetTomSelect(); } - if (changeDisabledState) { - this.changeTomSelectDisabledState(this.formElement.disabled); - } } createOptionsDataStructure(selectElement) { return Array.from(selectElement.options).map((option) => { @@ -208,6 +217,32 @@ class default_1 extends Controller { return (originalOptionsSet.size === newOptionsSet.size && [...originalOptionsSet].every((option) => newOptionsSet.has(option))); } + beforeMorphElement(event) { + if (event.target.classList.contains('ts-wrapper')) { + event.preventDefault(); + return; + } + if (event.target === this.element) { + const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : []; + if (this.areOptionsEquivalent(newOptions)) { + event.preventDefault(); + return; + } + } + } + beforeMorphAttribute(event) { + if (event.target.tagName === 'LABEL') { + console.log('before morph attribute', event.detail); + } + if (event.target.tagName === 'LABEL' && ['for', 'id'].includes(event.detail.attributeName)) { + event.preventDefault(); + return; + } + if (event.target === this.element && event.detail.attributeName === 'class') { + event.preventDefault(); + return; + } + } } _default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _default_1_getCommonConfig() { const plugins = {}; diff --git a/src/Autocomplete/assets/src/controller.ts b/src/Autocomplete/assets/src/controller.ts index bc3812b7a82..46252f0d205 100644 --- a/src/Autocomplete/assets/src/controller.ts +++ b/src/Autocomplete/assets/src/controller.ts @@ -52,6 +52,13 @@ export default class extends Controller { if (this.selectElement) { this.originalOptions = this.createOptionsDataStructure(this.selectElement); } + // TODO - also listen on `live:before-morph-element` + const parentElement = this.element.parentElement; + if (!parentElement) { + return; + } + parentElement.addEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this)); + parentElement.addEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this)); this.initializeTomSelect(); } @@ -85,6 +92,13 @@ export default class extends Controller { disconnect() { this.stopMutationObserver(); + const parentElement = this.element.parentElement; + if (!parentElement) { + return; + } + parentElement.removeEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this)); + parentElement.removeEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this)); + // TomSelect.destroy() resets the element to its original HTML. This // causes the selected value to be lost. We store it. let currentSelectedValues: string[] = []; @@ -373,18 +387,15 @@ export default class extends Controller { } private onMutations(mutations: MutationRecord[]): void { - let changeDisabledState = false; let requireReset = false; + if (this.tomSelect.isDisabled !== this.formElement.disabled) { + this.changeTomSelectDisabledState(this.formElement.disabled); + } + mutations.forEach((mutation) => { switch (mutation.type) { case 'attributes': - if (mutation.target === this.element && mutation.attributeName === 'disabled') { - changeDisabledState = true; - - break; - } - if (mutation.target === this.element && mutation.attributeName === 'multiple') { const isNowMultiple = this.element.hasAttribute('multiple'); const wasMultiple = mutation.oldValue === 'multiple'; @@ -401,14 +412,15 @@ export default class extends Controller { const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : []; const areOptionsEquivalent = this.areOptionsEquivalent(newOptions); - if (!areOptionsEquivalent || requireReset) { + + // TODO: test this: look for changes in the "value" of the select element + const value = this.selectElement ? Array.from(this.selectElement.options || []).map((option) => option.value) : this.formElement.value; + const didValueChange = value !== this.tomSelect.getValue(); + + if (!areOptionsEquivalent || requireReset || didValueChange) { this.originalOptions = newOptions; this.resetTomSelect(); } - - if (changeDisabledState) { - this.changeTomSelectDisabledState(this.formElement.disabled); - } } private createOptionsDataStructure( @@ -443,4 +455,46 @@ export default class extends Controller { [...originalOptionsSet].every((option) => newOptionsSet.has(option)) ); } + + private beforeMorphElement(event: any) { + // TomSelect adds this element to the DOM. Keep it. + if (event.target.classList.contains('ts-wrapper')) { + event.preventDefault(); + + return; + } + + if (event.target === this.element) { + const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : []; + if (this.areOptionsEquivalent(newOptions)) { + // prevent the