mirror of
https://github.com/chartdb/chartdb.git
synced 2026-05-20 21:19:08 -05:00
image export
This commit is contained in:
Generated
+7
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user