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 706dbc685a2..049d3b2d840 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 caadb483c79..a0aa9168806 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 00000000000..5db2f94749a --- /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; +}