diff --git a/components/form.js b/components/form.js index e28d64c3..bdc2e1e0 100644 --- a/components/form.js +++ b/components/form.js @@ -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 ( + {({ onKeyDown: userSuggestOnKeyDown, resetSuggestions: resetUserSuggestions }) => ( + {({ onKeyDown: territorySuggestOnKeyDown, resetSuggestions: resetTerritorySuggestions }) => + children({ + userSuggestOnKeyDown, + territorySuggestOnKeyDown, + resetUserSuggestions, + resetTerritorySuggestions + })} + + )} + + ) +} + 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 (
@@ -472,34 +532,25 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
- {({ onKeyDown: userSuggestOnKeyDown, resetSuggestions: resetUserSuggestions }) => ( - {({ onKeyDown: territorySuggestOnKeyDown, resetSuggestions: resetTerritorySuggestions }) => ( + + {({ userSuggestOnKeyDown, territorySuggestOnKeyDown, resetUserSuggestions, resetTerritorySuggestions }) => ( { - setTimeout(resetUserSuggestions, 500) - setTimeout(resetTerritorySuggestions, 500) - }} + onBlur={() => handleBlur(resetUserSuggestions, resetTerritorySuggestions)} onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDrop} onPaste={onPaste} className={dragStyle === 'over' ? styles.dragOver : ''} - />)} - - )} - + /> + )} +
{tab !== 'write' &&
diff --git a/components/search.js b/components/search.js index 67c9d23c..249683f9 100644 --- a/components/search.js +++ b/components/search.js @@ -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 })} >
- { - setQ(e.target.value?.trim()) - }} + setOuterQ={setQ} /> @@ -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 ( +
+ + {({ userSuggestOnKeyDown, territorySuggestOnKeyDown, resetUserSuggestions, resetTerritorySuggestions }) => ( + { + handleKeyDown(e, userSuggestOnKeyDown, territorySuggestOnKeyDown) + }} + onBlur={() => handleBlur(resetUserSuggestions, resetTerritorySuggestions)} + {...props} + /> + )} + +
+ ) +}