import React, { useState, useRef, useEffect, useMemo } from 'react'; import { SearchIcon } from './Icons'; interface MultiSelectDropdownProps { label: string; selected: string[]; options: string[]; onChange: (newSelected: string[]) => void; className?: string; enableRangeSelect?: boolean; } const MultiSelectDropdown: React.FC = ({ label, selected, options, onChange, className, enableRangeSelect }) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [rangeInput, setRangeInput] = useState(''); const dropdownRef = useRef(null); const inputRef = useRef(null); 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); }, []); // Focus input when opening useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); } if (!isOpen) { setSearchTerm(''); // Reset search when closing } }, [isOpen]); // Prevent body scroll when dropdown is open on mobile useEffect(() => { if (isOpen && window.innerWidth < 768) { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; } }, [isOpen]); const filteredOptions = useMemo(() => { if (!searchTerm) return options; return options.filter(opt => opt.toLowerCase().includes(searchTerm.toLowerCase())); }, [options, searchTerm]); const toggleOption = (option: string) => { if (selected.includes(option)) { onChange(selected.filter((item) => item !== option)); } else { onChange([...selected, option]); } }; const handleSelectAll = () => { // If searching, only select/deselect visible options if (searchTerm) { const allFilteredSelected = filteredOptions.every(opt => selected.includes(opt)); if (allFilteredSelected) { // Deselect all filtered options onChange(selected.filter(item => !filteredOptions.includes(item))); } else { // Select all filtered options (add unique ones) const newSelected = Array.from(new Set([...selected, ...filteredOptions])); onChange(newSelected); } } else { // Standard behavior if (selected.length === options.length) { onChange([]); // Deselect all } else { onChange([...options]); // Select all } } }; const handleClear = () => { onChange([]); }; const handleRangeApply = () => { if (!rangeInput.trim()) return; const input = rangeInput.trim().toLowerCase().replace('w', ''); // Allow "W5" -> "5" let predicate: (num: number) => boolean = () => false; // Parse Input if (input.includes('-')) { const [start, end] = input.split('-').map(s => parseFloat(s.trim())); if (!isNaN(start) && !isNaN(end)) { predicate = (n) => n >= start && n <= end; } } else if (input.startsWith('<=')) { const val = parseFloat(input.substring(2).trim()); if (!isNaN(val)) predicate = (n) => n <= val; } else if (input.startsWith('>=')) { const val = parseFloat(input.substring(2).trim()); if (!isNaN(val)) predicate = (n) => n >= val; } else if (input.startsWith('<')) { const val = parseFloat(input.substring(1).trim()); if (!isNaN(val)) predicate = (n) => n < val; } else if (input.startsWith('>')) { const val = parseFloat(input.substring(1).trim()); if (!isNaN(val)) predicate = (n) => n > val; } else { // Exact match const val = parseFloat(input); if (!isNaN(val)) predicate = (n) => n === val; } // Apply to Options const newSelected = options.filter(opt => { // Extract number from option (e.g. "W1" -> 1, "2023" -> 2023) const num = parseFloat(opt.replace(/\D/g, '')); if (isNaN(num)) return false; return predicate(num); }); onChange(newSelected); setRangeInput(''); // Clear input after apply }; return (
{/* Mobile overlay backdrop */} {isOpen && (
setIsOpen(false)} /> )} {isOpen && (
{/* Mobile header with close */}

{label}

{/* Search Bar */}
setSearchTerm(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); if (filteredOptions.length > 0) { const allSelected = filteredOptions.every(opt => selected.includes(opt)); if (allSelected) { // Deselect all filtered onChange(selected.filter(item => !filteredOptions.includes(item))); } else { // Select all filtered onChange(Array.from(new Set([...selected, ...filteredOptions]))); } setIsOpen(false); setSearchTerm(''); } } }} />
{/* Range Filter (Optional) */} {enableRangeSelect && (
setRangeInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleRangeApply()} />
)}
{filteredOptions.map((opt) => ( ))} {filteredOptions.length === 0 &&
No matches found
}
)}
); }; export default MultiSelectDropdown;