mirror of
https://github.com/Sosokker/go-chi-oapi-codegen-todolist.git
synced 2025-12-19 14:04:07 +01:00
122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { X } from "lucide-react"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Command, CommandGroup, CommandItem } from "@/components/ui/command"
|
|
import { Command as CommandPrimitive } from "cmdk"
|
|
|
|
interface MultiSelectProps {
|
|
options: { label: string; value: string }[]
|
|
selected: string[]
|
|
onChange: (selected: string[]) => void
|
|
placeholder?: string
|
|
}
|
|
|
|
export function MultiSelect({ options, selected, onChange, placeholder = "Select options" }: MultiSelectProps) {
|
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
const [open, setOpen] = React.useState(false)
|
|
const [inputValue, setInputValue] = React.useState("")
|
|
|
|
const handleUnselect = (value: string) => {
|
|
onChange(selected.filter((s) => s !== value))
|
|
}
|
|
|
|
const handleSelect = (value: string) => {
|
|
if (selected.includes(value)) {
|
|
onChange(selected.filter((s) => s !== value))
|
|
} else {
|
|
onChange([...selected, value])
|
|
}
|
|
}
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
const input = inputRef.current
|
|
if (input) {
|
|
if (e.key === "Delete" || e.key === "Backspace") {
|
|
if (input.value === "" && selected.length > 0) {
|
|
onChange(selected.slice(0, -1))
|
|
}
|
|
}
|
|
if (e.key === "Escape") {
|
|
input.blur()
|
|
}
|
|
}
|
|
}
|
|
|
|
const selectedOptions = options.filter((option) => selected.includes(option.value))
|
|
|
|
return (
|
|
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-transparent">
|
|
<div className="group border rounded-md px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
<div className="flex flex-wrap gap-1">
|
|
{selectedOptions.map((option) => (
|
|
<Badge key={option.value} variant="secondary" className="rounded-sm">
|
|
{option.label}
|
|
<button
|
|
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
handleUnselect(option.value)
|
|
}
|
|
}}
|
|
onMouseDown={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}}
|
|
onClick={() => handleUnselect(option.value)}
|
|
>
|
|
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
|
|
</button>
|
|
</Badge>
|
|
))}
|
|
<CommandPrimitive.Input
|
|
ref={inputRef}
|
|
value={inputValue}
|
|
onValueChange={setInputValue}
|
|
onBlur={() => setOpen(false)}
|
|
onFocus={() => setOpen(true)}
|
|
placeholder={selected.length === 0 ? placeholder : undefined}
|
|
className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="relative mt-2">
|
|
{open && options.length > 0 && (
|
|
<div className="absolute top-0 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
|
|
<CommandGroup className="h-full overflow-auto max-h-[200px]">
|
|
{options.map((option) => {
|
|
const isSelected = selected.includes(option.value)
|
|
return (
|
|
<CommandItem
|
|
key={option.value}
|
|
onMouseDown={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}}
|
|
onSelect={() => handleSelect(option.value)}
|
|
className={`flex items-center gap-2 ${isSelected ? "bg-muted" : ""}`}
|
|
>
|
|
<div
|
|
className={`border mr-2 flex h-4 w-4 items-center justify-center rounded-sm ${
|
|
isSelected
|
|
? "bg-primary border-primary text-primary-foreground"
|
|
: "opacity-50 [&_svg]:invisible"
|
|
}`}
|
|
>
|
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<span>{option.label}</span>
|
|
</CommandItem>
|
|
)
|
|
})}
|
|
</CommandGroup>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Command>
|
|
)
|
|
}
|