mirror of
https://github.com/docmost/docmost.git
synced 2025-11-10 11:12:05 +10:00
feat: emoji callout icon (#1323)
This commit is contained in:
@ -15,6 +15,11 @@ export interface EmojiPickerInterface {
|
|||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
removeEmojiAction: () => void;
|
removeEmojiAction: () => void;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
|
actionIconProps?: {
|
||||||
|
size?: string;
|
||||||
|
variant?: string;
|
||||||
|
c?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiPicker({
|
function EmojiPicker({
|
||||||
@ -22,6 +27,7 @@ function EmojiPicker({
|
|||||||
icon,
|
icon,
|
||||||
removeEmojiAction,
|
removeEmojiAction,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
actionIconProps,
|
||||||
}: EmojiPickerInterface) {
|
}: EmojiPickerInterface) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [opened, handlers] = useDisclosure(false);
|
const [opened, handlers] = useDisclosure(false);
|
||||||
@ -64,7 +70,12 @@ function EmojiPicker({
|
|||||||
closeOnEscape={true}
|
closeOnEscape={true}
|
||||||
>
|
>
|
||||||
<Popover.Target ref={setTarget}>
|
<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}
|
{icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
|
|||||||
@ -9,18 +9,21 @@ import {
|
|||||||
EditorMenuProps,
|
EditorMenuProps,
|
||||||
ShouldShowProps,
|
ShouldShowProps,
|
||||||
} from "@/features/editor/components/table/types/types.ts";
|
} from "@/features/editor/components/table/types/types.ts";
|
||||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
import { ActionIcon, Tooltip, Divider } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconAlertTriangleFilled,
|
IconAlertTriangleFilled,
|
||||||
IconCircleCheckFilled,
|
IconCircleCheckFilled,
|
||||||
IconCircleXFilled,
|
IconCircleXFilled,
|
||||||
IconInfoCircleFilled,
|
IconInfoCircleFilled,
|
||||||
|
IconMoodSmile,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { CalloutType } from "@docmost/editor-ext";
|
import { CalloutType } from "@docmost/editor-ext";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import EmojiPicker from "@/components/ui/emoji-picker.tsx";
|
||||||
|
|
||||||
export function CalloutMenu({ editor }: EditorMenuProps) {
|
export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@ -56,6 +59,36 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
[editor],
|
[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 (
|
return (
|
||||||
<BaseBubbleMenu
|
<BaseBubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
@ -130,6 +163,20 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
<IconCircleXFilled size={18} />
|
<IconCircleXFilled size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</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>
|
</ActionIcon.Group>
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { CalloutType } from "@docmost/editor-ext";
|
|||||||
|
|
||||||
export default function CalloutView(props: NodeViewProps) {
|
export default function CalloutView(props: NodeViewProps) {
|
||||||
const { node } = props;
|
const { node } = props;
|
||||||
const { type } = node.attrs;
|
const { type, icon } = node.attrs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper>
|
<NodeViewWrapper>
|
||||||
@ -19,7 +19,7 @@ export default function CalloutView(props: NodeViewProps) {
|
|||||||
variant="light"
|
variant="light"
|
||||||
title=""
|
title=""
|
||||||
color={getCalloutColor(type)}
|
color={getCalloutColor(type)}
|
||||||
icon={getCalloutIcon(type)}
|
icon={getCalloutIcon(type, icon)}
|
||||||
p="xs"
|
p="xs"
|
||||||
classNames={{
|
classNames={{
|
||||||
message: classes.message,
|
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) {
|
switch (type) {
|
||||||
case "info":
|
case "info":
|
||||||
return <IconInfoCircleFilled />;
|
return <IconInfoCircleFilled />;
|
||||||
|
|||||||
@ -18,6 +18,10 @@ export interface CalloutAttributes {
|
|||||||
* The type of callout.
|
* The type of callout.
|
||||||
*/
|
*/
|
||||||
type: CalloutType;
|
type: CalloutType;
|
||||||
|
/**
|
||||||
|
* The custom icon name for the callout.
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
declare module "@tiptap/core" {
|
||||||
@ -27,6 +31,7 @@ declare module "@tiptap/core" {
|
|||||||
unsetCallout: () => ReturnType;
|
unsetCallout: () => ReturnType;
|
||||||
toggleCallout: (attributes?: CalloutAttributes) => ReturnType;
|
toggleCallout: (attributes?: CalloutAttributes) => ReturnType;
|
||||||
updateCalloutType: (type: CalloutType) => ReturnType;
|
updateCalloutType: (type: CalloutType) => ReturnType;
|
||||||
|
updateCalloutIcon: (icon: string) => ReturnType;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,6 +63,13 @@ export const Callout = Node.create<CalloutOptions>({
|
|||||||
"data-callout-type": attributes.type,
|
"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", {
|
commands.updateAttributes("callout", {
|
||||||
type: getValidCalloutType(type),
|
type: getValidCalloutType(type),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
updateCalloutIcon:
|
||||||
|
(icon: string) =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.updateAttributes("callout", {
|
||||||
|
icon: icon || null,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user