Allow click similar to drag and drop

Hi,
I have a palette I drag and drop items from to a diagram. I want to be able to click on the item in the palette and then click on the place I want it to be dropped in the palette. I want my mouse to have a placeholder same as in drag and drop functionality.
How can I do this?
Thanks,

Implement a “ChangedSelection” DiagramEvent listener on the Palette that sets the ClickCreatingTool.archetypeNodeData of the main Diagram’s ToolManager.clickCreatingTool.

Thank you.
I would like to have a tooltip with the item to be added attached to my cursor from when I press the button in the palette to when I select were to place it in the diagram same as I have for drag and drop when I’m dragging an item. (See here for example Simulating Input Events)
Is there a way I can do this?

At the time that you set the main Diagram’s ClickCreatingTool.archetypeNodeData, you can also set its Diagram.defaultCursor to be a url string. Here’s a complete sample:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Editor</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
</head>
<body>
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div style="display: flex; flex-direction: column; margin: 0 2px 0 0">
      <div id="myPaletteDiv" style="flex-grow: 1; width: 100px; background-color: floralwhite; border: solid 1px black"></div>
    </div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
  </div>
  <div>
    <button id="myLoadButton">Load</button>
    <button id="mySaveButton">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":1, "text":"hello", "color":"green", "location":"0 0"},
{"key":2, "text":"world", "color":"red", "location":"70 0"}
  ],
  "linkDataArray": [
{"from":1, "to":2}
  ]}
  </textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

// initialize main Diagram
const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    { locationSpot: go.Spot.Center },
    new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.Shape,
      {
        fill: "white", stroke: "gray", strokeWidth: 2,
        portId: "", fromLinkable: true, toLinkable: true,
        fromLinkableDuplicates: true, toLinkableDuplicates: true,
        fromLinkableSelfNode: true, toLinkableSelfNode: true
      },
      new go.Binding("stroke", "color")),
    $(go.TextBlock,
      {
        margin: new go.Margin(5, 5, 3, 5), font: "10pt sans-serif",
        minSize: new go.Size(16, 16), maxSize: new go.Size(120, NaN),
        editable: true
      },
      new go.Binding("text").makeTwoWay())
  );

// initialize Palette
myPalette =
  new go.Palette("myPaletteDiv",
    {
      "ChangedSelection": e => {
        const node = e.diagram.selection.first();
        if (node) {
          myDiagram.toolManager.clickCreatingTool.archetypeNodeData = node.data;
          const b = node.actualBounds;
          myDiagram.defaultCursor =
            `url(${e.diagram.makeImageData({ parts: new go.List().add(node) })}) ${Math.round(b.width/2)} ${Math.round(b.height/2)}, auto`;
        } else {
          myDiagram.toolManager.clickCreatingTool.archetypeNodeData = null;
          myDiagram.defaultCursor = "auto";
        }
      },
      nodeTemplateMap: myDiagram.nodeTemplateMap,
      model: new go.GraphLinksModel([
        { text: "red node", color: "red" },
        { text: "green node", color: "green" },
        { text: "blue node", color: "blue" },
        { text: "orange node", color: "orange" }
      ])
    });

// save a model to and load a model from Json text, displayed below the Diagram
function save() {
  const str = myDiagram.model.toJson();
  document.getElementById("mySavedModel").value = str;
}
document.getElementById("mySaveButton").addEventListener("click", save);

function load() {
  const str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
}
document.getElementById("myLoadButton").addEventListener("click", load);

load();
  </script>
</body>
</html>

Thanks for the example.
My problem is that the items in the palette don’t look the same as the items in the diagram. I want the cursor to be the item that I add to the diagram, not the item I choose from the palette.
The item I chose from the palette might not exist yet in the diagram.
Is there a way to create an image from the template of the item in the diagram?

Normally when dragging from a Diagram or Palette to a Diagram, the standard dragging behavior will actually create a temporary instance of the dragged data in the target diagram. So the user is actually dragging an instance of a target diagram template. Try the example: GoJS Palette -- Northwoods Software

You can do a similar thing when creating the image for the default cursor. In a skipsUndoManager transaction, add the desired data to the target diagram model, get a reference to the corresponding new node, call its ensureBounds method to force its measurement and arrangement before the end of the temporary transaction, and then call makeImageData as my sample does. Don’t forget to remove the data from the model before the end of the transaction.

