// components/Autocomplete.js import { $, $html, $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; const query = $(val(value) || ""); const isOpen = $(false); const cursor = $(-1); const list = $(() => { const q = query().toLowerCase(); const data = val(items) || []; return q ? data.filter((o) => (typeof o === "string" ? o : o.label).toLowerCase().includes(q)) : data; }); const pick = (opt) => { const valStr = typeof opt === "string" ? opt : opt.value; const labelStr = typeof opt === "string" ? opt : opt.label; query(labelStr); if (typeof value === "function") value(valStr); onSelect?.(opt); isOpen(false); cursor(-1); }; const nav = (e) => { const items = list(); if (e.key === "ArrowDown") { e.preventDefault(); isOpen(true); cursor(Math.min(cursor() + 1, items.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(items[cursor()]); } else if (e.key === "Escape") { isOpen(false); } }; return $html("div", { class: 'relative w-full' }, [ Input({ label, class: className, placeholder: placeholder || tt("search")(), value: query, onfocus: () => isOpen(true), onblur: () => setTimeout(() => isOpen(false), 150), onkeydown: nav, oninput: (e) => { const v = e.target.value; query(v); if (typeof value === "function") value(v); isOpen(true); cursor(-1); }, ...rest, }), $html( "ul", { class: "absolute 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", style: () => (isOpen() && list().length ? "display:block" : "display:none"), }, [ $for( list, (opt, i) => $html("li", {}, [ $html( "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, ), () => (list().length ? null : $html("li", { class: "p-2 text-center opacity-50" }, tt("nodata")())), ], ), ]); };