From 5b543ca255af3c91e4ff716bd32468a90ba69a95 Mon Sep 17 00:00:00 2001
From: Milho001 <l.milhomemfrancochristino@uu.nl>
Date: Tue, 19 Sep 2023 13:21:10 +0000
Subject: [PATCH] feat(nodelink): popup persists on click

---
 .../lib/vis/nodelink/components/NLPixi.tsx    | 157 ++++++++++++------
 libs/shared/lib/vis/nodelink/nodelinkvis.tsx  |   9 +-
 2 files changed, 110 insertions(+), 56 deletions(-)

diff --git a/libs/shared/lib/vis/nodelink/components/NLPixi.tsx b/libs/shared/lib/vis/nodelink/components/NLPixi.tsx
index 79202fad0..2370370e5 100644
--- a/libs/shared/lib/vis/nodelink/components/NLPixi.tsx
+++ b/libs/shared/lib/vis/nodelink/components/NLPixi.tsx
@@ -1,6 +1,6 @@
 import { GraphType, LinkType, NodeType } from '../Types';
 import { tailwindColors } from 'config';
-import { ReactEventHandler, useEffect, useMemo, useRef, useState } from 'react';
+import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
 import { Application, Circle, Container, FederatedPointerEvent, Graphics, IPointData } from 'pixi.js';
 import { binaryColor, nodeColor as nodeColor } from './utils';
 import { select, zoom as d3zoom, drag as d3drag } from 'd3';
@@ -10,10 +10,12 @@ import { GraphQueryResult, GraphQueryResultFromBackendPayload } from '@graphpola
 import { useAppDispatch, useML } from '@graphpolaris/shared/lib/data-access';
 import { ML, setShortestPathSource, setShortestPathTarget } from '@graphpolaris/shared/lib/data-access/store/mlSlice';
 import { parseQueryResult } from './query2NL';