I am having a few issues with this solution.

  1. I have some items in the diagram for which the cursor changes when hovering over them. I don’t want that to happen but I want to keep the cursor with the node to be added.

  2. When dragging and dropping we see the node following the cursor. Is there a way to see both the node to be added and the cursor with this solution?

  3. I would like to allow only one placement of the selected item into the node after every selection.
    So after selecting the node in the palette I want to have the cursor with with node to be added and after a second click in the palette to place the node I want to change back to auto cursor and disable creation of node onclick. For this I was using diagram.toolManager.doMouseUp to detect mouseUp events.
    I have a second palette that from there I am dragging items into the diagram. If I click on an item to add from the first palette, then go to the second palette and drag and drop an item from there to the diagram, I want to stop the adding of the item on click from the first palette in this case. I tried using mouseUp for this but it is not called in this case.

  4. When clicking to place the node in the diagram, the node added to the diagram is not exactly in the same place the cursor was so it looks like the item ‘jumped’ to the side when you place it.

  1. So you want to disable the normal cursor behavior controlled by the GraphObject.cursor property. This override of the target diagram’s ToolManager.standardMouseOver method seems to work:
      "toolManager.standardMouseOver": function() {
        if (this.diagram.defaultCursor.startsWith("url")) {
          this.diagram.currentCursor = this.diagram.defaultCursor;
          return;
        }
        go.Tool.prototype.standardMouseOver.call(this);
      },
  1. I would think it would be very confusing to see both the dragged node and a copy of the dragged node as the cursor. OK, I just tried it, and I do find the results undesirable. But it’s your choice. Here’s the code, which is a simple modification of the “ChangedSelection” DiagramEvent listener in the Palette to also set the DraggingTool.copyCursor.
        if (node) {
          myDiagram.toolManager.clickCreatingTool.archetypeNodeData = node.data;
          const b = node.actualBounds;
          myDiagram.defaultCursor = myDiagram.toolManager.draggingTool.copyCursor =
            `url(${e.diagram.makeImageData({ parts: new go.List().add(node) })}) ${Math.round(b.width/2)} ${Math.round(b.height/2)}, auto`;
        } else {
          myDiagram.toolManager.clickCreatingTool.archetypeNodeData = null;
          myDiagram.toolManager.draggingTool.copyCursor = "copy";
          myDiagram.defaultCursor = "auto";
        }
  1. OK, that’s getting too complicated for me to understand. I don’t think you should be depending on mouse up events. Instead, change behaviors based on what is selected or not. Or add a separate state variable that keeps track of what has just happened so it knows what to do.

  2. Well, it behaves well in the sample I gave you above. Perhaps the locationSpot isn’t center? Or if you don’t want it to be go.Spot.Center, you can adjust the offset in the url cursor string.

I am having an issue with the cursor in this solution. My nodes are pretty big and it seems like there is a limit on how big the cursor can be. Is there a way to bypass this issue? If not, how else can I have the node I want to add follow my cursor similar to drag and drop?

Yes, MDN says that cursors are limited to 128x128 in Firefox and Chromium, but recommends a limit of 32x32: cursor - CSS: Cascading Style Sheets | MDN

Try the following instead, which doesn’t use cursors at all, but instead a temporary copy of the node that is dragged around by the modified ToolManager.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Editor</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
</head>
<body>
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div style="display: flex; flex-direction: column; margin: 0 2px 0 0">
      <div id="myPaletteDiv" style="flex-grow: 1; width: 100px; background-color: floralwhite; border: solid 1px black"></div>
      <div id="myOverviewDiv" style="margin: 2px 0 0 0; width: 100px; height: 100px; background-color: whitesmoke; border: solid 1px black"></div>
    </div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
  </div>
  <div>
    <button id="myLoadButton">Load</button>
    <button id="mySaveButton">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":1, "text":"hello", "color":"green", "location":"0 0"},
{"key":2, "text":"world", "color":"red", "location":"70 0"}
  ],
  "linkDataArray": [
{"from":1, "to":2}
  ]}
  </textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

// initialize main Diagram
const myDiagram =
  new go.Diagram("myDiagramDiv",
    { // the Diagram.defaultTool is a ToolManager; change it to show myDragPart at pointer's position
      "defaultTool.doMouseMove": function() {
        go.ToolManager.prototype.doMouseMove.call(this);
        if (myDragPart) {
          myDragPart.visible = true;
          myDragPart.location = this.diagram.lastInput.documentPoint;
        }
      },
      mouseEnter: e => {
        if (myDragPart) myDragPart.visible = true;
      },
      mouseLeave: e => {
        if (myDragPart) myDragPart.visible = false;
      },
      "clickCreatingTool.isDoubleClick": false,  // single click inserts a new node selected in Palette
      "undoManager.isEnabled": true
    });

var myDragPart = null;

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    { locationSpot: go.Spot.Center },
    new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.Shape,
      {
        fill: "white", stroke: "gray", strokeWidth: 2,
        portId: "", fromLinkable: true, toLinkable: true,
        fromLinkableDuplicates: true, toLinkableDuplicates: true,
        fromLinkableSelfNode: true, toLinkableSelfNode: true
      },
      new go.Binding("stroke", "color")),
    $(go.TextBlock,
      {
        margin: new go.Margin(5, 5, 3, 5), font: "10pt sans-serif",
        minSize: new go.Size(16, 16), maxSize: new go.Size(120, NaN),
        editable: true
      },
      new go.Binding("text").makeTwoWay())
  );

