[WIKI-402] feat: emoji support for all editors (#7275)
* feat: basic emoji * feat:emoji slash command * update slice command * refactor: emoji storage * refactor:types * refactor: emoji list * refactor: restructure extension * chore: add comments * chore: update comments * fix: fallback image
This commit is contained in:
parent
757019bf43
commit
ba6b822f60
17 changed files with 969 additions and 21 deletions
|
|
@ -1,21 +1,21 @@
|
|||
import { CharacterCountStorage } from "@tiptap/extension-character-count";
|
||||
// constants
|
||||
import type { EmojiStorage } from "@tiptap/extension-emoji";
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
import { type HeadingExtensionStorage } from "@/extensions";
|
||||
import { type CustomImageExtensionStorage } from "@/extensions/custom-image/types";
|
||||
import { type CustomLinkStorage } from "@/extensions/custom-link";
|
||||
import { type ImageExtensionStorage } from "@/extensions/image";
|
||||
import { type MentionExtensionStorage } from "@/extensions/mentions";
|
||||
import { type UtilityExtensionStorage } from "@/extensions/utility";
|
||||
import type { HeadingExtensionStorage } from "@/extensions";
|
||||
import type { CustomImageExtensionStorage } from "@/extensions/custom-image/types";
|
||||
import type { CustomLinkStorage } from "@/extensions/custom-link";
|
||||
import type { ImageExtensionStorage } from "@/extensions/image";
|
||||
import type { UtilityExtensionStorage } from "@/extensions/utility";
|
||||
|
||||
export type ExtensionStorageMap = {
|
||||
[CORE_EXTENSIONS.CUSTOM_IMAGE]: CustomImageExtensionStorage;
|
||||
[CORE_EXTENSIONS.IMAGE]: ImageExtensionStorage;
|
||||
[CORE_EXTENSIONS.CUSTOM_LINK]: CustomLinkStorage;
|
||||
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
|
||||
[CORE_EXTENSIONS.MENTION]: MentionExtensionStorage;
|
||||
[CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage;
|
||||
[CORE_EXTENSIONS.EMOJI]: EmojiStorage;
|
||||
[CORE_EXTENSIONS.CHARACTER_COUNT]: CharacterCountStorage;
|
||||
};
|
||||
|
||||
|
|
|
|||
1
packages/editor/src/ce/types/utils.ts
Normal file
1
packages/editor/src/ce/types/utils.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type TAdditionalActiveDropbarExtensions = never;
|
||||
|
|
@ -41,4 +41,5 @@ export enum CORE_EXTENSIONS {
|
|||
UNDERLINE = "underline",
|
||||
UTILITY = "utility",
|
||||
WORK_ITEM_EMBED = "issue-embed-component",
|
||||
EMOJI = "emoji",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { CustomCodeInlineExtension } from "./code-inline";
|
|||
import { CustomColorExtension } from "./custom-color";
|
||||
import { CustomImageExtensionConfig } from "./custom-image/extension-config";
|
||||
import { CustomLinkExtension } from "./custom-link";
|
||||
import { EmojiExtension } from "./emoji/extension";
|
||||
import { CustomHorizontalRule } from "./horizontal-rule";
|
||||
import { ImageExtensionConfig } from "./image";
|
||||
import { CustomMentionExtensionConfig } from "./mentions/extension-config";
|
||||
|
|
@ -55,6 +56,7 @@ export const CoreEditorExtensionsWithoutProps = [
|
|||
},
|
||||
dropcursor: false,
|
||||
}),
|
||||
EmojiExtension,
|
||||
CustomQuoteExtension,
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
import { Editor } from "@tiptap/react";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export interface EmojiItem {
|
||||
name: string;
|
||||
emoji: string;
|
||||
shortcodes: string[];
|
||||
tags: string[];
|
||||
fallbackImage?: string;
|
||||
}
|
||||
|
||||
export interface EmojiListProps {
|
||||
items: EmojiItem[];
|
||||
command: (item: { name: string }) => void;
|
||||
editor: Editor;
|
||||
}
|
||||
|
||||
export interface EmojiListRef {
|
||||
onKeyDown: (props: { event: KeyboardEvent }) => boolean;
|
||||
}
|
||||
|
||||
export const EmojiList = forwardRef<EmojiListRef, EmojiListProps>((props, ref) => {
|
||||
const { items, command } = props;
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
||||
// refs
|
||||
const emojiListContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectItem = useCallback(
|
||||
(index: number): void => {
|
||||
const item = items[index];
|
||||
if (item) {
|
||||
command({ name: item.name });
|
||||
}
|
||||
},
|
||||
[command, items]
|
||||
);
|
||||
|
||||
const upHandler = useCallback(() => {
|
||||
setSelectedIndex((prevIndex) => (prevIndex + items.length - 1) % items.length);
|
||||
}, [items.length]);
|
||||
|
||||
const downHandler = useCallback(() => {
|
||||
setSelectedIndex((prevIndex) => (prevIndex + 1) % items.length);
|
||||
}, [items.length]);
|
||||
|
||||
const enterHandler = useCallback(() => {
|
||||
setSelectedIndex((prevIndex) => {
|
||||
selectItem(prevIndex);
|
||||
return prevIndex;
|
||||
});
|
||||
}, [selectItem]);
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [items]);
|
||||
|
||||
// scroll to the dropdown item when navigating via keyboard
|
||||
useLayoutEffect(() => {
|
||||
const container = emojiListContainer?.current;
|
||||
if (!container) return;
|
||||
|
||||
const item = container.querySelector(`#emoji-item-${selectedIndex}`) as HTMLElement;
|
||||
if (item) {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
|
||||
const isItemInView = itemRect.top >= containerRect.top && itemRect.bottom <= containerRect.bottom;
|
||||
|
||||
if (!isItemInView) {
|
||||
item.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
onKeyDown: ({ event }: { event: KeyboardEvent }): boolean => {
|
||||
if (event.key === "ArrowUp") {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
enterHandler();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
[upHandler, downHandler, enterHandler]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
ref={emojiListContainer}
|
||||
role="listbox"
|
||||
aria-label="Emoji suggestions"
|
||||
className="z-10 max-h-[90vh] w-[16rem] overflow-y-auto rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg space-y-1"
|
||||
>
|
||||
{items.length ? (
|
||||
items.map((item, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
const emojiKey = item.shortcodes.join(" - ");
|
||||
|
||||
return (
|
||||
<button
|
||||
key={emojiKey}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
aria-label={`${item.name} emoji`}
|
||||
id={`emoji-item-${index}`}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full rounded px-2 py-1.5 text-sm text-left truncate text-custom-text-200 hover:bg-custom-background-80 transition-colors duration-150",
|
||||
{
|
||||
"bg-custom-background-80": isSelected,
|
||||
}
|
||||
)}
|
||||
onClick={() => selectItem(index)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
>
|
||||
<span className="size-5 grid place-items-center flex-shrink-0 text-base">
|
||||
{item.fallbackImage ? (
|
||||
<img src={item.fallbackImage} alt={item.name} className="size-4 object-contain" />
|
||||
) : (
|
||||
item.emoji
|
||||
)}
|
||||
</span>
|
||||
<span className="flex-grow truncate">
|
||||
<span className="font-medium">:{item.name}:</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center text-sm text-custom-text-400 py-2">No emojis found</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
EmojiList.displayName = "EmojiList";
|
||||
30
packages/editor/src/core/extensions/emoji/extension.ts
Normal file
30
packages/editor/src/core/extensions/emoji/extension.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import Emoji, { EmojiItem, gitHubEmojis, shortcodeToEmoji } from "@tiptap/extension-emoji";
|
||||
// local imports
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import suggestion from "./suggestion";
|
||||
|
||||
export const EmojiExtension = Emoji.extend({
|
||||
addStorage() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: ProseMirrorNode) {
|
||||
const emojiItem = shortcodeToEmoji(node.attrs.name, this.options.emojis)
|
||||
if(emojiItem?.emoji) {
|
||||
state.write(emojiItem?.emoji);
|
||||
} else if(emojiItem?.fallbackImage) {
|
||||
state.write(`\n![${emojiItem.name}-${emojiItem.shortcodes[0]}](${emojiItem?.fallbackImage})\n`);
|
||||
} else {
|
||||
state.write(`:${node.attrs.name}:`);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
},
|
||||
}).configure({
|
||||
emojis: gitHubEmojis,
|
||||
suggestion: suggestion,
|
||||
enableEmoticons: true,
|
||||
});
|
||||
126
packages/editor/src/core/extensions/emoji/suggestion.ts
Normal file
126
packages/editor/src/core/extensions/emoji/suggestion.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import type { EmojiOptions } from "@tiptap/extension-emoji";
|
||||
import { ReactRenderer, Editor } from "@tiptap/react";
|
||||
import { SuggestionProps, SuggestionKeyDownProps } from "@tiptap/suggestion";
|
||||
import tippy, { Instance as TippyInstance } from "tippy.js";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// helpers
|
||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||
// local imports
|
||||
import { EmojiItem, EmojiList, EmojiListRef, EmojiListProps } from "./components/emojis-list";
|
||||
|
||||
const DEFAULT_EMOJIS = ["+1", "-1", "smile", "orange_heart", "eyes"];
|
||||
|
||||
const emojiSuggestion: EmojiOptions["suggestion"] = {
|
||||
items: ({ editor, query }: { editor: Editor; query: string }): EmojiItem[] => {
|
||||
if (query.trim() === "") {
|
||||
const { emojis } = getExtensionStorage(editor, CORE_EXTENSIONS.EMOJI);
|
||||
const defaultEmojis = DEFAULT_EMOJIS.map((name) =>
|
||||
emojis.find((emoji: EmojiItem) => emoji.shortcodes.includes(name) || emoji.name === name)
|
||||
)
|
||||
.filter(Boolean)
|
||||
.slice(0, 5);
|
||||
return defaultEmojis as EmojiItem[];
|
||||
}
|
||||
return getExtensionStorage(editor, CORE_EXTENSIONS.EMOJI)
|
||||
.emojis.filter(({ shortcodes, tags }) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return (
|
||||
shortcodes.find((shortcode: string) => shortcode.startsWith(lowerQuery)) ||
|
||||
tags.find((tag: string) => tag.startsWith(lowerQuery))
|
||||
);
|
||||
})
|
||||
.slice(0, 5) as EmojiItem[];
|
||||
},
|
||||
|
||||
allowSpaces: false,
|
||||
|
||||
render: () => {
|
||||
let component: ReactRenderer<EmojiListRef, EmojiListProps>;
|
||||
let popup: TippyInstance[] | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props: SuggestionProps): void => {
|
||||
const emojiListProps: EmojiListProps = {
|
||||
items: props.items,
|
||||
command: props.command,
|
||||
editor: props.editor,
|
||||
};
|
||||
|
||||
getExtensionStorage(props.editor, CORE_EXTENSIONS.UTILITY).activeDropbarExtensions.push(CORE_EXTENSIONS.EMOJI);
|
||||
|
||||
component = new ReactRenderer(EmojiList, {
|
||||
props: emojiListProps,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
if (!props.clientRect) return;
|
||||
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect as () => DOMRect,
|
||||
appendTo: () =>
|
||||
document.querySelector(".active-editor") ??
|
||||
document.querySelector('[id^="editor-container"]') ??
|
||||
document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "bottom-start",
|
||||
hideOnClick: false,
|
||||
sticky: "reference",
|
||||
animation: false,
|
||||
duration: 0,
|
||||
offset: [0, 8],
|
||||
});
|
||||
},
|
||||
|
||||
onUpdate: (props: SuggestionProps): void => {
|
||||
const emojiListProps: EmojiListProps = {
|
||||
items: props.items,
|
||||
command: props.command,
|
||||
editor: props.editor,
|
||||
};
|
||||
|
||||
component.updateProps(emojiListProps);
|
||||
|
||||
if (popup && props.clientRect) {
|
||||
popup[0]?.setProps({
|
||||
getReferenceClientRect: props.clientRect as () => DOMRect,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: (props: SuggestionKeyDownProps): boolean => {
|
||||
if (props.event.key === "Escape") {
|
||||
if (popup) {
|
||||
popup[0]?.hide();
|
||||
}
|
||||
if (component) {
|
||||
component.destroy();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props) || false;
|
||||
},
|
||||
|
||||
onExit: (props: SuggestionProps): void => {
|
||||
const utilityStorage = getExtensionStorage(props.editor, CORE_EXTENSIONS.UTILITY);
|
||||
const index = utilityStorage.activeDropbarExtensions.indexOf(CORE_EXTENSIONS.EMOJI);
|
||||
if (index > -1) {
|
||||
utilityStorage.activeDropbarExtensions.splice(index, 1);
|
||||
}
|
||||
|
||||
if (popup) {
|
||||
popup[0]?.destroy();
|
||||
}
|
||||
if (component) {
|
||||
component.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default emojiSuggestion;
|
||||
|
|
@ -11,11 +11,13 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
|
|||
addKeyboardShortcuts(this) {
|
||||
return {
|
||||
Enter: () => {
|
||||
const isMentionOpen = getExtensionStorage(this.editor, CORE_EXTENSIONS.MENTION)?.mentionsOpen;
|
||||
if (!isMentionOpen) {
|
||||
const { activeDropbarExtensions } = getExtensionStorage(this.editor, CORE_EXTENSIONS.UTILITY);
|
||||
|
||||
if (activeDropbarExtensions.length === 0) {
|
||||
onEnterKeyPress?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"Shift-Enter": ({ editor }) =>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
|||
import type { IEditorProps } from "@/types";
|
||||
// local imports
|
||||
import { CustomImageExtension } from "./custom-image/extension";
|
||||
import { EmojiExtension } from "./emoji/extension";
|
||||
|
||||
type TArguments = Pick<
|
||||
IEditorProps,
|
||||
|
|
@ -97,6 +98,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
|||
},
|
||||
...(enableHistory ? {} : { history: false }),
|
||||
}),
|
||||
EmojiExtension,
|
||||
CustomQuoteExtension,
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
|
|
|
|||
|
|
@ -12,11 +12,7 @@ export type TMentionExtensionOptions = MentionOptions & {
|
|||
getMentionedEntityDetails: TMentionHandler["getMentionedEntityDetails"];
|
||||
};
|
||||
|
||||
export type MentionExtensionStorage = {
|
||||
mentionsOpen: boolean;
|
||||
};
|
||||
|
||||
export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOptions, MentionExtensionStorage>({
|
||||
export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOptions>({
|
||||
addAttributes() {
|
||||
return {
|
||||
[EMentionComponentAttributeNames.ID]: {
|
||||
|
|
@ -54,7 +50,6 @@ export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOpti
|
|||
addStorage() {
|
||||
const options = this.options;
|
||||
return {
|
||||
mentionsOpen: false,
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: NodeType) {
|
||||
state.write(getMentionDisplayText(options, node));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { CommandListInstance } from "@/helpers/tippy";
|
|||
import { TMentionHandler } from "@/types";
|
||||
// local components
|
||||
import { MentionsListDropdown, MentionsListDropdownProps } from "./mentions-list-dropdown";
|
||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
|
||||
export const renderMentionsDropdown =
|
||||
(props: Pick<TMentionHandler, "searchCallback">): SuggestionOptions["render"] =>
|
||||
|
|
@ -28,7 +30,9 @@ export const renderMentionsDropdown =
|
|||
},
|
||||
editor: props.editor,
|
||||
});
|
||||
props.editor.storage.mentionsOpen = true;
|
||||
getExtensionStorage(props.editor, CORE_EXTENSIONS.UTILITY).activeDropbarExtensions.push(
|
||||
CORE_EXTENSIONS.MENTION
|
||||
);
|
||||
// @ts-expect-error - Tippy types are incorrect
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
|
|
@ -64,7 +68,11 @@ export const renderMentionsDropdown =
|
|||
return false;
|
||||
},
|
||||
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
|
||||
props.editor.storage.mentionsOpen = false;
|
||||
const utilityStorage = getExtensionStorage(props.editor, CORE_EXTENSIONS.UTILITY);
|
||||
const index = utilityStorage.activeDropbarExtensions.indexOf(CORE_EXTENSIONS.MENTION);
|
||||
if (index > -1) {
|
||||
utilityStorage.activeDropbarExtensions.splice(index, 1);
|
||||
}
|
||||
popup?.[0]?.destroy();
|
||||
component?.destroy();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
ListTodo,
|
||||
MessageSquareText,
|
||||
MinusSquare,
|
||||
Smile,
|
||||
Table,
|
||||
TextQuote,
|
||||
} from "lucide-react";
|
||||
|
|
@ -189,6 +190,17 @@ export const getSlashCommandFilteredSections =
|
|||
icon: <MinusSquare className="size-3.5" />,
|
||||
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
commandKey: "emoji",
|
||||
key: "emoji",
|
||||
title: "Emoji",
|
||||
description: "Insert an emoji",
|
||||
searchTerms: ["emoji", "icons", "reaction", "emoticon", "emotags"],
|
||||
icon: <Smile className="size-3.5" />,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().insertContentAt(range, "<p>:</p>").run();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { Extension } from "@tiptap/core";
|
||||
import codemark from "prosemirror-codemark";
|
||||
// helpers
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
import { restorePublicImages } from "@/helpers/image-helpers";
|
||||
// plugins
|
||||
import { TAdditionalActiveDropbarExtensions } from "@/plane-editor/types/utils";
|
||||
import { DropHandlerPlugin } from "@/plugins/drop";
|
||||
import { FilePlugins } from "@/plugins/file/root";
|
||||
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||
// types
|
||||
|
||||
import type { IEditorProps, TEditorAsset, TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
type TActiveDropbarExtensions = CORE_EXTENSIONS.MENTION | CORE_EXTENSIONS.EMOJI | TAdditionalActiveDropbarExtensions;
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands {
|
||||
|
|
@ -30,6 +34,7 @@ export interface UtilityExtensionStorage {
|
|||
assetsList: TEditorAsset[];
|
||||
assetsUploadStatus: TFileHandler["assetsUploadStatus"];
|
||||
uploadInProgress: boolean;
|
||||
activeDropbarExtensions: TActiveDropbarExtensions[];
|
||||
}
|
||||
|
||||
type Props = Pick<IEditorProps, "disabledExtensions"> & {
|
||||
|
|
@ -70,6 +75,7 @@ export const UtilityExtension = (props: Props) => {
|
|||
assetsList: [],
|
||||
assetsUploadStatus: isEditable && "assetsUploadStatus" in fileHandler ? fileHandler.assetsUploadStatus : {},
|
||||
uploadInProgress: false,
|
||||
activeDropbarExtensions: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ export type TEditorCommands =
|
|||
| "background-color"
|
||||
| "text-align"
|
||||
| "callout"
|
||||
| "attachment";
|
||||
| "attachment"
|
||||
| "emoji";
|
||||
|
||||
export type TCommandExtraProps = {
|
||||
image: {
|
||||
|
|
|
|||
|
|
@ -490,3 +490,13 @@ p.editor-paragraph-block + p.editor-paragraph-block {
|
|||
background-color: var(--editor-colors-purple-background);
|
||||
}
|
||||
/* end background colors */
|
||||
|
||||
/* emoji styles */
|
||||
span[data-name][data-type="emoji"] img {
|
||||
display: inline !important;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 1.25em;
|
||||
max-height: 1.25em;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue