OK, try this now.
<!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",
{
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: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink" },
{ key: 41, text: "Delta", color: "pink" },
{ key: 5, text: "Epsilon", color: "yellow" },
{ key: 6, text: "Zeta", color: "lightblue" },
{ key: 7, text: "Eta", color: "orange" },
{ key: 8, text: "Theta", color: "lightgreen" },
],
[
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 3, to: 4 },
{ from: 3, to: 41 },
{ from: 4, to: 5 },
{ from: 1, to: 6 },
{ from: 6, to: 7 },
{ from: 6, to: 8 },
]);
// 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>