Skip to content

Commit 59e3900

Browse files
committed
feat: Create DownloadImageButton component to download tree view as png image
1 parent 061c684 commit 59e3900

File tree

4 files changed

+67
-1
lines changed

4 files changed

+67
-1
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Loading, useTheme } from '@nextui-org/react';
2+
import { toPng } from 'html-to-image';
3+
import { memo, useCallback } from 'react';
4+
import { CircleTransparentButton } from '../../ui/components/CircleTransparentButton';
5+
import { Icon } from '../../ui/icon/Icon';
6+
import { downloadAsFile } from '../../utils/file-download.util';
7+
import { useBoolean } from '../../utils/react-hooks/useBoolean';
8+
9+
const _DownloadImageButton = () => {
10+
const { theme } = useTheme();
11+
const { bool: isDownloading, setTrue: startDownload, setFalse: stopDownload } = useBoolean();
12+
13+
const SELF_CLASSNAME = 'download-image-button';
14+
15+
const handleClick = useCallback(() => {
16+
startDownload();
17+
18+
toPng(document.querySelector('.react-flow') as HTMLElement, {
19+
filter: (node) => {
20+
// we don't want to add the minimap, controls and download image button to the image
21+
const filterTargetTokens = ['react-flow__minimap', 'react-flow__controls', SELF_CLASSNAME];
22+
23+
const isFilterTargetToken: boolean = filterTargetTokens.some((token: string) =>
24+
node?.classList?.contains(token)
25+
);
26+
27+
return !isFilterTargetToken;
28+
},
29+
})
30+
.then((dataUrl: string) => downloadAsFile(dataUrl, 'json-sea.png'))
31+
.finally(() => stopDownload());
32+
}, [startDownload, stopDownload]);
33+
34+
return (
35+
<CircleTransparentButton
36+
style={{
37+
position: 'absolute',
38+
right: 8,
39+
top: 8,
40+
zIndex: 10,
41+
}}
42+
className={SELF_CLASSNAME}
43+
onClick={isDownloading ? undefined : handleClick}
44+
>
45+
{isDownloading ? (
46+
<Loading color="currentColor" size="xs" />
47+
) : (
48+
<Icon icon="camera" size={24} color={theme?.colors.accents8.value} />
49+
)}
50+
</CircleTransparentButton>
51+
);
52+
};
53+
54+
/**
55+
* @see https://reactflow.dev/docs/examples/misc/download-image/
56+
*/
57+
export const DownloadImageButton = memo(_DownloadImageButton);

src/json-diagram/components/JsonDiagram.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { ArrayNode } from './ArrayNode';
2626
import { ChainEdge } from './ChainEdge';
2727
import { CustomMiniMap } from './CustomMiniMap';
2828
import { DefaultEdge } from './DefaultEdge';
29+
import { DownloadImageButton } from './DownloadImageButton';
2930
import { FitViewInvoker } from './FitViewInvoker';
3031
import { ObjectNode } from './ObjectNode';
3132
import { PrimitiveNode } from './PrimitiveNode';
@@ -86,6 +87,7 @@ const _JsonDiagram = () => {
8687
>
8788
<CustomMiniMap />
8889
<Controls position="bottom-right" showInteractive={false} />
90+
<DownloadImageButton />
8991
<Background variant={BackgroundVariant.Dots} />
9092
<FitViewInvoker seaNodes={seaNodes} />
9193
</ReactFlow>

src/store/json-engine/helpers/json-parser.helper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ export const jsonParser = (
164164

165165
/**
166166
* 2023-01-30 Suprisingly, ChatGPT helps to refactor complex `traverse` code into smaller.
167+
*
168+
* `traverse` function follows `preorder traversal`
169+
167170
* @implements
168171
* - if object
169172
* - add node(object)

src/ui/components/CircleTransparentButton.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import { isFunction } from '../../utils/function.util';
44

55
type Props = {
66
children: React.ReactNode;
7+
className?: string;
8+
style?: React.CSSProperties;
79
onClick?: React.MouseEventHandler<HTMLButtonElement>;
810
};
911

10-
const _CircleTransparentButton = ({ children, onClick }: Props) => {
12+
const _CircleTransparentButton = ({ children, className, style, onClick }: Props) => {
1113
return (
1214
<StyledHost
1315
css={{
16+
...style,
1417
cursor: isFunction(onClick) ? 'pointer' : 'initial',
1518
}}
19+
className={className}
1620
onClick={onClick}
1721
>
1822
{children}

0 commit comments

Comments
 (0)