如何在GoJS中指定位置添加动态端口?
GoJS动态端口自定义位置实现方案
问题说明
现有GoJS代码可在节点左侧添加动态端口,但无法通过绑定alignment属性实现端口的特定位置控制,需调整代码实现自定义定位需求。
实现步骤
1. 调整端口容器面板类型
将左侧端口的Vertical面板改为Position面板,让每个端口可基于坐标独立定位,而非固定垂直排列。
2. 给端口数据添加位置属性
在创建新端口时,为端口数据添加pos坐标属性,后续可通过修改该属性直接调整端口位置。
3. 绑定端口的Position属性
在端口的itemTemplate中,将端口面板的position属性与端口数据的pos字段绑定,实现数据驱动的位置控制。
4. 优化布局基准
设置端口容器的alignment为go.Spot.Center,确保定位基准与节点主体垂直居中对齐。
修改后的完整代码
<!DOCTYPE html> <html lang="en"> <body> <script src="https://unpkg.com/gojs@2.3.7/release/go.js"></script> <div id="allSampleContent" class="p-4 w-full"> <script id="code"> function init() { const $ = go.GraphObject.make; myDiagram = new go.Diagram("myDiagramDiv", { "undoManager.isEnabled": true }); myDiagram.addDiagramListener("Modified", e => { const button = document.getElementById("SaveButton"); if (button) button.disabled = !myDiagram.isModified; const idx = document.title.indexOf("*"); if (myDiagram.isModified) { if (idx < 0) document.title += "*"; } else { if (idx >= 0) document.title = document.title.slice(0, idx); } }); function makeButton(text, action, visiblePredicate) { return $("ContextMenuButton", $(go.TextBlock, text), { click: action }, visiblePredicate ? new go.Binding("visible", "", (o, e) => o.diagram ? visiblePredicate(o, e) : false).ofObject() : {}); } const nodeMenu = $("ContextMenu", makeButton("Add left port", (e, obj) => addPort("left")), ); const portSize = new go.Size(8, 8); const portMenu = $("ContextMenu", makeButton("Change color", (e, obj) => changeColor(obj.part.adornedObject)), makeButton("Remove side ports", (e, obj) => removeAll(obj.part.adornedObject))); myDiagram.nodeTemplate = $(go.Node, "Table", { locationObjectName: "BODY", locationSpot: go.Spot.Center, selectionObjectName: "BODY", contextMenu: nodeMenu }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Panel, "Auto", { row: 1, column: 1, name: "BODY", stretch: go.GraphObject.Fill }, $(go.Shape, "Rectangle", { fill: "#dbf6cb", stroke: null, strokeWidth: 0, minSize: new go.Size(60, 60) }), $(go.TextBlock, { margin: 10, textAlign: "center", font: "bold 14px Segoe UI,sans-serif", stroke: "#484848", editable: true }, new go.Binding("text", "name").makeTwoWay())), // 修改:将Vertical面板改为Position面板,支持自由定位 $(go.Panel, "Position", new go.Binding("itemArray", "leftArray"), { row: 1, column: 0, alignment: go.Spot.Center }, $(go.Panel, { _side: "left", fromSpot: go.Spot.Left, toSpot: go.Spot.Left, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), // 修改:绑定position属性到端口数据的pos字段 new go.Binding("position", "pos", go.Point.parse), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(0) }, new go.Binding("fill", "portColor")))), ); myDiagram.linkTemplate = $(CustomLink, { routing: go.Link.AvoidsNodes, corner: 4, curve: go.Link.JumpGap, reshapable: true, resegmentable: true, relinkableFrom: true, relinkableTo: true }, new go.Binding("points").makeTwoWay(), $(go.Shape, { stroke: "#2F4F4F", strokeWidth: 2 })); myDiagram.toolManager.clickCreatingTool.archetypeNodeData = { name: "Unit", leftArray: [], rightArray: [], topArray: [], bottomArray: [] }; myDiagram.contextMenu = $("ContextMenu", makeButton("Paste", (e, obj) => e.diagram.commandHandler.pasteSelection(e.diagram.toolManager.contextMenuTool.mouseDownPoint), o => o.diagram.commandHandler.canPasteSelection(o.diagram.toolManager.contextMenuTool.mouseDownPoint)), makeButton("Undo", (e, obj) => e.diagram.commandHandler.undo(), o => o.diagram.commandHandler.canUndo()), makeButton("Redo", (e, obj) => e.diagram.commandHandler.redo(), o => o.diagram.commandHandler.canRedo())); load(); } class CustomLink extends go.Link { findSidePortIndexAndCount(node, port) { const nodedata = node.data; if (nodedata !== null) { const portdata = port.data; const side = port._side; const arr = nodedata[side + "Array"]; const len = arr.length; for (let i = 0; i < len; i++) { if (arr[i] === portdata) return [i, len]; } } return [-1, len]; } computeEndSegmentLength(node, port, spot, from) { const esl = super.computeEndSegmentLength(node, port, spot, from); const other = this.getOtherPort(port); if (port !== null && other !== null) { const thispt = port.getDocumentPoint(this.computeSpot(from)); const otherpt = other.getDocumentPoint(this.computeSpot(!from)); if (Math.abs(thispt.x - otherpt.x) > 20 || Math.abs(thispt.y - otherpt.y) > 20) { const info = this.findSidePortIndexAndCount(node, port); const idx = info[0]; const count = info[1]; if (port._side == "top" || port._side == "bottom") { if (otherpt.x < thispt.x) { return esl + 4 + idx * 8; } else { return esl + (count - idx - 1) * 8; } } else { // left or right if (otherpt.y < thispt.y) { return esl + 4 + idx * 8; } else { return esl + (count - idx - 1) * 8; } } } } return esl; } hasCurviness() { if (isNaN(this.curviness)) return true; return super.hasCurviness(); } computeCurviness() { if (isNaN(this.curviness)) { const fromnode = this.fromNode; const fromport = this.fromPort; const fromspot = this.computeSpot(true); const frompt = fromport.getDocumentPoint(fromspot); const tonode = this.toNode; const toport = this.toPort; const tospot = this.computeSpot(false); const topt = toport.getDocumentPoint(tospot); if (Math.abs(frompt.x - topt.x) > 20 || Math.abs(frompt.y - topt.y) > 20) { if ((fromspot.equals(go.Spot.Left) || fromspot.equals(go.Spot.Right)) && (tospot.equals(go.Spot.Left) || tospot.equals(go.Spot.Right))) { const fromseglen = this.computeEndSegmentLength(fromnode, fromport, fromspot, true); const toseglen = this.computeEndSegmentLength(tonode, toport, tospot, false); const c = (fromseglen - toseglen) / 2; if (frompt.x + fromseglen >= topt.x - toseglen) { if (frompt.y < topt.y) return c; if (frompt.y > topt.y) return -c; } } else if ((fromspot.equals(go.Spot.Top) || fromspot.equals(go.Spot.Bottom)) && (tospot.equals(go.Spot.Top) || tospot.equals(go.Spot.Bottom))) { const fromseglen = this.computeEndSegmentLength(fromnode, fromport, fromspot, true); const toseglen = this.computeEndSegmentLength(tonode, toport, tospot, false); const c = (fromseglen - toseglen) / 2; if (frompt.x + fromseglen >= topt.x - toseglen) { if (frompt.y < topt.y) return c; if (frompt.y > topt.y) return -c; } } } } return super.computeCurviness(); } } function addPort(side) { myDiagram.startTransaction("addPort"); myDiagram.selection.each(node => { if (!(node instanceof go.Node)) return; let i = 0; while (node.findPort(side + i.toString()) !== node) i++; const name = side + i.toString(); const arr = node.data[side + "Array"]; if (arr) { // 修改:新增pos属性,示例循环使用上/中/下三个位置 const yPos = [-20, 0, 20][i % 3]; const newportdata = { portId: name, portColor: getPortColor(), pos: `0 ${yPos}` }; myDiagram.model.insertArrayItem(arr, -1, newportdata); } }); myDiagram.commitTransaction("addPort"); } function removeAll(port) { myDiagram.startTransaction("removePorts"); const nodedata = port.part.data; const side = port._side; myDiagram.model.setDataProperty(nodedata, side + "Array", []); myDiagram.commitTransaction("removePorts"); } function changeColor(port) { myDiagram.startTransaction("colorPort"); const data = port.data; myDiagram.model.setDataProperty(data, "portColor", getPortColor()); myDiagram.commitTransaction("colorPort"); } function getPortColor() { const portColors = ["#fae3d7", "#d6effc", "#ebe3fc", "#eaeef8", "#fadfe5", "#6cafdb", "#66d6d1"] return portColors[Math.floor(Math.random() * portColors.length)]; } function save() { document.getElementById("mySavedModel").value = myDiagram.model.toJson(); myDiagram.isModified = false; } function load() { myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value); } window.addEventListener('DOMContentLoaded', init); </script> <div id="sample"> <div id="myDiagramDiv" style="width: 600px; height: 500px; border: 1px solid black; position: relative; -webkit-tap-highlight-color: rgba(255, 255, 255, 0); cursor: auto;"> <canvas tabindex="0" width="898" height="748" style="position: absolute; top: 0px; left: 0px; z-index: 2; user-select: none; touch-action: none; letter-spacing: normal; word-spacing: normal; width: 599px; height: 499px; cursor: auto;">This text is displayed if your browser does not support the Canvas HTML element.</canvas> <div style="position: absolute; overflow: auto; width: 599px; height: 499px; z-index: 1;"> <div style="position: absolute; width: 1px; height: 1px;"></div> </div> </div> Add port to selected nodes: <button onclick="addPort('left')">Left</button> <div> <div> <button id="SaveButton" onclick="save()" disabled="">Save</button> <button onclick="load()">Load</button> Diagram Model saved in JSON format: </div> <textarea id="mySavedModel" style="width:100%;height:250px"> { "class": "GraphLinksModel", "copiesArrays": true, "copiesArrayObjects": true, "linkFromPortIdProperty": "fromPort", "linkToPortIdProperty": "toPort", "nodeDataArray": [{"key":1,"name":"Unit One","loc":"101 204","leftArray":[{"portColor":"#fae3d7","portId":"left0","pos":"0 0"}],"topArray":[],"bottomArray":[],"rightArray":[]}], "linkDataArray": []} </textarea> </div> </div> </body> </html>
关键修改说明
- 端口容器从
Vertical改为Position,解除固定排列限制 - 新增端口数据的
pos坐标属性,示例中循环分配上/中/下三个位置 - 绑定端口面板的
position到pos字段,实现数据驱动定位 - 设置端口容器对齐方式为居中,确保定位基准与节点主体匹配
内容的提问来源于stack exchange,提问作者Prem
相关产品推荐
相关产品推荐

