// components/ui/TagMultiCombobox.tsx 'use client'; import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Label, } from '@headlessui/react'; import { ChevronDownIcon, CheckIcon } from '@heroicons/react/20/solid'; import { useEffect, useMemo, useState } from 'react'; import clsx from 'clsx'; import Badge from '@/components/ui/Badge'; export type TagOption = { id?: string | null; name: string; }; type TagMultiComboboxProps = { label?: string; availableTags: TagOption[]; value: TagOption[]; // ausgewählte Tags onChange: (next: TagOption[]) => void; placeholder?: string; className?: string; }; export default function TagMultiCombobox({ label = 'Tags', availableTags, value, onChange, placeholder = 'Tags auswählen oder neu eingeben …', className, }: TagMultiComboboxProps) { const [query, setQuery] = useState(''); const [allTags, setAllTags] = useState(availableTags); const selectedNames = useMemo( () => new Set(value.map((t) => t.name.toLowerCase())), [value], ); useEffect(() => { setAllTags((prev) => { const byName = new Map(prev.map((t) => [t.name.toLowerCase(), t])); for (const t of availableTags) { const key = t.name.toLowerCase(); if (!byName.has(key)) { byName.set(key, t); } } return Array.from(byName.values()); }); }, [availableTags]); const filteredTags = useMemo(() => { if (!query.trim()) return allTags; const q = query.toLowerCase(); return allTags.filter((tag) => tag.name.toLowerCase().includes(q)); }, [allTags, query]); function handleRemoveTag(tag: TagOption) { onChange( value.filter((t) => t.name.toLowerCase() !== tag.name.toLowerCase()), ); } function handleToggleTag(tag: TagOption) { const key = tag.name.toLowerCase(); if (selectedNames.has(key)) { onChange(value.filter((t) => t.name.toLowerCase() !== key)); } else { onChange([...value, tag]); } } function handleCreateTagFromQuery() { const name = query.trim(); if (!name) return; const existing = allTags.find( (t) => t.name.toLowerCase() === name.toLowerCase(), ); if (existing) { if (!selectedNames.has(existing.name.toLowerCase())) { onChange([...value, existing]); } setQuery(''); return; } const newTag: TagOption = { id: `__new-${Date.now()}`, name, }; setAllTags((prev) => [...prev, newTag]); onChange([...value, newTag]); setQuery(''); } return ( {}} className={clsx('w-full', className)} >
{/* Chips + Input in einem „Feld“ */}
{/* Ausgewählte Tags als Badges */} {value.map((tag) => ( handleRemoveTag(tag)} > {tag.name} ))} {/* Eingabefeld */} setQuery(event.target.value)} displayValue={() => ''} placeholder={value.length === 0 ? placeholder : undefined} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); handleCreateTagFromQuery(); } else if ( event.key === 'Backspace' && !query && value.length > 0 ) { const last = value[value.length - 1]; handleRemoveTag(last); } }} /> {/* Chevron immer rechts im Feld */}
{/* Dropdown */} {query.trim().length > 0 && !allTags.some( (t) => t.name.toLowerCase() === query.trim().toLowerCase(), ) && ( { e.preventDefault(); handleCreateTagFromQuery(); }} > „{query.trim()}“ erstellen )} {filteredTags.length === 0 && query.trim().length === 0 && (
Keine Tags vorhanden.
)} {filteredTags.map((tag) => { const isSelected = selectedNames.has(tag.name.toLowerCase()); return ( clsx( 'flex cursor-default select-none items-center justify-between px-3 py-2 text-gray-900 dark:text-gray-100', active && 'bg-indigo-600 text-white dark:bg-indigo-500', ) } onClick={(e) => { e.preventDefault(); handleToggleTag(tag); }} > {tag.name} {isSelected && ( ); })}
); }