UNPKG

26.9 kBHTMLView Raw
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4<meta charset="utf-8"/>
5<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
6
7<link rel="stylesheet" href="../assets/css/style.css"/>
8<!-- Copyright 1998-2021 by Northwoods Software Corporation. --> <title> Extending GoJS -- Northwoods Software </title>
9 <link rel="stylesheet" href="../assets/css/prism.css" />
10 </head>
11 <script>
12
13 window.diagrams = [];
14 window.goCode = function(pre, w, h, parentid, animation) {
15 window.diagrams.push([pre, w, h, parentid, animation]);
16 }
17 </script>
18 <body>
19 <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary">
20 <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">
21 <div class="md:pl-4">
22 <a class="text-white hover:text-white no-underline hover:no-underline
23 font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../">
24 <h1 class="mb-0 p-1 ">GoJS</h1>
25 </a>
26 </div>
27 <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation">
28 <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
29 <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>
30 <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>
31 </svg>
32 </button>
33 <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20">
34 <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0">
35 <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li>
36 <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li>
37 <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li>
38 <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li>
39 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li>
40 <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li>
41 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li>
42 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html"
43 target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li>
44 <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html"
45 target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li>
46 </ul>
47 </div>
48 </div>
49 <hr class="border-b border-gray-600 opacity-50 my-0 py-0" />
50 </nav>
51
52 <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto">
53
54 <div id="navSide" class="flex flex-col w-full md:w-40 lg:w-48 text-gray-700 bg-white flex-shrink-0">
55 <div class="flex-shrink-0 px-8 py-4">
56 <button id="navButton" class="rounded-lg md:hidden focus:outline-none focus:ring" aria-label="Navigation">
57 <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
58 <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>
59 <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>
60 </svg>
61 </button>
62 </div>
63 <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">
64 <a href="index.html">Basics</a>
65 <a href="buildingObjects.html">Building Parts</a>
66 <a href="usingModels.html">Using Models</a>
67 <a href="dataBinding.html">Data Binding</a>
68 <a href="react.html">GoJS with React</a>
69 <a href="angular.html">GoJS with Angular</a>
70 <a href="textBlocks.html">TextBlocks</a>
71 <a href="shapes.html">Shapes</a>
72 <a href="pictures.html">Pictures</a>
73 <a href="panels.html">Panels</a>
74 <a href="tablePanels.html">Table Panels</a>
75 <a href="brush.html">Brushes</a>
76 <a href="sizing.html">Sizing Objects</a>
77 <a href="itemArrays.html">Item Arrays</a>
78 <a href="changedEvents.html">Changed Events</a>
79 <a href="transactions.html">Transactions</a>
80 <a href="viewport.html">Coordinates</a>
81 <a href="initialView.html">Initial View</a>
82 <a href="collections.html">Collections</a>
83 <a href="links.html">Links</a>
84 <a href="linkLabels.html">Link Labels</a>
85 <a href="connectionPoints.html">Link Points</a>
86 <a href="ports.html">Ports</a>
87 <a href="nodes.html">Nodes</a>
88 <a href="debugging.html">Debugging</a>
89 <a href="layouts.html">Layouts</a>
90 <a href="trees.html">Trees</a>
91 <a href="subtrees.html">SubTrees</a>
92 <a href="groups.html">Groups</a>
93 <a href="subgraphs.html">SubGraphs</a>
94 <a href="sizedGroups.html">Sized Groups</a>
95 <a href="selection.html">Selection</a>
96 <a href="highlighting.html">Highlighting</a>
97 <a href="animation.html">Animation</a>
98 <a href="toolTips.html">ToolTips</a>
99 <a href="contextmenus.html">Context Menus</a>
100 <a href="events.html">Diagram Events</a>
101 <a href="tools.html">Tools</a>
102 <a href="commands.html">Commands</a>
103 <a href="permissions.html">Permissions</a>
104 <a href="validation.html">Validation</a>
105 <a href="HTMLInteraction.html">HTML Interaction</a>
106 <a href="layers.html">Layers &amp; Z-ordering</a>
107 <a href="palette.html">Palette</a>
108 <a href="overview.html">Overview</a>
109 <a href="resizing.html">Resizing Diagrams</a>
110 <a href="replacingDeleting.html">Replacing and Deleting</a>
111 <a href="buttons.html">Buttons</a>
112 <a href="templateMaps.html">Template Maps</a>
113 <a href="legends.html">Legends and Titles</a>
114 <a href="extensions.html">Extensions</a>
115 <a href="geometry.html">Geometry Strings</a>
116 <a href="grids.html">Grid Patterns</a>
117 <a href="graduatedPanels.html">Graduated Panels</a>
118 <a href="makingImages.html">Diagram Images</a>
119 <a href="makingSVG.html">Diagram SVG</a>
120 <a href="printing.html">Printing</a>
121 <a href="serverSideImages.html">Server-side Images</a>
122 <a href="nodeScript.html">GoJS in Node.js</a>
123 <a href="testing.html">Testing</a>
124 <a href="storage.html">Storage</a>
125 <a href="performance.html">Performance</a>
126 <a href="source.html">Building from Source</a>
127 <a href="platforms.html">Platforms</a>
128 <a href="deployment.html">Deployment</a>
129 </nav>
130 </div>
131 <div class="pt-4 px-2 md:px-0 lg:px-4 pb-16 w-full overflow-hidden">
132
133
134
135
136<h1>Extending GoJS</h1>
137<p>
138 <b>GoJS</b> can be extended in a variety of ways.
139 The most common way to change the standard behavior is to set properties on the <a>GraphObject</a>, <a>Diagram</a>, <a>CommandHandler</a>, <a>Tool</a>, or <a>Layout</a>.
140 But when no such properties exist, you might need to override methods of CommandHandler, Tool, Layout, Link, or Node.
141 Methods that you can override are documented in the API reference.
142 This page describes how to override methods, either by replacing a method on an instance (a feature of JavaScript) or by defining a subclass.
143 You should not modify the prototypes of any of the <b>GoJS</b> classes.
144</p>
145
146<p class="box bg-danger">
147 Do not modify the prototypes of the <b>GoJS</b> classes.<br />
148 Only use the properties and methods documented in the <a href="../api/index.html">API</a>.
149</p>
150
151<p class="box bg-info">
152 In addition to our samples, <b>GoJS</b> provides an <strong><a href="../extensions/">extensions gallery</a></strong>,
153 showcasing the creation of custom tools and layouts.
154 Those classes and samples have been translated into TypeScript, available at <code>../extensionsTS/</code>.
155 Those extension classes are UMD modules; they use the <code>../release/go.js</code> library.
156 The extension classes are also available as ES6 modules at <code>../extensionsJSM/</code>; these use the <code>../release/go-module.js</code> library.
157 We recommend that you copy the files that you need into your project, so that you can adjust how they refer to the "go.js" library
158 and so that you can include them into your own building and packaging procedures.
159</p>
160
161<p class="box bg-danger">
162 Note that the API for extension classes may change with any version, even point releases.
163 If you intend to use an extension in production, you should copy the code to your own source directory.
164</p>
165
166<p class="box bg-danger">
167 When using modern JavaScript, use arrow functions (<code>e => {}</code>) for event handlers and conversion functions.
168 Use <code>function</code> only when overriding a method and you want access to <code>this</code> object
169 whose method you have overridden.
170</p>
171
172<h2 id="CommandHandler">Command Handler</h2>
173<p>
174 Overriding the <a>CommandHandler</a> allows you to alter default functionality and create your own key bindings.
175 See the <a href="commands.html">intro page on Commands</a> for more.
176 However, the techniques shown below for overriding methods on Tools and Layouts also applies to the CommandHandler.
177</p>
178
179<h2 id="ToolsAndLayouts">Tools and Layouts</h2>
180<p>
181 <b>GoJS</b> operates on nodes and links using many tools and layouts, all of which are subclasses of the <a>Tool</a> and <a>Layout</a> classes.
182 See the <a href="tools.html">intro page on Tools</a> for more about Tools, and the <a href="layouts.html">intro page on Layouts</a> for more about Layouts.
183</p>
184<p>
185 Tools can be modified, or they can be replaced in or added to the <a>Diagram.toolManager</a>.
186 All tools must inherit from the <a>Tool</a> class or from a class that inherits from Tool.
187</p>
188
189<p class="box bg-info">
190 Some of our samples, such as the <a href="../samples/pipes.html">Pipes sample</a>, contain examples of custom tools.
191 More custom tool examples are available in the <a href="../extensions/">extensions gallery</a>.
192 TypeScript versions of those classes and samples are available in <code>../extensionsTS/</code>.
193 Those custom tool classes are also available as ES6 modules in <code>../extensionsJSM/</code>.
194</p>
195
196<p>
197 Layouts can be modified, or they can be used by setting <a>Diagram.layout</a> or <a>Group.layout</a>.
198 All Layouts must inherit from the <a>Layout</a> class or a class that inherits from Layout.
199</p>
200
201<p class="box bg-info">
202 Some of our samples, such as the <a href="../samples/parseTree.html">Parse Tree sample</a>, contain examples of custom layouts.
203 More custom layout examples are available in the <a href="../extensions/">extensions gallery</a>.
204 TypeScript versions of those classes are available in <code>../extensionsTS/</code>.
205 Those custom layout classes are also available as ES6 modules in <code>../extensionsJSM/</code>.
206</p>
207
208<h2 id="OverridingMethodWithoutDefiningSubclass">Overriding a Method Without Defining a Subclass</h2>
209<p>
210 Often we can avoid subclassing a Tool in its entirety and merely override a single method.
211 This is common when we want to make a small change to the behavior of a method.
212 Here we show how to override the <code>ClickSelectingTool.standardMouseSelect</code> method by modifying the tool instance of a particular Diagram.
213</p>
214
215<p>
216 One can override Layout methods in this manner also, but that is rarely done -- layouts are almost always subclassed.
217 It cannot be done for layouts that are the value of <a>Group.layout</a> because those layouts may be copied and cannot be shared.
218</p>
219
220<p>
221 Since we are not creating a new (sub)class, we set the method directly on the Diagram's <a>ClickSelectingTool</a>, which is referenced through its <a>ToolManager</a>.
222 Typical scaffolding for overriding a method in such a manner is as follows:
223</p>
224
225<pre class="lang-js"><code>
226 var tool = diagram.toolManager.clickSelectingTool;
227 tool.standardMouseSelect = function() {
228 // Maybe do something else before
229 // ...
230
231 // Be careful about using 'this' within such functions!
232
233 // In cases where you want normal behavior, call the base functionality.
234 // Note the reference to the prototype
235 // and the call to 'call' passing it what 'this' should be.
236 go.ClickSelectingTool.prototype.standardMouseSelect.call(tool);
237
238 // Maybe do something else after
239 // ...
240 }
241</code></pre>
242
243<p>
244 As a concrete example, we will override <a>Tool.standardMouseSelect</a> to select only Nodes and Links that have a width and height of less than 50 diagram units.
245 This means that we must find the to-be-selected object using <code>diagram.findPartAt</code>, check its bounds, and quit if the bounds are too large.
246 Otherwise, we call the base functionality to select as we normally might.
247</p>
248
249<pre class="lang-js" id="tool"><code>
250 diagram.nodeTemplate =
251 $(go.Node, "Auto",
252 $(go.Shape, "Rectangle",
253 { fill: "white" },
254 new go.Binding("fill", "color")),
255 $(go.TextBlock,
256 { margin: 5 },
257 new go.Binding("text", "key"))
258 );
259
260 var tool = diagram.toolManager.clickSelectingTool;
261 tool.standardMouseSelect = function() {
262 var diagram = tool.diagram;
263 var e = diagram.lastInput;
264 // to select containing Group if Part.canSelect() is false
265 var curobj = diagram.findPartAt(e.documentPoint, false);
266 if (curobj !== null) {
267 var bounds = curobj.actualBounds;
268 // End the selection process if the bounds are greater than 50 width or height
269 if (bounds.width > 50 || bounds.height > 50) {
270 // If this was a left click with no modifier, we want to act the same as if
271 // we are clicking on the Diagram background, so we clear the selection
272 if (e.left && !e.control && !e.shift) {
273 diagram.clearSelection();
274 }
275 // Then return, so that we do not call the base functionality
276 return;
277 }
278 }
279 // otherwise, call the base functionality
280 go.ClickSelectingTool.prototype.standardMouseSelect.call(tool);
281 }
282
283 diagram.model = new go.Model([
284 { key: "Alpha", color: "lightblue" },
285 { key: "Epsilon", color: "thistle" },
286 { key: "Psi", color: "lightcoral" },
287 { key: "Gamma", color: "lightgreen" }
288 ]);
289</code></pre>
290<p>
291 Running this code, we see that the "Epsilon" and "Gamma" nodes are not selectable, because they are both wider than 50.
292 Note that this custom tool does not change the behavior of other tools that might select, such as the <a>DraggingTool</a> or the <a>DragSelectingTool</a>.
293</p>
294<script>goCode("tool", 300, 130)</script>
295
296
297<h2 id="OverridingMethodsBySubclassingLayout">Overriding Methods by Subclassing a Layout</h2>
298<p>
299 Layouts can be subclassed to create custom Layouts that inherit the properties and methods of an existing layout.
300 Subclassing in traditional JavaScript for <b>GoJS</b> requires a few key steps:
301</p>
302
303<ul>
304 <li>Create a new class (function), and call the base class constructor.
305 <li>Call <a>Diagram,inherit</a> with the new class and the base class.
306 <li>Modify the prototype of your derived class to add new functionality.
307</ul>
308
309<p>
310 To create a new Layout, called <code>CascadeLayout</code>, we would start with the following scaffolding:
311</p>
312<pre class="lang-js"><code>
313 function CascadeLayout() {
314 // Note the direct constructor call, no use of "prototype"
315 go.Layout.call(this);
316 // new properties go here, on "this"
317 }
318 go.Diagram.inherit(CascadeLayout, go.Layout);
319
320 // Note setting the method on the prototype
321 CascadeLayout.prototype.doLayout = function(coll) {
322 // Layout logic goes here.
323 // You can reliably use "this" to refer to the layout instance
324 // on which this method was called.
325 }
326</code></pre>
327
328<p>
329 Note that if you are writing in modern JavaScript (ECMAScript 6) or in TypeScript, you can use newer syntax for defining classes:
330</p>
331<pre class="lang-js"><code>
332 export class CascadeLayout extends go.Layout {
333 // new data properties (fields) get declared and initialized here
334
335 constructor() {
336 super();
337 // other initializations can be done here
338 }
339
340 // optionally, define property getters and setters here
341
342 // override or define methods
343 public doLayout(coll) {
344 // Layout logic goes here.
345 }
346 }
347</code></pre>
348
349<p>
350 Layouts commonly need additional properties that act as layout options.
351 To add a "offset" property to <code>CascadeLayout</code>, we will use the convention that an underscore member is private, and will set a default value in the constructor:
352</p>
353<pre class="lang-js"><code>
354 function CascadeLayout() {
355 go.Layout.call(this);
356 this._offset = new go.Size(12, 12);
357 }
358</code></pre>
359
360<p>
361 Then, we use <code>Object.defineProperty</code> to make a "public" getter and setter.
362 Getters and setters allow us to do type checking and have side effects.
363 This setter makes sure the offset value is a <code>go.Size</code> object and invalidates the layout only if the value has changed.
364</p>
365<pre class="lang-js"><code>
366 Object.defineProperty(CascadeLayout.prototype, "offset", {
367 get: function() { return this._offset; },
368 set: function(val) {
369 if (!(val instanceof go.Size)) {
370 throw new Error("new value for CascadeLayout.offset must be a Size, not: " + val);
371 }
372 if (!this._offset.equals(val)) {
373 this._offset = val;
374 this.invalidateLayout();
375 }
376 }
377 });
378</code></pre>
379
380<p>
381 If you are writing in ECMAScript 6 or TypeScript, you can define property getters and setters.
382</p>
383<pre class="lang-js"><code>
384 get offset() { return this._offset; }
385 set offset(val) {
386 if (!(val instanceof go.Size)) {
387 throw new Error("new value for CascadeLayout.offset must be a Size, not: " + val);
388 }
389 if (!this._offset.equals(val)) {
390 this._offset = val;
391 this.invalidateLayout();
392 }
393 }
394</code></pre>
395
396<p>
397 If the layout might be used as the value of <a>Group.layout</a>,
398 you will need to make sure the instance that you set in the Group template can be copied correctly.
399</p>
400<pre class="lang-js"><code>
401 CascadeLayout.prototype.cloneProtected = function(copy) {
402 go.Layout.prototype.cloneProtected.call(this, copy);
403 copy._offset = this._offset;
404 }
405</code></pre>
406
407<p>
408 Lastly we'll define a <a>Layout.doLayout</a>, being sure to look at the documentation and accomodate all possible input, as doLayout has one argument that can either be a <a>Diagram</a>, or a <a>Group</a>, or an <a>Iterable</a> collection.
409</p>
410<p>
411 All together, we can see the cascade layout in action:
412</p>
413<pre class="lang-js" id="example"><code>
414 /**
415 * @constructor
416 * @extends Layout
417 * @class
418 * This layout arranges nodes in a cascade specified by the offset property
419 */
420 function CascadeLayout() {
421 go.Layout.call(this);
422 this._offset = new go.Size(12, 12);
423 }
424 go.Diagram.inherit(CascadeLayout, go.Layout);
425
426 CascadeLayout.prototype.cloneProtected = function(copy) {
427 go.Layout.prototype.cloneProtected.call(this, copy);
428 copy._offset = this._offset;
429 }
430
431 Object.defineProperty(CascadeLayout.prototype, "offset", {
432 get: function() { return this._offset; },
433 set: function(val) {
434 if (!(val instanceof go.Size)) {
435 throw new Error("new value for CascadeLayout.offset must be a Size, not: " + val);
436 }
437 if (!this._offset.equals(val)) {
438 this._offset = val;
439 this.invalidateLayout();
440 }
441 }
442 });
443
444 /**
445 * This method positions all Nodes and ignores all Links.
446 * @this {CascadeLayout}
447 * @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
448 */
449 CascadeLayout.prototype.doLayout = function(coll) {
450 // get the Nodes and Links to be laid out
451 var parts = this.collectParts(coll);
452
453 // Start the layout at the arrangement origin, a property inherited from Layout
454 var x = this.arrangementOrigin.x;
455 var y = this.arrangementOrigin.y;
456 var offset = this.offset;
457
458 var it = parts.iterator;
459 while (it.next()) {
460 var node = it.value;
461 if (!(node instanceof go.Node)) continue; // ignore Links
462 node.move(new go.Point(x, y));
463 x += offset.width;
464 y += offset.height;
465 }
466 }
467 // end of CascadeLayout
468
469 // Regular diagram setup:
470
471 diagram.layout = new CascadeLayout();
472
473 diagram.nodeTemplate =
474 $(go.Node, "Auto",
475 $(go.Shape, "Rectangle",
476 { fill: "white" },
477 new go.Binding("fill", "color")),
478 $(go.TextBlock,
479 { margin: 5 },
480 new go.Binding("text", "key"))
481 );
482
483 diagram.model = new go.Model([
484 { key: "Alpha", color: "lightblue" },
485 { key: "Beta", color: "thistle" },
486 { key: "Delta", color: "lightcoral" },
487 { key: "Gamma", color: "lightgreen" }
488 ]);
489</code></pre>
490<script>goCode("example", 300, 200)</script>
491
492 </div>
493 </div>
494
495 <div class="bg-nwoods-primary">
496 <section class="max-w-screen-lg text-white container mx-auto py-2 px-12">
497 <p id="version" class="leading-none mb-2 my-4">GoJS</p>
498 </section>
499 </div><footer class="bg-nwoods-primary text-white">
500 <div class="container max-w-screen-lg mx-auto px-8">
501 <div class="w-full py-6">
502
503 <div class="max-w-screen-lg xl:max-w-screen-xl mx-auto px-4 sm:px-6 md:px-8">
504 <ul class="text-sm font-medium pb-14 sm:pb-20 grid grid-cols-1 sm:grid-cols-3 gap-y-10">
505 <li class="list-none row-span-2">
506 <h2 class="text-base font-semibold tracking-wide">GoJS</h2>
507 <ul class="list-none space-y-4 md:space-y-1 px-0">
508 <li>
509 <a href="../samples/index.html">Samples</a>
510 </li>
511 <li>
512 <a href="../learn/index.html">Learn</a>
513 </li>
514 <li>
515 <a href="../intro/index.html">Intro</a>
516 </li>
517 <li>
518 <a href="../api/index.html">API</a>
519 </li>
520 <li>
521 <a href="../changelog.html">Changelog</a>
522 </li>
523 <li>
524 <a href="https://github.com/NorthwoodsSoftware/GoJS">GitHub</a>
525 </li>
526 </ul>
527 </li>
528 <li class="list-none row-span-2">
529 <h2 class="text-base font-semibold tracking-wide">Support</h2>
530 <ul class="list-none space-y-4 md:space-y-1 px-0">
531 <li>
532 <a href="https://www.nwoods.com/contact.html"
533 target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a>
534 </li>
535 <li>
536 <a href="https://forum.nwoods.com/c/gojs">Forum</a>
537 </li>
538 <li>
539 <a href="https://www.nwoods.com/app/activate.aspx?sku=gojs">Activate</a>
540 </li>
541 <li>
542 <a href="https://www.nwoods.com/sales/index.html"
543 target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a>
544 </li>
545 <li>
546 <a href="https://www.youtube.com/channel/UC9We8EoX596-6XFjJDtZIDg">Videos</a>
547 </li>
548 </ul>
549 </li>
550 <li class="list-none row-span-2">
551 <h2 class="text-base font-semibold tracking-wide">Company</h2>
552 <ul class="list-none space-y-4 md:space-y-1 px-0">
553 <li>
554 <a href="https://www.nwoods.com">Northwoods</a>
555 </li>
556 <li>
557 <a href="https://www.nwoods.com/about.html">About Us</a>
558 </li>
559 <li>
560 <a href="https://www.nwoods.com/contact.html">Contact Us</a>
561 </li>
562 <li>
563 <a href="https://twitter.com/northwoodsgo">Twitter</a>
564 </li>
565
566 </ul>
567 </li>
568 </ul>
569
570
571 <p class="text-sm text-gray-100 md:mb-6">
572 Copyright 1998-2021 <a class="text-white" href="https://www.nwoods.com">Northwoods Software</a>
573 </p>
574 </div>
575 </div>
576</footer> </body>
577
578<script async src="https://www.googletagmanager.com/gtag/js?id=UA-1506307-5"></script>
579<script>
580 window.dataLayer = window.dataLayer || [];
581 function gtag(){dataLayer.push(arguments);}
582 gtag('js', new Date()); gtag('config', 'UA-1506307-5');
583 var getOutboundLink = function(url, label) {
584 gtag('event', 'click', {
585 'event_category': 'outbound',
586 'event_label': label,
587 'transport_type': 'beacon'
588 });
589 }
590
591 // topnav
592 var topButton = document.getElementById("topnavButton");
593 var topnavList = document.getElementById("topnavList");
594 topButton.addEventListener("click", function() {
595 this.classList.toggle("active");
596 topnavList.classList.toggle("hidden");
597 document.getElementById("topnavOpen").classList.toggle("hidden");
598 document.getElementById("topnavClosed").classList.toggle("hidden");
599 });
600</script>
601 <script src="../assets/js/prism.js"></script>
602 <script src="../release/go.js"></script>
603 <script src="../assets/js/goDoc.js"></script>
604 <script>
605 document.addEventListener("DOMContentLoaded", function() {
606 if (window.go) document.getElementById('version').textContent = "GoJS version " + go.version;
607 if (window.goDoc) window.goDoc();
608 var d = window.diagrams;
609 for (var i = 0; i < d.length; i++) {
610 var dargs = d[i];
611 goCodeExecute(dargs[0], dargs[1], dargs[2], dargs[3], dargs[4]);
612 }
613 if (window.extra) window.extra();
614 });
615 </script>
616</html>