fix: Fixed issue where entering a space or punctuation in Super autocomplete would dismiss menu

This commit is contained in:
Mo
2022-12-06 21:43:45 -06:00
parent d691d7a3b3
commit a5b1ff9595
6 changed files with 66 additions and 10 deletions

View File

@@ -10,6 +10,7 @@ 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
@@ -117,7 +118,7 @@ const LinkedItemBubble = ({
{link.type === 'linked-by' && link.item.content_type !== ContentType.Tag && (
<span className={!isBidirectional ? 'hidden group-focus:block' : ''}>Linked By:</span>
)}
{link.item.title}
{getItemTitleInContextOfLinkBubble(link.item)}
</span>
</span>
{showUnlinkButton && (

View File

@@ -6,6 +6,7 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { observer } from 'mobx-react-lite'
import { useApplication } from '../ApplicationProvider'
import Icon from '../Icon/Icon'
import { getItemTitleInContextOfLinkBubble } from '@/Utils/Items/Search/doesItemMatchSearchQuery'
type Props = {
item: LinkableItem
@@ -16,7 +17,7 @@ const LinkedItemMeta = ({ item, searchQuery }: Props) => {
const application = useApplication()
const [icon, className] = getIconForItem(item, application)
const tagTitle = getTitleForLinkedTag(item, application)
const title = item.title ?? ''
const title = getItemTitleInContextOfLinkBubble(item)
return (
<>

View File

@@ -905,9 +905,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
</div>
)}
</div>
{editorMode !== 'super' && (
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
)}
<LinkedItemBubblesContainer linkingController={this.viewControllerManager.linkingController} />
</div>
)}

View File

@@ -1,5 +1,5 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { LexicalTypeaheadMenuPlugin, useBasicTypeaheadTriggerMatch } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { TextNode } from 'lexical'
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
import { ItemSelectionItemComponent } from './ItemSelectionItemComponent'
@@ -12,6 +12,7 @@ import { INSERT_BUBBLE_COMMAND, INSERT_FILE_COMMAND } from '../Commands'
import { useLinkingController } from '../../../../../Controllers/LinkingControllerProvider'
import { PopoverClassNames } from '../ClassNames'
import { isMobileScreen } from '@/Utils'
import { useTypeaheadAllowingSpacesAndPunctuation } from './useTypeaheadAllowingSpacesAndPunctuation'
type Props = {
currentNote: SNNote
@@ -26,7 +27,7 @@ export const ItemSelectionPlugin: FunctionComponent<Props> = ({ currentNote }) =
const [queryString, setQueryString] = useState<string | null>('')
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('@', {
const checkForTriggerMatch = useTypeaheadAllowingSpacesAndPunctuation('@', {
minLength: 0,
})

View File

@@ -0,0 +1,41 @@
import { LexicalEditor } from 'lexical'
import { useCallback } from 'react'
export type QueryMatch = {
leadOffset: number
matchingString: string
replaceableString: string
}
type TriggerFn = (text: string, editor: LexicalEditor) => QueryMatch | null
/**
* Derived from
* https://github.com/facebook/lexical/blob/main/packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx#L545
*/
export function useTypeaheadAllowingSpacesAndPunctuation(
trigger: string,
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
): TriggerFn {
return useCallback(
(text: string) => {
const validChars = '[^' + trigger + ']'
const TypeaheadTriggerRegex = new RegExp(
'(^|\\s|\\()(' + '[' + trigger + ']' + '((?:' + validChars + '){0,' + maxLength + '})' + ')$',
)
const match = TypeaheadTriggerRegex.exec(text)
if (match !== null) {
const maybeLeadingWhitespace = match[1]
const matchingString = match[3]
if (matchingString.length >= minLength) {
return {
leadOffset: match.index + maybeLeadingWhitespace.length,
matchingString,
replaceableString: match[2],
}
}
}
return null
},
[maxLength, minLength, trigger],
)
}

View File

@@ -1,12 +1,26 @@
import { SNTag, WebApplicationInterface, DecryptedItemInterface, ItemContent } from '@standardnotes/snjs'
import { WebApplicationInterface, DecryptedItemInterface, ItemContent, isNote, isTag } from '@standardnotes/snjs'
export function getItemTitleInContextOfLinkBubble(item: DecryptedItemInterface<ItemContent>) {
return item.title && item.title.length > 0 ? item.title : isNote(item) ? item.preview_plain : ''
}
function getItemSearchableString(item: DecryptedItemInterface<ItemContent>, application: WebApplicationInterface) {
if (isNote(item)) {
return item.title.length > 0 ? item.title : item.preview_plain
} else if (isTag(item)) {
return application.items.getTagLongTitle(item)
}
return item.title ?? ''
}
export function doesItemMatchSearchQuery(
item: DecryptedItemInterface<ItemContent>,
searchQuery: string,
application: WebApplicationInterface,
) {
const title = item instanceof SNTag ? application.items.getTagLongTitle(item) : item.title ?? ''
const matchesQuery = title.toLowerCase().includes(searchQuery.toLowerCase())
const title = getItemSearchableString(item, application).toLowerCase()
const matchesQuery = title.includes(searchQuery.toLowerCase())
const isArchivedOrTrashed = item.archived || item.trashed
const isValidSearchResult = matchesQuery && !isArchivedOrTrashed