diff --git a/src/editor/js/components/editor.tsx b/src/editor/js/components/editor.tsx index dd33237770d98ae8e4af784518bdc316cbc7d581..aaf4f4d1d6a92ef8abbe6144ba8ce04b79bc5ef0 100644 --- a/src/editor/js/components/editor.tsx +++ b/src/editor/js/components/editor.tsx @@ -96,7 +96,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.handleLinkCanvasObject = this.handleLinkCanvasObject.bind(this); this.handleBackgroundClick = this.handleBackgroundClick.bind(this); this.handleNodeDrag = this.handleNodeDrag.bind(this); - this.handleLinkClick = this.handleLinkClick.bind(this); + this.handleElementRightClick = this.handleElementRightClick.bind(this); this.selectNode = this.selectNode.bind(this); this.handleResize = this.handleResize.bind(this); this.handleBoxSelect = this.handleBoxSelect.bind(this); @@ -337,10 +337,9 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { } else if (element.link) { // Is link // Is it one of the adjacent links? - const found = this.selectedNodes.find((node: Node) => + return this.selectedNodes.some((node: Node) => node.links.find(element.equals) ); - return found !== undefined; } else { return false; } @@ -366,16 +365,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { * @param node Single node to select, or undefined. */ private selectNode(node: Node) { - if (node === undefined) { - this.setState({ - selectedNodes: undefined, - }); - return; - } - - this.setState({ - selectedNodes: [node], - }); + this.selectNodes([node]); } /** @@ -383,11 +373,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { * @param nodes Multiple nodes to mark as selected. */ private selectNodes(nodes: Node[]) { - if (nodes.length <= 0) { - this.setState(undefined); - return; - } - this.setState({ selectedNodes: nodes, }); @@ -402,10 +387,13 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { } // Remove undefines - const selectedNodes = this.state.selectedNodes.filter( + let selectedNodes = this.state.selectedNodes.filter( (n: Node) => n !== undefined ); + // Remove duplicates + selectedNodes = [...new Set(selectedNodes)]; + if (selectedNodes.length > 0) { return selectedNodes; } @@ -416,20 +404,17 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { private handleNodeClick(node: Node) { this.graphInFocus = true; - if (this.state.keys["Shift"]) { - // Connect to clicked node as parent while shift is pressed + if (this.state.keys["Control"]) { + // Connect to clicked node as parent while control is pressed if (this.selectedNodes == undefined) { // Have no node connected, so select this.selectNode(node); } else if (!this.selectedNodes.includes(node)) { // Already have *other* node/s selected, so connect - this.selectedNodes.forEach((selectedNode: Node) => - node.connect(selectedNode) - ); + this.connectSelectionToNode(node); } - } else if (this.state.keys["Control"]) { - // Delete node when control is pressed - node.delete(); + } else if (this.state.keys["Shift"]) { + this.toggleNodeSelection(node); } else { // By default, simply select node this.selectNode(node); @@ -437,6 +422,52 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.forceUpdate(); } + private connectSelectionToNode(node: Node) { + if (this.selectedNodes === undefined) { + return; + } + + if (this.selectedNodes.length == 1) { + node.connect(this.selectedNodes[0]); + return; + } + + // More than one new link => custom save point handling + try { + this.state.graph.disableStoring(); + this.selectedNodes.forEach((selectedNode: Node) => + node.connect(selectedNode) + ); + } finally { + this.state.graph.enableStoring(); + this.state.graph.storeCurrentData( + "Added " + + this.selectedNodes.length + + " links on [" + + node.toString() + + "]" + ); + } + } + + private toggleNodeSelection(node: Node) { + // Convert selection to array as basis + let selection = this.selectedNodes; + if (selection === undefined) { + selection = []; + } + + // Add/Remove node + if (selection.includes(node)) { + // Remove node from selection + selection = selection.filter((n: Node) => !n.equals(node)); + } else { + // Add node to selection + selection.push(node); + } + this.selectNodes(selection); + } + private handleNodeCanvasObject(node: Node, ctx: any, globalScale: any) { // add ring just for highlighted nodes if (this.isHighlighted(node)) { @@ -491,7 +522,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { const isNodeRelatedToSelection: boolean = this.selectedNodes === undefined || this.isHighlighted(node) || - !!this.selectedNodes.find((selectedNode: Node) => + this.selectedNodes.some((selectedNode: Node) => selectedNode.neighbors.includes(node) ); @@ -561,7 +592,10 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { private handleNodeDrag(node: Node) { this.graphInFocus = true; - this.selectNode(node); + + if (!this.selectedNodes || !this.selectedNodes.includes(node)) { + this.selectNode(node); + } // Should run connect logic? if (!this.state.connectOnDrag) { @@ -585,14 +619,14 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.forceUpdate(); } - private handleLinkClick(link: Link) { + /** + * Processes right-click event on graph elements by deleting them. + */ + private handleElementRightClick(element: GraphElement) { this.graphInFocus = true; - if (this.state.keys["Control"]) { - // Delete link when control is pressed - link.delete(); - this.forceUpdate(); - } + element.delete(); + this.forceUpdate(); } private handleEngineStop() { @@ -612,7 +646,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { return; } - this.selectNodes(selectedNodes); + this.selectNodes(selectedNodes.concat(this.selectedNodes)); } render(): React.ReactNode { @@ -658,7 +692,12 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { linkCanvasObjectMode={() => "replace"} nodeColor={(node: Node) => node.type.color} onNodeDrag={this.handleNodeDrag} - onLinkClick={this.handleLinkClick} + onLinkRightClick={ + this.handleElementRightClick + } + onNodeRightClick={ + this.handleElementRightClick + } onBackgroundClick={(event: any) => this.handleBackgroundClick( event, @@ -736,14 +775,17 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { <ul className="instructions"> <li>Click background to create node</li> <li> - SHIFT+Click and drag on background to select and - edit multiple nodes + SHIFT+Click and drag on background to add nodes + to selection </li> <li>CTRL+Click background to clear selection</li> <li>Click node to select and edit</li> - <li>CTRL+Click node to delete</li> - <li>CTRL+Click link to delete</li> - <li>SHIFT+Click a second node to connect</li> + <li> + SHIFT+Click node to add or remove from selection + </li> + <li>CTRL+Click another node to connect</li> + <li>Right-Click node to delete</li> + <li>Right-Click link to delete</li> {this.state.connectOnDrag ? ( <li> Drag node close to other node to connect @@ -751,7 +793,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { ) : ( "" )} - <li>DELETE to delete selected node</li> + <li>DELETE to delete selected nodes</li> <li>ESCAPE to clear selection</li> </ul> </div>