From 1cdd04e19342c6159bf77149c28e2efacbaf08bf Mon Sep 17 00:00:00 2001 From: JYC333 <22962980+JYC333@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:10:45 +0000 Subject: [PATCH] refactor: add new autocomplete registry --- .../src/services/attribute_autocomplete.ts | 183 ++++++++++++++++-- 1 file changed, 165 insertions(+), 18 deletions(-) diff --git a/apps/client/src/services/attribute_autocomplete.ts b/apps/client/src/services/attribute_autocomplete.ts index 22d99d49b8..6cc82661e3 100644 --- a/apps/client/src/services/attribute_autocomplete.ts +++ b/apps/client/src/services/attribute_autocomplete.ts @@ -1,19 +1,135 @@ +import { autocomplete } from "@algolia/autocomplete-js"; +import type { AutocompleteApi } from "@algolia/autocomplete-js"; +import type { BaseItem } from "@algolia/autocomplete-core"; import type { AttributeType } from "../entities/fattribute.js"; import server from "./server.js"; -interface InitOptions { +// --------------------------------------------------------------------------- +// Global instance registry for "close all" functionality +// --------------------------------------------------------------------------- + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const activeInstances = new Set>(); + +export function closeAllAttributeAutocompletes(): void { + for (const api of activeInstances) { + api.setIsOpen(false); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const instanceMap = new WeakMap>(); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface NameItem extends BaseItem { + name: string; +} + +/** New API: pass a container div, autocomplete-js creates its own input inside. */ +interface NewInitOptions { + container: HTMLElement; + attributeType?: AttributeType | (() => AttributeType); + open: boolean; + onValueChange?: (value: string) => void; +} + +/** Old API: pass a jQuery input element, uses legacy autocomplete.js plugin. */ +interface OldInitOptions { $el: JQuery; attributeType?: AttributeType | (() => AttributeType); open: boolean; - nameCallback?: () => string; } -/** - * @param $el - element on which to init autocomplete - * @param attributeType - "relation" or "label" or callback providing one of those values as a type of autocompleted attributes - * @param open - should the autocomplete be opened after init? - */ -function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) { +type InitAttributeNameOptions = NewInitOptions | OldInitOptions; + +function isNewApi(opts: InitAttributeNameOptions): opts is NewInitOptions { + return "container" in opts; +} + +// --------------------------------------------------------------------------- +// Attribute name autocomplete +// --------------------------------------------------------------------------- + +function initAttributeNameAutocomplete(opts: InitAttributeNameOptions) { + if (isNewApi(opts)) { + initAttributeNameNew(opts); + } else { + initAttributeNameLegacy(opts); + } +} + +/** New implementation using @algolia/autocomplete-js */ +function initAttributeNameNew({ container, attributeType, open, onValueChange }: NewInitOptions) { + // Only init once per container + if (instanceMap.has(container)) { + if (open) { + const api = instanceMap.get(container)!; + api.setIsOpen(true); + api.refresh(); + } + return; + } + + const api = autocomplete({ + container, + panelContainer: document.body, + openOnFocus: true, + detachedMediaQuery: "none", + placeholder: "", + classNames: { + input: "form-control", + }, + + getSources({ query }) { + return [ + { + sourceId: "attribute-names", + getItems() { + const type = typeof attributeType === "function" ? attributeType() : attributeType; + return server + .get(`attribute-names/?type=${type}&query=${encodeURIComponent(query)}`) + .then((names) => names.map((name) => ({ name }))); + }, + getItemInputValue({ item }) { + return item.name; + }, + onSelect({ item }) { + onValueChange?.(item.name); + }, + templates: { + item({ item, html }) { + return html`
${item.name}
`; + }, + }, + }, + ]; + }, + + onStateChange({ state, prevState }) { + if (!state.isOpen && prevState.isOpen) { + onValueChange?.(state.query); + } + }, + + shouldPanelOpen() { + return true; + }, + }); + + instanceMap.set(container, api); + activeInstances.add(api); + + if (open) { + api.setIsOpen(true); + api.refresh(); + } +} + +/** Legacy implementation using old autocomplete.js jQuery plugin */ +function initAttributeNameLegacy({ $el, attributeType, open }: OldInitOptions) { if (!$el.hasClass("aa-input")) { $el.autocomplete( { @@ -26,14 +142,11 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions [ { displayKey: "name", - // disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type cache: false, source: async (term, cb) => { const type = typeof attributeType === "function" ? attributeType() : attributeType; - const names = await server.get(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`); const result = names.map((name) => ({ name })); - cb(result); } } @@ -52,10 +165,18 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions } } -async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptions) { +// --------------------------------------------------------------------------- +// Label value autocomplete (still using old autocomplete.js) +// --------------------------------------------------------------------------- + +interface LabelValueInitOptions { + $el: JQuery; + open: boolean; + nameCallback?: () => string; +} + +async function initLabelValueAutocomplete({ $el, open, nameCallback }: LabelValueInitOptions) { if ($el.hasClass("aa-input")) { - // we reinit every time because autocomplete seems to have a bug where it retains state from last - // open even though the value was reset $el.autocomplete("destroy"); } @@ -78,7 +199,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio { appendTo: document.querySelector("body"), hint: false, - openOnFocus: false, // handled manually + openOnFocus: false, minLength: 0, tabAutocomplete: false }, @@ -88,9 +209,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio cache: false, source: async function (term, cb) { term = term.toLowerCase(); - const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term)); - cb(filtered); } } @@ -108,7 +227,35 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptio } } +// --------------------------------------------------------------------------- +// Utilities for the new autocomplete-js containers +// --------------------------------------------------------------------------- + +function getInput(container: HTMLElement): HTMLInputElement | null { + return container.querySelector(".aa-Input"); +} + +function setInputValue(container: HTMLElement, value: string): void { + const input = getInput(container); + if (input) { + input.value = value; + } + const api = instanceMap.get(container); + if (api) { + api.setQuery(value); + } +} + +function getInputValue(container: HTMLElement): string { + const input = getInput(container); + return input?.value ?? ""; +} + export default { initAttributeNameAutocomplete, - initLabelValueAutocomplete + initLabelValueAutocomplete, + closeAllAttributeAutocompletes, + getInput, + setInputValue, + getInputValue, };