UNPKG

gojs

Version:

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

614 lines (579 loc) 28.9 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"/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title> GoJS Transactions -- Northwoods Software </title> <link rel="stylesheet" href="../assets/css/prism.css" /> </head> <script> window.diagrams = []; window.goCode = function(pre, w, h, parentid, animation) { window.diagrams.push([pre, w, h, parentid, animation]); } </script> <body> <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-40 lg:w-48 text-gray-700 bg-white flex-shrink-0"> <div class="flex-shrink-0 px-8 py-4"> <button id="navButton" class="rounded-lg md:hidden focus:outline-none focus:ring" aria-label="Navigation"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <path id="navOpen" 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="navClosed" 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> <nav id="navList" class="min-h-screen hidden md:block sidebar-nav flex-grow px-1 lg:px-4 pb-4 md:pb-0 md:overflow-y-auto break-words"> <a href="index.html">Basics</a> <a href="buildingObjects.html">Building Parts</a> <a href="usingModels.html">Using Models</a> <a href="dataBinding.html">Data Binding</a> <a href="react.html">GoJS with React</a> <a href="angular.html">GoJS with Angular</a> <a href="textBlocks.html">TextBlocks</a> <a href="shapes.html">Shapes</a> <a href="pictures.html">Pictures</a> <a href="panels.html">Panels</a> <a href="tablePanels.html">Table Panels</a> <a href="brush.html">Brushes</a> <a href="sizing.html">Sizing Objects</a> <a href="itemArrays.html">Item Arrays</a> <a href="changedEvents.html">Changed Events</a> <a href="transactions.html">Transactions</a> <a href="viewport.html">Coordinates</a> <a href="initialView.html">Initial View</a> <a href="collections.html">Collections</a> <a href="links.html">Links</a> <a href="linkLabels.html">Link Labels</a> <a href="connectionPoints.html">Link Points</a> <a href="ports.html">Ports</a> <a href="nodes.html">Nodes</a> <a href="typings.html">Typings</a> <a href="debugging.html">Debugging</a> <a href="layouts.html">Layouts</a> <a href="trees.html">Trees</a> <a href="subtrees.html">SubTrees</a> <a href="groups.html">Groups</a> <a href="subgraphs.html">SubGraphs</a> <a href="sizedGroups.html">Sized Groups</a> <a href="selection.html">Selection</a> <a href="highlighting.html">Highlighting</a> <a href="animation.html">Animation</a> <a href="toolTips.html">ToolTips</a> <a href="contextmenus.html">Context Menus</a> <a href="events.html">Diagram Events</a> <a href="tools.html">Tools</a> <a href="commands.html">Commands</a> <a href="permissions.html">Permissions</a> <a href="validation.html">Validation</a> <a href="HTMLInteraction.html">HTML Interaction</a> <a href="layers.html">Layers &amp; Z-ordering</a> <a href="palette.html">Palette</a> <a href="overview.html">Overview</a> <a href="resizing.html">Resizing Diagrams</a> <a href="replacingDeleting.html">Replacing and Deleting</a> <a href="buttons.html">Buttons</a> <a href="templateMaps.html">Template Maps</a> <a href="legends.html">Legends and Titles</a> <a href="extensions.html">Extensions</a> <a href="geometry.html">Geometry Strings</a> <a href="grids.html">Grid Patterns</a> <a href="graduatedPanels.html">Graduated Panels</a> <a href="makingImages.html">Diagram Images</a> <a href="makingSVG.html">Diagram SVG</a> <a href="printing.html">Printing</a> <a href="serverSideImages.html">Server-side Images</a> <a href="nodeScript.html">GoJS in Node.js</a> <a href="testing.html">Testing</a> <a href="storage.html">Storage</a> <a href="performance.html">Performance</a> <a href="source.html">Building from Source</a> <a href="platforms.html">Platforms</a> <a href="deployment.html">Deployment</a> </nav> </div> <div class="pt-4 px-2 md:px-0 lg:px-4 pb-16 w-full overflow-hidden"> <h1>Transactions and the UndoManager</h1> <p> <b>GoJS</b> models and diagrams make use of an <a>UndoManager</a> that can record all changes and support undoing and redoing those changes. Each state change is recorded in a <a>ChangedEvent</a>, which includes enough information about both before and after to be able to reproduce the state change in either direction, backward (undo) or forward (redo). Such changes are grouped together into <a>Transaction</a>s so that a user action, which may result in many changes, can be undone and redone as a single operation. </p> <p> Not all state changes result in <a>ChangedEvent</a>s that can be recorded by the UndoManager. Some properties are considered transient, such as <a>Diagram.position</a>, <a>Diagram.scale</a>, <a>Diagram.currentTool</a>, <a>Diagram.currentCursor</a>, or <a>Diagram.isModified</a>. Some changes are structural or considered unchanging, such as <a>Diagram.model</a>, any property of <a>CommandHandler</a>, or any of the tool or layout properties. But most <a>GraphObject</a> and model properties do raise a ChangedEvent on the Diagram or Model, respectively, when a property value has been changed. </p> <h2 id="Transactions">Transactions</h2> <p> Whenever you modify a model or its data programmatically in response to some event, you should wrap the code in a transaction. Call <a>Diagram.startTransaction</a> or <a>Model.startTransaction</a>, make the changes, and then call <a>Diagram.commitTransaction</a> or <a>Model.commitTransaction</a>. Although the primary benefit from using transactions is to group together side-effects for undo/redo, you should use transactions even if your application does not support undo/redo by the user. </p> <p> As with database transactions, you will want to perform transactions that are short and infrequent. Do not leave transactions ongoing between user actions. Consider whether it would be better to have a single transaction surrounding a loop instead of starting and finishing a transaction repeatedly within a loop. Do not execute transactions within a property setter -- such granularity is too small. Instead execute a transaction where the properties are set in response to some user action or external event. </p> <p> However, unlike database transactions, you do not need to conduct a transaction in order to access any state. All JavaScript objects are in memory, so you can look at their properties at any time that it would make sense to do so. But when you want to make state changes to a <a>Diagram</a> or a <a>GraphObject</a> or a <a>Model</a> or a JavaScript object in a model, do so within a transaction. </p> <p> The only exception is that transactions are unnecessary when initializing a model or a diagram before assigning the model to the <a>Diagram.model</a> property. (A Diagram only gets access to an UndoManager via the Model, the <a>Model.undoManager</a> property.) </p> <p> Furthermore many event handlers and listeners are already executed within transactions that are conducted by <a>Tool</a>s or <a>CommandHandler</a> commands, so you often will not need to start and commit a transaction within such functions. Read the API documentation for details about whether a function is called within a transaction. For example, setting <a>GraphObject.click</a> to an event handler to respond to a click on an object needs to perform a transaction if it wants to modify the model or the diagram. Most custom click event handlers do not change the diagram but instead update some HTML. </p> <p> But implementing an "ExternalObjectsDropped" <a>DiagramEvent</a> listener, which usually does want to modify the just-dropped Parts in the <a>Diagram.selection</a>, is called within the <a>DraggingTool</a>'s transaction, so no additional start/commit transaction calls are needed. </p> <p> Finally, some customizations, such as the <a>Node.linkValidation</a> predicate, should not modify the diagram or model at all. </p> <p> Both model changes and diagram changes are recorded in the <a>UndoManager</a> only if the model's <a>UndoManager.isEnabled</a> has been set to true. If you do not want the user to be able to perform undo or redo and also prevent the recording of any <a>Transaction</a>s, but you still want to get "Transaction"-type <a>ChangedEvent</a>s because you want to update a database, you can set <a>UndoManager.maxHistoryLength</a> to zero. </p> <p> To better understand the relationships between objects and transactions in memory, look at this diagram: <p> <pre class="lang-js" id="transactionsDiagram" style="display:none"><code> diagram.nodeTemplate = $(go.Node, "Auto", { scale : 1.6, isShadowed: true }, new go.Binding("location", "pos", go.Point.parse), { locationSpot: go.Spot.Center, portId: "NODE" }, $(go.Shape, "RoundedRectangle", { fill: "white", portId: "SHAPE" }, new go.Binding("fill", "color"), new go.Binding("strokeWidth", "strokeW")), $(go.TextBlock, { margin: 4, portId: "TEXTBLOCK" }, new go.Binding("text", "txt")) ); // Represents the nodeDataArray for the two nodes diagram.nodeTemplateMap.add("dataNode", $(go.Node, "Auto", { locationSpot: go.Spot.Center, scale: 1.2, selectionAdorned: true, fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides, isShadowed: true }, new go.Binding("location", "pos", go.Point.parse), new go.Binding("toSpot", "tSpot"), new go.Binding("fromSpot", "fSpot"), $(go.Shape, "Rectangle", { fill: "white" }), $(go.Panel, "Vertical", { defaultStretch: go.GraphObject.Horizontal }, $(go.TextBlock, headerStyle(), // Header: {portId: "HEADER" }, new go.Binding("text", "head")), $(go.Shape, "LineH", { height: 1, stretch: go.GraphObject.Fill }), $(go.TextBlock, textStyle(), // Location: { portId: "ROW1" }, new go.Binding("text", "txt1")), $(go.Shape, "LineH", { height: 1, stretch: go.GraphObject.Fill }), $(go.TextBlock, textStyle(), // Fill: { portId: "ROW2" }, new go.Binding("text", "txt2")) ) ) ); diagram.nodeTemplateMap.add("dataNodeChanged", $(go.Node, "Auto", { locationSpot: go.Spot.Center, scale: 1.2, selectionAdorned: true, fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides, isShadowed: true }, new go.Binding("location", "pos", go.Point.parse), new go.Binding("toSpot", "tSpot"), new go.Binding("fromSpot", "fSpot"), $(go.Shape, "Rectangle", { fill: "white" }), $(go.Panel, "Vertical", { defaultStretch: go.GraphObject.Horizontal }, $(go.TextBlock, headerStyle(), // Header: {portId: "HEADER" }, new go.Binding("text", "head")), $(go.Shape, "LineH", { height: 1, stretch: go.GraphObject.Fill }), $(go.TextBlock, textStyle(), // Location: { portId: "ROW1" }, new go.Binding("text", "txt1")), $(go.Shape, "LineH", { height: 1, stretch: go.GraphObject.Fill }), $(go.TextBlock, textStyle(), // Fill: { portId: "ROW2" }, new go.Binding("text", "txt2")), $(go.Shape, "LineH", { height: 1, stretch: go.GraphObject.Fill }), $(go.TextBlock, textStyle(), // Text: { portId: "ROW3" }, new go.Binding("text", "txt3")), ) ) ); diagram.linkTemplateMap.add("dataNode", // Links from dataNode to Nodes $(go.Link, { routing: go.Link.Orthogonal, corner: 5 }, $(go.Shape, { stroke: "gray", strokeWidth: 2 }), $(go.Shape, { toArrow: "Standard", stroke: "gray", fill: "gray" }) )); diagram.nodeTemplateMap.add("title", $(go.Node, "Auto", new go.Binding("location", "pos", go.Point.parse), $(go.TextBlock, { font: "bold 25pt sans-serif", textAlign: "center"}, new go.Binding("text", "txt")) )); diagram.nodeTemplateMap.add("nodeDataArray", $(go.Node, "Auto", { locationSpot: go.Spot.Center, scale: 1.2, selectionAdorned: true, fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides, shadowColor: "#C5C1AA" }, new go.Binding("location", "pos", go.Point.parse), $(go.Shape, "Rectangle", { fill: "lightgray" }), $(go.Panel, "Vertical", { defaultStretch: go.GraphObject.Horizontal }, $(go.TextBlock, headerStyle(), { portId: "HEADER", text: "nodeDataArray" }), $(go.Shape, "LineH", { height: 1, stretch: go.GraphObject.Fill }), $(go.TextBlock, textStyle(), { portId: "dataNode1", desiredSize: new go.Size(NaN,16) }), $(go.Shape, "LineH", { height: 1, stretch: go.GraphObject.Fill }), $(go.TextBlock, textStyle(), { portId: "dataNode2", desiredSize: new go.Size(NaN,16) }) ) )); diagram.linkTemplateMap.add("Data", $(go.Link, { corner: 10, routing: go.Link.Orthogonal }, new go.Binding("curviness"), $(go.Shape, { stroke: "gray" , strokeWidth: 2 }), $(go.Shape, { toArrow: "Standard", fill: "gray", stroke: "gray", strokeWidth: 2 }), $(go.TextBlock, { font: "bold 12pt Courier", segmentOffset: new go.Point(0, -10) }, new go.Binding("text", "label"), new go.Binding("segmentOffset", "offset")) )); diagram.scale = 0.8; const model = new go.GraphLinksModel(); model.linkFromPortIdProperty = "fPID"; model.linkToPortIdProperty = "tPID" model.nodeDataArray = [ { key: 1, txt: "Diagram", color: "white", pos: "15 305"}, { key: 2, txt: "Model", color: "white", pos: "215 305"}, { key: 3, category: "dataNode", pos: "215 440", head: "Node Data Array", txt1: "nodeDataArray[0]", txt2: "nodeDataArray[1]"}, { key: 4, pos: "215 187", color: "white", txt: "UndoManager"}, { key: 5, pos: "215, 50", category: "dataNode", head: "List of Transactions", txt1: "history[0]", txt2: "history[1]"}, { key: 6, pos: "630, 230", category: "dataNode", head: "List of ChangedEvents", txt1: "changes[0]", txt2: "changes[1]", fSpot: go.Spot.RightSide}, { key: 7, pos: "15, 561", txt: "Alpha", color: "palegreen"}, { key: 8, category: "dataNode", pos: "510 590", head: "Node Data", txt1: "color: \"palegreen\"", txt2: "text: \"Alpha\""}, { key: 9, category: "dataNodeChanged", pos: "630 422", head: "ChangedEvent", txt1: "propertyName: \"color\"", txt2: "newValue: \"palegreen\"", txt3: "oldValue: \"red\""}, { key: 10, category: "dataNodeChanged", pos: "530, 60", head: "Transaction", txt1: "name: \"change color\"", txt2: "isComplete: true", txt3: "changes: . . ."}, ]; model.linkDataArray = [ { from: 1, to: 2, category: "Data", label: ".model", offset: new go.Point(12, 14)}, { from: 2, to: 4, category: "Data", label: ".undoManager", offset: new go.Point(-10, 60)}, { from: 1, to: 4, category: "Data", label: ".undoManager", offset: new go.Point(0, -63)}, { from: 2, tPID: "HEADER", to: 3, category: "Data", label: ".nodeDataArray", offset: new go.Point(0, -72)}, { from: 4, to: 5, category: "Data", label: ".history", offset: new go.Point(0, -45)}, { from: 5, fPID: "ROW1", tPID: "HEADER", to: 10, category: "Data", label: "history[0]", offset: new go.Point(35, 10)}, { from: 1, to: 7, category: "Data", curviness: -70}, { from: 7, tPID: "HEADER", to: 8, category: "Data", curviness: -70, label: ".data", offset: new go.Point(-70, -10)}, { from: 3, tPID: "HEADER", fPID: "ROW1", to: 8, category: "Data", label: "nodeDataArray[0]", offset: new go.Point(-15, -85)}, { from: 6, to: 9, fPID: "ROW2", category: "Data", label: "changes[1]", offset: new go.Point(0, 54)}, { from: 9, to: 8, category: "Data", label: ".object", offset: new go.Point(-10, -13)}, { from: 10, to: 6, fPID: "ROW3", category: "Data", label: ".changes", offset: new go.Point(-10, 13)}, ]; diagram.model = model; // Formatting function headerStyle() { return { margin: 3, font: "bold 12pt sans-serif", minSize: new go.Size(140, 16), maxSize: new go.Size(120, NaN), textAlign: "center" }; } function textStyle() { return { margin: 3, font: "italic 10pt sans-serif", minSize: new go.Size(16, 16), maxSize: new go.Size(160, NaN), textAlign: "left" }; } </code></pre> <script>goCode("transactionsDiagram", 650, 550)</script> <p> A typical case for using transactions is when some command makes a change to the model. </p> <pre class="lang-js" id="transaction"><code> // define a function named "addChild" that is invoked by a button click addChild = () => { const selnode = diagram.selection.first(); if (!(selnode instanceof go.Node)) return; diagram.commit(d => { // have the Model add a new node data const newnode = { key: "N" }; d.model.addNodeData(newnode); // this makes sure the key is unique // and then add a link data connecting the original node with the new one const newlink = { from: selnode.data.key, to: newnode.key }; // add the new link to the model d.model.addLinkData(newlink); }, "add node and link"); }; diagram.nodeTemplate = $(go.Node, "Auto", $(go.Shape, "RoundedRectangle", { fill: "whitesmoke" }), $(go.TextBlock, { margin: 5 }, new go.Binding("text", "key")) ); diagram.layout = $(go.TreeLayout); const nodeDataArray = [ { key: "Alpha" }, { key: "Beta" } ]; const linkDataArray = [ { from: "Alpha", to: "Beta" } ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); diagram.model.undoManager.isEnabled = true; </code></pre> <p> In the following example, select a node and then click the button. The addChild function adds a link connecting the selected node to a new node. When no Node is selected, nothing happens. </p> <button onclick="addChild()">addChild() to selected Node</button> <script>goCode("transaction", 600, 200)</script> <h2 id="SupportUndoManager">Supporting the UndoManager</h2> <p> Changes to JavaScript data properties do not automatically result in any notifications that can be observed. Thus when you want to change the value of a property in a manner that can be undone and redone, you should call <a>Model.setDataProperty</a> (or <a>Model.set</a>, which is an abbreviation for that method). This will get the previous value for the property, set the property to the new value, and call <a>Model.raiseDataChanged</a>, which will also automatically update any target bindings in the Node corresponding to the data. </p> <pre class="lang-js" id="changingData"><code> diagram.nodeTemplate = $(go.Node, "Auto", $(go.Shape, "RoundedRectangle", { fill: "whitesmoke" }), $(go.TextBlock, { margin: 5 }, new go.Binding("text", "someValue")) // bind to the "someValue" data property ); const nodeDataArray = [ { key: "Alpha", someValue: 1 } ]; diagram.model = new go.GraphLinksModel(nodeDataArray); diagram.model.undoManager.isEnabled = true; // define a function named "incrementData" callable by onclick incrementData = () => { diagram.model.commit(m => { const data = m.nodeDataArray[0]; // get the first node data m.set(data, "someValue", data.someValue + 1); }, "increment"); }; </code></pre> <p> Move the node around. Click on the button to increase the value of the "someValue" property on the first node data. Click to focus in the Diagram and then Ctrl-Z and Ctrl-Y to undo and redo the moves and value changes. </p> <button onclick="incrementData()" >incrementData()</button> <script>goCode("changingData", 250, 150)</script> </div> </div> <div class="bg-nwoods-primary"> <section class="max-w-screen-lg text-white container mx-auto py-2 px-12"> <p id="version" class="leading-none mb-2 my-4">GoJS</p> </section> </div><footer class="bg-nwoods-primary text-white"> <div class="container max-w-screen-lg mx-auto px-8"> <div class="w-full py-6"> <div class="max-w-screen-lg xl:max-w-screen-xl mx-auto px-4 sm:px-6 md:px-8"> <ul class="text-sm font-medium pb-6 grid grid-cols-2 sm:grid-cols-3 gap-y-10"> <li class="list-none row-span-2"> <h2 class="text-base font-semibold tracking-wide">GoJS</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a href="../samples/index.html">Samples</a> </li> <li> <a href="../learn/index.html">Learn</a> </li> <li> <a href="../intro/index.html">Intro</a> </li> <li> <a href="../api/index.html">API</a> </li> <li> <a href="../changelog.html">Changelog</a> </li> <li> <a href="https://github.com/NorthwoodsSoftware/GoJS">GitHub</a> </li> </ul> </li> <li class="list-none row-span-2"> <h2 class="text-base font-semibold tracking-wide">Support</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a href="https://www.nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a> </li> <li> <a href="https://forum.nwoods.com/c/gojs">Forum</a> </li> <li> <a href="https://www.nwoods.com/app/activate.aspx?sku=gojs">Activate</a> </li> <li> <a 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> <li> <a href="https://www.youtube.com/channel/UC9We8EoX596-6XFjJDtZIDg">Videos</a> </li> </ul> </li> <li class="list-none row-span-2"> <h2 class="text-base font-semibold tracking-wide">Company</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a target="_blank" href="https://www.nwoods.com">Northwoods</a> </li> <li> <a target="_blank" href="https://www.nwoods.com/about.html">About Us</a> </li> <li> <a target="_blank" href="https://www.nwoods.com/contact.html">Contact Us</a> </li> <li> <a target="_blank" href="https://www.nwoods.com/consulting.html">Consulting</a> </li> <li> <a target="_blank" href="https://twitter.com/northwoodsgo">Twitter</a> </li> </ul> </li> </ul> <p class="text-sm text-gray-100 md:mb-6"> Copyright 1998-2023 <a class="text-white" href="https://www.nwoods.com">Northwoods Software</a> </p> </div> </div> </footer> </body> <script async src="https://www.googletagmanager.com/gtag/js?id=G-S5QK8VSK84"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-S5QK8VSK84'); var getOutboundLink = function(url, label) { gtag('event', 'click', { 'event_category': 'outbound', 'event_label': label, 'transport_type': 'beacon' }); } // topnav var topButton = document.getElementById("topnavButton"); var topnavList = document.getElementById("topnavList"); topButton.addEventListener("click", function() { this.classList.toggle("active"); topnavList.classList.toggle("hidden"); document.getElementById("topnavOpen").classList.toggle("hidden"); document.getElementById("topnavClosed").classList.toggle("hidden"); }); </script> <script src="../assets/js/prism.js"></script> <script src="../release/go.js"></script> <script src="../assets/js/goDoc.js"></script> <script> document.addEventListener("DOMContentLoaded", function() { if (window.go) document.getElementById('version').textContent = "GoJS version " + go.version; if (window.goDoc) window.goDoc(); var d = window.diagrams; for (var i = 0; i < d.length; i++) { var dargs = d[i]; goCodeExecute(dargs[0], dargs[1], dargs[2], dargs[3], dargs[4]); } if (window.extra) window.extra(); }); </script> </html>