返回 presentation-ai
export-toolbar-button.tsx
根目录 / src / components / plate / ui / export-toolbar-button.tsx
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
175 lines Plain Text