// initialize Palette
myPalette =
  new go.Palette("myPaletteDiv",
    {
      "ChangedSelection": e => {
        const node = e.diagram.selection.first();
        if (node) {
          myDiagram.commit(diag => {
            if (myDragPart) myDiagram.remove(myDragPart);
            diag.toolManager.clickCreatingTool.archetypeNodeData = node.data;
            const data = diag.model.copyNodeData(node.data);
            diag.model.addNodeData(data);
            const newnode = diag.findNodeForData(data);
            newnode.visible = false;
            newnode.layerName = "Tool";
            newnode.location = diag.documentBounds.center;
            myDragPart = newnode;
          }, null);
        } else {
          myDiagram.toolManager.clickCreatingTool.archetypeNodeData = null;
          if (myDragPart) {
            myDiagram.model.removeNodeData(myDragPart.data);
            myDragPart = null;
          }
        }
      },
      nodeTemplateMap: myDiagram.nodeTemplateMap,
      model: new go.GraphLinksModel([
        { text: "red node", color: "red" },
        { text: "green node", color: "green" },
        { text: "blue node", color: "blue" },
        { text: "orange node", color: "orange" }
      ])
    });

// initialize Overview
myOverview =
  new go.Overview("myOverviewDiv",
    {
      observed: myDiagram,
      contentAlignment: go.Spot.Center
    });

// save a model to and load a model from Json text, displayed below the Diagram
function save() {
  const str = myDiagram.model.toJson();
  document.getElementById("mySavedModel").value = str;
}
document.getElementById("mySaveButton").addEventListener("click", save);

function load() {
  const str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
}
document.getElementById("myLoadButton").addEventListener("click", load);

load();
  </script>
</body>
</html>

This worked well, but I’m facing now another issue. When I click to place the new node when I’m hovering over an existing node in the diagram nothing happens. Is there a way to create the new node even if I’m hovering over an existing node?

Regarding the alternative solution using a temporary copy of a node instead of a cursor, I have an issue with that since our nodes are wrapped with large linkable areas and when clicking to place the node, nothing happens the same as when clicking on any other node in the diagram. (Moving the temporary node further away from the cursor solves this issue but doesn’t look the way I want it to.)
Maybe we can just place the temporary node instead of creating a new node?

I thought you changed your mind and don’t want to set the cursor.

As you can see in the docs, ClickCreatingTool.canStart:

This tool can run when the diagram is not read-only and supports creating new nodes, and when there has been a click (or double-click if isDoubleClick is true) in the background of the diagram (not on a Part), and archetypeNodeData is an object that can be copied and added to the model.

So you need to override that method so that it doesn’t care about whether the click or double-click is on a Part rather than in the background of the diagram. Adapted from the code I just gave you:

  new go.Diagram("myDiagramDiv",
    { // the Diagram.defaultTool is a ToolManager; change it to show myDragPart at pointer's position
      "defaultTool.doMouseMove": function() {
        go.ToolManager.prototype.doMouseMove.call(this);
        if (myDragPart) {
          myDragPart.visible = true;
          myDragPart.location = this.diagram.lastInput.documentPoint;
        }
      },
      mouseEnter: e => {
        if (myDragPart) myDragPart.visible = true;
      },
      mouseLeave: e => {
        if (myDragPart) myDragPart.visible = false;
      },
      // optional choice:
      "clickCreatingTool.isDoubleClick": false,  // single click inserts a new node selected in Palette
      "clickCreatingTool.canStart": function() {
        if (!this.isEnabled) return false;

        // gotta have some node data that can be copied
        if (this.archetypeNodeData === null) return false;

        const diagram = this.diagram;

        // heed IsReadOnly & AllowInsert
        if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
        if (!diagram.allowInsert) return false;

        // only works with the left button
        if (!diagram.lastInput.left) return false;

        // the mouse down point needs to be near the mouse up point
        if (this.isBeyondDragSize()) return false;

        // maybe requires double-click; otherwise avoid accidental double-create
        if (this.isDoubleClick) {
          if (diagram.lastInput.clickCount === 1) this._firstPoint = diagram.lastInput.viewPoint.copy();
          if (diagram.lastInput.clickCount !== 2) return false;
          if (this.isBeyondDragSize(this._firstPoint)) return false;
        } else {
          if (diagram.lastInput.clickCount !== 1) return false;
        }

        return true;
      },
      "clickCreatingTool._firstPoint": new go.Point(),