144 lines
4.3 KiB
TypeScript
144 lines
4.3 KiB
TypeScript
import { LinkingController } from '@/Controllers/LinkingController'
|
|
import { classNames } from '@standardnotes/utils'
|
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
|
import { observer } from 'mobx-react-lite'
|
|
import { KeyboardEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react'
|
|
import { ContentType } from '@standardnotes/snjs'
|
|
import Icon from '../Icon/Icon'
|
|
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
|
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
|
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
|
|
import { useApplication } from '../ApplicationProvider'
|
|
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
|
|
import { getItemTitleInContextOfLinkBubble } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
|
|
|
|
type Props = {
|
|
link: ItemLink
|
|
activateItem?: (item: LinkableItem) => Promise<void>
|
|
unlinkItem: LinkingController['unlinkItemFromSelectedItem']
|
|
focusPreviousItem?: () => void
|
|
focusNextItem?: () => void
|
|
focusedId?: string | undefined
|
|
setFocusedId?: (id: string) => void
|
|
isBidirectional: boolean
|
|
inlineFlex?: boolean
|
|
className?: string
|
|
readonly?: boolean
|
|
}
|
|
|
|
const LinkedItemBubble = ({
|
|
link,
|
|
activateItem,
|
|
unlinkItem,
|
|
focusPreviousItem,
|
|
focusNextItem,
|
|
focusedId,
|
|
setFocusedId,
|
|
isBidirectional,
|
|
inlineFlex,
|
|
className,
|
|
readonly,
|
|
}: Props) => {
|
|
const ref = useRef<HTMLButtonElement>(null)
|
|
const application = useApplication()
|
|
|
|
const [showUnlinkButton, setShowUnlinkButton] = useState(false)
|
|
const unlinkButtonRef = useRef<HTMLAnchorElement | null>(null)
|
|
|
|
const [wasClicked, setWasClicked] = useState(false)
|
|
|
|
const handleFocus = () => {
|
|
if (focusedId !== link.id) {
|
|
setFocusedId?.(link.id)
|
|
}
|
|
setShowUnlinkButton(true)
|
|
}
|
|
|
|
const onBlur = () => {
|
|
setShowUnlinkButton(false)
|
|
setWasClicked(false)
|
|
}
|
|
|
|
const onClick: MouseEventHandler = (event) => {
|
|
if (wasClicked && event.target !== unlinkButtonRef.current) {
|
|
setWasClicked(false)
|
|
if (readonly) {
|
|
return
|
|
}
|
|
void activateItem?.(link.item)
|
|
} else {
|
|
setWasClicked(true)
|
|
}
|
|
}
|
|
|
|
const onUnlinkClick: MouseEventHandler = (event) => {
|
|
event.stopPropagation()
|
|
void unlinkItem(link.item)
|
|
}
|
|
|
|
const onKeyDown: KeyboardEventHandler = (event) => {
|
|
switch (event.key) {
|
|
case KeyboardKey.Backspace: {
|
|
focusPreviousItem?.()
|
|
void unlinkItem(link.item)
|
|
break
|
|
}
|
|
case KeyboardKey.Left:
|
|
focusPreviousItem?.()
|
|
break
|
|
case KeyboardKey.Right:
|
|
focusNextItem?.()
|
|
break
|
|
}
|
|
}
|
|
|
|
const [icon, iconClassName] = getIconForItem(link.item, application)
|
|
const tagTitle = getTitleForLinkedTag(link.item, application)
|
|
|
|
useEffect(() => {
|
|
if (link.id === focusedId) {
|
|
ref.current?.focus()
|
|
}
|
|
}, [focusedId, link.id])
|
|
|
|
return (
|
|
<button
|
|
ref={ref}
|
|
className={classNames(
|
|
'group h-6 cursor-pointer items-center rounded bg-passive-4-opacity-variant py-2 pl-1 pr-2 text-sm',
|
|
'text-text hover:bg-contrast focus:bg-contrast lg:text-xs',
|
|
inlineFlex ? 'inline-flex' : 'flex',
|
|
className,
|
|
)}
|
|
onFocus={handleFocus}
|
|
onBlur={onBlur}
|
|
onClick={onClick}
|
|
title={tagTitle ? tagTitle.longTitle : link.item.title}
|
|
onKeyDown={onKeyDown}
|
|
>
|
|
<Icon type={icon} className={classNames('mr-1 flex-shrink-0', iconClassName)} size="small" />
|
|
<span className="flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap">
|
|
{tagTitle && <span className="text-passive-1">{tagTitle.titlePrefix}</span>}
|
|
<span className="flex items-center gap-1">
|
|
{link.type === 'linked-by' && link.item.content_type !== ContentType.TYPES.Tag && (
|
|
<span className={!isBidirectional ? 'hidden group-focus:block' : ''}>Linked By:</span>
|
|
)}
|
|
{getItemTitleInContextOfLinkBubble(link.item)}
|
|
</span>
|
|
</span>
|
|
{showUnlinkButton && !readonly && (
|
|
<a
|
|
ref={unlinkButtonRef}
|
|
role="button"
|
|
className="ml-2 -mr-1 flex cursor-pointer border-0 bg-transparent p-0"
|
|
onClick={onUnlinkClick}
|
|
>
|
|
<Icon type="close" className="text-neutral hover:text-info" size="small" />
|
|
</a>
|
|
)}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
export default observer(LinkedItemBubble)
|