editable_graph.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import * as React from 'react';
  2. import {d3Types, Graph, GraphProps, Forces} from "./graph";
  3. import {
  4. NodeCreation,
  5. NodeDeletion,
  6. EdgeCreation,
  7. EdgeUpdate,
  8. PrimitiveDelta,
  9. } from "../onion/primitive_delta";
  10. import {NodeState, ValueState, GraphDeltaExecutor} from "../onion/delta_executor";
  11. import {
  12. CompositeLevel,
  13. } from "../onion/composite_delta";
  14. import {PrimitiveValue, UUID} from "../onion/types";
  15. import {
  16. Version,
  17. initialVersion,
  18. VersionRegistry,
  19. } from "../onion/version";
  20. export type NodeType = d3Types.d3Node<NodeState|ValueState>;
  21. export type LinkType = d3Types.d3Link<null>;
  22. export type GraphType = d3Types.d3Graph<NodeState|ValueState,null>;
  23. interface EditableGraphProps {
  24. graph: GraphType;
  25. graphDeltaExecutor: GraphDeltaExecutor;
  26. forces: Forces;
  27. version: Version;
  28. generateUUID: () => UUID;
  29. newVersionHandler: (Version) => void;
  30. newDeltaHandler: (CompositeDelta) => void;
  31. setNextNodePosition: (x:number, y:number) => void;
  32. }
  33. interface EditableGraphState {
  34. }
  35. export class EditableGraph extends React.Component<EditableGraphProps, EditableGraphState> {
  36. 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.
  37. readonly compositeLvl: CompositeLevel = new CompositeLevel();
  38. readonly versionRegistry: VersionRegistry = new VersionRegistry();
  39. constructor(props) {
  40. super(props);
  41. this.state = {showCrossHair: false};
  42. }
  43. mouseDownHandler = (event, {x,y}, mouseDownNode: NodeType | undefined) => {
  44. event.stopPropagation();
  45. if (mouseDownNode) {
  46. this.mouseDownNode = mouseDownNode;
  47. }
  48. }
  49. mouseUpHandler = (event, {x,y}, mouseUpNode: NodeType | undefined) => {
  50. event.stopPropagation();
  51. // Construct the delta(s) that capture the user's change:
  52. const deltas: PrimitiveDelta[] = (() => {
  53. if (event.button === 2) { // right mouse button
  54. if (this.mouseDownNode !== null && this.mouseDownNode.obj instanceof NodeState) {
  55. // create outgoing edge...
  56. if (mouseUpNode !== undefined) {
  57. // right mouse button was dragged from one node to another -> create/update edge from one node to the other
  58. const label = prompt("Edge label (ESC to cancel):", "label");
  59. if (label !== null) {
  60. return this.mouseDownNode.obj.getDeltasForSetEdge(label, mouseUpNode.obj);
  61. }
  62. }
  63. else {
  64. // right mouse button was dragged from a node to empty space -> create/update edge from node to a value
  65. const label = prompt("Edge label (ESC to cancel):", "label");
  66. if (label !== null) {
  67. let enteredValue: string|null = "\"42\"";
  68. while (true) {
  69. enteredValue = prompt("Target value (enter a JSON-parsable(!) string (with quotes), number or boolean):\n(ESC to cancel)", enteredValue);
  70. if (enteredValue === null) {
  71. break;
  72. }
  73. let parsedValue;
  74. try {
  75. parsedValue = JSON.parse(enteredValue);
  76. } catch (err) {
  77. alert("Invalid JSON!");
  78. continue;
  79. }
  80. const typeofParsedValue = typeof parsedValue;
  81. if (typeofParsedValue !== "string" && typeofParsedValue !== "number" && typeofParsedValue !== "boolean") {
  82. alert("Expected string, number or boolean. Got: " + typeofParsedValue);
  83. continue;
  84. }
  85. return this.mouseDownNode.obj.getDeltasForSetEdge(label, this.props.graphDeltaExecutor.getValueState(parsedValue));
  86. }
  87. }
  88. }
  89. }
  90. else {
  91. // right mouse button clicked -> create node
  92. const uuid = this.props.generateUUID();
  93. this.props.setNextNodePosition(x,y);
  94. return [new NodeCreation(uuid)];
  95. }
  96. }
  97. else if (event.button === 1) { // middle mouse button
  98. if (mouseUpNode !== undefined) {
  99. // middle mouse button click on node -> delete node (and incoming/outgoing edges)
  100. return mouseUpNode.obj.getDeltasForDelete();
  101. }
  102. }
  103. return [];
  104. })();
  105. if (deltas.length > 0) {
  106. // Let the world know that there is a new (composite) delta, and a new version:
  107. const composite = this.compositeLvl.createComposite(deltas);
  108. const version = this.versionRegistry.createVersion(this.props.version, composite);
  109. this.props.newDeltaHandler(composite);
  110. this.props.newVersionHandler(version);
  111. // Actually update the graph state:
  112. deltas.forEach(d => d.exec(this.props.graphDeltaExecutor));
  113. }
  114. this.mouseDownNode = null;
  115. }
  116. render() {
  117. return (
  118. <Graph
  119. graph={this.props.graph}
  120. forces={this.props.forces}
  121. mouseDownHandler={this.mouseDownHandler}
  122. mouseUpHandler={this.mouseUpHandler}
  123. />
  124. );
  125. }
  126. }