+import { NLPopup } from './NLPopup';
 
 type Props = {
-  onClick: (node: NodeType) => void;
-  onHover: (data: { node: NodeType; pos: IPointData } | undefined) => void;
+  onClick: (node: NodeType, pos: IPointData) => void;
+  // onHover: (data: { node: NodeType; pos: IPointData }) => void;
+  // onUnHover: (data: { node: NodeType; pos: IPointData }) => void;
   highlightNodes: NodeType[];
   currentShortestPathEdges?: LinkType[];
   highlightedLinks?: LinkType[];
@@ -29,6 +31,9 @@ const links = new Container();
 //////////////////
 
 export const NLPixi = (props: Props) => {
+  const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>();
+  const [popups, setPopups] = useState<{ node: NodeType; pos: IPointData }[]>([]);
+
   const nodeMap = useRef(new Map<string, Graphics>());
   const linkMap = useRef(new Map<string, Graphics>());
   const viewport = useRef<Viewport>();
@@ -39,6 +44,71 @@ export const NLPixi = (props: Props) => {
   const onlyClicked = useRef(false);
   const dispatch = useAppDispatch();
 
+  const imperative = useRef<any>(null);
+
+  useImperativeHandle(imperative, () => ({
+    onDragStart(node: NodeType, gfx: Graphics) {
+      if (viewport.current) viewport.current.pause = true;
+      dragging.current = { node, gfx };
+      onlyClicked.current = true;
+    },
+
+    onDragMove(movementX: number, movementY: number) {
+      if (dragging.current) {
+        onlyClicked.current = false;
+        // if (popups.length > 0) setPopups([]);
+        if (quickPopup) setQuickPopup(undefined);
+        const idx = popups.findIndex((p) => p.node.id === dragging.current?.node.id);
+        if (idx >= 0) {
+          const p = popups[idx];
+          p.pos.x += movementX / (viewport.current?.scaled || 1);
+          p.pos.y += movementY / (viewport.current?.scaled || 1);
+          popups[idx] = p;
+          setPopups([...popups]);
+        }
+
+        if (!dragging.current.node.fx) dragging.current.node.fx = dragging.current.node.x || 0;
+        if (!dragging.current.node.fy) dragging.current.node.fy = dragging.current.node.y || 0;
+        dragging.current.node.fx += movementX / (viewport.current?.scaled || 1);
+        dragging.current.node.fy += movementY / (viewport.current?.scaled || 1);
+        force.simulation.alpha(0.1).restart();
+      }
+    },
+
+    onDragEnd() {
+      if (dragging.current) {
+        dragging.current.node.fx = null;
+        dragging.current.node.fy = null;
+        if (viewport.current) viewport.current.pause = false;
+        if (onlyClicked.current) {
+          onlyClicked.current = false;
+
+          if (popups.filter((d) => d.node.id === dragging.current?.node.id).length > 0) {
+            setPopups(popups.filter((p) => p.node.id !== dragging.current?.node.id));
+          } else {
+            console.log('clicked', popups);
+            setPopups([...popups, { node: dragging.current.node, pos: toGlobal(dragging.current.node) }]);
+          }
+
+          props.onClick(dragging.current.node, toGlobal(dragging.current.node));
+        }
+        this.onHover(dragging.current.node);
+        dragging.current = null;
+      }
+    },
+    onHover(node: NodeType) {
+      if (viewport?.current && !viewport?.current?.pause && node && popups.filter((p) => p.node.id === node.id).length === 0) {
+        setQuickPopup({ node, pos: toGlobal(node) });
+      }
+    },
+    onUnHover() {
+      setQuickPopup(undefined);
+    },
+    onPan() {
+      setPopups([]);
+    },
+  }));
+
   // useEffect(() => {
   //   app.renderer.resize(props.windowSize.width, props.windowSize.height);
   //   app.render();
@@ -73,57 +143,28 @@ export const NLPixi = (props: Props) => {
     }
   }, [ref]);
 
-  function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Graphics) {
-    // store a reference to the data
-    // the reason for this is because of multitouch
-    // we want to track the movement of this particular touch
-    event.stopPropagation();
-    console.log('dragstart', node);
-    onHover(event, undefined);
-
-    if (viewport.current) viewport.current.pause = true;
-    dragging.current = { node, gfx };
-    onlyClicked.current = true;
-  }
-
-  function onHover(event: FederatedPointerEvent, node: NodeType | undefined) {
-    event.stopPropagation();
-    if (viewport?.current && !viewport?.current?.pause && node) {
+  function toGlobal(node: NodeType): IPointData {
+    if (viewport?.current) {
       const rect = ref.current?.getBoundingClientRect();
       const x = (rect?.x || 0) + (node.x || 0);
       const y = (rect?.y || 0) + (node.y || 0);
-      props.onHover({ node, pos: viewport.current.toScreen(x, y) });
-    } else {
-      props.onHover(undefined);
-    }
+      return viewport.current.toScreen(x, y);
+    } else return { x: 0, y: 0 };
   }
 
-  function onDragEnd(event: FederatedPointerEvent) {
-    if (dragging.current) {
-      event.stopPropagation();
-      dragging.current.node.fx = null;
-      dragging.current.node.fy = null;
-      if (viewport.current) viewport.current.pause = false;
-      if (onlyClicked.current) {
-        onlyClicked.current = false;
-        props.onClick(dragging.current.node);
-      }
-      onHover(event, dragging.current.node);
-      dragging.current = null;
-    }
+  function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Graphics) {
+    event.stopPropagation();
+    imperative.current.onDragStart(node, gfx);
   }
 
   function onDragMove(event: FederatedPointerEvent) {
-    if (dragging.current) {
-      onlyClicked.current = false;
-      event.stopPropagation();
-
-      if (!dragging.current.node.fx) dragging.current.node.fx = dragging.current.node.x || 0;
-      if (!dragging.current.node.fy) dragging.current.node.fy = dragging.current.node.y || 0;
-      dragging.current.node.fx += event.movementX / (viewport.current?.scaled || 1);
-      dragging.current.node.fy += event.movementY / (viewport.current?.scaled || 1);
-      force.simulation.alpha(0.1).restart();
-    }
+    event.stopPropagation();
+    imperative.current.onDragMove(event.movementX, event.movementY);
+  }
+
+  function onDragEnd(event: FederatedPointerEvent) {
+    event.stopPropagation();
+    imperative.current.onDragEnd();
   }
 
   const updateNode = (node: NodeType) => {
@@ -151,8 +192,14 @@ export const NLPixi = (props: Props) => {
 
     gfx.off('mouseover');
     gfx.off('mousedown');
-    gfx.on('mouseover', (e) => onHover(e, node));
-    gfx.on('mouseout', (e) => onHover(e, undefined));
+    gfx.on('mouseover', (e) => {
+      e.stopPropagation();
+      imperative.current.onHover(node);
+    });
+    gfx.on('mouseout', (e) => {
+      e.stopPropagation();
+      imperative.current.onUnHover();
+    });
     gfx.on('mousedown', (e) => onDragStart(e, node, gfx));
 
     // if (!item.position) {
@@ -394,6 +441,10 @@ export const NLPixi = (props: Props) => {
 
     viewport.current.addChild(links);
     viewport.current.addChild(nodes);
+    viewport.current.on('drag-start', (event) => {
+      imperative.current.onPan();
+    });
+
     // app.stage.addChild(links);
     // app.stage.addChild(nodes);
     app.stage.eventMode = 'dynamic';
@@ -410,5 +461,13 @@ export const NLPixi = (props: Props) => {
     isSetup.current = true;
   };
 
-  return <div className="h-full w-full overflow-hidden" ref={ref}></div>;
+  return (
+    <>
+      {popups.map((popup) => (
+        <NLPopup onClose={() => {}} data={popup} key={popup.node.id} />
+      ))}
+      {quickPopup && <NLPopup onClose={() => {}} data={quickPopup} />}
+      <div className="h-full w-full overflow-hidden" ref={ref}></div>
+    </>
+  );
 };
diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx
index 74baf7208..5b2c7737d 100644
--- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx
+++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx
@@ -46,7 +46,6 @@ export const NodeLinkVis = React.memo((props: Props) => {
   const [graph, setGraph] = useImmer<GraphType | undefined>(undefined);
   const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]);
   const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]);
-  const [popup, setPopup] = useState<undefined | { node: NodeType; pos: PIXI.IPointData }>(undefined);
 
   const graphQueryResult = useGraphQueryResult();
   const ml = useML();
@@ -67,6 +66,7 @@ export const NodeLinkVis = React.memo((props: Props) => {
 
   const onClickedNode = (node: NodeType, ml: ML) => {
     console.log('shortestPath', graph, ml.shortestPath.enabled);
+
     if (graph) {
       if (ml.shortestPath.enabled) {
         console.log('shortestPath');
@@ -103,18 +103,13 @@ export const NodeLinkVis = React.memo((props: Props) => {
   return (
     <>
       <div className="h-full w-full overflow-hidden" ref={ref}>
-        {!!popup && <NLPopup onClose={() => setPopup(undefined)} data={popup} />}
         <NLPixi
           graph={graph}
           highlightNodes={highlightNodes}
           highlightedLinks={highlightedLinks}
-          onClick={(node) => {
-            console.log(ml.shortestPath);
+          onClick={(node, pos) => {
             onClickedNode(node, ml);
           }}
-          onHover={(data) => {
-            setPopup(data);
-          }}
         />
 
         {/* <VisConfigPanelComponent> */}
-- 
GitLab