index.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import React from "react";
  2. import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
  3. import { Canvas, Edge, EdgeProps, ElkRoot, NodeProps } from "reaflow";
  4. import { CustomNode } from "src/components/CustomNode";
  5. import useGraph from "src/store/useGraph";
  6. import useUser from "src/store/useUser";
  7. import { getNodePath } from "src/utils/getNodePath";
  8. import styled from "styled-components";
  9. import { Loading } from "../Loading";
  10. import { ErrorView } from "./ErrorView";
  11. import { PremiumView } from "./PremiumView";
  12. interface GraphProps {
  13. isWidget?: boolean;
  14. openNodeModal: () => void;
  15. }
  16. const StyledEditorWrapper = styled.div<{ widget: boolean }>`
  17. position: absolute;
  18. width: 100%;
  19. height: ${({ widget }) => (widget ? "calc(100vh - 36px)" : "calc(100vh - 65px)")};
  20. background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
  21. background-image: ${({ theme }) =>
  22. `radial-gradient(#505050 1px, ${theme.BACKGROUND_SECONDARY} 1px)`};
  23. background-size: 25px 25px;
  24. :active {
  25. cursor: move;
  26. }
  27. .dragging,
  28. .dragging button {
  29. pointer-events: none;
  30. }
  31. rect {
  32. fill: ${({ theme }) => theme.BACKGROUND_NODE};
  33. }
  34. @media only screen and (max-width: 1440px) {
  35. background-image: ${({ theme }) =>
  36. `radial-gradient(#505050 0.5px, ${theme.BACKGROUND_SECONDARY} 0.5px)`};
  37. background-size: 15px 15px;
  38. }
  39. @media only screen and (max-width: 320px) {
  40. height: 100vh;
  41. }
  42. `;
  43. export const Graph = ({ isWidget = false, openNodeModal }: GraphProps) => {
  44. const isPremium = useUser(state => state.isPremium);
  45. const setLoading = useGraph(state => state.setLoading);
  46. const setZoomPanPinch = useGraph(state => state.setZoomPanPinch);
  47. const centerView = useGraph(state => state.centerView);
  48. const setSelectedNode = useGraph(state => state.setSelectedNode);
  49. const loading = useGraph(state => state.loading);
  50. const direction = useGraph(state => state.direction);
  51. const nodes = useGraph(state => state.nodes);
  52. const edges = useGraph(state => state.edges);
  53. const collapsedNodes = useGraph(state => state.collapsedNodes);
  54. const collapsedEdges = useGraph(state => state.collapsedEdges);
  55. React.useEffect(() => {
  56. const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
  57. const edgeList = collapsedEdges.map(id => `[class$="edge-${id}"]`);
  58. const hiddenItems = document.querySelectorAll(".hide");
  59. hiddenItems.forEach(item => item.classList.remove("hide"));
  60. if (nodeList.length) {
  61. const selectedNodes = document.querySelectorAll(nodeList.join(","));
  62. selectedNodes.forEach(node => node.classList.add("hide"));
  63. }
  64. if (edgeList.length) {
  65. const selectedEdges = document.querySelectorAll(edgeList.join(","));
  66. selectedEdges.forEach(edge => edge.classList.add("hide"));
  67. }
  68. }, [collapsedNodes, collapsedEdges]);
  69. const [size, setSize] = React.useState({
  70. width: 1,
  71. height: 1,
  72. });
  73. const handleNodeClick = React.useCallback(
  74. (_: React.MouseEvent<SVGElement>, data: NodeData) => {
  75. if (setSelectedNode)
  76. setSelectedNode({ nodeData: data, path: getNodePath(nodes, edges, data.id) });
  77. if (openNodeModal) openNodeModal();
  78. },
  79. [edges, nodes, openNodeModal, setSelectedNode]
  80. );
  81. const onInit = React.useCallback(
  82. (ref: ReactZoomPanPinchRef) => {
  83. setZoomPanPinch(ref);
  84. },
  85. [setZoomPanPinch]
  86. );
  87. const onLayoutChange = React.useCallback(
  88. (layout: ElkRoot) => {
  89. if (layout.width && layout.height) {
  90. const areaSize = layout.width * layout.height;
  91. const changeRatio = Math.abs((areaSize * 100) / (size.width * size.height) - 100);
  92. setSize({
  93. width: (layout.width as number) + 400,
  94. height: (layout.height as number) + 400,
  95. });
  96. setTimeout(() => {
  97. setLoading(false);
  98. window.requestAnimationFrame(() => {
  99. if (changeRatio > 70 || isWidget) centerView();
  100. });
  101. });
  102. }
  103. },
  104. [centerView, isWidget, setLoading, size.height, size.width]
  105. );
  106. const onCanvasClick = React.useCallback(() => {
  107. const input = document.querySelector("input:focus") as HTMLInputElement;
  108. if (input) input.blur();
  109. }, []);
  110. const memoizedNode = React.useCallback(
  111. (props: JSX.IntrinsicAttributes & NodeProps<any>) => (
  112. <CustomNode {...props} onClick={handleNodeClick} animated={false} />
  113. ),
  114. [handleNodeClick]
  115. );
  116. const memoizedEdge = React.useCallback(
  117. (props: JSX.IntrinsicAttributes & Partial<EdgeProps>) => (
  118. <Edge {...props} containerClassName={`edge-${props.id}`} />
  119. ),
  120. []
  121. );
  122. if (nodes.length > 8_000) return <ErrorView />;
  123. if (nodes.length > 1_000 && !isWidget) {
  124. if (!isPremium()) return <PremiumView />;
  125. }
  126. return (
  127. <>
  128. <Loading message="Painting graph..." loading={loading} />
  129. <StyledEditorWrapper onContextMenu={e => e.preventDefault()} widget={isWidget}>
  130. <TransformWrapper
  131. maxScale={2}
  132. minScale={0.05}
  133. initialScale={0.4}
  134. wheel={{ step: 0.08 }}
  135. zoomAnimation={{ animationType: "linear" }}
  136. doubleClick={{ disabled: true }}
  137. onInit={onInit}
  138. onPanning={ref => ref.instance.wrapperComponent?.classList.add("dragging")}
  139. onPanningStop={ref => ref.instance.wrapperComponent?.classList.remove("dragging")}
  140. >
  141. <TransformComponent
  142. wrapperStyle={{
  143. width: "100%",
  144. height: "100%",
  145. overflow: "hidden",
  146. display: loading ? "none" : "block",
  147. }}
  148. >
  149. <Canvas
  150. className="jsoncrack-canvas"
  151. nodes={nodes}
  152. edges={edges}
  153. maxWidth={size.width}
  154. maxHeight={size.height}
  155. direction={direction}
  156. onLayoutChange={onLayoutChange}
  157. onCanvasClick={onCanvasClick}
  158. node={memoizedNode}
  159. edge={memoizedEdge}
  160. key={direction}
  161. zoomable={false}
  162. animated={false}
  163. readonly={true}
  164. dragEdge={null}
  165. dragNode={null}
  166. fit={true}
  167. />
  168. </TransformComponent>
  169. </TransformWrapper>
  170. </StyledEditorWrapper>
  171. </>
  172. );
  173. };