| 1 | "use client"; |
| 2 | |
| 3 | import { type DropdownMenuProps } from "@radix-ui/react-dropdown-menu"; |
| 4 | import { |
| 5 | CalendarIcon, |
| 6 | ChevronRightIcon, |
| 7 | Columns3Icon, |
| 8 | FileCodeIcon, |
| 9 | FilmIcon, |
| 10 | Heading1Icon, |
| 11 | Heading2Icon, |
| 12 | Heading3Icon, |
| 13 | ImageIcon, |
| 14 | Link2Icon, |
| 15 | ListIcon, |
| 16 | ListOrderedIcon, |
| 17 | MinusIcon, |
| 18 | PilcrowIcon, |
| 19 | PlusIcon, |
| 20 | QuoteIcon, |
| 21 | RadicalIcon, |
| 22 | SquareIcon, |
| 23 | TableIcon, |
| 24 | TableOfContentsIcon, |
| 25 | } from "lucide-react"; |
| 26 | import { KEYS, type TElement } from "platejs"; |
| 27 | import { useEditorRef, type PlateEditor } from "platejs/react"; |
| 28 | import * as React from "react"; |
| 29 | |
| 30 | import { PRESENTATION_TITLE_ELEMENT } from "@/components/notebook/presentation/editor/lib"; |
| 31 | import { |
| 32 | DropdownMenu, |
| 33 | DropdownMenuContent, |
| 34 | DropdownMenuItem, |
| 35 | DropdownMenuTrigger, |
| 36 | } from "@/components/plate/ui/dropdown-menu"; |
| 37 | import { |
| 38 | insertBlock, |
| 39 | insertInlineElement, |
| 40 | } from "@/components/plate/utils/transforms"; |
| 41 | import { ToolbarButton, ToolbarMenuGroup } from "./toolbar"; |
| 42 | |
| 43 | type Group = { |
| 44 | group: string; |
| 45 | items: Item[]; |
| 46 | }; |
| 47 | |
| 48 | interface Item { |
| 49 | icon: React.ReactNode; |
| 50 | value: string; |
| 51 | onSelect: (editor: PlateEditor, value: string) => void; |
| 52 | focusEditor?: boolean; |
| 53 | label?: string; |
| 54 | nodeType?: string; |
| 55 | props?: Partial<TElement>; |
| 56 | } |
| 57 | |
| 58 | const groups: Group[] = [ |
| 59 | { |
| 60 | group: "Basic blocks", |
| 61 | items: [ |
| 62 | { |
| 63 | icon: <Heading1Icon />, |
| 64 | label: "Title", |
| 65 | props: { variant: "title" }, |
| 66 | value: PRESENTATION_TITLE_ELEMENT, |
| 67 | }, |
| 68 | { |
| 69 | icon: <Heading2Icon />, |
| 70 | label: "Display", |
| 71 | props: { variant: "display" }, |
| 72 | value: "presentation-title-display", |
| 73 | nodeType: PRESENTATION_TITLE_ELEMENT, |
| 74 | }, |
| 75 | { |
| 76 | icon: <Heading3Icon />, |
| 77 | label: "Humongous", |
| 78 | props: { variant: "humongous" }, |
| 79 | value: "presentation-title-humongous", |
| 80 | nodeType: PRESENTATION_TITLE_ELEMENT, |
| 81 | }, |
| 82 | { |
| 83 | icon: <PilcrowIcon />, |
| 84 | label: "Paragraph", |
| 85 | value: KEYS.p, |
| 86 | }, |
| 87 | { |
| 88 | icon: <Heading1Icon />, |
| 89 | label: "Heading 1", |
| 90 | value: "h1", |
| 91 | }, |
| 92 | { |
| 93 | icon: <Heading2Icon />, |
| 94 | label: "Heading 2", |
| 95 | value: "h2", |
| 96 | }, |
| 97 | { |
| 98 | icon: <Heading3Icon />, |
| 99 | label: "Heading 3", |
| 100 | value: "h3", |
| 101 | }, |
| 102 | { |
| 103 | icon: <TableIcon />, |
| 104 | label: "Table", |
| 105 | value: KEYS.table, |
| 106 | }, |
| 107 | { |
| 108 | icon: <FileCodeIcon />, |
| 109 | label: "Code", |
| 110 | value: KEYS.codeBlock, |
| 111 | }, |
| 112 | { |
| 113 | icon: <QuoteIcon />, |
| 114 | label: "Quote", |
| 115 | value: KEYS.blockquote, |
| 116 | }, |
| 117 | { |
| 118 | icon: <MinusIcon />, |
| 119 | label: "Divider", |
| 120 | value: KEYS.hr, |
| 121 | }, |
| 122 | ].map((item) => ({ |
| 123 | ...item, |
| 124 | onSelect: (editor) => { |
| 125 | insertBlock(editor, item.nodeType ?? item.value, { |
| 126 | props: item.props, |
| 127 | }); |
| 128 | }, |
| 129 | })), |
| 130 | }, |
| 131 | { |
| 132 | group: "Lists", |
| 133 | items: [ |
| 134 | { |
| 135 | icon: <ListIcon />, |
| 136 | label: "Bulleted list", |
| 137 | value: KEYS.ul, |
| 138 | }, |
| 139 | { |
| 140 | icon: <ListOrderedIcon />, |
| 141 | label: "Numbered list", |
| 142 | value: KEYS.ol, |
| 143 | }, |
| 144 | { |
| 145 | icon: <SquareIcon />, |
| 146 | label: "To-do list", |
| 147 | value: KEYS.listTodo, |
| 148 | }, |
| 149 | { |
| 150 | icon: <ChevronRightIcon />, |
| 151 | label: "Toggle list", |
| 152 | value: KEYS.toggle, |
| 153 | }, |
| 154 | ].map((item) => ({ |
| 155 | ...item, |
| 156 | onSelect: (editor, value) => { |
| 157 | insertBlock(editor, value); |
| 158 | }, |
| 159 | })), |
| 160 | }, |
| 161 | { |
| 162 | group: "Media", |
| 163 | items: [ |
| 164 | { |
| 165 | icon: <ImageIcon />, |
| 166 | label: "Image", |
| 167 | value: KEYS.img, |
| 168 | }, |
| 169 | { |
| 170 | icon: <FilmIcon />, |
| 171 | label: "Embed", |
| 172 | value: KEYS.mediaEmbed, |
| 173 | }, |
| 174 | ].map((item) => ({ |
| 175 | ...item, |
| 176 | onSelect: (editor, value) => { |
| 177 | insertBlock(editor, value); |
| 178 | }, |
| 179 | })), |
| 180 | }, |
| 181 | { |
| 182 | group: "Advanced blocks", |
| 183 | items: [ |
| 184 | { |
| 185 | icon: <TableOfContentsIcon />, |
| 186 | label: "Table of contents", |
| 187 | value: KEYS.toc, |
| 188 | }, |
| 189 | { |
| 190 | icon: <Columns3Icon />, |
| 191 | label: "3 columns", |
| 192 | value: "action_three_columns", |
| 193 | }, |
| 194 | { |
| 195 | focusEditor: false, |
| 196 | icon: <RadicalIcon />, |
| 197 | label: "Equation", |
| 198 | value: KEYS.equation, |
| 199 | }, |
| 200 | ].map((item) => ({ |
| 201 | ...item, |
| 202 | onSelect: (editor, value) => { |
| 203 | insertBlock(editor, value); |
| 204 | }, |
| 205 | })), |
| 206 | }, |
| 207 | { |
| 208 | group: "Inline", |
| 209 | items: [ |
| 210 | { |
| 211 | icon: <Link2Icon />, |
| 212 | label: "Link", |
| 213 | value: KEYS.link, |
| 214 | }, |
| 215 | { |
| 216 | focusEditor: true, |
| 217 | icon: <CalendarIcon />, |
| 218 | label: "Date", |
| 219 | value: KEYS.date, |
| 220 | }, |
| 221 | { |
| 222 | focusEditor: false, |
| 223 | icon: <RadicalIcon />, |
| 224 | label: "Inline Equation", |
| 225 | value: KEYS.inlineEquation, |
| 226 | }, |
| 227 | ].map((item) => ({ |
| 228 | ...item, |
| 229 | onSelect: (editor, value) => { |
| 230 | insertInlineElement(editor, value); |
| 231 | }, |
| 232 | })), |
| 233 | }, |
| 234 | ]; |
| 235 | |
| 236 | export function InsertToolbarButton(props: DropdownMenuProps) { |
| 237 | const editor = useEditorRef(); |
| 238 | const [open, setOpen] = React.useState(false); |
| 239 | |
| 240 | return ( |
| 241 | <DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}> |
| 242 | <DropdownMenuTrigger asChild> |
| 243 | <ToolbarButton pressed={open} tooltip="Insert" isDropdown> |
| 244 | <PlusIcon /> |
| 245 | </ToolbarButton> |
| 246 | </DropdownMenuTrigger> |
| 247 | |
| 248 | <DropdownMenuContent |
| 249 | className="flex max-h-125 min-w-0 flex-col overflow-y-auto" |
| 250 | align="start" |
| 251 | > |
| 252 | {groups.map(({ group, items: nestedItems }) => ( |
| 253 | <ToolbarMenuGroup key={group} label={group}> |
| 254 | {nestedItems.map(({ icon, label, value, onSelect }) => ( |
| 255 | <DropdownMenuItem |
| 256 | key={value} |
| 257 | className="min-w-45" |
| 258 | onSelect={() => { |
| 259 | onSelect(editor, value); |
| 260 | editor.tf.focus(); |
| 261 | }} |
| 262 | > |
| 263 | {icon} |
| 264 | {label} |
| 265 | </DropdownMenuItem> |
| 266 | ))} |
| 267 | </ToolbarMenuGroup> |
| 268 | ))} |
| 269 | </DropdownMenuContent> |
| 270 | </DropdownMenu> |
| 271 | ); |
| 272 | } |
| 273 |