user and territory autocomplete in search bar (#2217)
* autocomplete in the search bar * update some naming conventions * create dual autocomplete --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
aebba27c57
commit
524b1b97f3
@ -228,6 +228,86 @@ function useEntityAutocomplete ({
|
||||
}
|
||||
}
|
||||
|
||||
export function useDualAutocomplete ({ meta, helpers, innerRef, setSelectionRange }) {
|
||||
const userAutocomplete = useEntityAutocomplete({
|
||||
prefix: '@',
|
||||
meta,
|
||||
helpers,
|
||||
innerRef,
|
||||
setSelectionRange,
|
||||
SuggestComponent: UserSuggest
|
||||
})
|
||||
|
||||
const territoryAutocomplete = useEntityAutocomplete({
|
||||
prefix: '~',
|
||||
meta,
|
||||
helpers,
|
||||
innerRef,
|
||||
setSelectionRange,
|
||||
SuggestComponent: TerritorySuggest
|
||||
})
|
||||
|
||||
const handleTextChange = useCallback((e) => {
|
||||
// Try to match user mentions first, then territories
|
||||
if (!userAutocomplete.handleTextChange(e)) {
|
||||
territoryAutocomplete.handleTextChange(e)
|
||||
}
|
||||
}, [userAutocomplete, territoryAutocomplete])
|
||||
|
||||
const handleKeyDown = useCallback((e, userOnKeyDown, territoryOnKeyDown) => {
|
||||
const metaOrCtrl = e.metaKey || e.ctrlKey
|
||||
if (!metaOrCtrl) {
|
||||
if (userAutocomplete.entityData) {
|
||||
return userOnKeyDown(e)
|
||||
} else if (territoryAutocomplete.entityData) {
|
||||
return territoryOnKeyDown(e)
|
||||
}
|
||||
}
|
||||
return false // Didn't handle the event
|
||||
}, [userAutocomplete.entityData, territoryAutocomplete.entityData])
|
||||
|
||||
const handleBlur = useCallback((resetUserSuggestions, resetTerritorySuggestions) => {
|
||||
setTimeout(resetUserSuggestions, 500)
|
||||
setTimeout(resetTerritorySuggestions, 500)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
userAutocomplete,
|
||||
territoryAutocomplete,
|
||||
handleTextChange,
|
||||
handleKeyDown,
|
||||
handleBlur
|
||||
}
|
||||
}
|
||||
|
||||
export function DualAutocompleteWrapper ({
|
||||
userAutocomplete,
|
||||
territoryAutocomplete,
|
||||
children
|
||||
}) {
|
||||
return (
|
||||
<UserSuggest
|
||||
query={userAutocomplete.entityData?.query}
|
||||
onSelect={userAutocomplete.handleSelect}
|
||||
dropdownStyle={userAutocomplete.entityData?.style}
|
||||
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions: resetUserSuggestions }) => (
|
||||
<TerritorySuggest
|
||||
query={territoryAutocomplete.entityData?.query}
|
||||
onSelect={territoryAutocomplete.handleSelect}
|
||||
dropdownStyle={territoryAutocomplete.entityData?.style}
|
||||
>{({ onKeyDown: territorySuggestOnKeyDown, resetSuggestions: resetTerritorySuggestions }) =>
|
||||
children({
|
||||
userSuggestOnKeyDown,
|
||||
territorySuggestOnKeyDown,
|
||||
resetUserSuggestions,
|
||||
resetTerritorySuggestions
|
||||
})}
|
||||
</TerritorySuggest>
|
||||
)}
|
||||
</UserSuggest>
|
||||
)
|
||||
}
|
||||
|
||||
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) {
|
||||
const [tab, setTab] = useState('write')
|
||||
const [, meta, helpers] = useField(props)
|
||||
@ -287,22 +367,11 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
||||
}
|
||||
}, [innerRef, selectionRange.start, selectionRange.end])
|
||||
|
||||
const userAutocomplete = useEntityAutocomplete({
|
||||
prefix: '@',
|
||||
const { userAutocomplete, territoryAutocomplete, handleTextChange, handleKeyDown, handleBlur } = useDualAutocomplete({
|
||||
meta,
|
||||
helpers,
|
||||
innerRef,
|
||||
setSelectionRange,
|
||||
SuggestComponent: UserSuggest
|
||||
})
|
||||
|
||||
const territoryAutocomplete = useEntityAutocomplete({
|
||||
prefix: '~',
|
||||
meta,
|
||||
helpers,
|
||||
innerRef,
|
||||
setSelectionRange,
|
||||
SuggestComponent: TerritorySuggest
|
||||
setSelectionRange
|
||||
})
|
||||
|
||||
const uploadFeesUpdate = useDebounceCallback(
|
||||
@ -313,56 +382,9 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
||||
|
||||
const onChangeInner = useCallback((formik, e) => {
|
||||
if (onChange) onChange(formik, e)
|
||||
// check for mentions and territory suggestions
|
||||
uploadFeesUpdate(e.target.value)
|
||||
|
||||
// Try to match user mentions first, then territories
|
||||
if (!userAutocomplete.handleTextChange(e)) {
|
||||
territoryAutocomplete.handleTextChange(e)
|
||||
}
|
||||
}, [onChange, uploadFeesUpdate, userAutocomplete, territoryAutocomplete])
|
||||
|
||||
const onKeyDownInner = useCallback((userSuggestOnKeyDown, territorySuggestOnKeyDown) => {
|
||||
return (e) => {
|
||||
const metaOrCtrl = e.metaKey || e.ctrlKey
|
||||
if (metaOrCtrl) {
|
||||
if (e.key === 'k') {
|
||||
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
|
||||
e.preventDefault()
|
||||
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
if (e.key === 'b') {
|
||||
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
|
||||
e.preventDefault()
|
||||
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
if (e.key === 'i') {
|
||||
// some browsers might use CTRL+I to do something else so prevent that behavior too
|
||||
e.preventDefault()
|
||||
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
if (e.key === 'u') {
|
||||
// some browsers might use CTRL+U to do something else so prevent that behavior too
|
||||
e.preventDefault()
|
||||
imageUploadRef.current?.click()
|
||||
}
|
||||
if (e.key === 'Tab' && e.altKey) {
|
||||
e.preventDefault()
|
||||
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
}
|
||||
|
||||
if (!metaOrCtrl) {
|
||||
if (userAutocomplete.entityData) {
|
||||
userSuggestOnKeyDown(e)
|
||||
} else if (territoryAutocomplete.entityData) {
|
||||
territorySuggestOnKeyDown(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (onKeyDown) onKeyDown(e)
|
||||
}
|
||||
}, [innerRef, helpers?.setValue, setSelectionRange, onKeyDown, userAutocomplete.entityData, territoryAutocomplete.entityData])
|
||||
handleTextChange(e)
|
||||
}, [onChange, uploadFeesUpdate, handleTextChange])
|
||||
|
||||
const onPaste = useCallback((event) => {
|
||||
const items = event.clipboardData.items
|
||||
@ -406,6 +428,44 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
||||
setDragStyle(null)
|
||||
}, [setDragStyle])
|
||||
|
||||
const onKeyDownInner = useCallback((userSuggestOnKeyDown, territorySuggestOnKeyDown) => {
|
||||
return (e) => {
|
||||
const metaOrCtrl = e.metaKey || e.ctrlKey
|
||||
|
||||
// Handle markdown shortcuts first
|
||||
if (metaOrCtrl) {
|
||||
if (e.key === 'k') {
|
||||
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
|
||||
e.preventDefault()
|
||||
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
if (e.key === 'b') {
|
||||
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
|
||||
e.preventDefault()
|
||||
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
if (e.key === 'i') {
|
||||
// some browsers might use CTRL+I to do something else so prevent that behavior too
|
||||
e.preventDefault()
|
||||
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
if (e.key === 'u') {
|
||||
// some browsers might use CTRL+U to do something else so prevent that behavior too
|
||||
e.preventDefault()
|
||||
imageUploadRef.current?.click()
|
||||
}
|
||||
if (e.key === 'Tab' && e.altKey) {
|
||||
e.preventDefault()
|
||||
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||
}
|
||||
} else {
|
||||
handleKeyDown(e, userSuggestOnKeyDown, territorySuggestOnKeyDown)
|
||||
}
|
||||
|
||||
if (onKeyDown) onKeyDown(e)
|
||||
}
|
||||
}, [innerRef, helpers?.setValue, setSelectionRange, onKeyDown, handleKeyDown, imageUploadRef])
|
||||
|
||||
return (
|
||||
<FormGroup label={label} className={groupClassName}>
|
||||
<div className={`${styles.markdownInput} ${tab === 'write' ? styles.noTopLeftRadius : ''}`}>
|
||||
@ -472,34 +532,25 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
||||
</span>
|
||||
</Nav>
|
||||
<div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}>
|
||||
<UserSuggest
|
||||
query={userAutocomplete.entityData?.query}
|
||||
onSelect={userAutocomplete.handleSelect}
|
||||
dropdownStyle={userAutocomplete.entityData?.style}
|
||||
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions: resetUserSuggestions }) => (
|
||||
<TerritorySuggest
|
||||
query={territoryAutocomplete.entityData?.query}
|
||||
onSelect={territoryAutocomplete.handleSelect}
|
||||
dropdownStyle={territoryAutocomplete.entityData?.style}
|
||||
>{({ onKeyDown: territorySuggestOnKeyDown, resetSuggestions: resetTerritorySuggestions }) => (
|
||||
<DualAutocompleteWrapper
|
||||
userAutocomplete={userAutocomplete}
|
||||
territoryAutocomplete={territoryAutocomplete}
|
||||
>
|
||||
{({ userSuggestOnKeyDown, territorySuggestOnKeyDown, resetUserSuggestions, resetTerritorySuggestions }) => (
|
||||
<InputInner
|
||||
innerRef={innerRef}
|
||||
{...props}
|
||||
onChange={onChangeInner}
|
||||
onKeyDown={onKeyDownInner(userSuggestOnKeyDown, territorySuggestOnKeyDown)}
|
||||
onBlur={() => {
|
||||
setTimeout(resetUserSuggestions, 500)
|
||||
setTimeout(resetTerritorySuggestions, 500)
|
||||
}}
|
||||
onBlur={() => handleBlur(resetUserSuggestions, resetTerritorySuggestions)}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
onPaste={onPaste}
|
||||
className={dragStyle === 'over' ? styles.dragOver : ''}
|
||||
/>)}
|
||||
</TerritorySuggest>
|
||||
)}
|
||||
</UserSuggest>
|
||||
/>
|
||||
)}
|
||||
</DualAutocompleteWrapper>
|
||||
</div>
|
||||
{tab !== 'write' &&
|
||||
<div className='form-group'>
|
||||
|
@ -1,22 +1,26 @@
|
||||
import Container from 'react-bootstrap/Container'
|
||||
import styles from './search.module.css'
|
||||
import SearchIcon from '@/svgs/search-line.svg'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Form, Input, Select, DatePicker, SubmitButton } from './form'
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
DatePicker,
|
||||
SubmitButton,
|
||||
useDualAutocomplete,
|
||||
DualAutocompleteWrapper
|
||||
} from './form'
|
||||
import { useRouter } from 'next/router'
|
||||
import { whenToFrom } from '@/lib/time'
|
||||
import { useMe } from './me'
|
||||
import { useField } from 'formik'
|
||||
|
||||
export default function Search ({ sub }) {
|
||||
const router = useRouter()
|
||||
const [q, setQ] = useState(router.query.q || '')
|
||||
const inputRef = useRef(null)
|
||||
const { me } = useMe()
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const search = async values => {
|
||||
let prefix = ''
|
||||
if (sub) {
|
||||
@ -63,18 +67,13 @@ export default function Search ({ sub }) {
|
||||
onSubmit={values => search({ ...values })}
|
||||
>
|
||||
<div className={`${styles.active} mb-3`}>
|
||||
<Input
|
||||
<SearchInput
|
||||
name='q'
|
||||
required
|
||||
autoFocus
|
||||
groupClassName='me-3 mb-0 flex-grow-1'
|
||||
className='flex-grow-1'
|
||||
clear
|
||||
innerRef={inputRef}
|
||||
overrideValue={q}
|
||||
onChange={async (formik, e) => {
|
||||
setQ(e.target.value?.trim())
|
||||
}}
|
||||
setOuterQ={setQ}
|
||||
/>
|
||||
<SubmitButton variant='primary' className={styles.search}>
|
||||
<SearchIcon width={22} height={22} />
|
||||
@ -135,3 +134,52 @@ export default function Search ({ sub }) {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchInput ({ name, setOuterQ, ...props }) {
|
||||
const [, meta, helpers] = useField(name)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (meta.value !== undefined) setOuterQ(meta.value.trim())
|
||||
}, [meta.value, setOuterQ])
|
||||
|
||||
const setCaret = useCallback(({ start, end }) => {
|
||||
inputRef.current?.setSelectionRange(start, end)
|
||||
}, [])
|
||||
|
||||
const { userAutocomplete, territoryAutocomplete, handleTextChange, handleKeyDown, handleBlur } = useDualAutocomplete({
|
||||
meta,
|
||||
helpers,
|
||||
innerRef: inputRef,
|
||||
setSelectionRange: setCaret
|
||||
})
|
||||
|
||||
const handleChangeWithOuter = useCallback((formik, e) => {
|
||||
setOuterQ(e.target.value.trim())
|
||||
handleTextChange(e)
|
||||
}, [setOuterQ, handleTextChange])
|
||||
|
||||
return (
|
||||
<div className='position-relative flex-grow-1'>
|
||||
<DualAutocompleteWrapper
|
||||
userAutocomplete={userAutocomplete}
|
||||
territoryAutocomplete={territoryAutocomplete}
|
||||
>
|
||||
{({ userSuggestOnKeyDown, territorySuggestOnKeyDown, resetUserSuggestions, resetTerritorySuggestions }) => (
|
||||
<Input
|
||||
name={name}
|
||||
innerRef={inputRef}
|
||||
clear
|
||||
autoComplete='off'
|
||||
onChange={handleChangeWithOuter}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, userSuggestOnKeyDown, territorySuggestOnKeyDown)
|
||||
}}
|
||||
onBlur={() => handleBlur(resetUserSuggestions, resetTerritorySuggestions)}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</DualAutocompleteWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user