Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m50s
144 lines
4.8 KiB
React
144 lines
4.8 KiB
React
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { IconHash } from '@tabler/icons-react';
|
|
|
|
function CommunityAutocompleteInput({
|
|
value,
|
|
onChange,
|
|
communities = [],
|
|
placeholder = '',
|
|
className = 'form-control',
|
|
onSelectMeta,
|
|
maxSuggestions = 8,
|
|
useShortValue = false, // если true, передавать только короткую часть community (120 вместо 65000:120)
|
|
}) {
|
|
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();
|
|
if (!q) return communities.slice(0, maxSuggestions);
|
|
const filtered = communities.filter((c) => {
|
|
const v = (c.value || '').toLowerCase();
|
|
const n = (c.name || '').toLowerCase();
|
|
const d = (c.description || '').toLowerCase();
|
|
const t = Array.isArray(c.tags) ? c.tags.join(' ').toLowerCase() : '';
|
|
return v.includes(q) || n.includes(q) || d.includes(q) || t.includes(q);
|
|
});
|
|
return filtered.slice(0, maxSuggestions);
|
|
}, [value, communities, 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) => {
|
|
// Если useShortValue=true, извлекаем только часть после двоеточия (если есть)
|
|
let valueToSet = item.value;
|
|
if (useShortValue && typeof item.value === 'string' && item.value.includes(':')) {
|
|
valueToSet = item.value.split(':')[1] || item.value;
|
|
}
|
|
onChange(valueToSet);
|
|
if (onSelectMeta) onSelectMeta(item);
|
|
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);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div ref={containerRef} className="position-relative" style={{ width: '100%' }}>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
className={className}
|
|
placeholder={placeholder}
|
|
value={value}
|
|
onChange={(e) => {
|
|
onChange(e.target.value);
|
|
setOpen(true);
|
|
}}
|
|
onFocus={() => setOpen(true)}
|
|
onKeyDown={handleKeyDown}
|
|
autoComplete="off"
|
|
/>
|
|
|
|
{open && suggestions.length > 0 && (
|
|
<div
|
|
className="dropdown-menu show"
|
|
style={{
|
|
display: 'block',
|
|
width: '100%',
|
|
maxHeight: 280,
|
|
overflowY: 'auto',
|
|
}}
|
|
>
|
|
{suggestions.map((s, idx) => {
|
|
const label = s.name ? `${s.value} - ${s.name}` : `${s.value}`;
|
|
const description = s.description || '';
|
|
const tags = Array.isArray(s.tags) ? s.tags : [];
|
|
return (
|
|
<button
|
|
type="button"
|
|
key={`${s.value}-${idx}`}
|
|
className={`dropdown-item${idx === activeIndex ? ' active' : ''}`}
|
|
onMouseDown={(e) => { e.preventDefault(); selectItem(s); }}
|
|
onMouseEnter={() => setActiveIndex(idx)}
|
|
>
|
|
<div className="d-flex align-items-start">
|
|
<span className="avatar me-2 bg-blue-lt text-blue border-0" style={{ width: 22, height: 22 }}>
|
|
<IconHash size={14} />
|
|
</span>
|
|
<div className="flex-fill text-start">
|
|
<div className="fw-medium">{label}</div>
|
|
{(description || tags.length > 0) && (
|
|
<div className="text-muted small text-truncate" style={{ maxWidth: '100%' }}>
|
|
{description}
|
|
{tags.length > 0 && (
|
|
<> ({tags.join(', ')})</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default CommunityAutocompleteInput;
|
|
|
|
|