196 lines
6.6 KiB
TypeScript
196 lines
6.6 KiB
TypeScript
import { BubbleMenu, BubbleMenuProps, isNodeSelection } from "@tiptap/react";
|
|
import { FC, useState } from "react";
|
|
import {
|
|
BoldIcon,
|
|
ItalicIcon,
|
|
UnderlineIcon,
|
|
StrikethroughIcon,
|
|
CodeIcon,
|
|
} from "lucide-react";
|
|
import { NodeSelector } from "./node-selector";
|
|
import { ColorSelector } from "./color-selector";
|
|
import { LinkSelector } from "./link-selector";
|
|
import { cn } from "@/lib/utils";
|
|
import { TableSelector } from "./table-selector";
|
|
import { AISelector } from "./ai-selectors/edit/ai-edit-selector";
|
|
import { TranslateSelector } from "./ai-selectors/translate/ai-translate-selector";
|
|
|
|
export interface BubbleMenuItem {
|
|
name: string;
|
|
isActive: () => boolean;
|
|
command: () => void;
|
|
icon: typeof BoldIcon;
|
|
}
|
|
|
|
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
|
|
|
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|
const items: BubbleMenuItem[] = [
|
|
{
|
|
name: "bold",
|
|
isActive: () => props.editor!.isActive("bold"),
|
|
command: () => props.editor!.chain().focus().toggleBold().run(),
|
|
icon: BoldIcon,
|
|
},
|
|
{
|
|
name: "italic",
|
|
isActive: () => props.editor!.isActive("italic"),
|
|
command: () => props.editor!.chain().focus().toggleItalic().run(),
|
|
icon: ItalicIcon,
|
|
},
|
|
{
|
|
name: "underline",
|
|
isActive: () => props.editor!.isActive("underline"),
|
|
command: () => props.editor!.chain().focus().toggleUnderline().run(),
|
|
icon: UnderlineIcon,
|
|
},
|
|
{
|
|
name: "strike",
|
|
isActive: () => props.editor!.isActive("strike"),
|
|
command: () => props.editor!.chain().focus().toggleStrike().run(),
|
|
icon: StrikethroughIcon,
|
|
},
|
|
{
|
|
name: "code",
|
|
isActive: () => props.editor!.isActive("code"),
|
|
command: () => props.editor!.chain().focus().toggleCode().run(),
|
|
icon: CodeIcon,
|
|
},
|
|
];
|
|
|
|
const bubbleMenuProps: EditorBubbleMenuProps = {
|
|
...props,
|
|
shouldShow: ({ state, editor }) => {
|
|
const { selection } = state;
|
|
const { empty } = selection;
|
|
|
|
// don't show bubble menu if:
|
|
// - the selected node is an image
|
|
// - the selection is empty
|
|
// - the selection is a node selection (for drag handles)
|
|
if (editor.isActive("image") || empty || isNodeSelection(selection)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
tippyOptions: {
|
|
moveTransition: "transform 0.15s ease-out",
|
|
onHidden: () => {
|
|
setIsNodeSelectorOpen(false);
|
|
setIsColorSelectorOpen(false);
|
|
setIsLinkSelectorOpen(false);
|
|
setIsTableSelectorOpen(false);
|
|
setIsAISelectorOpen(false);
|
|
setIsTranslateSelectorOpen(false);
|
|
},
|
|
},
|
|
};
|
|
|
|
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
|
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
|
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
|
const [isTableSelectorOpen, setIsTableSelectorOpen] = useState(false);
|
|
const [isAISelectorOpen, setIsAISelectorOpen] = useState(false);
|
|
const [isTranslateSelectorOpen, setIsTranslateSelectorOpen] = useState(false);
|
|
|
|
return (
|
|
<BubbleMenu
|
|
{...bubbleMenuProps}
|
|
className="novel-flex novel-w-fit novel-max-w-[97vw] novel-overflow-x-auto novel-divide-x novel-divide-stone-200 novel-rounded novel-border novel-border-stone-200 novel-bg-white novel-shadow-xl">
|
|
{props.editor && (
|
|
<>
|
|
<AISelector
|
|
editor={props.editor}
|
|
isOpen={isAISelectorOpen}
|
|
setIsOpen={() => {
|
|
setIsAISelectorOpen(!isAISelectorOpen);
|
|
setIsNodeSelectorOpen(false);
|
|
setIsColorSelectorOpen(false);
|
|
setIsTableSelectorOpen(false);
|
|
setIsLinkSelectorOpen(false);
|
|
setIsTranslateSelectorOpen(false);
|
|
}}
|
|
/>
|
|
<NodeSelector
|
|
editor={props.editor}
|
|
isOpen={isNodeSelectorOpen}
|
|
setIsOpen={() => {
|
|
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
|
setIsColorSelectorOpen(false);
|
|
setIsTableSelectorOpen(false);
|
|
setIsLinkSelectorOpen(false);
|
|
setIsAISelectorOpen(false);
|
|
setIsTranslateSelectorOpen(false);
|
|
}}
|
|
/>
|
|
{props.editor.isActive("table") && (
|
|
<TableSelector
|
|
editor={props.editor}
|
|
isOpen={isTableSelectorOpen}
|
|
setIsOpen={() => {
|
|
setIsTableSelectorOpen(!isTableSelectorOpen);
|
|
setIsColorSelectorOpen(false);
|
|
setIsNodeSelectorOpen(false);
|
|
setIsLinkSelectorOpen(false);
|
|
setIsAISelectorOpen(false);
|
|
setIsTranslateSelectorOpen(false);
|
|
}}
|
|
/>
|
|
)}
|
|
<LinkSelector
|
|
editor={props.editor}
|
|
isOpen={isLinkSelectorOpen}
|
|
setIsOpen={() => {
|
|
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
|
setIsColorSelectorOpen(false);
|
|
setIsTableSelectorOpen(false);
|
|
setIsNodeSelectorOpen(false);
|
|
setIsAISelectorOpen(false);
|
|
setIsTranslateSelectorOpen(false);
|
|
}}
|
|
/>
|
|
<div className="novel-flex">
|
|
{items.map((item, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={item.command}
|
|
className="novel-p-2 novel-text-stone-600 hover:novel-bg-stone-100 active:novel-bg-stone-200"
|
|
type="button">
|
|
<item.icon
|
|
className={cn("novel-h-4 novel-w-4", {
|
|
"novel-text-blue-500": item.isActive(),
|
|
})}
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<ColorSelector
|
|
editor={props.editor}
|
|
isOpen={isColorSelectorOpen}
|
|
setIsOpen={() => {
|
|
setIsColorSelectorOpen(!isColorSelectorOpen);
|
|
setIsTableSelectorOpen(false);
|
|
setIsNodeSelectorOpen(false);
|
|
setIsLinkSelectorOpen(false);
|
|
setIsAISelectorOpen(false);
|
|
setIsTranslateSelectorOpen(false);
|
|
}}
|
|
/>
|
|
<TranslateSelector
|
|
editor={props.editor}
|
|
isOpen={isTranslateSelectorOpen}
|
|
setIsOpen={() => {
|
|
setIsTranslateSelectorOpen(!isTranslateSelectorOpen);
|
|
setIsAISelectorOpen(false);
|
|
setIsNodeSelectorOpen(false);
|
|
setIsColorSelectorOpen(false);
|
|
setIsTableSelectorOpen(false);
|
|
setIsLinkSelectorOpen(false);
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
</BubbleMenu>
|
|
);
|
|
};
|