remove unused MentionInput component
This commit is contained in:
@@ -1,164 +0,0 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
||||||
import { api } from '../lib/api'
|
|
||||||
import Avatar from './Avatar'
|
|
||||||
|
|
||||||
interface MentionUser {
|
|
||||||
id: string
|
|
||||||
username: string
|
|
||||||
avatarUrl: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value: string
|
|
||||||
onChange: (value: string) => void
|
|
||||||
placeholder?: string
|
|
||||||
rows?: number
|
|
||||||
ariaLabel?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MentionInput({ value, onChange, placeholder, rows = 3, ariaLabel }: Props) {
|
|
||||||
const ref = useRef<HTMLTextAreaElement>(null)
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [users, setUsers] = useState<MentionUser[]>([])
|
|
||||||
const [active, setActive] = useState(0)
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>()
|
|
||||||
|
|
||||||
const getMentionQuery = useCallback((): string | null => {
|
|
||||||
const ta = ref.current
|
|
||||||
if (!ta) return null
|
|
||||||
const pos = ta.selectionStart
|
|
||||||
const before = value.slice(0, pos)
|
|
||||||
const match = before.match(/@([a-zA-Z0-9_]{2,30})$/)
|
|
||||||
return match ? match[1] : null
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const q = getMentionQuery()
|
|
||||||
if (!q || q.length < 2) {
|
|
||||||
setUsers([])
|
|
||||||
setQuery('')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (q === query) return
|
|
||||||
setQuery(q)
|
|
||||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
||||||
debounceRef.current = setTimeout(() => {
|
|
||||||
api.get<{ users: MentionUser[] }>(`/users/search?q=${encodeURIComponent(q)}`)
|
|
||||||
.then((r) => {
|
|
||||||
setUsers(r.users)
|
|
||||||
setActive(0)
|
|
||||||
})
|
|
||||||
.catch(() => setUsers([]))
|
|
||||||
}, 200)
|
|
||||||
return () => { if (debounceRef.current) clearTimeout(debounceRef.current) }
|
|
||||||
}, [value, getMentionQuery])
|
|
||||||
|
|
||||||
const insertMention = (username: string) => {
|
|
||||||
const ta = ref.current
|
|
||||||
if (!ta) return
|
|
||||||
const pos = ta.selectionStart
|
|
||||||
const before = value.slice(0, pos)
|
|
||||||
const after = value.slice(pos)
|
|
||||||
const atIdx = before.lastIndexOf('@')
|
|
||||||
if (atIdx === -1) return
|
|
||||||
const newValue = before.slice(0, atIdx) + '@' + username + ' ' + after
|
|
||||||
onChange(newValue)
|
|
||||||
setUsers([])
|
|
||||||
setQuery('')
|
|
||||||
const newPos = atIdx + username.length + 2
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
ta.focus()
|
|
||||||
ta.setSelectionRange(newPos, newPos)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (users.length === 0) return
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault()
|
|
||||||
setActive((a) => (a + 1) % users.length)
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault()
|
|
||||||
setActive((a) => (a - 1 + users.length) % users.length)
|
|
||||||
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
||||||
if (users[active]) {
|
|
||||||
e.preventDefault()
|
|
||||||
insertMention(users[active].username)
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
setUsers([])
|
|
||||||
setQuery('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (e: MouseEvent) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
||||||
setUsers([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handler)
|
|
||||||
return () => document.removeEventListener('mousedown', handler)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
<textarea
|
|
||||||
ref={ref}
|
|
||||||
className="input w-full"
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder={placeholder}
|
|
||||||
rows={rows}
|
|
||||||
style={{ resize: 'vertical' }}
|
|
||||||
aria-label={ariaLabel || 'Comment'}
|
|
||||||
/>
|
|
||||||
{users.length > 0 && (
|
|
||||||
<div
|
|
||||||
ref={dropdownRef}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
bottom: -4,
|
|
||||||
transform: 'translateY(100%)',
|
|
||||||
zIndex: 50,
|
|
||||||
background: 'var(--surface)',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: 'var(--radius-md)',
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
||||||
minWidth: 200,
|
|
||||||
maxHeight: 240,
|
|
||||||
overflow: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{users.map((u, i) => (
|
|
||||||
<button
|
|
||||||
key={u.id}
|
|
||||||
onClick={() => insertMention(u.username)}
|
|
||||||
className="flex items-center gap-2 w-full px-3"
|
|
||||||
style={{
|
|
||||||
minHeight: 44,
|
|
||||||
background: i === active ? 'var(--surface-hover)' : 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 'var(--text-sm)',
|
|
||||||
color: 'var(--text)',
|
|
||||||
textAlign: 'left',
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setActive(i)}
|
|
||||||
>
|
|
||||||
<Avatar userId={u.id} name={u.username} avatarUrl={u.avatarUrl} size={22} />
|
|
||||||
<span>@{u.username}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user