import * as React from 'react'; import {d3Types, Graph, GraphProps, Forces} from "./graph"; import { NodeCreation, NodeDeletion, EdgeCreation, EdgeUpdate, PrimitiveDelta, } from "../onion/primitive_delta"; import {NodeState, ValueState, GraphDeltaExecutor} from "../onion/delta_executor"; import { CompositeLevel, } from "../onion/composite_delta"; import {PrimitiveValue, UUID} from "../onion/types"; import { Version, initialVersion, VersionRegistry, } from "../onion/version"; export type NodeType = d3Types.d3Node; export type LinkType = d3Types.d3Link; export type GraphType = d3Types.d3Graph; interface EditableGraphProps { graph: GraphType; graphDeltaExecutor: GraphDeltaExecutor; forces: Forces; version: Version; generateUUID: () => UUID; newVersionHandler: (Version) => void; newDeltaHandler: (CompositeDelta) => void; setNextNodePosition: (x:number, y:number) => void; } interface EditableGraphState { } export class EditableGraph extends React.Component { mouseDownNode: NodeType | null = null; // only while mouse button is down, does this record the d3Node that was under the cursor when the mouse went down. readonly compositeLvl: CompositeLevel = new CompositeLevel(); readonly versionRegistry: VersionRegistry = new VersionRegistry(); constructor(props) { super(props); this.state = {showCrossHair: false}; } mouseDownHandler = (event, {x,y}, mouseDownNode: NodeType | undefined) => { event.stopPropagation(); if (mouseDownNode) { this.mouseDownNode = mouseDownNode; } } mouseUpHandler = (event, {x,y}, mouseUpNode: NodeType | undefined) => { event.stopPropagation(); // Construct the delta(s) that capture the user's change: const deltas: PrimitiveDelta[] = (() => { if (event.button === 2) { // right mouse button if (this.mouseDownNode !== null && this.mouseDownNode.obj instanceof NodeState) { // create outgoing edge... if (mouseUpNode !== undefined) { // right mouse button was dragged from one node to another -> create/update edge from one node to the other const label = prompt("Edge label (ESC to cancel):", "label"); if (label !== null) { return this.mouseDownNode.obj.getDeltasForSetEdge(label, mouseUpNode.obj); } } else { // right mouse button was dragged from a node to empty space -> create/update edge from node to a value const label = prompt("Edge label (ESC to cancel):", "label"); if (label !== null) { let enteredValue: string|null = "\"42\""; while (true) { enteredValue = prompt("Target value (enter a JSON-parsable(!) string (with quotes), number or boolean):\n(ESC to cancel)", enteredValue); if (enteredValue === null) { break; } let parsedValue; try { parsedValue = JSON.parse(enteredValue); } catch (err) { alert("Invalid JSON!"); continue; } const typeofParsedValue = typeof parsedValue; if (typeofParsedValue !== "string" && typeofParsedValue !== "number" && typeofParsedValue !== "boolean") { alert("Expected string, number or boolean. Got: " + typeofParsedValue); continue; } return this.mouseDownNode.obj.getDeltasForSetEdge(label, this.props.graphDeltaExecutor.getValueState(parsedValue)); } } } } else { // right mouse button clicked -> create node const uuid = this.props.generateUUID(); this.props.setNextNodePosition(x,y); return [new NodeCreation(uuid)]; } } else if (event.button === 1) { // middle mouse button if (mouseUpNode !== undefined) { // middle mouse button click on node -> delete node (and incoming/outgoing edges) return mouseUpNode.obj.getDeltasForDelete(); } } return []; })(); if (deltas.length > 0) { // Let the world know that there is a new (composite) delta, and a new version: const composite = this.compositeLvl.createComposite(deltas); const version = this.versionRegistry.createVersion(this.props.version, composite); this.props.newDeltaHandler(composite); this.props.newVersionHandler(version); // Actually update the graph state: deltas.forEach(d => d.exec(this.props.graphDeltaExecutor)); } this.mouseDownNode = null; } render() { return ( ); } }