editable_graph.tsx 4.7 KB

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