Kaushal Sachan
Kaushal Sachan

Reputation: 1243

React flow Heighlight Connecting Edge of a Node on mouse over

I figured it out using the helper functions. Not sure if there is a better/preferred way to do this, but this seems to work. Here are my recursive functions that get all incoming and outgoing nodes from a selected node.

Upvotes: 0

Views: 60

Answers (1)

Kaushal Sachan
Kaushal Sachan

Reputation: 1243

  const getAllIncomers = (node : Node, nodes: Node[], edges:Edge[], prevIncomers: Node[] = []) => {
    const incomers = getIncomers(node, nodes, edges);
    const result = incomers.reduce(
      (memo:Node[], incomer:Node) => {
        memo.push(incomer);
  
        if ((prevIncomers.findIndex(n => n.id == incomer.id) == -1)) {
          prevIncomers.push(incomer);
  
          getAllIncomers(incomer, nodes, edges, prevIncomers).forEach((foundNode:Node) => {
            memo.push(foundNode);
  
            if ((prevIncomers.findIndex(n => n.id == foundNode.id) == -1)) {
              prevIncomers.push(incomer);
  
            }
          });
        }
        return memo;
      },
      []
    );
    return result;
  }




const getAllOutgoers = (node:Node, nodes:Node[], edges:Edge[], prevOutgoers:Node[] = []) => {
    const outgoers = getOutgoers(node, nodes, edges);
    return outgoers.reduce(
      (memo:Node[], outgoer:Node) => {
        memo.push(outgoer);
  
        if ((prevOutgoers.findIndex(n => n.id == outgoer.id) == -1)) {
          prevOutgoers.push(outgoer);
  
          getAllOutgoers(outgoer, nodes, edges, prevOutgoers).forEach((foundNode:Node) => {
            memo.push(foundNode);
  
            if ((prevOutgoers.findIndex(n => n.id == foundNode.id) == -1)) {
              prevOutgoers.push(foundNode);
            }
          });
        }
        return memo;
      },
      []
    )
  }

I've also wrote custom getIncomers and getOutgoers in order to use them with custom nodes

const getIncomers = (node:Node, nodes:Node[], edges:Edge[]) => {
    if (!isNode(node)) {
      return [];
    }
  
    const incomersIds = edges
      .filter((e) => e.target === node.id)
      .map((e) => e.source);
  
    return nodes.filter((e) => incomersIds.map((id) => {
      const matches = /([\w-^]+)__([\w-]+)/.exec(id);
      if (matches === null) {
        return id;
      }
      return matches[1];
    }).includes(e.id));
  };




const getOutgoers = (node:Node, nodes:Node[], edges:Edge[]) => {
    if (!isNode(node)) {
      return [];
    }
  
    const outgoerIds = edges
      .filter((e) => e.source === node.id)
      .map((e) => e.target);
  
    return nodes.filter((n) => outgoerIds.map((id) => {
      const matches = /([\w-^]+)__([\w-]+)/.exec(id);
      if (matches === null) {
        return id;
      }
      return matches[1];
    }).includes(n.id));
  };

Now the highlight & Reset function looks like this

const highlightPath = (node:Node, nodes:Node[], edges:Edge[], selection:Boolean) => {
        if (node && [...nodes, ...edges]) {
            const allIncomers = getAllIncomers(node, nodes, edges)
            const allOutgoers = getAllOutgoers(node, nodes, edges)

            setNodes((prevElements) => {
                return prevElements?.map((elem) => {
                    const incomerIds = allIncomers.map((i) => i.id)
                    const outgoerIds = allOutgoers.map((o) => o.id)

                    if (isNode(elem) && (allOutgoers.length > 0 || allIncomers.length > 0)) {
                        const highlight = elem.id === node.id || incomerIds.includes(elem.id) || outgoerIds.includes(elem.id)

                        elem.style = {
                            ...elem.style,
                            opacity: highlight ? 1 : 0.25,
                        }
                    }

                    if (isEdge(elem)) {
                        if (selection) {
                            const animated =
                                incomerIds.includes(elem.source) && (incomerIds.includes(elem.target) || node.id === elem.target)
                            elem.animated = animated

                            elem.style = {
                                ...elem.style,
                                stroke: animated ? colors.blue[600] : '#b1b1b7',
                                opacity: animated ? 1 : 0.25,
                            }
                        } else {
                            elem.animated = false
                            elem.style = {
                                ...elem.style,
                                stroke: '#b1b1b7',
                                opacity: 1,
                            }
                        }
                    }

                    return elem
                })
            })
        }
    }

const resetNodeStyles = () => {
        setNodes((prevElements) => {
            return prevElements?.map((elem) => {
                if (isNode(elem)) {
                    elem.style = {
                        ...elem.style,
                        opacity: 1,
                    }
                } else {
                    // elem.animated = false
                    // elem.style = {
                    //  ...elem.style,
                    //  stroke: '#b1b1b7',
                    //  opacity: 1,
                    // }
                }

                return elem
            })
        })
    }

Finally use like this

<ReactFlow
            nodes={nodes}
            edges={edges}
            snapToGrid={true}
            preventScrolling={true}
            snapGrid={[10, 10]}
            elementsSelectable={true}
            // onNodeMouseEnter={(_event, node) => !selectedNode && highlightPath(node, [...nodes, ...edges])}
            // onNodeMouseLeave={() => !selectedNode && resetNodeStyles()}
            onSelectionChange={(selectedElements) => {
                const node = selectedElements.nodes[0]
                setSelectedNode(node)
                highlightPath(node, nodes, edges, true)
            }}
            onPaneClick={() => {
                resetNodeStyles()
                setSelectedNode(undefined)
            }}
        />

To change init trigger from select to hover - uncomment two lines. And add

const [selectedNode, setSelectedNode] = useState();

Upvotes: 0

Related Questions