// components/Autocomplete.js // import { $, Tag, For } from "../sigpro.js"; import { val } from "../core/utils.js"; import { tt } from "../core/i18n.js"; import { Input } from "./Input.js"; /** * Autocomplete component * * daisyUI classes used: * - input, input-bordered, input-primary, input-secondary * - menu, menu-dropdown, menu-dropdown-show * - bg-base-100, rounded-box, shadow-xl, border, border-base-300 * - absolute, left-0, w-full, mt-1, p-2, max-h-60, overflow-y-auto * - z-50, active, bg-primary, text-primary-content */ export const Autocomplete = (props) => { const { class: className, items = [], value, onselect, label, placeholder, ...rest } = props; // Inicializamos query con el valor actual de la señal recibida o string vacío const query = $(val(value) || ""); const isOpen = $(false); const cursor = $(-1); // FIX CRÍTICO: En lugar de una computed automática, usamos una señal manual // y un Watch para garantizar que la lista se actualice SÍNCRONAMENTE. const list = $([]); Watch(() => { const q = String(query()).toLowerCase(); const data = val(items) || []; const filtered = q ? data.filter((o) => (typeof o === "string" ? o : o.label).toLowerCase().includes(q)) : data; list(filtered); }); const pick = (opt) => { const valStr = typeof opt === "string" ? opt : opt.value; const labelStr = typeof opt === "string" ? opt : opt.label; // Actualizamos ambas señales query(labelStr); if (typeof value === "function") value(valStr); onselect?.(opt); isOpen(false); cursor(-1); }; const nav = (e) => { const currentItems = list(); if (e.key === "ArrowDown") { e.preventDefault(); isOpen(true); cursor(Math.min(cursor() + 1, currentItems.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); cursor(Math.max(cursor() - 1, 0)); } else if (e.key === "Enter" && cursor() >= 0) { e.preventDefault(); pick(currentItems[cursor()]); } else if (e.key === "Escape") { isOpen(false); } }; return Tag("div", { class: 'relative w-full' }, [ Input({ label, class: className, placeholder: placeholder || tt("search")(), value: query, // Vinculado a la señal query onfocus: () => isOpen(true), onblur: () => setTimeout(() => isOpen(false), 150), onkeydown: nav, oninput: (e) => { const v = e.target.value; query(v); // Esto dispara el Watch de arriba y actualiza 'list' if (typeof value === "function") value(v); isOpen(true); cursor(-1); }, ...rest, }), Tag( "ul", { class: "absolute dropdown-menu left-0 w-full menu bg-base-100 rounded-box mt-1 p-2 shadow-xl max-h-60 overflow-y-auto border border-base-300 z-50", // Usamos una función para que el estilo sea reactivo style: () => (isOpen() && list().length ? "display:block" : "display:none"), }, [ For( list, (opt, i) => Tag("li", {}, [ Tag( "a", { class: () => `block w-full ${cursor() === i ? "active bg-primary text-primary-content" : ""}`, onclick: () => pick(opt), onmouseenter: () => cursor(i), }, typeof opt === "string" ? opt : opt.label, ), ]), (opt, i) => (typeof opt === "string" ? opt : opt.value) + i, ), // Mensaje de "no hay datos" reactivo () => (list().length ? null : Tag("li", { class: "p-2 text-center opacity-50" }, tt("nodata")())), ], ), ]); };