From 68474e75d56ed4b4b445cc9b7f59cca96a4ca5db Mon Sep 17 00:00:00 2001 From: Guy Ben-Aharon Date: Sat, 16 Nov 2024 16:57:13 +0200 Subject: [PATCH] fix(export image): Add support for displaying cardinality relationships + background (#407) --- .../export-image-provider.tsx | 113 ++++++++++++++++-- src/index.css | 3 + .../editor-page/canvas/marker-definitions.tsx | 2 +- 3 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/context/export-image-context/export-image-provider.tsx b/src/context/export-image-context/export-image-provider.tsx index 849de724..d7984dd8 100644 --- a/src/context/export-image-context/export-image-provider.tsx +++ b/src/context/export-image-context/export-image-provider.tsx @@ -5,12 +5,14 @@ import { toJpeg, toPng, toSvg } from 'html-to-image'; import { useReactFlow } from '@xyflow/react'; import { useChartDB } from '@/hooks/use-chartdb'; import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; +import { useTheme } from '@/hooks/use-theme'; export const ExportImageProvider: React.FC = ({ children, }) => { const { hideLoader, showLoader } = useFullScreenLoader(); const { setNodes, getViewport } = useReactFlow(); + const { effectiveTheme } = useTheme(); const { diagramName } = useChartDB(); const downloadImage = useCallback( @@ -59,13 +61,101 @@ export const ExportImageProvider: React.FC = ({ const imageCreateFn = imageCreatorMap[type]; setTimeout(async () => { - const dataUrl = await imageCreateFn( - window.document.querySelector( - '.react-flow__viewport' - ) as HTMLElement, - { + const viewportElement = window.document.querySelector( + '.react-flow__viewport' + ) as HTMLElement; + + const markerDefs = document.querySelector( + '.marker-definitions defs' + ); + + const tempSvg = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'svg' + ); + tempSvg.style.position = 'absolute'; + tempSvg.style.top = '0'; + tempSvg.style.left = '0'; + tempSvg.style.width = '100%'; + tempSvg.style.height = '100%'; + tempSvg.style.overflow = 'visible'; + tempSvg.style.zIndex = '-50'; + tempSvg.setAttribute( + 'viewBox', + `0 0 ${reactFlowBounds.width} ${reactFlowBounds.height}` + ); + + const defs = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'defs' + ); + + if (markerDefs) { + defs.innerHTML = markerDefs.innerHTML; + } + + const pattern = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'pattern' + ); + pattern.setAttribute('id', 'background-pattern'); + pattern.setAttribute('width', String(16 * viewport.zoom)); + pattern.setAttribute('height', String(16 * viewport.zoom)); + pattern.setAttribute('patternUnits', 'userSpaceOnUse'); + pattern.setAttribute( + 'patternTransform', + `translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})` + ); + + const dot = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'circle' + ); + + const dotSize = viewport.zoom * 0.5; + dot.setAttribute('cx', String(viewport.zoom)); + dot.setAttribute('cy', String(viewport.zoom)); + dot.setAttribute('r', String(dotSize)); + const dotColor = + effectiveTheme === 'light' ? '#92939C' : '#777777'; + dot.setAttribute('fill', dotColor); + + pattern.appendChild(dot); + defs.appendChild(pattern); + tempSvg.appendChild(defs); + + const backgroundRect = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'rect' + ); + const padding = 2000; + backgroundRect.setAttribute('x', String(-viewport.x - padding)); + backgroundRect.setAttribute('y', String(-viewport.y - padding)); + backgroundRect.setAttribute( + 'width', + String(reactFlowBounds.width + 2 * padding) + ); + backgroundRect.setAttribute( + 'height', + String(reactFlowBounds.height + 2 * padding) + ); + backgroundRect.setAttribute('fill', 'url(#background-pattern)'); + tempSvg.appendChild(backgroundRect); + + viewportElement.insertBefore( + tempSvg, + viewportElement.firstChild + ); + + try { + const dataUrl = await imageCreateFn(viewportElement, { ...(type === 'jpeg' || type === 'png' - ? { backgroundColor: '#ffffff' } + ? { + backgroundColor: + effectiveTheme === 'light' + ? '#ffffff' + : '#141414', + } : {}), width: reactFlowBounds.width, height: reactFlowBounds.height, @@ -76,11 +166,13 @@ export const ExportImageProvider: React.FC = ({ }, quality: 1, pixelRatio: scale, - } - ); + }); - downloadImage(dataUrl, type); - hideLoader(); + downloadImage(dataUrl, type); + } finally { + viewportElement.removeChild(tempSvg); + hideLoader(); + } }, 0); }, [ @@ -90,6 +182,7 @@ export const ExportImageProvider: React.FC = ({ imageCreatorMap, setNodes, showLoader, + effectiveTheme, ] ); diff --git a/src/index.css b/src/index.css index cbbb43c8..b6983c81 100644 --- a/src/index.css +++ b/src/index.css @@ -19,4 +19,7 @@ .scrollable-flex > div { @apply !flex; } + + .marker-definitions { + } } diff --git a/src/pages/editor-page/canvas/marker-definitions.tsx b/src/pages/editor-page/canvas/marker-definitions.tsx index 4d78c4bd..7830f2bf 100644 --- a/src/pages/editor-page/canvas/marker-definitions.tsx +++ b/src/pages/editor-page/canvas/marker-definitions.tsx @@ -19,7 +19,7 @@ export const MarkerDefinitions: React.FC = () => { } return ( - + {Object.entries(cardinalityOptions).map(([cardinality, text]) => sideOptions.map((side) =>