From fe7dbc09c3f59d7569fa2ba9f24bbd2c3af83f74 Mon Sep 17 00:00:00 2001
From: Maximilian Giller <m.giller@tu-bs.de>
Date: Sun, 4 Sep 2022 12:51:40 +0200
Subject: [PATCH] Working select box

---
 src/editor/js/components/editor.tsx      | 100 +++++++++-----
 src/editor/js/components/selectlayer.css |  28 ++++
 src/editor/js/components/selectlayer.tsx | 161 +++++++++++++++++++++++
 3 files changed, 258 insertions(+), 31 deletions(-)
 create mode 100644 src/editor/js/components/selectlayer.css
 create mode 100644 src/editor/js/components/selectlayer.tsx

diff --git a/src/editor/js/components/editor.tsx b/src/editor/js/components/editor.tsx
index 979ed71..aa051e0 100644
--- a/src/editor/js/components/editor.tsx
+++ b/src/editor/js/components/editor.tsx
@@ -11,6 +11,7 @@ import { GraphElement } from "../structures/graph/graphelement";
 import { Link } from "../structures/graph/link";
 import { NodeTypesEditor } from "./nodetypeseditor";
 import { SpaceManager } from "./spacemanager";
+import { SelectLayer } from "./selectlayer";
 
 type propTypes = {
     spaceId: string;
@@ -23,9 +24,13 @@ type stateTypes = {
     keys: { [name: string]: boolean };
     graphWidth: number;
 };
+type graphCoordinates = {
+    x: number;
+    y: number;
+};
 type clickPosition = {
-    graph: { x: number; y: number };
-    window: { x: number; y: number };
+    graph: graphCoordinates;
+    window: graphCoordinates;
 };
 type positionTranslate = {
     x: number;
@@ -40,6 +45,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
     private renderer: any;
     private graphContainer: any;
     private graphInFocus = false;
+    private selectBoxStart: graphCoordinates = undefined;
 
     constructor(props: propTypes) {
         super(props);
@@ -61,6 +67,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         this.handleLinkClick = this.handleLinkClick.bind(this);
         this.selectNode = this.selectNode.bind(this);
         this.handleResize = this.handleResize.bind(this);
+        this.handleBoxSelect = this.handleBoxSelect.bind(this);
 
         this.renderer = React.createRef();
         this.graphContainer = React.createRef();
@@ -196,6 +203,12 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         }
 
         // Just deselect if shift key is pressed
+        if (this.state.keys["Shift"] && !this.selectBoxStart) {
+            this.selectBoxStart = position.graph;
+            return;
+        }
+
+        // Just deselect if control key is pressed
         if (this.state.keys["Control"]) {
             this.selectNode(undefined);
             return;
@@ -465,6 +478,10 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         this.forceUpdate();
     }
 
+    private handleBoxSelect(selectedNodes: Node[]) {
+        console.log(selectedNodes);
+    }
+
     render(): React.ReactNode {
         return (
             <div id="ks-editor">
@@ -473,35 +490,52 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
                 <SpaceManager />
                 <div id="content">
                     <div id="force-graph-renderer" ref={this.graphContainer}>
-                        {this.state.graph ? (
-                            <ForceGraph2D
-                                ref={this.renderer}
-                                width={this.state.graphWidth}
-                                graphData={{
-                                    nodes: this.state.graph.data.nodes,
-                                    links: this.state.graph.links,
-                                }}
-                                onNodeClick={this.handleNodeClick}
-                                autoPauseRedraw={false}
-                                cooldownTicks={0}
-                                warmupTicks={this.warmupTicks}
-                                onEngineStop={this.handleEngineStop}
-                                nodeCanvasObject={this.handleNodeCanvasObject}
-                                nodeCanvasObjectMode={() => "after"}
-                                linkCanvasObject={this.handleLinkCanvasObject}
-                                linkCanvasObjectMode={() => "replace"}
-                                nodeColor={(node: Node) => node.type.color}
-                                onNodeDrag={this.handleNodeDrag}
-                                onNodeDragEnd={this.handleNodeDragEnd}
-                                onLinkClick={this.handleLinkClick}
-                                onBackgroundClick={(event: any) =>
-                                    this.handleBackgroundClick(
-                                        event,
-                                        this.extractPositions(event)
-                                    )
-                                }
-                            />
-                        ) : undefined}
+                        <SelectLayer
+                            allNodes={
+                                this.state.graph ? this.state.graph.nodes : []
+                            }
+                            screen2GraphCoords={
+                                this.renderer.current
+                                    ? this.renderer.current.screen2GraphCoords
+                                    : undefined
+                            }
+                            isEnable={() => this.state.keys["Shift"]}
+                            onBoxSelect={this.handleBoxSelect}
+                        >
+                            {this.state.graph ? (
+                                <ForceGraph2D
+                                    ref={this.renderer}
+                                    width={this.state.graphWidth}
+                                    graphData={{
+                                        nodes: this.state.graph.data.nodes,
+                                        links: this.state.graph.links,
+                                    }}
+                                    onNodeClick={this.handleNodeClick}
+                                    autoPauseRedraw={false}
+                                    cooldownTicks={0}
+                                    warmupTicks={this.warmupTicks}
+                                    onEngineStop={this.handleEngineStop}
+                                    nodeCanvasObject={
+                                        this.handleNodeCanvasObject
+                                    }
+                                    nodeCanvasObjectMode={() => "after"}
+                                    linkCanvasObject={
+                                        this.handleLinkCanvasObject
+                                    }
+                                    linkCanvasObjectMode={() => "replace"}
+                                    nodeColor={(node: Node) => node.type.color}
+                                    onNodeDrag={this.handleNodeDrag}
+                                    onNodeDragEnd={this.handleNodeDragEnd}
+                                    onLinkClick={this.handleLinkClick}
+                                    onBackgroundClick={(event: any) =>
+                                        this.handleBackgroundClick(
+                                            event,
+                                            this.extractPositions(event)
+                                        )
+                                    }
+                                />
+                            ) : undefined}
+                        </SelectLayer>
                     </div>
                     <div id="sidepanel">
                         <HistoryNavigator
@@ -565,6 +599,10 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
                         <hr />
                         <ul className="instructions">
                             <li>Click background to create node</li>
+                            <li>
+                                SHIFT+Click and drag on background to select and
+                                edit multiple nodes
+                            </li>
                             <li>CTRL+Click background to clear selection</li>
                             <li>Click node to select and edit</li>
                             <li>CTRL+Click node to delete</li>
diff --git a/src/editor/js/components/selectlayer.css b/src/editor/js/components/selectlayer.css
new file mode 100644
index 0000000..d30216a
--- /dev/null
+++ b/src/editor/js/components/selectlayer.css
@@ -0,0 +1,28 @@
+div#ks-editor #select-layer {
+    position: relative;
+}
+
+div#ks-editor #box-select {
+    position: absolute;
+    z-index: 300000;
+    border-style: dotted;
+    border-color: rgba(0, 0, 0, 0);
+    border-width: 2px;
+    background-color: rgba(0, 0, 0, 0);
+    pointer-events: none;
+
+    -webkit-transition-property: border-color, background-color;
+    -moz-transition-property: border-color, background-color;
+    -o-transition-property: border-color, background-color;
+    transition-property: border-color, background-color;
+
+    -webkit-transition-duration: 200ms;
+    -moz-transition-duration: 200ms;
+    -o-transition-duration: 200ms;
+    transition-duration: 200ms;
+}
+
+div#ks-editor #box-select.visible {
+    border-color: #3e74cc;
+    background-color: rgba(255, 255, 255, 0.5);
+}
diff --git a/src/editor/js/components/selectlayer.tsx b/src/editor/js/components/selectlayer.tsx
new file mode 100644
index 0000000..94ece31
--- /dev/null
+++ b/src/editor/js/components/selectlayer.tsx
@@ -0,0 +1,161 @@
+import React from "react";
+import { ReactNode } from "react";
+import { Node } from "../structures/graph/node";
+import "./selectlayer.css";
+
+type propTypes = {
+    children: any;
+    allNodes: Node[];
+    isEnable: () => boolean;
+    screen2GraphCoords: (x: number, y: number) => any;
+    onBoxSelect: (nodes: Node[]) => void;
+};
+
+type layerCoordinates = {
+    x: number;
+    y: number;
+};
+
+export class SelectLayer extends React.Component<propTypes> {
+    private layerContainer: any;
+    private layerBox: any;
+    private initialSelectPoint: layerCoordinates = undefined;
+
+    constructor(props: propTypes) {
+        super(props);
+
+        this.isSelecting = this.isSelecting.bind(this);
+        this.onBoxSelect = this.onBoxSelect.bind(this);
+        this.boxSelectOnPointerDown = this.boxSelectOnPointerDown.bind(this);
+        this.boxSelectOnPointerMove = this.boxSelectOnPointerMove.bind(this);
+        this.boxSelectOnPointerUp = this.boxSelectOnPointerUp.bind(this);
+
+        this.layerContainer = React.createRef();
+        this.layerBox = React.createRef();
+    }
+
+    componentDidMount(): void {
+        this.setupBoxSelect();
+    }
+
+    setupBoxSelect() {
+        // Source: https://github.com/vasturiano/force-graph/issues/151#issuecomment-735850938
+        this.layerContainer.current.onpointerdown = this.boxSelectOnPointerDown;
+        this.layerContainer.current.onpointermove = this.boxSelectOnPointerMove;
+        this.layerContainer.current.onpointerup = this.boxSelectOnPointerUp;
+    }
+
+    private isSelecting(): boolean {
+        if (!this.initialSelectPoint) {
+            return false;
+        }
+
+        if (!this.props.isEnable()) {
+            this.initialSelectPoint = undefined;
+            this.layerBox.current.className = "";
+            return false;
+        }
+
+        return true;
+    }
+
+    onBoxSelect(left: number, bottom: number, top: number, right: number) {
+        // Filter out selected nodes
+        const hitNodes: Node[] = [];
+        const tl = this.props.screen2GraphCoords(left, top);
+        const br = this.props.screen2GraphCoords(right, bottom);
+        this.props.allNodes.forEach((node: any) => {
+            if (
+                tl.x < node.x &&
+                node.x < br.x &&
+                br.y > node.y &&
+                node.y > tl.y
+            ) {
+                // Add node if in box area
+                hitNodes.push(node);
+            }
+        });
+
+        this.props.onBoxSelect(hitNodes);
+    }
+
+    boxSelectOnPointerDown(e: any) {
+        if (!this.props.isEnable()) {
+            return;
+        }
+
+        e.preventDefault();
+        this.layerBox.current.style.left = e.offsetX.toString() + "px";
+        this.layerBox.current.style.top = e.offsetY.toString() + "px";
+        this.layerBox.current.style.width = "0px";
+        this.layerBox.current.style.height = "0px";
+        this.initialSelectPoint = {
+            x: e.offsetX,
+            y: e.offsetY,
+        };
+        this.layerBox.current.className = "visible";
+    }
+
+    boxSelectOnPointerMove(e: any) {
+        if (!this.isSelecting()) {
+            return;
+        }
+
+        e.preventDefault();
+        if (e.offsetX < this.initialSelectPoint.x) {
+            this.layerBox.current.style.left = e.offsetX.toString() + "px";
+            this.layerBox.current.style.width =
+                (this.initialSelectPoint.x - e.offsetX).toString() + "px";
+        } else {
+            this.layerBox.current.style.left =
+                this.initialSelectPoint.x.toString() + "px";
+            this.layerBox.current.style.width =
+                (e.offsetX - this.initialSelectPoint.x).toString() + "px";
+        }
+        if (e.offsetY < this.initialSelectPoint.y) {
+            this.layerBox.current.style.top = e.offsetY.toString() + "px";
+            this.layerBox.current.style.height =
+                (this.initialSelectPoint.y - e.offsetY).toString() + "px";
+        } else {
+            this.layerBox.current.style.top =
+                this.initialSelectPoint.y.toString() + "px";
+            this.layerBox.current.style.height =
+                (e.offsetY - this.initialSelectPoint.y).toString() + "px";
+        }
+    }
+
+    boxSelectOnPointerUp(e: any) {
+        if (!this.isSelecting()) {
+            return;
+        }
+
+        e.preventDefault();
+        let left, bottom, top, right;
+        if (e.offsetX < this.initialSelectPoint.x) {
+            left = e.offsetX;
+            right = this.initialSelectPoint.x;
+        } else {
+            left = this.initialSelectPoint.x;
+            right = e.offsetX;
+        }
+        if (e.offsetY < this.initialSelectPoint.y) {
+            top = e.offsetY;
+            bottom = this.initialSelectPoint.y;
+        } else {
+            top = this.initialSelectPoint.y;
+            bottom = e.offsetY;
+        }
+        this.initialSelectPoint = undefined;
+        this.layerBox.current.className = "";
+        this.onBoxSelect(left, bottom, top, right);
+    }
+
+    render(): ReactNode {
+        return (
+            <div ref={this.layerContainer} id="select-layer">
+                <div ref={this.layerBox} id="box-select"></div>
+                {this.props.children}
+            </div>
+        );
+    }
+}
-- 
GitLab