[WIKI-728] fix: update emoji insertion method #7962
This commit is contained in:
parent
0a738d419e
commit
fd542a85fa
5 changed files with 52 additions and 28 deletions
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
@ -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();
|
||||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CommandListInstance, EmojisListDropdownProps>(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;
|
||||
|
|
|
|||
|
|
@ -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: <Smile className="size-3.5" />,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().insertContentAt(range, "<p>:</p>").run();
|
||||
openEmojiPicker(editor, range);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue