Files
sigpro-ui/src/components/Autocomplete.js
2026-04-03 23:54:11 +02:00

106 lines
3.1 KiB
JavaScript

// 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")())),
],
),
]);
};