UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

567 lines (530 loc) 25.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/> <meta name="description" content="Nodes with varying lists of ports on each of four sides."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title>Dynamic Ports</title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary"> <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2"> <div class="md:pl-4"> <a class="text-white hover:text-white no-underline hover:no-underline font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../"> <h1 class="my-0 p-1 ">GoJS</h1> </a> </div> <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path> <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20"> <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0"> <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li> </ul> </div> </div> <hr class="border-b border-gray-600 opacity-50 my-0 py-0" /> </nav> <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="../release/go.js"></script> <div id="allSampleContent" class="p-4 w-full"> <script id="code"> function init() { // Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make // For details, see https://gojs.net/latest/intro/buildingObjects.html const $ = go.GraphObject.make; //for conciseness in defining node templates myDiagram = new go.Diagram("myDiagramDiv", //Diagram refers to its DIV HTML element by id { "undoManager.isEnabled": true }); // when the document is modified, add a "*" to the title and enable the "Save" button 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); } }); // To simplify this code we define a function for creating a context menu button: function makeButton(text, action, visiblePredicate) { return $("ContextMenuButton", $(go.TextBlock, text), { click: action }, // don't bother with binding GraphObject.visible if there's no predicate visiblePredicate ? new go.Binding("visible", "", (o, e) => o.diagram ? visiblePredicate(o, e) : false).ofObject() : {}); } const nodeMenu = // context menu for each Node $("ContextMenu", makeButton("Copy", (e, obj) => e.diagram.commandHandler.copySelection()), makeButton("Delete", (e, obj) => e.diagram.commandHandler.deleteSelection()), $(go.Shape, "LineH", { strokeWidth: 2, height: 1, stretch: go.GraphObject.Horizontal }), makeButton("Add top port", (e, obj) => addPort("top")), makeButton("Add left port", (e, obj) => addPort("left")), makeButton("Add right port", (e, obj) => addPort("right")), makeButton("Add bottom port", (e, obj) => addPort("bottom")) ); const portSize = new go.Size(8, 8); const portMenu = // context menu for each port $("ContextMenu", makeButton("Swap order", (e, obj) => swapOrder(obj.part.adornedObject)), makeButton("Remove port", // in the click event handler, the obj.part is the Adornment; // its adornedObject is the port (e, obj) => removePort(obj.part.adornedObject)), makeButton("Change color", (e, obj) => changeColor(obj.part.adornedObject)), makeButton("Remove side ports", (e, obj) => removeAll(obj.part.adornedObject)) ); // the node template // includes a panel on each side with an itemArray of panels containing ports 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), // the body $(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()) ), // end Auto Panel body // the Panel holding the left port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.leftArray $(go.Panel, "Vertical", new go.Binding("itemArray", "leftArray"), { row: 1, column: 0, itemTemplate: $(go.Panel, { _side: "left", // internal property to make it easier to tell which side it's on fromSpot: go.Spot.Left, toSpot: go.Spot.Left, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(1, 0) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ), // end Vertical Panel // the Panel holding the top port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.topArray $(go.Panel, "Horizontal", new go.Binding("itemArray", "topArray"), { row: 0, column: 1, itemTemplate: $(go.Panel, { _side: "top", fromSpot: go.Spot.Top, toSpot: go.Spot.Top, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(0, 1) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ), // end Horizontal Panel // the Panel holding the right port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.rightArray $(go.Panel, "Vertical", new go.Binding("itemArray", "rightArray"), { row: 1, column: 2, itemTemplate: $(go.Panel, { _side: "right", fromSpot: go.Spot.Right, toSpot: go.Spot.Right, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(1, 0) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ), // end Vertical Panel // the Panel holding the bottom port elements, which are themselves Panels, // created for each item in the itemArray, bound to data.bottomArray $(go.Panel, "Horizontal", new go.Binding("itemArray", "bottomArray"), { row: 2, column: 1, itemTemplate: $(go.Panel, { _side: "bottom", fromSpot: go.Spot.Bottom, toSpot: go.Spot.Bottom, fromLinkable: true, toLinkable: true, cursor: "pointer", contextMenu: portMenu }, new go.Binding("portId", "portId"), $(go.Shape, "Rectangle", { stroke: null, strokeWidth: 0, desiredSize: portSize, margin: new go.Margin(0, 1) }, new go.Binding("fill", "portColor")) ) // end itemTemplate } ) // end Horizontal Panel ); // end Node // an orthogonal link template, reshapable and relinkable myDiagram.linkTemplate = $(CustomLink, // defined below { 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 }) ); // support double-clicking in the background to add a copy of this data as a node 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 the diagram from JSON data load(); } // This custom-routing Link class tries to separate parallel links from each other. // This assumes that ports are lined up in a row/column on a side of the node. 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(); } } // end CustomLink class // Add a port to the specified side of the selected nodes. function addPort(side) { myDiagram.startTransaction("addPort"); myDiagram.selection.each(node => { // skip any selected Links if (!(node instanceof go.Node)) return; // compute the next available index number for the side let i = 0; while (node.findPort(side + i.toString()) !== node) i++; // now this new port name is unique within the whole Node because of the side prefix const name = side + i.toString(); // get the Array of port data to be modified const arr = node.data[side + "Array"]; if (arr) { // create a new port data object const newportdata = { portId: name, portColor: getPortColor() }; // and add it to the Array of port data myDiagram.model.insertArrayItem(arr, -1, newportdata); } }); myDiagram.commitTransaction("addPort"); } // Exchange the position/order of the given port with the next one. // If it's the last one, swap with the previous one. function swapOrder(port) { const arr = port.panel.itemArray; if (arr.length >= 2) { // only if there are at least two ports! for (let i = 0; i < arr.length; i++) { if (arr[i].portId === port.portId) { myDiagram.startTransaction("swap ports"); if (i >= arr.length - 1) i--; // now can swap I and I+1, even if it's the last port const newarr = arr.slice(0); // copy Array newarr[i] = arr[i + 1]; // swap items newarr[i + 1] = arr[i]; // remember the new Array in the model myDiagram.model.setDataProperty(port.part.data, port._side + "Array", newarr); port.part.findLinksConnected(newarr[i].portId).each(l => l.invalidateRoute()); port.part.findLinksConnected(newarr[i+1].portId).each(l => l.invalidateRoute()); myDiagram.commitTransaction("swap ports"); break; } } } } // Remove the clicked port from the node. // Links to the port will be redrawn to the node's shape. function removePort(port) { myDiagram.startTransaction("removePort"); const pid = port.portId; const arr = port.panel.itemArray; for (let i = 0; i < arr.length; i++) { if (arr[i].portId === pid) { myDiagram.model.removeArrayItem(arr, i); break; } } myDiagram.commitTransaction("removePort"); } // Remove all ports from the same side of the node as the clicked port. function removeAll(port) { myDiagram.startTransaction("removePorts"); const nodedata = port.part.data; const side = port._side; // there are four property names, all ending in "Array" myDiagram.model.setDataProperty(nodedata, side + "Array", []); // an empty Array myDiagram.commitTransaction("removePorts"); } // Change the color of the clicked port. function changeColor(port) { myDiagram.startTransaction("colorPort"); const data = port.data; myDiagram.model.setDataProperty(data, "portColor", getPortColor()); myDiagram.commitTransaction("colorPort"); } // Use some pastel colors for ports function getPortColor() { const portColors = ["#fae3d7", "#d6effc", "#ebe3fc", "#eaeef8", "#fadfe5", "#6cafdb", "#66d6d1"] return portColors[Math.floor(Math.random() * portColors.length)]; } // Save the model to / load it from JSON text shown on the page itself, not in a database. function save() { document.getElementById("mySavedModel").value = myDiagram.model.toJson(); myDiagram.isModified = false; } function load() { myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value); // When copying a node, we need to copy the data that the node is bound to. // This JavaScript object includes properties for the node as a whole, and // four properties that are Arrays holding data for each port. // Those arrays and port data objects need to be copied too. // Thus Model.copiesArrays and Model.copiesArrayObjects both need to be true. // Link data includes the names of the to- and from- ports; // so the GraphLinksModel needs to set these property names: // linkFromPortIdProperty and linkToPortIdProperty. } window.addEventListener('DOMContentLoaded', init); </script> <div id="sample"> <div id="myDiagramDiv" style="width:600px; height:500px; border:1px solid black"></div> Add port to selected nodes: <button onclick="addPort('top')">Top</button> <button onclick="addPort('bottom')">Bottom</button> <button onclick="addPort('left')">Left</button> <button onclick="addPort('right')">Right</button> <p> Double-click in the diagram background in order to add a new node there. In this sample you can add ports to a selected node by clicking the above buttons or by using the context menu. Draw links between ports by dragging between ports. If you select a link you can relink or reshape it. Right-click or touch-hold on a port to bring up a context menu that allows you to remove it or change its color. </p> <p> The diagram also uses a custom link to allow for special routing to help parallel links avoid each other using overridden <a>Link.computeEndSegmentLength</a>, <a>Link.hasCurviness</a>, and <a>Link.computeCurviness</a> functions. </p> <p> See the <a href="../intro/ports.html">Ports Intro page</a> for an explanation of GoJS ports. </p> <div> <div> <button id="SaveButton" onclick="save()">Save</button> <button onclick="load()">Load</button> Diagram Model saved in JSON format: </div> <textarea id="mySavedModel" style="width:100%;height:250px"> { "class": "go.GraphLinksModel", "copiesArrays": true, "copiesArrayObjects": true, "linkFromPortIdProperty": "fromPort", "linkToPortIdProperty": "toPort", "nodeDataArray": [ {"key":1, "name":"Unit One", "loc":"101 204", "leftArray":[ {"portColor":"#fae3d7", "portId":"left0"} ], "topArray":[ {"portColor":"#d6effc", "portId":"top0"} ], "bottomArray":[ {"portColor":"#ebe3fc", "portId":"bottom0"} ], "rightArray":[ {"portColor":"#eaeef8", "portId":"right0"},{"portColor":"#fadfe5", "portId":"right1"} ] }, {"key":2, "name":"Unit Two", "loc":"320 152", "leftArray":[ {"portColor":"#6cafdb", "portId":"left0"},{"portColor":"#66d6d1", "portId":"left1"},{"portColor":"#fae3d7", "portId":"left2"} ], "topArray":[ {"portColor":"#d6effc", "portId":"top0"} ], "bottomArray":[ {"portColor":"#eaeef8", "portId":"bottom0"},{"portColor":"#eaeef8", "portId":"bottom1"},{"portColor":"#6cafdb", "portId":"bottom2"} ], "rightArray":[ ] }, {"key":3, "name":"Unit Three", "loc":"384 319", "leftArray":[ {"portColor":"#66d6d1", "portId":"left0"},{"portColor":"#fadfe5", "portId":"left1"},{"portColor":"#6cafdb", "portId":"left2"} ], "topArray":[ {"portColor":"#66d6d1", "portId":"top0"} ], "bottomArray":[ {"portColor":"#6cafdb", "portId":"bottom0"} ], "rightArray":[ ] }, {"key":4, "name":"Unit Four", "loc":"138 351", "leftArray":[ {"portColor":"#fae3d7", "portId":"left0"} ], "topArray":[ {"portColor":"#6cafdb", "portId":"top0"} ], "bottomArray":[ {"portColor":"#6cafdb", "portId":"bottom0"} ], "rightArray":[ {"portColor":"#6cafdb", "portId":"right0"},{"portColor":"#66d6d1", "portId":"right1"} ] } ], "linkDataArray": [ {"from":4, "to":2, "fromPort":"top0", "toPort":"bottom0"}, {"from":4, "to":2, "fromPort":"top0", "toPort":"bottom0"}, {"from":3, "to":2, "fromPort":"top0", "toPort":"bottom1"}, {"from":4, "to":3, "fromPort":"right0", "toPort":"left0"}, {"from":4, "to":3, "fromPort":"right1", "toPort":"left2"}, {"from":1, "to":2, "fromPort":"right0", "toPort":"left1"}, {"from":1, "to":2, "fromPort":"right1", "toPort":"left2"} ]} </textarea> </div> </div> </div> <!-- * * * * * * * * * * * * * --> <!-- End of GoJS sample code --> </div> </body> <!-- This script is part of the gojs.net website, and is not needed to run the sample --> <script src="../assets/js/goSamples.js"></script> </html>