From 3ca64ee0d13fce3c261e12a83d09823e2a296b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20L=C3=BCtzner?= Date: Wed, 15 Jul 2026 17:10:22 +0200 Subject: [PATCH] autocomplete shenanigans --- .idea/php.xml | 2 +- .idea/pl_mail.iml | 2 +- assets/controllers.json | 11 + .../address_autocomplete_controller.js | 204 ++++++------- assets/styles/tom-select.css | 278 ------------------ composer.json | 6 +- composer.lock | 175 ++++++----- config/bundles.php | 1 + config/reference.php | 4 +- config/routes/ux_autocomplete.yaml | 3 + importmap.php | 5 +- src/Entity/Contact.php | 7 + src/Form/ComposeType.php | 17 +- src/Form/ContactAutocompleteField.php | 35 +++ symfony.lock | 12 + templates/compose/_window.html.twig | 27 +- 16 files changed, 280 insertions(+), 509 deletions(-) delete mode 100644 assets/styles/tom-select.css create mode 100644 config/routes/ux_autocomplete.yaml create mode 100644 src/Form/ContactAutocompleteField.php diff --git a/.idea/php.xml b/.idea/php.xml index 42a32031..6c7fd446 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -153,7 +153,6 @@ - @@ -167,6 +166,7 @@ + diff --git a/.idea/pl_mail.iml b/.idea/pl_mail.iml index e759430a..17777333 100644 --- a/.idea/pl_mail.iml +++ b/.idea/pl_mail.iml @@ -139,7 +139,6 @@ - @@ -153,6 +152,7 @@ + diff --git a/assets/controllers.json b/assets/controllers.json index 8d7580b8..4c681b6d 100644 --- a/assets/controllers.json +++ b/assets/controllers.json @@ -1,5 +1,16 @@ { "controllers": { + "@symfony/ux-autocomplete": { + "autocomplete": { + "enabled": true, + "fetch": "eager", + "autoimport": { + "tom-select/dist/css/tom-select.default.css": true, + "tom-select/dist/css/tom-select.bootstrap4.css": false, + "tom-select/dist/css/tom-select.bootstrap5.css": false + } + } + }, "@symfony/ux-turbo": { "turbo-core": { "enabled": true, diff --git a/assets/controllers/address_autocomplete_controller.js b/assets/controllers/address_autocomplete_controller.js index 0564c56e..0bb124cf 100644 --- a/assets/controllers/address_autocomplete_controller.js +++ b/assets/controllers/address_autocomplete_controller.js @@ -19,110 +19,110 @@ export default class extends Controller { #tomSelect = null; connect() { - const input = this.inputTarget; - - // Forward clicks anywhere in the row to the TS input. - this._boundRowClick = (e) => { - console.log("row click", e.target); - if (e.target.closest(".item")) { return; } - this.#tomSelect?.focus(); - }; - this.element.addEventListener("click", this._boundRowClick); - - // Gather any pre-filled addresses from the hidden input's value - // (e.g. when editing a draft or doing reply/forward). - const prefilledJson = input.dataset.prefilled; - let prefilledItems = []; - if (prefilledJson) { - try { prefilledItems = JSON.parse(prefilledJson); } catch (_) {} - } - - this.#tomSelect = new TomSelect(input, { - // Allow typing any address freely - create: true, - createOnBlur: true, - createFilter: /\S+@\S+\.\S+/, // only create if it looks like an email - maxItems: null, // unlimited recipients - persist: false, // don't keep typed text after blur - valueField: "value", - labelField: "label", - searchField: ["label", "email"], - openOnFocus: false, - hideSelected: true, - closeAfterSelect: false, // keep open so you can add another quickly - preload: false, - onItemAdd: function() { - this.setTextboxValue(''); - this.close(); - }, - - // ── Seed with pre-filled addresses (reply/forward/draft) ─────── - options: prefilledItems, - items: prefilledItems.map((i) => i.value), - - // ── Remote search ───────────────────────────────────────────── - load: async (query, callback) => { - if (query.length < MIN_CHARS) { - callback([]); - return; - } - - try { - const response = await fetch( - `${AUTOCOMPLETE_URL}?q=${encodeURIComponent(query)}`, - { headers: { "X-Requested-With": "XMLHttpRequest" } }, - ); - - if (!response.ok) { callback([]); return; } - - const contacts = await response.json(); - - callback(contacts.map((c) => ({ - value: c.email, - label: c.displayName ? `${c.displayName} <${c.email}>` : c.email, - email: c.email, - displayName: c.displayName ?? "", - initials: c.initials ?? c.email[0].toUpperCase(), - }))); - } catch (_) { - callback([]); - } - }, - - // ── Renderers ──────────────────────────────────────────────── - render: { - // Suggestion row in the dropdown - option: (data, escape) => ` -
- ${escape(data.initials ?? data.email?.[0]?.toUpperCase() ?? "?")} - - ${data.displayName ? `${escape(data.displayName)}` : ""} - ${escape(data.email ?? data.value)} - -
`, - - // Selected chip inside the control - item: (data, escape) => ` -
- ${escape(data.displayName || data.email || data.value)} -
`, - - // "Add " row — rendered subtly at the bottom - option_create: (data, escape) => ` -
- - Add ${escape(data.input)} -
`, - - no_results: () => `
No contacts found
`, - loading: () => `
Searching…
`, - }, - }); + // const input = this.inputTarget; + // + // // Forward clicks anywhere in the row to the TS input. + // this._boundRowClick = (e) => { + // console.log("row click", e.target); + // if (e.target.closest(".item")) { return; } + // this.#tomSelect?.focus(); + // }; + // this.element.addEventListener("click", this._boundRowClick); + // + // // Gather any pre-filled addresses from the hidden input's value + // // (e.g. when editing a draft or doing reply/forward). + // const prefilledJson = input.dataset.prefilled; + // let prefilledItems = []; + // if (prefilledJson) { + // try { prefilledItems = JSON.parse(prefilledJson); } catch (_) {} + // } + // + // this.#tomSelect = new TomSelect(input, { + // // Allow typing any address freely + // create: true, + // createOnBlur: true, + // createFilter: /\S+@\S+\.\S+/, // only create if it looks like an email + // maxItems: null, // unlimited recipients + // persist: false, // don't keep typed text after blur + // valueField: "value", + // labelField: "label", + // searchField: ["label", "email"], + // openOnFocus: false, + // hideSelected: true, + // closeAfterSelect: false, // keep open so you can add another quickly + // preload: false, + // onItemAdd: function() { + // this.setTextboxValue(''); + // this.close(); + // }, + // + // // ── Seed with pre-filled addresses (reply/forward/draft) ─────── + // options: prefilledItems, + // items: prefilledItems.map((i) => i.value), + // + // // ── Remote search ───────────────────────────────────────────── + // load: async (query, callback) => { + // if (query.length < MIN_CHARS) { + // callback([]); + // return; + // } + // + // try { + // const response = await fetch( + // `${AUTOCOMPLETE_URL}?q=${encodeURIComponent(query)}`, + // { headers: { "X-Requested-With": "XMLHttpRequest" } }, + // ); + // + // if (!response.ok) { callback([]); return; } + // + // const contacts = await response.json(); + // + // callback(contacts.map((c) => ({ + // value: c.email, + // label: c.displayName ? `${c.displayName} <${c.email}>` : c.email, + // email: c.email, + // displayName: c.displayName ?? "", + // initials: c.initials ?? c.email[0].toUpperCase(), + // }))); + // } catch (_) { + // callback([]); + // } + // }, + // + // // ── Renderers ──────────────────────────────────────────────── + // render: { + // // Suggestion row in the dropdown + // option: (data, escape) => ` + //
+ // ${escape(data.initials ?? data.email?.[0]?.toUpperCase() ?? "?")} + // + // ${data.displayName ? `${escape(data.displayName)}` : ""} + // ${escape(data.email ?? data.value)} + // + //
`, + // + // // Selected chip inside the control + // item: (data, escape) => ` + //
+ // ${escape(data.displayName || data.email || data.value)} + //
`, + // + // // "Add " row — rendered subtly at the bottom + // option_create: (data, escape) => ` + //
+ // + // Add ${escape(data.input)} + //
`, + // + // no_results: () => `
No contacts found
`, + // loading: () => `
Searching…
`, + // }, + // }); } disconnect() { - this.element.removeEventListener("click", this._boundRowClick); - this.#tomSelect?.destroy(); - this.#tomSelect = null; + // this.element.removeEventListener("click", this._boundRowClick); + // this.#tomSelect?.destroy(); + // this.#tomSelect = null; } } diff --git a/assets/styles/tom-select.css b/assets/styles/tom-select.css deleted file mode 100644 index 4d314c1c..00000000 --- a/assets/styles/tom-select.css +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Tom Select — compose-window theme - * - * Tom Select wraps the - {% for addr in form.toAddresses.vars.data ?? [] %} - {% if addr.address is defined and addr.address %} - - {% endif %} - {% endfor %} - - -
- - -
+{{ form_widget(form.toAddresses) }} {# ── Cc ───────────────────────────────────────────────── #}