Skip to content

Commit a1193d9

Browse files
authored
Merge pull request #3807 from udecode/html2canvas
export
2 parents f8d65c4 + c94b28c commit a1193d9

16 files changed

+1770
-1082
lines changed
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
title: Export
3+
---
4+
5+
<ComponentPreview name="playground-demo" id="basic-elements" />
6+
7+
<PackageInfo>
8+
9+
## Features
10+
11+
- Export editor content to:
12+
- Client-side export with no server dependencies
13+
14+
</PackageInfo>
15+
16+
## Usage
17+
18+
Install the [PDF Toolbar Button](/docs/plate-ui/pdf-toolbar-button) component.
19+
20+
```
21+
22+
## Examples
23+
24+
### Plate UI
25+
26+
Refer to the preview above.
27+
28+
### Plate Plus
29+
30+
- Server-side PDF export:
31+
- High-quality PDF generation
32+
- Custom fonts and styling
33+
- Headers and footers
34+
- Page numbers
35+
- Font selectable
36+
- Advanced export options:
37+
- Paper size selection
38+
- Margin controls
39+
- Orientation settings
40+
- Compression level
41+
- Enterprise-ready features:
42+
{/* - Batch processing */}
43+
{/* - Watermarking */}
44+
- Custom templates
45+
- Password protection
46+
47+
Try it out with our server-side PDF export:
48+
49+
<ComponentPreviewPro name="export-demo" />

apps/www/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,15 @@
139139
"contentlayer2": "^0.4.6",
140140
"date-fns": "^3.6.0",
141141
"framer-motion": "^11.5.4",
142+
"html2canvas": "^1.4.1",
142143
"lodash.template": "^4.5.0",
143144
"lucide-react": "0.460.0",
144145
"match-sorter": "6.3.4",
145146
"next": "15.0.3",
146147
"next-contentlayer2": "^0.4.6",
147148
"next-themes": "^0.4.3",
148149
"nuqs": "^2.0.3",
150+
"pdf-lib": "^1.17.1",
149151
"prismjs": "^1.29.0",
150152
"react": "^18.3.1",
151153
"react-day-picker": "^8.10.1",

apps/www/public/ai-selection.png

-107 KB
Binary file not shown.

apps/www/public/r/index.json

+31
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,37 @@
15581558
"registryDependencies": [],
15591559
"type": "registry:ui"
15601560
},
1561+
{
1562+
"dependencies": [
1563+
"html2canvas",
1564+
"pdf-lib"
1565+
],
1566+
"doc": {
1567+
"description": "A toolbar button to export editor content as PDF.",
1568+
"docs": [
1569+
{
1570+
"route": "/docs/export",
1571+
"title": "Export"
1572+
}
1573+
],
1574+
"examples": [
1575+
"basic-nodes-demo"
1576+
],
1577+
"label": "New",
1578+
"title": "PDF Toolbar Button"
1579+
},
1580+
"files": [
1581+
{
1582+
"path": "plate-ui/export-toolbar-button.tsx",
1583+
"type": "registry:ui"
1584+
}
1585+
],
1586+
"name": "export-toolbar-button",
1587+
"registryDependencies": [
1588+
"toolbar"
1589+
],
1590+
"type": "registry:ui"
1591+
},
15611592
{
15621593
"dependencies": [
15631594
"@udecode/plate-caption"

apps/www/public/r/styles/default/editor.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"files": [
1717
{
18-
"content": "'use client';\n\nimport React from 'react';\n\nimport type { PlateContentProps } from '@udecode/plate-common/react';\nimport type { VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@udecode/cn';\nimport {\n PlateContent,\n useEditorContainerRef,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport { cva } from 'class-variance-authority';\n\nconst editorContainerVariants = cva(\n 'relative w-full cursor-text overflow-y-auto caret-primary selection:bg-brand/25 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n variant: {\n default: 'h-full',\n demo: 'h-[650px]',\n },\n },\n }\n);\n\nexport const EditorContainer = ({\n className,\n variant,\n ...props\n}: React.HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof editorContainerVariants>) => {\n const editor = useEditorRef();\n const containerRef = useEditorContainerRef();\n\n return (\n <div\n id={editor.uid}\n ref={containerRef}\n className={cn(\n 'ignore-click-outside/toolbar',\n editorContainerVariants({ variant }),\n className\n )}\n role=\"button\"\n {...props}\n />\n );\n};\n\nEditorContainer.displayName = 'EditorContainer';\n\nconst editorVariants = cva(\n cn(\n 'group/editor',\n 'relative w-full overflow-x-hidden whitespace-pre-wrap break-words',\n 'rounded-md ring-offset-background placeholder:text-muted-foreground/80 focus-visible:outline-none',\n '[&_[data-slate-placeholder]]:text-muted-foreground/80 [&_[data-slate-placeholder]]:!opacity-100',\n '[&_[data-slate-placeholder]]:top-[auto_!important]',\n '[&_strong]:font-bold'\n ),\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n disabled: {\n true: 'cursor-not-allowed opacity-50',\n },\n focused: {\n true: 'ring-2 ring-ring ring-offset-2',\n },\n variant: {\n ai: 'w-full px-0 text-base md:text-sm',\n aiChat:\n 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm',\n default:\n 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n demo: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n fullWidth: 'size-full px-16 pb-72 pt-4 text-base sm:px-24',\n none: '',\n },\n },\n }\n);\n\nexport type EditorProps = PlateContentProps &\n VariantProps<typeof editorVariants>;\n\nexport const Editor = React.forwardRef<HTMLDivElement, EditorProps>(\n ({ className, disabled, focused, variant, ...props }, ref) => {\n return (\n <PlateContent\n ref={ref}\n className={cn(\n editorVariants({\n disabled,\n focused,\n variant,\n }),\n className\n )}\n disabled={disabled}\n disableDefaultStyles\n {...props}\n />\n );\n }\n);\n\nEditor.displayName = 'Editor';\n",
18+
"content": "'use client';\n\nimport React from 'react';\n\nimport type { PlateContentProps } from '@udecode/plate-common/react';\nimport type { VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@udecode/cn';\nimport {\n PlateContent,\n useEditorContainerRef,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport { cva } from 'class-variance-authority';\n\nconst editorContainerVariants = cva(\n 'relative w-full cursor-text overflow-y-auto caret-primary selection:bg-brand/25 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n variant: {\n default: 'h-full',\n demo: 'h-[650px]',\n },\n },\n }\n);\n\nexport const EditorContainer = ({\n className,\n variant,\n ...props\n}: React.HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof editorContainerVariants>) => {\n const editor = useEditorRef();\n const containerRef = useEditorContainerRef();\n\n return (\n <div\n id={editor.uid}\n ref={containerRef}\n className={cn(\n 'ignore-click-outside/toolbar',\n editorContainerVariants({ variant }),\n className\n )}\n // Adding this role attribute could cause the content captured by html2canvas to be incorrectly centered.\n // role=\"button\"\n {...props}\n />\n );\n};\n\nEditorContainer.displayName = 'EditorContainer';\n\nconst editorVariants = cva(\n cn(\n 'group/editor',\n 'relative w-full overflow-x-hidden whitespace-pre-wrap break-words',\n 'rounded-md ring-offset-background placeholder:text-muted-foreground/80 focus-visible:outline-none',\n '[&_[data-slate-placeholder]]:text-muted-foreground/80 [&_[data-slate-placeholder]]:!opacity-100',\n '[&_[data-slate-placeholder]]:top-[auto_!important]',\n '[&_strong]:font-bold'\n ),\n {\n defaultVariants: {\n variant: 'default',\n },\n variants: {\n disabled: {\n true: 'cursor-not-allowed opacity-50',\n },\n focused: {\n true: 'ring-2 ring-ring ring-offset-2',\n },\n variant: {\n ai: 'w-full px-0 text-base md:text-sm',\n aiChat:\n 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm',\n default:\n 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n demo: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',\n fullWidth: 'size-full px-16 pb-72 pt-4 text-base sm:px-24',\n none: '',\n },\n },\n }\n);\n\nexport type EditorProps = PlateContentProps &\n VariantProps<typeof editorVariants>;\n\nexport const Editor = React.forwardRef<HTMLDivElement, EditorProps>(\n ({ className, disabled, focused, variant, ...props }, ref) => {\n return (\n <PlateContent\n ref={ref}\n className={cn(\n editorVariants({\n disabled,\n focused,\n variant,\n }),\n className\n )}\n disabled={disabled}\n disableDefaultStyles\n {...props}\n />\n );\n }\n);\n\nEditor.displayName = 'Editor';\n",
1919
"path": "plate-ui/editor.tsx",
2020
"target": "components/plate-ui/editor.tsx",
2121
"type": "registry:ui"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"dependencies": [
3+
"html2canvas",
4+
"pdf-lib"
5+
],
6+
"doc": {
7+
"description": "A toolbar button to export editor content as PDF.",
8+
"docs": [
9+
{
10+
"route": "/docs/export",
11+
"title": "Export"
12+
}
13+
],
14+
"examples": [
15+
"basic-nodes-demo"
16+
],
17+
"label": "New",
18+
"title": "PDF Toolbar Button"
19+
},
20+
"files": [
21+
{
22+
"content": "'use client';\n\nimport React from 'react';\n\nimport { withRef } from '@udecode/cn';\nimport { toDOMNode, useEditorRef } from '@udecode/plate-common/react';\nimport { ArrowDownToLineIcon } from 'lucide-react';\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\n\nexport const ExportToolbarButton = withRef<typeof ToolbarSplitButton>(\n ({ children, ...props }, ref) => {\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const getCanvas = async () => {\n const { default: html2canvas } = await import('html2canvas');\n\n const style = document.createElement('style');\n document.head.append(style);\n style.sheet?.insertRule(\n 'body > div:last-child img { display: inline-block !important; }'\n );\n\n const canvas = await html2canvas(toDOMNode(editor, editor)!);\n style.remove();\n\n return canvas;\n };\n\n const downloadFile = (href: string, filename: string) => {\n const element = document.createElement('a');\n element.setAttribute('href', href);\n element.setAttribute('download', filename);\n element.style.display = 'none';\n document.body.append(element);\n element.click();\n element.remove();\n };\n\n const exportToPdf = async () => {\n const canvas = await getCanvas();\n\n const PDFLib = await import('pdf-lib');\n const pdfDoc = await PDFLib.PDFDocument.create();\n const page = pdfDoc.addPage([canvas.width, canvas.height]);\n const imageEmbed = await pdfDoc.embedPng(canvas.toDataURL('PNG'));\n\n page.drawImage(imageEmbed, {\n height: canvas.height,\n width: canvas.width,\n x: 0,\n y: 0,\n });\n const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true });\n\n downloadFile(pdfBase64, 'plate.pdf');\n };\n\n const exportToImage = async () => {\n const canvas = await getCanvas();\n downloadFile(canvas.toDataURL('image/png'), 'plate.png');\n };\n\n return (\n <ToolbarSplitButton\n ref={ref}\n onClick={exportToPdf}\n onKeyDown={(e) => {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n openState.onOpenChange(true);\n }\n }}\n pressed={openState.open}\n tooltip=\"Export\"\n {...props}\n >\n <ToolbarSplitButtonPrimary>\n <ArrowDownToLineIcon className=\"size-4\" />\n </ToolbarSplitButtonPrimary>\n\n <DropdownMenu {...openState} modal={false}>\n <DropdownMenuTrigger asChild>\n <ToolbarSplitButtonSecondary />\n </DropdownMenuTrigger>\n\n <DropdownMenuContent\n onClick={(e) => e.stopPropagation()}\n align=\"start\"\n alignOffset={-32}\n >\n <DropdownMenuGroup>\n <DropdownMenuItem onSelect={exportToPdf}>\n Export as PDF\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={exportToImage}>\n Export via Image\n </DropdownMenuItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n </ToolbarSplitButton>\n );\n }\n);\n",
23+
"path": "plate-ui/export-toolbar-button.tsx",
24+
"target": "components/plate-ui/export-toolbar-button.tsx",
25+
"type": "registry:ui"
26+
}
27+
],
28+
"name": "export-toolbar-button",
29+
"registryDependencies": [
30+
"toolbar"
31+
],
32+
"type": "registry:ui"
33+
}

0 commit comments

Comments
 (0)