Просмотр исходного кода

basic multi-select and edge fix

jusrhee 5 лет назад
Родитель
Сommit
f7c2152688

+ 8 - 8
dashboard/src/main/home/dashboard/expanded-chart/graph/Edge.tsx

@@ -38,7 +38,7 @@ export default class Edge extends Component<PropsType, StateType> {
     var cx = ((x1 + x2) / 2) - (length / 2);
     var cy = ((y1 + y2) / 2) - (thickness / 2);
     // angle
-    var angle = Math.atan2((y1-y2),(x1-x2))*(180/Math.PI);
+    var angle = Math.atan2((y1 - y2), (x1 - x2)) * (180 / Math.PI);
 
     return (
       <StyledEdge
@@ -48,9 +48,10 @@ export default class Edge extends Component<PropsType, StateType> {
         angle={angle}
         onMouseEnter={() => setCurrentEdge(edge)}
         onMouseLeave={() => setCurrentEdge(null)}
+        type={edge.type}
       >
-        <VisibleLine color={edgeColors[edge.type]} />
         {this.state.showArrowHead ? <ArrowHead color={edgeColors[edge.type]} /> : null}
+        <VisibleLine color={edgeColors[edge.type]} />
       </StyledEdge>
     );
   }
@@ -58,12 +59,11 @@ export default class Edge extends Component<PropsType, StateType> {
 
 const ArrowHead = styled.div`
   width: 0; 
-  height: 0; 
+  height: 0;
+  margin-left: 20px;
   border-top: 5px solid transparent;
-  border-bottom: 5px solid transparent;
-  margin-right: 20px;
-
-  border-left: 10px solid ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
+  border-bottom: 5px solid transparent; 
+  border-right: 10px solid ${(props: { color: string }) => props.color ? props.color : '#ffffff66'};
 `;
 
 const VisibleLine = styled.section`
@@ -83,7 +83,7 @@ const StyledEdge: any = styled.div.attrs((props: any) => ({
   position: absolute;
   height: ${thickness}px;
   cursor: pointer;
-  z-index: 0;
+  z-index: ${(props: { type: string, color: string }) => props.type == 'ControlRel' ? '1' : '0'};
   display: flex;
   align-items: center;
   justify-content: center;

+ 112 - 45
dashboard/src/main/home/dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -6,6 +6,7 @@ import { ResourceType, NodeType, EdgeType } from '../../../../../shared/types';
 import Node from './Node';
 import Edge from './Edge';
 import InfoPanel from './InfoPanel';
+import SelectRegion from './SelectRegion';
 
 const zoomConstant = 0.01;
 const panConstant = 0.8;
@@ -18,23 +19,27 @@ type PropsType = {
 type StateType = {
   nodes: NodeType[],
   edges: EdgeType[],
-  activeIds: number[],
-  originX: number | null,
+  activeIds: number[], // IDs of all currently selected nodes
+  originX: number | null, 
   originY: number | null,
   cursorX: number | null,
   cursorY: number | null,
-  deltaX: number | null,
-  deltaY: number | null,
-  panX: number | null,
-  panY: number | null,
-  dragBg: boolean,
-  preventDrag: boolean,
+  deltaX: number | null, // Dragging bg x-displacement
+  deltaY: number | null, // Dragging y-displacement
+  panX: number | null, // Two-finger pan x-displacement 
+  panY: number | null, // Two-finger pan y-displacement
+  anchorX: number | null, // Initial cursorX during region select
+  anchorY: number | null, // Initial cursorY during region select
+  dragBg: boolean, // Boolean to track if all nodes should move with mouse (bg drag)
+  preventBgDrag: boolean, // Prevents bg drag when moving selected with mouse down
+  relocateAllowed: boolean, // Suppresses movement of selected when drawing select region
   scale: number,
   showKind: boolean,
   currentNode: NodeType | null,
-  currentEdge: EdgeType | null,
+  currentEdge: EdgeType | null
 };
 
+// TODO: region-based unselect, shift-click, multi-region
 export default class GraphDisplay extends Component<PropsType, StateType> {
   state = {
     nodes: [] as NodeType[],
@@ -48,18 +53,23 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     deltaY: null as (number | null),
     panX: null as (number | null),
     panY: null as (number | null),
+    anchorX: null as (number | null),
+    anchorY: null as (number | null),
     dragBg: false,
-    preventDrag: false,
+    preventBgDrag: false,
     scale: 0.5,
     showKind: true,
     currentNode: null as (NodeType | null),
-    currentEdge: null as (EdgeType | null)
+    currentEdge: null as (EdgeType | null),
+    relocateAllowed: false
   }
 
   spaceRef: any = React.createRef();
 
   componentDidMount() {
     let { components } = this.props;
+
+    // Initialize origin
     let height = this.spaceRef.offsetHeight;
     let width = this.spaceRef.offsetWidth;
     this.setState({
@@ -71,25 +81,35 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     this.spaceRef.addEventListener("touchmove", (e: any) => e.preventDefault());
     this.spaceRef.addEventListener("mousewheel", (e: any) => e.preventDefault());
     let nodes = components.map((c: ResourceType) => {
-      return { id: c.ID, name: c.Name, kind: c.Kind, x: 0, y: 0, w: 40, h: 40 }
+      return { id: c.ID, name: c.Name, kind: c.Kind, x: 0, y: 0, w: 40, h: 40 };
     });
 
-    let edges = [] as EdgeType[]
+    document.addEventListener("keydown", this.handleKeyDown);
+    document.addEventListener("keyup", this.handleKeyUp);
+
+    let edges = [] as EdgeType[];
     components.map((c: ResourceType) => {
       c.Relations.ControlRels.map((rel: any) => {
         if (rel.Source == c.ID) {
-          edges.push({ type: "ControlRel", source: rel.Source, target: rel.Target })
+          edges.push({ type: "ControlRel", source: rel.Source, target: rel.Target });
         }
       })
       c.Relations.LabelRels.map((rel: any) => {
         if (rel.Source == c.ID) {
-          edges.push({ type: "LabelRel", source: rel.Source, target: rel.Target })
+          edges.push({ type: "LabelRel", source: rel.Source, target: rel.Target });
         }
       })
 
-      this.setState({ edges })
+      this.setState({ edges });
     });
-    this.setState({nodes})
+    this.setState({ nodes });
+  }
+
+  componentWillUnmount() {
+    this.spaceRef.removeEventListener("touchmove", (e: any) => e.preventDefault());
+    this.spaceRef.removeEventListener("mousewheel", (e: any) => e.preventDefault());
+    document.removeEventListener("keydown", this.handleKeyDown);
+    document.removeEventListener("keyup", this.handleKeyUp);
   }
 
   // Update origin when expanding/collapsing (can improve w/ resize listener)
@@ -104,33 +124,48 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     }
   }
 
-  componentWillUnmount() {
-    this.spaceRef.removeEventListener("touchmove", (e: any) => e.preventDefault());
-    this.spaceRef.removeEventListener("mousewheel", (e: any) => e.preventDefault());
+  // Handle shift key for multi-select
+  handleKeyDown = (e: any) => {
+    if (e.key === 'Shift') {
+      this.setState({
+        anchorX: this.state.cursorX,
+        anchorY: this.state.cursorY,
+        relocateAllowed: false
+      });
+    }
+  }
+
+  handleKeyUp = (e: any) => {
+    if (e.key === 'Shift') {
+      this.setState({ anchorX: null, anchorY: null });
+    }
   }
 
   // Push to activeIds if not already present
-  handleClickNode = (id: number) => {
+  handleClickNode = (clickedId: number) => {
     let holding = this.state.activeIds;
-    if (!holding.includes(id)) {
-      holding.push(id);
+    if (!holding.includes(clickedId)) {
+      holding.push(clickedId);
     }
 
     // Track and store offset to grab node from anywhere (must store)
-    let node = this.state.nodes[id];
-    if (!node.toCursorX && !node.toCursorY) {
-      node.toCursorX = node.x - this.state.cursorX;
-      node.toCursorY = node.y - this.state.cursorY;
-    } else {
-      node.toCursorX = 0;
-      node.toCursorY = 0;
-    }
+    this.state.nodes.forEach((node: NodeType) => {
+      if (this.state.activeIds.includes(node.id)) {
+        if (!node.toCursorX && !node.toCursorY) {
+          node.toCursorX = node.x - this.state.cursorX;
+          node.toCursorY = node.y - this.state.cursorY;
+        } else {
+          node.toCursorX = 0;
+          node.toCursorY = 0;
+        }
+      }
+    });
 
-    this.setState({ activeIds: holding, preventDrag: true });
+    this.setState({ activeIds: holding, preventBgDrag: true, relocateAllowed: true });
   }
 
   handleReleaseNode = () => {
-    this.setState({ activeIds: [], preventDrag: false });
+    this.setState({ activeIds: [], preventBgDrag: false });
 
     // Only update dot position state on release for all active
     let { activeIds, nodes} = this.state;
@@ -141,8 +176,8 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     }
   }
 
-  onMouseMove = (e: any) => {
-    let { originX, originY, dragBg, preventDrag, scale, panX, panY } = this.state;
+  handleMouseMove = (e: any) => {
+    let { originX, originY, dragBg, preventBgDrag, scale, panX, panY, anchorX, anchorY, nodes, activeIds, relocateAllowed } = this.state;
     
     // Suppress navigation gestures
     if (scale !== 1 || panX !== 0 || panY !== 0) {
@@ -156,13 +191,25 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     this.setState({ cursorX, cursorY });
 
     // Track delta for dragging background
-    if (dragBg && !preventDrag) {
+    if (dragBg && !preventBgDrag) {
       this.setState({ deltaX: e.movementX, deltaY: e.movementY });
     }
+
+    // Check if within select region 
+    if (anchorX && anchorY) {
+      nodes.forEach((node: NodeType) => {
+        if (node.x > Math.min(anchorX, cursorX) && node.x < Math.max(anchorX, cursorX)
+          && node.y > Math.min(anchorY, cursorY) && node.y < Math.max(anchorY, cursorY)
+        ) {
+          activeIds.push(node.id);
+          this.setState({ activeIds });
+        }
+      });
+    } 
   }
 
   // Handle pan XOR zoom (two-finger gestures count as onWheel)
-  handleOnWheel = (e: any) => {
+  handleWheel = (e: any) => {
 
     // Pinch/zoom sets e.ctrlKey to true
     if (e.ctrlKey) {
@@ -176,18 +223,18 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
 
   // Pass origin to node for offset
   renderNodes = () => {
-    let { activeIds, originX, originY, cursorX, cursorY, scale, panX, panY } = this.state;
+    let { activeIds, originX, originY, cursorX, cursorY, scale, panX, panY, anchorX, anchorY, relocateAllowed } = this.state;
 
     return this.state.nodes.map((node: NodeType, i: number) => {
 
-      // Update dot position if currently selected
-      if (activeIds.includes(node.id)) {
+      // Update position if not highlighting and active
+      if (activeIds.includes(node.id) && relocateAllowed && !anchorX && !anchorY) {
         node.x = cursorX + node.toCursorX;
         node.y = cursorY + node.toCursorY;
       }
 
       // Apply movement from dragging background
-      if (this.state.dragBg && !this.state.preventDrag) {
+      if (this.state.dragBg && !this.state.preventBgDrag) {
         node.x += this.state.deltaX;
         node.y -= this.state.deltaY;
       }
@@ -199,8 +246,10 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
       }
 
       // Apply pan 
-      node.x -= panConstant * panX;
-      node.y += panConstant * panY;
+      if (this.state.panX !== 0 || this.state.panY !== 0) {
+        node.x -= panConstant * panX;
+        node.y += panConstant * panY;
+      }
       
       return (
         <Node
@@ -236,17 +285,35 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     });
   }
 
+  renderSelectRegion = () => {
+    if (this.state.anchorX && this.state.anchorY) {
+      return (
+        <SelectRegion
+          anchorX={this.state.anchorX}
+          anchorY={this.state.anchorY}
+          originX={this.state.originX}
+          originY={this.state.originY}
+          cursorX={this.state.cursorX}
+          cursorY={this.state.cursorY}
+        />
+      );
+    }
+  }
+
   render() {
     return (
       <StyledGraphDisplay
         ref={element => this.spaceRef = element}
-        onMouseMove={this.onMouseMove}
+        onMouseMove={this.handleMouseMove}
         onMouseDown={() => this.setState({ dragBg: true })}
         onMouseUp={() => this.setState({ dragBg: false })}
-        onWheel={this.handleOnWheel}
+        onWheel={this.handleWheel}
+        onClick={() => this.setState({ activeIds: [] })}
       >
         {this.renderNodes()}
         {this.renderEdges()}
+        {this.renderSelectRegion()}
+
         <InfoPanel
           currentNode={this.state.currentNode}
           currentEdge={this.state.currentEdge}

+ 1 - 1
dashboard/src/main/home/dashboard/expanded-chart/graph/Node.tsx

@@ -117,5 +117,5 @@ const StyledNode: any = styled.div.attrs((props: NodeType) => ({
   box-shadow: ${(props: any) => props.isActive ? '0 0 10px #ffffff66' : '0px 0px 10px 2px #00000022'};
   color: #ffffff22;
   border-radius: 100px;
-  z-index: 1;
+  z-index: 3;
 `;

+ 60 - 0
dashboard/src/main/home/dashboard/expanded-chart/graph/SelectRegion.tsx

@@ -0,0 +1,60 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+type PropsType = {
+  anchorX: number,
+  anchorY: number,
+  originX: number,
+  originY: number,
+  cursorX: number,
+  cursorY: number
+};
+
+type StateType = {
+};
+
+export default class SelectRegion extends Component<PropsType, StateType> {
+  state = {
+  }
+
+  render() {
+    let { cursorX, cursorY, anchorX, anchorY, originX, originY } = this.props;
+    
+    var x, y, w, h;
+    if (cursorY < anchorY) {
+      y = anchorY;
+    } else {
+      y = cursorY;
+    }
+    if (cursorX < anchorX) {
+      x = cursorX;
+    } else {
+      x = anchorX;
+    }
+
+    w = Math.abs(cursorX - anchorX);
+    h = Math.abs(cursorY - anchorY);
+
+    return (
+      <StyledSelectRegion
+        x={Math.round(originX + x)}
+        y={Math.round(originY - y)}
+        w={w}
+        h={h}
+      />
+    );
+  }
+}
+
+const StyledSelectRegion: any = styled.div.attrs((props: { x: number, y: number, w: number, h: number }) => ({
+  style: {
+    top: props.y + 'px',
+    left: props.x + 'px',
+    width: props.w + 'px',
+    height: props.h + 'px'
+    },
+}))`
+  position: absolute;
+  background: #ffffff22;
+  z-index: 1;
+`;

+ 2 - 2
dashboard/src/shared/rosettaStone.tsx

@@ -8,6 +8,6 @@ export const kindToIcon: any = {
 }
 
 export const edgeColors: any = {
-  'ControlRel': '#fcb603',
-  'LabelRel': '#9a93fa'
+  'LabelRel': '#949EFF',
+  'ControlRel': '#fcb603'
 };