From 7a0f679b315ef6fcf77cae0701fe1f09c729af08 Mon Sep 17 00:00:00 2001 From: Axel RICHARD Date: Tue, 22 Aug 2023 13:17:51 +0200 Subject: [PATCH] [2296] Add "export to SVG" action in the panel of react-flow prototype Bug: https://github.com/eclipse-sirius/sirius-web/issues/2296 Signed-off-by: Axel RICHARD --- CHANGELOG.adoc | 1 + package-lock.json | 14 ++++++ .../package.json | 5 ++- .../src/renderer/DiagramPanel.tsx | 43 ++++++++++++++++--- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 4e42074436..dd0c2b16a7 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -48,6 +48,7 @@ All of its remaining content was only relevant inside of Sirius Web and it has t - [releng] Add some automatic checks to simplify code reviews - [releng] Make some cypress tests more robust +- https://github.com/eclipse-sirius/sirius-web/issues/2296[#2296] [diagram] Add "export to SVG" action in the diagram panel of the react-flow prototype == v2023.8.0 diff --git a/package-lock.json b/package-lock.json index 30252fae10..4aae75df89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6673,6 +6673,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "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==" + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -13858,6 +13863,9 @@ "name": "@eclipse-sirius/sirius-components-diagrams-reactflow", "version": "2023.8.0", "license": "EPL-2.0", + "dependencies": { + "html-to-image": "^1.11.11" + }, "devDependencies": { "@apollo/client": "3.8.1", "@eclipse-sirius/sirius-components-core": "~2023.8.0", @@ -16045,6 +16053,7 @@ "@vitest/coverage-v8": "0.34.2", "elkjs": "0.8.2", "graphql": "16.8.0", + "html-to-image": "*", "jsdom": "16.7.0", "prettier": "2.7.1", "react": "17.0.2", @@ -21338,6 +21347,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "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==" + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/package.json b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/package.json index 0f79f672b4..c04fdfd4cf 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/package.json +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/package.json @@ -49,12 +49,13 @@ "@vitejs/plugin-react": "4.0.4", "@vitest/coverage-v8": "0.34.2", "elkjs": "0.8.2", - "jsdom": "16.7.0", "graphql": "16.8.0", + "html-to-image": "1.11.11", + "jsdom": "16.7.0", + "prettier": "2.7.1", "react": "17.0.2", "react-dom": "17.0.2", "reactflow": "11.8.1", - "prettier": "2.7.1", "rollup-plugin-peer-deps-external": "2.2.4", "typescript": "5.1.6", "vite": "4.4.9", diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramPanel.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramPanel.tsx index 15e3475452..3369d27e2b 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramPanel.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramPanel.tsx @@ -19,19 +19,28 @@ import FullscreenIcon from '@material-ui/icons/Fullscreen'; import FullscreenExitIcon from '@material-ui/icons/FullscreenExit'; import GridOffIcon from '@material-ui/icons/GridOff'; import GridOnIcon from '@material-ui/icons/GridOn'; +import ImageIcon from '@material-ui/icons/Image'; import ShareIcon from '@material-ui/icons/Share'; import TonalityIcon from '@material-ui/icons/Tonality'; import VisibilityOffIcon from '@material-ui/icons/VisibilityOff'; import ZoomInIcon from '@material-ui/icons/ZoomIn'; import ZoomOutIcon from '@material-ui/icons/ZoomOut'; +import { toSvg } from 'html-to-image'; import { useState } from 'react'; -import { Panel, useReactFlow } from 'reactflow'; +import { Panel, Rect, Transform, getRectOfNodes, getTransformForBounds, useReactFlow } from 'reactflow'; import { DiagramPanelProps, DiagramPanelState } from './DiagramPanel.types'; import { ShareDiagramDialog } from './ShareDiagramDialog'; import { useFadeDiagramElements } from './fade/useFadeDiagramElements'; import { useHideDiagramElements } from './hide/useHideDiagramElements'; +const downloadImage = (dataUrl: string) => { + const a: HTMLAnchorElement = document.createElement('a'); + a.setAttribute('download', 'diagram.svg'); + a.setAttribute('href', dataUrl); + a.click(); +}; + export const DiagramPanel = ({ fullscreen, onFullscreen, @@ -50,18 +59,32 @@ export const DiagramPanel = ({ const handleShare = () => setState((prevState) => ({ ...prevState, dialogOpen: 'Share' })); const handleCloseDialog = () => setState((prevState) => ({ ...prevState, dialogOpen: null })); - const reactFlowInstance = useReactFlow(); const { fadeDiagramElements } = useFadeDiagramElements(); const { hideDiagramElements } = useHideDiagramElements(); const onUnfadeAll = () => fadeDiagramElements([...getAllElementsIds()], false); const onUnhideAll = () => hideDiagramElements([...getAllElementsIds()], false); + const handleExport = () => { + const imageWidth: number = window.screen.width; + const imageHeight: number = window.screen.height; + const nodesBounds: Rect = getRectOfNodes(reactFlow.getNodes()); + const transform: Transform = getTransformForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2); + + toSvg(document.querySelector('.react-flow__viewport') as HTMLElement, { + backgroundColor: '#ffffff', + width: imageWidth, + height: imageHeight, + style: { + width: imageWidth.toString(), + height: imageHeight.toString(), + transform: `translate(${transform[0]}px, ${transform[1]}px) scale(${transform[2]})`, + }, + }).then(downloadImage); + }; + const getAllElementsIds = () => { - return [ - ...reactFlowInstance.getNodes().map((elem) => elem.id), - ...reactFlowInstance.getEdges().map((elem) => elem.id), - ]; + return [...reactFlow.getNodes().map((elem) => elem.id), ...reactFlow.getEdges().map((elem) => elem.id)]; }; return ( @@ -89,6 +112,14 @@ export const DiagramPanel = ({ + + + {snapToGrid ? ( onSnapToGrid(false)}>