image export

This commit is contained in:
Guy Ben-Aharon
2024-08-22 20:25:13 +03:00
parent 085f843766
commit 753ed085dc
7 changed files with 142 additions and 3 deletions
+7
View File
@@ -31,6 +31,7 @@
"cmdk": "^1.0.0",
"dexie": "^4.0.8",
"eslint-config-airbnb-typescript": "^18.0.0",
"html-to-image": "^1.11.11",
"lucide-react": "^0.424.0",
"nanoid": "^5.0.7",
"react": "^18.3.1",
@@ -5721,6 +5722,12 @@
"node": "*"
}
},
"node_modules/html-to-image": {
"version": "1.11.11",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
"integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==",
"license": "MIT"
},
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+1
View File
@@ -34,6 +34,7 @@
"cmdk": "^1.0.0",
"dexie": "^4.0.8",
"eslint-config-airbnb-typescript": "^18.0.0",
"html-to-image": "^1.11.11",
"lucide-react": "^0.424.0",
"nanoid": "^5.0.7",
"react": "^18.3.1",
@@ -0,0 +1,11 @@
import { createContext } from 'react';
import { emptyFn } from '@/lib/utils';
export type ImageType = 'png' | 'jpeg' | 'svg';
export interface ExportImageContext {
exportImage: (type: ImageType) => Promise<void>;
}
export const exportImageContext = createContext<ExportImageContext>({
exportImage: emptyFn,
});
@@ -0,0 +1,92 @@
import React, { useCallback, useMemo } from 'react';
import {
ExportImageContext,
exportImageContext,
ImageType,
} from './export-image-context';
import { toJpeg, toPng, toSvg } from 'html-to-image';
import {
getNodesBounds,
getViewportForBounds,
useReactFlow,
} from '@xyflow/react';
import { useChartDB } from '@/hooks/use-chartdb';
const imageWidth = 1024;
const imageHeight = 768;
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { getNodes, setNodes } = useReactFlow();
const { diagramName } = useChartDB();
const downloadImage = useCallback(
(dataUrl: string, type: ImageType) => {
const a = document.createElement('a');
a.setAttribute('download', `${diagramName}.${type}`);
a.setAttribute('href', dataUrl);
a.click();
},
[diagramName]
);
const imageCreatorMap: Record<
ImageType,
typeof toJpeg | typeof toPng | typeof toSvg
> = useMemo(
() => ({
jpeg: toJpeg,
png: toPng,
svg: toSvg,
}),
[]
);
const exportImage: ExportImageContext['exportImage'] = useCallback(
async (type) => {
setNodes((nodes) =>
nodes.map((node) => ({ ...node, selected: false }))
);
const nodesBounds = getNodesBounds(getNodes());
const viewport = getViewportForBounds(
nodesBounds,
imageWidth,
imageHeight,
0.5,
2,
0
);
const imageCreateFn = imageCreatorMap[type];
const dataUrl = await imageCreateFn(
window.document.querySelector(
'.react-flow__viewport'
) as HTMLElement,
{
...(type === 'jpeg' ? { backgroundColor: '#ffffff' } : {}),
width: imageWidth,
height: imageHeight,
style: {
width: `${imageWidth}px`,
height: `${imageHeight}px`,
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
},
}
);
downloadImage(dataUrl, type);
},
[downloadImage, getNodes, imageCreatorMap, setNodes]
);
return (
<exportImageContext.Provider value={{ exportImage }}>
{children}
</exportImageContext.Provider>
);
};
+4
View File
@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { exportImageContext } from '@/context/export-image-context/export-image-context';
export const useExportImage = () => useContext(exportImageContext);
@@ -26,6 +26,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useExportImage } from '@/hooks/use-export-image';
export interface TopNavbarProps {}
@@ -33,6 +34,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
const { diagramName, updateDiagramName, currentDiagram } = useChartDB();
const { openCreateDiagramDialog, openOpenDiagramDialog } = useDialog();
const [editMode, setEditMode] = useState(false);
const { exportImage } = useExportImage();
const [editedDiagramName, setEditedDiagramName] =
React.useState(diagramName);
const inputRef = React.useRef<HTMLInputElement>(null);
@@ -73,6 +75,18 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
console.log({ currentDiagram });
}, [currentDiagram]);
const exportPNG = useCallback(() => {
exportImage('png');
}, [exportImage]);
const exportSVG = useCallback(() => {
exportImage('svg');
}, [exportImage]);
const exportJPG = useCallback(() => {
exportImage('jpeg');
}, [exportImage]);
return (
<nav className="flex flex-row items-center justify-between px-4 h-12 border-b">
<div className="flex flex-1 justify-start gap-x-3">
@@ -104,8 +118,15 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
Export as
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>PNG</MenubarItem>
<MenubarItem>JPG</MenubarItem>
<MenubarItem onClick={exportPNG}>
PNG
</MenubarItem>
<MenubarItem onClick={exportJPG}>
JPG
</MenubarItem>
<MenubarItem onClick={exportSVG}>
SVG
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
+4 -1
View File
@@ -10,6 +10,7 @@ import { HistoryProvider } from './context/history-context/history-provider';
import { RedoUndoStackProvider } from './context/history-context/redo-undo-stack-provider';
import { LayoutProvider } from './context/layout-context/layout-provider';
import { DialogProvider } from './context/dialog-context/dialog-provider';
import { ExportImageProvider } from './context/export-image-context/export-image-provider';
const routes: RouteObject[] = [
...['', 'diagrams/:diagramId'].map((path) => ({
@@ -23,7 +24,9 @@ const routes: RouteObject[] = [
<HistoryProvider>
<DialogProvider>
<ReactFlowProvider>
<EditorPage />
<ExportImageProvider>
<EditorPage />
</ExportImageProvider>
</ReactFlowProvider>
</DialogProvider>
</HistoryProvider>