[WIKI-728] fix: update emoji insertion method #7962

This commit is contained in:
Vipin Chaudhary 2025-11-06 19:46:32 +05:30 committed by GitHub
parent 0a738d419e
commit fd542a85fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 52 additions and 28 deletions

View file

@ -19,10 +19,11 @@ export type EmojiListRef = {
export type EmojisListDropdownProps = SuggestionProps<EmojiItem, { name: string }> & { export type EmojisListDropdownProps = SuggestionProps<EmojiItem, { name: string }> & {
onClose: () => void; onClose: () => void;
forceOpen?: boolean;
}; };
export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownProps>((props, ref) => { export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownProps>((props, ref) => {
const { items, command, query, onClose } = props; const { items, command, query, onClose, forceOpen = false } = props;
// states // states
const [selectedIndex, setSelectedIndex] = useState<number>(0); const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@ -41,7 +42,13 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(event: KeyboardEvent): boolean => { (event: KeyboardEvent): boolean => {
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; return false;
} }
@ -62,7 +69,7 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro
return false; return false;
}, },
[query.length, items.length, selectItem, selectedIndex] [items.length, query.length, forceOpen, selectItem, selectedIndex]
); );
// Show animation // Show animation
@ -101,7 +108,7 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro
useOutsideClickDetector(dropdownContainerRef, onClose); useOutsideClickDetector(dropdownContainerRef, onClose);
if (query.length <= 0) return null; if (query.length === 0 && !forceOpen) return null;
return ( return (
<> <>

View file

@ -11,22 +11,17 @@ import {
removeDuplicates, removeDuplicates,
} from "@tiptap/core"; } from "@tiptap/core";
import { EmojiStorage, emojis, emojiToShortcode, shortcodeToEmoji } from "@tiptap/extension-emoji"; 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 Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
import emojiRegex from "emoji-regex"; import emojiRegex from "emoji-regex";
import { isEmojiSupported } from "is-emoji-supported"; import { isEmojiSupported } from "is-emoji-supported";
// helpers // helpers
import { customFindSuggestionMatch } from "@/helpers/find-suggestion-match"; import { customFindSuggestionMatch } from "@/helpers/find-suggestion-match";
declare module "@tiptap/core" { // Extended storage type to include our custom forceOpen flag
interface Commands<ReturnType> { export interface ExtendedEmojiStorage extends EmojiStorage {
emoji: { forceOpen: boolean;
/**
* Add an emoji
*/
setEmoji: (shortcode: string) => ReturnType;
};
}
} }
export type EmojiItem = { export type EmojiItem = {
@ -114,18 +109,22 @@ export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
editor editor
.chain() .chain()
.focus() .focus()
.insertContentAt(range, [ .command(({ tr, state, dispatch }) => {
{ if (!dispatch) return true;
type: this.name,
attrs: props, const { schema } = state;
}, const emojiNode = schema.nodes[this.name].create(props);
{ const spaceNode = schema.text(" ");
type: "text",
text: " ", const fragment = Fragment.from([emojiNode, spaceNode]);
},
]) tr.replaceWith(range.from, range.to, fragment);
.command(({ tr, state }) => {
tr.setStoredMarks(state.doc.resolve(state.selection.to - 2).marks()); const newPos = range.from + fragment.size;
tr.setSelection(TextSelection.near(tr.doc.resolve(newPos)));
tr.setStoredMarks(tr.doc.resolve(range.from).marks());
return true; return true;
}) })
.run(); .run();
@ -157,6 +156,7 @@ export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
return { return {
emojis: this.options.emojis, emojis: this.options.emojis,
isSupported: (emojiItem) => (emojiItem.version ? supportMap[emojiItem.version] : false), isSupported: (emojiItem) => (emojiItem.version ? supportMap[emojiItem.version] : false),
forceOpen: false,
}; };
}, },

View file

@ -7,6 +7,7 @@ import { updateFloatingUIFloaterPosition } from "@/helpers/floating-ui";
import { CommandListInstance, DROPDOWN_NAVIGATION_KEYS } from "@/helpers/tippy"; import { CommandListInstance, DROPDOWN_NAVIGATION_KEYS } from "@/helpers/tippy";
// local imports // local imports
import { type EmojiItem, EmojisListDropdown, EmojisListDropdownProps } from "./components/emojis-list"; import { type EmojiItem, EmojisListDropdown, EmojisListDropdownProps } from "./components/emojis-list";
import type { ExtendedEmojiStorage } from "./emoji";
const DEFAULT_EMOJIS = ["+1", "-1", "smile", "orange_heart", "eyes"]; const DEFAULT_EMOJIS = ["+1", "-1", "smile", "orange_heart", "eyes"];
@ -54,16 +55,21 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = {
component?.destroy(); component?.destroy();
component = null; component = null;
(editor || editorRef)?.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.EMOJI); (editor || editorRef)?.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.EMOJI);
const emojiStorage = editor?.storage.emoji as ExtendedEmojiStorage;
emojiStorage.forceOpen = false;
cleanup(); cleanup();
}; };
return { return {
onStart: (props) => { onStart: (props) => {
editorRef = props.editor; editorRef = props.editor;
const emojiStorage = props.editor.storage.emoji as ExtendedEmojiStorage;
const forceOpen = emojiStorage.forceOpen || false;
component = new ReactRenderer<CommandListInstance, EmojisListDropdownProps>(EmojisListDropdown, { component = new ReactRenderer<CommandListInstance, EmojisListDropdownProps>(EmojisListDropdown, {
props: { props: {
...props, ...props,
onClose: () => handleClose(props.editor), onClose: () => handleClose(props.editor),
forceOpen,
} satisfies EmojisListDropdownProps, } satisfies EmojisListDropdownProps,
editor: props.editor, editor: props.editor,
className: "fixed z-[100]", className: "fixed z-[100]",
@ -76,7 +82,9 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = {
onUpdate: (props) => { onUpdate: (props) => {
if (!component || !component.element) return; 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; if (!props.clientRect) return;
cleanup(); cleanup();
cleanup = updateFloatingUIFloaterPosition(props.editor, component.element as HTMLElement).cleanup; cleanup = updateFloatingUIFloaterPosition(props.editor, component.element as HTMLElement).cleanup;

View file

@ -33,6 +33,7 @@ import {
insertImage, insertImage,
insertCallout, insertCallout,
setText, setText,
openEmojiPicker,
} from "@/helpers/editor-commands"; } from "@/helpers/editor-commands";
// plane editor extensions // plane editor extensions
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions"; import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
@ -198,7 +199,7 @@ export const getSlashCommandFilteredSections =
searchTerms: ["emoji", "icons", "reaction", "emoticon", "emotags"], searchTerms: ["emoji", "icons", "reaction", "emoticon", "emotags"],
icon: <Smile className="size-3.5" />, icon: <Smile className="size-3.5" />,
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor.chain().focus().insertContentAt(range, "<p>:</p>").run(); openEmojiPicker(editor, range);
}, },
}, },
], ],

View file

@ -5,6 +5,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension";
import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text"; import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text";
import type { InsertImageComponentProps } from "@/extensions/custom-image/types"; import type { InsertImageComponentProps } from "@/extensions/custom-image/types";
// helpers // helpers
import { ExtendedEmojiStorage } from "@/extensions/emoji/emoji";
import { findTableAncestor } from "@/helpers/common"; import { findTableAncestor } from "@/helpers/common";
export const setText = (editor: Editor, range?: Range) => { 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(); if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
else editor.chain().focus().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();
};