|
|
|
@@ -25,8 +25,8 @@ const getTokenAt = (value: string, pos: number) => { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const replaceToken = (value: string, start: number, end: number, text: string) => ( |
|
|
|
`${ value.slice (0, start) }${ text }${ value.slice (end) }`) |
|
|
|
const replaceToken = (value: string, start: number, end: number, text: string) => |
|
|
|
`${ value.slice (0, start) }${ text }${ value.slice (end) }` |
|
|
|
|
|
|
|
|
|
|
|
type Props = { |
|
|
|
@@ -38,16 +38,17 @@ export default (({ tags, setTags }: Props) => { |
|
|
|
const ref = useRef<HTMLTextAreaElement> (null) |
|
|
|
|
|
|
|
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 }) |
|
|
|
const [focused, setFocused] = useState (false) |
|
|
|
const [suggestions, setSuggestions] = useState<Tag[]> ([]) |
|
|
|
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false) |
|
|
|
|
|
|
|
const handleTagSelect = (tag: Tag) => { |
|
|
|
setSuggestionsVsbl (false) |
|
|
|
const textarea = ref.current! |
|
|
|
const newValue = replaceToken (tags, bounds.start, bounds.end, tag.name) |
|
|
|
const newValue = replaceToken (tags, bounds.start, bounds.end, tag.name + ' ') |
|
|
|
setTags (newValue) |
|
|
|
requestAnimationFrame (async () => { |
|
|
|
const p = bounds.start + tag.name.length |
|
|
|
const p = bounds.start + tag.name.length + 1 |
|
|
|
textarea.selectionStart = textarea.selectionEnd = p |
|
|
|
textarea.focus () |
|
|
|
await recompute (p, newValue) |
|
|
|
@@ -56,14 +57,21 @@ export default (({ tags, setTags }: Props) => { |
|
|
|
|
|
|
|
const recompute = async (pos: number, v: string = tags) => { |
|
|
|
const { start, end, token } = getTokenAt (v, pos) |
|
|
|
if (!(token.trim ())) |
|
|
|
{ |
|
|
|
setSuggestionsVsbl (false) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
setBounds ({ start, end }) |
|
|
|
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q: token } }) |
|
|
|
|
|
|
|
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q: token, nico: '0' } }) |
|
|
|
setSuggestions (data.filter (t => t.postCount > 0)) |
|
|
|
setSuggestionsVsbl (suggestions.length > 0) |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<div> |
|
|
|
<div className="relative w-full"> |
|
|
|
<Label>タグ</Label> |
|
|
|
<TextArea |
|
|
|
ref={ref} |
|
|
|
@@ -72,11 +80,20 @@ export default (({ tags, setTags }: Props) => { |
|
|
|
onSelect={async (ev: SyntheticEvent<HTMLTextAreaElement>) => { |
|
|
|
const pos = (ev.target as HTMLTextAreaElement).selectionStart |
|
|
|
await recompute (pos) |
|
|
|
}} |
|
|
|
onFocus={() => { |
|
|
|
setFocused (true) |
|
|
|
}} |
|
|
|
onBlur={() => { |
|
|
|
setFocused (false) |
|
|
|
setSuggestionsVsbl (false) |
|
|
|
}}/> |
|
|
|
<TagSearchBox suggestions={suggestionsVsbl && suggestions.length |
|
|
|
{focused && ( |
|
|
|
<TagSearchBox |
|
|
|
suggestions={suggestionsVsbl && suggestions.length > 0 |
|
|
|
? suggestions |
|
|
|
: [] as Tag[]} |
|
|
|
activeIndex={-1} |
|
|
|
onSelect={handleTagSelect}/> |
|
|
|
onSelect={handleTagSelect}/>)} |
|
|
|
</div>) |
|
|
|
}) satisfies FC<Props> |