Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export type EmojiListRef = {

export type EmojisListDropdownProps = SuggestionProps<EmojiItem, { name: string }> & {
onClose: () => void;
forceOpen?: boolean;
};

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

const handleKeyDown = useCallback(
(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;
}

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

return false;
},
[query.length, items.length, selectItem, selectedIndex]
[items.length, query.length, forceOpen, selectItem, selectedIndex]
);

// Show animation
Expand Down Expand Up @@ -101,7 +108,7 @@ export const EmojisListDropdown = forwardRef<EmojiListRef, EmojisListDropdownPro

useOutsideClickDetector(dropdownContainerRef, onClose);

if (query.length <= 0) return null;
if (query.length === 0 && !forceOpen) return null;

return (
<>
Expand Down
44 changes: 22 additions & 22 deletions packages/editor/src/core/extensions/emoji/emoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType> {
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 = {
Expand Down Expand Up @@ -114,18 +109,22 @@ export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
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();
Expand Down Expand Up @@ -157,6 +156,7 @@ export const Emoji = Node.create<EmojiOptions, EmojiStorage>({
return {
emojis: this.options.emojis,
isSupported: (emojiItem) => (emojiItem.version ? supportMap[emojiItem.version] : false),
forceOpen: false,
};
},

Expand Down
10 changes: 9 additions & 1 deletion packages/editor/src/core/extensions/emoji/suggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Expand Down Expand Up @@ -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<CommandListInstance, EmojisListDropdownProps>(EmojisListDropdown, {
props: {
...props,
onClose: () => handleClose(props.editor),
forceOpen,
} satisfies EmojisListDropdownProps,
editor: props.editor,
className: "fixed z-[100]",
Expand All @@ -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).cleanup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
insertImage,
insertCallout,
setText,
openEmojiPicker,
} from "@/helpers/editor-commands";
// plane editor extensions
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
Expand Down Expand Up @@ -198,7 +199,7 @@ export const getSlashCommandFilteredSections =
searchTerms: ["emoji", "icons", "reaction", "emoticon", "emotags"],
icon: <Smile className="size-3.5" />,
command: ({ editor, range }) => {
editor.chain().focus().insertContentAt(range, "<p>:</p>").run();
openEmojiPicker(editor, range);
},
},
],
Expand Down
8 changes: 8 additions & 0 deletions packages/editor/src/core/helpers/editor-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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();
};
Loading