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