Have nodes follow cursor

Hello,

Our design requirements request that a user should be able to add a selection nodes with a click of a button. The button exists outside of the canvas boundaries. When the user adds nodes with a button click, the node(s) should follow the cursor around (similar to a click and drag effect) until the canvas is double clicked. Upon double clicking the canvas, the nodes should be placed at the cursor location. However, the node(s) will not follow the cursor outside of the boundaries of the canvas.

Is this possible and if so, do you have any suggestions, examples or documentation to look over?

Thank you for your help!

This requires a custom Tool.

Would you expect to be able to scroll/zoom the diagram durimg this operation?

I think what Iā€™m going to do is make a dummy html element that appears like our node and have that chase around the cursor and then add the nodes on a click. That would allow scrolling and zooming.

Thanks =)

It might be easiest to customize the ClickCreatingTool. I can look into this later today.

Is it somehow possible to create a new node that does not currently exist on the diagram using a tool?

Yes, that is what the ClickCreatingTool is for. Look at any of the samples that set ClickCreatingTool.archetypeNodeData, such as Basic: Basic Sample Showing ToolTips and Context Menus for Nodes, Links, Groups, and Diagram | GoJS Diagramming Library

Thanks! I was able to get a node added to the diagram using this property when creating the diagram.

ā€˜clickCreatingTool.archetypeNodeDataā€™: go.GraphObject.make(
go.TextBlock,
{
width: 30,
height: 30,
alignment: go.Spot.Left,
textAlign: ā€˜leftā€™,
text: ā€˜HI THERE DUDEā€™
},
)

Is it at all possible to update this value dynamically to create different nodes provided certain circumstances? Or can this property only be set when the diagram is initially created?

Lastly, we need our diagram to update on mousemove events (and not clicks), so from what I read, I think a custom tool is necessary. Is that correct?

No, ClickCreatingTool.archetypeNodeData should have as a value a mode data object, typically a JavaScript Object. Not a GraphObject!

Yes, youā€™ll need a custom tool. The question is exactly what gestures and in what order events must happen. Itā€™s clear that you want one or more Parts (which ones? more than one?) to be moved when the user is moving the mouse without holding down any mouse buttons. But how does the user get into that state? What happens if the user clicks a mouse button or a modifier key or some other keyboard keys?

Thanks for your responses. Hereā€™s our scenario with a bit more detail:

The user can click an ā€œEdit Buttonā€ in our application. This will pop up a dialog and allow them to select some stuff. When they click ā€œAddā€ the dialog will close and 1 to many new nodes will be added to the diagram. These new node(s) should follow the cursor (while moving within the diagram) until the user clicks on the canvas. Once they click the diagram, those new nodes will be placed permanently on the canvas and no longer follow the cursor.

How should this work on a touch-only device?

What should happen as the user moves the mouse from the HTML Button into the Diagramā€™s viewport?

How can the user cancel the behavior during the drag? Presumably they can hit the ā€œEscapeā€ key, but wonā€™t modifications have been made to the diagram/model already?

How should this work on a touch-only device?
-We do not have any specifications or requirements for a touch-only device.

What should happen as the user moves the mouse from the HTML Button into the Diagramā€™s viewport?
-After clicking add, the nodes will chase around the mouse within the boundaries of the diagram. If the mouse leaves the diagram, then the nodes remain on the canvas at the last x y coordinate, until the mouse reenters the diagram. They will follow the cursor until the canvas is clicked (or escape key is pressed).

How can the user cancel the behavior during the drag? Presumably they can hit the ā€œEscapeā€ key, but wonā€™t modifications have been made to the diagram/model already?
-The intention is to use ā€œEscapeā€ to clear the diagram of the chasing nodes. That is, if ā€œEscapeā€ was pressed before a diagram click, adding the nodes to the canvas. We will in theory have access to the nodeā€™s unique identifiers, so an ā€œEscapeā€ press will filter off the set of nodes from the node array.

Here is our rudimentary tool thus far:

NodeAddDraggingTool extends go.Tool {
  canStart(): boolean {
    console.log('canStart')
    return true;
  }

  doMouseMove(): void {
    console.log('We want continuous logs here, until nodes are dropped or escape is canceled')
    console.log('doMouseMove', )
  }
}

in initDiagram: 
dia.toolManager.mouseMoveTools.add(new NodeAddDraggingTool())

What we are noticing with the code above, is that the diagram needs a click in order to get the mouse move logs. The mouse move logs only log briefly, and then stop until the diagram is clicked again. Is it possible to get continuous logs until either the escape key is pressed or the new nodes have been clicked to the diagram?

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <button id="myTestButton">Add and Drag Nodes</button>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go.js"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = myDiagram.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue" },
  { key: 2, text: "Beta", color: "orange" },
  { key: 3, text: "Gamma", color: "lightgreen" },
  { key: 4, text: "Delta", color: "pink" }
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 2 },
  { from: 3, to: 4 },
  { from: 4, to: 1 }
]);

class CustomInsertionTool extends go.Tool {
  constructor() {
    super();
    this._nodeDataArray = null;
    this._nodes = null;
    this._map = null;
    this._startPoint = new go.Point();
  }

  // don't need canStart override if always invoked programmaticly

  // provide the node data objects to be added when this tool is invoked
  get nodeDataArray() { return this._nodeDataArray; }
  set nodeDataArray(arr) {
    if (Array.isArray(arr)) {
      this._nodeDataArray = arr;
    } else {
      this._nodeDataArray = null;
    }
  }

