From 1bd51350b29c74ac192f9d114698e905c3487654 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Tue, 15 Jul 2025 14:10:45 +0530 Subject: [PATCH 1/9] fix: update find suggestion logic --- .../editor/src/core/extensions/emoji/emoji.ts | 2 + .../extensions/emoji/find-suggestion-match.ts | 107 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 packages/editor/src/core/extensions/emoji/find-suggestion-match.ts diff --git a/packages/editor/src/core/extensions/emoji/emoji.ts b/packages/editor/src/core/extensions/emoji/emoji.ts index 12fe7e06f3b..e03ad143995 100644 --- a/packages/editor/src/core/extensions/emoji/emoji.ts +++ b/packages/editor/src/core/extensions/emoji/emoji.ts @@ -15,6 +15,7 @@ import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; import emojiRegex from "emoji-regex"; import { isEmojiSupported } from "is-emoji-supported"; +import { customFindSuggestionMatch } from "./find-suggestion-match"; declare module "@tiptap/core" { interface Commands { @@ -343,6 +344,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/find-suggestion-match.ts b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts new file mode 100644 index 00000000000..27c96093185 --- /dev/null +++ b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts @@ -0,0 +1,107 @@ +import { escapeForRegEx, Range } from "@tiptap/core"; +import { ResolvedPos } from "@tiptap/pm/model"; + +export interface Trigger { + char: string; + allowSpaces: boolean; + allowToIncludeChar: boolean; + allowedPrefixes: string[] | null; + startOfLine: boolean; + $position: ResolvedPos; +} + +export type SuggestionMatch = { + range: Range; + query: string; + text: string; +} | null; + +export function customFindSuggestionMatch(config: Trigger): SuggestionMatch { + const { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = config; + console.log("customFindSuggestionMatch"); + + 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"); + + // Get the text block that contains the current position + const textBlock = $position.parent; + + if (!textBlock.isTextblock) { + return null; + } + + // Get the text content of the entire block + const blockText = textBlock.textContent; + const blockStart = $position.start(); + const relativePos = $position.pos - blockStart; + + if (relativePos < 0) { + return null; + } + + // Look for the trigger character in the text before the current position + const textBeforeCursor = blockText.slice(0, relativePos); + + // Find the last occurrence of the trigger character + const lastTriggerIndex = textBeforeCursor.lastIndexOf(char); + + if (lastTriggerIndex === -1) { + return null; + } + + // Check if the trigger character has an allowed prefix + const prefixChar = lastTriggerIndex > 0 ? textBeforeCursor[lastTriggerIndex - 1] : "\0"; + const matchPrefixIsAllowed = new RegExp(`^[${allowedPrefixes?.join("")}\0]?$`).test(prefixChar); + + if (allowedPrefixes !== null && !matchPrefixIsAllowed) { + return null; + } + + // Extract the potential match text from the trigger character to the cursor position + const matchText = textBeforeCursor.slice(lastTriggerIndex); + + // Test if this matches our regex pattern + const match = matchText.match(regexp); + + if (!match) { + return null; + } + + // Calculate the absolute positions + const from = blockStart + lastTriggerIndex; + 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(blockText.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) { + // Additional check: make sure we're not inside a code block or other restricted context + const $from = $position.doc.resolve(from); + if ($from.parent.type.spec.code) { + return null; + } + + return { + range: { + from, + to, + }, + query: match[0].slice(char.length), + text: match[0], + }; + } + + return null; +} From fffc5c7db860bec1d4421d7b603b5f45a823f743 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Tue, 15 Jul 2025 14:20:14 +0530 Subject: [PATCH 2/9] refactor: remove logs --- .../editor/src/core/extensions/emoji/find-suggestion-match.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts index 27c96093185..ce4b08f1250 100644 --- a/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts +++ b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts @@ -18,7 +18,6 @@ export type SuggestionMatch = { export function customFindSuggestionMatch(config: Trigger): SuggestionMatch { const { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = config; - console.log("customFindSuggestionMatch"); const allowSpaces = allowSpacesOption && !allowToIncludeChar; From 47a27a471c378ea03fd8b81d7cebb8db561d45e4 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Tue, 15 Jul 2025 17:16:01 +0530 Subject: [PATCH 3/9] refactor : make logic simpler --- .../extensions/emoji/find-suggestion-match.ts | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts index ce4b08f1250..c5f90ad19dc 100644 --- a/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts +++ b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts @@ -29,69 +29,51 @@ export function customFindSuggestionMatch(config: Trigger): SuggestionMatch { ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${finalEscapedChar}|$)`, "gm") : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${finalEscapedChar}]*`, "gm"); - // Get the text block that contains the current position - const textBlock = $position.parent; - - if (!textBlock.isTextblock) { + // 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 text content of the entire block - const blockText = textBlock.textContent; - const blockStart = $position.start(); - const relativePos = $position.pos - blockStart; + // 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 (relativePos < 0) { + if (!text) { return null; } - // Look for the trigger character in the text before the current position - const textBeforeCursor = blockText.slice(0, relativePos); - - // Find the last occurrence of the trigger character - const lastTriggerIndex = textBeforeCursor.lastIndexOf(char); + const textFrom = paragraphStart; + const match = Array.from(text.matchAll(regexp)).pop(); - if (lastTriggerIndex === -1) { + if (!match || match.input === undefined || match.index === undefined) { return null; } - // Check if the trigger character has an allowed prefix - const prefixChar = lastTriggerIndex > 0 ? textBeforeCursor[lastTriggerIndex - 1] : "\0"; - const matchPrefixIsAllowed = new RegExp(`^[${allowedPrefixes?.join("")}\0]?$`).test(prefixChar); + // 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("")}\0]?$`).test(matchPrefix); if (allowedPrefixes !== null && !matchPrefixIsAllowed) { return null; } - // Extract the potential match text from the trigger character to the cursor position - const matchText = textBeforeCursor.slice(lastTriggerIndex); - - // Test if this matches our regex pattern - const match = matchText.match(regexp); - - if (!match) { - return null; - } - - // Calculate the absolute positions - const from = blockStart + lastTriggerIndex; + // 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(blockText.slice(to - 1, to + 1))) { + 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) { - // Additional check: make sure we're not inside a code block or other restricted context - const $from = $position.doc.resolve(from); - if ($from.parent.type.spec.code) { - return null; - } - return { range: { from, From 1da78efd9a9b2fca06d6be91e84a21e7fad3feec Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Tue, 15 Jul 2025 20:45:13 +0530 Subject: [PATCH 4/9] feat: check for one char to show suggestion --- .../emoji/components/emojis-list.tsx | 93 ++++++++++--------- .../src/core/extensions/emoji/suggestion.ts | 2 + 2 files changed, 50 insertions(+), 45 deletions(-) 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..e310ebbf9a9 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); @@ -142,52 +143,54 @@ export const EmojiList = forwardRef((props, ref) = ); return ( -
-
- {items.length ? ( - items.map((item, index) => { - const isSelected = index === selectedIndex; - const emojiKey = item.shortcodes.join(" - "); - - return ( - - ); - }) - ) : ( -
No emojis found
- )} + onClick={() => selectItem(index)} + onMouseEnter={() => setSelectedIndex(index)} + > + + {item.fallbackImage ? ( + {item.name} + ) : ( + item.emoji + )} + + + :{item.name}: + + + ); + }) + ) : ( +
No emojis found
+ )} +
- + ) ); }); 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, }); }, From 59ef1d22a66604aabf2b983f362f0af69c78b267 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Tue, 15 Jul 2025 20:46:52 +0530 Subject: [PATCH 5/9] refactor : import types from extension --- .../extensions/emoji/find-suggestion-match.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts index c5f90ad19dc..bd4e82cd6ae 100644 --- a/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts +++ b/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts @@ -1,20 +1,5 @@ -import { escapeForRegEx, Range } from "@tiptap/core"; -import { ResolvedPos } from "@tiptap/pm/model"; - -export interface Trigger { - char: string; - allowSpaces: boolean; - allowToIncludeChar: boolean; - allowedPrefixes: string[] | null; - startOfLine: boolean; - $position: ResolvedPos; -} - -export type SuggestionMatch = { - range: Range; - query: string; - text: string; -} | null; +import { escapeForRegEx } from "@tiptap/core"; +import { Trigger, SuggestionMatch } from "@tiptap/suggestion"; export function customFindSuggestionMatch(config: Trigger): SuggestionMatch { const { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = config; From 455c11408bf59712dc54e7eddac3ecda183943ee Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Wed, 16 Jul 2025 18:58:36 +0530 Subject: [PATCH 6/9] refactor: add early return --- .../emoji/components/emojis-list.tsx | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) 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 e310ebbf9a9..049d3b2d840 100644 --- a/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx +++ b/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx @@ -142,55 +142,57 @@ export const EmojiList = forwardRef((props, ref) = [handleKeyDown] ); + if (query.length <= 0) { + return null; + } + return ( - query.length > 0 && ( -
-
- {items.length ? ( - items.map((item, index) => { - const isSelected = index === selectedIndex; - const emojiKey = item.shortcodes.join(" - "); - - return ( - - ); - }) - ) : ( -
No emojis found
- )} -
+ + + :{item.name}: + + + ); + }) + ) : ( +
No emojis found
+ )}
- ) + ); }); From dadb2da5e0cd7f56988af035e9ba0d2dd2911c70 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Wed, 16 Jul 2025 20:19:03 +0530 Subject: [PATCH 7/9] refactor : put custom suggestion in helper --- packages/editor/src/core/extensions/emoji/emoji.ts | 5 +++-- .../{extensions/emoji => helpers}/find-suggestion-match.ts | 0 2 files changed, 3 insertions(+), 2 deletions(-) rename packages/editor/src/core/{extensions/emoji => helpers}/find-suggestion-match.ts (100%) diff --git a/packages/editor/src/core/extensions/emoji/emoji.ts b/packages/editor/src/core/extensions/emoji/emoji.ts index e03ad143995..f69041ee256 100644 --- a/packages/editor/src/core/extensions/emoji/emoji.ts +++ b/packages/editor/src/core/extensions/emoji/emoji.ts @@ -15,7 +15,8 @@ import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; import emojiRegex from "emoji-regex"; import { isEmojiSupported } from "is-emoji-supported"; -import { customFindSuggestionMatch } from "./find-suggestion-match"; +// helpers +import { customFindSuggestionMatch } from "@/helpers/find-suggestion-match"; declare module "@tiptap/core" { interface Commands { @@ -103,7 +104,7 @@ export const Emoji = Node.create({ enableEmoticons: false, forceFallbackImages: false, suggestion: { - char: ":", + char: "*", pluginKey: EmojiSuggestionPluginKey, command: ({ editor, range, props }) => { // increase range.to by one when the next node is of type "text" diff --git a/packages/editor/src/core/extensions/emoji/find-suggestion-match.ts b/packages/editor/src/core/helpers/find-suggestion-match.ts similarity index 100% rename from packages/editor/src/core/extensions/emoji/find-suggestion-match.ts rename to packages/editor/src/core/helpers/find-suggestion-match.ts From af405f80c1d8e437b075ceac7f05b158ddfc8b59 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Wed, 16 Jul 2025 20:22:04 +0530 Subject: [PATCH 8/9] fix : char --- packages/editor/src/core/extensions/emoji/emoji.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/core/extensions/emoji/emoji.ts b/packages/editor/src/core/extensions/emoji/emoji.ts index f69041ee256..581a094b8c9 100644 --- a/packages/editor/src/core/extensions/emoji/emoji.ts +++ b/packages/editor/src/core/extensions/emoji/emoji.ts @@ -104,7 +104,7 @@ export const Emoji = Node.create({ enableEmoticons: false, forceFallbackImages: false, suggestion: { - char: "*", + char: ":", pluginKey: EmojiSuggestionPluginKey, command: ({ editor, range, props }) => { // increase range.to by one when the next node is of type "text" From 6347567d697571d5a4e9a7237ae7edc733cf0fd9 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Thu, 17 Jul 2025 13:04:57 +0530 Subject: [PATCH 9/9] fix: types --- packages/editor/src/core/helpers/find-suggestion-match.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/core/helpers/find-suggestion-match.ts b/packages/editor/src/core/helpers/find-suggestion-match.ts index bd4e82cd6ae..5db2f94749a 100644 --- a/packages/editor/src/core/helpers/find-suggestion-match.ts +++ b/packages/editor/src/core/helpers/find-suggestion-match.ts @@ -1,7 +1,7 @@ import { escapeForRegEx } from "@tiptap/core"; import { Trigger, SuggestionMatch } from "@tiptap/suggestion"; -export function customFindSuggestionMatch(config: Trigger): SuggestionMatch { +export function customFindSuggestionMatch(config: Trigger): SuggestionMatch | null { const { char, allowSpaces: allowSpacesOption, allowToIncludeChar, allowedPrefixes, startOfLine, $position } = config; const allowSpaces = allowSpacesOption && !allowToIncludeChar; @@ -40,9 +40,9 @@ export function customFindSuggestionMatch(config: Trigger): SuggestionMatch { // 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("")}\0]?$`).test(matchPrefix); + const matchPrefixIsAllowed = new RegExp(`^[${allowedPrefixes?.join("")}]?$`).test(matchPrefix); - if (allowedPrefixes !== null && !matchPrefixIsAllowed) { + if (allowedPrefixes && allowedPrefixes.length > 0 && !matchPrefixIsAllowed) { return null; }