Drag and Drop Example Content Alignment

Hi @walter,

I have a followup question to that big drag and drop question I had earlier.

https://forum.nwoods.com/t/undo-redo-with-non-realtime-dragging-and-shifting/16680

It seems I found another issue with the example you shared. I only uncovered it after getting a new requirement from UX. It basically has to do with the contentAlignment option on the diagram object. I am looking to set it to go.Spot.Left or go.Spot(0, 0.3), something along those lines.

What I found was that when setting contentAlignment to a value on the diagram, it seems that the moveParts function that shifts the subtree right is somehow acting wacky. It is odd because it is only acting weird when dragging from the palette and not when reordering between existing nodes.

To reproduce, drag over a node from the palette but don’t drop just yet. Drag so that you make drop zones appear and disappear again and again. You’ll see that during the drag over phase, the whole tree of nodes is jumping up vertically in both directions as the adornments are added and cleaned up.

I copied your exact example and just simplified the existing node/link data and added contentAlignment on the diagram. You can copy what I shared and should see the issue. Please let me know if you have any suggestions, thanks!

<!DOCTYPE html>
<html>
  <head>
    <title>Splice Node into Link with DropZone</title>
    <!-- Copyright 1998-2024 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>
    <textarea id="mySavedModel" style="width: 100%; height: 250px"></textarea>

    <script src="https://unpkg.com/gojs"></script>
    <script src="https://unpkg.com/gojs/extensions/NonRealtimeDraggingTool.js"></script>
    <script id="code">
      class DropZoneDraggingTool extends NonRealtimeDraggingTool {
        constructor(init) {
          super();
          this.subtreeOffset = new go.Point(100, 0);
          const $ = go.GraphObject.make;
          this.DropZone = $(
            go.Adornment,
            "Auto",
            { locationSpot: go.Spot.Center },
            $(go.Shape, "RoundedRectangle", { fill: "white", strokeWidth: 5, stroke: "lightblue" }),
            $(
              go.Panel,
              "Auto",
              { width: 100, height: 60, margin: 5 },
              $(go.Shape, "RoundedRectangle", { fill: "white", stroke: "lightblue", strokeDashArray: [4, 2] }),
              $(
                go.Panel,
                "Spot",
                $(go.Shape, { fill: "white", stroke: "lightgray", strokeDashArray: [4, 2], width: 30, height: 20 }),
                $(go.Shape, {
                  alignment: new go.Spot(0.5, 0.5, 0, -10),
                  geometryString: "M5 0L5 20 M0 15 L5 20 10 15",
                  stroke: "lightgray",
                  strokeWidth: 2,
                })
              )
            )
          );
          this.DropZone.ensureBounds();
          // internal state
          this._subtree = null;
          this._wasAvoidsNodes = false;
          this._draggedNode = null;
          if (init) Object.assign(this, init);
        }

        // User must drag a single Node
        findDraggablePart() {
          if (this.diagram.selection.count > 1) return null;
          const part = super.findDraggablePart();
          if (part instanceof go.Node) {
            this._draggedNode = part;
            return part;
          }
          return null;
        }

        // Show a DropZone if dragging over a Link (other than one connected with _draggedNode)
        doDragOver(pt, obj) {
          // method override
          // find Part at PT, ignoring temporary Parts except for the DropZone (temporary because it's an Adornment)
          var trgt = this.diagram.findObjectAt(
            pt,
            (x) => x.part,
            (x) => x === this.DropZone || !x.layer.isTemporary
          );
          if (trgt instanceof go.Link) {
            const link = trgt;
            if (link.fromNode === this._draggedNode || link.toNode === this._draggedNode) return;
            if (this.DropZone.adornedPart === null) {
              const mid = link.routeBounds.center;
              const needsShift = link.routeBounds.width < this.DropZone.actualBounds.width + 100 || link.routeBounds.height < this.DropZone.actualBounds.height + 100;
              const oldskips = this.diagram.skipsUndoManager;
              this.diagram.skipsUndoManager = true;
              if (needsShift) {
                this._wasAvoidsNodes = link.routing === go.Link.AvoidsNodes;
                if (this._wasAvoidsNodes) link.routing = go.Link.Orthogonal;
                this._subtree = link.toNode.findTreeParts();
                // shift subtree rightward
                this.diagram.moveParts(this._subtree, this.subtreeOffset);
              }
              link.isHighlighted = true;
              this.DropZone.adornedObject = link;
              link.addAdornment("DropZone", this.DropZone);
              this.DropZone.location = new go.Point(mid.x + 50, mid.y);
              this.diagram.skipsUndoManager = oldskips;
            }
          } else if (trgt !== this.DropZone) {
            this.cleanup();
          }
        }

        cleanup() {
          const link = this.DropZone.adornedPart;
          if (link) {
            const oldskips = this.diagram.skipsUndoManager;
            this.diagram.skipsUndoManager = true;
            if (this._subtree) {
              // shift subtree leftward
              this.diagram.moveParts(this._subtree, new go.Point(-this.subtreeOffset.x, -this.subtreeOffset.y));
              this._subtree = null;
              if (this._wasAvoidsNodes) link.routing = go.Link.AvoidsNodes;
              this._wasAvoidsNodes = false;
            }
            link.isHighlighted = false;
            link.removeAdornment("DropZone");
            this.DropZone.adornedObject = null;
            this.diagram.skipsUndoManager = oldskips;
          }
        }

        // If dropped into DropZone, splice it into the corresponding Link
        // (Note, not using doDropOnto due to undo problems.
        // Overriding doMouseUp means needing "ExternalObjectsDropped" listener too,
        // duplicating some of the work.)
        doMouseUp() {
          // method override
          const link = this.DropZone.adornedPart;
          const node = this._draggedNode;
          const pt = this.diagram.lastInput.documentPoint;
          const trgt = this.diagram.findObjectAt(
            pt,
            (x) => x.part,
            (x) => x === this.DropZone
          );
          if (trgt === this.DropZone) {
            this.cleanup();
            this.spliceIntoLink(link, node);
          } else {
            this.cleanup();
          }
          super.doMouseUp();
        }

        // Splice the _draggedNode into the dropped-onto Link
        spliceIntoLink(link, node) {
          if (!link || !node) return;
          const diag = this.diagram;
          if (!diag) return;
          // disconnect node being dropped (copy collection to avoid iterating over modifications)
          new go.List(node.findLinksConnected()).each((l) => diag.remove(l));
          const to = link.toNode;
          const linkdata = {};
          diag.model.addLinkData(linkdata);
          const newlink = diag.findLinkForData(linkdata);
          if (newlink !== null) {
            // splice in that node
            link.toNode = node;
            newlink.fromNode = node;
            newlink.toNode = to;
          }
        }

        doDeactivate() {
          this._subtree = null;
          this._draggedNode = null;
          super.doDeactivate();
        }

        doCancel() {
          this.cleanup();
          super.doCancel();
        }
      }

      const $ = go.GraphObject.make;

      const myDiagram = new go.Diagram("myDiagramDiv", {
        contentAlignment: go.Spot.Center,
        layout: new go.TreeLayout(),
        // install the replacement DraggingTool:
        draggingTool: new DropZoneDraggingTool({ duration: 400 }),
        ExternalObjectsDropped: (e) => {
          const tool = e.diagram.toolManager.draggingTool;
          const pt = e.diagram.lastInput.documentPoint;
          const trgt = e.diagram.findObjectAt(
            pt,
            (x) => x.part,
            (x) => x === tool.DropZone
          );
          if (trgt === tool.DropZone) {
            const link = tool.DropZone.adornedPart;
            const node = e.diagram.selection.first();
            tool.cleanup();
            tool.spliceIntoLink(link, node);
          }
        },
        "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();
          }
        },
      });

      myDiagram.nodeTemplate = $(go.Node, "Auto", { locationSpot: go.Spot.Center }, $(go.Shape, { fill: "white" }, new go.Binding("fill", "color")), $(go.TextBlock, { margin: 8 }, new go.Binding("text")));

      myDiagram.linkTemplate = $(
        go.Link,
        { routing: go.Link.AvoidsNodes, reshapable: true },
        // the highlight path Shape
        $(
          go.Shape,
          { isPanelMain: true, strokeWidth: 7, stroke: "transparent" },
          // when highlighted, show this thick Shape in red
          new go.Binding("stroke", "isHighlighted", (h) => (h ? "red" : "transparent")).ofObject()
        ),
        // the normal path Shape
        $(go.Shape, { isPanelMain: true, strokeWidth: 1.5 }),
        $(go.Shape, { toArrow: "OpenTriangle" })
      );

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

      // initialize Palette
      myPalette = new go.Palette("myPaletteDiv", {
        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" },
        ]),
      });

      myPalette.toolManager.draggingTool.doCancel = function () {
        myDiagram.toolManager.draggingTool.doCancel();
        go.DraggingTool.prototype.doCancel.call(this);
      };

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

[Sorry, but I hate how your pretty printer is formatting the code.]

I’m unable to reproduce any odd behavior with your code.
What should I do?

Hmmm, I’ll look into getting a small recording then because not sure if I can explain better than above.

Some of this is to be expected in order to maintain Diagram.contentAlignment, if the vertical size of the document bounds changes. But it seems quite reasonable to me.

It doesn’t happen when the Diagram.documentBounds doesn’t change.

I see what you are saying but wouldn’t that mean reordering would also result in the same behavior or no?

Here is a 2 part video of dragging from the palette that shows the issues, I think the second one shows some bigger shifts. I made the nodes slightly bigger. My nodes in my own diagram are even bigger and I feel like that makes the jumps even bigger.
Drag From Palette Part 1
Drag From Palette Part 2

Here is a clip of the reordering. You can see how nodes don’t move vertically at all.
Drag Reorder Node

Oh, I see – when dragging a Node in from another Diagram, the Node itself occupies space that is included in the Diagram.documentBounds.

Right after you initialize the Diagram, set its “Tool” Layer.isInDocumentBounds to false. For example:

myDiagram.findLayer("Tool").isInDocumentBounds = false;

Hopefully that won’t adversely affect any of the other Tools you might be using.

That did it! Thanks very much Walter