Skip to content

Commit fd542a8

Browse files
[WIKI-728] fix: update emoji insertion method #7962
1 parent 0a738d4 commit fd542a8

File tree

5 files changed

+52
-28
lines changed

5 files changed

+52
-28
lines changed

packages/editor/src/core/extensions/emoji/components/emojis-list.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ export type EmojiListRef = {
1919

2020
export type EmojisListDropdownProps = SuggestionProps<EmojiItem, { name: string }> & {
2121
onClose: () => void;
22+
forceOpen?: boolean;
2223
};
2324

2425
export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownProps>((props, ref) => {
25-
const { items, command, query, onClose } = props;
26+
const { items, command, query, onClose, forceOpen = false } = props;
2627
// states
2728
const [selectedIndex, setSelectedIndex] = useState<number>(0);
2829
const [isVisible, setIsVisible] = useState(false);
@@ -41,7 +42,13 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro
4142

4243
const handleKeyDown = useCallback(
4344
(event: KeyboardEvent): boolean => {
44-
if (query.length <= 0) {
45+
// Allow keyboard navigation if we have items to show
46+
if (items.length === 0) {
47+
return false;
48+
}
49+
50+
// Don't handle keyboard if modal shouldn't be visible (query empty without forceOpen)
51+
if (query.length === 0 && !forceOpen) {
4552
return false;
4653
}
4754

@@ -62,7 +69,7 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro
6269

6370
return false;
6471
},
65-
[query.length, items.length, selectItem, selectedIndex]
72+
[items.length, query.length, forceOpen, selectItem, selectedIndex]
6673
);
6774

6875
// Show animation
@@ -101,7 +108,7 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro
101108

102109
useOutsideClickDetector(dropdownContainerRef, onClose);
103110

104-
if (query.length <= 0) return null;
111+
if (query.length === 0 && !forceOpen) return null;
105112

106113
return (
107114
<>

packages/editor/src/core/extensions/emoji/emoji.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,17 @@ import {
1111
removeDuplicates,
1212
} from "@tiptap/core";
1313
import { EmojiStorage, emojis, emojiToShortcode, shortcodeToEmoji } from "@tiptap/extension-emoji";
14-
import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
14+
import { Fragment } from "@tiptap/pm/model";
15+
import { Plugin, PluginKey, TextSelection, Transaction } from "@tiptap/pm/state";
1516
import Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
1617
import emojiRegex from "emoji-regex";
1718
import { isEmojiSupported } from "is-emoji-supported";
1819
// helpers
1920
import { customFindSuggestionMatch } from "@/helpers/find-suggestion-match";
2021

21-
declare module "@tiptap/core" {
22-
interface Commands<ReturnType> {
23-
emoji: {
24-
/**
25-
* Add an emoji
26-
*/
27-
setEmoji: (shortcode: string) => ReturnType;
28-
};
29-
}
22+
// Extended storage type to include our custom forceOpen flag
23+
export interface ExtendedEmojiStorage extends EmojiStorage {
24+
forceOpen: boolean;
3025
}
3126

3227
export type EmojiItem = {
@@ -114,18 +109,22 @@ export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
114109
editor
115110
.chain()
116111
.focus()
117-
.insertContentAt(range, [
118-
{
119-
type: this.name,
120-
attrs: props,
121-
},
122-
{
123-
type: "text",
124-
text: " ",
125-
},
126-
])
127-
.command(({ tr, state }) => {
128-
tr.setStoredMarks(state.doc.resolve(state.selection.to - 2).marks());
112+
.command(({ tr, state, dispatch }) => {
113+
if (!dispatch) return true;
114+
115+
const { schema } = state;
116+
const emojiNode = schema.nodes[this.name].create(props);
117+
const spaceNode = schema.text(" ");
118+
119+
const fragment = Fragment.from([emojiNode, spaceNode]);
120+
121+
tr.replaceWith(range.from, range.to, fragment);
122+
123+
const newPos = range.from + fragment.size;
124+
tr.setSelection(TextSelection.near(tr.doc.resolve(newPos)));
125+
126+
tr.setStoredMarks(tr.doc.resolve(range.from).marks());
127+
129128
return true;
130129
})
131130
.run();
@@ -157,6 +156,7 @@ export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
157156
return {
158157
emojis: this.options.emojis,
159158
isSupported: (emojiItem) => (emojiItem.version ? supportMap[emojiItem.version] : false),
159+
forceOpen: false,
160160
};
161161
},
162162

packages/editor/src/core/extensions/emoji/suggestion.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { updateFloatingUIFloaterPosition } from "@/helpers/floating-ui";
77
import { CommandListInstance, DROPDOWN_NAVIGATION_KEYS } from "@/helpers/tippy";
88
// local imports
99
import { type EmojiItem, EmojisListDropdown, EmojisListDropdownProps } from "./components/emojis-list";
10+
import type { ExtendedEmojiStorage } from "./emoji";
1011

1112
const DEFAULT_EMOJIS = ["+1", "-1", "smile", "orange_heart", "eyes"];
1213

@@ -54,16 +55,21 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = {
5455
component?.destroy();
5556
component = null;
5657
(editor || editorRef)?.commands.removeActiveDropbarExtension(CORE_EXTENSIONS.EMOJI);
58+
const emojiStorage = editor?.storage.emoji as ExtendedEmojiStorage;
59+
emojiStorage.forceOpen = false;
5760
cleanup();
5861
};
5962

6063
return {
6164
onStart: (props) => {
6265
editorRef = props.editor;
66+
const emojiStorage = props.editor.storage.emoji as ExtendedEmojiStorage;
67+
const forceOpen = emojiStorage.forceOpen || false;
6368
component = new ReactRenderer<CommandListInstance, EmojisListDropdownProps>(EmojisListDropdown, {
6469
props: {
6570
...props,
6671
onClose: () => handleClose(props.editor),
72+
forceOpen,
6773
} satisfies EmojisListDropdownProps,
6874
editor: props.editor,
6975
className: "fixed z-[100]",
@@ -76,7 +82,9 @@ export const emojiSuggestion: EmojiOptions["suggestion"] = {
7682

7783
onUpdate: (props) => {
7884
if (!component || !component.element) return;
79-
component.updateProps(props);
85+
const emojiStorage = props.editor.storage.emoji as ExtendedEmojiStorage;
86+
const forceOpen = emojiStorage.forceOpen || false;
87+
component.updateProps({ ...props, forceOpen });
8088
if (!props.clientRect) return;
8189
cleanup();
8290
cleanup = updateFloatingUIFloaterPosition(props.editor, component.element as HTMLElement).cleanup;

packages/editor/src/core/extensions/slash-commands/command-items-list.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
insertImage,
3434
insertCallout,
3535
setText,
36+
openEmojiPicker,
3637
} from "@/helpers/editor-commands";
3738
// plane editor extensions
3839
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
@@ -198,7 +199,7 @@ export const getSlashCommandFilteredSections =
198199
searchTerms: ["emoji", "icons", "reaction", "emoticon", "emotags"],
199200
icon: <Smile className="size-3.5" />,
200201
command: ({ editor, range }) => {
201-
editor.chain().focus().insertContentAt(range, "<p>:</p>").run();
202+
openEmojiPicker(editor, range);
202203
},
203204
},
204205
],

packages/editor/src/core/helpers/editor-commands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension";
55
import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text";
66
import type { InsertImageComponentProps } from "@/extensions/custom-image/types";
77
// helpers
8+
import { ExtendedEmojiStorage } from "@/extensions/emoji/emoji";
89
import { findTableAncestor } from "@/helpers/common";
910

1011
export const setText = (editor: Editor, range?: Range) => {
@@ -184,3 +185,10 @@ export const insertCallout = (editor: Editor, range?: Range) => {
184185
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
185186
else editor.chain().focus().insertCallout().run();
186187
};
188+
189+
export const openEmojiPicker = (editor: Editor, range?: Range) => {
190+
if (range) editor.chain().focus().deleteRange(range).run();
191+
const emojiStorage = editor.storage.emoji as ExtendedEmojiStorage;
192+
emojiStorage.forceOpen = true;
193+
editor.chain().focus().insertContent(":").run();
194+
};

0 commit comments

Comments
 (0)