Files
latosa-escrima/frontend/components/editor/editor-menu.tsx

685 lines
16 KiB
TypeScript

"use client";
import type { Editor } from "@tiptap/react";
import {
AlignCenter,
AlignJustifyIcon,
AlignLeft,
AlignRight,
Bold,
Heading1,
Heading2,
Heading3,
ImageIcon,
Italic,
LinkIcon,
List,
ListOrdered,
Minus,
Pilcrow,
Quote,
Strikethrough,
Underline,
X,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Level } from "@tiptap/extension-heading";
import { Separator } from "@/components/ui/separator";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { SizeOption } from "./extensions/marks";
interface EditorMenuProps {
editor: Editor | null;
}
const sizeOptions: SizeOption[] = ["xs", "sm", "base", "lg", "xl"];
const tailwindColors = [
{ value: "default", label: "Default" }, // Changed from '' to "default"
{ value: "gray-500", label: "Gray" },
{ value: "red-500", label: "Red" },
{ value: "yellow-500", label: "Yellow" },
{ value: "green-500", label: "Green" },
{ value: "blue-500", label: "Blue" },
{ value: "indigo-500", label: "Indigo" },
{ value: "purple-500", label: "Purple" },
{ value: "pink-500", label: "Pink" },
];
export function EditorMenu({ editor }: EditorMenuProps) {
if (!editor) {
return null;
}
return (
<TooltipProvider delayDuration={0}>
<div className="flex flex-wrap gap-2 rounded-t-lg border bg-background p-1">
<div className="flex flex-wrap gap-2">
{/* Existing formatting toggles */}
<ToggleGroup
type="multiple"
size="sm"
className="flex flex-wrap gap-1"
>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="bold"
aria-label="Toggle bold"
aria-selected={editor.isActive("bold")}
onClick={() =>
editor
.chain()
.focus()
.toggleBold()
.run()
}
>
<Bold className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Bold</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="italic"
aria-label="Toggle italic"
aria-selected={editor.isActive("italic")}
onClick={() =>
editor
.chain()
.focus()
.toggleItalic()
.run()
}
>
<Italic className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Italic</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="underline"
aria-label="Toggle underline"
aria-selected={editor.isActive("underline")}
onClick={() =>
editor
.chain()
.focus()
.toggleUnderline()
.run()
}
>
<Underline className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Underline</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="strike"
aria-label="Toggle strikethrough"
aria-selected={editor.isActive("strike")}
onClick={() =>
editor
.chain()
.focus()
.toggleStrike()
.run()
}
>
<Strikethrough className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Strikethrough</TooltipContent>
</Tooltip>
</ToggleGroup>
<Separator orientation="vertical" className="h-8" />
{/* Font Size Selector */}
<Select
value={
editor.isActive("fontSize")
? editor.getAttributes("fontSize").size ||
"base"
: "base"
}
onValueChange={(value: SizeOption) => {
editor
.chain()
.focus()
.setFontSize({ size: value })
.run();
}}
>
<SelectTrigger className="h-8 w-[100px]">
<SelectValue placeholder="Size" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sizeOptions.map((size) => (
<SelectItem key={size} value={size}>
{size.toUpperCase()}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
{/* Text Color Selector */}
{/*<Select
value={
editor.isActive("textColor")
? editor.getAttributes("textColor").color ||
"default"
: "default"
}
onValueChange={(value) => {
if (value === "default") {
editor.chain().focus().unsetTextColor().run();
} else {
editor
.chain()
.focus()
.setTextColor({ color: value })
.run();
}
}}
>
<SelectTrigger className="h-8 w-[100px]">
<SelectValue placeholder="Color" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{tailwindColors.map(({ value, label }) => (
<SelectItem key={value} value={value}>
<div className="flex items-center gap-2">
<span
className={`h-3 w-3 rounded-full ${value === "default" ? "bg-gray-200" : `bg-${value}`}`}
/>
{label}
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select> */}
<Separator orientation="vertical" className="h-8" />
{/* Heading Selector */}
<Select
value={getActiveHeading(editor)}
onValueChange={(value) => {
if (value === "paragraph") {
editor.chain().focus().setParagraph().run();
} else {
editor
.chain()
.focus()
.toggleHeading({
level: Number.parseInt(value) as Level,
})
.run();
}
}}
>
<SelectTrigger className="h-8 w-[130px]">
<SelectValue placeholder="Paragraph" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="paragraph">
<div className="flex items-center gap-2">
<Pilcrow className="h-4 w-4" />
<span>Paragraph</span>
</div>
</SelectItem>
<SelectItem value="1">
<div className="flex items-center gap-2">
<Heading1 className="h-4 w-4" />
<span>Heading 1</span>
</div>
</SelectItem>
<SelectItem value="2">
<div className="flex items-center gap-2">
<Heading2 className="h-4 w-4" />
<span>Heading 2</span>
</div>
</SelectItem>
<SelectItem value="3">
<div className="flex items-center gap-2">
<Heading3 className="h-4 w-4" />
<span>Heading 3</span>
</div>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Separator orientation="vertical" className="h-8" />
{/* Alignment Controls */}
<ToggleGroup
type="single"
size="sm"
className="flex flex-wrap gap-1"
>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="left"
aria-label="Align left"
aria-selected={editor.isActive({
textAlign: "left",
})}
onClick={() =>
editor
.chain()
.focus()
.setTextAlign("left")
.run()
}
>
<AlignLeft className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Align left</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="center"
aria-label="Align center"
aria-selected={editor.isActive({
textAlign: "center",
})}
onClick={() =>
editor
.chain()
.focus()
.setTextAlign("center")
.run()
}
>
<AlignCenter className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Align center</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="right"
aria-label="Align right"
aria-selected={editor.isActive({
textAlign: "right",
})}
onClick={() =>
editor
.chain()
.focus()
.setTextAlign("right")
.run()
}
>
<AlignRight className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Align right</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="justify"
aria-label="Justify"
aria-selected={editor.isActive({
textAlign: "justify",
})}
onClick={() =>
editor
.chain()
.focus()
.setTextAlign("justify")
.run()
}
>
<AlignJustifyIcon className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Align justify</TooltipContent>
</Tooltip>
</ToggleGroup>
<Separator orientation="vertical" className="h-8" />
{/* List and Blockquote Controls */}
<ToggleGroup
type="multiple"
size="sm"
className="flex flex-wrap gap-1"
>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="bulletList"
aria-label="Toggle bullet list"
aria-selected={editor.isActive(
"bulletList",
)}
onClick={() =>
editor
.chain()
.focus()
.toggleBulletList()
.run()
}
>
<List className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Bullet list</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="orderedList"
aria-label="Toggle ordered list"
aria-selected={editor.isActive(
"orderedList",
)}
onClick={() =>
editor
.chain()
.focus()
.toggleOrderedList()
.run()
}
>
<ListOrdered className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Ordered list</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ToggleGroupItem
value="blockquote"
aria-label="Toggle blockquote"
aria-selected={editor.isActive(
"blockquote",
)}
onClick={() =>
editor
.chain()
.focus()
.toggleBlockquote()
.run()
}
>
<Quote className="h-4 w-4" />
</ToggleGroupItem>
</TooltipTrigger>
<TooltipContent>Blockquote</TooltipContent>
</Tooltip>
</ToggleGroup>
<Separator orientation="vertical" className="h-8" />
{/* Additional Controls */}
<div className="flex gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() =>
editor
.chain()
.focus()
.setHorizontalRule()
.run()
}
>
<Minus className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Horizontal rule</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() =>
editor
.chain()
.focus()
.clearNodes()
.unsetAllMarks()
.run()
}
>
<X className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Clear formatting</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-8" />
<Tooltip>
<TooltipTrigger asChild>
<LinkPopover editor={editor} />
</TooltipTrigger>
<TooltipContent>Add link</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<ImageDialog editor={editor} />
</TooltipTrigger>
<TooltipContent>Add image</TooltipContent>
</Tooltip>
</div>
</div>
</div>
</TooltipProvider>
);
}
// Rest of the components remain unchanged
function getActiveHeading(editor: Editor) {
if (editor.isActive("heading", { level: 1 })) return "1";
if (editor.isActive("heading", { level: 2 })) return "2";
if (editor.isActive("heading", { level: 3 })) return "3";
return "paragraph";
}
const LinkPopover: React.FC<{ editor: Editor }> = ({ editor }) => {
const [linkUrl, setLinkUrl] = useState("");
const [linkNewTab, setLinkNewTab] = useState(false);
const [isLinkOpen, setIsLinkOpen] = useState(false);
const setLink = () => {
if (linkUrl) {
editor
.chain()
.focus()
.setLink({
href: linkUrl,
target: linkNewTab ? "_blank" : null,
})
.run();
} else {
editor.chain().focus().unsetLink().run();
}
setIsLinkOpen(false);
setLinkUrl("");
setLinkNewTab(false);
};
return (
<Popover open={isLinkOpen} onOpenChange={setIsLinkOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className={editor.isActive("link") ? "bg-accent" : ""}
>
<LinkIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="link">Link URL</Label>
<Input
id="link"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
placeholder="https://example.com"
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="new-tab"
checked={linkNewTab}
onCheckedChange={setLinkNewTab}
/>
<Label htmlFor="new-tab">Open in new tab</Label>
</div>
<div className="flex gap-2">
<Button onClick={setLink} className="flex-1">
Save
</Button>
{editor.isActive("link") && (
<Button
variant="outline"
onClick={() => {
editor.chain().focus().unsetLink().run();
setIsLinkOpen(false);
}}
>
Remove
</Button>
)}
</div>
</div>
</PopoverContent>
</Popover>
);
};
const ImageDialog: React.FC<{ editor: Editor }> = ({ editor }) => {
const [isImageOpen, setIsImageOpen] = useState(false);
const [imageUrl, setImageUrl] = useState("");
const [imageAlt, setImageAlt] = useState("");
const addImage = () => {
if (imageUrl) {
editor
.chain()
.focus()
.setImage({
src: imageUrl,
alt: imageAlt,
})
.run();
}
setIsImageOpen(false);
setImageUrl("");
setImageAlt("");
};
return (
<Dialog open={isImageOpen} onOpenChange={setIsImageOpen}>
<Button
variant="outline"
size="sm"
onClick={() => setIsImageOpen(true)}
>
<ImageIcon className="h-4 w-4" />
</Button>
<DialogContent>
<DialogHeader>
<DialogTitle>Add image</DialogTitle>
<DialogDescription>
Insert an image by providing its URL and alt text.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="image-url">Image URL</Label>
<Input
id="image-url"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://example.com/image.jpg"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="image-alt">Alt text</Label>
<Input
id="image-alt"
value={imageAlt}
onChange={(e) => setImageAlt(e.target.value)}
placeholder="Description of the image"
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsImageOpen(false)}
>
Cancel
</Button>
<Button onClick={addImage}>Add image</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};