This commit is contained in:
92
components/autocomplete.js
Normal file
92
components/autocomplete.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// components/Autocomplete.js
|
||||
import { $, Tag, For, Watch } from "sigpro";
|
||||
|
||||
export const Autocomplete = (props) => {
|
||||
const query = $("");
|
||||
const isOpen = $(false);
|
||||
const cursor = $(-1);
|
||||
const filteredItems = $([]);
|
||||
|
||||
Watch(() => {
|
||||
const v = typeof props.value === "function" ? props.value() : props.value;
|
||||
return v || "";
|
||||
}, (newVal) => setTimeout(() => query(newVal), 0));
|
||||
|
||||
Watch(() => {
|
||||
const q = String(query()).toLowerCase();
|
||||
const allItems = typeof props.items === "function" ? props.items() : props.items;
|
||||
const filtered = q
|
||||
? allItems.filter((item) =>
|
||||
(typeof item === "string" ? item : item.label).toLowerCase().includes(q)
|
||||
)
|
||||
: allItems;
|
||||
filteredItems(filtered);
|
||||
});
|
||||
|
||||
const pick = (item) => {
|
||||
const display = typeof item === "string" ? item : item.label;
|
||||
const actual = typeof item === "string" ? item : item.value;
|
||||
query(display);
|
||||
if (typeof props.value === "function") props.value(actual);
|
||||
props.onselect?.(item);
|
||||
isOpen(false);
|
||||
cursor(-1);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
const list = filteredItems();
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
isOpen(true);
|
||||
cursor(Math.min(cursor() + 1, list.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(list[cursor()]);
|
||||
} else if (e.key === "Escape") {
|
||||
isOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return Tag("div", { class: `relative w-full ${props.class ?? ''}` }, [
|
||||
Tag("label", { class: "input input-bordered w-full" }, [
|
||||
Tag("span", { class: "icon-[lucide--search]" }),
|
||||
Tag("input", {
|
||||
...props,
|
||||
type: "text",
|
||||
class: "grow",
|
||||
placeholder: props.placeholder || "Buscar...",
|
||||
value: query,
|
||||
onfocus: () => isOpen(true),
|
||||
onblur: () => setTimeout(() => isOpen(false), 150),
|
||||
onkeydown: handleKeyDown,
|
||||
oninput: (e) => {
|
||||
const newVal = e.target.value;
|
||||
query(newVal);
|
||||
if (typeof props.value === "function") props.value(newVal);
|
||||
isOpen(true);
|
||||
cursor(-1);
|
||||
}
|
||||
})
|
||||
]),
|
||||
|
||||
Tag("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: () => `display: ${isOpen() && filteredItems().length ? "block" : "none"};`
|
||||
}, [
|
||||
For(filteredItems, (item, idx) =>
|
||||
Tag("li", {}, [
|
||||
Tag("a", {
|
||||
class: () => `block w-full ${cursor() === idx ? "active bg-primary text-primary-content" : ""}`,
|
||||
onclick: () => pick(item),
|
||||
onmouseenter: () => cursor(idx)
|
||||
}, typeof item === "string" ? item : item.label)
|
||||
]),
|
||||
(item, idx) => (typeof item === "string" ? item : item.value) + idx
|
||||
),
|
||||
() => filteredItems().length === 0 ? Tag("li", { class: "flex justify-center p-4 opacity-50" }, Tag("span", { class: "icon-[lucide--search-x] text-2xl" })) : null
|
||||
])
|
||||
]);
|
||||
};
|
||||
Reference in New Issue
Block a user