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?
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.
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?
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.
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>
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.
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:
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.
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.