#206 タグ補完追加
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
// TODO: TagSearch と共通化する.
|
||||||
|
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
import TagSearchBox from '@/components/TagSearchBox'
|
import TagSearchBox from '@/components/TagSearchBox'
|
||||||
@@ -81,9 +83,7 @@ export default (({ tags, setTags }: Props) => {
|
|||||||
const pos = (ev.target as HTMLTextAreaElement).selectionStart
|
const pos = (ev.target as HTMLTextAreaElement).selectionStart
|
||||||
await recompute (pos)
|
await recompute (pos)
|
||||||
}}
|
}}
|
||||||
onFocus={() => {
|
onFocus={() => setFocused (true)}
|
||||||
setFocused (true)
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setFocused (false)
|
setFocused (false)
|
||||||
setSuggestionsVsbl (false)
|
setSuggestionsVsbl (false)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// TODO: タグ入力系すべてに同様の処理あるため共通化する.
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,20 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import TagLink from '@/components/TagLink'
|
import TagLink from '@/components/TagLink'
|
||||||
|
import TagSearchBox from '@/components/TagSearchBox'
|
||||||
import DateTimeField from '@/components/common/DateTimeField'
|
import DateTimeField from '@/components/common/DateTimeField'
|
||||||
import Label from '@/components/common/Label'
|
import Label from '@/components/common/Label'
|
||||||
import PageTitle from '@/components/common/PageTitle'
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import Pagination from '@/components/common/Pagination'
|
import Pagination from '@/components/common/Pagination'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
|
import { apiGet } from '@/lib/api'
|
||||||
import { fetchPosts } from '@/lib/posts'
|
import { fetchPosts } from '@/lib/posts'
|
||||||
import { postsKeys } from '@/lib/queryKeys'
|
import { postsKeys } from '@/lib/queryKeys'
|
||||||
|
|
||||||
import type { FC, FormEvent } from 'react'
|
import type { FC, ChangeEvent, FormEvent, KeyboardEvent } from 'react'
|
||||||
|
|
||||||
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
|
const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
|
||||||
@@ -46,11 +50,14 @@ export default (() => {
|
|||||||
const qUpdatedFrom = query.get ('updated_from')
|
const qUpdatedFrom = query.get ('updated_from')
|
||||||
const qUpdatedTo = query.get ('updated_to')
|
const qUpdatedTo = query.get ('updated_to')
|
||||||
|
|
||||||
|
const [activeIndex, setActiveIndex] = useState (-1)
|
||||||
const [createdFrom, setCreatedFrom] = useState (qCreatedFrom)
|
const [createdFrom, setCreatedFrom] = useState (qCreatedFrom)
|
||||||
const [createdTo, setCreatedTo] = useState (qCreatedTo)
|
const [createdTo, setCreatedTo] = useState (qCreatedTo)
|
||||||
const [matchType, setMatchType] = useState (qMatch ?? 'all')
|
const [matchType, setMatchType] = useState (qMatch ?? 'all')
|
||||||
const [originalCreatedFrom, setOriginalCreatedFrom] = useState (qOriginalCreatedFrom)
|
const [originalCreatedFrom, setOriginalCreatedFrom] = useState (qOriginalCreatedFrom)
|
||||||
const [originalCreatedTo, setOriginalCreatedTo] = useState (qOriginalCreatedTo)
|
const [originalCreatedTo, setOriginalCreatedTo] = useState (qOriginalCreatedTo)
|
||||||
|
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||||
|
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
||||||
const [tagsStr, setTagsStr] = useState (qTags)
|
const [tagsStr, setTagsStr] = useState (qTags)
|
||||||
const [title, setTitle] = useState (qTitle ?? '')
|
const [title, setTitle] = useState (qTitle ?? '')
|
||||||
const [updatedFrom, setUpdatedFrom] = useState (qUpdatedFrom)
|
const [updatedFrom, setUpdatedFrom] = useState (qUpdatedFrom)
|
||||||
@@ -88,6 +95,58 @@ export default (() => {
|
|||||||
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
||||||
}, [location.search])
|
}, [location.search])
|
||||||
|
|
||||||
|
// TODO: TagSearch からのコピペのため,共通化を考へる.
|
||||||
|
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTagsStr (ev.target.value)
|
||||||
|
|
||||||
|
const q = ev.target.value.trim ().split (' ').at (-1)
|
||||||
|
if (!(q))
|
||||||
|
{
|
||||||
|
setSuggestions ([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q } })
|
||||||
|
setSuggestions (data.filter (t => t.postCount > 0))
|
||||||
|
if (suggestions.length > 0)
|
||||||
|
setSuggestionsVsbl (true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: TagSearch からのコピペのため,共通化を考へる.
|
||||||
|
const handleKeyDown = (ev: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
switch (ev.key)
|
||||||
|
{
|
||||||
|
case 'ArrowDown':
|
||||||
|
ev.preventDefault ()
|
||||||
|
setActiveIndex (i => Math.min (i + 1, suggestions.length - 1))
|
||||||
|
setSuggestionsVsbl (true)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'ArrowUp':
|
||||||
|
ev.preventDefault ()
|
||||||
|
setActiveIndex (i => Math.max (i - 1, -1))
|
||||||
|
setSuggestionsVsbl (true)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'Enter':
|
||||||
|
if (activeIndex < 0)
|
||||||
|
break
|
||||||
|
ev.preventDefault ()
|
||||||
|
const selected = suggestions[activeIndex]
|
||||||
|
selected && handleTagSelect (selected)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'Escape':
|
||||||
|
ev.preventDefault ()
|
||||||
|
setSuggestionsVsbl (false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (ev.key === 'Enter' && (!(suggestionsVsbl) || activeIndex < 0))
|
||||||
|
{
|
||||||
|
setSuggestionsVsbl (false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
const qs = new URLSearchParams ()
|
const qs = new URLSearchParams ()
|
||||||
setIf (qs, 'tags', tagsStr)
|
setIf (qs, 'tags', tagsStr)
|
||||||
@@ -104,6 +163,15 @@ export default (() => {
|
|||||||
navigate (`${ location.pathname }?${ qs.toString () }`)
|
navigate (`${ location.pathname }?${ qs.toString () }`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: TagSearch からのコピペのため,共通化を考へる.
|
||||||
|
const handleTagSelect = (tag: Tag) => {
|
||||||
|
const parts = tagsStr.split (' ')
|
||||||
|
parts[parts.length - 1] = tag.name
|
||||||
|
setTagsStr (parts.join (' ') + ' ')
|
||||||
|
setSuggestions ([])
|
||||||
|
setActiveIndex (-1)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearch = (e: FormEvent) => {
|
const handleSearch = (e: FormEvent) => {
|
||||||
e.preventDefault ()
|
e.preventDefault ()
|
||||||
search ()
|
search ()
|
||||||
@@ -140,13 +208,21 @@ export default (() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* タグ */}
|
{/* タグ */}
|
||||||
<div>
|
<div className="relative">
|
||||||
<Label>タグ</Label>
|
<Label>タグ</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={tagsStr}
|
value={tagsStr}
|
||||||
onChange={e => setTagsStr (e.target.value)}
|
onChange={whenChanged}
|
||||||
|
onFocus={() => setSuggestionsVsbl (true)}
|
||||||
|
onBlur={() => setSuggestionsVsbl (false)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
className="w-full border p-2 rounded"/>
|
className="w-full border p-2 rounded"/>
|
||||||
|
<TagSearchBox
|
||||||
|
suggestions={
|
||||||
|
suggestionsVsbl && suggestions.length > 0 ? suggestions : [] as Tag[]}
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
onSelect={handleTagSelect}/>
|
||||||
<fieldset className="w-full my-2">
|
<fieldset className="w-full my-2">
|
||||||
<label>検索区分:</label>
|
<label>検索区分:</label>
|
||||||
<label className="mx-2">
|
<label className="mx-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user