car_mms/app/components/ui/MultiSelect.tsx
2025-09-11 14:22:27 +03:00

171 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef, useEffect } from "react";
import { Text } from "./Text";
interface Option {
value: string | number;
label: string;
}
interface MultiSelectProps {
name?: string;
label?: string;
options: Option[];
value: (string | number)[];
onChange: (values: (string | number)[]) => void;
placeholder?: string;
error?: string;
required?: boolean;
disabled?: boolean;
className?: string;
}
export function MultiSelect({
name,
label,
options,
value,
onChange,
placeholder = "اختر العناصر...",
error,
required,
disabled,
className = ""
}: MultiSelectProps) {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleToggleOption = (optionValue: string | number) => {
if (disabled) return;
const newValue = value.includes(optionValue)
? value.filter(v => v !== optionValue)
: [...value, optionValue];
onChange(newValue);
};
const handleRemoveItem = (optionValue: string | number) => {
if (disabled) return;
onChange(value.filter(v => v !== optionValue));
};
const selectedOptions = options.filter(option => value.includes(option.value));
const displayText = selectedOptions.length > 0
? `تم اختيار ${selectedOptions.length} عنصر`
: placeholder;
return (
<div className={`relative ${className}`} ref={containerRef}>
{label && (
<label className="block text-sm font-medium text-gray-700 mb-2">
{label}
{required && <span className="text-red-500 mr-1">*</span>}
</label>
)}
{/* Hidden input for form submission */}
{name && (
<input
type="hidden"
name={name}
value={JSON.stringify(value)}
/>
)}
{/* Selected items display */}
{selectedOptions.length > 0 && (
<div className="mb-2 flex flex-wrap gap-2">
{selectedOptions.map((option) => (
<span
key={option.value}
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full"
>
{option.label}
{!disabled && (
<button
type="button"
onClick={() => handleRemoveItem(option.value)}
className="mr-1 text-blue-600 hover:text-blue-800"
>
×
</button>
)}
</span>
))}
</div>
)}
{/* Dropdown trigger */}
<div
className={`w-full px-3 py-2 border rounded-lg shadow-sm cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white ${
error
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300'
} ${disabled ? 'bg-gray-50 cursor-not-allowed' : ''}`}
onClick={() => !disabled && setIsOpen(!isOpen)}
>
<div className="flex items-center justify-between">
<span className={selectedOptions.length > 0 ? 'text-gray-900' : 'text-gray-500'}>
{displayText}
</span>
<svg
className={`h-5 w-5 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
viewBox="0 0 20 20"
fill="currentColor"
>
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
</div>
{/* Dropdown menu */}
{isOpen && (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-auto">
{options.length === 0 ? (
<div className="px-3 py-2 text-gray-500 text-sm">
لا توجد خيارات متاحة
</div>
) : (
options.map((option) => {
const isSelected = value.includes(option.value);
return (
<div
key={option.value}
className={`px-3 py-2 cursor-pointer hover:bg-gray-50 flex items-center justify-between ${
isSelected ? 'bg-blue-50 text-blue-700' : 'text-gray-900'
}`}
onClick={() => handleToggleOption(option.value)}
>
<span>{option.label}</span>
{isSelected && (
<svg className="h-4 w-4 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</div>
);
})
)}
</div>
)}
{error && (
<Text size="sm" color="error" className="mt-1">
{error}
</Text>
)}
</div>
);
}