[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 }> & {
|
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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue