"use client" import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { createContext, useCallback, useContext, useEffect, useRef, useState, type ComponentPropsWithoutRef, type ReactNode, } from "react" import { Badge } from "@/components/ui/badge" type MultiSelectContextType = { open: boolean setOpen: (open: boolean) => void selectedValues: Set toggleValue: (value: string) => void items: Map onItemAdded: (value: string, label: ReactNode) => void } const MultiSelectContext = createContext(null) export function MultiSelect({ children, values, defaultValues, onValuesChange, }: { children: ReactNode values?: string[] defaultValues?: string[] onValuesChange?: (values: string[]) => void }) { const [open, setOpen] = useState(false) const [internalValues, setInternalValues] = useState( new Set(values ?? defaultValues), ) const selectedValues = values ? new Set(values) : internalValues const [items, setItems] = useState>(new Map()) function toggleValue(value: string) { const getNewSet = (prev: Set) => { const newSet = new Set(prev) if (newSet.has(value)) { newSet.delete(value) } else { newSet.add(value) } return newSet } setInternalValues(getNewSet) onValuesChange?.([...getNewSet(selectedValues)]) } const onItemAdded = useCallback((value: string, label: ReactNode) => { setItems(prev => { if (prev.get(value) === label) return prev return new Map(prev).set(value, label) }) }, []) return ( {children} ) } export function MultiSelectTrigger({ className, children, ...props }: { className?: string children?: ReactNode } & ComponentPropsWithoutRef) { const { open } = useMultiSelectContext() return ( ) } export function MultiSelectValue({ placeholder, clickToRemove = true, className, overflowBehavior = "wrap-when-open", ...props }: { placeholder?: string clickToRemove?: boolean overflowBehavior?: "wrap" | "wrap-when-open" | "cutoff" } & Omit, "children">) { const { selectedValues, toggleValue, items, open } = useMultiSelectContext() const [overflowAmount, setOverflowAmount] = useState(0) const valueRef = useRef(null) const overflowRef = useRef(null) const shouldWrap = overflowBehavior === "wrap" || (overflowBehavior === "wrap-when-open" && open) const checkOverflow = useCallback(() => { if (valueRef.current == null) return const containerElement = valueRef.current const overflowElement = overflowRef.current const items = containerElement.querySelectorAll( "[data-selected-item]", ) if (overflowElement != null) overflowElement.style.display = "none" items.forEach(child => child.style.removeProperty("display")) let amount = 0 for (let i = items.length - 1; i >= 0; i--) { const child = items[i]! if (containerElement.scrollWidth <= containerElement.clientWidth) { break } amount = items.length - i child.style.display = "none" overflowElement?.style.removeProperty("display") } setOverflowAmount(amount) }, []) const handleResize = useCallback( (node: HTMLDivElement) => { valueRef.current = node const mutationObserver = new MutationObserver(checkOverflow) const observer = new ResizeObserver(debounce(checkOverflow, 100)) mutationObserver.observe(node, { childList: true, attributes: true, attributeFilter: ["class", "style"], }) observer.observe(node) return () => { observer.disconnect() mutationObserver.disconnect() valueRef.current = null } }, [checkOverflow], ) if (selectedValues.size === 0 && placeholder) { return ( {placeholder} ) } return (
{[...selectedValues] .filter(value => items.has(value)) .map(value => ( { e.stopPropagation() toggleValue(value) } : undefined } > {items.get(value)} {clickToRemove && ( )} ))} 0 && !shouldWrap ? "block" : "none", }} variant="outline" ref={overflowRef} > +{overflowAmount}
) } export function MultiSelectContent({ search = true, children, ...props }: { search?: boolean | { placeholder?: string; emptyMessage?: string } children: ReactNode } & Omit, "children">) { const canSearch = typeof search === "object" ? true : search return ( <>
{children}
{canSearch ? ( ) : (