feat: emoji callout icon (#1323)

This commit is contained in:
Finn Dittmar
2025-08-31 22:16:52 +02:00
committed by GitHub
parent 242fb6bb57
commit 5968764508
4 changed files with 86 additions and 5 deletions

View File

@ -15,6 +15,11 @@ export interface EmojiPickerInterface {
icon: ReactNode;
removeEmojiAction: () => void;
readOnly: boolean;
actionIconProps?: {
size?: string;
variant?: string;
c?: string;
};
}
function EmojiPicker({
@ -22,6 +27,7 @@ function EmojiPicker({
icon,
removeEmojiAction,
readOnly,
actionIconProps,
}: EmojiPickerInterface) {
const { t } = useTranslation();
const [opened, handlers] = useDisclosure(false);
@ -64,7 +70,12 @@ function EmojiPicker({
closeOnEscape={true}
>
<Popover.Target ref={setTarget}>
<ActionIcon c="gray" variant="transparent" onClick={handlers.toggle}>
<ActionIcon
c={actionIconProps?.c || "gray"}
variant={actionIconProps?.variant || "transparent"}
size={actionIconProps?.size}
onClick={handlers.toggle}
>
{icon}
</ActionIcon>
</Popover.Target>

View File

@ -9,18 +9,21 @@ import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip } from "@mantine/core";
import { ActionIcon, Tooltip, Divider } from "@mantine/core";
import {
IconAlertTriangleFilled,
IconCircleCheckFilled,
IconCircleXFilled,
IconInfoCircleFilled,
IconMoodSmile,
} from "@tabler/icons-react";
import { CalloutType } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next";
import EmojiPicker from "@/components/ui/emoji-picker.tsx";
export function CalloutMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
@ -56,6 +59,36 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
[editor],
);
const setCalloutIcon = useCallback(
(emoji: any) => {
const emojiChar = emoji?.native || emoji?.emoji || emoji;
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.updateCalloutIcon(emojiChar)
.run();
},
[editor],
);
const removeCalloutIcon = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.updateCalloutIcon("")
.run();
}, [editor]);
const getCurrentIcon = () => {
const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === "callout";
const parent = findParentNode(predicate)(selection);
const icon = parent?.node.attrs.icon;
return icon || null;
};
const currentIcon = getCurrentIcon();
return (
<BaseBubbleMenu
editor={editor}
@ -130,6 +163,20 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
<IconCircleXFilled size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Custom emoji")}>
<EmojiPicker
onEmojiSelect={setCalloutIcon}
removeEmojiAction={removeCalloutIcon}
readOnly={false}
icon={currentIcon || <IconMoodSmile size={18} />}
actionIconProps={{
size: "lg",
variant: "default",
c: undefined
}}
/>
</Tooltip>
</ActionIcon.Group>
</BaseBubbleMenu>
);

View File

@ -11,7 +11,7 @@ import { CalloutType } from "@docmost/editor-ext";
export default function CalloutView(props: NodeViewProps) {
const { node } = props;
const { type } = node.attrs;
const { type, icon } = node.attrs;
return (
<NodeViewWrapper>
@ -19,7 +19,7 @@ export default function CalloutView(props: NodeViewProps) {
variant="light"
title=""
color={getCalloutColor(type)}
icon={getCalloutIcon(type)}
icon={getCalloutIcon(type, icon)}
p="xs"
classNames={{
message: classes.message,
@ -32,7 +32,11 @@ export default function CalloutView(props: NodeViewProps) {
);
}
function getCalloutIcon(type: CalloutType) {
function getCalloutIcon(type: CalloutType, customIcon?: string) {
if (customIcon && customIcon.trim() !== "") {
return <span style={{ fontSize: '18px' }}>{customIcon}</span>;
}
switch (type) {
case "info":
return <IconInfoCircleFilled />;

View File

@ -18,6 +18,10 @@ export interface CalloutAttributes {
* The type of callout.
*/
type: CalloutType;
/**
* The custom icon name for the callout.
*/
icon?: string;
}
declare module "@tiptap/core" {
@ -27,6 +31,7 @@ declare module "@tiptap/core" {
unsetCallout: () => ReturnType;
toggleCallout: (attributes?: CalloutAttributes) => ReturnType;
updateCalloutType: (type: CalloutType) => ReturnType;
updateCalloutIcon: (icon: string) => ReturnType;
};
}
}
@ -58,6 +63,13 @@ export const Callout = Node.create<CalloutOptions>({
"data-callout-type": attributes.type,
}),
},
icon: {
default: null,
parseHTML: (element) => element.getAttribute("data-callout-icon"),
renderHTML: (attributes) => ({
"data-callout-icon": attributes.icon,
}),
},
};
},
@ -107,6 +119,13 @@ export const Callout = Node.create<CalloutOptions>({
commands.updateAttributes("callout", {
type: getValidCalloutType(type),
}),
updateCalloutIcon:
(icon: string) =>
({ commands }) =>
commands.updateAttributes("callout", {
icon: icon || null,
}),
};
},