170 lines
6.9 KiB
TypeScript
170 lines
6.9 KiB
TypeScript
import { useEffect, useRef, useState, type FC, type MouseEvent as ReactMouseEvent } from 'react'
|
||
import { ArrowDown2, CloseCircle, TickSquare } from 'iconsax-react'
|
||
import { clx } from '../helpers/utils'
|
||
import type { ItemsSelectType } from './Select'
|
||
|
||
type Props = {
|
||
className?: string
|
||
items: ItemsSelectType[]
|
||
value: string[]
|
||
onChange: (value: string[]) => void
|
||
onBlur?: () => void
|
||
error_text?: string
|
||
placeholder?: string
|
||
label?: string
|
||
disabled?: boolean
|
||
}
|
||
|
||
const MultiSelect: FC<Props> = ({
|
||
className,
|
||
items,
|
||
value,
|
||
onChange,
|
||
onBlur,
|
||
error_text,
|
||
placeholder = 'انتخاب کنید',
|
||
label,
|
||
disabled,
|
||
}) => {
|
||
const [isOpen, setIsOpen] = useState(false)
|
||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||
const selectedValues = value ?? []
|
||
|
||
useEffect(() => {
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
||
if (isOpen) {
|
||
setIsOpen(false)
|
||
onBlur?.()
|
||
}
|
||
}
|
||
}
|
||
document.addEventListener('mousedown', handleClickOutside)
|
||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||
}, [isOpen, onBlur])
|
||
|
||
const selectedItems = items.filter((item) =>
|
||
selectedValues.includes(String(item.value)),
|
||
)
|
||
|
||
const toggleValue = (itemValue: string) => {
|
||
if (selectedValues.includes(itemValue)) {
|
||
onChange(selectedValues.filter((v) => v !== itemValue))
|
||
} else {
|
||
onChange([...selectedValues, itemValue])
|
||
}
|
||
}
|
||
|
||
const handleClear = (e: ReactMouseEvent) => {
|
||
e.stopPropagation()
|
||
onChange([])
|
||
onBlur?.()
|
||
}
|
||
|
||
const displayText =
|
||
selectedItems.length === 0
|
||
? placeholder
|
||
: selectedItems.length <= 2
|
||
? selectedItems.map((item) => item.label).join('، ')
|
||
: `${selectedItems.length} مورد انتخاب شده`
|
||
|
||
return (
|
||
<div className={clx('w-full', className)} ref={wrapperRef}>
|
||
{label && (
|
||
<label className="text-sm text-primary-content">{label}</label>
|
||
)}
|
||
<div className="relative">
|
||
<button
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => setIsOpen((open) => !open)}
|
||
className={clx(
|
||
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors text-right',
|
||
selectedItems.length === 0 && 'text-[#8c90a3]',
|
||
selectedItems.length > 0 && 'pl-10',
|
||
label && 'mt-1',
|
||
disabled && 'opacity-50 cursor-not-allowed',
|
||
)}
|
||
>
|
||
<span className="block truncate pr-6">{displayText}</span>
|
||
<ArrowDown2
|
||
size={16}
|
||
color="#8c90a3"
|
||
className="absolute z-0 top-3 left-2 pointer-events-none"
|
||
/>
|
||
</button>
|
||
{selectedItems.length > 0 && (
|
||
<button
|
||
type="button"
|
||
onClick={handleClear}
|
||
className="absolute top-0 bottom-0 my-auto left-8 z-10 flex items-center"
|
||
aria-label="پاک کردن"
|
||
>
|
||
<CloseCircle size={16} color="#8C90A3" />
|
||
</button>
|
||
)}
|
||
{isOpen && (
|
||
<div className="absolute z-20 top-full left-0 right-0 mt-1 bg-white border border-border rounded-[10px] shadow-lg max-h-60 overflow-auto">
|
||
{items.length === 0 ? (
|
||
<div className="px-3 py-2.5 text-xs text-[#8c90a3]">
|
||
موردی برای انتخاب نیست
|
||
</div>
|
||
) : (
|
||
<ul className="py-1">
|
||
{items.map((item) => {
|
||
const itemValue = String(item.value)
|
||
const isSelected = selectedValues.includes(itemValue)
|
||
return (
|
||
<li key={itemValue}>
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleValue(itemValue)}
|
||
className={clx(
|
||
'w-full text-right px-3 py-2 text-sm hover:bg-muted transition-colors flex items-center justify-between gap-2',
|
||
isSelected && 'bg-blue-50 text-[#0037FF]',
|
||
)}
|
||
>
|
||
<span className="truncate">{item.label}</span>
|
||
{isSelected && (
|
||
<TickSquare size={16} color="#0037FF" className="shrink-0" />
|
||
)}
|
||
</button>
|
||
</li>
|
||
)
|
||
})}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{selectedItems.length > 0 && (
|
||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||
{selectedItems.map((item) => (
|
||
<span
|
||
key={String(item.value)}
|
||
className="inline-flex items-center gap-1 rounded-lg border border-border bg-[#FBFCFF] px-2 py-1 text-xs text-[#1F2937]"
|
||
>
|
||
{item.label}
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleValue(String(item.value))}
|
||
aria-label={`حذف ${item.label}`}
|
||
className="text-[#8c90a3] hover:text-red-500"
|
||
>
|
||
<CloseCircle size={12} color="currentColor" />
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
{error_text ? (
|
||
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
||
{error_text}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default MultiSelect
|