249 lines
8.9 KiB
TypeScript
249 lines
8.9 KiB
TypeScript
/**
|
|
* Селектор предметов по названию (как SubjectSelect, для прогресса ученика).
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
export interface SubjectNameOption {
|
|
subject: string;
|
|
}
|
|
|
|
interface SubjectNameSelectProps {
|
|
options: SubjectNameOption[];
|
|
value: string | null;
|
|
onChange: (value: string | null) => void;
|
|
disabled?: boolean;
|
|
placeholder?: string;
|
|
}
|
|
|
|
export const SubjectNameSelect: React.FC<SubjectNameSelectProps> = ({
|
|
options,
|
|
value,
|
|
onChange,
|
|
disabled = false,
|
|
placeholder = 'Выберите предмет',
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const filtered = options.filter((o) =>
|
|
o.subject.toLowerCase().includes(searchQuery.toLowerCase().trim())
|
|
);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
setIsOpen(false);
|
|
setSearchQuery('');
|
|
}
|
|
};
|
|
if (isOpen) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
setTimeout(() => searchInputRef.current?.focus(), 0);
|
|
}
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, [isOpen]);
|
|
|
|
const selected = options.find((o) => o.subject === value);
|
|
|
|
return (
|
|
<div ref={containerRef} style={{ position: 'relative', width: '100%', minWidth: 160 }}>
|
|
<button
|
|
type="button"
|
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
disabled={disabled}
|
|
style={{
|
|
width: '100%',
|
|
padding: '12px 16px',
|
|
fontSize: '16px',
|
|
color: selected ? 'var(--md-sys-color-on-surface)' : 'var(--md-sys-color-on-surface-variant)',
|
|
background: 'var(--md-sys-color-surface)',
|
|
border: `1px solid ${isOpen ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'}`,
|
|
borderWidth: isOpen ? 2 : 1,
|
|
borderRadius: 4,
|
|
fontFamily: 'inherit',
|
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
outline: 'none',
|
|
transition: 'all 0.2s ease',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 12,
|
|
textAlign: 'left',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0 }}>
|
|
{selected ? (
|
|
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{selected.subject}
|
|
</span>
|
|
) : (
|
|
<span>{placeholder}</span>
|
|
)}
|
|
</div>
|
|
<svg
|
|
width={20}
|
|
height={20}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth={2}
|
|
style={{ flexShrink: 0, transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s ease' }}
|
|
>
|
|
<polyline points="6 9 12 15 18 9" />
|
|
</svg>
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: 'calc(100% + 4px)',
|
|
left: 0,
|
|
right: 0,
|
|
background: 'var(--md-sys-color-surface)',
|
|
border: '1px solid var(--md-sys-color-outline-variant)',
|
|
borderRadius: 12,
|
|
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
|
|
zIndex: 1000,
|
|
maxHeight: 320,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div style={{ padding: 12, borderBottom: '1px solid var(--md-sys-color-outline-variant)' }}>
|
|
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
<svg
|
|
width={20}
|
|
height={20}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="var(--md-sys-color-on-surface-variant)"
|
|
strokeWidth={2}
|
|
style={{ position: 'absolute', left: 12, pointerEvents: 'none' }}
|
|
>
|
|
<circle cx="11" cy="11" r="8" />
|
|
<path d="m21 21-4.35-4.35" />
|
|
</svg>
|
|
<input
|
|
ref={searchInputRef}
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Поиск предмета..."
|
|
style={{
|
|
width: '100%',
|
|
padding: '8px 12px 8px 40px',
|
|
fontSize: 14,
|
|
color: 'var(--md-sys-color-on-surface)',
|
|
background: 'var(--md-sys-color-surface-variant)',
|
|
border: 'none',
|
|
borderRadius: 8,
|
|
fontFamily: 'inherit',
|
|
outline: 'none',
|
|
}}
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setSearchQuery('')}
|
|
style={{
|
|
position: 'absolute',
|
|
right: 8,
|
|
width: 24,
|
|
height: 24,
|
|
padding: 0,
|
|
background: 'transparent',
|
|
border: 'none',
|
|
borderRadius: '50%',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: 'var(--md-sys-color-on-surface-variant)',
|
|
}}
|
|
>
|
|
<svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
<line x1={18} y1={6} x2={6} y2={18} />
|
|
<line x1={6} y1={6} x2={18} y2={18} />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div style={{ overflowY: 'auto', flex: 1 }}>
|
|
{filtered.length === 0 ? (
|
|
<div style={{ padding: 24, textAlign: 'center', color: 'var(--md-sys-color-on-surface-variant)', fontSize: 14 }}>
|
|
{searchQuery ? 'Предметы не найдены' : 'Нет доступных предметов'}
|
|
</div>
|
|
) : (
|
|
filtered.map((o) => (
|
|
<button
|
|
key={o.subject}
|
|
type="button"
|
|
onClick={() => {
|
|
onChange(o.subject);
|
|
setIsOpen(false);
|
|
setSearchQuery('');
|
|
}}
|
|
style={{
|
|
width: '100%',
|
|
padding: '12px 16px',
|
|
background: value === o.subject ? 'var(--md-sys-color-secondary-container)' : 'transparent',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
transition: 'background 0.2s ease',
|
|
textAlign: 'left',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (value !== o.subject) e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (value !== o.subject) e.currentTarget.style.background = 'transparent';
|
|
}}
|
|
>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div
|
|
style={{
|
|
fontSize: 14,
|
|
fontWeight: 500,
|
|
color: 'var(--md-sys-color-on-surface)',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{o.subject}
|
|
</div>
|
|
</div>
|
|
{value === o.subject && (
|
|
<svg
|
|
width={20}
|
|
height={20}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="var(--md-sys-color-primary)"
|
|
strokeWidth={2}
|
|
style={{ flexShrink: 0 }}
|
|
>
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|