Partially interactable background parts

Hi,

we have some parts (e.g. Shapes such as Rectangles), which can be locked via the context menu.
If these parts are locked they should not be selectable, to prevent the user from accidently moving them instead of panning the diagram.
We have functions which are executed, when the user double-clicks the background. These should not be blocked by locked background parts.
The user should also to be able to clear the current selection by clicking a locked background part (like clicking the empty diagram).

To unlock the user uses a contextClick on the locked part and then has the option to “unlock” the part.

In short:
Is there a way to make a part only respond to context clicks and ignore everything else as if it were non-pickable?

Cheers
Niklas

Try this:

<!DOCTYPE html>
<html>
<head>
  <title>Parts with only context click</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  "Lockable" Parts are normally in the un-<b>pickable</b> Layer named "Lockable".
  But ContextMenuTool has been customized to support context clicks on "Lockable" Parts.
  Users can edit the text of a "Lockable" Part only if it is unlocked (i.e. in the default Layer).
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">

// Allow context clicks to find Parts in the "Lockable" Layer
class LockableContextMenuTool extends go.ContextMenuTool {
  findObjectWithContextMenu(obj) {
    let found = null;
    const lay = this.diagram.findLayer("Lockable");
    if (lay) {
      this.diagram.commit(diag => {
        lay.pickable = true;
        lay.isTemporary = false;
        found = super.findObjectWithContextMenu(obj);
        lay.pickable = false;
        lay.isTemporary = true;
      }, null);  // skipsUndoManager
    } else {
      found = super.findObjectWithContextMenu(obj);
    }
    return found;
  }
}

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      // install custom ContextMenuTool
      contextMenuTool: new LockableContextMenuTool(),
      "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 = e.model.toJson();
        }
      }
    });

// create a new Layer just for "Lockable"s
myDiagram.addLayerAfter(
  new go.Layer({ name: "Lockable", isTemporary: true, pickable: false }),
  myDiagram.findLayer("Grid")
);

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

// The special kind of Part that is normally in the "Lockable" Layer.
// It has a context menu that supports unlocking the Part by moving it to the default Layer.
// Such a Part's context menu will then support locking that Part by moving it back to the "Lockable" Layer.
myDiagram.nodeTemplateMap.add("Lockable",
  new go.Part("Auto", {
      layerName: "Lockable",
      contextMenu:
        go.GraphObject.build("ContextMenu")
          .add(
            go.GraphObject.build("ContextMenuButton", {
                click: (e, button) => {
                  e.diagram.commit(diag => {
                    const part = button.part.adornedPart;
                    part.layerName = part.layerName === "Lockable" ? "" : "Lockable";
                    part.isSelected = (part.layerName !== "Lockable");
                  });
                }
              })
              .add(
                new go.TextBlock("Unlock")
                  .bindObject("text", "adornedPart", part => part.layerName === "Lockable" ? "Unlock" : "Lock")
              )
          ),
      width: 100, height: 100
    })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({ fill: "whitesmoke" }),
      new go.TextBlock({ editable: true })
        .bindTwoWay("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" },
  { category: "Lockable", text: "Lockable #1", loc: "0 0" },
  { category: "Lockable", text: "Lockable #2", loc: "-120 -120" },
  { category: "Lockable", text: "Lockable #3", loc: "150 150" },
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 2 },
  { from: 3, to: 4 },
  { from: 4, to: 1 }
]);
  </script>
</body>
</html>

Hi walter,

thanks for the suggestion with the custom ContextMenuTool.
That works for us and seems to be extensible/robust enough.

Cheers,
Niklas