87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
import React, {useEffect, useRef, useState} from "react";
|
|
|
|
|
|
interface Option {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
interface MultiSelectProps {
|
|
label: string;
|
|
options: Option[];
|
|
selected: string[];
|
|
onChange: (selected: string[]) => void;
|
|
}
|
|
|
|
export const MultiSelect: React.FC<MultiSelectProps> = ({ label, options, selected, onChange }) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const toggleDropdown = () => setIsOpen(!isOpen);
|
|
|
|
// Close dropdown if clicked outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
// Toggle individual option
|
|
const handleOptionToggle = (value: string) => {
|
|
if (selected.includes(value)) {
|
|
onChange(selected.filter(item => item !== value));
|
|
} else {
|
|
onChange([...selected, value]);
|
|
}
|
|
};
|
|
|
|
// Display "All ..." if nothing is selected.
|
|
const buttonLabel =
|
|
selected.length === 0
|
|
? `All ${label}`
|
|
: selected
|
|
.map(val => {
|
|
const opt = options.find(o => o.value === val);
|
|
return opt ? opt.label : val;
|
|
})
|
|
.join(', ');
|
|
|
|
// Sort options so that selected items appear at the top
|
|
const sortedOptions = [...options].sort((a, b) => {
|
|
const aSelected = selected.includes(a.value);
|
|
const bSelected = selected.includes(b.value);
|
|
if (aSelected && !bSelected) return -1;
|
|
if (!aSelected && bSelected) return 1;
|
|
return 0;
|
|
});
|
|
|
|
return (
|
|
<div className="dropdown" ref={dropdownRef}>
|
|
<button onClick={toggleDropdown} className="btn m-1">
|
|
{buttonLabel}
|
|
</button>
|
|
{isOpen && (
|
|
<ul className="dropdown-content p-2 shadow bg-base-100 rounded-box w-52 max-h-60 overflow-y-auto flex flex-col">
|
|
{sortedOptions.map(opt => (
|
|
<li key={opt.value}>
|
|
<label className="cursor-pointer flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.includes(opt.value)}
|
|
onChange={() => handleOptionToggle(opt.value)}
|
|
className="checkbox checkbox-sm checkbox-primary mr-2"
|
|
/>
|
|
{opt.label}
|
|
</label>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|