import { useMemo, useRef, useState } from "react"; import { normalize } from "../lib/normalize"; import { cn } from "../lib/utils"; import { useClickOutside } from "./useClickOutside"; import { ConfirmDeleteDialog } from "./ConfirmDeleteDialog"; export interface MultiSelectComboboxProps { values: string[]; onChange: (values: string[]) => void; options?: string[]; // گزینه‌های کش‌شده (مشخصه‌های استفاده‌شده قبلی) placeholder?: string; className?: string; inputClassName?: string; allowCreate?: boolean; // اجازه‌ی تایپ مقدار جدید (پیش‌فرض true) } // چند-انتخابی خلاق (creatable multi-select) با چیپ‌ها: // - چیپ‌های قابل حذف با × // - input بدون حاشیه بعد از آخرین چیپ (فوکوس خودکار) // - منوی autocomplete با فیلتر زنده + گزینه‌ی «ایجاد '{query}'» // - Enter برای افزودن، Backspace برای حذف آخرین چیپ، جلوگیری از تکراری export function MultiSelectCombobox({ values, onChange, options = [], placeholder, className, inputClassName, allowCreate = true, }: MultiSelectComboboxProps) { const [text, setText] = useState(""); const [open, setOpen] = useState(false); const [highlight, setHighlight] = useState(-1); const [pendingRemove, setPendingRemove] = useState(null); const inputRef = useRef(null); const ref = useClickOutside(() => setOpen(false)); // گزینه‌ها = کش + مقادیر انتخاب‌شده (تا چیپ‌های قبلی در لیست هم دیده شوند) const all = useMemo(() => { const set = new Set(); options.forEach((o) => o && set.add(o)); values.forEach((v) => v && set.add(v)); return [...set].sort((a, b) => a.localeCompare(b, "fa")); }, [options, values]); const q = useMemo(() => normalize(text), [text]); const filtered = useMemo(() => { if (!q) return all; return all.filter((x) => normalize(x).includes(q)); }, [all, q]); const exact = q.length > 0 && filtered.some((x) => normalize(x) === q); const showCreate = allowCreate && q.length > 0 && !exact; const listCount = filtered.length + (showCreate ? 1 : 0); const add = (val: string) => { const v = val.trim(); if (!v) return; if (values.some((x) => normalize(x) === normalize(v))) { setText(""); setHighlight(-1); inputRef.current?.focus(); return; // تکراری — اضافه نمی‌شود } onChange([...values, v]); setText(""); setHighlight(-1); inputRef.current?.focus(); }; const remove = (v: string) => { onChange(values.filter((x) => x !== v)); inputRef.current?.focus(); }; const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); if (highlight >= 0 && highlight < filtered.length) { add(filtered[highlight]); } else if (text.trim()) { add(text.trim()); } return; } if (e.key === "Backspace" && !text && values.length) { e.preventDefault(); remove(values[values.length - 1]); return; } if (e.key === "ArrowDown") { e.preventDefault(); setOpen(true); setHighlight((h) => (h + 1) % Math.max(listCount, 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setHighlight((h) => (h - 1 + Math.max(listCount, 1)) % Math.max(listCount, 1)); } else if (e.key === "Escape") { setOpen(false); } }; return (
inputRef.current?.focus()} > {values.map((v) => ( {v} ))} { setText(e.target.value); setHighlight(-1); setOpen(true); }} onFocus={() => setOpen(true)} onKeyDown={onKeyDown} />
{open && (
{listCount === 0 && (
{allowCreate && text.trim() ? ( برای افزودن «{text.trim()}» Enter بزنید ) : ( "موردی یافت نشد" )}
)} {filtered.map((it, i) => ( ))} {showCreate && ( )}
)} { if (pendingRemove !== null) remove(pendingRemove); setPendingRemove(null); }} onCancel={() => setPendingRemove(null)} />
); }