From a5b1ff95957a9d407663d28429785457aef3a1b4 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 6 Dec 2022 21:43:45 -0600 Subject: [PATCH] fix: Fixed issue where entering a space or punctuation in Super autocomplete would dismiss menu --- .../LinkedItems/LinkedItemBubble.tsx | 3 +- .../Components/LinkedItems/LinkedItemMeta.tsx | 3 +- .../Components/NoteView/NoteView.tsx | 4 +- .../ItemSelectionPlugin.tsx | 5 ++- ...eTypeaheadAllowingSpacesAndPunctuation.tsx | 41 +++++++++++++++++++ .../Items/Search/doesItemMatchSearchQuery.ts | 20 +++++++-- 6 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/useTypeaheadAllowingSpacesAndPunctuation.tsx diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx index 432ab05b3..5d1c54499 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx @@ -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 && ( Linked By: )} - {link.item.title} + {getItemTitleInContextOfLinkBubble(link.item)} {showUnlinkButton && ( diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx index 00d523876..a4f993f56 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemMeta.tsx @@ -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 ( <> diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 606685a82..53c074243 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -905,9 +905,7 @@ class NoteView extends AbstractComponent { )} - {editorMode !== 'super' && ( - - )} + )} diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx index 09bd136f3..898cfcb98 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx @@ -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 = ({ currentNote }) = const [queryString, setQueryString] = useState('') - const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('@', { + const checkForTriggerMatch = useTypeaheadAllowingSpacesAndPunctuation('@', { minLength: 0, }) diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/useTypeaheadAllowingSpacesAndPunctuation.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/useTypeaheadAllowingSpacesAndPunctuation.tsx new file mode 100644 index 000000000..8a601dcf4 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/useTypeaheadAllowingSpacesAndPunctuation.tsx @@ -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], + ) +} diff --git a/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts b/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts index 351561d2d..4405d7ef2 100644 --- a/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts +++ b/packages/web/src/javascripts/Utils/Items/Search/doesItemMatchSearchQuery.ts @@ -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) { + return item.title && item.title.length > 0 ? item.title : isNote(item) ? item.preview_plain : '' +} + +function getItemSearchableString(item: DecryptedItemInterface, 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, 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