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>