Skip to content

Commit 524b1b9

Browse files
ed-kunghuumn
andauthored
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>
1 parent aebba27 commit 524b1b9

File tree

2 files changed

+193
-94
lines changed

2 files changed

+193
-94
lines changed

components/form.js

Lines changed: 131 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,86 @@ function useEntityAutocomplete ({
228228
}
229229
}
230230

231+
export function useDualAutocomplete ({ meta, helpers, innerRef, setSelectionRange }) {
232+
const userAutocomplete = useEntityAutocomplete({
233+
prefix: '@',
234+
meta,
235+
helpers,
236+
innerRef,
237+
setSelectionRange,
238+
SuggestComponent: UserSuggest
239+
})
240+
241+
const territoryAutocomplete = useEntityAutocomplete({
242+
prefix: '~',
243+
meta,
244+
helpers,
245+
innerRef,
246+
setSelectionRange,
247+
SuggestComponent: TerritorySuggest
248+
})
249+
250+
const handleTextChange = useCallback((e) => {
251+
// Try to match user mentions first, then territories
252+
if (!userAutocomplete.handleTextChange(e)) {
253+
territoryAutocomplete.handleTextChange(e)
254+
}
255+
}, [userAutocomplete, territoryAutocomplete])
256+
257+
const handleKeyDown = useCallback((e, userOnKeyDown, territoryOnKeyDown) => {
258+
const metaOrCtrl = e.metaKey || e.ctrlKey
259+
if (!metaOrCtrl) {
260+
if (userAutocomplete.entityData) {
261+
return userOnKeyDown(e)
262+
} else if (territoryAutocomplete.entityData) {
263+
return territoryOnKeyDown(e)
264+
}
265+
}
266+
return false // Didn't handle the event
267+
}, [userAutocomplete.entityData, territoryAutocomplete.entityData])
268+
269+
const handleBlur = useCallback((resetUserSuggestions, resetTerritorySuggestions) => {
270+
setTimeout(resetUserSuggestions, 500)
271+
setTimeout(resetTerritorySuggestions, 500)
272+
}, [])
273+
274+
return {
275+
userAutocomplete,
276+
territoryAutocomplete,
277+
handleTextChange,
278+
handleKeyDown,
279+
handleBlur
280+
}
281+
}
282+
283+
export function DualAutocompleteWrapper ({
284+
userAutocomplete,
285+
territoryAutocomplete,
286+
children
287+
}) {
288+
return (
289+
<UserSuggest
290+
query={userAutocomplete.entityData?.query}
291+
onSelect={userAutocomplete.handleSelect}
292+
dropdownStyle={userAutocomplete.entityData?.style}
293+
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions: resetUserSuggestions }) => (
294+
<TerritorySuggest
295+
query={territoryAutocomplete.entityData?.query}
296+
onSelect={territoryAutocomplete.handleSelect}
297+
dropdownStyle={territoryAutocomplete.entityData?.style}
298+
>{({ onKeyDown: territorySuggestOnKeyDown, resetSuggestions: resetTerritorySuggestions }) =>
299+
children({
300+
userSuggestOnKeyDown,
301+
territorySuggestOnKeyDown,
302+
resetUserSuggestions,
303+
resetTerritorySuggestions
304+
})}
305+
</TerritorySuggest>
306+
)}
307+
</UserSuggest>
308+
)
309+
}
310+
231311
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) {
232312
const [tab, setTab] = useState('write')
233313
const [, meta, helpers] = useField(props)
@@ -287,22 +367,11 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
287367
}
288368
}, [innerRef, selectionRange.start, selectionRange.end])
289369

