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