148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
import {
|
|
FormEventHandler,
|
|
ForwardedRef,
|
|
KeyboardEventHandler,
|
|
forwardRef,
|
|
useDeferredValue,
|
|
useEffect,
|
|
useRef,
|
|
} from 'react'
|
|
import { observer } from 'mobx-react-lite'
|
|
import { classNames } from '@standardnotes/utils'
|
|
import { LinkingController } from '@/Controllers/LinkingController'
|
|
import { ElementIds } from '@/Constants/ElementIDs'
|
|
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
|
|
import { useApplication } from '../ApplicationProvider'
|
|
import { DecryptedItem, SNNote } from '@standardnotes/snjs'
|
|
import { Combobox, ComboboxItem, ComboboxPopover, useComboboxStore, VisuallyHidden } from '@ariakit/react'
|
|
import LinkedItemMeta from './LinkedItemMeta'
|
|
import { LinkedItemSearchResultsAddTagOption } from './LinkedItemSearchResultsAddTagOption'
|
|
import { Slot } from '@radix-ui/react-slot'
|
|
import Icon from '../Icon/Icon'
|
|
import { PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
|
import { mergeRefs } from '@/Hooks/mergeRefs'
|
|
|
|
type Props = {
|
|
linkingController: LinkingController
|
|
focusPreviousItem: () => void
|
|
focusedId: string | undefined
|
|
setFocusedId: (id: string) => void
|
|
hoverLabel?: string
|
|
item: DecryptedItem
|
|
}
|
|
|
|
const ItemLinkAutocompleteInput = forwardRef(
|
|
(
|
|
{ linkingController, focusPreviousItem, focusedId, setFocusedId, hoverLabel, item }: Props,
|
|
forwardedRef: ForwardedRef<HTMLInputElement>,
|
|
) => {
|
|
const application = useApplication()
|
|
|
|
const { getLinkedTagsForItem, linkItems, createAndAddNewTag, isEntitledToNoteLinking } = linkingController
|
|
|
|
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
|
|
|
|
const combobox = useComboboxStore()
|
|
const value = combobox.useState('value')
|
|
const searchQuery = useDeferredValue(value)
|
|
|
|
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, item)
|
|
|
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
|
|
const onFormSubmit: FormEventHandler = async (event) => {
|
|
event.preventDefault()
|
|
if (searchQuery !== '') {
|
|
await createAndAddNewTag(searchQuery)
|
|
combobox.setValue('')
|
|
}
|
|
}
|
|
|
|
const handleFocus = () => {
|
|
if (focusedId !== ElementIds.ItemLinkAutocompleteInput) {
|
|
setFocusedId(ElementIds.ItemLinkAutocompleteInput)
|
|
}
|
|
}
|
|
|
|
const onKeyDown: KeyboardEventHandler = (event) => {
|
|
switch (event.key) {
|
|
case KeyboardKey.Left:
|
|
if (searchQuery.length === 0) {
|
|
focusPreviousItem()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (focusedId === ElementIds.ItemLinkAutocompleteInput) {
|
|
inputRef.current?.focus()
|
|
}
|
|
}, [focusedId])
|
|
|
|
return (
|
|
<div>
|
|
<form onSubmit={onFormSubmit}>
|
|
<label>
|
|
<VisuallyHidden>Link tags, notes or files</VisuallyHidden>
|
|
<Combobox
|
|
store={combobox}
|
|
placeholder="Link tags, notes, files..."
|
|
className={classNames(
|
|
`${tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70'}`,
|
|
'h-7 w-70 bg-transparent text-sm text-text focus:border-b-2 focus:border-info focus:shadow-none focus:outline-none lg:text-xs',
|
|
)}
|
|
title={hoverLabel}
|
|
id={ElementIds.ItemLinkAutocompleteInput}
|
|
ref={mergeRefs([inputRef, forwardedRef])}
|
|
onFocus={handleFocus}
|
|
onKeyDown={onKeyDown}
|
|
/>
|
|
</label>
|
|
<ComboboxPopover
|
|
store={combobox}
|
|
className={classNames(
|
|
'z-dropdown-menu max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] overflow-y-auto rounded bg-default py-2 shadow-main',
|
|
unlinkedItems.length === 0 && !shouldShowCreateTag && 'hidden',
|
|
)}
|
|
>
|
|
{unlinkedItems.map((result) => {
|
|
const cannotLinkItem = !isEntitledToNoteLinking && result instanceof SNNote
|
|
|
|
return (
|
|
<ComboboxItem
|
|
key={result.uuid}
|
|
className="flex w-full cursor-pointer items-center justify-between gap-4 overflow-hidden px-3 py-2 hover:bg-contrast hover:text-foreground [&[data-active-item]]:bg-info-backdrop"
|
|
hideOnClick
|
|
onClick={() => {
|
|
linkItems(item, result).catch(console.error)
|
|
combobox.setValue('')
|
|
}}
|
|
>
|
|
<LinkedItemMeta item={result} searchQuery={searchQuery} />
|
|
{cannotLinkItem && <Icon type={PremiumFeatureIconName} className="ml-auto flex-shrink-0 text-info" />}
|
|
</ComboboxItem>
|
|
)
|
|
})}
|
|
{shouldShowCreateTag && (
|
|
<ComboboxItem
|
|
hideOnClick
|
|
as={Slot}
|
|
onClick={() => {
|
|
void createAndAddNewTag(searchQuery)
|
|
combobox.setValue('')
|
|
}}
|
|
>
|
|
<LinkedItemSearchResultsAddTagOption searchQuery={searchQuery} />
|
|
</ComboboxItem>
|
|
)}
|
|
</ComboboxPopover>
|
|
</form>
|
|
</div>
|
|
)
|
|
},
|
|
)
|
|
|
|
export default observer(ItemLinkAutocompleteInput)
|