From 6bb79df0eb21e27c500ada0c08098c23e0a60063 Mon Sep 17 00:00:00 2001 From: Vipin Chaudhary Date: Thu, 17 Jul 2025 13:07:12 +0530 Subject: [PATCH] [WIKI-547] fix: update find suggestion logic for emoji extension (#7411) * fix: update find suggestion logic * refactor: remove logs * refactor : make logic simpler * feat: check for one char to show suggestion * refactor : import types from extension * refactor: add early return * refactor : put custom suggestion in helper * fix : char * fix: types --- .../emoji/components/emojis-list.tsx | 7 +- .../editor/src/core/extensions/emoji/emoji.ts | 3 + .../src/core/extensions/emoji/suggestion.ts | 2 + .../src/core/helpers/find-suggestion-match.ts | 73 +++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 packages/editor/src/core/helpers/find-suggestion-match.ts diff --git a/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx b/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx index 706dbc685..049d3b2d8 100644 --- a/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx +++ b/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx @@ -17,6 +17,7 @@ export interface EmojiListProps { items: EmojiItem[]; command: (item: { name: string }) => void; editor: Editor; + query: string; } export interface EmojiListRef { @@ -43,7 +44,7 @@ const updatePosition = (editor: Editor, element: HTMLElement) => { }; export const EmojiList = forwardRef((props, ref) => { - const { items, command, editor } = props; + const { items, command, editor, query } = props; const [selectedIndex, setSelectedIndex] = useState(0); const [isVisible, setIsVisible] = useState(false); const containerRef = useRef(null); @@ -141,6 +142,10 @@ export const EmojiList = forwardRef((props, ref) = [handleKeyDown] ); + if (query.length <= 0) { + return null; + } + return (
{ @@ -343,6 +345,7 @@ export const Emoji = Node.create({ return [ Suggestion({ editor: this.editor, + findSuggestionMatch: customFindSuggestionMatch, ...this.options.suggestion, }), diff --git a/packages/editor/src/core/extensions/emoji/suggestion.ts b/packages/editor/src/core/extensions/emoji/suggestion.ts index caadb483c..a0aa91688 100644 --- a/packages/editor/src/core/extensions/emoji/suggestion.ts +++ b/packages/editor/src/core/extensions/emoji/suggestion.ts @@ -64,6 +64,7 @@ const emojiSuggestion: EmojiOptions["suggestion"] = { items: props.items, command: props.command, editor: props.editor, + query: props.query, }, editor: props.editor, }); @@ -81,6 +82,7 @@ const emojiSuggestion: EmojiOptions["suggestion"] = { items: props.items, command: props.command, editor: props.editor, + query: props.query, }); }, diff --git a/packages/editor/src/core/helpers/find-suggestion-match.ts b/packages/editor/src/core/helpers/find-suggestion-match.ts new file mode 100644 index 000000000..5db2f9474 --- /dev/null +++ b/packages/editor/src/core/helpers/find-suggestion-match.ts @@ -0,0 +1,73 @@ +import { escapeForRegEx } from "@tiptap/core"; +import { Trigger, SuggestionMatch } from "@tiptap/suggestion"; + +export function customFindSuggestionMatch(config: Trigger): SuggestionMatch | null { + const { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = config; + + const allowSpaces = allowSpacesOption && !allowToIncludeChar; + + const escapedChar = escapeForRegEx(char); + const suffix = new RegExp(`\\s${escapedChar}$`); + const prefix = startOfLine ? "^" : ""; + const finalEscapedChar = allowToIncludeChar ? "" : escapedChar; + const regexp = allowSpaces + ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${finalEscapedChar}|$)`, "gm") + : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${finalEscapedChar}]*`, "gm"); + + // Instead of just looking at nodeBefore.text, we need to extract text from the current paragraph + // to properly handle text with decorators like bold, italic, etc. + const currentParagraph = $position.parent; + if (!currentParagraph.isTextblock) { + return null; + } + + // Get the start position of the current paragraph + const paragraphStart = $position.start(); + // Extract text content using textBetween which handles text across different nodes/marks + const text = $position.doc.textBetween(paragraphStart, $position.pos, "\0", "\0"); + + if (!text) { + return null; + } + + const textFrom = paragraphStart; + const match = Array.from(text.matchAll(regexp)).pop(); + + if (!match || match.input === undefined || match.index === undefined) { + return null; + } + + // JavaScript doesn't have lookbehinds. This hacks a check that first character + // is a space or the start of the line + const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index); + const matchPrefixIsAllowed = new RegExp(`^[${allowedPrefixes?.join("")}]?$`).test(matchPrefix); + + if (allowedPrefixes && allowedPrefixes.length > 0 && !matchPrefixIsAllowed) { + return null; + } + + // The absolute position of the match in the document + const from = textFrom + match.index; + let to = from + match[0].length; + + // Edge case handling; if spaces are allowed and we're directly in between + // two triggers + if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) { + match[0] += " "; + to += 1; + } + + // If the $position is located within the matched substring, return that range + if (from < $position.pos && to >= $position.pos) { + return { + range: { + from, + to, + }, + query: match[0].slice(char.length), + text: match[0], + }; + } + + return null; +}