290-
const userAutocomplete = useEntityAutocomplete({
291-
prefix: '@',
292-
meta,
293-
helpers,
294-
innerRef,
295-
setSelectionRange,
296-
SuggestComponent: UserSuggest
297-
})
298-
299-
const territoryAutocomplete = useEntityAutocomplete({
300-
prefix: '~',
370+
const { userAutocomplete, territoryAutocomplete, handleTextChange, handleKeyDown, handleBlur } = useDualAutocomplete({
301371
meta,
302372
helpers,
303373
innerRef,
304-
setSelectionRange,
305-
SuggestComponent: TerritorySuggest
374+
setSelectionRange
306375
})
307376

308377
const uploadFeesUpdate = useDebounceCallback(
@@ -313,56 +382,9 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
313382

314383
const onChangeInner = useCallback((formik, e) => {
315384
if (onChange) onChange(formik, e)
316-
// check for mentions and territory suggestions
317385
uploadFeesUpdate(e.target.value)
318-
319-
// Try to match user mentions first, then territories
320-
if (!userAutocomplete.handleTextChange(e)) {
321-
territoryAutocomplete.handleTextChange(e)
322-
}
323-
}, [onChange, uploadFeesUpdate, userAutocomplete, territoryAutocomplete])
324-
325-
const onKeyDownInner = useCallback((userSuggestOnKeyDown, territorySuggestOnKeyDown) => {
326-
return (e) => {
327-
const metaOrCtrl = e.metaKey || e.ctrlKey
328-
if (metaOrCtrl) {
329-
if (e.key === 'k') {
330-
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
331-
e.preventDefault()
332-
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
333-
}
334-
if (e.key === 'b') {
335-
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
336-
e.preventDefault()
337-
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
338-
}
339-
if (e.key === 'i') {
340-
// some browsers might use CTRL+I to do something else so prevent that behavior too
341-
e.preventDefault()
342-
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
343-
}
344-
if (e.key === 'u') {
345-
// some browsers might use CTRL+U to do something else so prevent that behavior too
346-
e.preventDefault()
347-
imageUploadRef.current?.click()
348-
}
349-
if (e.key === 'Tab' && e.altKey) {
350-
e.preventDefault()
351-
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
352-
}
353-
}
354-
355-
if (!metaOrCtrl) {
356-
if (userAutocomplete.entityData) {
357-
userSuggestOnKeyDown(e)
358-
} else if (territoryAutocomplete.entityData) {
359-
territorySuggestOnKeyDown(e)
360-
}
361-
}
362-
363-
if (onKeyDown) onKeyDown(e)
364-
}
365-
}, [innerRef, helpers?.setValue, setSelectionRange, onKeyDown, userAutocomplete.entityData, territoryAutocomplete.entityData])
386+
handleTextChange(e)
387+
}, [onChange, uploadFeesUpdate, handleTextChange])
366388

367389
const onPaste = useCallback((event) => {
368390
const items = event.clipboardData.items
@@ -406,6 +428,44 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
406428
setDragStyle(null)
407429
}, [setDragStyle])
408430

431+
const onKeyDownInner = useCallback((userSuggestOnKeyDown, territorySuggestOnKeyDown) => {
432+
return (e) => {
433+
const metaOrCtrl = e.metaKey || e.ctrlKey
434+
435+
// Handle markdown shortcuts first
436+
if (metaOrCtrl) {
437+
if (e.key === 'k') {
438+
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
439+
e.preventDefault()
440+
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
441+
}
442+
if (e.key === 'b') {
443+
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
444+
e.preventDefault()
445+
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
446+
}
447+
if (e.key === 'i') {
448+
// some browsers might use CTRL+I to do something else so prevent that behavior too
449+
e.preventDefault()
450+
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
451+
}
452+
if (e.key === 'u') {
453+
// some browsers might use CTRL+U to do something else so prevent that behavior too
454+
e.preventDefault()
455+
imageUploadRef.current?.click()
456+
}
457+
if (e.key === 'Tab' && e.altKey) {
458+
e.preventDefault()
459+
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
460+
}
461+
} else {
462+
handleKeyDown(e, userSuggestOnKeyDown, territorySuggestOnKeyDown)
463+
}
464+
465+
if (onKeyDown) onKeyDown(e)
466+
}
467+
}, [innerRef, helpers?.setValue, setSelectionRange, onKeyDown, handleKeyDown, imageUploadRef])
468+
409469
return (
410470
<FormGroup label={label} className={groupClassName}>
411471
<div className={`${styles.markdownInput} ${tab === 'write' ? styles.noTopLeftRadius : ''}`}>
@@ -472,34 +532,25 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
472532
</span>
473533
</Nav>
474534
<div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}>
475-
<UserSuggest
476-
query={userAutocomplete.entityData?.query}
477-
onSelect={userAutocomplete.handleSelect}
478-
dropdownStyle={userAutocomplete.entityData?.style}
479-
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions: resetUserSuggestions }) => (
480-
<TerritorySuggest
481-
query={territoryAutocomplete.entityData?.query}
482-
onSelect={territoryAutocomplete.handleSelect}
483-
dropdownStyle={territoryAutocomplete.entityData?.style}
484-
>{({ onKeyDown: territorySuggestOnKeyDown, resetSuggestions: resetTerritorySuggestions }) => (
535+
<DualAutocompleteWrapper
536+
userAutocomplete={userAutocomplete}
537+
territoryAutocomplete={territoryAutocomplete}
538+
>
539+
{({ userSuggestOnKeyDown, territorySuggestOnKeyDown, resetUserSuggestions, resetTerritorySuggestions }) => (
485540
<InputInner
486541
innerRef={innerRef}
487542
{...props}
488543
onChange={onChangeInner}
489544
onKeyDown={onKeyDownInner(userSuggestOnKeyDown, territorySuggestOnKeyDown)}
490-
onBlur={() => {
491-
setTimeout(resetUserSuggestions, 500)
492-
setTimeout(resetTerritorySuggestions, 500)
493-
}}
545+
onBlur={() => handleBlur(resetUserSuggestions, resetTerritorySuggestions)}
494546
onDragEnter={onDragEnter}
495547
onDragLeave={onDragLeave}
496548
onDrop={onDrop}
497549
onPaste={onPaste}
498550
className={dragStyle === 'over' ? styles.dragOver : ''}
499-
/>)}
500-
</TerritorySuggest>
501-
)}
502-
</UserSuggest>
551+
/>
552+
)}
553+
</DualAutocompleteWrapper>
503554
</div>
504555
{tab !== 'write' &&
505556
<div className='form-group'>

components/search.js

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import Container from 'react-bootstrap/Container'
22
import styles from './search.module.css'
33
import SearchIcon from '@/svgs/search-line.svg'
4-
import { useEffect, useMemo, useRef, useState } from 'react'
5-
import { Form, Input, Select, DatePicker, SubmitButton } from './form'
4+
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
5+
import {
6+
Form,
7+
Input,
8+
Select,
9+
DatePicker,
10+
SubmitButton,
11+
useDualAutocomplete,
12+
DualAutocompleteWrapper
13+
} from './form'
614
import { useRouter } from 'next/router'
715
import { whenToFrom } from '@/lib/time'
816
import { useMe } from './me'
17+
import { useField } from 'formik'
918

1019
export default function Search ({ sub }) {
1120
const router = useRouter()
1221
const [q, setQ] = useState(router.query.q || '')
13-
const inputRef = useRef(null)
1422
const { me } = useMe()
1523

16-
useEffect(() => {
17-
inputRef.current?.focus()
18-
}, [])
19-
2024
const search = async values => {
2125
let prefix = ''
2226
if (sub) {
@@ -63,18 +67,13 @@ export default function Search ({ sub }) {
6367
onSubmit={values => search({ ...values })}
6468
>
6569
<div className={`${styles.active} mb-3`}>
66-
<Input
70+
<SearchInput
6771
name='q'
6872
required
6973
autoFocus
7074
groupClassName='me-3 mb-0 flex-grow-1'
7175
className='flex-grow-1'
72-
clear
73-
innerRef={inputRef}
74-
overrideValue={q}
75-
onChange={async (formik, e) => {
76-
setQ(e.target.value?.trim())
77-
}}
76+
setOuterQ={setQ}
7877
/>
7978
<SubmitButton variant='primary' className={styles.search}>
8079
<SearchIcon width={22} height={22} />
@@ -135,3 +134,52 @@ export default function Search ({ sub }) {
135134
</>
136135
)
137136
}
137+
138+
function SearchInput ({ name, setOuterQ, ...props }) {
139+
const [, meta, helpers] = useField(name)
140+
const inputRef = useRef(null)
141+
142+
useEffect(() => {
143+
if (meta.value !== undefined) setOuterQ(meta.value.trim())
144+
}, [meta.value, setOuterQ])
145+
146+
const setCaret = useCallback(({ start, end }) => {
147+
inputRef.current?.setSelectionRange(start, end)
148+
}, [])
149+
150+
const { userAutocomplete, territoryAutocomplete, handleTextChange, handleKeyDown, handleBlur } = useDualAutocomplete({
151+
meta,
152+
helpers,
153+
innerRef: inputRef,
154+
setSelectionRange: setCaret
155+
})
156+
157+
const handleChangeWithOuter = useCallback((formik, e) => {
158+
setOuterQ(e.target.value.trim())
159+
handleTextChange(e)
160+
}, [setOuterQ, handleTextChange])
161+
162+
return (
163+
<div className='position-relative flex-grow-1'>
164+
<DualAutocompleteWrapper
165+
userAutocomplete={userAutocomplete}
166+
territoryAutocomplete={territoryAutocomplete}
167+
>
168+
{({ userSuggestOnKeyDown, territorySuggestOnKeyDown, resetUserSuggestions, resetTerritorySuggestions }) => (
169+
<Input
170+
name={name}
171+
innerRef={inputRef}
172+
clear
173+
autoComplete='off'
174+
onChange={handleChangeWithOuter}
175+
onKeyDown={(e) => {
176+
handleKeyDown(e, userSuggestOnKeyDown, territorySuggestOnKeyDown)
177+
}}
178+
onBlur={() => handleBlur(resetUserSuggestions, resetTerritorySuggestions)}
179+
{...props}
180+
/>
181+
)}
182+
</DualAutocompleteWrapper>
183+
</div>
184+
)
185+
}

0 commit comments

Comments
 (0)