Browse Source

#206 タグ補完追加

feature/206
みてるぞ 4 days ago
parent
commit
f8d2d753fe
3 changed files with 84 additions and 6 deletions
  1. +3
    -3
      frontend/src/components/PostFormTagsArea.tsx
  2. +2
    -0
      frontend/src/components/TagSearch.tsx
  3. +79
    -3
      frontend/src/pages/posts/PostSearchPage.tsx

+ 3
- 3
frontend/src/components/PostFormTagsArea.tsx View File

@@ -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={() => {
setFocused (true)
}}
onFocus={() => setFocused (true)}
onBlur={() => { onBlur={() => {
setFocused (false) setFocused (false)
setSuggestionsVsbl (false) setSuggestionsVsbl (false)


+ 2
- 0
frontend/src/components/TagSearch.tsx View File

@@ -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'




+ 79
- 3
frontend/src/pages/posts/PostSearchPage.tsx View File

@@ -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">


Loading…
Cancel
Save