index.tsx 4.7 KB

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