import { useEffect, useMemo, useRef, useState } from 'react'; import { IconServer } from '@tabler/icons-react'; /** * Красивый selector серверов с автодополнением. * Похож по UX на CommunityAutocompleteInput, но заточен под поля серверов. */ function ServerAutocompleteInput({ value, onChange, servers = [], placeholder = '', className = 'form-control', onSelectMeta, maxSuggestions = 12, }) { const containerRef = useRef(null); const inputRef = useRef(null); const [open, setOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const suggestions = useMemo(() => { const q = String(value || '').toLowerCase(); const list = (servers || []).map((s) => { const id = s.id || s.ip; const labelIp = s.ip || '—'; const labelDns = s.dns || ''; const labelProvider = s.provider || ''; const label = labelDns && labelProvider ? `${labelIp} (${labelDns}) – ${labelProvider}` : labelDns ? `${labelIp} (${labelDns})` : labelProvider ? `${labelIp} – ${labelProvider}` : `${labelIp}`; return { raw: s, value: id, label, search: [ id, s.ip, s.extIp, s.internalIp, s.dns, s.provider, s.country, s.gateway, ] .filter(Boolean) .join(' ') .toLowerCase(), }; }); if (!q) return list.slice(0, maxSuggestions); const filtered = list.filter((item) => item.search.includes(q)); return filtered.slice(0, maxSuggestions); }, [value, servers, maxSuggestions]); useEffect(() => { const handleOutside = (e) => { if (!containerRef.current) return; if (!containerRef.current.contains(e.target)) setOpen(false); }; document.addEventListener('click', handleOutside); return () => document.removeEventListener('click', handleOutside); }, []); const selectItem = (item) => { onChange(item.value); if (onSelectMeta) onSelectMeta(item.raw); setOpen(false); setActiveIndex(-1); if (inputRef.current) inputRef.current.focus(); }; const handleKeyDown = (e) => { if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { setOpen(true); return; } if (!open) return; if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex((prev) => Math.min(prev + 1, suggestions.length - 1)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex((prev) => Math.max(prev - 1, 0)); } else if (e.key === 'Enter') { if (activeIndex >= 0 && activeIndex < suggestions.length) { e.preventDefault(); selectItem(suggestions[activeIndex]); } } else if (e.key === 'Escape') { setOpen(false); setActiveIndex(-1); } }; // Текст в input: ищем label по выбранному serverId const displayValue = useMemo(() => { if (!value) return ''; const found = (servers || []).find((s) => (s.id || s.ip) === value); if (!found) return ''; const ip = found.ip || '—'; const dns = found.dns || ''; const provider = found.provider || ''; if (dns && provider) return `${ip} (${dns}) – ${provider}`; if (dns) return `${ip} (${dns})`; if (provider) return `${ip} – ${provider}`; return ip; }, [value, servers]); return (