uup
This commit is contained in:
171
app/components/ui/MultiSelect.tsx
Normal file
171
app/components/ui/MultiSelect.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user