From fd542a85fa3ae523092204846b0657b66fc9e98c Mon Sep 17 00:00:00 2001 From: Vipin Chaudhary Date: Thu, 6 Nov 2025 19:46:32 +0530 Subject: [PATCH] [WIKI-728] fix: update emoji insertion method #7962 --- .../emoji/components/emojis-list.tsx | 15 +++++-- .../editor/src/core/extensions/emoji/emoji.ts | 44 +++++++++---------- .../src/core/extensions/emoji/suggestion.ts | 10 ++++- .../slash-commands/command-items-list.tsx | 3 +- .../src/core/helpers/editor-commands.ts | 8 ++++ 5 files changed, 52 insertions(+), 28 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 39867662c..6edea4010 100644 --- a/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx +++ b/packages/editor/src/core/extensions/emoji/components/emojis-list.tsx @@ -19,10 +19,11 @@ export type EmojiListRef = { export type EmojisListDropdownProps = SuggestionProps & { onClose: () => void; + forceOpen?: boolean; }; export const EmojisListDropdown = forwardRef((props, ref) => { - const { items, command, query, onClose } = props; + const { items, command, query, onClose, forceOpen = false } = props; // states const [selectedIndex, setSelectedIndex] = useState(0); const [isVisible, setIsVisible] = useState(false); @@ -41,7 +42,13 @@ export const EmojisListDropdown = forwardRef { - if (query.length <= 0) { + // Allow keyboard navigation if we have items to show + if (items.length === 0) { + return false; + } + + // Don't handle keyboard if modal shouldn't be visible (query empty without forceOpen) + if (query.length === 0 && !forceOpen) { return false; } @@ -62,7 +69,7 @@ export const EmojisListDropdown = forwardRef diff --git a/packages/editor/src/core/extensions/emoji/emoji.ts b/packages/editor/src/core/extensions/emoji/emoji.ts index 2963b85d7..338573b1c 100644 --- a/packages/editor/src/core/extensions/emoji/emoji.ts +++ b/packages/editor/src/core/extensions/emoji/emoji.ts @@ -11,22 +11,17 @@ import { removeDuplicates, } from "@tiptap/core"; import { EmojiStorage, emojis, emojiToShortcode, shortcodeToEmoji } from "@tiptap/extension-emoji"; -import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +import { Fragment } from "@tiptap/pm/model"; +import { Plugin, PluginKey, TextSelection, Transaction } from "@tiptap/pm/state"; import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; import emojiRegex from "emoji-regex"; import { isEmojiSupported } from "is-emoji-supported"; // helpers import { customFindSuggestionMatch } from "@/helpers/find-suggestion-match"; -declare module "@tiptap/core" { - interface Commands { - emoji: { - /** - * Add an emoji - */ - setEmoji: (shortcode: string) => ReturnType; - }; - } +// Extended storage type to include our custom forceOpen flag +export interface ExtendedEmojiStorage extends EmojiStorage { + forceOpen: boolean; } export type EmojiItem = { @@ -114,18 +109,22 @@ export const Emoji = Node.create({ editor .chain() .focus() - .insertContentAt(range, [ - { - type: this.name, - attrs: props, - }, - { - type: "text", - text: " ", - }, - ]) - .command(({ tr, state }) => { - tr.setStoredMarks(state.doc.resolve(state.selection.to - 2).marks()); + .command(({ tr, state, dispatch }) => { + if (!dispatch) return true; + + const { schema } = state; + const emojiNode = schema.nodes[this.name].create(props); + const spaceNode = schema.text(" "); + + const fragment = Fragment.from([emojiNode, spaceNode]); + + tr.replaceWith(range.from, range.to, fragment); + + const newPos = range.from + fragment.size; + tr.setSelection(TextSelection.near(tr.doc.resolve(newPos))); + + tr.setStoredMarks(tr.doc.resolve(range.from).marks()); + return true; }) .run(); @@ -157,6 +156,7 @@ export const Emoji = Node.create({ return { emojis: this.options.emojis, isSupported: (emojiItem) => (emojiItem.version ? supportMap[emojiItem.version] : false), + forceOpen: false, }; }, diff --git a/packages/editor/src/core/extensions/emoji/suggestion.ts b/packages/editor/src/core/extensions/emoji/suggestion.ts index c67d68dae..06ddeb1e6 100644 --- a/packages/editor/src/core/extensions/emoji/suggestion.ts +++ b/packages/editor/src/core/extensions/emoji/suggestion.ts @@ -7,6 +7,7 @@ import { updateFloatingUIFloaterPosition } from "@/helpers/floating-ui"; import { CommandListInstance, DROPDOWN_NAVIGATION_KEYS } from "@/helpers/tippy"; // local imports import { type EmojiItem, EmojisListDropdown, EmojisListDropdownProps } from "./components/emojis-list"; +import type { ExtendedEmojiStorage } from "./emoji"; const DEFAULT_EMOJIS = ["+1", "-1", "smile", "orange_heart", "eyes"]; @@ -54,16 +55,21 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = { component?.destroy(); component = null; (editor || editorRef)?.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.EMOJI); + const emojiStorage = editor?.storage.emoji as ExtendedEmojiStorage; + emojiStorage.forceOpen = false; cleanup(); }; return { onStart: (props) => { editorRef = props.editor; + const emojiStorage = props.editor.storage.emoji as ExtendedEmojiStorage; + const forceOpen = emojiStorage.forceOpen || false; component = new ReactRenderer(EmojisListDropdown, { props: { ...props, onClose: () => handleClose(props.editor), + forceOpen, } satisfies EmojisListDropdownProps, editor: props.editor, className: "fixed z-[100]", @@ -76,7 +82,9 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = { onUpdate: (props) => { if (!component || !component.element) return; - component.updateProps(props); + const emojiStorage = props.editor.storage.emoji as ExtendedEmojiStorage; + const forceOpen = emojiStorage.forceOpen || false; + component.updateProps({ ...props, forceOpen }); if (!props.clientRect) return; cleanup(); cleanup = updateFloatingUIFloaterPosition(props.editor, component.element as HTMLElement).cleanup; diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index 54d717de3..a0e299438 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -33,6 +33,7 @@ import { insertImage, insertCallout, setText, + openEmojiPicker, } from "@/helpers/editor-commands"; // plane editor extensions import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions"; @@ -198,7 +199,7 @@ export const getSlashCommandFilteredSections = searchTerms: ["emoji", "icons", "reaction", "emoticon", "emotags"], icon: , command: ({ editor, range }) => { - editor.chain().focus().insertContentAt(range, "

:

").run(); + openEmojiPicker(editor, range); }, }, ], diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 3217e710b..8d776e609 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -5,6 +5,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension"; import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text"; import type { InsertImageComponentProps } from "@/extensions/custom-image/types"; // helpers +import { ExtendedEmojiStorage } from "@/extensions/emoji/emoji"; import { findTableAncestor } from "@/helpers/common"; export const setText = (editor: Editor, range?: Range) => { @@ -184,3 +185,10 @@ export const insertCallout = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).insertCallout().run(); else editor.chain().focus().insertCallout().run(); }; + +export const openEmojiPicker = (editor: Editor, range?: Range) => { + if (range) editor.chain().focus().deleteRange(range).run(); + const emojiStorage = editor.storage.emoji as ExtendedEmojiStorage; + emojiStorage.forceOpen = true; + editor.chain().focus().insertContent(":").run(); +};