mirror of
https://github.com/AykutSarac/jsoncrack.com.git
synced 2025-02-04 01:32:54 +08:00
implement collapse/expand
This commit is contained in:
parent
883d3a157a
commit
2143be74fc
@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { BiChevronLeft, BiChevronRight } from "react-icons/bi";
|
import { BiChevronLeft, BiChevronRight } from "react-icons/bi";
|
||||||
|
import { NodeData, EdgeData } from "reaflow";
|
||||||
import { ConditionalWrapper, CustomNodeProps } from "src/components/CustomNode";
|
import { ConditionalWrapper, CustomNodeProps } from "src/components/CustomNode";
|
||||||
import useConfig from "src/hooks/store/useConfig";
|
import useConfig from "src/hooks/store/useConfig";
|
||||||
|
import useGraph from "src/hooks/store/useGraph";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import * as Styled from "./styles";
|
import * as Styled from "./styles";
|
||||||
|
|
||||||
@ -23,7 +25,8 @@ const StyledExpand = styled.button`
|
|||||||
border-left: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
|
border-left: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TextNode: React.FC<CustomNodeProps<string>> = ({
|
const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
|
||||||
|
node,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
value,
|
value,
|
||||||
@ -34,6 +37,7 @@ const TextNode: React.FC<CustomNodeProps<string>> = ({
|
|||||||
const performanceMode = useConfig((state) => state.performanceMode);
|
const performanceMode = useConfig((state) => state.performanceMode);
|
||||||
const expand = useConfig((state) => state.expand);
|
const expand = useConfig((state) => state.expand);
|
||||||
const [isExpanded, setIsExpanded] = React.useState(!expand);
|
const [isExpanded, setIsExpanded] = React.useState(!expand);
|
||||||
|
const toggleNode = useGraph((state) => state.toggleNode);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setIsExpanded(expand);
|
setIsExpanded(expand);
|
||||||
@ -41,6 +45,7 @@ const TextNode: React.FC<CustomNodeProps<string>> = ({
|
|||||||
|
|
||||||
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
toggleNode(node, isExpanded);
|
||||||
setIsExpanded(!isExpanded);
|
setIsExpanded(!isExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Node {...nodeProps} label={<Label style={baseLabelStyle} />}>
|
<Node {...nodeProps} label={<Label style={baseLabelStyle} />}>
|
||||||
{({ width, height, x, y }) => {
|
{({ width, height, x, y, node }) => {
|
||||||
if (properties.text instanceof Object) {
|
if (properties.text instanceof Object) {
|
||||||
const entries = Object.entries<string>(properties.text);
|
const entries = Object.entries<string>(properties.text);
|
||||||
|
|
||||||
@ -54,6 +54,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TextNode
|
<TextNode
|
||||||
|
node={node}
|
||||||
isParent={properties.data.isParent}
|
isParent={properties.data.isParent}
|
||||||
value={properties.text}
|
value={properties.text}
|
||||||
width={width}
|
width={width}
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
TransformComponent,
|
TransformComponent,
|
||||||
TransformWrapper,
|
TransformWrapper,
|
||||||
} from "react-zoom-pan-pinch";
|
} from "react-zoom-pan-pinch";
|
||||||
import { Canvas, Edge, ElkRoot, useSelection } from "reaflow";
|
import { Canvas, Edge, ElkRoot, NodeData, useSelection } from "reaflow";
|
||||||
import { CustomNode } from "src/components/CustomNode";
|
import { CustomNode } from "src/components/CustomNode";
|
||||||
import { NodeModal } from "src/containers/Modals/NodeModal";
|
import { NodeModal } from "src/containers/Modals/NodeModal";
|
||||||
import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
|
import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
|
||||||
@ -12,6 +12,7 @@ import useConfig from "src/hooks/store/useConfig";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import shallow from "zustand/shallow";
|
import shallow from "zustand/shallow";
|
||||||
import useNodeTools from "src/hooks/store/useNodeTools";
|
import useNodeTools from "src/hooks/store/useNodeTools";
|
||||||
|
import useGraph from "src/hooks/store/useGraph";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
isWidget: boolean;
|
isWidget: boolean;
|
||||||
@ -34,18 +35,9 @@ const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
|
|||||||
|
|
||||||
const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
||||||
const json = useConfig((state) => state.json);
|
const json = useConfig((state) => state.json);
|
||||||
|
const nodes = useGraph((state) => state.nodes);
|
||||||
const [nodes, edges, newNodes, newEdges, selectedNode] = useNodeTools(
|
const edges = useGraph((state) => state.edges);
|
||||||
(state) => [
|
const setGraphValue = useGraph((state) => state.setGraphValue);
|
||||||
state.nodes,
|
|
||||||
state.edges,
|
|
||||||
state.newNodes,
|
|
||||||
state.newEdges,
|
|
||||||
state.selectedNode,
|
|
||||||
],
|
|
||||||
shallow
|
|
||||||
);
|
|
||||||
const setNodeTools = useNodeTools((state) => state.setNodeTools);
|
|
||||||
|
|
||||||
const [size, setSize] = React.useState({
|
const [size, setSize] = React.useState({
|
||||||
width: 2000,
|
width: 2000,
|
||||||
@ -61,11 +53,11 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const { nodes, edges } = getEdgeNodes(json, expand);
|
const { nodes, edges } = getEdgeNodes(json, expand);
|
||||||
|
|
||||||
setNodeTools("nodes", nodes);
|
setGraphValue("nodes", nodes);
|
||||||
setNodeTools("edges", edges);
|
setGraphValue("edges", edges);
|
||||||
setNodeTools("newNodes", nodes);
|
setGraphValue("initialNodes", nodes);
|
||||||
setNodeTools("newEdges", edges);
|
setGraphValue("initialEdges", edges);
|
||||||
}, [json, expand, setNodeTools]);
|
}, [expand, json, setGraphValue]);
|
||||||
|
|
||||||
const onInit = (ref: ReactZoomPanPinchRef) => {
|
const onInit = (ref: ReactZoomPanPinchRef) => {
|
||||||
setConfig("zoomPanPinch", ref);
|
setConfig("zoomPanPinch", ref);
|
||||||
@ -76,20 +68,6 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
|||||||
setSize({ width: layout.width, height: layout.height });
|
setSize({ width: layout.width, height: layout.height });
|
||||||
};
|
};
|
||||||
|
|
||||||
const { selections, onClick, removeSelection } = useSelection({
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
onSelection: (s) => {
|
|
||||||
if (!isWidget) {
|
|
||||||
if (s[0] === selectedNode) {
|
|
||||||
removeSelection(selectedNode);
|
|
||||||
} else {
|
|
||||||
setNodeTools("selectedNode", s[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEditorWrapper isWidget={isWidget}>
|
<StyledEditorWrapper isWidget={isWidget}>
|
||||||
<TransformWrapper
|
<TransformWrapper
|
||||||
@ -119,11 +97,7 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
|||||||
direction={layout}
|
direction={layout}
|
||||||
key={layout}
|
key={layout}
|
||||||
onLayoutChange={onLayoutChange}
|
onLayoutChange={onLayoutChange}
|
||||||
selections={selections}
|
|
||||||
node={(props) => <CustomNode {...props} />}
|
node={(props) => <CustomNode {...props} />}
|
||||||
edge={(props) =>
|
|
||||||
newEdges.find((e) => e.id === props.id) ? <Edge /> : <></>
|
|
||||||
}
|
|
||||||
zoomable={false}
|
zoomable={false}
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
|
139
src/hooks/store/useGraph.tsx
Normal file
139
src/hooks/store/useGraph.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import create from "zustand";
|
||||||
|
import { EdgeData, NodeData } from "reaflow";
|
||||||
|
import { Graph } from "src/components/Graph";
|
||||||
|
import { findEdgeChildren } from "src/utils/findEdgeChildren";
|
||||||
|
import { findNodeChildren } from "src/utils/findNodeChildren";
|
||||||
|
|
||||||
|
export interface Graph {
|
||||||
|
initialNodes: NodeData[];
|
||||||
|
initialEdges: EdgeData[];
|
||||||
|
nodes: NodeData[];
|
||||||
|
edges: EdgeData[];
|
||||||
|
collapsedNodes: { [key: string]: NodeData[] };
|
||||||
|
collapsedEdges: { [key: string]: EdgeData[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphActions {
|
||||||
|
setGraphValue: (key: keyof Graph, value: any) => void;
|
||||||
|
toggleNode: (node: NodeData, isExpanded: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNode(element: NodeData | EdgeData) {
|
||||||
|
if ("text" in element) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIncomers = (
|
||||||
|
node: NodeData,
|
||||||
|
nodes: NodeData[],
|
||||||
|
edges: EdgeData[]
|
||||||
|
): NodeData[] => {
|
||||||
|
if (!isNode(node)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomersIds = edges.filter((e) => e.to === node.id).map((e) => e.from);
|
||||||
|
return nodes.filter((n) => incomersIds.includes(n.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOutgoingEdges = (
|
||||||
|
node: NodeData,
|
||||||
|
edges: EdgeData[]
|
||||||
|
): EdgeData[] => {
|
||||||
|
if (!isNode(node)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges.filter((edge) => edge.from === node.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIncomingEdges = (
|
||||||
|
node: NodeData,
|
||||||
|
edges: EdgeData[]
|
||||||
|
): EdgeData[] => {
|
||||||
|
if (!isNode(node)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges.filter((edge) => edge.to === node.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOutgoers = (
|
||||||
|
node: NodeData,
|
||||||
|
nodes: NodeData[],
|
||||||
|
edges: EdgeData[]
|
||||||
|
) => {
|
||||||
|
const allOutgoers: NodeData[] = [];
|
||||||
|
|
||||||
|
if (!isNode(node)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = (currentNode: NodeData) => {
|
||||||
|
const outgoerIds = edges
|
||||||
|
.filter((e) => e.from === currentNode.id)
|
||||||
|
.map((e) => e.to);
|
||||||
|
const nodeList = nodes.filter((n) => outgoerIds.includes(n.id));
|
||||||
|
allOutgoers.push(...nodeList);
|
||||||
|
nodeList.forEach((node) => runner(node));
|
||||||
|
};
|
||||||
|
|
||||||
|
return allOutgoers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialStates: Graph = {
|
||||||
|
initialNodes: [],
|
||||||
|
initialEdges: [],
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
collapsedNodes: {},
|
||||||
|
collapsedEdges: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGraph = create<Graph & GraphActions>((set, get) => ({
|
||||||
|
...initialStates,
|
||||||
|
setGraphValue: (key, value) =>
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
[key]: value,
|
||||||
|
})),
|
||||||
|
toggleNode: (node, isExpanded) =>
|
||||||
|
set((state) => {
|
||||||
|
const childrenNodes = findNodeChildren(
|
||||||
|
node.id,
|
||||||
|
state.initialNodes,
|
||||||
|
state.initialEdges
|
||||||
|
);
|
||||||
|
const childrenEdges = findEdgeChildren(
|
||||||
|
node.id,
|
||||||
|
childrenNodes,
|
||||||
|
state.initialEdges
|
||||||
|
);
|
||||||
|
|
||||||
|
const expand = () => {
|
||||||
|
if (isExpanded) {
|
||||||
|
return {
|
||||||
|
nodes: state.nodes.filter(
|
||||||
|
(sNode) => !childrenNodes.map((n) => n.id).includes(sNode.id)
|
||||||
|
),
|
||||||
|
edges: state.edges.filter(
|
||||||
|
(sEdge) => !childrenEdges.map((n) => n.id).includes(sEdge.id)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: state.nodes.concat(childrenNodes),
|
||||||
|
edges: state.edges.concat(childrenEdges),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: expand().nodes,
|
||||||
|
edges: expand().edges,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useGraph;
|
@ -1,15 +1,17 @@
|
|||||||
import { NodeData, EdgeData } from "reaflow/dist/types";
|
import { NodeData, EdgeData } from "reaflow/dist/types";
|
||||||
|
|
||||||
|
export const findEdgeChildren = (
|
||||||
|
selectedNode: string,
|
||||||
|
connections: NodeData[],
|
||||||
|
edges: EdgeData[]
|
||||||
|
) => {
|
||||||
|
const nodeIds = connections.map((n) => n.id);
|
||||||
|
|
||||||
export const findEdgeChildren = (selectedNode: string, connections: NodeData[], edges: EdgeData[]) => {
|
nodeIds.push(selectedNode);
|
||||||
|
const newEdges = edges.filter(
|
||||||
|
(e) =>
|
||||||
|
nodeIds.includes(e.from as string) && nodeIds.includes(e.to as string)
|
||||||
|
);
|
||||||
|
|
||||||
const nodeIds = connections.map((n) => n.id);
|
return newEdges;
|
||||||
nodeIds.push(selectedNode);
|
};
|
||||||
const newEdges = edges.filter(
|
|
||||||
(e) =>
|
|
||||||
nodeIds.includes(e.from as string) && nodeIds.includes(e.to as string)
|
|
||||||
);
|
|
||||||
|
|
||||||
return newEdges;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user