  doStart() {
    this._nodes = new go.Set();  // remember the temporary nodes that are dragged
    // put the temporary nodes off-screen, below and to the right
    this._startPoint.setRectSpot(this.diagram.viewportBounds, go.Spot.BottomRight).offset(1000, 1000);
    const loc = this._startPoint.copy();
    // now add a temporary node for each node data object added to the model
    this.nodeDataArray.forEach(data => {
      this.diagram.model.addNodeData(data);
      const node = this.diagram.findNodeForData(data);
      this._nodes.add(node);
      node.layerName = "Adornment";  // this is a Layer.isTemporary Layer
      node.location = loc;
      loc.x += 20; loc.y += 20;  // you could customize this to place the new nodes how you like
    });
    // this is not necessary, but makes it visually clear what the new nodes are
    this.diagram.selectCollection(this._nodes);
    // remember the original locations of the temporary nodes
    this._map = new go.Map();
    this._nodes.each(node => {
      this._map.set(node, new go.DraggingInfo(node.location.copy()));
    });
    // so that the Escape key is passed to the current tool
    this.diagram.focus();
  }

  doStop() {
    this.diagram.removeParts(this._nodes);  // make sure there aren't any temporary nodes in the Diagram
    this._map = null;  // clear out any references, for garbage collection
    this._nodes = null;
  }

  doMouseMove() {
    if (!this.isActive) return;  // don't do anything until the tool is active
    const p = this.diagram.lastInput.documentPoint;
    this._nodes.each(node => {
      // move each temporary node according to the current mouse point offset by its original location relative to the startPoint
      const loc = this._map.get(node).point;
      node.moveTo(p.x + this._startPoint.x - loc.x, p.y + this._startPoint.y - loc.y, true);
    });
  }

  doMouseUp() {
    // now add the real ones, in a transaction
    const loc = this.diagram.lastInput.documentPoint.copy();
    this.diagram.commit(diag => {
      this.nodeDataArray.forEach(data => {
        // look up the offset of the temporary node
        const tempnode = diag.findNodeForData(data);
        const lastloc = tempnode.location.copy();
        diag.model.removeNodeData(data);  // remove the temporary node
        // now add it for real
        diag.model.addNodeData(data);
        const newnode = diag.findNodeForData(data);
        if (newnode) newnode.location = lastloc;
      })
    });
    this._nodes.clear();  // we've already removed all of the temporary nodes
    this.stopTool();
  }
}  // end CustomDraggingTool


document.getElementById("myTestButton").addEventListener("click", e => {
  const color = go.Brush.randomColor();
  const d1 = { text: "New 1", color: color };
  const d2 = { text: "New 2", color: color };
  const tool = new CustomInsertionTool();
  tool.nodeDataArray = [d1, d2];
  myDiagram.currentTool = tool;
  tool.doActivate();
});
  </script>
</body>
</html>

This is exactly what we need, thank you!

Have a nice weekend.

Hi Walter, I have our custom implementation mostly done aside from couple of kinks that need to be worked out. In your doStart() function, you make a call to this.diagram.focus() and you have a comment noting that the escape key is passed to the tool. When the user presses escape, this clears out any cursor-following nodes that havenā€™t been placed by a click on the diagram. However, I do not see any of my console logs for doStop(), etc. How exactly is this tool clearing out the cursor following nodes in this case?

I am trying to replicate similar behavior in a doKeyDown() function, where if that key is escape, I call stopTool() and clear out the cursor following nodes. We need to do some additional updates to nodes when the user hits escape, but I am getting unintended behaviors.

image

Is there a better way to stop the tool that what is shown above?

It is standard behavior for the Escape key to call Tool.doCancel, which calls Tool.doStop when the current tool is stopped and replaced by another tool. When I try it, the user hitting the Escape key does call CustomInsertionTool.doStop, so I cannot explain why you do not see the call to doStop.

The implementation of Tool.doKeyDown in v3.0 is:

  public doKeyDown(): void {
    const diagram = this.diagram;
    if (diagram.lastInput.code === 'Escape') {
      this.doCancel();
    }
  }

Yep, that is working. I was coding tired and had some functionality disabledā€¦

Question, we have some use cases where we might need to start and cancel a transaction within the tool. In the event that the user has nodes following the cursor, but hits escape, we want to clear a transactionā€¦

Both tool.stopTransaction() and diagram.undomanager.rollbackTransaction() result in this being placed at the cursor when hitting escape:

image

On do start, we are calling diagram.startTransaction(), but in certain cases, we want to be able to cancel it (without the placeholders dropped on the diagram).

Edit - I think the placeholder was a result of calling setCollection(this._nodes).Calling removeParts(this._nodes) cleaned up the placeholder image.

There shouldnā€™t be any calls to startTransaction or stopTransaction ā€“ the transaction happens entirely in the doMouseUp call.

Correct, the nodes are committed in doMouseUp. However, there is a bit more to our use case of how nodes are added and various api calls/node updates resulting from those placed nodes.

Are you specifically saying not to use transactions in the tool, or just the transaction is taken care of in doMouseDown?

I donā€™t know what you want to do, so itā€™s hard for me to make a recommendation. I just know that the simplest case is handled by doing the whole transaction when handling the mouse-up event.

Certainly there are tools that conduct transactions over longer periods of time (i.e. over many input events). Usually that is implemented by calling Tool.startTransaction in an override of Tool.doActivate, and calling Tool.stopTransaction in an override of Tool.doDeactivate. Then your code just needs to set Tool.transactionResult to the transaction name that you want to commit, or else to null if you want it to be rolled-back.

1 Like