| 1 | "use client"; |
| 2 | |
| 3 | import { MarkdownPlugin } from "@platejs/markdown"; |
| 4 | import { type DropdownMenuProps } from "@radix-ui/react-dropdown-menu"; |
| 5 | import { ArrowDownToLineIcon } from "lucide-react"; |
| 6 | import { useEditorRef } from "platejs/react"; |
| 7 | import { |
| 8 | createStaticEditor as createSlateEditor, |
| 9 | serializeHtml, |
| 10 | } from "platejs/static"; |
| 11 | import * as React from "react"; |
| 12 | |
| 13 | import { BaseEditorKit } from "@/components/plate/editor-base-kit"; |
| 14 | import { |
| 15 | DropdownMenu, |
| 16 | DropdownMenuContent, |
| 17 | DropdownMenuGroup, |
| 18 | DropdownMenuItem, |
| 19 | DropdownMenuTrigger, |
| 20 | } from "@/components/plate/ui/dropdown-menu"; |
| 21 | import { loadHtml2Canvas, loadPdfLib } from "@/lib/dynamic-imports"; |
| 22 | import { EditorStatic } from "./editor-static"; |
| 23 | import { ToolbarButton } from "./toolbar"; |
| 24 | |
| 25 | const downloadFile = async (url: string, filename: string) => { |
| 26 | const response = await fetch(url); |
| 27 | |
| 28 | const blob = await response.blob(); |
| 29 | const blobUrl = window.URL.createObjectURL(blob); |
| 30 | |
| 31 | const link = document.createElement("a"); |
| 32 | link.href = blobUrl; |
| 33 | link.download = filename; |
| 34 | document.body.append(link); |
| 35 | link.click(); |
| 36 | link.remove(); |
| 37 | |
| 38 | // Clean up the blob URL |
| 39 | window.URL.revokeObjectURL(blobUrl); |
| 40 | }; |
| 41 | |
| 42 | const siteUrl = "https://platejs.org"; |
| 43 | |
| 44 | export function ExportToolbarButton(props: DropdownMenuProps) { |
| 45 | const editor = useEditorRef(); |
| 46 | const [open, setOpen] = React.useState(false); |
| 47 | |
| 48 | const getCanvas = async () => { |
| 49 | const html2canvas = await loadHtml2Canvas(); |
| 50 | |
| 51 | const style = document.createElement("style"); |
| 52 | document.head.append(style); |
| 53 | |
| 54 | const canvas = await html2canvas(editor.api.toDOMNode(editor)!, { |
| 55 | onclone: (document: Document) => { |
| 56 | const editorElement = document.querySelector( |
| 57 | '[contenteditable="true"]', |
| 58 | ); |
| 59 | if (editorElement) { |
| 60 | Array.from(editorElement.querySelectorAll("*")).forEach((element) => { |
| 61 | const existingStyle = element.getAttribute("style") || ""; |
| 62 | element.setAttribute( |
| 63 | "style", |
| 64 | `${existingStyle}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important`, |
| 65 | ); |
| 66 | }); |
| 67 | } |
| 68 | }, |
| 69 | }); |
| 70 | style.remove(); |
| 71 | |
| 72 | return canvas; |
| 73 | }; |
| 74 | |
| 75 | const exportToPdf = async () => { |
| 76 | const [canvas, PDFLib] = await Promise.all([getCanvas(), loadPdfLib()]); |
| 77 | const pdfDoc = await PDFLib.PDFDocument.create(); |
| 78 | const page = pdfDoc.addPage([canvas.width, canvas.height]); |
| 79 | const imageEmbed = await pdfDoc.embedPng(canvas.toDataURL("PNG")); |
| 80 | const { height, width } = imageEmbed.scale(1); |
| 81 | page.drawImage(imageEmbed, { |
| 82 | height, |
| 83 | width, |
| 84 | x: 0, |
| 85 | y: 0, |
| 86 | }); |
| 87 | const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true }); |
| 88 | |
| 89 | await downloadFile(pdfBase64, "plate.pdf"); |
| 90 | }; |
| 91 | |
| 92 | const exportToImage = async () => { |
| 93 | const canvas = await getCanvas(); |
| 94 | await downloadFile(canvas.toDataURL("image/png"), "plate.png"); |
| 95 | }; |
| 96 | |
| 97 | const exportToHtml = async () => { |
| 98 | const editorStatic = createSlateEditor({ |
| 99 | plugins: BaseEditorKit, |
| 100 | value: editor.children, |
| 101 | }); |
| 102 | |
| 103 | const editorHtml = await serializeHtml(editorStatic, { |
| 104 | editorComponent: EditorStatic, |
| 105 | props: { style: { padding: "0 calc(50% - 350px)", paddingBottom: "" } }, |
| 106 | }); |
| 107 | |
| 108 | const tailwindCss = `<link rel="stylesheet" href="${siteUrl}/tailwind.css">`; |
| 109 | const katexCss = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.css" integrity="sha384-9PvLvaiSKCPkFKB1ZsEoTjgnJn+O3KvEwtsz37/XrkYft3DTk2gHdYvd9oWgW3tV" crossorigin="anonymous">`; |
| 110 | |
| 111 | const html = `<!DOCTYPE html> |
| 112 | <html lang="en"> |
| 113 | <head> |
| 114 | <meta charset="utf-8" /> |
| 115 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 116 | <meta name="color-scheme" content="light dark" /> |
| 117 | <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| 118 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| 119 | <link |
| 120 | href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap" |
| 121 | rel="stylesheet" |
| 122 | /> |
| 123 | ${tailwindCss} |
| 124 | ${katexCss} |
| 125 | <style> |
| 126 | :root { |
| 127 | --font-sans: 'Inter', 'Inter Fallback'; |
| 128 | --font-mono: 'JetBrains Mono', 'JetBrains Mono Fallback'; |
| 129 | } |
| 130 | </style> |
| 131 | </head> |
| 132 | <body> |
| 133 | ${editorHtml} |
| 134 | </body> |
| 135 | </html>`; |
| 136 | |
| 137 | const url = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; |
| 138 | |
| 139 | await downloadFile(url, "plate.html"); |
| 140 | }; |
| 141 | |
| 142 | const exportToMarkdown = async () => { |
| 143 | const md = editor.getApi(MarkdownPlugin).markdown.serialize(); |
| 144 | const url = `data:text/markdown;charset=utf-8,${encodeURIComponent(md)}`; |
| 145 | await downloadFile(url, "plate.md"); |
| 146 | }; |
| 147 | |
| 148 | return ( |
| 149 | <DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}> |
| 150 | <DropdownMenuTrigger asChild> |
| 151 | <ToolbarButton pressed={open} tooltip="Export" isDropdown> |
| 152 | <ArrowDownToLineIcon className="size-4" /> |
| 153 | </ToolbarButton> |
| 154 | </DropdownMenuTrigger> |
| 155 | |
| 156 | <DropdownMenuContent align="start"> |
| 157 | <DropdownMenuGroup> |
| 158 | <DropdownMenuItem onSelect={exportToHtml}> |
| 159 | Export as HTML |
| 160 | </DropdownMenuItem> |
| 161 | <DropdownMenuItem onSelect={exportToPdf}> |
| 162 | Export as PDF |
| 163 | </DropdownMenuItem> |
| 164 | <DropdownMenuItem onSelect={exportToImage}> |
| 165 | Export as Image |
| 166 | </DropdownMenuItem> |
| 167 | <DropdownMenuItem onSelect={exportToMarkdown}> |
| 168 | Export as Markdown |
| 169 | </DropdownMenuItem> |
| 170 | </DropdownMenuGroup> |
| 171 | </DropdownMenuContent> |
| 172 | </DropdownMenu> |
| 173 | ); |
| 174 | } |
| 175 |