index.tsx 4.6 KB

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