feat: 后端全量更新 - 含所有本次需求

- Contract.php: 返回合约账户余额(balance_contract)
- My.php: 地址管理增加BTC/ETH
- AppContract.php: 一键平仓(closeall)
- AppProxy.php: 代理专属注册链接 + 分级权限(L1/L2)
- site.php: 手续费减半(0.018→0.009)
- agent_permission_setup.sql: 代理权限SQL
- crypto_news_crawler.py: 新闻自动采集脚本
This commit is contained in:
li
2026-03-30 20:16:32 +08:00
commit 1b24994e74
6721 changed files with 1308571 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html>
<head>
<title>Arranging Layout of the Class Hierarchy</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Arrange disconnected circular subgraphs in a circle and put disconnected nodes in a grid underneath." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="ArrangingLayout.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
initialAutoScale: go.Diagram.Uniform,
layout:
$(ArrangingLayout,
{ // create a circular arrangement of circular layouts
primaryLayout: $(go.CircularLayout), // must specify the primaryLayout
arrangingLayout: $(go.CircularLayout, { nodeDiameterFormula: go.CircularLayout.Circular, spacing: 30 }),
// Uncommenting this filter will force all of the nodes and links to go into the main subset and thus
// will cause all those nodes to be arranged by this.arrangingLayout, here a CircularLayout,
// rather than by the this.sideLayout, which by default is a GridLayout.
//filter: function(part) { return true; },
// additional custom properties for use by preparePrimaryLayout
_colors: ["red", "orange", "yellow", "lime", "cyan"], // possible node colors
_colorIndex: 0, // cycle through the given colors
// called for each separate connected subgraph
preparePrimaryLayout: function(lay, coll) { // color all of the nodes in each subgraph
var root = null; // find the root node in this subgraph
coll.each(function(node) {
if (node instanceof go.Node && node.findLinksInto().count === 0) root = node;
});
var color = "white"; // determine the color for the nodes in this subgraph
if (root !== null) {
// root.key will be the name of the class that this node represents
// Special case: "LayoutNetwork", "LayoutVertex", and "LayoutEdge" classes are "violet"
if (root.key.indexOf("Layout") === 0 && root.key.length > "Layout".length) {
color = "violet";
} else { // otherwise cycle through the Array of colors
color = this._colors[this._colorIndex++ % this._colors.length];
}
}
coll.each(function(node) { // assign the fill color for all of the nodes in the subgraph
if (node instanceof go.Node) {
var shape = node.findObject("SHAPE");
if (shape !== null) shape.fill = color;
}
});
},
prepareSideLayout: function(lay, coll, b) { // called once for the sideLayout
// adjust how wide the GridLayout lays out
lay.wrappingWidth = Math.max(b.width, this.diagram.viewportBounds.width);
}
})
});
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
$(go.Shape, { name: "SHAPE", figure: "RoundedRectangle", fill: "lightgray" },
new go.Binding("fill", "color")),
$(go.TextBlock, { margin: 2, textAlign: "center" },
new go.Binding("text", "key", function(s) {
// insert newlines between lowercase followed by uppercase characters
var arr = s.split("");
for (var i = 1; i < arr.length-1; i++) {
var a = arr[i-1];
var b = arr[i];
if (a === a.toLowerCase() && b === b.toUpperCase()) {
arr.splice(i, 0, "\n");
i += 2;
}
}
return arr.join("");
})));
myDiagram.linkTemplate =
$(go.Link,
{ layerName: "Background" },
$(go.Shape));
// Collect all of the data for the model of the class hierarchy
var nodeDataArray = [];
// Iterate over all of the classes in "go"
for (var k in go) {
var cls = go[k];
if (!cls) continue;
var proto = cls.prototype;
if (!proto) continue;
proto.constructor.className = k; // remember name
// find base class constructor
var base = Object.getPrototypeOf(proto).constructor;
if (base === Object) { // "root" node?
nodeDataArray.push({ key: k });
} else {
// add a node for this class and a tree-parent reference to the base class name
nodeDataArray.push({ key: k, parent: base.className });
}
}
// Create the model for the hierarchy diagram
myDiagram.model = new go.TreeModel(nodeDataArray);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:800px; min-width: 200px"></div>
<p>
This sample demonstrates a custom Layout, <a>ArrangingLayout</a>, that provides layouts of layouts.
It assumes the graph should be split up and laid out by potentially three separate Layouts.
</p>
<p>
The first step of ArrangingLayout is that all unconnected nodes are separated out to be laid out later by
the <a>ArrangingLayout.sideLayout</a>, which by default is a <a>GridLayout</a>.
</p>
<p>
The remaining nodes and links are partitioned into separate subgraphs with no links between subgraphs.
The <a>ArrangingLayout.primaryLayout</a> is performed on each subgraph.
</p>
<p>
If there is more than one subgraph, those subgraphs are treated as if they were individual nodes and are
laid out by the <a>ArrangingLayout.arrangingLayout</a>.
</p>
<p>
Finally the unconnected nodes are laid out by <a>ArrangingLayout.sideLayout</a> and they are all positioned
at the <a>ArrangingLayout.side</a> Spot relative to the main body of nodes and links.
</p>
<p>
This extension layout is defined in its own file, as <a href="ArrangingLayout.js">ArrangingLayout.js</a>.
</p>
</div>
</body>
</html>
+383
View File
@@ -0,0 +1,383 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Layout that provides one way to have a layout of layouts.
// It partitions nodes and links into separate subgraphs, applies a primary
// layout to each subgraph, and then arranges those results by an
// arranging layout. Any disconnected nodes are laid out later by a
// side layout, by default in a grid underneath the main body of subgraphs.
/**
* @constructor
* @extends Layout
* @class
* This layout uses three separate Layouts.
*
* One is used for laying out nodes and links that are connected together: {@link #primaryLayout}.
* This defaults to null and must be set to an instance of a {@link Layout},
* such as a {@link TreeLayout} or a {@link ForceDirectedLayout} or a custom Layout.
*
* One is used to arrange separate subnetworks of the main graph: {@link #arrangingLayout}.
* This defaults to an instance of {@link GridLayout}.
*
* One is used for laying out the additional nodes along one of the sides of the main graph: {@link #sideLayout}.
* This also defaults to an instance of {@link GridLayout}.
* A filter predicate, {@link #filter}, splits up the collection of nodes and links into two subsets,
* one for the main layout and one for the side layout.
* By default, when there is no filter, it puts all nodes that have no link connections into the
* subset to be processed by the side layout.
*
* If all pairs of nodes in the main graph can be reached by some path of undirected links,
* there are no separate subnetworks, so the {@link #arrangingLayout} need not be used and
* the {@link #primaryLayout} would apply to all of those nodes and links.
*
* But if there are disconnected subnetworks, the {@link #primaryLayout} is applied to each subnetwork,
* and then all of those results are arranged by the {@link #arrangingLayout}.
*
* In either case if there are any nodes in the side graph, those are arranged by the {@link #sideLayout}
* to be on the side of the arrangement of the main graph of nodes and links.
*
* Note: if you do not want to have singleton nodes be arranged by {@link #sideLayout},
* set {@link #filter} to <code>function(part) { return true; }</code>.
* That will cause all singleton nodes to be arranged by {@link #arrangingLayout} as if they
* were each their own subgraph.
*
* If you both don't want to use {@link #sideLayout} and you don't want to use {@link #arrangingLayout}
* to lay out connected subgraphs, don't use this ArrangingLayout at all --
* just use whatever Layout you would have assigned to {@link #primaryLayout}.
*/
function ArrangingLayout() {
go.Layout.call(this);
this._filter = null;
this._primaryLayout = new go.GridLayout();
this._primaryLayout.cellSize = new go.Size(1, 1);
this._arrangingLayout = new go.GridLayout();
this._arrangingLayout.cellSize = new go.Size(1, 1);
this._sideLayout = new go.GridLayout();
this._sideLayout.cellSize = new go.Size(1, 1);
this._side = go.Spot.BottomSide;
this._spacing = new go.Size(20, 20);
}
go.Diagram.inherit(ArrangingLayout, go.Layout);
/**
* @hidden @internal
* Copies properties to a cloned Layout.
* @this {ArrangingLayout}
* @param {Layout} copy
* @override
*/
ArrangingLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
copy._filter = this._filter;
if (this._primaryLayout !== null) copy._primaryLayout = this._primaryLayout.copy();
if (this._arrangingLayout !== null) copy._arrangingLayout = this._arrangingLayout.copy();
if (this._sideLayout !== null) copy._sideLayout = this._sideLayout.copy();
copy._side = this._side.copy();
copy._spacing = this._spacing.copy();
};
/**
* @hidden @internal
* @this {ArrangingLayout}
* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
*/
ArrangingLayout.prototype.doLayout = function(coll) {
coll = this.collectParts(coll);
var diagram = this.diagram;
if (diagram === null) throw new Error("No Diagram for this Layout");
// implementations of doLayout that do not make use of a LayoutNetwork
// need to perform their own transactions
diagram.startTransaction("Arranging Layout");
var maincoll = new go.Set();
var sidecoll = new go.Set();
this.splitParts(coll, maincoll, sidecoll);
var mainnet = null;
var subnets = null;
if (this.arrangingLayout !== null) {
var mainnet = this.makeNetwork(maincoll);
var subnets = mainnet.splitIntoSubNetworks();
}
var bounds = null;
if (this.arrangingLayout !== null && subnets !== null && subnets.count > 1) {
var groups = new go.Set();
var it = subnets.iterator;
while (it.next()) {
var net = it.value;
var subcoll = net.findAllParts();
this.preparePrimaryLayout(this.primaryLayout, subcoll);
this.primaryLayout.doLayout(subcoll);
groups.add(this._makeMainNode(subcoll));
}
var mit = mainnet.vertexes.iterator;
while (mit.next()) {
var v = mit.value;
if (v.node) {
var subcoll = new go.Set();
subcoll.add(v.node);
this.preparePrimaryLayout(this.primaryLayout, subcoll);
this.primaryLayout.doLayout(subcoll);
groups.add(this._makeMainNode(subcoll));
}
}
this.arrangingLayout.doLayout(groups);
var git = groups.iterator;
while (git.next()) {
var grp = git.value;
this.moveSubgraph(grp._subcoll, grp._subcollBounds, new go.Rect(grp.position, grp.desiredSize));
}
bounds = diagram.computePartsBounds(groups); // not maincoll due to links without real bounds
} else { // no this.arrangingLayout
this.preparePrimaryLayout(this.primaryLayout, maincoll);
this.primaryLayout.doLayout(maincoll);
bounds = diagram.computePartsBounds(maincoll);
this.moveSubgraph(maincoll, bounds, bounds);
}
if (!bounds.isReal()) bounds = new go.Rect(0, 0, 0, 0);
this.prepareSideLayout(this.sideLayout, sidecoll, bounds);
if (sidecoll.count > 0) {
this.sideLayout.doLayout(sidecoll);
var sidebounds = diagram.computePartsBounds(sidecoll);
if (!sidebounds.isReal()) sidebounds = new go.Rect(0, 0, 0, 0);
this.moveSideCollection(sidecoll, bounds, sidebounds);
}
diagram.commitTransaction("Arranging Layout");
};
/**
* @hidden @internal
* @param {*} subcoll
*/
ArrangingLayout.prototype._makeMainNode = function(subcoll) {
var grp = new go.Node();
grp.locationSpot = go.Spot.Center;
grp._subcoll = subcoll;
var grpb = this.diagram.computePartsBounds(subcoll);
grp._subcollBounds = grpb;
grp.desiredSize = grpb.size;
grp.position = grpb.position;
return grp;
}
/**
* Assign all of the Parts in the given collection into either the
* set of Nodes and Links for the main graph or the set of Nodes and Links
* for the side graph.
*
* By default this just calls the {@link #filter} on each non-Link to decide,
* and then looks at each Link's connected Nodes to decide.
*
* A null filter assigns all Nodes that have connected Links to the main graph, and
* all Links will be assigned to the main graph, and the side graph will only contain
* Parts with no connected Links.
* @param {Set} coll
* @param {Set} maincoll
* @param {Set} sidecoll
*/
ArrangingLayout.prototype.splitParts = function(coll, maincoll, sidecoll) {
// first consider all Nodes
var pred = this.filter;
coll.each(function(p) {
if (p instanceof go.Link) return;
var main;
if (pred) main = pred(p);
else if (p instanceof go.Node) main = (p.linksConnected.count > 0);
else main = (p instanceof go.Link);
if (main) {
maincoll.add(p);
} else {
sidecoll.add(p);
}
});
// now assign Links based on which Nodes they connect with
coll.each(function(p) {
if (p instanceof go.Link) {
if (maincoll.contains(p.fromNode) && maincoll.contains(p.toNode)) {
maincoll.add(p);
} else if (sidecoll.contains(p.fromNode) && sidecoll.contains(p.toNode)) {
sidecoll.add(p);
}
}
});
}
/**
* This method is called just before the primaryLayout is performed so that
* there can be adjustments made to the primaryLayout, if desired.
* By default this method makes no adjustments to the primaryLayout.
* @param {Layout} primaryLayout the sideLayout that may be modified for the results of the primaryLayout
* @param {Set} mainColl the Nodes and Links to be laid out by primaryLayout after being separated into subnetworks
*/
ArrangingLayout.prototype.preparePrimaryLayout = function(primaryLayout, mainColl) {
// by default this is a no-op
}
/**
* Move a Set of Nodes and Links to the given area.
* @param {Set} subColl the Set of Nodes and Links that form a separate connected subgraph
* @param {Rect} subbounds the area occupied by the subColl
* @param {Rect} bounds the area where they should be moved according to the arrangingLayout
*/
ArrangingLayout.prototype.moveSubgraph = function(subColl, subbounds, bounds) {
this.diagram.moveParts(subColl, bounds.position.subtract(subbounds.position), false);
}
/**
* This method is called just after the main layouts (the primaryLayouts and arrangingLayout)
* have been performed and just before the sideLayout is performed so that there can be
* adjustments made to the sideLayout, if desired.
* By default this method makes no adjustments to the sideLayout.
* @param {Layout} sideLayout the sideLayout that may be modified for the results of the main layouts
* @param {Set} sideColl the Nodes and Links filtered out to be laid out by sideLayout
* @param {Rect} mainBounds the area occupied by the nodes and links of the main layout, after it was performed
*/
ArrangingLayout.prototype.prepareSideLayout = function(sideLayout, sideColl, mainBounds) {
// by default this is a no-op
}
/**
* This method is called just after the sideLayout has been performed in order to move
* its parts to the desired area relative to the results of the main layouts.
* By default this calls {@link Diagram#moveParts} on the sidecoll collection to the {@link #side} of the mainbounds.
* This won't get called if there are no Parts in the sidecoll collection.
* @param {Set} sidecoll a collection of Parts that were laid out by the sideLayout
* @param {Rect} mainbounds the area occupied by the results of the main layouts
* @param {Rect} sidebounds the area occupied by the results of the sideLayout
*/
ArrangingLayout.prototype.moveSideCollection = function(sidecoll, mainbounds, sidebounds) {
var diagram = this.diagram;
if (this.side.includesSide(go.Spot.BottomSide)) {
diagram.moveParts(sidecoll, new go.Point(mainbounds.x - sidebounds.x, mainbounds.y + mainbounds.height + this.spacing.height - sidebounds.y), false);
} else if (this.side.includesSide(go.Spot.RightSide)) {
diagram.moveParts(sidecoll, new go.Point(mainbounds.x + mainbounds.width + this.spacing.width - sidebounds.x, mainbounds.y - sidebounds.y), false);
} else if (this.side.includesSide(go.Spot.TopSide)) {
diagram.moveParts(sidecoll, new go.Point(mainbounds.x - sidebounds.x, mainbounds.y - sidebounds.height - this.spacing.height - sidebounds.y), false);
} else if (this.side.includesSide(go.Spot.LeftSide)) {
diagram.moveParts(sidecoll, new go.Point(mainbounds.x - sidebounds.width - this.spacing.width - sidebounds.x, mainbounds.y - sidebounds.y), false);
}
}
// Public properties
/**
* Gets or sets the predicate function to call on each non-Link.
* If the predicate returns true, the part will be laid out by the main layouts,
* the primaryLayouts and the arrangingLayout, otherwise by the sideLayout.
* The default value is a function that is true when there are any links connecting with the node.
* Such default behavior will have the sideLayout position all of the singleton nodes.
* @name ArrangingLayout#side
* @return {function}
*/
Object.defineProperty(ArrangingLayout.prototype, "filter", {
get: function() { return this._filter; },
set: function(val) {
if (val && typeof val !== 'function') throw new Error("new value for ArrangingLayout.filter must be a function, not: " + val);
if (this._filter !== val) {
this._filter = val;
this.invalidateLayout();
}
}
});
/**
* Gets or sets the side {@link Spot} where the side nodes and links should be laid out,
* relative to the results of the main Layout.
* The default value is Spot.BottomSide.
* Currently only handles a single side.
* @name ArrangingLayout#side
* @return {Spot}
*/
Object.defineProperty(ArrangingLayout.prototype, "side", {
get: function() { return this._side; },
set: function(val) {
if (!(val instanceof go.Spot) || !val.isSide()) {
throw new Error("new value for ArrangingLayout.side must be a side Spot, not: " + val);
}
if (!this._side.equals(val)) {
this._side = val.copy();
this.invalidateLayout();
}
}
});
/**
* Gets or sets the space between the main layout and the side layout.
* The default value is Size(20, 20).
* @name ArrangingLayout#spacing
* @return {Size}
*/
Object.defineProperty(ArrangingLayout.prototype, "spacing", {
get: function() { return this._spacing; },
set: function(val) {
if (!(val instanceof go.Size)) throw new Error("new value for ArrangingLayout.spacing must be a Size, not: " + val);
if (!this._spacing.equals(val)) {
this._spacing = val.copy();
this.invalidateLayout();
}
}
});
/**
* Gets or sets the Layout used for the main part of the diagram.
* The default value is an instance of GridLayout.
* Any new value must not be null.
* @name ArrangingLayout#primaryLayout
* @return {Layout}
*/
Object.defineProperty(ArrangingLayout.prototype, "primaryLayout", {
get: function() { return this._primaryLayout; },
set: function(val) {
if (!(val instanceof go.Layout)) throw new Error("layout does not inherit from go.Layout: " + val);
this._primaryLayout = val;
this.invalidateLayout();
}
});
/**
* Gets or sets the Layout used to arrange multiple separate connected subgraphs of the main graph.
* The default value is an instance of GridLayout.
* Set this property to null in order to get the default behavior of the @{link #primaryLayout}
* when dealing with multiple connected graphs as a whole.
* @name ArrangingLayout#arrangingLayout
* @return {Layout}
*/
Object.defineProperty(ArrangingLayout.prototype, "arrangingLayout", {
get: function() { return this._arrangingLayout; },
set: function(val) {
if (val && !(val instanceof go.Layout)) throw new Error("layout does not inherit from go.Layout: " + val);
this._arrangingLayout = val;
this.invalidateLayout();
}
});
/**
* Gets or sets the Layout used to arrange the "side" nodes and links -- those outside of the main layout.
* The default value is an instance of GridLayout.
* Any new value must not be null.
* @name ArrangingLayout#sideLayout
* @return {Layout}
*/
Object.defineProperty(ArrangingLayout.prototype, "sideLayout", {
get: function() { return this._sideLayout; },
set: function(val) {
if (!(val instanceof go.Layout)) throw new Error("layout does not inherit from go.Layout: " + val);
this._sideLayout = val;
this.invalidateLayout();
}
});
+109
View File
@@ -0,0 +1,109 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// These are the definitions for all of the predefined arrowheads.
// You do not need to load this file in order to use arrowheads.
// Typical custom definition:
// go.Shape.defineArrowheadGeometry("Zigzag", "M0,4 L1,8 3,0 5,8 7,0 8,4");
// Typical usage in a link template:
// myDiagram.linkTemplate =
// $(go.Link,
// $(go.Shape),
// $(go.Shape, { toArrow: "Zigzag" })
// );
go.Shape.defineArrowheadGeometry("Standard", "F1 m 0,0 l 8,4 -8,4 2,-4 z");
go.Shape.defineArrowheadGeometry("Backward", "F1 m 8,0 l -2,4 2,4 -8,-4 z");
go.Shape.defineArrowheadGeometry("Triangle", "F1 m 0,0 l 8,4.62 -8,4.62 z");
go.Shape.defineArrowheadGeometry("BackwardTriangle", "F1 m 8,4 l 0,4 -8,-4 8,-4 0,4 z");
go.Shape.defineArrowheadGeometry("Boomerang", "F1 m 0,0 l 8,4 -8,4 4,-4 -4,-4 z");
go.Shape.defineArrowheadGeometry("BackwardBoomerang", "F1 m 8,0 l -8,4 8,4 -4,-4 4,-4 z");
go.Shape.defineArrowheadGeometry("SidewaysV", "m 0,0 l 8,4 -8,4 0,-1 6,-3 -6,-3 0,-1 z");
go.Shape.defineArrowheadGeometry("BackwardV", "m 8,0 l -8,4 8,4 0,-1 -6,-3 6,-3 0,-1 z");
go.Shape.defineArrowheadGeometry("OpenTriangle", "m 0,0 l 8,4 -8,4");
go.Shape.defineArrowheadGeometry("BackwardOpenTriangle", "m 8,0 l -8,4 8,4");
go.Shape.defineArrowheadGeometry("OpenTriangleLine", "m 0,0 l 8,4 -8,4 m 8.5,0 l 0,-8");
go.Shape.defineArrowheadGeometry("BackwardOpenTriangleLine", "m 8,0 l -8,4 8,4 m -8.5,0 l 0,-8");
go.Shape.defineArrowheadGeometry("OpenTriangleTop", "m 0,0 l 8,4 m 0,4");
go.Shape.defineArrowheadGeometry("BackwardOpenTriangleTop", "m 8,0 l -8,4 m 0,4");
go.Shape.defineArrowheadGeometry("OpenTriangleBottom", "m 0,8 l 8,-4");
go.Shape.defineArrowheadGeometry("BackwardOpenTriangleBottom", "m 0,4 l 8,4");
go.Shape.defineArrowheadGeometry("HalfTriangleTop", "F1 m 0,0 l 0,4 8,0 z m 0,8");
go.Shape.defineArrowheadGeometry("BackwardHalfTriangleTop", "F1 m 8,0 l 0,4 -8,0 z m 0,8");
go.Shape.defineArrowheadGeometry("HalfTriangleBottom", "F1 m 0,4 l 0,4 8,-4 z");
go.Shape.defineArrowheadGeometry("BackwardHalfTriangleBottom", "F1 m 8,4 l 0,4 -8,-4 z");
go.Shape.defineArrowheadGeometry("ForwardSemiCircle", "m 4,0 b 270 180 0 4 4");
go.Shape.defineArrowheadGeometry("BackwardSemiCircle", "m 4,8 b 90 180 0 -4 4");
go.Shape.defineArrowheadGeometry("Feather", "m 0,0 l 3,4 -3,4");
go.Shape.defineArrowheadGeometry("BackwardFeather", "m 3,0 l -3,4 3,4");
go.Shape.defineArrowheadGeometry("DoubleFeathers", "m 0,0 l 3,4 -3,4 m 3,-8 l 3,4 -3,4");
go.Shape.defineArrowheadGeometry("BackwardDoubleFeathers", "m 3,0 l -3,4 3,4 m 3,-8 l -3,4 3,4");
go.Shape.defineArrowheadGeometry("TripleFeathers", "m 0,0 l 3,4 -3,4 m 3,-8 l 3,4 -3,4 m 3,-8 l 3,4 -3,4");
go.Shape.defineArrowheadGeometry("BackwardTripleFeathers", "m 3,0 l -3,4 3,4 m 3,-8 l -3,4 3,4 m 3,-8 l -3,4 3,4");
go.Shape.defineArrowheadGeometry("ForwardSlash", "m 0,8 l 5,-8");
go.Shape.defineArrowheadGeometry("BackSlash", "m 0,0 l 5,8");
go.Shape.defineArrowheadGeometry("DoubleForwardSlash", "m 0,8 l 4,-8 m -2,8 l 4,-8");
go.Shape.defineArrowheadGeometry("DoubleBackSlash", "m 0,0 l 4,8 m -2,-8 l 4,8");
go.Shape.defineArrowheadGeometry("TripleForwardSlash", "m 0,8 l 4,-8 m -2,8 l 4,-8 m -2,8 l 4,-8");
go.Shape.defineArrowheadGeometry("TripleBackSlash", "m 0,0 l 4,8 m -2,-8 l 4,8 m -2,-8 l 4,8");
go.Shape.defineArrowheadGeometry("Fork", "m 0,4 l 8,0 m -8,0 l 8,-4 m -8,4 l 8,4");
go.Shape.defineArrowheadGeometry("BackwardFork", "m 8,4 l -8,0 m 8,0 l -8,-4 m 8,4 l -8,4");
go.Shape.defineArrowheadGeometry("LineFork", "m 0,0 l 0,8 m 0,-4 l 8,0 m -8,0 l 8,-4 m -8,4 l 8,4");
go.Shape.defineArrowheadGeometry("BackwardLineFork", "m 8,4 l -8,0 m 8,0 l -8,-4 m 8,4 l -8,4 m 8,-8 l 0,8");
go.Shape.defineArrowheadGeometry("CircleFork", "F1 m 6,4 b 0 360 -3 0 3 z m 0,0 l 6,0 m -6,0 l 6,-4 m -6,4 l 6,4");
go.Shape.defineArrowheadGeometry("BackwardCircleFork", "F1 m 0,4 l 6,0 m -6,-4 l 6,4 m -6,4 l 6,-4 m 6,0 b 0 360 -3 0 3");
go.Shape.defineArrowheadGeometry("CircleLineFork", "F1 m 6,4 b 0 360 -3 0 3 z m 1,-4 l 0,8 m 0,-4 l 6,0 m -6,0 l 6,-4 m -6,4 l 6,4");
go.Shape.defineArrowheadGeometry("BackwardCircleLineFork", "F1 m 0,4 l 6,0 m -6,-4 l 6,4 m -6,4 l 6,-4 m 0,-4 l 0,8 m 7,-4 b 0 360 -3 0 3");
go.Shape.defineArrowheadGeometry("Circle", "F1 m 8,4 b 0 360 -4 0 4 z");
go.Shape.defineArrowheadGeometry("Block", "F1 m 0,0 l 0,8 8,0 0,-8 z");
go.Shape.defineArrowheadGeometry("StretchedDiamond", "F1 m 0,3 l 5,-3 5,3 -5,3 -5,-3 z");
go.Shape.defineArrowheadGeometry("Diamond", "F1 m 0,4 l 4,-4 4,4 -4,4 -4,-4 z");
go.Shape.defineArrowheadGeometry("Chevron", "F1 m 0,0 l 5,0 3,4 -3,4 -5,0 3,-4 -3,-4 z");
go.Shape.defineArrowheadGeometry("StretchedChevron", "F1 m 0,0 l 8,0 3,4 -3,4 -8,0 3,-4 -3,-4 z");
go.Shape.defineArrowheadGeometry("NormalArrow", "F1 m 0,2 l 4,0 0,-2 4,4 -4,4 0,-2 -4,0 z");
go.Shape.defineArrowheadGeometry("X", "m 0,0 l 8,8 m 0,-8 l -8,8");
go.Shape.defineArrowheadGeometry("TailedNormalArrow", "F1 m 0,0 l 2,0 1,2 3,0 0,-2 2,4 -2,4 0,-2 -3,0 -1,2 -2,0 1,-4 -1,-4 z");
go.Shape.defineArrowheadGeometry("DoubleTriangle", "F1 m 0,0 l 4,4 -4,4 0,-8 z m 4,0 l 4,4 -4,4 0,-8 z");
go.Shape.defineArrowheadGeometry("BigEndArrow" , "F1 m 0,0 l 5,2 0,-2 3,4 -3,4 0,-2 -5,2 0,-8 z");
go.Shape.defineArrowheadGeometry("ConcaveTailArrow", "F1 m 0,2 h 4 v -2 l 4,4 -4,4 v -2 h -4 l 2,-2 -2,-2 z");
go.Shape.defineArrowheadGeometry("RoundedTriangle", "F1 m 0,1 a 1,1 0 0 1 1,-1 l 7,3 a 0.5,1 0 0 1 0,2 l -7,3 a 1,1 0 0 1 -1,-1 l 0,-6 z");
go.Shape.defineArrowheadGeometry("SimpleArrow", "F1 m 1,2 l -1,-2 2,0 1,2 -1,2 -2,0 1,-2 5,0 0,-2 2,2 -2,2 0,-2 z");
go.Shape.defineArrowheadGeometry("AccelerationArrow" , "F1 m 0,0 l 0,8 0.2,0 0,-8 -0.2,0 z m 2,0 l 0,8 1,0 0,-8 -1,0 z m 3,0 l 2,0 2,4 -2,4 -2,0 0,-8 z");
go.Shape.defineArrowheadGeometry("BoxArrow" , "F1 m 0,0 l 4,0 0,2 2,0 0,-2 2,4 -2,4 0,-2 -2,0 0,2 -4,0 0,-8 z");
go.Shape.defineArrowheadGeometry("TriangleLine" , "F1 m 8,4 l -8,-4 0,8 8,-4 z m 0.5,4 l 0,-8");
go.Shape.defineArrowheadGeometry("CircleEndedArrow" , "F1 m 10,4 l -2,-3 0,2 -2,0 0,2 2,0 0,2 2,-3 z m -4,0 b 0 360 -3 0 3 z");
go.Shape.defineArrowheadGeometry("DynamicWidthArrow" , "F1 m 0,3 l 2,0 2,-1 2,-2 2,4 -2,4 -2,-2 -2,-1 -2,0 0,-2 z");
go.Shape.defineArrowheadGeometry("EquilibriumArrow" , "m 0,3 l 8,0 -3,-3 m 3,5 l -8,0 3,3");
go.Shape.defineArrowheadGeometry("FastForward" , "F1 m 0,0 l 3.5,4 0,-4 3.5,4 0,-4 1,0 0,8 -1,0 0,-4 -3.5,4 0,-4 -3.5,4 0,-8 z");
go.Shape.defineArrowheadGeometry("Kite", "F1 m 0,4 l 2,-4 6,4 -6,4 -2,-4 z");
go.Shape.defineArrowheadGeometry("HalfArrowTop", "F1 m 0,0 l 4,4 4,0 -8,-4 z m 0,8");
go.Shape.defineArrowheadGeometry("HalfArrowBottom", "F1 m 0,8 l 4,-4 4,0 -8,4 z");
go.Shape.defineArrowheadGeometry("OpposingDirectionDoubleArrow" , "F1 m 0,4 l 2,-4 0,2 4,0 0,-2 2,4 -2,4 0,-2 -4,0 0,2 -2,-4 z");
go.Shape.defineArrowheadGeometry("PartialDoubleTriangle" , "F1 m 0,0 4,3 0,-3 4,4 -4,4 0,-3 -4,3 0,-8 z");
go.Shape.defineArrowheadGeometry("LineCircle", "F1 m 0,0 l 0,8 m 7 -4 b 0 360 -3 0 3 z");
go.Shape.defineArrowheadGeometry("DoubleLineCircle" , "F1 m 0,0 l 0,8 m 2,-8 l 0,8 m 7 -4 b 0 360 -3 0 3 z");
go.Shape.defineArrowheadGeometry("TripleLineCircle" , "F1 m 0,0 l 0,8 m 2,-8 l 0,8 m 2,-8 l 0,8 m 7 -4 b 0 360 -3 0 3 z");
go.Shape.defineArrowheadGeometry("CircleLine" , "F1 m 6 4 b 0 360 -3 0 3 z m 1,-4 l 0,8");
go.Shape.defineArrowheadGeometry("DiamondCircle", "F1 m 8,4 l -4,4 -4,-4 4,-4 4,4 m 8,0 b 0 360 -4 0 4 z");
go.Shape.defineArrowheadGeometry("PlusCircle" , "F1 m 8,4 b 0 360 -4 0 4 l -8 0 z m -4 -4 l 0 8");
go.Shape.defineArrowheadGeometry("OpenRightTriangleTop", "m 8,0 l 0,4 -8,0 m 0,4");
go.Shape.defineArrowheadGeometry("OpenRightTriangleBottom" , "m 8,8 l 0,-4 -8,0");
go.Shape.defineArrowheadGeometry("Line", "m 0,0 l 0,8");
go.Shape.defineArrowheadGeometry("DoubleLine", "m 0,0 l 0,8 m 2,0 l 0,-8");
go.Shape.defineArrowheadGeometry("TripleLine", "m 0,0 l 0,8 m 2,0 l 0,-8 m 2,0 l 0,8");
go.Shape.defineArrowheadGeometry("PentagonArrow", "F1 m 8,4 l -4,-4 -4,0 0,8 4,0 4,-4 z");
+67
View File
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<title>Balloon Links</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A demonstration of the BalloonLink extension for implementing word balloons or speech bubbles as comments in diagrams about particular objects." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="BalloonLink.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
"undoManager.isEnabled": true // enable undo & redo
});
// define a simple Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the TextBlock
$(go.Shape, "Rectangle", { strokeWidth: 0 },
// Shape.fill is bound to Node.data.color
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 }, // some room around the text
// TextBlock.text is bound to Node.data.key
new go.Binding("text", "key"))
);
myDiagram.linkTemplate =
$(BalloonLink,
$(go.Shape,
{ stroke: "limegreen", strokeWidth: 3, fill: "limegreen" }
)
);
// create the model data that will be represented by Nodes and Links
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" }
],
[
{ from: "Alpha", to: "Beta" }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:300px; height:300px"></div>
<p>
A <b>BalloonLink</b> is a custom <a>Link</a> that draws a "balloon" shape around the Link.fromNode.
It will create a triangular shape with the base at the fromNode and the other point at the toNode.
It is defined in its own file, as <a href="BalloonLink.js">BalloonLink.js</a>.
</p>
<p>
Usage can also be seen in the <a href="../samples/comments.html">Comments</a> sample.
</p>
</div>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Link that draws a "balloon" shape around the Link.fromNode
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Link
* @class
* This custom Link class customizes its Shape to surround the comment node.
* If the Shape is filled, it will obscure the comment itself unless the Link is behind the comment node.
* Thus the default layer for BalloonLinks is "Background".
*/
function BalloonLink() {
go.Link.call(this);
this.layerName = "Background";
this._base = 10;
}
go.Diagram.inherit(BalloonLink, go.Link);
/**
* @ignore
* Copies properties to a cloned BalloonLink.
* @this {BalloonLink}
* @param {BalloonLink} copy
* @override
*/
BalloonLink.prototype.cloneProtected = function(copy) {
go.Link.prototype.cloneProtected.call(this, copy);
copy._base = this._base;
}
/*
* The width of the base of the triangle at the center point of the Link.fromNode.
* The default value is 10.
* @name BalloonLink#base
* @return {number}
*/
Object.defineProperty(BalloonLink.prototype, "base", {
get: function() { return this._base; },
set: function(value) { this._base = value; }
});
/**
* Produce a Geometry from the Link's route that draws a "balloon" shape around the Link.fromNode
* and has a triangular shape with the base at the fromNode and the top at the toNode.
* @this {BalloonLink}
*/
BalloonLink.prototype.makeGeometry = function() {
// assume the fromNode is the comment and the toNode is the commented-upon node
var bb = this.fromNode.actualBounds;
var nb = this.toNode.actualBounds;
var numpts = this.pointsCount;
var p0 = bb.center;
var pn = this.getPoint(numpts - 1);
if (bb.intersectsRect(nb)) {
pn = nb.center;
}
var pos = this.routeBounds;
// compute the intersection points for the triangular arrow
var ang = pn.directionPoint(p0);
var L = new go.Point(this.base, 0).rotate(ang - 90).add(p0);
var R = new go.Point(this.base, 0).rotate(ang + 90).add(p0);
this.getLinkPointFromPoint(this.fromNode, this.fromNode, L, pn, true, L);
this.getLinkPointFromPoint(this.fromNode, this.fromNode, R, pn, true, R);
// form a triangular arrow from the comment to the commented node
var fig = new go.PathFigure(pn.x - pos.x, pn.y - pos.y, true); // filled; start at arrow point at commented node
fig.add(new go.PathSegment(go.PathSegment.Line, R.x - pos.x, R.y - pos.y)); // a triangle base point on comment's edge
var side = 0;
if (L.y >= bb.bottom || R.y >= bb.bottom) side = 2;
else if (L.x <= bb.x && R.x <= bb.x) side = 1;
else if (L.x >= bb.right && R.x >= bb.right) side = 3;
this.pathToCorner(side, bb, fig, pos, L, R);
this.pathToCorner(side + 1, bb, fig, pos, L, R);
this.pathToCorner(side + 2, bb, fig, pos, L, R);
this.pathToCorner(side + 3, bb, fig, pos, L, R);
fig.add(new go.PathSegment(go.PathSegment.Line, L.x - pos.x, L.y - pos.y).close()); // the other triangle base point on comment's edge
// return a Geometry
return new go.Geometry().add(fig);
};
/**
* @ignore
* Draw a line to a corner, but not if the comment arrow encompasses that corner.
* @this {BalloonLink}
*/
BalloonLink.prototype.pathToCorner = function(side, bb, fig, pos, L, R) {
switch (side % 4) {
case 0: if (!(L.y <= bb.y && R.x <= bb.x)) fig.add(new go.PathSegment(go.PathSegment.Line, bb.x - pos.x, bb.y - pos.y)); break;
case 1: if (!(L.x <= bb.x && R.y >= bb.bottom)) fig.add(new go.PathSegment(go.PathSegment.Line, bb.x - pos.x, bb.bottom - pos.y)); break;
case 2: if (!(L.y >= bb.bottom && R.x >= bb.right)) fig.add(new go.PathSegment(go.PathSegment.Line, bb.right - pos.x, bb.bottom - pos.y)); break;
case 3: if (!(L.x >= bb.right && R.y <= bb.y)) fig.add(new go.PathSegment(go.PathSegment.Line, bb.right - pos.x, bb.y - pos.y)); break;
}
};
// end BalloonLink class
+577
View File
@@ -0,0 +1,577 @@
'use strict';
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// These are the definitions for all of the predefined buttons.
// You do not need to load this file in order to use buttons.
// A 'Button' is a Panel that has a Shape surrounding some content
// and that has mouseEnter/mouseLeave behavior to highlight the button.
// The content of the button, whether a TextBlock or a Picture or a complicated Panel,
// must be supplied by the caller.
// The caller must also provide a click event handler.
// Typical usage:
// $('Button',
// $(go.TextBlock, 'Click me!'), // the content is just the text label
// { click: function(e, obj) { alert('I was clicked'); } }
// )
// Note that a button click event handler is not invoked upon a click if isEnabledObject() returns false.
go.GraphObject.defineBuilder('Button', function (args) {
// default colors for 'Button' shape
var buttonFillNormal = '#F5F5F5';
var buttonStrokeNormal = '#BDBDBD';
var buttonFillOver = '#E0E0E0';
var buttonStrokeOver = '#9E9E9E';
var buttonFillPressed = '#BDBDBD'; // set to null for no button pressed effects
var buttonStrokePressed = '#9E9E9E';
var buttonFillDisabled = '#E5E5E5';
// padding inside the ButtonBorder to match sizing from previous versions
var paddingHorizontal = 2.76142374915397;
var paddingVertical = 2.761423749153969;
var button = /** @type {Panel} */ (
go.GraphObject.make(go.Panel, 'Auto',
{
isActionable: true, // needed so that the ActionTool intercepts mouse events
enabledChanged: function (btn, enabled) {
var shape = btn.findObject('ButtonBorder');
if (shape !== null) {
shape.fill = enabled ? btn['_buttonFillNormal'] : btn['_buttonFillDisabled'];
}
},
cursor: 'pointer',
// save these values for the mouseEnter and mouseLeave event handlers
'_buttonFillNormal': buttonFillNormal,
'_buttonStrokeNormal': buttonStrokeNormal,
'_buttonFillOver': buttonFillOver,
'_buttonStrokeOver': buttonStrokeOver,
'_buttonFillPressed': buttonFillPressed,
'_buttonStrokePressed': buttonStrokePressed,
'_buttonFillDisabled': buttonFillDisabled
},
go.GraphObject.make(go.Shape, // the border
{
name: 'ButtonBorder',
figure: 'RoundedRectangle',
spot1: new go.Spot(0, 0, paddingHorizontal, paddingVertical),
spot2: new go.Spot(1, 1, -paddingHorizontal, -paddingVertical),
parameter1: 2,
parameter2: 2,
fill: buttonFillNormal,
stroke: buttonStrokeNormal
}
)
)
);
// There's no GraphObject inside the button shape -- it must be added as part of the button definition.
// This way the object could be a TextBlock or a Shape or a Picture or arbitrarily complex Panel.
// mouse-over behavior
button.mouseEnter = function (e, btn, prev) {
if (!btn.isEnabledObject()) return;
var shape = btn.findObject('ButtonBorder'); // the border Shape
if (shape instanceof go.Shape) {
var brush = btn['_buttonFillOver'];
btn['_buttonFillNormal'] = shape.fill;
shape.fill = brush;
brush = btn['_buttonStrokeOver'];
btn['_buttonStrokeNormal'] = shape.stroke;
shape.stroke = brush;
}
};
button.mouseLeave = function (e, btn, prev) {
if (!btn.isEnabledObject()) return;
var shape = btn.findObject('ButtonBorder'); // the border Shape
if (shape instanceof go.Shape) {
shape.fill = btn['_buttonFillNormal'];
shape.stroke = btn['_buttonStrokeNormal'];
}
};
// mousedown/mouseup behavior
button.actionDown = function (e, btn) {
if (!btn.isEnabledObject()) return;
if (btn['_buttonFillPressed'] === null) return;
if (e.button !== 0) return;
var shape = btn.findObject('ButtonBorder'); // the border Shape
if (shape instanceof go.Shape) {
var diagram = e.diagram;
var oldskip = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
var brush = btn['_buttonFillPressed'];
btn['_buttonFillOver'] = shape.fill;
shape.fill = brush;
brush = btn['_buttonStrokePressed'];
btn['_buttonStrokeOver'] = shape.stroke;
shape.stroke = brush;
diagram.skipsUndoManager = oldskip;
}
};
button.actionUp = function (e, btn) {
if (!btn.isEnabledObject()) return;
if (btn['_buttonFillPressed'] === null) return;
if (e.button !== 0) return;
var shape = btn.findObject('ButtonBorder'); // the border Shape
if (shape instanceof go.Shape) {
var diagram = e.diagram;
var oldskip = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
if (overButton(e, btn)) {
shape.fill = btn['_buttonFillOver'];
shape.stroke = btn['_buttonStrokeOver'];
} else {
shape.fill = btn['_buttonFillNormal'];
shape.stroke = btn['_buttonStrokeNormal'];
}
diagram.skipsUndoManager = oldskip;
}
};
button.actionCancel = function (e, btn) {
if (!btn.isEnabledObject()) return;
if (btn['_buttonFillPressed'] === null) return;
var shape = btn.findObject('ButtonBorder'); // the border Shape
if (shape instanceof go.Shape) {
var diagram = e.diagram;
var oldskip = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
if (overButton(e, btn)) {
shape.fill = btn['_buttonFillOver'];
shape.stroke = btn['_buttonStrokeOver'];
} else {
shape.fill = btn['_buttonFillNormal'];
shape.stroke = btn['_buttonStrokeNormal'];
}
diagram.skipsUndoManager = oldskip;
}
};
button.actionMove = function (e, btn) {
if (!btn.isEnabledObject()) return;
if (btn['_buttonFillPressed'] === null) return;
var diagram = e.diagram;
if (diagram.firstInput.button !== 0) return;
diagram.currentTool.standardMouseOver();
if (overButton(e, btn)) {
var shape = btn.findObject('ButtonBorder');
if (shape instanceof go.Shape) {
var oldskip = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
let brush = btn['_buttonFillPressed'];
if (shape.fill !== brush) shape.fill = brush;
brush = btn['_buttonStrokePressed'];
if (shape.stroke !== brush) shape.stroke = brush;
diagram.skipsUndoManager = oldskip;
}
}
};
function overButton(e, btn) {
var over = e.diagram.findObjectAt(
e.documentPoint,
function (x) {
while (x.panel !== null) {
if (x.isActionable) return x;
x = x.panel;
}
return x;
},
function (x) { return x === btn; }
);
return over !== null;
}
return button;
});
// This is a complete Button that you can have in a Node template
// to allow the user to collapse/expand the subtree beginning at that Node.
// Typical usage within a Node template:
// $('TreeExpanderButton')
go.GraphObject.defineBuilder('TreeExpanderButton', function (args) {
var button = /** @type {Panel} */ (
go.GraphObject.make('Button',
{ // set these values for the isTreeExpanded binding conversion
'_treeExpandedFigure': 'MinusLine',
'_treeCollapsedFigure': 'PlusLine'
},
go.GraphObject.make(go.Shape, // the icon
{
name: 'ButtonIcon',
figure: 'MinusLine', // default value for isTreeExpanded is true
stroke: '#424242',
strokeWidth: 2,
desiredSize: new go.Size(8, 8)
},
// bind the Shape.figure to the Node.isTreeExpanded value using this converter:
new go.Binding('figure', 'isTreeExpanded',
function (exp, shape) {
var but = shape.panel;
return exp ? but['_treeExpandedFigure'] : but['_treeCollapsedFigure'];
}
).ofObject()
),
// assume initially not visible because there are no links coming out
{ visible: false },
// bind the button visibility to whether it's not a leaf node
new go.Binding('visible', 'isTreeLeaf',
function (leaf) { return !leaf; }
).ofObject()
)
);
// tree expand/collapse behavior
button.click = function (e, btn) {
var node = btn.part;
if (node instanceof go.Adornment) node = node.adornedPart;
if (!(node instanceof go.Node)) return;
var diagram = node.diagram;
if (diagram === null) return;
var cmd = diagram.commandHandler;
if (node.isTreeExpanded) {
if (!cmd.canCollapseTree(node)) return;
} else {
if (!cmd.canExpandTree(node)) return;
}
e.handled = true;
if (node.isTreeExpanded) {
cmd.collapseTree(node);
} else {
cmd.expandTree(node);
}
};
return button;
});
// This is a complete Button that you can have in a Group template
// to allow the user to collapse/expand the subgraph that the Group holds.
// Typical usage within a Group template:
// $('SubGraphExpanderButton')
go.GraphObject.defineBuilder('SubGraphExpanderButton', function (args) {
var button = /** @type {Panel} */ (
go.GraphObject.make('Button',
{ // set these values for the isSubGraphExpanded binding conversion
'_subGraphExpandedFigure': 'MinusLine',
'_subGraphCollapsedFigure': 'PlusLine'
},
go.GraphObject.make(go.Shape, // the icon
{
name: 'ButtonIcon',
figure: 'MinusLine', // default value for isSubGraphExpanded is true
stroke: '#424242',
strokeWidth: 2,
desiredSize: new go.Size(8, 8)
},
// bind the Shape.figure to the Group.isSubGraphExpanded value using this converter:
new go.Binding('figure', 'isSubGraphExpanded',
function (exp, shape) {
var but = shape.panel;
return exp ? but['_subGraphExpandedFigure'] : but['_subGraphCollapsedFigure'];
}
).ofObject()
)
)
);
// subgraph expand/collapse behavior
button.click = function (e, btn) {
var group = btn.part;
if (group instanceof go.Adornment) group = group.adornedPart;
if (!(group instanceof go.Group)) return;
var diagram = group.diagram;
if (diagram === null) return;
var cmd = diagram.commandHandler;
if (group.isSubGraphExpanded) {
if (!cmd.canCollapseSubGraph(group)) return;
} else {
if (!cmd.canExpandSubGraph(group)) return;
}
e.handled = true;
if (group.isSubGraphExpanded) {
cmd.collapseSubGraph(group);
} else {
cmd.expandSubGraph(group);
}
};
return button;
});
// This is just an "Auto" Adornment that can hold some contents within a light gray, shadowed box.
// Typical usage:
// toolTip:
// $("ToolTip",
// $(go.TextBlock, . . .)
// )
go.GraphObject.defineBuilder('ToolTip', function (args) {
var ad = go.GraphObject.make(go.Adornment, 'Auto',
{
isShadowed: true,
shadowColor: 'rgba(0, 0, 0, .4)',
shadowOffset: new go.Point(0, 3),
shadowBlur: 5
},
go.GraphObject.make(go.Shape,
{
name: 'Border',
figure: 'RoundedRectangle',
parameter1: 1,
parameter2: 1,
fill: '#F5F5F5',
stroke: '#F0F0F0',
spot1: new go.Spot(0, 0, 4, 6),
spot2: new go.Spot(1, 1, -4, -4)
}
)
);
return ad;
});
// This is just a "Vertical" Adornment that can hold some "ContextMenuButton"s.
// Typical usage:
// contextMenu:
// $("ContextMenu",
// $("ContextMenuButton",
// $(go.TextBlock, . . .),
// { click: . . .}
// ),
// $("ContextMenuButton", . . .)
// )
go.GraphObject.defineBuilder('ContextMenu', function (args) {
var ad = go.GraphObject.make(go.Adornment, 'Vertical',
{
background: '#F5F5F5',
isShadowed: true,
shadowColor: 'rgba(0, 0, 0, .4)',
shadowOffset: new go.Point(0, 3),
shadowBlur: 5
},
// don't set the background if the ContextMenu is adorning something and there's a Placeholder
new go.Binding('background', '', function (obj) {
var part = obj.adornedPart;
if (part !== null && obj.placeholder !== null) return null;
return '#F5F5F5';
})
);
return ad;
});
// This just holds the 'ButtonBorder' Shape that acts as the border
// around the button contents, which must be supplied by the caller.
// The button contents are usually a TextBlock or Panel consisting of a Shape and a TextBlock.
// Typical usage within an Adornment that is either a GraphObject.contextMenu or a Diagram.contextMenu:
// $('ContextMenuButton',
// $(go.TextBlock, text),
// { click: function(e, obj) { alert('Command for ' + obj.part.adornedPart); } },
// new go.Binding('visible', '', function(data) { return ...OK to perform Command...; })
// )
go.GraphObject.defineBuilder('ContextMenuButton', function (args) {
var button = /** @type {Panel} */ (go.GraphObject.make('Button'));
button.stretch = go.GraphObject.Horizontal;
var border = button.findObject('ButtonBorder');
if (border instanceof go.Shape) {
border.figure = 'Rectangle';
border.spot1 = new go.Spot(0, 0, 2, 3);
border.spot2 = new go.Spot(1, 1, -2, -2);
}
return button;
});
// This button is used to toggle the visibility of a GraphObject named
// by the second argument to GraphObject.make. If the second argument is not present
// or if it is not a string, this assumes that the element name is 'COLLAPSIBLE'.
// You can only control the visibility of one element in a Part at a time,
// although that element might be an arbitrarily complex Panel.
// Typical usage:
// $(go.Panel, . . .,
// $('PanelExpanderButton', 'COLLAPSIBLE'),
// . . .,
// $(go.Panel, . . .,
// { name: 'COLLAPSIBLE' },
// . . . stuff to be hidden or shown as the PanelExpanderButton is clicked . . .
// ),
// . . .
// )
go.GraphObject.defineBuilder('PanelExpanderButton', function (args) {
var eltname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args, 'COLLAPSIBLE'));
var button = /** @type {Panel} */ (
go.GraphObject.make('Button',
{ // set these values for the button's look
'_buttonExpandedFigure': 'M0 0 M0 6 L4 2 8 6 M8 8',
'_buttonCollapsedFigure': 'M0 0 M0 2 L4 6 8 2 M8 8',
'_buttonFillNormal': 'rgba(0, 0, 0, 0)',
'_buttonStrokeNormal': null,
'_buttonFillOver': 'rgba(0, 0, 0, .2)',
'_buttonStrokeOver': null,
'_buttonFillPressed': 'rgba(0, 0, 0, .4)',
'_buttonStrokePressed': null
},
go.GraphObject.make(go.Shape,
{ name: 'ButtonIcon', strokeWidth: 2 },
new go.Binding('geometryString', 'visible',
function (vis) { return vis ? button['_buttonExpandedFigure'] : button['_buttonCollapsedFigure']; }
).ofObject(eltname)
)
)
);
var border = button.findObject('ButtonBorder');
if (border instanceof go.Shape) {
border.stroke = null;
border.fill = 'rgba(0, 0, 0, 0)';
}
button.click = function (e, btn) {
var diagram = btn.diagram;
if (diagram === null) return;
if (diagram.isReadOnly) return;
var elt = btn.findTemplateBinder();
if (elt === null) elt = btn.part;
if (elt !== null) {
var pan = elt.findObject(eltname);
if (pan !== null) {
e.handled = true;
diagram.startTransaction('Collapse/Expand Panel');
pan.visible = !pan.visible;
diagram.commitTransaction('Collapse/Expand Panel');
}
}
};
return button;
});
// Define a common checkbox button; the first argument is the name of the data property
// to which the state of this checkbox is data bound. If the first argument is not a string,
// it raises an error. If no data binding of the checked state is desired,
// pass an empty string as the first argument.
// Examples:
// $('CheckBoxButton', 'dataPropertyName', ...)
// or:
// $('CheckBoxButton', '', { '_doClick': function(e, obj) { alert('clicked!'); } })
go.GraphObject.defineBuilder('CheckBoxButton', function (args) {
// process the one required string argument for this kind of button
var propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
var button = /** @type {Panel} */ (
go.GraphObject.make('Button',
{ desiredSize: new go.Size(14, 14) },
go.GraphObject.make(go.Shape,
{
name: 'ButtonIcon',
geometryString: 'M0 0 M0 8.85 L4.9 13.75 16.2 2.45 M16.2 16.2', // a 'check' mark
strokeWidth: 2,
stretch: go.GraphObject.Fill, // this Shape expands to fill the Button
geometryStretch: go.GraphObject.Uniform, // the check mark fills the Shape without distortion
visible: false // visible set to false: not checked, unless data.PROPNAME is true
},
// create a data Binding only if PROPNAME is supplied and not the empty string
(propname !== '' ? new go.Binding('visible', propname).makeTwoWay() : []))
)
);
button.click = function (e, btn) {
var diagram = e.diagram;
if (diagram === null || diagram.isReadOnly) return;
if (propname !== '' && diagram.model.isReadOnly) return;
e.handled = true;
var shape = btn.findObject('ButtonIcon');
diagram.startTransaction('checkbox');
shape.visible = !shape.visible; // this toggles data.checked due to TwoWay Binding
// support extra side-effects without clobbering the click event handler:
if (typeof btn['_doClick'] === 'function') btn['_doClick'](e, btn);
diagram.commitTransaction('checkbox');
};
return button;
});
// This defines a whole check-box -- including both a 'CheckBoxButton' and whatever you want as the check box label.
// Note that mouseEnter/mouseLeave/click events apply to everything in the panel, not just in the 'CheckBoxButton'.
// Examples:
// $('CheckBox', 'aBooleanDataProperty', $(go.TextBlock, 'the checkbox label'))
// or
// $('CheckBox', 'someProperty', $(go.TextBlock, 'A choice'),
// { '_doClick': function(e, obj) { ... perform extra side-effects ... } })
go.GraphObject.defineBuilder('CheckBox', function (args) {
// process the one required string argument for this kind of button
var propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
var button = /** @type {Panel} */ (
go.GraphObject.make('CheckBoxButton', propname, // bound to this data property
{
name: 'Button',
isActionable: false, // actionable is set on the whole horizontal panel
margin: new go.Margin(0, 1, 0, 0)
})
);
var box = /** @type {Panel} */ (
go.GraphObject.make(go.Panel, 'Horizontal',
button,
{
isActionable: true,
cursor: button.cursor,
margin: 1,
// transfer CheckBoxButton properties over to this new CheckBox panel
'_buttonFillNormal': button['_buttonFillNormal'],
'_buttonStrokeNormal': button['_buttonStrokeNormal'],
'_buttonFillOver': button['_buttonFillOver'],
'_buttonStrokeOver': button['_buttonStrokeOver'],
'_buttonFillPressed': button['_buttonFillPressed'],
'_buttonStrokePressed': button['_buttonStrokePressed'],
'_buttonFillDisabled': button['_buttonFillDisabled'],
mouseEnter: button.mouseEnter,
mouseLeave: button.mouseLeave,
actionDown: button.actionDown,
actionUp: button.actionUp,
actionCancel: button.actionCancel,
actionMove: button.actionMove,
click: button.click,
// also save original Button behavior, for potential use in a Panel.click event handler
'_buttonClick': button.click
}
)
);
// avoid potentially conflicting event handlers on the 'CheckBoxButton'
button.mouseEnter = null;
button.mouseLeave = null;
button.actionDown = null;
button.actionUp = null;
button.actionCancel = null;
button.actionMove = null;
button.click = null;
return box;
});
+155
View File
@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html>
<head>
<title>CheckBoxes</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="An implementation of CheckBoxes as GoJS objects to show and edit a boolean data property." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
"undoManager.isEnabled": true // enable undo & redo
});
// this template includes a lot of CheckBoxes, each configured in different manners
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the whole table
$(go.Shape, { strokeWidth: 0 }, // no border
new go.Binding("fill", "color")),
$(go.Panel, "Table",
{ padding: 3 },
$(go.TextBlock,
{ row: 0, column: 0, columnSpan: 2 },
{ margin: 3, font: "bold 10pt sans-serif" }, // some room around the bold text
new go.Binding("text", "key")),
// the first column has an assortment of CheckBoxes
$(go.Panel, "Vertical",
{ row: 1, column: 0, defaultAlignment: go.Spot.Left },
$("CheckBox", "choice1",
$(go.TextBlock, "default")
),
$("CheckBox", "choice2",
{ "ButtonIcon.stroke": "green" },
$(go.TextBlock, "green")
),
$("CheckBox", "choice3",
{ "ButtonIcon.stroke": "red", "ButtonIcon.figure": "XLine" },
$(go.TextBlock, "red X")
),
$("CheckBox", "choice4",
{ "_buttonFillOver": "pink", "_buttonStrokeOver": "red" },
$(go.TextBlock, "pink over")
),
$("CheckBox", "choice5",
{ "Button.width": 32, "Button.height": 32 },
$(go.TextBlock, "BIG",
{ font: "bold 12pt sans-serif" })
),
$("CheckBox", "choice6",
{
"Button.width": 20, "Button.height": 20,
"ButtonBorder.figure": "Circle", "ButtonBorder.stroke": "blue",
"ButtonIcon.figure": "Circle", "ButtonIcon.fill": "blue",
"ButtonIcon.strokeWidth": 0, "ButtonIcon.desiredSize": new go.Size(10, 10)
},
$(go.TextBlock, "blue circle")
),
$("CheckBox", "choice7", go.Panel.Vertical,
$(go.TextBlock, "vertical")
)
),
// the second column is a list of CheckBoxes
$(go.Panel, "Table",
{
row: 1, column: 1,
alignment: go.Spot.Top,
minSize: new go.Size(50, NaN),
itemTemplate:
$("CheckBox", "checked", go.Panel.TableRow,
$(go.TextBlock, // align text towards the right, next to the Button
{ column: 0, alignment: go.Spot.Right },
new go.Binding("text", "name")),
{ "Button.column": 1 } // put Button in second column, to the right of text
)
},
new go.Binding("itemArray", "items")
),
// now a checkbox at the bottom of the whole table
$("CheckBox", "", // not data bound
{ row: 3, columnSpan: 2, alignment: go.Spot.Left },
// this checkbox is not bound to model data, but it does toggle the Part.movable
// property of the Node that this is in
$(go.TextBlock, "Node is not movable"),
{ // _doClick is executed within a transaction by the CheckBoxButton click function
"_doClick": function(e, obj) {
obj.part.movable = !obj.part.movable; // toggle the Part.movable flag
}
}
)
)
);
// but use the default Link template, by not setting Diagram.linkTemplate
// create the model data that will be represented by Nodes and Links
myDiagram.model =
$(go.GraphLinksModel,
{
copiesArrays: true,
copiesArrayObjects: true,
"Changed": function(e) {
if (e.isTransactionFinished) document.getElementById("mySavedModel").textContent = myDiagram.model.toJson();
},
nodeDataArray:
[
{
key: "Alpha", color: "lightblue", choice1: true, choice2: true, choice3: true, choice4: true, choice5: true, choice6: true, choice7: true,
items: [{ name: "item 0" },
{ name: "item 1" },
{ name: "item 2" }]
},
{
key: "Beta", color: "orange",
items: [{ name: "B1", checked: false },
{ name: "Bee2", checked: true }]
},
{
key: "Gamma", color: "lightgreen",
items: [{ name: "C-one", checked: true },
{ name: "C-two", checked: true },
{ name: "C-three" }]
},
{
key: "Delta", color: "pink", choice1: true, choice2: false,
items: []
}
],
linkDataArray:
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Gamma", to: "Delta" },
{ from: "Delta", to: "Alpha" }
]
});
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:500px; height:500px"></div>
<p>
Various uses of CheckBoxes. These are predefined in the library.
You can see how they are defined in <a href="Buttons.js">Buttons.js</a>.
</p>
<textarea id="mySavedModel" style="width:100%;height:300px"></textarea>
</div>
</body>
</html>
+218
View File
@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html>
<head>
<title>Resizing Rows and Columns in a Table Panel</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Using the RowResizingTool and ColumnResizingTool to allow the user to change the size of rows and columns in a Table Panel." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="ColumnResizingTool.js"></script>
<script src="RowResizingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
validCycle: go.Diagram.CycleNotDirected, // don't allow loops
"undoManager.isEnabled": true
});
myDiagram.toolManager.mouseDownTools.add(new RowResizingTool());
myDiagram.toolManager.mouseDownTools.add(new ColumnResizingTool());
// This template is a Panel that is used to represent each item in a Panel.itemArray.
// The Panel is data bound to the item object.
var fieldTemplate =
$(go.Panel, "TableRow", // this Panel is a row in the containing Table
new go.Binding("portId", "name"), // this Panel is a "port"
{
background: "transparent", // so this port's background can be picked by the mouse
fromSpot: go.Spot.Right, // links only go from the right side to the left side
toSpot: go.Spot.Left,
// allow drawing links from or to this port:
fromLinkable: true, toLinkable: true
},
$(go.Shape,
{
column: 0,
width: 12, height: 12, margin: 4,
// but disallow drawing links from or to this shape:
fromLinkable: false, toLinkable: false
},
new go.Binding("figure", "figure"),
new go.Binding("fill", "color")),
$(go.TextBlock,
{
column: 1,
margin: new go.Margin(0, 2),
stretch: go.GraphObject.Horizontal,
font: "bold 13px sans-serif",
wrap: go.TextBlock.None,
overflow: go.TextBlock.OverflowEllipsis,
// and disallow drawing links from or to this text:
fromLinkable: false, toLinkable: false
},
new go.Binding("text", "name")),
$(go.TextBlock,
{
column: 2,
margin: new go.Margin(0, 2),
stretch: go.GraphObject.Horizontal,
font: "13px sans-serif",
maxLines: 3,
overflow: go.TextBlock.OverflowEllipsis,
editable: true
},
new go.Binding("text", "info").makeTwoWay())
);
// Return initialization for a RowColumnDefinition, specifying a particular column
// and adding a Binding of RowColumnDefinition.width to the IDX'th number in the data.widths Array
function makeWidthBinding(idx) {
// These two conversion functions are closed over the IDX variable.
// This source-to-target conversion extracts a number from the Array at the given index.
function getColumnWidth(arr) {
if (Array.isArray(arr) && idx < arr.length) return arr[idx];
return NaN;
}
// This target-to-source conversion sets a number in the Array at the given index.
function setColumnWidth(w, data) {
var arr = data.widths;
if (!arr) arr = [];
if (idx >= arr.length) {
for (var i = arr.length; i <= idx; i++) arr[i] = NaN; // default to NaN
}
arr[idx] = w;
return arr; // need to return the Array (as the value of data.widths)
}
return [
{ column: idx },
new go.Binding("width", "widths", getColumnWidth).makeTwoWay(setColumnWidth)
]
}
// This template represents a whole "record".
myDiagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// this rectangular shape surrounds the content of the node
$(go.Shape,
{ fill: "#EEEEEE" }),
// the content consists of a header and a list of items
$(go.Panel, "Vertical",
{ stretch: go.GraphObject.Horizontal, alignment: go.Spot.TopLeft },
// this is the header for the whole node
$(go.Panel, "Auto",
{ stretch: go.GraphObject.Horizontal }, // as wide as the whole node
$(go.Shape,
{ fill: "#1570A6", stroke: null }),
$(go.TextBlock,
{
alignment: go.Spot.Center,
margin: 3,
stroke: "white",
textAlign: "center",
font: "bold 12pt sans-serif"
},
new go.Binding("text", "key"))),
// this Panel holds a Panel for each item object in the itemArray;
// each item Panel is defined by the itemTemplate to be a TableRow in this Table
$(go.Panel, "Table",
{
name: "TABLE", stretch: go.GraphObject.Horizontal,
minSize: new go.Size(100, 10),
defaultAlignment: go.Spot.Left,
defaultStretch: go.GraphObject.Horizontal,
defaultColumnSeparatorStroke: "gray",
defaultRowSeparatorStroke: "gray",
itemTemplate: fieldTemplate
},
$(go.RowColumnDefinition, makeWidthBinding(0)),
$(go.RowColumnDefinition, makeWidthBinding(1)),
$(go.RowColumnDefinition, makeWidthBinding(2)),
new go.Binding("itemArray", "fields")
) // end Table Panel of items
) // end Vertical Panel
); // end Node
myDiagram.linkTemplate =
$(go.Link,
{ relinkableFrom: true, relinkableTo: true, toShortLength: 4 }, // let user reconnect links
$(go.Shape, { strokeWidth: 1.5 }),
$(go.Shape, { toArrow: "Standard", stroke: null })
);
myDiagram.model =
$(go.GraphLinksModel,
{
copiesArrays: true,
copiesArrayObjects: true,
linkFromPortIdProperty: "fromPort",
linkToPortIdProperty: "toPort",
// automatically update the model that is shown on this page
"Changed": function(e) {
if (e.isTransactionFinished) showModel();
},
nodeDataArray: [
{
key: "Record1",
widths: [NaN, NaN, 60],
fields: [
{ name: "field1", info: "first field", color: "#F7B84B", figure: "Ellipse" },
{ name: "field2", info: "the second one", color: "#F25022", figure: "Ellipse" },
{ name: "fieldThree", info: "3rd", color: "#00BCF2" }
],
loc: "0 0"
},
{
key: "Record2",
widths: [NaN, NaN, NaN],
fields: [
{ name: "fieldA", info: "", color: "#FFB900", figure: "Diamond" },
{ name: "fieldB", info: "", color: "#F25022", figure: "Rectangle" },
{ name: "fieldC", info: "", color: "#7FBA00", figure: "Diamond" },
{ name: "fieldD", info: "fourth", color: "#00BCF2", figure: "Rectangle" }
],
loc: "250 0"
}
],
linkDataArray: [
{ from: "Record1", fromPort: "field1", to: "Record2", toPort: "fieldA" },
{ from: "Record1", fromPort: "field2", to: "Record2", toPort: "fieldD" },
{ from: "Record1", fromPort: "fieldThree", to: "Record2", toPort: "fieldB" }
]
});
showModel(); // show the diagram's initial model
function showModel() {
document.getElementById("mySavedModel").textContent = myDiagram.model.toJson();
}
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<p>
This makes use of two tools, defined in their own files: <a href="ColumnResizingTool.js">ColumnResizingTool.js</a> and <a href="RowResizingTool.js">RowResizingTool.js</a>.
Each tool adds an <a>Adornment</a> to a selected node that has a resize handle for each column or each row of a "Table" <a>Panel</a>.
While resizing, you can press the Tab or the Delete key in order to stop the tool and restore the column or row to its natural size.
</p>
<p>
This sample also adds TwoWay Bindings to the <a>RowColumnDefinition.width</a> property for the columns.
Each column width is stored in the corresponding index of the node data's "widths" property, which must be an Array of numbers.
The default value is NaN, allowing the column to occupy its natural width.
Note that there are <b>no</b> Bindings for the row heights.
</p>
<p>The model data, automatically updated after each change or undo or redo:</p>
<textarea id="mySavedModel" style="width:100%;height:300px"></textarea>
<p>See also the <a href="../samples/addRemoveColumns.html">Add & Remove Rows & Columns</a> sample.</p>
</div>
</body>
</html>
+301
View File
@@ -0,0 +1,301 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for resizing each column of a named Table Panel in a selected Part.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
*/
function ColumnResizingTool() {
go.Tool.call(this);
this.name = "ColumnResizing";
var h = new go.Shape();
h.geometryString = "M0 0 V14 M2 0 V14";
h.desiredSize = new go.Size(2, 14);
h.cursor = "col-resize";
h.geometryStretch = go.GraphObject.None;
h.background = "rgba(255,255,255,0.5)";
h.stroke = "rgba(30,144,255,0.5)";
/** @type {GraphObject} */
this._handleArchetype = h;
/** @type {string} */
this._tableName = "TABLE";
// internal state
/** @type {GraphObject} */
this._handle = null;
/** @type {Panel} */
this._adornedTable = null;
}
go.Diagram.inherit(ColumnResizingTool, go.Tool);
/*
* A small GraphObject used as a resize handle for each column.
* This tool expects that this object's {@link GraphObject#desiredSize} (a.k.a width and height) has been set to real numbers.
* @name ColumnResizingTool#handleArchetype
* @return {GraphObject}
*/
Object.defineProperty(ColumnResizingTool.prototype, "handleArchetype", {
get: function() { return this._handleArchetype; },
set: function(value) { this._handleArchetype = value; }
});
/*
* The name of the Table Panel to be resized, by default the name "TABLE".
* @name ColumnResizingTool#tableName
* @return {string}
*/
Object.defineProperty(ColumnResizingTool.prototype, "tableName", {
get: function() { return this._tableName; },
set: function(value) { this._tableName = value; }
});
/*
* This read-only property returns the {@link GraphObject} that is the tool handle being dragged by the user.
* This will be contained by an {@link Adornment} whose category is "ColumnResizing".
* Its {@link Adornment#adornedObject} is the same as the {@link #adornedTable}.
* @name ColumnResizingTool#handle
* @return {GraphObject}
*/
Object.defineProperty(ColumnResizingTool.prototype, "handle", {
get: function() { return this._handle; }
});
/*
* Gets the {@link Panel} of type {@link Panel#Table} whose columns may be resized.
* This must be contained within the selected Part.
* @name ColumnResizingTool#adornedTable
* @return {Panel}
*/
Object.defineProperty(ColumnResizingTool.prototype, "adornedTable", {
get: function() { return this._adornedTable; }
});
/**
* Show an {@link Adornment} with a resize handle at each column.
* Don't show anything if {@link #tableName} doesn't identify a {@link Panel}
* that has a {@link Panel#type} of type {@link Panel#Table}.
* @this {ColumnResizingTool}
* @param {Part} part the part.
*/
ColumnResizingTool.prototype.updateAdornments = function(part) {
if (part === null || part instanceof go.Link) return; // this tool never applies to Links
if (part.isSelected && !this.diagram.isReadOnly) {
var selelt = part.findObject(this.tableName);
if (selelt instanceof go.Panel && selelt.actualBounds.isReal() && selelt.isVisibleObject() &&
part.actualBounds.isReal() && part.isVisible() &&
selelt.type === go.Panel.Table) {
var table = selelt;
var adornment = part.findAdornment(this.name);
if (adornment === null) {
adornment = this.makeAdornment(table);
part.addAdornment(this.name, adornment);
}
if (adornment !== null) {
var pad = table.padding;
var numcols = table.columnCount;
// update the position/alignment of each handle
adornment.elements.each(function(h) {
if (!h.pickable) return;
var coldef = table.getColumnDefinition(h.column);
var wid = coldef.actual;
if (wid > 0) wid = coldef.total;
var sep = 0;
// find next non-zero-width column's separatorStrokeWidth
var idx = h.column + 1;
while (idx < numcols && table.getColumnDefinition(idx).actual === 0) idx++;
if (idx < numcols) {
sep = table.getColumnDefinition(idx).separatorStrokeWidth;
if (isNaN(sep)) sep = table.defaultColumnSeparatorStrokeWidth;
}
h.alignment = new go.Spot(0, 0, pad.left + coldef.position + wid + sep/2, pad.top + h.height/2);
});
adornment.locationObject.desiredSize = table.actualBounds.size;
adornment.location = table.getDocumentPoint(adornment.locationSpot);
adornment.angle = table.getDocumentAngle();
return;
}
}
}
part.removeAdornment(this.name);
};
/*
* @this {ColumnResizingTool}
* @param {Panel} table the Table Panel whose columns may be resized
* @return {Adornment}
*/
ColumnResizingTool.prototype.makeAdornment = function(table) {
// the Adornment is a Spot Panel holding resize handles
var adornment = new go.Adornment();
adornment.category = this.name;
adornment.adornedObject = table;
adornment.type = go.Panel.Spot;
adornment.locationObjectName = "BLOCK";
// create the "main" element of the Spot Panel
var block = new go.TextBlock(); // doesn't matter much what this is
block.name = "BLOCK";
block.pickable = false; // it's transparent and not pickable
adornment.add(block);
// now add resize handles for each column
for (var i = 0; i < table.columnCount; i++) {
var coldef = table.getColumnDefinition(i);
adornment.add(this.makeHandle(table, coldef));
}
return adornment;
};
/*
* @this {ColumnResizingTool}
* @param {Panel} table the Table Panel whose columns may be resized
* @param {RowColumnDefinition} coldef the column definition to be resized
* @return a copy of the {@link #handleArchetype}
*/
ColumnResizingTool.prototype.makeHandle = function(table, coldef) {
var h = this.handleArchetype;
if (h === null) return null;
var c = h.copy();
c.column = coldef.index;
return c;
};
/*
* This predicate is true when there is a resize handle at the mouse down point.
* @this {ColumnResizingTool}
* @return {boolean}
*/
ColumnResizingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly) return false;
if (!diagram.lastInput.left) return false;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
return (h !== null);
};
/**
* @this {ColumnResizingTool}
*/
ColumnResizingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
if (this.handle === null) return;
var panel = this.handle.part.adornedObject;
if (!panel || panel.type !== go.Panel.Table) return;
this._adornedTable = panel;
diagram.isMouseCaptured = true;
this.startTransaction(this.name);
this.isActive = true;
};
/**
* @this {ColumnResizingTool}
*/
ColumnResizingTool.prototype.doDeactivate = function() {
this.stopTransaction();
this._handle = null;
this._adornedTable = null;
var diagram = this.diagram;
if (diagram !== null) diagram.isMouseCaptured = false;
this.isActive = false;
};
/**
* @this {ColumnResizingTool}
*/
ColumnResizingTool.prototype.doMouseMove = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newpt = this.computeResize(diagram.lastInput.documentPoint);
this.resize(newpt);
}
};
/**
* @this {ColumnResizingTool}
*/
ColumnResizingTool.prototype.doMouseUp = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newpt = this.computeResize(diagram.lastInput.documentPoint);
this.resize(newpt);
this.transactionResult = this.name; // success
}
this.stopTool();
};
/**
* This should change the {@link RowColumnDefinition#width} of the column being resized
* to a value corresponding to the given mouse point.
* @expose
* @this {ColumnResizingTool}
* @param {Point} newPoint the value of the call to {@link #computeResize}.
*/
ColumnResizingTool.prototype.resize = function(newPoint) {
var table = this.adornedTable;
var pad = table.padding;
var numcols = table.columnCount;
var locpt = table.getLocalPoint(newPoint);
var h = this.handle;
var coldef = table.getColumnDefinition(h.column);
var sep = 0;
var idx = h.column + 1;
while (idx < numcols && table.getColumnDefinition(idx).actual === 0) idx++;
if (idx < numcols) {
sep = table.getColumnDefinition(idx).separatorStrokeWidth;
if (isNaN(sep)) sep = table.defaultColumnSeparatorStrokeWidth;
}
coldef.width = Math.max(0, locpt.x - pad.left - coldef.position - (coldef.total - coldef.actual) - sep/2);
};
/**
* This can be overridden in order to customize the resizing process.
* @expose
* @this {ColumnResizingTool}
* @param {Point} p the point where the handle is being dragged.
* @return {Point}
*/
ColumnResizingTool.prototype.computeResize = function(p) {
return p;
};
/**
* Pressing the Delete key removes any column width setting and stops this tool.
* @this {ColumnResizingTool}
*/
ColumnResizingTool.prototype.doKeyDown = function() {
if (!this.isActive) return;
var e = this.diagram.lastInput;
if (e.key === 'Del' || e.key === '\t') { // remove width setting
var coldef = this.adornedTable.getColumnDefinition(this.handle.column);
coldef.width = NaN;
this.transactionResult = this.name; // success
this.stopTool();
} else {
go.Tool.prototype.doKeyDown.call(this);
}
};
+212
View File
@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html>
<head>
<title>State Chart with Simple Curved Link Reshaping</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Changing the curviness of a link using a single reshaping handle." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- /* Copyright 1998-2020 by Northwoods Software Corporation. */ -->
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="CurvedLinkReshapingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // must name or refer to the DIV HTML element
{
// have mouse wheel events zoom in and out instead of scroll up and down
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,
// support double-click in background creating a new node
"clickCreatingTool.archetypeNodeData": { text: "new node" },
"linkReshapingTool": new CurvedLinkReshapingTool(),
// enable undo & redo
"undoManager.isEnabled": true
});
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("SaveButton");
if (button) button.disabled = !myDiagram.isModified;
var idx = document.title.indexOf("*");
if (myDiagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
// define the Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// define the node's outer shape, which will surround the TextBlock
$(go.Shape, "RoundedRectangle",
{
parameter1: 20, // the corner has a large radius
fill: $(go.Brush, "Linear", { 0: "rgb(254, 201, 0)", 1: "rgb(254, 162, 0)" }),
stroke: "black",
portId: "",
fromLinkable: true,
fromLinkableSelfNode: true,
fromLinkableDuplicates: true,
toLinkable: true,
toLinkableSelfNode: true,
toLinkableDuplicates: true,
cursor: "pointer"
}),
$(go.TextBlock,
{
font: "bold 11pt helvetica, bold arial, sans-serif",
editable: true // editing the text automatically updates the model data
},
new go.Binding("text", "text").makeTwoWay())
);
// unlike the normal selection Adornment, this one includes a Button
myDiagram.nodeTemplate.selectionAdornmentTemplate =
$(go.Adornment, "Spot",
$(go.Panel, "Auto",
$(go.Shape, { fill: null, stroke: "blue", strokeWidth: 2 }),
$(go.Placeholder) // this represents the selected Node
),
// the button to create a "next" node, at the top-right corner
$("Button",
{
alignment: go.Spot.TopRight,
click: addNodeAndLink // this function is defined below
},
$(go.Shape, "PlusLine", { desiredSize: new go.Size(6, 6) })
) // end button
); // end Adornment
// clicking the button inserts a new node to the right of the selected node,
// and adds a link to that new node
function addNodeAndLink(e, obj) {
var adorn = obj.part;
e.handled = true;
var diagram = adorn.diagram;
diagram.startTransaction("Add State");
// get the node data for which the user clicked the button
var fromNode = adorn.adornedPart;
var fromData = fromNode.data;
// create a new "State" data object, positioned off to the right of the adorned Node
var toData = { text: "new" };
var p = fromNode.location.copy();
p.x += 200;
toData.loc = go.Point.stringify(p); // the "loc" property is a string, not a Point object
// add the new node data to the model
var model = diagram.model;
model.addNodeData(toData);
// create a link data from the old node data to the new node data
var linkdata = {
from: model.getKeyForNodeData(fromData), // or just: fromData.id
to: model.getKeyForNodeData(toData),
text: "transition"
};
// and add the link data to the model
model.addLinkData(linkdata);
// select the new Node
var newnode = diagram.findNodeForData(toData);
diagram.select(newnode);
diagram.commitTransaction("Add State");
// if the new node is off-screen, scroll the diagram to show the new node
diagram.scrollToRect(newnode.actualBounds);
}
// replace the default Link template in the linkTemplateMap
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{ curve: go.Link.Bezier, reshapable: true },
// don't need to save Link.points, so don't need TwoWay Binding on "points"
new go.Binding("curviness", "curviness").makeTwoWay(), // but save "curviness" automatically
$(go.Shape, // the link shape
{ strokeWidth: 1.5 }),
$(go.Shape, // the arrowhead
{ toArrow: "standard", stroke: null }),
$(go.Panel, "Auto",
$(go.Shape, // the label background, which becomes transparent around the edges
{
fill: $(go.Brush, "Radial",
{ 0: "rgb(240, 240, 240)", 0.3: "rgb(240, 240, 240)", 1: "rgba(240, 240, 240, 0)" }),
stroke: null
}),
$(go.TextBlock, "transition", // the label text
{
textAlign: "center",
font: "10pt helvetica, arial, sans-serif",
stroke: "black",
margin: 4,
editable: true // editing the text automatically updates the model data
},
new go.Binding("text", "text").makeTwoWay())
)
);
// read in the JSON-format data from the "mySavedModel" element
load();
}
// Show the diagram's model in JSON format
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: whitesmoke; border: solid 1px black; width: 100%; height: 400px"></div>
<p>
This sample is a modification of the <a href="../samples/stateChart.html">State Chart</a> sample
that makes use of the CurvedLinkReshapingTool that is defined in its own file, as <a href="CurvedLinkReshapingTool.js">CurvedLinkReshapingTool.js</a>.
</p>
<p>
Note that unlike the standard case of a Bezier-curved Link that is <a>Part.reshapable</a>, there is only one reshape handle
When the user drags that handle, the value of <a>Link.curviness</a> is modified, causing the link to be curved differently.
This sample also defines a TwoWay <a>Binding</a> on that property, thereby saving the curviness to the model data.
Unlike the regular State Chart sample, there is no Binding on <a>Link.points</a>, which is no longer needed when the curviness is the only modified property.
</p>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
Diagram Model saved in JSON format:
<br />
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"nodeKeyProperty": "id",
"nodeDataArray": [
{ "id": 0, "loc": "120 120", "text": "Initial" },
{ "id": 1, "loc": "330 120", "text": "First down" },
{ "id": 2, "loc": "226 376", "text": "First up" },
{ "id": 3, "loc": "60 276", "text": "Second down" },
{ "id": 4, "loc": "226 226", "text": "Wait" }
],
"linkDataArray": [
{ "from": 0, "to": 0, "text": "up or timer", "curviness": -20 },
{ "from": 0, "to": 1, "text": "down", "curviness": 20 },
{ "from": 1, "to": 0, "text": "up (moved)\nPOST", "curviness": 20 },
{ "from": 1, "to": 1, "text": "down", "curviness": -20 },
{ "from": 1, "to": 2, "text": "up (no move)" },
{ "from": 1, "to": 4, "text": "timer" },
{ "from": 2, "to": 0, "text": "timer\nPOST" },
{ "from": 2, "to": 3, "text": "down" },
{ "from": 3, "to": 0, "text": "up\nPOST\n(dblclick\nif no move)" },
{ "from": 3, "to": 3, "text": "down or timer", "curviness": 20 },
{ "from": 4, "to": 0, "text": "up\nPOST" },
{ "from": 4, "to": 4, "text": "down" }
]
}
</textarea></div>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom LinkReshapingTool that shows only a single reshape handle on a Bezier curved Link.
// Dragging that handle changes the value of {@link Link#curviness}.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends LinkReshapingTool
* @class
* This CurvedLinkReshapingTool class allows for a Link's path to be modified by the user
* via the dragging of a single tool handle at the middle of the link.
*/
function CurvedLinkReshapingTool() {
go.LinkReshapingTool.call(this);
/** @type {number} */
this._originalCurviness = NaN;
}
go.Diagram.inherit(CurvedLinkReshapingTool, go.LinkReshapingTool);
/**
* @this {CurvedLinkReshapingTool}
* @param {Shape} pathshape
* @return {Adornment}
*/
CurvedLinkReshapingTool.prototype.makeAdornment = function(pathshape) {
var link = pathshape.part;
if (link !== null && link.curve === go.Link.Bezier && link.pointsCount === 4) {
var adornment = new go.Adornment();
adornment.type = go.Panel.Link;
var h = this.makeHandle();
this.setReshapingBehavior(h, go.LinkReshapingTool.All);
h.cursor = 'move';
adornment.add(h);
adornment.category = this.name;
adornment.adornedObject = pathshape;
return adornment;
} else {
return go.LinkReshapingTool.prototype.makeAdornment.call(this, pathshape);
}
};
/**
* @this {CurvedLinkReshapingTool}
*/
CurvedLinkReshapingTool.prototype.doActivate = function() {
go.LinkReshapingTool.prototype.doActivate.call(this);
this._originalCurviness = this.adornedLink.curviness;
};
/**
* @this {CurvedLinkReshapingTool}
*/
CurvedLinkReshapingTool.prototype.doCancel = function() {
this.adornedLink.curviness = this._originalCurviness;
go.LinkReshapingTool.prototype.doCancel.call(this);
};
/**
* @this {CurvedLinkReshapingTool}
* @param {Point} newpt
* @return {Point}
*/
CurvedLinkReshapingTool.prototype.reshape = function(newpt) {
var link = this.adornedLink;
if (link !== null && link.curve === go.Link.Bezier && link.pointsCount === 4) {
var start = link.getPoint(0);
var end = link.getPoint(3);
var ang = start.directionPoint(end);
var mid = new go.Point((start.x + end.x) / 2, (start.y + end.y) / 2);
var a = new go.Point(9999, 0).rotate(ang + 90).add(mid);
var b = new go.Point(9999, 0).rotate(ang - 90).add(mid);
var q = newpt.copy().projectOntoLineSegmentPoint(a, b);
var curviness = Math.sqrt(mid.distanceSquaredPoint(q));
if (link.fromPort === link.toPort) {
if (newpt.y < link.fromPort.getDocumentPoint(go.Spot.Center).y) curviness = -curviness;
} else {
var diff = mid.directionPoint(q) - ang;
if ((diff > 0 && diff < 180) || (diff < -180)) curviness = -curviness;
}
link.curviness = curviness;
return q;
} else {
go.LinkReshapingTool.prototype.reshape.call(this, newpt);
}
}
+53
View File
@@ -0,0 +1,53 @@
/*
Default CSS for the Data inspector
see also: DataInspector.js, DataInspector.html
*/
/*
Grey color palette
https://www.google.com/design/spec/style/color.html
/* #FAFAFA; /* Grey 50 */
/* #F5F5F5; /* Grey 100 */
/* #EEEEEE; /* Grey 200 */
/* #E0E0E0; /* Grey 300 */
/* #BDBDBD; /* Grey 400 */
/* #9E9E9E; /* Grey 500 */
/* #757575; /* Grey 600 */
/* #616161; /* Grey 700 */
/* #424242; /* Grey 800 */
/* #212121; /* Grey 900 */
.inspector {
display: inline-block;
font: bold 14px helvetica, sans-serif;
background-color: #212121; /* Grey 900 */
color: #F5F5F5; /* Grey 100 */
cursor: default;
}
.inspector table {
border-collapse: separate;
border-spacing: 2px;
}
.inspector td, th {
padding: 2px;
}
.inspector input {
background-color: #424242; /* Grey 800 */
color: #F5F5F5; /* Grey 100 */
font: bold 12px helvetica, sans-serif;
border: 0px;
padding: 2px;
}
.inspector input:disabled {
background-color: #BDBDBD; /* Grey 400 */
color: #616161; /* Grey 700 */
}
.inspector select {
background-color: #424242;
}
+222
View File
@@ -0,0 +1,222 @@
<!DOCTYPE html>
<html>
<head>
<title>Data Inspector</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="An HTML panel that displays the properties of some model data and allows the user to edit their values." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<link rel='stylesheet' href='DataInspector.css' />
<script src="DataInspector.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
"animationManager.isEnabled": false,
// allow double-click in background to create a new node
"clickCreatingTool.archetypeNodeData": { text: "Node", color: "white" },
// allow Ctrl-G to call groupSelection()
"commandHandler.archetypeGroupData": { text: "Group", isGroup: true, color: "blue" },
// enable undo & redo
"undoManager.isEnabled": true,
// automatically show the state of the diagram's model on the page
"ModelChanged": function(e) {
if (e.isTransactionFinished) {
document.getElementById("savedModel").textContent = myDiagram.model.toJson();
}
}
});
// These nodes have text surrounded by a rounded rectangle
// whose fill color is bound to the node data.
// The user can drag a node by dragging its TextBlock label.
// Dragging from the Shape will start drawing a new link.
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{ locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape, "Rectangle",
{
stroke: null, strokeWidth: 0,
fill: "white", // the default fill, if there is no data-binding
portId: "", cursor: "pointer", // the Shape is the port, not the whole Node
// allow all kinds of links from and to this port
fromLinkable: true, fromLinkableSelfNode: true, fromLinkableDuplicates: true,
toLinkable: true, toLinkableSelfNode: true, toLinkableDuplicates: true
},
new go.Binding("fill", "color")),
$(go.TextBlock,
{
font: "bold 18px sans-serif",
stroke: '#111',
margin: 8, // make some extra space for the shape around the text
isMultiline: false, // don't allow newlines in text
editable: true // allow in-place editing by user
},
new go.Binding("text", "text").makeTwoWay())
);
// The link shape and arrowhead have their stroke brush data bound to the "color" property
myDiagram.linkTemplate =
$(go.Link,
{ toShortLength: 3, relinkableFrom: true, relinkableTo: true }, // allow the user to relink existing links
$(go.Shape,
{ strokeWidth: 2 },
new go.Binding("stroke", "color")),
$(go.Shape,
{ toArrow: "Standard", stroke: null },
new go.Binding("fill", "color"))
);
// Groups consist of a title in the color given by the group node data
// above a translucent gray rectangle surrounding the member parts
myDiagram.groupTemplate =
$(go.Group, "Vertical",
{
selectionObjectName: "PANEL", // selection handle goes around shape, not label
ungroupable: true
}, // enable Ctrl-Shift-G to ungroup a selected Group
$(go.TextBlock,
{
font: "bold 19px sans-serif",
isMultiline: false, // don't allow newlines in text
editable: true // allow in-place editing by user
},
new go.Binding("text", "text").makeTwoWay(),
new go.Binding("stroke", "color")),
$(go.Panel, "Auto",
{ name: "PANEL" },
$(go.Shape, "Rectangle", // the rectangular shape around the members
{ fill: "rgba(128,128,128,0.2)", stroke: "gray", strokeWidth: 3 }),
$(go.Placeholder, { padding: 10 }) // represents where the members are
)
);
// Create the Diagram's Model:
var nodeDataArray = [
{ key: 1, text: "Alpha", color: "#B2DFDB", state: "one" },
{ key: 2, text: "Beta", color: "#B2B2DB", state: "two", password: "1234" },
{ key: 3, text: "Gamma", color: "#1DE9B6", state: 2, group: 5, flag: false, choices: [1, 2, 3, 4, 5] },
{ key: 4, text: "Delta", color: "#00BFA5", state: "three", group: 5, flag: true },
{ key: 5, text: "Epsilon", color: "#00BFA5", isGroup: true }
];
var linkDataArray = [
{ from: 1, to: 2, color: "#5E35B1" },
{ from: 2, to: 2, color: "#5E35B1" },
{ from: 3, to: 4, color: "#6200EA" },
{ from: 3, to: 1, color: "#6200EA" }
];
myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
// some shared model data
myDiagram.model.modelData = { test: true, hello: "world", version: 42 };
// select a Node, so that the first Inspector shows something
myDiagram.select(myDiagram.nodes.first());
// Declare which properties to show and how.
// By default, all properties on the model data objects are shown unless the inspector option "includesOwnProperties" is set to false.
// Show the primary selection's data, or blanks if no Part is selected:
var inspector = new Inspector('myInspectorDiv', myDiagram,
{
// allows for multiple nodes to be inspected at once
multipleSelection: true,
// max number of node properties will be shown when multiple selection is true
showSize: 4,
// when multipleSelection is true, when showAllProperties is true it takes the union of properties
// otherwise it takes the intersection of properties
showAllProperties: true,
// uncomment this line to only inspect the named properties below instead of all properties on each object:
// includesOwnProperties: false,
properties: {
"text": {},
// key would be automatically added for nodes, but we want to declare it read-only also:
"key": { readOnly: true, show: Inspector.showIfPresent },
// color would be automatically added for nodes, but we want to declare it a color also:
"color": { show: Inspector.showIfPresent, type: 'color' },
// Comments and LinkComments are not in any node or link data (yet), so we add them here:
"Comments": { show: Inspector.showIfNode },
"LinkComments": { show: Inspector.showIfLink },
"isGroup": { readOnly: true, show: Inspector.showIfPresent },
"flag": { show: Inspector.showIfNode, type: 'checkbox' },
"state": {
show: Inspector.showIfNode,
type: "select",
choices: function(node, propName) {
if (Array.isArray(node.data.choices)) return node.data.choices;
return ["one", "two", "three", "four", "five"];
}
},
"choices": { show: false }, // must not be shown at all
// an example of specifying the <input> type
"password": { show: Inspector.showIfPresent, type: 'password' }
}
});
// Always show the first Node:
var inspector2 = new Inspector('myInspectorDiv2', myDiagram,
{
// By default the inspector works on the Diagram selection.
// This property lets us inspect a specific object by calling Inspector.inspectObject.
inspectSelection: false,
properties: {
"text": {},
// This property we want to declare as a color, to show a color-picker:
"color": { type: 'color' },
// key would be automatically added for node data, but we want to declare it read-only also:
"key": { readOnly: true, show: Inspector.showIfPresent }
}
});
// If not inspecting a selection, you can programatically decide what to inspect (a Part, or a JavaScript object)
inspector2.inspectObject(myDiagram.nodes.first().data);
// Always show the model.modelData:
var inspector3 = new Inspector('myInspectorDiv3', myDiagram,
{
inspectSelection: false
});
inspector3.inspectObject(myDiagram.model.modelData);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<span style="display: inline-block; vertical-align: top;">
<div style="margin-left: 10px;">
<div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:400px;"></div>
</div>
</span>
<span style="display: inline-block; vertical-align: top;">
Selected Part:<br/>
<div id="myInspectorDiv" class="inspector"> </div><br/>
First Node's data:<br />
<div id="myInspectorDiv2" class="inspector"> </div><br />
Model.modelData:<br />
<div id="myInspectorDiv3" class="inspector"> </div><br />
</span>
<div>
<p>An HTML-based inspector that displays and allows editing of data for the selected Part (if any),
or for a particular JavaScript object, or for the shared <a>Model.modelData</a> object,
which exists even if there are no nodes or links.
<p>The inspector code lies in <a href="DataInspector.js">DataInspector.js</a> and <a href="DataInspector.css">DataInspector.css</a>. This code is meant to be a starting point for making your own model data inspector.
<p>On browsers that support it, color types display a color picker. There are various plugins and polyfills for this functionaltiy if you wish to extend the data inspector.
<p>
This shows the contents of the model after each transaction:
<pre id="savedModel" />
</div>
</div>
</body>
</html>
+719
View File
@@ -0,0 +1,719 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/**
This class implements an inspector for GoJS model data objects.
The constructor takes three arguments:
{string} divid a string referencing the HTML ID of the to-be inspector's div.
{Diagram} diagram a reference to a GoJS Diagram.
{Object} options An optional JS Object describing options for the inspector.
Options:
inspectSelection {boolean} Default true, whether to automatically show and populate the Inspector
with the currently selected Diagram Part. If set to false, the inspector won't show anything
until you call Inspector.inspectObject(object) with a Part or JavaScript object as the argument.
includesOwnProperties {boolean} Default true, whether to list all properties currently on the inspected data object.
properties {Object} An object of string:Object pairs representing propertyName:propertyOptions.
Can be used to include or exclude additional properties.
propertyModified function(propertyName, newValue) a callback
multipleSelection {boolean} Default false, whether to allow multiple selection and change the properties of all the selected instead of
the single first object
showAllProperties {boolean} Default false, whether properties that are shown with multipleSelection use the intersect of the properties when false or the union when true
only affects if multipleSelection is true
showSize {number} Defaults 0, shows how many nodes are showed when selecting multiple nodes
when its lower than 1, it shows all nodes
Options for properties:
show: {boolean|function} a boolean value to show or hide the property from the inspector, or a predicate function to show conditionally.
readOnly: {boolean|function} whether or not the property is read-only
type: {string} a string describing the data type. Supported values: "string|number|boolean|color|arrayofnumber|point|rect|size|spot|margin|select"
defaultValue: {*} a default value for the property. Defaults to the empty string.
choices: {Array|function} when type == "select", the Array of choices to use or a function that returns the Array of choices.
Example usage of Inspector:
var inspector = new Inspector("myInspector", myDiagram,
{
includesOwnProperties: false,
properties: {
"key": { show: Inspector.showIfPresent, readOnly: true },
"comments": { show: Inspector.showIfNode },
"LinkComments": { show: Inspector.showIfLink },
"chosen": { show: Inspector.showIfNode, type: "checkbox" },
"state": { show: Inspector.showIfNode, type: "select", choices: ["Stopped", "Parked", "Moving"] }
}
});
This is the basic HTML Structure that the Inspector creates within the given DIV element:
<div id="divid" class="inspector">
<tr>
<td>propertyName</td>
<td><input value=propertyValue /></td>
</tr>
...
</div>
*/
function Inspector(divid, diagram, options) {
var mainDiv = document.getElementById(divid);
mainDiv.className = "inspector";
mainDiv.innerHTML = "";
this._div = mainDiv;
this._diagram = diagram;
this._inspectedProperties = {};
this._multipleProperties = {};
// Either a GoJS Part or a simple data object, such as Model.modelData
this.inspectedObject = null;
// Inspector options defaults:
this.includesOwnProperties = true;
this.declaredProperties = {};
this.inspectsSelection = true;
this.propertyModified = null;
this.multipleSelection = false;
this.showAllProperties = false;
this.showSize = 0;
if (options !== undefined) {
if (options["includesOwnProperties"] !== undefined) this.includesOwnProperties = options["includesOwnProperties"];
if (options["properties"] !== undefined) this.declaredProperties = options["properties"];
if (options["inspectSelection"] !== undefined) this.inspectsSelection = options["inspectSelection"];
if (options["propertyModified"] !== undefined) this.propertyModified = options["propertyModified"];
if (options['multipleSelection'] !== undefined) this.multipleSelection = options['multipleSelection'];
if (options['showAllProperties'] !== undefined) this.showAllProperties = options['showAllProperties'];
if (options['showSize'] !== undefined) this.showSize = options['showSize'];
}
var self = this;
diagram.addModelChangedListener(function(e) {
if (e.isTransactionFinished) self.inspectObject();
});
if (this.inspectsSelection) {
diagram.addDiagramListener("ChangedSelection", function(e) { self.inspectObject(); });
}
}
// Some static predicates to use with the "show" property.
Inspector.showIfNode = function(part) { return part instanceof go.Node };
Inspector.showIfLink = function(part) { return part instanceof go.Link };
Inspector.showIfGroup = function(part) { return part instanceof go.Group };
// Only show the property if its present. Useful for "key" which will be shown on Nodes and Groups, but normally not on Links
Inspector.showIfPresent = function(data, propname) {
if (data instanceof go.Part) data = data.data;
return typeof data === "object" && data[propname] !== undefined;
};
/**
* Update the HTML state of this Inspector given the properties of the {@link #inspectedObject}.
* @param {Object} object is an optional argument, used when {@link #inspectSelection} is false to
* set {@link #inspectedObject} and show and edit that object's properties.
*/
Inspector.prototype.inspectObject = function(object) {
var inspectedObject = null;
var inspectedObjects = null;
if (object === null) return;
if (object === undefined) {
if (this.inspectsSelection) {
if (this.multipleSelection) { // gets the selection if multiple selection is true
inspectedObjects = this._diagram.selection;
} else { // otherwise grab the first object
inspectedObject = this._diagram.selection.first();
}
} else { // if there is a single inspected object
inspectedObject = this.inspectedObject;
}
} else { // if object was passed in as a parameter
inspectedObject = object;
}
if (inspectedObjects && inspectedObjects.count === 1) {
inspectedObject = inspectedObjects.first();
}
if (inspectedObjects && inspectedObjects.count <= 1) {
inspectedObjects = null;
}
// single object or no objects
if (!inspectedObjects || !this.multipleSelection) {
if (inspectedObject === null) {
this.inspectedObject = inspectedObject;
this.updateAllHTML();
return;
}
this.inspectedObject = inspectedObject;
if (this.inspectObject === null) return;
var mainDiv = this._div;
mainDiv.innerHTML = '';
// use either the Part.data or the object itself (for model.modelData)
var data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
if (!data) return;
// Build table:
var table = document.createElement('table');
var tbody = document.createElement('tbody');
this._inspectedProperties = {};
this.tabIndex = 0;
var declaredProperties = this.declaredProperties;
// Go through all the properties passed in to the inspector and show them, if appropriate:
for (var name in declaredProperties) {
var desc = declaredProperties[name];
if (!this.canShowProperty(name, desc, inspectedObject)) continue;
var val = this.findValue(name, desc, data);
tbody.appendChild(this.buildPropertyRow(name, val));
}
// Go through all the properties on the model data and show them, if appropriate:
if (this.includesOwnProperties) {
for (var k in data) {
if (k === '__gohashid') continue; // skip internal GoJS hash property
if (this._inspectedProperties[k]) continue; // already exists
if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject)) continue;
tbody.appendChild(this.buildPropertyRow(k, data[k]));
}
}
table.appendChild(tbody);
mainDiv.appendChild(table);
} else { // multiple objects selected
var mainDiv = this._div;
mainDiv.innerHTML = '';
var shared = new go.Map(); // for properties that the nodes have in common
var properties = new go.Map(); // for adding properties
var all = new go.Map(); // used later to prevent changing properties when unneeded
var it = inspectedObjects.iterator;
// Build table:
var table = document.createElement('table');
var tbody = document.createElement('tbody');
this._inspectedProperties = {};
this.tabIndex = 0;
var declaredProperties = this.declaredProperties;
it.next();
inspectedObject = it.value;
this.inspectedObject = inspectedObject;
var data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
if (data) { // initial pass to set shared and all
// Go through all the properties passed in to the inspector and add them to the map, if appropriate:
for (var name in declaredProperties) {
var desc = declaredProperties[name];
if (!this.canShowProperty(name, desc, inspectedObject)) continue;
var val = this.findValue(name, desc, data);
if (val === '' && desc && desc.type === 'checkbox') {
shared.add(name, false);
all.add(name, false);
} else {
shared.add(name, val);
all.add(name, val);
}
}
// Go through all the properties on the model data and add them to the map, if appropriate:
if (this.includesOwnProperties) {
for (var k in data) {
if (k === '__gohashid') continue; // skip internal GoJS hash property
if (this._inspectedProperties[k]) continue; // already exists
if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject)) continue;
shared.add(k, data[k]);
all.add(k, data[k]);
}
}
}
var nodecount = 2;
while (it.next() && (this.showSize < 1 || nodecount <= this.showSize)) { // grabs all the properties from the other selected objects
properties.clear();
inspectedObject = it.value;
if (inspectedObject) {
// use either the Part.data or the object itself (for model.modelData)
data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
if (data) {
// Go through all the properties passed in to the inspector and add them to properties to add, if appropriate:
for (var name in declaredProperties) {
var desc = declaredProperties[name];
if (!this.canShowProperty(name, desc, inspectedObject)) continue;
var val = this.findValue(name, desc, data);
if (val === '' && desc && desc.type === 'checkbox') {
properties.add(name, false);
} else {
properties.add(name, val);
}
}
// Go through all the properties on the model data and add them to properties to add, if appropriate:
if (this.includesOwnProperties) {
for (var k in data) {
if (k === '__gohashid') continue; // skip internal GoJS hash property
if (this._inspectedProperties[k]) continue; // already exists
if (declaredProperties[k] && !this.canShowProperty(k, declaredProperties[k], inspectedObject)) continue;
properties.add(k, data[k]);
}
}
}
}
if (!this.showAllProperties) {
// Cleans up shared map with properties that aren't shared between the selected objects
// Also adds properties to the add and shared maps if applicable
var addIt = shared.iterator;
var toRemove = [];
while (addIt.next()) {
if (properties.has(addIt.key)) {
var newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
all.set(addIt.key, newVal);
if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color'
&& declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !== 'select')
|| !declaredProperties[addIt.key]) { // for non-string properties i.e color
newVal = shared.get(addIt.key) + '|' + properties.get(addIt.key);
shared.set(addIt.key, newVal);
}
} else { // toRemove array since addIt is still iterating
toRemove.push(addIt.key);
}
}
for (var i = 0; i < toRemove.length; i++) { // removes anything that doesn't showAllPropertiess
shared.remove(toRemove[i]);
all.remove(toRemove[i]);
}
} else {
// Adds missing properties to all with the correct amount of seperators
var addIt = properties.iterator;
while (addIt.next()) {
if (all.has(addIt.key)) {
if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color'
&& declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !== 'select')
|| !declaredProperties[addIt.key]) { // for non-string properties i.e color
var newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
all.set(addIt.key, newVal);
}
} else {
var newVal = '';
for (var i = 0; i < nodecount - 1; i++) newVal += '|';
newVal += properties.get(addIt.key);
all.set(addIt.key, newVal);
}
}
// Adds bars in case properties is not in all
addIt = all.iterator;
while (addIt.next()) {
if (!properties.has(addIt.key)) {
if ((declaredProperties[addIt.key] && declaredProperties[addIt.key].type !== 'color'
&& declaredProperties[addIt.key].type !== 'checkbox' && declaredProperties[addIt.key].type !== 'select')
|| !declaredProperties[addIt.key]) { // for non-string properties i.e color
var newVal = all.get(addIt.key) + '|';
all.set(addIt.key, newVal);
}
}
}
}
nodecount++;
}
// builds the table property rows and sets multipleProperties to help with updateall
var mapIt;
if (!this.showAllProperties) mapIt = shared.iterator;
else mapIt = all.iterator;
while (mapIt.next()) {
tbody.appendChild(this.buildPropertyRow(mapIt.key, mapIt.value)); // shows the properties that are allowed
}
table.appendChild(tbody);
mainDiv.appendChild(table);
var allIt = all.iterator;
while (allIt.next()) {
this._multipleProperties[allIt.key] = allIt.value; // used for updateall to know which properties to change
}
}
};
/**
* @ignore
* This predicate should be false if the given property should not be shown.
* Normally it only checks the value of "show" on the property descriptor.
* The default value is true.
* @param {string} propertyName the property name
* @param {Object} propertyDesc the property descriptor
* @param {Object} inspectedObject the data object
* @return {boolean} whether a particular property should be shown in this Inspector
*/
Inspector.prototype.canShowProperty = function(propertyName, propertyDesc, inspectedObject) {
if (propertyDesc.show === false) return false;
// if "show" is a predicate, make sure it passes or do not show this property
if (typeof propertyDesc.show === "function") return propertyDesc.show(inspectedObject, propertyName);
return true;
}
/**
* @ignore
* This predicate should be false if the given property should not be editable by the user.
* Normally it only checks the value of "readOnly" on the property descriptor.
* The default value is true.
* @param {string} propertyName the property name
* @param {Object} propertyDesc the property descriptor
* @param {Object} inspectedObject the data object
* @return {boolean} whether a particular property should be shown in this Inspector
*/
Inspector.prototype.canEditProperty = function(propertyName, propertyDesc, inspectedObject) {
if (this._diagram.isReadOnly || this._diagram.isModelReadOnly) return false;
// assume property values that are functions of Objects cannot be edited
var data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
var valtype = typeof data[propertyName];
if (valtype === "function") return false;
if (propertyDesc) {
if (propertyDesc.readOnly === true) return false;
// if "readOnly" is a predicate, make sure it passes or do not show this property
if (typeof propertyDesc.readOnly === "function") return !propertyDesc.readOnly(inspectedObject, propertyName);
}
return true;
}
/**
* @ignore
* @param {any} propName
* @param {any} propDesc
* @param {any} data
* @return {any}
*/
Inspector.prototype.findValue = function(propName, propDesc, data) {
var val = '';
if (propDesc && propDesc.defaultValue !== undefined) val = propDesc.defaultValue;
if (data[propName] !== undefined) val = data[propName];
if (val === undefined) return '';
return val;
}
/**
* @ignore
* This sets this._inspectedProperties[propertyName] and creates the HTML table row:
* <tr>
* <td>propertyName</td>
* <td><input value=propertyValue /></td>
* </tr>
* @param {string} propertyName the property name
* @param {*} propertyValue the property value
* @return the table row
*/
Inspector.prototype.buildPropertyRow = function(propertyName, propertyValue) {
var mainDiv = this._div;
var tr = document.createElement("tr");
var td1 = document.createElement("td");
td1.textContent = propertyName;
tr.appendChild(td1);
var td2 = document.createElement("td");
var decProp = this.declaredProperties[propertyName];
var input = null;
var self = this;
function updateall() { self.updateAllProperties(); }
if (decProp && decProp.type === "select") {
input = document.createElement("select");
this.updateSelect(decProp, input, propertyName, propertyValue);
input.addEventListener("change", updateall);
} else {
input = document.createElement("input");
input.value = this.convertToString(propertyValue);
if (decProp) {
var t = decProp.type;
if (t !== 'string' && t !== 'number' && t !== 'boolean' &&
t !== 'arrayofnumber' && t !== 'point' && t !== 'size' &&
t !== 'rect' && t !== 'spot' && t !== 'margin') {
input.setAttribute("type", decProp.type);
}
if (decProp.type === "color") {
if (input.type === "color") {
input.value = this.convertToColor(propertyValue);
// input.addEventListener("input", updateall);
input.addEventListener("change", updateall);
}
} if (decProp.type === "checkbox") {
input.checked = !!propertyValue;
input.addEventListener("change", updateall);
}
}
if (input.type !== "color") input.addEventListener("blur", updateall);
}
if (input) {
input.tabIndex = this.tabIndex++;
input.disabled = !this.canEditProperty(propertyName, decProp, this.inspectedObject);
td2.appendChild(input);
}
tr.appendChild(td2);
this._inspectedProperties[propertyName] = input;
return tr;
};
/**
* @ignore
* HTML5 color input will only take hex,
* so var HTML5 canvas convert the color into hex format.
* This converts "rgb(255, 0, 0)" into "#FF0000", etc.
* @param {string} propertyValue
* @return {string}
*/
Inspector.prototype.convertToColor = function(propertyValue) {
var ctx = document.createElement("canvas").getContext("2d");
ctx.fillStyle = propertyValue;
return ctx.fillStyle;
};
/**
* @ignore
* @param {string}
* @return {Array.<number>}
*/
Inspector.prototype.convertToArrayOfNumber = function(propertyValue) {
if (propertyValue === "null") return null;
var split = propertyValue.split(' ');
var arr = [];
for (var i = 0; i < split.length; i++) {
var str = split[i];
if (!str) continue;
arr.push(parseFloat(str));
}
return arr;
};
/**
* @ignore
* @param {*}
* @return {string}
*/
Inspector.prototype.convertToString = function(x) {
if (x === undefined) return "undefined";
if (x === null) return "null";
if (x instanceof go.Point) return go.Point.stringify(x);
if (x instanceof go.Size) return go.Size.stringify(x);
if (x instanceof go.Rect) return go.Rect.stringify(x);
if (x instanceof go.Spot) return go.Spot.stringify(x);
if (x instanceof go.Margin) return go.Margin.stringify(x);
if (x instanceof go.List) return this.convertToString(x.toArray());
if (Array.isArray(x)) {
var str = "";
for (var i = 0; i < x.length; i++) {
if (i > 0) str += " ";
var v = x[i];
str += this.convertToString(v);
}
return str;
}
return x.toString();
};
/**
* @ignore
* Update all of the HTML in this Inspector.
*/
Inspector.prototype.updateAllHTML = function() {
var inspectedProps = this._inspectedProperties;
var diagram = this._diagram;
var isPart = this.inspectedObject instanceof go.Part;
var data = isPart ? this.inspectedObject.data : this.inspectedObject;
if (!data) { // clear out all of the fields
for (var name in inspectedProps) {
var input = inspectedProps[name];
if (input instanceof HTMLSelectElement) {
input.innerHTML = "";
} else if (input.type === "color") {
input.value = "#000000";
} else if (input.type === "checkbox") {
input.checked = false;
} else {
input.value = "";
}
}
} else {
for (var name in inspectedProps) {
var input = inspectedProps[name];
var propertyValue = data[name];
if (input instanceof HTMLSelectElement) {
var decProp = this.declaredProperties[name];
this.updateSelect(decProp, input, name, propertyValue);
} else if (input.type === "color") {
input.value = this.convertToColor(propertyValue);
} else if (input.type === "checkbox") {
input.checked = !!propertyValue;
} else {
input.value = this.convertToString(propertyValue);
}
}
}
}
/**
* @ignore
* Update an HTMLSelectElement with an appropriate list of choices, given the propertyName
*/
Inspector.prototype.updateSelect = function(decProp, select, propertyName, propertyValue) {
select.innerHTML = ""; // clear out anything that was there
var choices = decProp.choices;
if (typeof choices === "function") choices = choices(this.inspectedObject, propertyName);
if (!Array.isArray(choices)) choices = [];
decProp.choicesArray = choices; // remember list of actual choice values (not strings)
for (var i = 0; i < choices.length; i++) {
var choice = choices[i];
var opt = document.createElement("option");
opt.text = this.convertToString(choice);
select.add(opt, null);
}
select.value = this.convertToString(propertyValue);
}
/**
* @ignore
* Update all of the data properties of {@link #inspectedObject} according to the
* current values held in the HTML input elements.
*/
Inspector.prototype.updateAllProperties = function() {
var inspectedProps = this._inspectedProperties;
var diagram = this._diagram;
if (diagram.selection.count === 1 || !this.multipleSelection) { // single object update
var isPart = this.inspectedObject instanceof go.Part;
var data = isPart ? this.inspectedObject.data : this.inspectedObject;
if (!data) return; // must not try to update data when there's no data!
diagram.startTransaction('set all properties');
for (var name in inspectedProps) {
var input = inspectedProps[name];
var value = input.value;
// don't update "readOnly" data properties
var decProp = this.declaredProperties[name];
if (!this.canEditProperty(name, decProp, this.inspectedObject)) continue;
// If it's a boolean, or if its previous value was boolean,
// parse the value to be a boolean and then update the input.value to match
var type = '';
if (decProp !== undefined && decProp.type !== undefined) {
type = decProp.type;
}
if (type === '') {
var oldval = data[name];
if (typeof oldval === 'boolean') type = 'boolean'; // infer boolean
else if (typeof oldval === 'number') type = 'number';
else if (oldval instanceof go.Point) type = 'point';
else if (oldval instanceof go.Size) type = 'size';
else if (oldval instanceof go.Rect) type = 'rect';
else if (oldval instanceof go.Spot) type = 'spot';
else if (oldval instanceof go.Margin) type = 'margin';
}
// convert to specific type, if needed
switch (type) {
case 'boolean': value = !(value === false || value === 'false' || value === '0'); break;
case 'number': value = parseFloat(value); break;
case 'arrayofnumber': value = this.convertToArrayOfNumber(value); break;
case 'point': value = go.Point.parse(value); break;
case 'size': value = go.Size.parse(value); break;
case 'rect': value = go.Rect.parse(value); break;
case 'spot': value = go.Spot.parse(value); break;
case 'margin': value = go.Margin.parse(value); break;
case 'checkbox': value = input.checked; break;
case 'select': value = decProp.choicesArray[input.selectedIndex]; break;
}
// in case parsed to be different, such as in the case of boolean values,
// the value shown should match the actual value
input.value = value;
// modify the data object in an undo-able fashion
diagram.model.setDataProperty(data, name, value);
// notify any listener
if (this.propertyModified !== null) this.propertyModified(name, value, this);
}
diagram.commitTransaction('set all properties');
} else { // selection object update
diagram.startTransaction('set all properties');
for (var name in inspectedProps) {
var input = inspectedProps[name];
var value = input.value;
var arr1 = value.split('|');
var arr2 = [];
if (this._multipleProperties[name]) {
// don't split if it is union and its checkbox type
if (this.declaredProperties[name] && this.declaredProperties[name].type === 'checkbox' && this.showAllProperties) {
arr2.push(this._multipleProperties[name]);
} else {
arr2 = this._multipleProperties[name].toString().split('|');
}
}
var it = diagram.selection.iterator;
var change = false;
if (this.declaredProperties[name] && this.declaredProperties[name].type === 'checkbox') change = true; // always change checkbox
if (arr1.length < arr2.length // i.e Alpha|Beta -> Alpha procs the change
&& (!this.declaredProperties[name] // from and to links
|| !(this.declaredProperties[name] // do not change color checkbox and choices due to them always having less
&& (this.declaredProperties[name].type === 'color' || this.declaredProperties[name].type === 'checkbox' || this.declaredProperties[name].type === 'choices')))) {
change = true;
} else { // standard detection in change in properties
for (var j = 0; j < arr1.length && j < arr2.length; j++) {
if (!(arr1[j] === arr2[j])
&& !(this.declaredProperties[name] && this.declaredProperties[name].type === 'color' && arr1[j].toLowerCase() === arr2[j].toLowerCase())) {
change = true;
}
}
}
if (change) { // only change properties it needs to change instead all of them
for (var i = 0; i < diagram.selection.count; i++) {
it.next();
var isPart = it.value instanceof go.Part;
var data = isPart ? it.value.data : it.value;
if (data) { // ignores the selected node if there is no data
if (i < arr1.length) value = arr1[i];
else value = arr1[0];
// don't update "readOnly" data properties
var decProp = this.declaredProperties[name];
if (!this.canEditProperty(name, decProp, it.value)) continue;
// If it's a boolean, or if its previous value was boolean,
// parse the value to be a boolean and then update the input.value to match
var type = '';
if (decProp !== undefined && decProp.type !== undefined) {
type = decProp.type;
}
if (type === '') {
var oldval = data[name];
if (typeof oldval === 'boolean') type = 'boolean'; // infer boolean
else if (typeof oldval === 'number') type = 'number';
else if (oldval instanceof go.Point) type = 'point';
else if (oldval instanceof go.Size) type = 'size';
else if (oldval instanceof go.Rect) type = 'rect';
else if (oldval instanceof go.Spot) type = 'spot';
else if (oldval instanceof go.Margin) type = 'margin';
}
// convert to specific type, if needed
switch (type) {
case 'boolean': value = !(value === false || value === 'false' || value === '0'); break;
case 'number': value = parseFloat(value); break;
case 'arrayofnumber': value = this.convertToArrayOfNumber(value); break;
case 'point': value = go.Point.parse(value); break;
case 'size': value = go.Size.parse(value); break;
case 'rect': value = go.Rect.parse(value); break;
case 'spot': value = go.Spot.parse(value); break;
case 'margin': value = go.Margin.parse(value); break;
case 'checkbox': value = input.checked; break;
case 'select': value = decProp.choicesArray[input.selectedIndex]; break;
}
// in case parsed to be different, such as in the case of boolean values,
// the value shown should match the actual value
input.value = value;
// modify the data object in an undo-able fashion
diagram.model.setDataProperty(data, name, value);
// notify any listener
if (this.propertyModified !== null) this.propertyModified(name, value, this);
}
}
}
}
diagram.commitTransaction('set all properties');
}
};
+133
View File
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<title>Using Dimensioning Links</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Dimensioning Links show the distance from a spot on a node to another spot on a node." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="../extensions/DimensioningLink.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"undoManager.isEnabled": true
});
// A simple resizable node
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{ locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{ resizable: true },
$(go.Shape, { strokeWidth: 0, fill: "lightgray" },
new go.Binding("fill", "color")),
$(go.TextBlock, { margin: 10 },
new go.Binding("text", "key"))
);
// A generalized example template using a DimensioningLink.
// Most usage might not have so many Bindings.
myDiagram.linkTemplateMap.add("Dimensioning",
$(DimensioningLink,
new go.Binding("fromSpot", "fromSpot", go.Spot.parse),
new go.Binding("toSpot", "toSpot", go.Spot.parse),
new go.Binding("direction"),
new go.Binding("extension"),
new go.Binding("inset"),
$(go.Shape, { stroke: "gray" },
new go.Binding("stroke", "color")),
$(go.Shape, { fromArrow: "BackwardOpenTriangle", segmentIndex: 2, stroke: "gray" },
new go.Binding("stroke", "color")),
$(go.Shape, { toArrow: "OpenTriangle", segmentIndex: -3, stroke: "gray" },
new go.Binding("stroke", "color")),
$(go.TextBlock,
{
segmentIndex: 2,
segmentFraction: 0.5,
segmentOrientation: go.Link.OrientUpright,
alignmentFocus: go.Spot.Bottom,
stroke: "gray",
font: "8pt sans-serif"
},
new go.Binding("text", "", showDistance).ofObject(),
new go.Binding("stroke", "color"))
));
// Return a string representing the distance between the two points.
// This is the cartesian distance if this.direction is NaN;
// otherwise it is the orthogonal distance along that axis.
function showDistance(link) {
var numpts = link.pointsCount;
if (numpts < 2) return "";
var p0 = link.getPoint(0);
var pn = link.getPoint(numpts - 1);
var ang = link.direction;
if (isNaN(ang)) return Math.floor(Math.sqrt(p0.distanceSquaredPoint(pn))) + "";
var rad = ang * Math.PI / 180;
return Math.floor(Math.abs(Math.cos(rad) * (p0.x - pn.x)) +
Math.abs(Math.sin(rad) * (p0.y - pn.y))) + "";
}
myDiagram.model = new go.GraphLinksModel([
{ key: "Alpha", loc: "0 50" },
{ key: "Beta", loc: "150 0" },
{ key: "Gamma", loc: "100 150" }
], [
{
from: "Alpha", to: "Beta", category: "Dimensioning",
fromSpot: "TopRight", toSpot: "TopLeft"
},
{
from: "Alpha", to: "Beta", category: "Dimensioning",
fromSpot: "TopLeft", toSpot: "TopRight", extension: 50, color: "blue"
},
{
from: "Alpha", to: "Beta", category: "Dimensioning",
fromSpot: "TopLeft", toSpot: "TopLeft", direction: 270, color: "green"
},
{
from: "Alpha", to: "Beta", category: "Dimensioning",
fromSpot: "BottomRight", toSpot: "BottomRight", direction: 90, color: "purple"
},
{
from: "Alpha", to: "Beta", category: "Dimensioning",
fromSpot: "Center", toSpot: "Center", extension: 50, direction: NaN, color: "red"
},
{
from: "Gamma", to: "Gamma", category: "Dimensioning",
fromSpot: "TopLeft", toSpot: "TopRight", direction: 0
},
{
from: "Gamma", to: "Gamma", category: "Dimensioning",
fromSpot: "TopRight", toSpot: "BottomRight", direction: 90
},
{
from: "Gamma", to: "Gamma", category: "Dimensioning",
fromSpot: "BottomRight", toSpot: "BottomLeft", direction: 180
},
{
from: "Gamma", to: "Gamma", category: "Dimensioning",
fromSpot: "BottomLeft", toSpot: "TopLeft", direction: 270
}
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<p>
This sample makes use of the DimensioningLink class, which inherits from the <a>Link</a> class.
That class is defined at <a href="../extensions/DimensioningLink.js">DimensioningLink.js</a>.
</p>
</div>
</body>
</html>
+214
View File
@@ -0,0 +1,214 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom routed Link for showing the distances between a point on one node and a point on another node.
// Note that because this is a Link, the points being measured must be on Nodes, not simple Parts.
// The exact point on each Node is determined by the Link.fromSpot and Link.toSpot.
// Several properties of the DimensioningLink customize the appearance of the dimensioning:
// direction, for orientation of the dimension line and which side it is on,
// extension, for how far the dimension line is from the measured points,
// inset, for leaving room for a text label, and
// gap, for distance that the extension line starts from the measured points
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Link
* @class
*/
function DimensioningLink() {
go.Link.call(this);
this.isLayoutPositioned = false;
this.isTreeLink = false;
this.routing = go.Link.Orthogonal;
/** @type {number} */
this._direction = 0;
/** @type {number} */
this._extension = 30;
/** @type {number} */
this._inset = 10;
/** @type {number} */
this._gap = 10;
}
go.Diagram.inherit(DimensioningLink, go.Link);
/**
* @ignore
* Copies properties to a cloned DimensioningLink.
* @this {DimensioningLink}
* @param {DimensioningLink} copy
* @override
*/
DimensioningLink.prototype.cloneProtected = function(copy) {
go.Link.prototype.cloneProtected.call(this, copy);
copy._direction = this._direction;
copy._extension = this._extension;
copy._inset = this._inset;
copy._gap = this._gap;
}
/*
* The general angle at which the measurement should be made.
* The default value is 0, meaning to go measure only along the X axis,
* with the dimension line and label above the two nodes (at lower Y coordinates).
* New values must be one of: 0, 90, 180, 270, or NaN.
* The value NaN indicates that the measurement is point-to-point and not orthogonal.
* @name DimensioningLink#direction
* @return {number}
*/
Object.defineProperty(DimensioningLink.prototype, "direction", {
get: function() { return this._direction; },
set: function(value) {
if (isNaN(value) || value === 0 || value === 90 || value === 180 || value === 270) {
this._direction = value;
} else {
throw new Error("DimensioningLink: invalid new direction: " + value);
}
}
});
/*
* The distance at which the dimension line should be from the points being measured.
* The default value is 30.
* Larger values mean further away from the nodes.
* The new value must be greater than or equal to zero.
* @name DimensioningLink#extension
* @return {number}
*/
Object.defineProperty(DimensioningLink.prototype, "extension", {
get: function() { return this._extension; },
set: function(value) { this._extension = value; }
});
/*
* The distance that the dimension line should be "indented" from the ends of the
* extension lines that are orthogonal to the dimension line.
* The default value is 10.
* @name DimensioningLink#inset
* @return {number}
*/
Object.defineProperty(DimensioningLink.prototype, "inset", {
get: function() { return this._inset; },
set: function(value) {
if (value >= 0) {
this._inset = value;
} else {
throw new Error("DimensioningLink: invalid new inset: " + value);
}
}
});
/*
* The distance that the extension lines should come short of the measured points.
* The default value is 10.
* @name DimensioningLink#gap
* @return {number}
*/
Object.defineProperty(DimensioningLink.prototype, "gap", {
get: function() { return this._gap; },
set: function(value) {
if (value >= 0) {
this._gap = value;
} else {
throw new Error("DimensioningLink: invalid new gap: " + value);
}
}
});
/**
* @return {boolean} true if it computed a route of points
*/
DimensioningLink.prototype.computePoints = function() {
var fromnode = this.fromNode;
if (!fromnode) return false;
var fromport = this.fromPort;
var fromspot = this.computeSpot(true);
var tonode = this.toNode;
if (!tonode) return false;
var toport = this.toPort;
var tospot = this.computeSpot(false);
var frompoint = this.getLinkPoint(fromnode, fromport, fromspot, true, true, tonode, toport);
if (!frompoint.isReal()) return false;
var topoint = this.getLinkPoint(tonode, toport, tospot, false, true, fromnode, fromport);
if (!topoint.isReal()) return false;
this.clearPoints();
var ang = this.direction;
if (isNaN(ang)) {
ang = frompoint.directionPoint(topoint);
var p = new go.Point(this.extension, 0);
p.rotate(ang + 90);
var q = new go.Point(this.extension - this.inset, 0);
q.rotate(ang + 90);
var g = new go.Point(this.gap, 0);
g.rotate(ang + 90);
this.addPointAt(frompoint.x + g.x, frompoint.y + g.y);
this.addPointAt(frompoint.x + p.x, frompoint.y + p.y);
this.addPointAt(frompoint.x + q.x, frompoint.y + q.y);
this.addPointAt(topoint.x + q.x, topoint.y + q.y);
this.addPointAt(topoint.x + p.x, topoint.y + p.y);
this.addPointAt(topoint.x + g.x, topoint.y + g.y);
} else {
var dist = this.extension;
var r = 0.0;
var s = 0.0;
var t0 = 0.0;
var t1 = 0.0;
if (ang === 0 || ang === 180) {
if (ang === 0) {
r = Math.min(frompoint.y, topoint.y) - this.extension;
s = r + this.inset;
t0 = frompoint.y - this.gap;
t1 = topoint.y - this.gap;
} else {
r = Math.max(frompoint.y, topoint.y) + this.extension;
s = r - this.inset;
t0 = frompoint.y + this.gap;
t1 = topoint.y + this.gap;
}
this.addPointAt(frompoint.x, t0);
this.addPointAt(frompoint.x + 0.01, r);
this.addPointAt(frompoint.x, s);
this.addPointAt(topoint.x, s);
this.addPointAt(topoint.x - 0.01, r);
this.addPointAt(topoint.x, t1);
} else if (ang === 90 || ang === 270) {
if (ang === 90) {
r = Math.max(frompoint.x, topoint.x) + this.extension;
s = r - this.inset;
t0 = frompoint.x + this.gap;
t1 = topoint.x + this.gap;
} else {
r = Math.min(frompoint.x, topoint.x) - this.extension;
s = r + this.inset;
t0 = frompoint.x - this.gap;
t1 = topoint.x - this.gap;
}
this.addPointAt(t0, frompoint.y);
this.addPointAt(r, frompoint.y + 0.01);
this.addPointAt(s, frompoint.y);
this.addPointAt(s, topoint.y);
this.addPointAt(r, topoint.y - 0.01);
this.addPointAt(t1, topoint.y);
}
}
this.updateTargetBindings();
return true;
};
+216
View File
@@ -0,0 +1,216 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
function DoubleTreeLayout() {
go.Layout.call(this);
this._vertical = false;
this._directionFunction = function(node) { return true; };
this._bottomRightOptions = null;
this._topLeftOptions = null;
}
go.Diagram.inherit(DoubleTreeLayout, go.Layout);
/*
* When false, the layout should grow towards the left and towards the right;
* when true, the layout show grow upwards and downwards.
* The default value is false.
* @name DoubleTreeLayout#vertical
* @return {boolean}
*/
Object.defineProperty(DoubleTreeLayout.prototype, "vertical", {
get: function() { return this._vertical; },
set: function(val) {
if (typeof val !== "boolean") throw new Error("new value for DoubleTreeLayout.vertical must be a boolean value.");
if (this._vertical !== val) {
this._vertical = val;
this.invalidateLayout();
}
}
});
/*
* This function is called on each child node of the root node
* in order to determine whether the subtree starting from that child node
* will grow towards larger coordinates or towards smaller ones.
* The value must be a function and must not be null.
* It must return true if #isPositiveDirection should return true; otherwise it should return false.
* @name DoubleTreeLayout#directionFunction
* @return {function}
*/
Object.defineProperty(DoubleTreeLayout.prototype, "directionFunction", {
get: function() { return this._directionFunction; },
set: function(val) {
if (typeof val !== "function") {
throw new Error("new value for DoubleTreeLayout.directionFunction must be a function taking a node data object and returning a boolean.");
}
if (this._directionFunction !== val) {
this._directionFunction = val;
this.invalidateLayout();
}
}
});
/**
* Gets or sets the options to be applied to a {@link TreeLayout}.
* By default this is null -- no properties are set on the TreeLayout
* other than the {@link TreeLayout#angle}, depending on {@link #vertical} and
* the result of calling {@link #directionFunction}.
*/
Object.defineProperty(DoubleTreeLayout.prototype, "bottomRightOptions", {
get: function() { return this._bottomRightOptions; },
set: function(value) {
if (this._bottomRightOptions !== value) {
this._bottomRightOptions = value;
this.invalidateLayout();
}
}
});
/**
* Gets or sets the options to be applied to a {@link TreeLayout}.
* By default this is null -- no properties are set on the TreeLayout
* other than the {@link TreeLayout#angle}, depending on {@link #vertical} and
* the result of calling {@link #directionFunction}.
*/
Object.defineProperty(DoubleTreeLayout.prototype, "topLeftOptions", {
get: function() { return this._topLeftOptions; },
set: function(value) {
if (this._topLeftOptions !== value) {
this._topLeftOptions = value;
this.invalidateLayout();
}
}
});
/**
* @ignore
* Copies properties to a cloned Layout.
* @this {DoubleTreeLayout}
* @param {Layout} copy
*/
DoubleTreeLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
copy._vertical = this._vertical;
copy._directionFunction = this._directionFunction;
copy._bottomRightOptions = this._bottomRightOptions;
copy._topLeftOptions = this._topLeftOptions;
};
/**
* Perform two {@link TreeLayout}s by splitting the collection of Parts
* into two separate subsets but sharing only a single root Node.
* @param coll
*/
DoubleTreeLayout.prototype.doLayout = function(coll) {
coll = this.collectParts(coll);
if (coll.count === 0) return;
var diagram = this.diagram;
if (diagram !== null) diagram.startTransaction("Double Tree Layout");
// split the nodes and links into two Sets, depending on direction
var leftParts = new go.Set();
var rightParts = new go.Set();
this.separatePartsForLayout(coll, leftParts, rightParts);
// but the ROOT node will be in both collections
// create and perform two TreeLayouts, one in each direction,
// without moving the ROOT node, on the different subsets of nodes and links
var layout1 = this.createTreeLayout(false);
layout1.angle = this.vertical ? 270 : 180;
layout1.arrangement = go.TreeLayout.ArrangementFixedRoots;
var layout2 = this.createTreeLayout(true);
layout2.angle = this.vertical ? 90 : 0;
layout2.arrangement = go.TreeLayout.ArrangementFixedRoots;
layout1.doLayout(leftParts);
layout2.doLayout(rightParts);
if (diagram !== null) diagram.commitTransaction("Double Tree Layout");
}
/**
* This just returns an instance of {@link TreeLayout}.
* The caller will set the {@link TreeLayout#angle}.
* @param {boolean} positive true for growth downward or rightward
* @return {TreeLayout}
*/
DoubleTreeLayout.prototype.createTreeLayout = function(positive) {
var lay = new go.TreeLayout();
var opts = this.topLeftOptions;
if (positive) opts = this.bottomRightOptions;
if (opts) for (var p in opts) { lay[p] = opts[p]; }
return lay;
}
/**
* This is called by #doLayout to split the collection of Nodes and Links into two Sets,
* one for the subtrees growing towards the left or upwards, and one for the subtrees
* growing towards the right or downwards.
*/
DoubleTreeLayout.prototype.separatePartsForLayout = function(coll, leftParts, rightParts) {
var root = null; // the one root
var roots = new go.Set(); // in case there are multiple roots
coll.each(function(node) {
if (node instanceof go.Node && node.findTreeParentNode() === null) roots.add(node);
});
if (roots.count === 0) { // just choose the first node as the root
var it = coll.iterator;
while (it.next()) {
if (it.value instanceof go.Node) {
root = it.value;
break;
}
}
} else if (roots.count === 1) { // normal case: just one root node
root = roots.first();
} else { // multiple root nodes -- create a dummy node to be the one real root
root = new go.Node(); // the new root node
root.location = new go.Point(0, 0);
var forwards = (this.diagram ? this.diagram.isTreePathToChildren : true);
// now make dummy links from the one root node to each node
roots.each(function(child) {
var link = new go.Link();
if (forwards) {
link.fromNode = root;
link.toNode = child;
} else {
link.fromNode = child;
link.toNode = root;
}
});
}
// the ROOT node is shared by both subtrees
leftParts.add(root);
rightParts.add(root);
var lay = this;
// look at all of the immediate children of the ROOT node
root.findTreeChildrenNodes().each(function(child) {
// in what direction is this child growing?
var bottomright = lay.isPositiveDirection(child);
var coll = bottomright ? rightParts : leftParts;
// add the whole subtree starting with this child node
coll.addAll(child.findTreeParts());
// and also add the link from the ROOT node to this child node
coll.add(child.findTreeParentLink());
});
}
/**
* This predicate is called on each child node of the root node,
* and only on immediate children of the root.
* It should return true if this child node is the root of a subtree that should grow
* rightwards or downwards, or false otherwise.
* @param {Node} child
* @returns {boolean} true if grows towards right or towards bottom; false otherwise
*/
DoubleTreeLayout.prototype.isPositiveDirection = function(child) {
var f = this.directionFunction;
if (!f) throw new Error("No DoubleTreeLayout.directionFunction supplied on the layout");
return f(child);
}
+91
View File
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html>
<head>
<title>Drag Creating Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Create nodes by dragging, thereby specifying their initial size." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="DragCreatingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
// Define the template for Nodes, just some text inside a colored rectangle
nodeTemplate:
$(go.Node, "Auto",
{ minSize: new go.Size(60, 20), resizable: true },
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
new go.Binding("position", "pos", go.Point.parse).makeTwoWay(go.Point.stringify),
// temporarily put selected nodes in Foreground layer
new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
$(go.Shape, "Rectangle",
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 2 },
new go.Binding("text", "color"))),
"undoManager.isEnabled": true
});
myDiagram.add(
$(go.Part,
{ layerName: "Grid", location: new go.Point(0, 0) },
$(go.TextBlock, "Mouse-down and then drag in the background\nto add a Node there with the drawn size.",
{ stroke: "brown" })
));
// Add an instance of the custom tool defined in DragCreatingTool.js.
// This needs to be inserted before the standard DragSelectingTool,
// which is normally the third Tool in the ToolManager.mouseMoveTools list.
// Note that if you do not set the DragCreatingTool.delay, the default value will
// require a wait after the mouse down event. Not waiting will allow the DragSelectingTool
// and the PanningTool to be able to run instead of the DragCreatingTool, depending on the delay.
myDiagram.toolManager.mouseMoveTools.insertAt(2,
$(DragCreatingTool,
{
isEnabled: true, // disabled by the checkbox
delay: 0, // always canStart(), so PanningTool never gets the chance to run
box: $(go.Part,
{ layerName: "Tool" },
$(go.Shape,
{ name: "SHAPE", fill: null, stroke: "cyan", strokeWidth: 2 })
),
archetypeNodeData: { color: "white" }, // initial properties shared by all nodes
insertPart: function(bounds) { // override DragCreatingTool.insertPart
// use a different color each time
this.archetypeNodeData.color = go.Brush.randomColor();
// call the base method to do normal behavior and return its result
return DragCreatingTool.prototype.insertPart.call(this, bounds);
}
}));
}
function toolEnabled() {
var enable = document.getElementById("ToolEnabled").checked;
var tool = myDiagram.toolManager.findTool("DragCreating");
if (tool !== null) tool.isEnabled = enable;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%;height: 800px"></div>
<label><input id="ToolEnabled" type="checkbox" checked="checked" onclick="toolEnabled()" />DragCreatingTool enabled</label>
<p>
This sample demonstrates the DragCreatingTool, which replaces the standard DragSelectingTool. It is defined in its own file, as <a href="DragCreatingTool.js">DragCreatingTool.js</a>.
</p>
<p>
Press in the background and then drag to show the area to be occupied by the new node.
The mouse-up event will add a copy of the DragCreatingTool.archetypeNodeData object, causing a new node to be created.
The tool will assign its <a>GraphObject.position</a> and <a>GraphObject.desiredSize</a>.
</p>
</div>
</body>
</html>
+273
View File
@@ -0,0 +1,273 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for creating a new Node with custom size by dragging its outline in the background.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* The DragCreatingTool lets the user create a new node by dragging in the background
* to indicate its size and position.
* <p/>
* The default drag selection box is a magenta rectangle.
* You can modify the {@link #box} to customize its appearance.
* <p/>
* This tool will not be able to start running unless you have set the
* {@link #archetypeNodeData} property to an object that can be copied and added to the diagram's model.
* <p/>
* You can use this tool in a modal manner by executing:
* <pre>
* diagram.currentTool = new DragCreatingTool();
* </pre>
* <p/>
* Use this tool in a mode-less manner by executing:
* <pre>
* myDiagram.toolManager.mouseMoveTools.insertAt(2, new DragCreatingTool());
* </pre>
* However when used mode-lessly as a mouse-move tool, in {@link ToolManager#mouseMoveTools},
* this cannot start running unless there has been a motionless delay
* after the mouse-down event of at least {@link #delay} milliseconds.
* <p/>
* This tool does not utilize any {@link Adornment}s or tool handles,
* but it does temporarily add the {@link #box} Part to the diagram.
* This tool does conduct a transaction when inserting the new node.
*/
function DragCreatingTool() {
go.Tool.call(this);
this.name = "DragCreating";
/** @type {Object} */
this._archetypeNodeData = null;
var b = new go.Part();
b.layerName = "Tool";
b.selectable = false;
var r = new go.Shape();
r.name = "SHAPE";
r.figure = "Rectangle";
r.fill = null;
r.stroke = "magenta";
r.position = new go.Point(0, 0);
b.add(r);
/** @type {Part} */
this._box = b;
/** @type {number} */
this._delay = 175;
}
go.Diagram.inherit(DragCreatingTool, go.Tool);
/**
* This tool can run when there has been a mouse-drag, far enough away not to be a click,
* and there has been delay of at least {@link #delay} milliseconds
* after the mouse-down before a mouse-move.
* <p/>
* This method may be overridden.
* @this {DragCreatingTool}
* @return {boolean}
*/
DragCreatingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
// gotta have some node data that can be copied
if (this.archetypeNodeData === null) return false;
var diagram = this.diagram;
if (diagram === null) return false;
// heed IsReadOnly & AllowInsert
if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
if (!diagram.allowInsert) return false;
var e = diagram.lastInput;
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
if (!e.left) return false;
// don't include the following checks when this tool is running modally
if (diagram.currentTool !== this) {
if (!this.isBeyondDragSize()) return false;
// must wait for "delay" milliseconds before that tool can run
if (e.timestamp - diagram.firstInput.timestamp < this.delay) return false;
}
return true;
};
/**
* Capture the mouse and show the {@link #box}.
* @this {DragCreatingTool}
*/
DragCreatingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this.isActive = true;
diagram.isMouseCaptured = true;
diagram.add(this.box);
this.doMouseMove();
};
/**
* Release the mouse and remove any {@link #box}.
* @this {DragCreatingTool}
*/
DragCreatingTool.prototype.doDeactivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
diagram.remove(this.box);
diagram.isMouseCaptured = false;
this.isActive = false;
};
/**
* Update the {@link #box}'s position and size according to the value
* of {@link #computeBoxBounds}.
* @this {DragCreatingTool}
*/
DragCreatingTool.prototype.doMouseMove = function() {
var diagram = this.diagram;
if (diagram === null) return;
if (this.isActive && this.box !== null) {
var r = this.computeBoxBounds();
var shape = this.box.findObject("SHAPE");
if (shape === null) shape = this.box.findMainElement();
shape.desiredSize = r.size;
this.box.position = r.position;
}
};
/**
* Call {@link #insertPart} with the value of a call to {@link #computeBoxBounds}.
* @this {DragCreatingTool}
*/
DragCreatingTool.prototype.doMouseUp = function() {
if (this.isActive) {
var diagram = this.diagram;
diagram.remove(this.box);
try {
diagram.currentCursor = "wait";
this.insertPart(this.computeBoxBounds());
} finally {
diagram.currentCursor = "";
}
}
this.stopTool();
};
/**
* This just returns a {@link Rect} stretching from the mouse-down point to the current mouse point.
* <p/>
* This method may be overridden.
* @this {DragCreatingTool}
* @return {Rect} a {@link Rect} in document coordinates.
*/
DragCreatingTool.prototype.computeBoxBounds = function() {
var diagram = this.diagram;
if (diagram === null) return new go.Rect(0, 0, 0, 0);
var start = diagram.firstInput.documentPoint;
var latest = diagram.lastInput.documentPoint;
return new go.Rect(start, latest);
};
/**
* Create a node by adding a copy of the {@link #archetypeNodeData} object
* to the diagram's model, assign its {@link GraphObject#position} and {@link GraphObject#desiredSize}
* according to the given bounds, and select the new part.
* <p>
* The actual part that is added to the diagram may be a {@link Part}, a {@link Node},
* or even a {@link Group}, depending on the properties of the {@link #archetypeNodeData}
* and the type of the template that is copied to create the part.
* @this {DragCreatingTool}
* @param {Rect} bounds a Point in document coordinates.
* @return {Part} the newly created Part, or null if it failed.
*/
DragCreatingTool.prototype.insertPart = function(bounds) {
var diagram = this.diagram;
if (diagram === null) return null;
var arch = this.archetypeNodeData;
if (arch === null) return null;
diagram.raiseDiagramEvent("ChangingSelection", diagram.selection);
this.startTransaction(this.name);
var part = null;
if (arch !== null) {
var data = diagram.model.copyNodeData(arch);
if (data) {
diagram.model.addNodeData(data);
part = diagram.findPartForData(data);
}
}
if (part !== null) {
part.position = bounds.position;
part.resizeObject.desiredSize = bounds.size;
if (diagram.allowSelect) {
diagram.clearSelection();
part.isSelected = true;
}
}
// set the TransactionResult before raising event, in case it changes the result or cancels the tool
this.transactionResult = this.name;
this.stopTransaction();
diagram.raiseDiagramEvent("ChangedSelection", diagram.selection);
return part;
};
// Public properties
/**
* Gets or sets the {@link Part} used as the "rubber-band box"
* that is stretched to follow the mouse, as feedback for what area will
* be passed to {@link #insertPart} upon a mouse-up.
* <p/>
* Initially this is a {@link Part} containing only a simple magenta rectangular {@link Shape}.
* The object to be resized should be named "SHAPE".
* Setting this property does not raise any events.
* <p/>
* Modifying this property while this tool {@link Tool#isActive} might have no effect.
* @name DragCreatingTool#box
* @return {Part}
*/
Object.defineProperty(DragCreatingTool.prototype, "box", {
get: function() { return this._box; },
set: function(val) { this._box = val; }
});
/**
* Gets or sets the time in milliseconds for which the mouse must be stationary
* before this tool can be started.
* The default value is 175 milliseconds.
* A value of zero will allow this tool to run without any wait after the mouse down.
* Setting this property does not raise any events.
* @name DragCreatingTool#delay
* @return {number}
*/
Object.defineProperty(DragCreatingTool.prototype, "delay", {
get: function() { return this._delay; },
set: function(val) { this._delay = val; }
});
/**
* Gets or sets a data object that will be copied and added to the diagram's model each time this tool executes.
* The default value is null.
* The value must be non-null for this tool to be able to run.
* Setting this property does not raise any events.
* @name DragCreatingTool#archetypeNodeData
* @return {Object}
*/
Object.defineProperty(DragCreatingTool.prototype, "archetypeNodeData", {
get: function() { return this._archetypeNodeData; },
set: function(val) { this._archetypeNodeData = val; }
});
+110
View File
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<title>Drag Zooming Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Users can zoom into and out of a diagram by drawing a rectangle showing what part of the document should be shown by the new viewport." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="DragZoomingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
initialDocumentSpot: go.Spot.Center,
initialViewportSpot: go.Spot.Center,
// Define the template for Nodes, just some text inside a colored rectangle
nodeTemplate:
$(go.Node, "Spot",
{ width: 70, height: 20 },
$(go.Shape, "Rectangle",
new go.Binding("fill", "c")),
$(go.TextBlock,
{ margin: 2 },
new go.Binding("text", "c"))),
// Define the template for Links, just a simple line
linkTemplate:
$(go.Link,
$(go.Shape, { stroke: "black" })),
layout:
$(go.TreeLayout,
{
angle: 90,
nodeSpacing: 4,
compaction: go.TreeLayout.CompactionNone
}),
model:
$(go.TreeModel,
{ // we use single character property names, to save space if rendered as JSON
nodeKeyProperty: "k",
nodeParentKeyProperty: "p"
})
});
// Add an instance of the custom tool defined in DragZoomingTool.js.
// This needs to be inserted before the standard DragSelectingTool,
// which is normally the third Tool in the ToolManager.mouseMoveTools list.
myDiagram.toolManager.mouseMoveTools.insertAt(2, new DragZoomingTool());
// This is a status message
myLoading =
$(go.Part,
{ selectable: false, location: new go.Point(0, 0) },
$(go.TextBlock, "loading...",
{ stroke: "red", font: "20pt sans-serif" }));
// temporarily add the status indicator
myDiagram.add(myLoading);
// allow the myLoading indicator to be shown now,
// but allow objects added in loadTree to also be considered part of the initial Diagram
myDiagram.delayInitialization(loadTree);
}
function loadTree() {
// create some tree data
var total = 99;
var treedata = [];
for (var i = 0; i < total; i++) {
// these property names are also specified when creating the TreeModel
var d = {
k: i, // this node data's key
c: go.Brush.randomColor(), // the node's color
p: (i > 0 ? Math.floor(Math.random() * i / 2) : undefined) // the random parent's key
};
treedata.push(d);
}
// give the Diagram's model all the data
myDiagram.model.nodeDataArray = treedata;
// remove the status indicator
myDiagram.remove(myLoading);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%;height: 800px"></div>
<p>
This sample demonstrates the DragZoomingTool, which replaces the standard DragSelectingTool. It is defined in its own file, as <a href="DragZoomingTool.js">DragZoomingTool.js</a>.
</p>
<p>
Press in the background, wait briefly, and then drag to zoom in to show the area of the drawn rectangle.
Hold down the Shift key to zoom out.
The rectangle always has the same aspect ratio as the viewport of the diagram.
</p>
</div>
</body>
</html>
+273
View File
@@ -0,0 +1,273 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for zooming into a selected area
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* The DragZoomingTool lets the user zoom into a diagram by stretching a box
* to indicate the new contents of the diagram's viewport (the area of the
* model shown by the Diagram).
* Hold down the Shift key in order to zoom out.
* <p/>
* The default drag selection box is a magenta rectangle.
* You can modify the {@link #box} to customize its appearance.
* <p/>
* The diagram that is zoomed by this tool is specified by the {@link #zoomedDiagram} property.
* If the value is null, the tool zooms its own {@link Tool#diagram}.
* <p/>
* You can use this tool in a modal manner by executing:
* <pre>
* diagram.currentTool = new DragZoomingTool();
* </pre>
* <p/>
* Use this tool in a mode-less manner by executing:
* <pre>
* myDiagram.toolManager.mouseMoveTools.insertAt(2, new DragZoomingTool());
* </pre>
* However when used mode-lessly as a mouse-move tool, in {@link ToolManager#mouseMoveTools},
* this cannot start running unless there has been a motionless delay
* after the mouse-down event of at least {@link #delay} milliseconds.
* <p/>
* This tool does not utilize any {@link Adornment}s or tool handles,
* but it does temporarily add the {@link #box} part to the diagram.
* This tool does not modify the model or conduct any transaction.
*/
function DragZoomingTool() {
go.Tool.call(this);
this.name = "DragZooming";
var b = new go.Part();
b.layerName = "Tool";
b.selectable = false;
var r = new go.Shape();
r.name = "SHAPE";
r.figure = "Rectangle";
r.fill = null;
r.stroke = "magenta";
r.position = new go.Point(0, 0);
b.add(r);
/** @type {Part} */
this._box = b;
/** @type {number} */
this._delay = 175;
/** @type {Diagram} */
this._zoomedDiagram = null;
}
go.Diagram.inherit(DragZoomingTool, go.Tool);
/**
* This tool can run when there has been a mouse-drag, far enough away not to be a click,
* and there has been delay of at least {@link #delay} milliseconds
* after the mouse-down before a mouse-move.
* <p/>
* This method may be overridden.
* @this {DragZoomingTool}
* @return {boolean}
*/
DragZoomingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null) return false;
var e = diagram.lastInput;
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
if (!e.left) return false;
// don't include the following checks when this tool is running modally
if (diagram.currentTool !== this) {
if (!this.isBeyondDragSize()) return false;
// must wait for "delay" milliseconds before that tool can run
if (e.timestamp - diagram.firstInput.timestamp < this.delay) return false;
}
return true;
};
/**
* Capture the mouse and show the {@link #box}.
* @this {DragZoomingTool}
*/
DragZoomingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this.isActive = true;
diagram.isMouseCaptured = true;
diagram.skipsUndoManager = true;
diagram.add(this.box);
this.doMouseMove();
};
/**
* Release the mouse and remove any {@link #box}.
* @this {DragZoomingTool}
*/
DragZoomingTool.prototype.doDeactivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
diagram.remove(this.box);
diagram.skipsUndoManager = false;
diagram.isMouseCaptured = false;
this.isActive = false;
};
/**
* Update the {@link #box}'s position and size according to the value
* of {@link #computeBoxBounds}.
* @this {DragZoomingTool}
*/
DragZoomingTool.prototype.doMouseMove = function() {
var diagram = this.diagram;
if (diagram === null) return;
if (this.isActive && this.box !== null) {
var r = this.computeBoxBounds();
var shape = this.box.findObject("SHAPE");
if (shape === null) shape = this.box.findMainElement();
shape.desiredSize = r.size;
this.box.position = r.position;
}
};
/**
* Call {@link #zoomToRect} with the value of a call to {@link #computeBoxBounds}.
* @this {DragZoomingTool}
*/
DragZoomingTool.prototype.doMouseUp = function() {
if (this.isActive) {
var diagram = this.diagram;
diagram.remove(this.box);
try {
diagram.currentCursor = "wait";
this.zoomToRect(this.computeBoxBounds());
} finally {
diagram.currentCursor = "";
}
}
this.stopTool();
};
/**
* This just returns a {@link Rect} stretching from the mouse-down point to the current mouse point
* while maintaining the aspect ratio of the {@link #zoomedDiagram}.
* <p/>
* This method may be overridden.
* @this {DragZoomingTool}
* @return {Rect} a {@link Rect} in document coordinates.
*/
DragZoomingTool.prototype.computeBoxBounds = function() {
var diagram = this.diagram;
if (diagram === null) return new go.Rect(0, 0, 0, 0);
var start = diagram.firstInput.documentPoint;
var latest = diagram.lastInput.documentPoint;
var adx = latest.x - start.x;
var ady = latest.y - start.y;
var observed = this.zoomedDiagram;
if (observed === null) observed = this.diagram;
if (observed === null) {
return new go.Rect(start, latest);
}
var vrect = observed.viewportBounds;
if (vrect.height === 0 || ady === 0) {
return new go.Rect(start, latest);
}
var vratio = vrect.width / vrect.height;
var lx;
var ly;
if (Math.abs(adx / ady) < vratio) {
lx = start.x + adx;
ly = start.y + Math.ceil(Math.abs(adx) / vratio) * (ady < 0 ? -1 : 1);
} else {
lx = start.x + Math.ceil(Math.abs(ady) * vratio) * (adx < 0 ? -1 : 1);
ly = start.y + ady;
}
return new go.Rect(start, new go.Point(lx, ly));
};
/**
* This method is called to change the {@link #zoomedDiagram}'s viewport to match the given rectangle.
* <p/>
* This method may be overridden.
* @this {DragZoomingTool}
* @param {Rect} r a rectangular bounds in document coordinates.
*/
DragZoomingTool.prototype.zoomToRect = function(r) {
if (r.width < 0.1) return;
var observed = this.zoomedDiagram;
if (observed === null) observed = this.diagram;
if (observed === null) return;
// zoom out when using the Shift modifier
if (this.diagram.lastInput.shift) {
observed.scale = Math.max(observed.scale * r.width / observed.viewportBounds.width, observed.minScale);
observed.centerRect(r);
} else {
// do scale first, so the Diagram's position normalization isn't constrained unduly when increasing scale
observed.scale = Math.min(observed.viewportBounds.width * observed.scale / r.width, observed.maxScale);
observed.position = new go.Point(r.x, r.y);
}
};
// Public properties
/**
* Gets or sets the {@link Part} used as the "rubber-band zoom box"
* that is stretched to follow the mouse, as feedback for what area will
* be passed to {@link #zoomToRect} upon a mouse-up.
* <p/>
* Initially this is a {@link Part} containing only a simple magenta rectangular {@link Shape}.
* The object to be resized should be named "SHAPE".
* Setting this property does not raise any events.
* <p/>
* Modifying this property while this tool {@link Tool#isActive} might have no effect.
* @name DragZoomingTool#box
* @return {Part}
*/
Object.defineProperty(DragZoomingTool.prototype, "box", {
get: function() { return this._box; },
set: function(val) { this._box = val; }
});
/**
* Gets or sets the time in milliseconds for which the mouse must be stationary
* before this tool can be started.
* The default value is 175 milliseconds.
* Setting this property does not raise any events.
* @name DragZoomingTool#delay
* @return {number}
*/
Object.defineProperty(DragZoomingTool.prototype, "delay", {
get: function() { return this._delay; },
set: function(val) { this._delay = val; }
});
/**
* Gets or sets the {@link Diagram} whose {@link Diagram#position} and {@link Diagram#scale}
* should be set to display the drawn {@link #box} rectangular bounds.
* <p/>
* The default value is null, which causes {@link #zoomToRect} to modify this tool's {@link Tool#diagram}.
* Setting this property does not raise any events.
* @name DragZoomingTool#zoomedDiagram
* @return {Diagram}
*/
Object.defineProperty(DragZoomingTool.prototype, "zoomedDiagram", {
get: function() { return this._zoomedDiagram; },
set: function(val) { this._zoomedDiagram = val; }
});
+127
View File
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<title>Drawing Commands</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="The DrawCommandHandler extension implements various commands for aligning and rotating objects and for handling arrow keys to select or shift." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="DrawCommandHandler.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
commandHandler: new DrawCommandHandler(), // defined in DrawCommandHandler.js
"commandHandler.archetypeGroupData": { isGroup: true },
"undoManager.isEnabled": true // enable undo & redo
});
// define a simple Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the TextBlock
{ locationSpot: go.Spot.Center },
$(go.Shape, "RoundedRectangle", { strokeWidth: 0 },
// Shape.fill is bound to Node.data.color
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 }, // some room around the text
// TextBlock.text is bound to Node.data.key
new go.Binding("text", "key"))
);
// but use the default Link template, by not setting Diagram.linkTemplate
// create the model data that will be represented by Nodes and Links
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" },
{ key: "Gamma", color: "lightgreen" },
{ key: "Delta", color: "pink" }
],
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Gamma", to: "Delta" }
]);
}
function askSpace() {
var space = prompt("Desired space between nodes (in pixels):", "0");
return space;
}
// update arrowkey function
function arrowMode() {
// no transaction needed, because we are modifying the CommandHandler for future use
var move = document.getElementById("move");
var select = document.getElementById("select");
var scroll = document.getElementById("scroll");
var tree = document.getElementById("tree");
if (move.checked === true) {
myDiagram.commandHandler.arrowKeyBehavior = "move";
} else if (select.checked === true) {
myDiagram.commandHandler.arrowKeyBehavior = "select";
} else if (scroll.checked === true) {
myDiagram.commandHandler.arrowKeyBehavior = "scroll";
} else if (tree.checked === true) {
myDiagram.commandHandler.arrowKeyBehavior = "tree";
}
}
</script>
</head>
<body onload="init()">
<div id="sample">
<!-- The DIV for the Diagram needs an explicit size or else we won't see anything.
Also add a border to help see the edges. -->
<div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:400px"></div>
<p>
Align:
<button onclick="myDiagram.commandHandler.alignLeft()">Left Sides</button>
<button onclick="myDiagram.commandHandler.alignRight()">Right Sides</button>
<button onclick="myDiagram.commandHandler.alignTop()">Tops</button>
<button onclick="myDiagram.commandHandler.alignBottom()">Bottoms</button>
<button onclick="myDiagram.commandHandler.alignCenterX()">Center X</button>
<button onclick="myDiagram.commandHandler.alignCenterY()">Center Y</button>
<button onclick="myDiagram.commandHandler.alignRow(askSpace())">Row</button>
<button onclick="myDiagram.commandHandler.alignColumn(askSpace())">Column</button>
</br>
Rotate:
<button onclick="myDiagram.commandHandler.rotate(45)">45°</button>
<button onclick="myDiagram.commandHandler.rotate(-45)">-45°</button>
<button onclick="myDiagram.commandHandler.rotate(90)">90°</button>
<button onclick="myDiagram.commandHandler.rotate(-90)">-90°</button>
<button onclick="myDiagram.commandHandler.rotate(180)">180°</button>
</br>
Z-Order:
<button onclick="myDiagram.commandHandler.pullToFront()">Pull to Front</button>
<button onclick="myDiagram.commandHandler.pushToBack()">Push to Back</button>
</br>
Arrow Mode:
<input type="radio" name="arrow" id="move" onclick="arrowMode()" checked="checked">Move</input>
<input type="radio" name="arrow" id="select" onclick="arrowMode()">Select</input>
<input type="radio" name="arrow" id="scroll" onclick="arrowMode()">Scroll</input>
<input type="radio" name="arrow" id="tree" onclick="arrowMode()">Tree</input>
</p>
<p>
This example demonstrates a custom <a>CommandHandler</a>.
It allows the user to position selected Parts in a diagram relative to each other,
overrides <a>CommandHandler.doKeyDown</a> to allow handling the arrow keys in additional manners,
and uses a "paste offset" so that pasting objects will cascade them rather than place them on top of one another.
It is defined in its own file, as <a href="DrawCommandHandler.js">DrawCommandHandler.js</a>.
</p>
<p>
The above buttons can be used to align Parts, rotate Parts, or change the behavior of the arrow keys.
</p>
<p>
Usage can also be seen in the <a href="../projects/bpmn/BPMN.html">BPMN Editor</a> sample.
</p>
</div>
</body>
</html>
+653
View File
@@ -0,0 +1,653 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends CommandHandler
* @class
* This CommandHandler class allows the user to position selected Parts in a diagram
* relative to the first part selected, in addition to overriding the doKeyDown method
* of the CommandHandler for handling the arrow keys in additional manners.
* <p>
* Typical usage:
* <pre>
* $(go.Diagram, "myDiagramDiv",
* {
* commandHandler: $(DrawCommandHandler),
* . . .
* }
* )
* </pre>
* or:
* <pre>
* myDiagram.commandHandler = new DrawCommandHandler();
* </pre>*/
function DrawCommandHandler() {
go.CommandHandler.call(this);
this._arrowKeyBehavior = "move";
this._pasteOffset = new go.Point(10, 10);
this._lastPasteOffset = new go.Point(0, 0);
}
go.Diagram.inherit(DrawCommandHandler, go.CommandHandler);
/**
* This controls whether or not the user can invoke the {@link #alignLeft}, {@link #alignRight},
* {@link #alignTop}, {@link #alignBottom}, {@link #alignCenterX}, {@link #alignCenterY} commands.
* @this {DrawCommandHandler}
* @return {boolean}
* This returns true:
* if the diagram is not {@link Diagram#isReadOnly},
* if the model is not {@link Model#isReadOnly}, and
* if there are at least two selected {@link Part}s.
*/
DrawCommandHandler.prototype.canAlignSelection = function() {
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly || diagram.isModelReadOnly) return false;
if (diagram.selection.count < 2) return false;
return true;
};
/**
* Aligns selected parts along the left-most edge of the left-most part.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.alignLeft = function() {
var diagram = this.diagram;
diagram.startTransaction("aligning left");
var minPosition = Infinity;
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
minPosition = Math.min(current.position.x, minPosition);
});
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
current.move(new go.Point(minPosition, current.position.y));
});
diagram.commitTransaction("aligning left");
};
/**
* Aligns selected parts at the right-most edge of the right-most part.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.alignRight = function() {
var diagram = this.diagram;
diagram.startTransaction("aligning right");
var maxPosition = -Infinity;
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
var rightSideLoc = current.actualBounds.x + current.actualBounds.width;
maxPosition = Math.max(rightSideLoc, maxPosition);
});
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
current.move(new go.Point(maxPosition - current.actualBounds.width, current.position.y));
});
diagram.commitTransaction("aligning right");
};
/**
* Aligns selected parts at the top-most edge of the top-most part.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.alignTop = function() {
var diagram = this.diagram;
diagram.startTransaction("alignTop");
var minPosition = Infinity;
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
minPosition = Math.min(current.position.y, minPosition);
});
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
current.move(new go.Point(current.position.x, minPosition));
});
diagram.commitTransaction("alignTop");
};
/**
* Aligns selected parts at the bottom-most edge of the bottom-most part.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.alignBottom = function() {
var diagram = this.diagram;
diagram.startTransaction("aligning bottom");
var maxPosition = -Infinity;
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
var bottomSideLoc = current.actualBounds.y + current.actualBounds.height;
maxPosition = Math.max(bottomSideLoc, maxPosition);
});
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
current.move(new go.Point(current.actualBounds.x, maxPosition - current.actualBounds.height));
});
diagram.commitTransaction("aligning bottom");
};
/**
* Aligns selected parts at the x-value of the center point of the first selected part.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.alignCenterX = function() {
var diagram = this.diagram;
var firstSelection = diagram.selection.first();
if (!firstSelection) return;
diagram.startTransaction("aligning Center X");
var centerX = firstSelection.actualBounds.x + firstSelection.actualBounds.width / 2;
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
current.move(new go.Point(centerX - current.actualBounds.width / 2, current.actualBounds.y));
});
diagram.commitTransaction("aligning Center X");
};
/**
* Aligns selected parts at the y-value of the center point of the first selected part.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.alignCenterY = function() {
var diagram = this.diagram;
var firstSelection = diagram.selection.first();
if (!firstSelection) return;
diagram.startTransaction("aligning Center Y");
var centerY = firstSelection.actualBounds.y + firstSelection.actualBounds.height / 2;
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
current.move(new go.Point(current.actualBounds.x, centerY - current.actualBounds.height / 2));
});
diagram.commitTransaction("aligning Center Y");
};
/**
* Aligns selected parts top-to-bottom in order of the order selected.
* Distance between parts can be specified. Default distance is 0.
* @this {DrawCommandHandler}
* @param {number} distance
*/
DrawCommandHandler.prototype.alignColumn = function(distance) {
var diagram = this.diagram;
diagram.startTransaction("align Column");
if (distance === undefined) distance = 0; // for aligning edge to edge
distance = parseFloat(distance);
var selectedParts = new Array();
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
selectedParts.push(current);
});
for (var i = 0; i < selectedParts.length - 1; i++) {
var current = selectedParts[i];
// adds distance specified between parts
var curBottomSideLoc = current.actualBounds.y + current.actualBounds.height + distance;
var next = selectedParts[i + 1];
next.move(new go.Point(current.actualBounds.x, curBottomSideLoc));
}
diagram.commitTransaction("align Column");
};
/**
* Aligns selected parts left-to-right in order of the order selected.
* Distance between parts can be specified. Default distance is 0.
* @this {DrawCommandHandler}
* @param {number} distance
*/
DrawCommandHandler.prototype.alignRow = function(distance) {
if (distance === undefined) distance = 0; // for aligning edge to edge
distance = parseFloat(distance);
var diagram = this.diagram;
diagram.startTransaction("align Row");
var selectedParts = new Array();
diagram.selection.each(function(current) {
if (current instanceof go.Link) return; // skips over go.Link
selectedParts.push(current);
});
for (var i = 0; i < selectedParts.length - 1; i++) {
var current = selectedParts[i];
// adds distance specified between parts
var curRightSideLoc = current.actualBounds.x + current.actualBounds.width + distance;
var next = selectedParts[i + 1];
next.move(new go.Point(curRightSideLoc, current.actualBounds.y));
}
diagram.commitTransaction("align Row");
};
/**
* This controls whether or not the user can invoke the {@link #rotate} command.
* @this {DrawCommandHandler}
* @param {number=} angle the positive (clockwise) or negative (counter-clockwise) change in the rotation angle of each Part, in degrees.
* @return {boolean}
* This returns true:
* if the diagram is not {@link Diagram#isReadOnly},
* if the model is not {@link Model#isReadOnly}, and
* if there is at least one selected {@link Part}.
*/
DrawCommandHandler.prototype.canRotate = function(number) {
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly || diagram.isModelReadOnly) return false;
if (diagram.selection.count < 1) return false;
return true;
};
/**
* Change the angle of the parts connected with the given part. This is in the command handler
* so it can be easily accessed for the purpose of creating commands that change the rotation of a part.
* @this {DrawCommandHandler}
* @param {number=} angle the positive (clockwise) or negative (counter-clockwise) change in the rotation angle of each Part, in degrees.
*/
DrawCommandHandler.prototype.rotate = function(angle) {
if (angle === undefined) angle = 90;
var diagram = this.diagram;
diagram.startTransaction("rotate " + angle.toString());
var diagram = this.diagram;
diagram.selection.each(function(current) {
if (current instanceof go.Link || current instanceof go.Group) return; // skips over Links and Groups
current.angle += angle;
});
diagram.commitTransaction("rotate " + angle.toString());
};
/**
* Change the z-ordering of selected parts to pull them forward, in front of all other parts
* in their respective layers.
* All unselected parts in each layer with a selected Part with a non-numeric {@link Part#zOrder} will get a zOrder of zero.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.pullToFront = function() {
var diagram = this.diagram;
diagram.startTransaction("pullToFront");
// find the affected Layers
var layers = new go.Map();
diagram.selection.each(function(part) {
layers.set(part.layer, 0);
});
// find the maximum zOrder in each Layer
layers.iteratorKeys.each(function(layer) {
var max = 0;
layer.parts.each(function(part) {
if (part.isSelected) return;
var z = part.zOrder;
if (isNaN(z)) {
part.zOrder = 0;
} else {
max = Math.max(max, z);
}
});
layers.set(layer, max);
});
// assign each selected Part.zOrder to the computed value for each Layer
diagram.selection.each(function(part) {
DrawCommandHandler._assignZOrder(part, layers.get(part.layer) + 1);
});
diagram.commitTransaction("pullToFront");
};
/**
* Change the z-ordering of selected parts to push them backward, behind of all other parts
* in their respective layers.
* All unselected parts in each layer with a selected Part with a non-numeric {@link Part#zOrder} will get a zOrder of zero.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype.pushToBack = function() {
var diagram = this.diagram;
diagram.startTransaction("pushToBack");
// find the affected Layers
var layers = new go.Map();
diagram.selection.each(function(part) {
layers.set(part.layer, 0);
});
// find the minimum zOrder in each Layer
layers.iteratorKeys.each(function(layer) {
var min = 0;
layer.parts.each(function(part) {
if (part.isSelected) return;
var z = part.zOrder;
if (isNaN(z)) {
part.zOrder = 0;
} else {
min = Math.min(min, z);
}
});
layers.set(layer, min);
});
// assign each selected Part.zOrder to the computed value for each Layer
diagram.selection.each(function(part) {
DrawCommandHandler._assignZOrder(part,
// make sure a group's nested nodes are also behind everything else
layers.get(part.layer) - 1 - DrawCommandHandler._findGroupDepth(part));
});
diagram.commitTransaction("pushToBack");
};
DrawCommandHandler._assignZOrder = function(part, z, root) {
if (root === undefined) root = part;
if (part.layer === root.layer) part.zOrder = z;
if (part instanceof go.Group) {
part.memberParts.each(function(m) {
DrawCommandHandler._assignZOrder(m, z+1, root);
});
}
};
DrawCommandHandler._findGroupDepth = function(part) {
if (part instanceof go.Group) {
var d = 0;
part.memberParts.each(function(m) {
d = Math.max(d, DrawCommandHandler._findGroupDepth(m));
});
return d+1;
} else {
return 0;
}
};
/**
* This implements custom behaviors for arrow key keyboard events.
* Set {@link #arrowKeyBehavior} to "select", "move" (the default), "scroll" (the standard behavior), or "none"
* to affect the behavior when the user types an arrow key.
* @this {DrawCommandHandler}*/
DrawCommandHandler.prototype.doKeyDown = function() {
var diagram = this.diagram;
if (diagram === null) return;
var e = diagram.lastInput;
// determines the function of the arrow keys
if (e.key === "Up" || e.key === "Down" || e.key === "Left" || e.key === "Right") {
var behavior = this.arrowKeyBehavior;
if (behavior === "none") {
// no-op
return;
} else if (behavior === "select") {
this._arrowKeySelect();
return;
} else if (behavior === "move") {
this._arrowKeyMove();
return;
} else if (behavior === "tree") {
this._arrowKeyTree();
return;
}
// otherwise drop through to get the default scrolling behavior
}
// otherwise still does all standard commands
go.CommandHandler.prototype.doKeyDown.call(this);
};
/**
* Collects in an Array all of the non-Link Parts currently in the Diagram.
* @this {DrawCommandHandler}
* @return {Array}
*/
DrawCommandHandler.prototype._getAllParts = function() {
var allParts = new Array();
this.diagram.nodes.each(function(node) { allParts.push(node); });
this.diagram.parts.each(function(part) { allParts.push(part); });
// note that this ignores Links
return allParts;
};
/**
* To be called when arrow keys should move the Diagram.selection.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype._arrowKeyMove = function() {
var diagram = this.diagram;
var e = diagram.lastInput;
// moves all selected parts in the specified direction
var vdistance = 0;
var hdistance = 0;
// if control is being held down, move pixel by pixel. Else, moves by grid cell size
if (e.control || e.meta) {
vdistance = 1;
hdistance = 1;
} else if (diagram.grid !== null) {
var cellsize = diagram.grid.gridCellSize;
hdistance = cellsize.width;
vdistance = cellsize.height;
}
diagram.startTransaction("arrowKeyMove");
diagram.selection.each(function(part) {
if (e.key === "Up") {
part.move(new go.Point(part.actualBounds.x, part.actualBounds.y - vdistance));
} else if (e.key === "Down") {
part.move(new go.Point(part.actualBounds.x, part.actualBounds.y + vdistance));
} else if (e.key === "Left") {
part.move(new go.Point(part.actualBounds.x - hdistance, part.actualBounds.y));
} else if (e.key === "Right") {
part.move(new go.Point(part.actualBounds.x + hdistance, part.actualBounds.y));
}
});
diagram.commitTransaction("arrowKeyMove");
};
/**
* To be called when arrow keys should change selection.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype._arrowKeySelect = function() {
var diagram = this.diagram;
var e = diagram.lastInput;
// with a part selected, arrow keys change the selection
// arrow keys + shift selects the additional part in the specified direction
// arrow keys + control toggles the selection of the additional part
var nextPart = null;
if (e.key === "Up") {
nextPart = this._findNearestPartTowards(270);
} else if (e.key === "Down") {
nextPart = this._findNearestPartTowards(90);
} else if (e.key === "Left") {
nextPart = this._findNearestPartTowards(180);
} else if (e.key === "Right") {
nextPart = this._findNearestPartTowards(0);
}
if (nextPart !== null) {
if (e.shift) {
nextPart.isSelected = true;
} else if (e.control || e.meta) {
nextPart.isSelected = !nextPart.isSelected;
} else {
diagram.select(nextPart);
}
}
};
/**
* Finds the nearest Part in the specified direction, based on their center points.
* if it doesn't find anything, it just returns the current Part.
* @this {DrawCommandHandler}
* @param {number} dir the direction, in degrees
* @return {Part} the closest Part found in the given direction
*/
DrawCommandHandler.prototype._findNearestPartTowards = function(dir) {
var originalPart = this.diagram.selection.first();
if (originalPart === null) return null;
var originalPoint = originalPart.actualBounds.center;
var allParts = this._getAllParts();
var closestDistance = Infinity;
var closest = originalPart; // if no parts meet the criteria, the same part remains selected
for (var i = 0; i < allParts.length; i++) {
var nextPart = allParts[i];
if (nextPart === originalPart) continue; // skips over currently selected part
var nextPoint = nextPart.actualBounds.center;
var angle = originalPoint.directionPoint(nextPoint);
var anglediff = this._angleCloseness(angle, dir);
if (anglediff <= 45) { // if this part's center is within the desired direction's sector,
var distance = originalPoint.distanceSquaredPoint(nextPoint);
distance *= 1+Math.sin(anglediff*Math.PI/180); // the more different from the intended angle, the further it is
if (distance < closestDistance) { // and if it's closer than any other part,
closestDistance = distance; // remember it as a better choice
closest = nextPart;
}
}
}
return closest;
};
/**
* @this {DrawCommandHandler}
* @param {number} a
* @param {number} dir
* @return {number}
*/
DrawCommandHandler.prototype._angleCloseness = function(a, dir) {
return Math.min(Math.abs(dir - a), Math.min(Math.abs(dir + 360 - a), Math.abs(dir - 360 - a)));
};
/**
* To be called when arrow keys should change the selected node in a tree and expand or collapse subtrees.
* @this {DrawCommandHandler}
*/
DrawCommandHandler.prototype._arrowKeyTree = function() {
var diagram = this.diagram;
var selected = diagram.selection.first();
if (!(selected instanceof go.Node)) return;
var e = diagram.lastInput;
if (e.key === "Right") {
if (selected.isTreeLeaf) {
// no-op
} else if (!selected.isTreeExpanded) {
if (diagram.commandHandler.canExpandTree(selected)) {
diagram.commandHandler.expandTree(selected); // expands the tree
}
} else { // already expanded -- select the first child node
var first = this._sortTreeChildrenByY(selected).first();
if (first !== null) diagram.select(first);
}
} else if (e.key === "Left") {
if (!selected.isTreeLeaf && selected.isTreeExpanded) {
if (diagram.commandHandler.canCollapseTree(selected)) {
diagram.commandHandler.collapseTree(selected); // collapses the tree
}
} else { // either a leaf or is already collapsed -- select the parent node
var parent = selected.findTreeParentNode();
if (parent !== null) diagram.select(parent);
}
} else if (e.key === "Up") {
var parent = selected.findTreeParentNode();
if (parent !== null) {
var list = this._sortTreeChildrenByY(parent);
var idx = list.indexOf(selected);
if (idx > 0) { // if there is a previous sibling
var prev = list.elt(idx - 1);
// keep looking at the last child until it's a leaf or collapsed
while (prev !== null && prev.isTreeExpanded && !prev.isTreeLeaf) {
var children = this._sortTreeChildrenByY(prev);
prev = children.last();
}
if (prev !== null) diagram.select(prev);
} else { // no previous sibling -- select parent
diagram.select(parent);
}
}
} else if (e.key === "Down") {
// if at an expanded parent, select the first child
if (selected.isTreeExpanded && !selected.isTreeLeaf) {
var first = this._sortTreeChildrenByY(selected).first();
if (first !== null) diagram.select(first);
} else {
while (selected !== null) {
var parent = selected.findTreeParentNode();
if (parent === null) break;
var list = this._sortTreeChildrenByY(parent);
var idx = list.indexOf(selected);
if (idx < list.length - 1) { // select next lower node
diagram.select(list.elt(idx + 1));
break;
} else { // already at bottom of list of children
selected = parent;
}
}
}
}
// make sure the selection is now in the viewport, but not necessarily centered
var sel = diagram.selection.first();
if (sel !== null) diagram.scrollToRect(sel.actualBounds);
}
DrawCommandHandler.prototype._sortTreeChildrenByY = function(node) {
var list = new go.List().addAll(node.findTreeChildrenNodes());
list.sort(function(a, b) {
var aloc = a.location;
var bloc = b.location;
if (aloc.y < bloc.y) return -1;
if (aloc.y > bloc.y) return 1;
if (aloc.x < bloc.x) return -1;
if (aloc.x > bloc.x) return 1;
return 0;
});
return list;
};
/**
* Reset the last offset for pasting.
* @this {DrawCommandHandler}
* @param {Iterable.<Part>} coll a collection of {@link Part}s.
*/
DrawCommandHandler.prototype.copyToClipboard = function(coll) {
go.CommandHandler.prototype.copyToClipboard.call(this, coll);
this._lastPasteOffset.set(this.pasteOffset);
};
/**
* Paste from the clipboard with an offset incremented on each paste, and reset when copied.
* @this {DrawCommandHandler}
* @return {Set.<Part>} a collection of newly pasted {@link Part}s
*/
DrawCommandHandler.prototype.pasteFromClipboard = function() {
var coll = go.CommandHandler.prototype.pasteFromClipboard.call(this);
this.diagram.moveParts(coll, this._lastPasteOffset);
this._lastPasteOffset.add(this.pasteOffset);
return coll;
};
/**
* Gets or sets the arrow key behavior. Possible values are "move", "select", "scroll", and "tree".
* The default value is "move".
* @name DrawCommandHandler#arrowKeyBehavior
* @return {string}
*/
Object.defineProperty(DrawCommandHandler.prototype, "arrowKeyBehavior", {
get: function() { return this._arrowKeyBehavior; },
set: function(val) {
if (val !== "move" && val !== "select" && val !== "scroll" && val !== "tree" && val !== "none") {
throw new Error("DrawCommandHandler.arrowKeyBehavior must be either \"move\", \"select\", \"scroll\", \"tree\", or \"none\", not: " + val);
}
this._arrowKeyBehavior = val;
}
});
/**
* Gets or sets the offset at which each repeated pasteSelection() puts the new copied parts from the clipboard.
* The default value is (10,10).
* @name DrawCommandHandler#pasteOffset
* @return {Point}
*/
Object.defineProperty(DrawCommandHandler.prototype, "pasteOffset", {
get: function() { return this._pasteOffset; },
set: function(val) {
if (!(val instanceof go.Point)) throw new Error("DrawCommandHandler.pasteOffset must be a Point, not: " + val);
this._pasteOffset.set(val);
}
});
+796
View File
@@ -0,0 +1,796 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
function ExtendedBrush(type) {
if (arguments.length === 0)
go.Brush.call(this);
else
go.Brush.call(this, type);
}
go.Diagram.inherit(ExtendedBrush, go.Brush);
ExtendedBrush['parse'] =
/**
* This static method can be used to read in a {@link Brush} from a string that was produced by {@link ExtendedBrush.stringify}.
* @param {string} str
* @return {Brush}
*/
ExtendedBrush.parse = function(str, w, h) {
if (str.indexOf("linear") !== -1) {
return ExtendedBrush._parseLinearGradientCSS(str, w, h);
} else if (str.indexOf("radial") !== -1) {
return ExtendedBrush._parseRadialGradientCSS(str, w, h);
} else if (go.Brush.isValidColor(str)) {
var b = new go.Brush(go.Brush.Solid);
b.color = str;
return b;
} else { //only works with image urls right now
//TODO deal with Canvas elements
var b = new go.Brush(go.Brush.Pattern);
var image = document.createElement("img");
image.src = str;
b.pattern = image;
return b;
}
};
ExtendedBrush['stringify'] =
/**
* This static method can be used to write out a {@link Brush} as a string that can be read by {@link Brush.parse}.
* @param {Brush} val
* @return {string}
*/
ExtendedBrush.stringify = function(val) {
if (!(val instanceof go.Brush)) throw new Error("ExtendedBrush.stringify requires a Brush argument, not: " + val);
var str = "";
if (val.type === go.Brush.Solid) {
return val.color;
} else if (val.type === go.Brush.Linear) {
str = "linear-gradient(";
var ang = ExtendedBrush._angleBetweenSpots(val);
if (isNaN(ang)) ang = 180;
str += ang + "deg";
str += ExtendedBrush._convertStopsToCSS(val);
str += ")";
} else if (val.type === go.Brush.Radial) {
str = "radial-gradient(";
if (val.endRadius) str += Math.round(val.endRadius) + "px ";
if (val.ellipseHeight) str += Math.round(val.ellipseHeight) + "px "; //temp until we figure out canvas scaling
str += "at ";
str += val.start.x * 100 + "% ";
str += val.start.y * 100 + "% ";
str += ExtendedBrush._convertStopsToCSS(val);
str += ")";
} else if (val.type === go.Brush.Pattern) {
if (val.pattern) str = val.pattern.getAttribute("src");
}
return str;
}
ExtendedBrush._convertStopsToCSS = function(brush) {
var it = brush.colorStops.iterator;
var str = "";
while (it.next()) {
str += ", " + it.value + " " + it.key * 100 + "%";
}
return str;
};
ExtendedBrush._UNITS = ["px", "pt", "pc", "in", "cm", "mm"];
ExtendedBrush._lengthToPX = function(length, unit) {
var pxPerInch = 96;
var cmPerInch = 2.54;
switch (unit) {
case "px": return length;
case "pt": return length * 3 / 4;
case "pc": return length * 9;
case "in": return length * pxPerInch;
case "cm": return length * pxPerInch / cmPerInch;
case "mm": return length * pxPerInch / (cmPerInch * 10);
default: return NaN;
}
};
ExtendedBrush._pxToPercent = function(px, l, w, angle) {
angle = parseFloat(angle);
if (angle % 180 === 0) return px / l;
angle *= Math.PI / 180;
return px / (Math.abs(w * Math.sin(angle)) + Math.abs(l * Math.cos(angle)));
}
ExtendedBrush._parseLinearGradientCSS = function(cssstring, w, h) {
var css = cssstring.match(/\((.*)\)/g);
if (css === null) throw new Error("Invalid CSS Linear Gradient: " + cssstring);
css = css[0];
//removes outer parentheses
css = css.substring(1, css.length - 1);
//splits string into components at commas not within parentheses
//css = css.split(/,+(?![^\(]*\))/g);
css = css.split(/,(?![^\(]*\))/g);
css[0] = css[0].trim();
var isValidColor = go.Brush.isValidColor(css[0].split(/\s(?![^\(]*\))/g)[0]);
if (isValidColor) {
// if the first param isn't a color, it's a CSS <angle>
// or a malformed attempt at a color, such as "blavk"
// if input's good it works for now, TODO improve later.
css.splice(0, 0, "180deg");
}
//standardizes any angle measurement or direction to degrees
css[0] = ExtendedBrush._linearGradientAngleToDegrees(css[0], w, h);
var angle = parseFloat(css[0]) + 180; //adjusts for css having 180 as the default start point
//converts color/percent strings to array objects
var colors = ExtendedBrush._createColorStopArray(css);
/* by now we have the list of color stops, and the computed angle. The color stops need to be bound in a map that the Brush class will like,
and the angle needs to be computed with the dimensions of the thing that the brush is coloring, in order to supply the brush with it's start
and end spots. once that stuff is computed, stick them on the Brush 'b', below, and return it.
*/
var b = new go.Brush(go.Brush.Linear);
var spots = ExtendedBrush._calculateLinearGradientSpots(angle, w, h);
b.start = spots[0];
b.end = spots[1];
for (var i = 0; i < colors.length; i++) {
b.addColorStop(colors[i].position, colors[i].color);
}
return b;
}
//parses array of gradient parameters to color stop array. used by both gradient parsers
ExtendedBrush._createColorStopArray = function(css) {
var colors = [];
for (var i = 1; i < css.length; i++) {
css[i] = css[i].trim();
var arr = css[i].split(/\s+(?![^\(]*\))/g);//whitespace not within parentheses
var value;
var obj = {};
if (!go.Brush.isValidColor(arr[0])) {
throw new Error("Invalid CSS Color in Linear Gradient: " + arr[0] + " in " + css);
}
if (arr[1] !== undefined) {
// we have a measurement
var unit = arr[1].match(/[^\d.]+/g)[0];
if (ExtendedBrush._UNITS.indexOf(unit) !== -1) {
arr[1] = parseFloat(arr[1]); // bites off anything not a number: "90px" -> 90.0 ... "px" is still stored in unit
var len = ExtendedBrush._lengthToPX(arr[1], unit);
obj["position"] = ExtendedBrush._pxToPercent(len, w, h, angle);
} else if (unit === "%") {
obj["position"] = parseFloat(arr[1]) / 100;
} else {
throw new Error("Invalid Linear Gradient Unit: " + unit + " in " + css);
}
} else {
obj["position"] = NaN;
}
obj["color"] = arr[0];
colors[i - 1] = obj;
}
if (isNaN(colors[0].position)) colors[0].position = 0;
if (isNaN(colors[colors.length - 1].position)) colors[colors.length - 1].position = 1;
//recursively fills in missing percents in the array
ExtendedBrush._fixLinearGradientPositions(colors);
return colors;
}
ExtendedBrush._fixLinearGradientPositions = function(arr, start, end) {
if (start === undefined) start = 0;
if (end === undefined) end = arr.length;
while (start < end - 1 && !isNaN(arr[start + 1].position)) start++;
if (start === end - 1) return;
var tempEnd = start + 1;
while (tempEnd < end && isNaN(arr[tempEnd].position)) tempEnd++;
var step = (arr[tempEnd].position - arr[start].position) / (tempEnd - start);
for (var i = 1; i < tempEnd - start; i++) {
arr[i + start].position = Math.round((arr[start].position + i * step) * 1000) / 1000;
}
if (tempEnd < end - 1) {
ExtendedBrush._fixLinearGradientPositions(arr, tempEnd, end);
}
};
ExtendedBrush._calculateLinearGradientSpots = function(angle, w, h) {
angle = parseFloat(angle);
angle = (angle % 360 + 360) % 360;
if (angle === 90) return [new go.Spot(0, 0, w, h / 2), new go.Spot(0, 0, 0, h / 2)];
if (angle === 270) return [new go.Spot(0, 0, 0, h / 2), new go.Spot(0, 0, w, h / 2)];
var tempAngle = -Math.abs((angle % 180) - 90) + 90; 90
var tan = Math.tan(tempAngle * Math.PI / 180);
var x = (h * tan - w) * 0.5 / (tan * tan + 1);
var y = x * tan;
if (angle >= 90 && angle <= 270) y = h - y;
if (angle < 180)
x = w + x;
else
x = -x;
return ([new go.Spot(0, 0, x, y), new go.Spot(0, 0, w - x, h - y)]);
};
ExtendedBrush._applyLinearGradientSpots = function(angle, w, h, brush) {
var spots = ExtendedBrush._calculateLinearGradientSpots(angle, w, h);
brush.start = spots[0];
brush.end = spots[1];
return brush;
}
ExtendedBrush._linearGradientAngleToDegrees = function(string, w, h) {
//true if there is a "to " at the start of the first parameter,
//indicating that a direction was specified rather than angle
var isNumericalInput = string.indexOf("to ") < 0;
//0s without units still accepted
var digit_arr = string.match(/\d/g);
var zero_arr = string.match(/0/g);
if (zero_arr !== null && (digit_arr.length === zero_arr.length)) return 0;
if (isNumericalInput) {
string = string.match(/[^a-z]+|\D+/g);
switch (string[1]) {
case ("deg"): return string[0];
case ("rad"): return string[0] * 180 / Math.PI;
case ("turn"): return string[0] * 360;
case ("grad"): return string[0] * 9 / 10;
default: throw new Error("Invalid CSS Linear Gradient direction: " + string[1]);
}
} else {
var direction = 0;
if (string.indexOf("right") >= 0) direction = 90;
else if (string.indexOf("left") >= 0) direction = -90;
var sign = direction === 0 ? 0 : direction > 0 ? 1 : -1; //needed because Chrome/IE/Safari/Opera/Mosaic/Netscape don't support Math.sign()
if (string.indexOf("top") >= 0) {
direction -= sign * Math.atan(w / h) * 180 / Math.PI;
} else if (string.indexOf("bottom") >= 0) {
if (direction === 0) direction = 180;
else direction += sign * Math.atan(w / h) * 180 / Math.PI
}
if (direction === 0 && string.indexOf("top") < 0) {
throw new Error("Invalid CSS Linear Gradient direction: " + string);
}
return Math.round(direction);
}
};
ExtendedBrush._angleBetweenSpots = function(brush) {
var start = brush.start;
var end = brush.end;
if (isNaN(start.x + start.y + end.x + end.y))
throw new Error("The brush does not have valid spots");
var x = end.offsetX - start.offsetX;
var y = start.offsetY - end.offsetY;
var angle = Math.atan((start.offsetX - end.offsetX) / (end.offsetY - start.offsetY)) * 180 / Math.PI;
if (start.offsetY > end.offsetY) angle += 180;
return (angle + 360) % 360;
}
ExtendedBrush._parseRadialGradientCSS = function(css, w, h) {
//removes browser specific tags
css = css.match(/\((.*)\)/g);
if (css === null) {
throw new Error("Invalid CSS Linear Gradient");
}
css = css[0];
css = css.substring(1, css.length - 1);
//splits string into components at commas not within parenthesesd and removes whitespace
css = css.split(/,(?![^\(]*\))/g);
css[0] = css[0].trim();
var isValidColor = go.Brush.isValidColor(css[0].split(" ")[0]);
//default shape paramenters
var radii = [w / 2, h / 2]; //stores two radii
var center = new go.Spot(0.5, 0.5, 0, 0); //stores the center of the gradient
//goes through all cases to set radii and center
if (!isValidColor) { //parses only if there were intial parameters specified
//could be only a partial shape/position specification
var shape = css[0]; //first specified parameter of gradient
shape = shape.split("at")
if (shape.length === 1) {
center = new go.Spot(0.5, 0.5, 0, 0); //no center was specified, so it must be "at center";
}
else if (shape.length === 2) { //assigns something to center. one must have been specified if length===2
center = ExtendedBrush._parseCenter(shape[1], w, h);
}
else {
throw new Error("invalid css radial gradient string");
}
//uses center to calculate radii
radii = ExtendedBrush._parseShapeDescription(shape[0], w, h, center);
}
var colors = ExtendedBrush._createColorStopArray(css);
// console.log("RADIAL: ", "\nCENTER: ", center, "\nRADII: ", radii, "\nCOLORS: ", colors);
// assigns stops, center, and radii to a brush
var b = new go.Brush(go.Brush.Radial);
b.start = center; //concentric in CSS, so start and end are the same
b.end = center;
b.startRadius = 0; //css starts at 0 automatically
b.endRadius = radii[0]; //end radius is the width of the ending shape
//TEMP until we implement scaling the gradient in canvas
b.ellipseHeight = radii[1];
for (var i = 0; i < colors.length; i++) {
b.addColorStop(colors[i].position, colors[i].color);
}
return b;
}
//parses a position string to determine where the radial gradient is centered
ExtendedBrush._parseCenter = function(str, w, h) { //h and w values are only needed if pixels are being used
var arr = []; //stores the x and y coodinates
str = str.trim();
var parts = str.split(/\s+/g);
if (parts.length === 1) {
arr = ExtendedBrush._englishPositionToCoordinate(parts[0]);
} else if (parts.length === 2) {
var digits = str.match(/\d+/g);
//if specified only with english, no percents/pixels, compute center
if (digits === null) {
arr = ExtendedBrush._englishPositionToCoordinate(str);
} else if (digits.length === 1) { //know one of the params are numbers
//if the first param is a string, second is a number
var num;
var pos;
if (parts[0].match(/\d+/g) === null) {
pos = parts[0];
num = parts[1];
} else { //if second param is string and first is number
pos = parts[1];
num = parts[0];
}
num = ExtendedBrush._parseLengthToPercent(num);
//correctly assigns the width coordinate
arr = ExtendedBrush._englishPositionToCoordinate(pos);
//overwrites height to the user specified value
arr[1] = num;
} else if (digits.length === 2) { //both the params are numbers
arr[0] = ExtendedBrush._parseLengthToPercent(parts[0], w);
arr[1] = ExtendedBrush._parseLengthToPercent(parts[1], h);
}
} else if (parts.length === 3) {
//TODO must be two positions and a number
//correct behavior here is not known, only works in IE. need to check css spec
} else if (parts.length === 4) {
switch (parts[0]) {
case ("right"):
arr[0] = 1 - ExtendedBrush._parseLengthToPercent(arr[1], w);
case ("left"):
arr[0] = ExtendedBrush._parseLengthToPercent(arr[1], w);
default:
throw new Error("invalid location keyword: " + parts[0]);
}
switch (parts[2]) {
case ("top"):
arr[0] = ExtendedBrush._parseLengthToPercent(arr[3], h);
case ("bottom"):
arr[0] = 1 - ExtendedBrush._parseLengthToPercent(arr[3], h);
default:
throw new Error("invalid location keyword: " + parts[2]);
}
} else {
throw new Error("invalid CSS position description");
}
return new go.Spot(arr[0], arr[1], 0, 0);
}
ExtendedBrush._parseLengthToPercent = function(str, dimension) {
if (str.indexOf("%") < 0) {//if specified by a length unit
return ExtendedBrush._parseLengthToPX(str) / (dimension);
} else { //if specified by percentage
return parseFloat(str) / 100;
}
}
ExtendedBrush._parseLengthToPX = function(str, dimension) {
var len = parseFloat(str.match(/\d+/g)[0]);
var unit = str.match(/\D+/g)[0];
return ExtendedBrush._lengthToPX(len, unit)
}
ExtendedBrush._englishPositionToCoordinate = function(str) {
var x = .5;
var y = .5;
if (str.indexOf("bottom") > -1)
y = 1;
else if (str.indexOf("top") > -1)
y = 0;
if (str.indexOf("left") > -1)
x = 0;
else if (str.indexOf("right") > -1)
x = 1;
return [x, y];
}
//needs to return something at some point
ExtendedBrush._parseShapeDescription = function(str, w, h, center) {
var x = center.x;
var y = center.y;
str = str.trim();
var split = str.split(" ");
if (split === null) return [w / 2, h / 2];
if (split.length === 1) {
//distance from center to farthest side horizontally and vertically
var a = Math.abs(w / 2 - x * w) + w / 2;
var b = Math.abs(h / 2 - y * h) + h / 2;
if (str.indexOf("ellipse") > -1) {
return [a * Math.sqrt(2), b * Math.sqrt(2)];
}
else if (str.indexOf("circle") > -1) {
var r = Math.sqrt(a * a + b * b);
return [r, r];
}
else {
//must not contain explicit shape paramenters, only an extent, so parse that
return ExtendedBrush._parseExtentKeyword(str, w, h, center);
}
} else if (split.length === 2) {
var digits = str.match(/\d+/g);
if (digits === null) {
//must be shape and extent
return ExtendedBrush._parseExtentKeyword(str, w, h, center);
} else if (digits.length === 1) {
//only valid description would be "circle" and a radius specified as a length, NOT a percentage
if (split[0].indexOf("circle") > -1 && split[1].indexOf("%") < 0) {
var r = ExtendedBrush._parseLengthToPX(split[1]);
return [r, r];
} else
throw new Error("Invalid CSS shape description");
} else if (digits.length === 2) { //must both be ellipse radii
return [ExtendedBrush._parseLengthToPX(split[0]),
ExtendedBrush._parseLengthToPX(split[1])];
} else {
throw new Error("Invalid CSS shape description");
}
} else if (split.length === 3) {
if (split[0] !== "ellipse")
throw new Error("invalid CSS shape description");
return [ExtendedBrush._parseLengthToPercent(split[1], w) * w,
ExtendedBrush._parseLengthToPercent(split[2], h) * h]
} else {
throw new Error("invalid CSS shape description");
}
}
ExtendedBrush._parseExtentKeyword = function(str, w, h, center) {
if (!str) return [w / 2, h / 2];
var x = center.x;
var y = center.y;
//distance from center to farthest side horizontally and vertically
var a = Math.abs(w / 2 - x * w) + w / 2;
var b = Math.abs(h / 2 - y * h) + h / 2;
var split = str.split(" ");
var extent; //stores the extent keyword
var arr; //stores the radii to be returned
if (split === null)
throw new Error("invalid extent keyword");
else
extent = split[split.length - 1];
//either a circle or an ellipse, nothing specified defaults to ellipse
if (str.indexOf("circle") > -1) {
switch (extent) {
case ("closest-corner"):
var r = Math.sqrt((w - a) * (w - a) + (h - b) * (h - b));
break;
case ("farthest-corner"):
var r = Math.sqrt(a * a + b * b);
break;
case ("closest-side"):
var r = Math.min(w - a, h - b);
break;
case ("farthest-side"):
var r = Math.max(a, b);
break;
default:
throw new Error("invalid extent keyword: " + extent);
}
return [r, r]
} else { //must be an ellipse
switch (extent) {
case ("closest-corner"):
return [(w - a) * Math.sqrt(2), (h - b) * Math.sqrt(2)];
case ("farthest-corner"):
return [a * Math.sqrt(2), b * Math.sqrt(2)];
case ("closest-side"):
return [w - a, h - b];
case ("farthest-side"):
return [a, b];
default:
throw new Error("invalid extent keyword: " + extent);
}
}
}
ExtendedBrush._makePaletteFromOneColor = function(color, number) {
var colorArr = ExtendedBrush._RGB_to_Lab(ExtendedBrush.CSSStringToRGB(color));
var arr = [];
var inc = 100 / (number + 2);
var numBelow = Math.floor(colorArr[0] / inc) - 1;
for (var i = 1; i <= number; i++) {
arr[i - 1] = go.Brush.lightenBy(color, inc * (i - numBelow) / 100);
}
return arr;
};
ExtendedBrush._makePaletteFromTwoColors = function(color1, color2, number) {
color1 = ExtendedBrush._RGB_to_Lab(ExtendedBrush.CSSStringToRGB(color1));
color2 = ExtendedBrush._RGB_to_Lab(ExtendedBrush.CSSStringToRGB(color2));
var arr = [];
var deltaA = ExtendedBrush._MAX_Lab_A - ExtendedBrush._MIN_Lab_A;
var deltaB = ExtendedBrush._MAX_Lab_B - ExtendedBrush._MIN_Lab_B;
var btm = number - 1;
var diffA = (color1[1] - color2[1]) / btm;
var diffB = (color1[2] - color2[2]) / btm;
var diffL = (color1[0] - color2[0]) / btm;
for (var i = 0; i < number; i++) {
var rgb = ExtendedBrush._Lab_to_RGB([color2[0] + i * diffL, color2[1] + i * diffA, color2[2] + i * diffB]);
var css = ExtendedBrush._RGBArrayToCSS(rgb);
arr[i] = css;
}
return arr;
}
ExtendedBrush['makeColorPalette'] =
/**
* Creates an array of valid equidistant colors.
* @name ExtendedBrush#makeColorPalette
* @param {string} color1 a valid color to be used as the basis for the color palette
* @param {string=} color2 an additional color that will be used in conjunction with color1
* @param {number=} number the amount of colors to be generated, the default is 3
* @return {Array}
*/
/** @type {Array} */
ExtendedBrush.makeColorPalette = function(color1, color2, number) {
// make sure we have 1-3 parameters
if (arguments.length < 1) {
throw new Error('Please provide at least one color, and at most two color and a number');
} else if (arguments.length > 3) {
throw new Error('Please provide no more than two colors, and an optional number argument');
}
// we have 1-3 parameters, proceed
// if no palette length is provided we give them a palette of length 3
var defaultPaletteLength = 3;
// make sure that the first parameter is a string
if (typeof color1 !== "string") throw new Error(color1 + " is not a string");
/* check to see if the last parameter is undefined.
This is used later to see if the second parameter should be handled as a color string
or as a number for the length of the palette
*/
var numundefined = number === undefined;
/*
This helper function will throw an error if the number passed into it is not real, and if it's not at least 1
*/
var checkForNumberError = function(number) {
if (typeof number !== "number" || isNaN(number) || number === Infinity || number < 1) throw new Error('Please provide a number greater than or equal to one, not: ' + number);
}
if (arguments.length === 1) {
// we only have a legal color string, so return a palette with the defaultPaletteLength
return ExtendedBrush._makePaletteFromOneColor(color1, defaultPaletteLength);
}
// by now we have a color, and any something else, be it another color, a number, or both
if (typeof color2 === "string") {
if (numundefined) {
// we only have 2 colors, so make a palette of them with the defaultPaletteLenght
return ExtendedBrush._makePaletteFromTwoColors(color1, color2, defaultPaletteLength);
} else {
checkForNumberError(number);
// we have two strings and a valid number, make a palette with those
return ExtendedBrush._makePaletteFromTwoColors(color1, color2, number);
}
} else {
if (typeof color2 === "number") {
if (numundefined) {
number = color2;
checkForNumberError(number);
// make a palette with one color and the specified length
if (number === 1) {
color1 = ExtendedBrush.CSSStringToRGB(color1);
color1.splice(3, 1);
var x = [ExtendedBrush._RGBArrayToCSS(color1)];
return x;
}
return ExtendedBrush._makePaletteFromOneColor(color1, number);
} else {
throw new Error("Please provide only only one number");
}
} else {
throw new Error('Please provide either a color string or a number');
}
}
}
ExtendedBrush._sharedTempCtx = null;
ExtendedBrush.CSSStringToRGB = function(CSSColorString) {
if (!go.Brush.isValidColor(CSSColorString))
throw new Error("Invalid CSS Color String: " + CSSColorString);
var canvas = ExtendedBrush._sharedTempCtx;
if (canvas === null) canvas = ExtendedBrush._sharedTempCtx = document.createElement('canvas').getContext('2d');
canvas.clearRect(0, 0, 1, 1);
canvas.fillStyle = CSSColorString;
canvas.fillRect(0, 0, 1, 1);
var data = canvas.getImageData(0, 0, 1, 1).data;
var arr = [];
for (var i = 0; i < data.length; i++) arr.push(data[i]);
return arr;
};
ExtendedBrush._RGBArrayToCSS = function(RGB_Array) {
if (RGB_Array.length === 3) var str = "rgb(";
else if (RGB_Array.length === 4) var str = "rgba(";
else throw new Error("invalid RGB or RGBa array: " + RGB_Array);
for (var i = 0; i < RGB_Array.length; i++) {
str += RGB_Array[i] + (i !== RGB_Array.length - 1 ? ", " : "");
}
return str + ")";
};
ExtendedBrush._MIN_Lab_A = -93;
ExtendedBrush._MAX_Lab_A = 92;
ExtendedBrush._MIN_Lab_B = -114;
ExtendedBrush._MAX_Lab_B = 92;
ExtendedBrush._RGB_to_Lab = function(rgb) {
return ExtendedBrush._XYZtoLab((ExtendedBrush._RGBtoXYZ(rgb)));
};
ExtendedBrush._Lab_to_RGB = function(Lab) {
return ExtendedBrush._XYZtoRGB(ExtendedBrush._LabtoXYZ(Lab));
};
ExtendedBrush._sRGBtoXYZMatrix = [
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041]
];
ExtendedBrush._XYZtosRGBMatrix = [
[3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[0.0556434, -0.2040259, 1.0572252]
];
ExtendedBrush._rowMultiplication = function(row, colorArray) {
var sum = 0;
for (var i = 0; i < colorArray.length; i++) {
sum += row[i] * colorArray[i];
}
return sum;
};
ExtendedBrush._RGB_XYZ_Inverse_Companding = function(RGB_value) {
RGB_value /= 255;
if (RGB_value <= .04045) {
return RGB_value / 12.92;
}
return Math.pow(((RGB_value + .055) / 1.055), 2.4);
};
ExtendedBrush._XYZ_RGB_Companding = function(XYZ_value) {
if (XYZ_value * 12.92 <= .04045)
return XYZ_value * 12.92;
return 1.055 * Math.pow(XYZ_value, .416667) - .055;
};
ExtendedBrush._RGBtoXYZ = function(rgb) {
rgb.splice(3, 1); // remove alpha value the Canvas gives us
var applied = [];
var i;
for (i = 0; i < rgb.length; i++)
rgb[i] = ExtendedBrush._RGB_XYZ_Inverse_Companding(rgb[i]);
for (i = 0; i < rgb.length; i++)
applied[i] = ExtendedBrush._rowMultiplication(ExtendedBrush._sRGBtoXYZMatrix[i], rgb);
return applied;
};
ExtendedBrush._XYZtoRGB = function(xyz) {
var applied = [];
var i;
for (i = 0; i < xyz.length; i++)
applied[i] = ExtendedBrush._rowMultiplication(ExtendedBrush._XYZtosRGBMatrix[i], xyz);
for (i = 0; i < xyz.length; i++) {
applied[i] = ExtendedBrush._XYZ_RGB_Companding(applied[i]) * 255;
if (applied[i] < 0) applied[i] = 0;
applied[i] *= 100;
applied[i] = Math.round(applied[i]);
applied[i] /= 100;
applied[i] = Math.floor(applied[i]);
if (applied[i] > 255) applied[i] = 255;
if (applied[i] < 0) applied[i] = 0;
}
return applied;
};
ExtendedBrush._Lab_EPSILON = 216 / 24389;
ExtendedBrush._Lab_KAPPA = 24389 / 27;
ExtendedBrush._XYZ_Lab_HelperFunction = function(inputXYZ) {
if (inputXYZ > ExtendedBrush._Lab_EPSILON) {
return Math.pow(inputXYZ, 1 / 3);
}
return (ExtendedBrush._Lab_KAPPA * inputXYZ + 16) / 116;
};
ExtendedBrush._XYZtoLab = function(xyz) {
var Lab = [];
var y = ExtendedBrush._XYZ_Lab_HelperFunction(xyz[1]);
Lab[0] = 116 * y - 16;
Lab[1] = 500 * (ExtendedBrush._XYZ_Lab_HelperFunction(xyz[0]) - y);
Lab[2] = 200 * (y - ExtendedBrush._XYZ_Lab_HelperFunction(xyz[2]));
return Lab;
};
ExtendedBrush._Lab_XYZ_HelperFunction = function(inputLab) {
var inputCbd = inputLab * inputLab * inputLab;
if (inputCbd > ExtendedBrush._Lab_EPSILON)
return inputCbd;
return (116 * inputLab - 16) / ExtendedBrush._Lab_KAPPA;
};
ExtendedBrush._LabtoXYZ = function(Lab) {
var working_arr = [];
working_arr[1] = (Lab[0] + 16) / 116;
working_arr[0] = (Lab[1] / 500) + working_arr[1];
working_arr[2] = (-Lab[2] / 200) + working_arr[1];
var XYZ = [];
XYZ[1] = ExtendedBrush._Lab_XYZ_HelperFunction(working_arr[1]);
XYZ[0] = ExtendedBrush._Lab_XYZ_HelperFunction(working_arr[0]);
XYZ[2] = ExtendedBrush._Lab_XYZ_HelperFunction(working_arr[2]);
return XYZ;
};
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html>
<head>
<title>Fishbone Layout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Cause-and-effect diagrams using FishboneLayout, also known as Ishikawa or herringbone diagrams." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="FishboneLayout.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // refers to its DIV HTML element by id
{ isReadOnly: true }); // do not allow the user to modify the diagram
// define the normal node template, just some text
myDiagram.nodeTemplate =
$(go.Node,
$(go.TextBlock,
new go.Binding("text"),
new go.Binding("font", "", convertFont))
);
function convertFont(data) {
var size = data.size;
if (size === undefined) size = 13;
var weight = data.weight;
if (weight === undefined) weight = "";
return weight + " " + size + "px sans-serif";
}
// This demo switches the Diagram.linkTemplate between the "normal" and the "fishbone" templates.
// If you are only doing a FishboneLayout, you could just set Diagram.linkTemplate
// to the template named "fishbone" here, and not switch templates dynamically.
// define the non-fishbone link template
myDiagram.linkTemplateMap.add("normal",
$(go.Link,
{ routing: go.Link.Orthogonal, corner: 4 },
$(go.Shape)
));
// use this link template for fishbone layouts
myDiagram.linkTemplateMap.add("fishbone",
$(FishboneLink, // defined above
$(go.Shape)
));
// here is the structured data used to build the model
var json =
{
"text": "Incorrect Deliveries", "size": 18, "weight": "Bold", "causes": [
{
"text": "Skills", "size": 14, "weight": "Bold", "causes": [
{
"text": "knowledge", "weight": "Bold", "causes": [
{
"text": "procedures", "causes": [
{ "text": "documentation" }
]
},
{ "text": "products" }
]
},
{ "text": "literacy", "weight": "Bold" }
]
},
{
"text": "Procedures", "size": 14, "weight": "Bold", "causes": [
{
"text": "manual", "weight": "Bold", "causes": [
{ "text": "consistency" }
]
},
{
"text": "automated", "weight": "Bold", "causes": [
{ "text": "correctness" },
{ "text": "reliability" }
]
}
]
},
{
"text": "Communication", "size": 14, "weight": "Bold", "causes": [
{ "text": "ambiguity", "weight": "Bold" },
{
"text": "sales staff", "weight": "Bold", "causes": [
{
"text": "order details", "causes": [
{ "text": "lack of knowledge" }
]
}
]
},
{
"text": "telephone orders", "weight": "Bold", "causes": [
{ "text": "lack of information" }
]
},
{
"text": "picking slips", "weight": "Bold", "causes": [
{ "text": "details" },
{ "text": "legibility" }
]
}
]
},
{
"text": "Transport", "size": 14, "weight": "Bold", "causes": [
{
"text": "information", "weight": "Bold", "causes": [
{ "text": "incorrect person" },
{
"text": "incorrect addresses", "causes": [
{
"text": "customer data base", "causes": [
{ "text": "not up-to-date" },
{ "text": "incorrect program" }
]
}
]
},
{ "text": "incorrect dept" }
]
},
{
"text": "carriers", "weight": "Bold", "causes": [
{ "text": "efficiency" },
{ "text": "methods" }
]
}
]
}
]
};
function walkJson(obj, arr) {
var key = arr.length;
obj.key = key;
arr.push(obj);
var children = obj.causes;
if (children) {
for (var i = 0; i < children.length; i++) {
var o = children[i];
o.parent = key; // reference to parent node data
walkJson(o, arr);
}
}
}
// build the tree model
var nodeDataArray = [];
walkJson(json, nodeDataArray);
myDiagram.model = new go.TreeModel(nodeDataArray);
layoutFishbone();
}
// use FishboneLayout and FishboneLink
function layoutFishbone() {
myDiagram.startTransaction("fishbone layout");
myDiagram.linkTemplate = myDiagram.linkTemplateMap.get("fishbone");
myDiagram.layout = go.GraphObject.make(FishboneLayout, { // defined above
angle: 180,
layerSpacing: 10,
nodeSpacing: 20,
rowSpacing: 10
});
myDiagram.commitTransaction("fishbone layout");
}
// make the layout a branching tree layout and use a normal link template
function layoutBranching() {
myDiagram.startTransaction("branching layout");
myDiagram.linkTemplate = myDiagram.linkTemplateMap.get("normal");
myDiagram.layout = go.GraphObject.make(go.TreeLayout, {
angle: 180,
layerSpacing: 20,
alignment: go.TreeLayout.AlignmentBusBranching
});
myDiagram.commitTransaction("branching layout");
}
// make the layout a basic tree layout and use a normal link template
function layoutNormal() {
myDiagram.startTransaction("normal layout");
myDiagram.linkTemplate = myDiagram.linkTemplateMap.get("normal");
myDiagram.layout = go.GraphObject.make(go.TreeLayout, {
angle: 180,
breadthLimit: 1000,
alignment: go.TreeLayout.AlignmentStart
});
myDiagram.commitTransaction("normal layout");
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="height:550px;width:100%;border:1px solid black;"></div>
<div id="buttons">
<label>Layout:</label>
<button onclick="layoutFishbone()">Fishbone</button>
<button onclick="layoutBranching()">Branching</button>
<button onclick="layoutNormal()">Normal</button>
</div>
<br />
<div id="description">
<p>
This sample shows a "fishbone" layout of a tree model of cause-and-effect relationships.
This type of layout is often seen in root cause analysis, or RCA.
The layout is defined in its own file, as <a href="FishboneLayout.js">FishboneLayout.js</a>.
When using FishboneLayout the diagram uses FishboneLink in order to get custom routing for the links.
</p>
<p>
The buttons each set the <a>Diagram.layout</a> within a transaction.
</p>
</div>
</div>
</body>
</html>
+261
View File
@@ -0,0 +1,261 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// FishboneLayout is a custom Layout derived from TreeLayout for creating "fishbone" diagrams.
// A fishbone diagram also requires a Link class that implements custom routing, FishboneLink,
// which is also defined in this file.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends TreeLayout
* @class
* This only works for angle === 0 or angle === 180.
* <p>
* This layout assumes Links are automatically routed in the way needed by fishbone diagrams,
* by using the FishboneLink class instead of go.Link.
*/
function FishboneLayout() {
go.TreeLayout.call(this);
this.alignment = go.TreeLayout.AlignmentBusBranching;
this.setsPortSpot = false;
this.setsChildPortSpot = false;
}
go.Diagram.inherit(FishboneLayout, go.TreeLayout);
FishboneLayout.prototype.makeNetwork = function(coll) {
// assert(this.angle === 0 || this.angle === 180);
// assert(this.alignment === go.TreeLayout.AlignmentBusBranching);
// assert(this.path !== go.TreeLayout.PathSource);
// call base method for standard behavior
var net = go.TreeLayout.prototype.makeNetwork.call(this, coll);
// make a copy of the collection of TreeVertexes
// because we will be modifying the TreeNetwork.vertexes collection in the loop
var verts = new go.List(/*go.TreeVertex*/).addAll(net.vertexes);
verts.each(function(v) {
// ignore leaves of tree
if (v.destinationEdges.count === 0) return;
if (v.destinationEdges.count % 2 === 1) {
// if there's an odd number of real children, add two dummies
var dummy = net.createVertex();
dummy.bounds = new go.Rect();
dummy.focus = new go.Point();
net.addVertex(dummy);
net.linkVertexes(v, dummy, null);
}
// make sure there's an odd number of children, including at least one dummy;
// commitNodes will move the parent node to where this dummy child node is placed
var dummy2 = net.createVertex();
dummy2.bounds = v.bounds;
dummy2.focus = v.focus;
net.addVertex(dummy2);
net.linkVertexes(v, dummy2, null);
});
return net;
};
FishboneLayout.prototype.assignTreeVertexValues = function(v) {
go.TreeLayout.prototype.assignTreeVertexValues.call(this, v);
v._direction = 0; // add this property to each TreeVertex
if (v.parent !== null) {
// The parent node will be moved to where the last dummy will be;
// reduce the space to account for the future hole.
if (v.angle === 0 || v.angle === 180) {
v.layerSpacing -= v.bounds.width;
} else {
v.layerSpacing -= v.bounds.height;
}
}
};
FishboneLayout.prototype.commitNodes = function() {
// vertex Angle is set by BusBranching "inheritance";
// assign spots assuming overall Angle === 0 or 180
// and links are always connecting horizontal with vertical
this.network.edges.each(function(e) {
var link = e.link;
if (link === null) return;
link.fromSpot = go.Spot.None;
link.toSpot = go.Spot.None;
var v = e.fromVertex;
var w = e.toVertex;
if (v.angle === 0) {
link.fromSpot = go.Spot.Left;
} else if (v.angle === 180) {
link.fromSpot = go.Spot.Right;
}
if (w.angle === 0) {
link.toSpot = go.Spot.Left;
} else if (w.angle === 180) {
link.toSpot = go.Spot.Right;
}
});
// move the parent node to the location of the last dummy
this.network.vertexes.each(function(v) {
var len = v.children.length;
if (len === 0) return; // ignore leaf nodes
if (v.parent === null) return; // don't move root node
var dummy2 = v.children[len-1];
v.centerX = dummy2.centerX;
v.centerY = dummy2.centerY;
});
var layout = this;
this.network.vertexes.each(function(v) {
if (v.parent === null) {
layout.shift(v);
}
});
// now actually change the Node.location of all nodes
go.TreeLayout.prototype.commitNodes.call(this);
};
// don't use the standard routing done by TreeLayout
FishboneLayout.prototype.commitLinks = function() { };
FishboneLayout.prototype.shift = function(v) {
var p = v.parent;
if (p !== null && (v.angle === 90 || v.angle === 270)) {
var g = p.parent;
if (g !== null) {
var shift = v.nodeSpacing;
if (g._direction > 0) {
if (g.angle === 90) {
if (p.angle === 0) {
v._direction = 1;
if (v.angle === 270) this.shiftAll(2, -shift, p, v);
} else if (p.angle === 180) {
v._direction = -1;
if (v.angle === 90) this.shiftAll(-2, shift, p, v);
}
} else if (g.angle === 270) {
if (p.angle === 0) {
v._direction = 1;
if (v.angle === 90) this.shiftAll(2, -shift, p, v);
} else if (p.angle === 180) {
v._direction = -1;
if (v.angle === 270) this.shiftAll(-2, shift, p, v);
}
}
} else if (g._direction < 0) {
if (g.angle === 90) {
if (p.angle === 0) {
v._direction = 1;
if (v.angle === 90) this.shiftAll(2, -shift, p, v);
} else if (p.angle === 180) {
v._direction = -1;
if (v.angle === 270) this.shiftAll(-2, shift, p, v);
}
} else if (g.angle === 270) {
if (p.angle === 0) {
v._direction = 1;
if (v.angle === 270) this.shiftAll(2, -shift, p, v);
} else if (p.angle === 180) {
v._direction = -1;
if (v.angle === 90) this.shiftAll(-2, shift, p, v);
}
}
}
} else { // g === null: V is a child of the tree ROOT
var dir = ((p.angle === 0) ? 1 : -1);
v._direction = dir;
this.shiftAll(dir, 0, p, v);
}
}
for (var i = 0; i < v.children.length; i++) {
var c = v.children[i];
this.shift(c);
};
};
FishboneLayout.prototype.shiftAll = function(direction, absolute, root, v) {
// assert(root.angle === 0 || root.angle === 180);
var locx = v.centerX;
locx += direction * Math.abs(root.centerY - v.centerY) / 2;
locx += absolute;
v.centerX = locx;
for (var i = 0; i < v.children.length; i++) {
var c = v.children[i];
this.shiftAll(direction, absolute, root, c);
};
};
// end FishboneLayout
// FishboneLink has custom routing
function FishboneLink() {
go.Link.call(this);
};
go.Diagram.inherit(FishboneLink, go.Link);
FishboneLink.prototype.computeAdjusting = function() { return this.adjusting; }
FishboneLink.prototype.computePoints = function() {
var result = go.Link.prototype.computePoints.call(this);
if (result) {
// insert middle point to maintain horizontal lines
if (this.fromSpot.equals(go.Spot.Right) || this.fromSpot.equals(go.Spot.Left)) {
var p1;
// deal with root node being on the "wrong" side
var fromnode = this.fromNode;
if (fromnode.findLinksInto().count === 0) {
// pretend the link is coming from the opposite direction than the declared FromSpot
var fromport = this.fromPort;
var fromctr = fromport.getDocumentPoint(go.Spot.Center);
var fromfar = fromctr.copy();
fromfar.x += (this.fromSpot.equals(go.Spot.Left) ? 99999 : -99999);
p1 = this.getLinkPointFromPoint(fromnode, fromport, fromctr, fromfar, true).copy();
// update the route points
this.setPoint(0, p1);
var endseg = this.fromEndSegmentLength;
if (isNaN(endseg)) endseg = fromport.fromEndSegmentLength;
p1.x += (this.fromSpot.equals(go.Spot.Left)) ? endseg : -endseg;
this.setPoint(1, p1);
} else {
p1 = this.getPoint(1); // points 0 & 1 should be OK already
}
var tonode = this.toNode;
var toport = this.toPort;
var toctr = toport.getDocumentPoint(go.Spot.Center);
var far = toctr.copy();
far.x += (this.fromSpot.equals(go.Spot.Left)) ? -99999/2 : 99999/2;
far.y += (toctr.y < p1.y) ? 99999 : -99999;
var p2 = this.getLinkPointFromPoint(tonode, toport, toctr, far, false);
this.setPoint(2, p2);
var dx = Math.abs(p2.y-p1.y)/2;
if (this.fromSpot.equals(go.Spot.Left)) dx = -dx;
this.insertPoint(2, new go.Point(p2.x+dx, p1.y));
} else if (this.toSpot.equals(go.Spot.Right) || this.toSpot.equals(go.Spot.Left)) {
var p1 = this.getPoint(1); // points 1 & 2 should be OK already
var fromnode = this.fromNode;
var fromport = this.fromPort;
var parentlink = fromnode.findLinksInto().first();
var fromctr = fromport.getDocumentPoint(go.Spot.Center);
var far = fromctr.copy();
far.x += (parentlink !== null && parentlink.fromSpot.equals(go.Spot.Left)) ? -99999/2 : 99999/2;
far.y += (fromctr.y < p1.y) ? 99999 : -99999;
var p0 = this.getLinkPointFromPoint(fromnode, fromport, fromctr, far, true);
this.setPoint(0, p0);
var dx = Math.abs(p1.y-p0.y)/2;
if (parentlink !== null && parentlink.fromSpot.equals(go.Spot.Left)) dx = -dx;
this.insertPoint(1, new go.Point(p0.x+dx, p1.y));
}
}
return result;
};
// end FishboneLink
+120
View File
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>Freehand Drawing Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Let the user draw a Shape using the mouse or finger without any constraints." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="FreehandDrawingTool.js"></script>
<script src="GeometryReshapingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv");
myDiagram.toolManager.mouseDownTools.insertAt(3, new GeometryReshapingTool());
myDiagram.nodeTemplateMap.add("FreehandDrawing",
$(go.Part,
{ locationSpot: go.Spot.Center, isLayoutPositioned: false },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{
selectionAdorned: true, selectionObjectName: "SHAPE",
selectionAdornmentTemplate: // custom selection adornment: a blue rectangle
$(go.Adornment, "Auto",
$(go.Shape, { stroke: "dodgerblue", fill: null }),
$(go.Placeholder, { margin: -1 }))
},
{ resizable: true, resizeObjectName: "SHAPE" },
{ rotatable: true, rotateObjectName: "SHAPE" },
{ reshapable: true }, // GeometryReshapingTool assumes nonexistent Part.reshapeObjectName would be "SHAPE"
$(go.Shape,
{ name: "SHAPE", fill: null, strokeWidth: 1.5 },
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
new go.Binding("angle").makeTwoWay(),
new go.Binding("geometryString", "geo").makeTwoWay(),
new go.Binding("fill"),
new go.Binding("stroke"),
new go.Binding("strokeWidth"))
));
// create drawing tool for myDiagram, defined in FreehandDrawingTool.js
var tool = new FreehandDrawingTool();
// provide the default JavaScript object for a new polygon in the model
tool.archetypePartData =
{ stroke: "green", strokeWidth: 3, category: "FreehandDrawing" };
// allow the tool to start on top of an existing Part
tool.isBackgroundOnly = false;
// install as first mouse-move-tool
myDiagram.toolManager.mouseMoveTools.insertAt(0, tool);
load(); // load a simple diagram from the textarea
}
function mode(draw) {
var tool = myDiagram.toolManager.findTool("FreehandDrawing");
tool.isEnabled = draw;
}
function updateAllAdornments() { // called after checkboxes change Diagram.allow...
myDiagram.selection.each(function(p) { p.updateAdornments(); });
}
// save a model to and load a model from Json text, displayed below the Diagram
function save() {
var str = '{ "position": "' + go.Point.stringify(myDiagram.position) + '",\n "model": ' + myDiagram.model.toJson() + ' }';
document.getElementById("mySavedDiagram").value = str;
}
function load() {
var str = document.getElementById("mySavedDiagram").value;
try {
var json = JSON.parse(str);
myDiagram.initialPosition = go.Point.parse(json.position || "0 0");
myDiagram.model = go.Model.fromJson(json.model);
myDiagram.model.undoManager.isEnabled = true;
} catch (ex) {
alert(ex);
}
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 600px"></div>
<div id="buttons">
<button onclick="mode(false)">Select</button>
<button onclick="mode(true)">Draw Mode</button>
<button onclick="save()">Save</button>
<button onclick="load()">Load</button>
<label><input type="checkbox" onclick="myDiagram.allowResize = !myDiagram.allowResize; updateAllAdornments()" checked="checked" />Allow Resizing</label>
<label><input type="checkbox" onclick="myDiagram.allowReshape = !myDiagram.allowReshape; updateAllAdornments()" checked="checked" />Allow Reshaping</label>
<label><input type="checkbox" onclick="myDiagram.allowRotate = !myDiagram.allowRotate; updateAllAdornments()" checked="checked" />Allow Rotating</label>
</div>
<p>
This sample demonstrates the FreehandDrawingTool. It is defined in its own file, as <a href="FreehandDrawingTool.js">FreehandDrawingTool.js</a>.
It also demonstrates the GeometryReshapingTool, another custom tool, defined in <a href="GeometryReshapingTool.js">GeometryReshapingTool.js</a>.
</p>
<p>
Press and drag to draw a line.
</p>
<p>
Click the "Select" button to switch back to the normal selection behavior, so that you can select, resize, and rotate the shapes.
The checkboxes control whether you can resize, reshape, and/or rotate selected shapes.
</p>
<textarea id="mySavedDiagram" style="width:100%;height:300px">
{ "position": "0 0",
"model": { "class": "go.GraphLinksModel",
"nodeDataArray": [ {"loc":"301 143", "category":"FreehandDrawing", "geo":"M0 70 L1 70 L2 70 L3 70 L5 70 L7 70 L8 70 L11 70 L13 70 L18 69 L21 69 L25 68 L29 67 L34 67 L38 67 L42 67 L47 66 L50 66 L53 66 L55 66 L57 66 L60 66 L63 66 L64 66 L66 66 L68 66 L70 66 L72 66 L74 66 L76 66 L78 65 L81 65 L83 65 L85 65 L88 65 L90 65 L92 65 L95 65 L98 65 L100 65 L102 65 L104 65 L106 65 L109 65 L110 65 L111 65 L112 65 L113 65 L114 65 L115 65 L116 65 L118 65 L119 65 L120 65 L121 65 L122 65 L123 65 L124 65 L125 65 L126 65 L127 65 L128 65 L129 65 L131 65 L131 64 L132 64 L133 64 L134 64 L135 64 L137 64 L138 64 L139 64 L140 64 L141 64 L140 64 L139 64 L138 64 L137 64 L135 65 L134 65 L132 66 L130 67 L129 67 L126 68 L123 70 L121 71 L119 72 L116 73 L114 74 L111 76 L109 77 L106 78 L104 79 L99 81 L96 84 L94 84 L90 87 L89 87 L87 88 L86 89 L84 89 L83 90 L81 91 L80 92 L79 92 L77 93 L76 94 L74 94 L74 95 L73 96 L71 96 L70 97 L68 98 L67 98 L66 99 L64 99 L64 100 L62 100 L61 101 L60 102 L59 102 L58 103 L57 103 L57 104 L56 104 L56 105 L54 105 L54 107 L53 107 L52 108 L51 108 L51 109 L49 110 L48 111 L47 112 L47 113 L46 113 L45 114 L44 114 L44 115 L44 116 L43 116 L43 117 L42 117 L42 119 L41 119 L41 120 L40 120 L40 121 L39 122 L39 123 L38 124 L37 125 L36 126 L36 127 L35 128 L34 128 L34 129 L33 130 L33 131 L32 131 L32 130 L32 129 L33 128 L34 125 L35 122 L37 119 L39 115 L41 111 L41 106 L43 103 L45 98 L47 93 L48 90 L49 86 L51 83 L52 81 L54 78 L55 74 L55 71 L56 68 L57 65 L58 62 L58 59 L58 55 L58 53 L58 51 L58 49 L58 48 L59 45 L60 44 L60 42 L61 40 L61 38 L62 36 L64 32 L64 30 L65 29 L66 27 L66 26 L66 25 L66 24 L67 23 L67 22 L67 21 L67 20 L67 19 L68 19 L68 18 L68 17 L69 16 L69 15 L69 14 L69 12 L69 11 L69 10 L70 9 L70 8 L70 7 L70 6 L71 5 L71 4 L71 3 L71 2 L71 1 L71 0 L71 1 L71 2 L71 5 L71 6 L71 8 L71 9 L71 12 L71 14 L72 16 L72 18 L73 20 L73 23 L74 25 L74 27 L75 29 L76 32 L77 34 L78 35 L79 38 L79 39 L81 42 L81 43 L82 45 L83 46 L83 47 L83 49 L85 50 L86 52 L86 55 L86 58 L88 60 L89 62 L89 64 L90 66 L91 67 L91 69 L92 70 L92 71 L93 72 L94 73 L94 74 L94 75 L95 76 L96 77 L96 79 L97 81 L98 82 L98 83 L98 84 L99 85 L99 86 L100 87 L100 90 L101 91 L102 92 L102 93 L103 95 L103 96 L103 97 L104 98 L104 100 L104 101 L105 102 L106 103 L106 104 L107 106 L108 108 L109 110 L110 111 L111 113 L112 114 L113 115 L113 117 L115 119 L116 121 L116 123 L118 124 L119 126 L120 127 L121 129 L121 130 L122 131 L123 131 L123 132 L124 132 L124 133 L125 134 L125 135 L126 135 L127 136 L128 137 L129 138 L130 139 L131 140 L130 139 L129 138 L128 138 L126 136 L124 136 L123 134 L121 133 L118 131 L116 130 L114 128 L111 127 L107 123 L105 122 L102 120 L100 119 L98 117 L95 116 L93 114 L90 113 L88 111 L85 110 L83 108 L81 106 L78 105 L77 104 L75 103 L72 102 L71 101 L70 100 L68 99 L67 98 L65 98 L64 97 L62 96 L60 96 L58 95 L57 94 L54 93 L53 93 L51 92 L49 91 L48 91 L47 91 L45 90 L44 90 L43 89 L42 89 L41 89 L40 88 L39 87 L37 87 L36 86 L35 85 L33 85 L32 84 L31 84 L30 83 L29 83 L28 82 L26 81 L25 81 L24 80 L22 80 L21 79 L21 78 L20 78 L19 78 L18 77 L17 77 L16 76 L15 76 L15 75 L14 75 L13 75 L12 74 L11 74 L10 74 L9 74 L7 73 L5 72 L4 72 L3 72 L2 71", "key":-1} ],
"linkDataArray": [ ]
} }
</textarea>
</div>
</body>
</html>
+233
View File
@@ -0,0 +1,233 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for freehand drawing
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* This tool allows the user to draw a shape using the mouse.
* It collects all of the points from a mouse-down, all mouse-moves, until a mouse-up,
* and puts all of those points in a {@link Geometry} used by a {@link Shape}.
* It then adds a node data object to the diagram's model.
* <p/>
* This tool may be installed as the first mouse down tool:
* <code>myDiagram.toolManager.mouseDownTools.insertAt(0, new FreehandDrawingTool());</code>
* <p/>
* The Shape used during the drawing operation can be customized by setting {@link #temporaryShape}.
* The node data added to the model can be customized by setting {@link #archetypePartData}.
*/
function FreehandDrawingTool() {
go.Tool.call(this);
this.name = "FreehandDrawing";
this._archetypePartData = {}; // the data to copy for a new polyline Part
this._isBackgroundOnly = true; // affects canStart()
// this is the Shape that is shown during a drawing operation
this._temporaryShape = go.GraphObject.make(go.Shape, { name: "SHAPE", fill: null, strokeWidth: 1.5 });
// the Shape has to be inside a temporary Part that is used during the drawing operation
go.GraphObject.make(go.Part, { layerName: "Tool" }, this._temporaryShape);
}
go.Diagram.inherit(FreehandDrawingTool, go.Tool);
/**
* Only start if the diagram is modifiable and allows insertions.
* OPTIONAL: if the user is starting in the diagram's background, not over an existing Part.
* @this {FreehandDrawingTool}
*/
FreehandDrawingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly || diagram.isModelReadOnly) return false;
if (!diagram.allowInsert) return false;
// don't include the following check when this tool is running modally
if (diagram.currentTool !== this && this.isBackgroundOnly) {
// only operates in the background, not on some Part
var part = diagram.findPartAt(diagram.lastInput.documentPoint, true);
if (part !== null) return false;
}
return true;
};
/**
* Capture the mouse and use a "crosshair" cursor.
* @this {FreehandDrawingTool}
*/
FreehandDrawingTool.prototype.doActivate = function() {
go.Tool.prototype.doActivate.call(this);
this.diagram.isMouseCaptured = true;
this.diagram.currentCursor = "crosshair";
};
/**
* Cleanup.
* @this {FreehandDrawingTool}
*/
FreehandDrawingTool.prototype.doDeactivate = function() {
go.Tool.prototype.doDeactivate.call(this);
if (this.temporaryShape !== null) {
this.diagram.remove(this.temporaryShape.part);
}
this.diagram.currentCursor = "";
this.diagram.isMouseCaptured = false;
};
/**
* This adds a Point to the {@link #temporaryShape}'s geometry.
* <p/>
* If the Shape is not yet in the Diagram, its geometry is initialized and
* its parent Part is added to the Diagram.
* <p/>
* If the point is less than half a pixel away from the previous point, it is ignored.
* @this {FreehandDrawingTool}
* @param {Point} p
*/
FreehandDrawingTool.prototype.addPoint = function(p) {
var shape = this.temporaryShape;
if (shape === null) return;
// for the temporary Shape, normalize the geometry to be in the viewport
var viewpt = this.diagram.viewportBounds.position;
var q = new go.Point(p.x-viewpt.x, p.y-viewpt.y);
var part = shape.part;
if (part.diagram === null) {
var fig = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill
var geo = new go.Geometry().add(fig); // the Shape.geometry consists of a single PathFigure
this.temporaryShape.geometry = geo;
// position the Shape's Part, accounting for the strokeWidth
part.position = new go.Point(viewpt.x - shape.strokeWidth/2, viewpt.y - shape.strokeWidth/2);
this.diagram.add(part);
}
// only add a point if it isn't too close to the last one
var segs = shape.geometry.figures.first().segments;
var idx = segs.count-1;
if (idx >= 0) {
var last = segs.elt(idx);
if (Math.abs(q.x - last.endX) < 0.5 && Math.abs(q.y - last.endY) < 0.5) return;
}
// must copy whole Geometry in order to add a PathSegment
var geo = shape.geometry.copy();
var fig = geo.figures.first();
var seg = new go.PathSegment(go.PathSegment.Line, q.x, q.y);
fig.add(seg);
shape.geometry = geo;
};
/**
* Start drawing the line by starting to accumulate points in the {@link #temporaryShape}'s geometry.
* @this {FreehandDrawingTool}
*/
FreehandDrawingTool.prototype.doMouseDown = function() {
if (!this.isActive) {
this.doActivate();
// the first point
this.addPoint(this.diagram.lastInput.documentPoint);
}
};
/**
* Keep accumulating points in the {@link #temporaryShape}'s geometry.
* @this {FreehandDrawingTool}
*/
FreehandDrawingTool.prototype.doMouseMove = function() {
if (this.isActive) {
this.addPoint(this.diagram.lastInput.documentPoint);
}
};
/**
* Finish drawing the line by adding a node data object holding the
* geometry string and the node position that the node template can bind to.
* This copies the {@link #archetypePartData} and adds it to the model.
* @this {FreehandDrawingTool}
*/
FreehandDrawingTool.prototype.doMouseUp = function() {
var started = false;
if (this.isActive) {
started = true;
var diagram = this.diagram;
// the last point
this.addPoint(diagram.lastInput.documentPoint);
// normalize geometry and node position
var viewpt = diagram.viewportBounds.position;
var geo = this.temporaryShape.geometry.copy();
var pos = geo.normalize();
pos.x = viewpt.x - pos.x;
pos.y = viewpt.y - pos.y;
diagram.startTransaction(this.name);
// create the node data for the model
var d = diagram.model.copyNodeData(this.archetypePartData);
// adding data to model creates the actual Part
diagram.model.addNodeData(d);
var part = diagram.findPartForData(d);
// assign the location
part.location = new go.Point(pos.x + geo.bounds.width/2, pos.y + geo.bounds.height/2);
// assign the Shape.geometry
var shape = part.findObject("SHAPE");
if (shape !== null) shape.geometry = geo;
}
this.stopTool();
if (started) diagram.commitTransaction(this.name);
};
// Public properties
/**
* Gets or sets the Shape that is used to hold the line as it is being drawn.
* The default value is a simple Shape drawing an unfilled open thin black line.
* @name FreehandDrawingTool#temporaryShape
* @return {Shape}
*/
Object.defineProperty(FreehandDrawingTool.prototype, "temporaryShape", {
get: function() { return this._temporaryShape; },
set: function(val) {
if (this._temporaryShape !== val && val !== null) {
val.name = "SHAPE";
var panel = this._temporaryShape.panel;
panel.remove(this._temporaryShape);
this._temporaryShape = val;
panel.add(this._temporaryShape);
}
}
});
/**
* Gets or sets the node data object that is copied and added to the model
* when the freehand drawing operation completes.
* @name FreehandDrawingTool#archetypePartData
* @return {Object}
*/
Object.defineProperty(FreehandDrawingTool.prototype, "archetypePartData", {
get: function() { return this._archetypePartData; },
set: function(val) { this._archetypePartData = val; }
});
/**
* Gets or sets whether this tool can only run if the user starts in the diagram's background
* rather than on top of an existing Part.
* The default value is true.
* @name FreehandDrawingTool#isBackgroundOnly
* @return {Object}
*/
Object.defineProperty(FreehandDrawingTool.prototype, "isBackgroundOnly", {
get: function() { return this._isBackgroundOnly; },
set: function(val) { this._isBackgroundOnly = val; }
});
+50
View File
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<title>Geometry Reshaping</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow the user to change a Shape by dragging a handle at a point of the Shape's Geometry." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="GeometryReshapingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
"undoManager.isEnabled": true // enable undo & redo
});
myDiagram.toolManager.mouseDownTools.insertAt(3, new GeometryReshapingTool());
myDiagram.nodeTemplate =
$(go.Node,
{ reshapable: true }, // GeometryReshapingTool assumes nonexistent Part.reshapeObjectName would be "SHAPE"
$(go.Shape,
{ name: "SHAPE", fill: "lightgray", strokeWidth: 1.5 },
new go.Binding("geometryString", "geo").makeTwoWay()
)
);
myDiagram.model = new go.GraphLinksModel([{ geo: "F M0 145 L75 2 L131 87 L195 0 L249 143z", key: -1 }], []);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 350px"></div>
<p>
The GeometryReshapingTool class allows for a Shape's Geometry to be modified by the user via the dragging of tool handles.
Reshape handles are drawn as Adornments at each point in the geometry.
It is defined in its own file, as <a href="GeometryReshapingTool.js">GeometryReshapingTool.js</a>.
</p>
<p>
Usage can also be seen in the <a href="FreehandDrawing.html">Freehand Drawing</a> and <a href="PolygonDrawing.html">Polygon Drawing</a> samples.
</p>
</div>
</body>
</html>
+357
View File
@@ -0,0 +1,357 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* This GeometryReshapingTool class allows for a Shape's Geometry to be modified by the user
* via the dragging of tool handles.
* This does not handle Links, whose routes should be reshaped by the LinkReshapingTool.
* The {@link #reshapeObjectName} needs to identify the named {@link Shape} within the
* selected {@link Part}.
* If the shape cannot be found or if its {@link Shape#geometry} is not of type {@link Geometry#Path},
* this will not show any GeometryReshaping {@link Adornment}.
* At the current time this tool does not support adding or removing {@link PathSegment}s to the Geometry.
*/
function GeometryReshapingTool() {
go.Tool.call(this);
this.name = "GeometryReshaping";
var h = new go.Shape();
h.figure = "Diamond";
h.desiredSize = new go.Size(7, 7);
h.fill = "lightblue";
h.stroke = "dodgerblue";
h.cursor = "move";
/** @type {GraphObject} */
this._handleArchetype = h;
/** @type {string} */
this._reshapeObjectName = 'SHAPE'; //??? can't add Part.reshapeObjectName property
// there's no Part.reshapeAdornmentTemplate either
// internal state
/** @type {GraphObject} */
this._handle = null;
/** @type {Shape} */
this._adornedShape = null;
/** @type {Geometry} */
this._originalGeometry = null; // in case the tool is cancelled and the UndoManager is not enabled
}
go.Diagram.inherit(GeometryReshapingTool, go.Tool);
/*
* A small GraphObject used as a reshape handle for each segment.
* The default GraphObject is a small blue diamond.
* @name GeometryReshapingTool#handleArchetype
* @return {GraphObject}
*/
Object.defineProperty(GeometryReshapingTool.prototype, "handleArchetype", {
get: function() { return this._handleArchetype; },
set: function(val) { this._handleArchetype = value; }
});
/*
* The name of the GraphObject to be reshaped.
* @name GeometryReshapingTool#reshapeObjectName
* @return {string}
*/
Object.defineProperty(GeometryReshapingTool.prototype, "reshapeObjectName", {
get: function() { return this._reshapeObjectName; },
set: function(val) { this._reshapeObjectName = value; }
});
/*
* This read-only property returns the {@link GraphObject} that is the tool handle being dragged by the user.
* This will be contained by an {@link Adornment} whose category is "GeometryReshaping".
* Its {@link Adornment#adornedObject} is the same as the {@link #adornedShape}.
* @name GeometryReshapingTool#handle
* @return {GraphObject}
*/
Object.defineProperty(GeometryReshapingTool.prototype, "handle", {
get: function() { return this._handle; }
});
/*
* Gets the {@link Shape} that is being reshaped.
* This must be contained within the selected Part.
* @name GeometryReshapingTool#adornedShape
* @return {Shape}
*/
Object.defineProperty(GeometryReshapingTool.prototype, "adornedShape", {
get: function() { return this._adornedShape; }
});
/*
* This read-only property remembers the original value for {@link Shape#geometry},
* so that it can be restored if this tool is cancelled.
* @name GeometryReshapingTool#originalGeometry
* @return {Geometry}
*/
Object.defineProperty(GeometryReshapingTool.prototype, "originalGeometry", {
get: function() { return this._originalGeometry; }
});
/**
* Show an {@link Adornment} with a reshape handle at each point of the geometry.
* Don't show anything if {@link #reshapeObjectName} doesn't identify a {@link Shape}
* that has a {@link Shape#geometry} of type {@link Geometry#Path}.
* @this {GeometryReshapingTool}
* @param {Part} part the part.
*/
GeometryReshapingTool.prototype.updateAdornments = function(part) {
if (part === null || part instanceof go.Link) return; // this tool never applies to Links
if (part.isSelected && !this.diagram.isReadOnly) {
var selelt = part.findObject(this.reshapeObjectName);
if (selelt instanceof go.Shape && selelt.actualBounds.isReal() && selelt.isVisibleObject() &&
part.canReshape() && part.actualBounds.isReal() && part.isVisible() &&
selelt.geometry.type === go.Geometry.Path) {
var adornment = part.findAdornment(this.name);
if (adornment === null) {
adornment = this.makeAdornment(selelt);
}
if (adornment !== null) {
// update the position/alignment of each handle
var geo = selelt.geometry;
var b = geo.bounds;
// update the size of the adornment
adornment.findObject("BODY").desiredSize = b.size;
adornment.elements.each(function(h) {
if (h._typ === undefined) return;
var fig = geo.figures.elt(h._fig);
var seg = fig.segments.elt(h._seg);
var x = 0;
var y = 0;
switch (h._typ) {
case 0: x = fig.startX; y = fig.startY; break;
case 1: x = seg.endX; y = seg.endY; break;
case 2: x = seg.point1X; y = seg.point1Y; break;
case 3: x = seg.point2X; y = seg.point2Y; break;
}
h.alignment = new go.Spot(0, 0, x - b.x, y - b.y);
});
part.addAdornment(this.name, adornment);
adornment.location = selelt.getDocumentPoint(go.Spot.TopLeft);
adornment.angle = selelt.getDocumentAngle();
return;
}
}
}
part.removeAdornment(this.name);
};
/*
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.makeAdornment = function(selelt) {
var adornment = new go.Adornment();
adornment.type = go.Panel.Spot;
adornment.locationObjectName = "BODY";
adornment.locationSpot = new go.Spot(0, 0, -selelt.strokeWidth / 2, -selelt.strokeWidth / 2);
var h = new go.Shape();
h.name = "BODY";
h.fill = null;
h.stroke = null;
h.strokeWidth = 0;
adornment.add(h);
var geo = selelt.geometry;
// requires Path Geometry, checked above in updateAdornments
for (var f = 0; f < geo.figures.count; f++) {
var fig = geo.figures.elt(f);
for (var g = 0; g < fig.segments.count; g++) {
var seg = fig.segments.elt(g);
var h;
if (g === 0) {
h = this.makeHandle(selelt, fig, seg);
if (h !== null) {
h._typ = 0;
h._fig = f;
h._seg = g;
adornment.add(h);
}
}
h = this.makeHandle(selelt, fig, seg);
if (h !== null) {
h._typ = 1;
h._fig = f;
h._seg = g;
adornment.add(h);
}
if (seg.type === go.PathSegment.QuadraticBezier || seg.type === go.PathSegment.Bezier) {
h = this.makeHandle(selelt, fig, seg);
if (h !== null) {
h._typ = 2;
h._fig = f;
h._seg = g;
adornment.add(h);
}
if (seg.type === go.PathSegment.Bezier) {
h = this.makeHandle(selelt, fig, seg);
if (h !== null) {
h._typ = 3;
h._fig = f;
h._seg = g;
adornment.add(h);
}
}
}
}
}
adornment.category = this.name;
adornment.adornedObject = selelt;
return adornment;
};
/*
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.makeHandle = function(selelt, fig, seg) {
var h = this.handleArchetype;
if (h === null) return null;
return h.copy();
};
/*
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly) return false;
if (!diagram.allowReshape) return false;
if (!diagram.lastInput.left) return false;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
return (h !== null);
};
/**
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
if (this._handle === null) return;
var shape = this._handle.part.adornedObject;
if (!shape) return;
this._adornedShape = shape;
diagram.isMouseCaptured = true;
this.startTransaction(this.name);
this._originalGeometry = shape.geometry;
this.isActive = true;
};
/**
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.doDeactivate = function() {
this.stopTransaction();
this._handle = null;
this._adornedShape = null;
var diagram = this.diagram;
if (diagram !== null) diagram.isMouseCaptured = false;
this.isActive = false;
};
/**
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.doCancel = function() {
var shape = this._adornedShape;
if (shape !== null) {
// explicitly restore the original route, in case !UndoManager.isEnabled
shape.geometry = this._originalGeometry;
}
this.stopTool();
};
/**
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.doMouseMove = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newpt = this.computeReshape(diagram.lastInput.documentPoint);
this.reshape(newpt);
}
};
/**
* @this {GeometryReshapingTool}
*/
GeometryReshapingTool.prototype.doMouseUp = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newpt = this.computeReshape(diagram.lastInput.documentPoint);
this.reshape(newpt);
this.transactionResult = this.name; // success
}
this.stopTool();
};
/**
* @expose
* @this {GeometryReshapingTool}
* @param {Point} newPoint the value of the call to {@link #computeReshape}.
*/
GeometryReshapingTool.prototype.reshape = function(newPoint) {
var shape = this.adornedShape;
if (shape === null || shape.geometry === null) return;
var locpt = shape.getLocalPoint(newPoint);
var geo = shape.geometry.copy();
var type = this.handle._typ;
if (type === undefined) return;
var fig = geo.figures.elt(this.handle._fig);
var seg = fig.segments.elt(this.handle._seg);
switch (type) {
case 0: fig.startX = locpt.x; fig.startY = locpt.y; break;
case 1: seg.endX = locpt.x; seg.endY = locpt.y; break;
case 2: seg.point1X = locpt.x; seg.point1Y = locpt.y; break;
case 3: seg.point2X = locpt.x; seg.point2Y = locpt.y; break;
}
var offset = geo.normalize(); // avoid any negative coordinates in the geometry
shape.desiredSize = new go.Size(NaN, NaN); // clear the desiredSize so Geometry can determine size
shape.geometry = geo; // modify the Shape
var part = shape.part; // move the Part holding the Shape
part.ensureBounds();
if (part.locationObject !== shape && !part.locationSpot.equals(go.Spot.Center)) { // but only if the locationSpot isn't Center
// support the whole Node being rotated
part.move(part.position.copy().subtract(offset.rotate(part.angle)));
}
this.updateAdornments(part); // update any Adornments of the Part
this.diagram.maybeUpdate(); // force more frequent drawing for smoother looking behavior
};
/**
* @expose
* @this {GeometryReshapingTool}
* @param {Point} p the point where the handle is being dragged.
* @return {Point}
*/
GeometryReshapingTool.prototype.computeReshape = function(p) {
return p; // no constraints on the points
};
+75
View File
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<title>Guided Dragging</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A demonstration of the GuidedDraggingTool extension." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="GuidedDraggingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
draggingTool: new GuidedDraggingTool(), // defined in GuidedDraggingTool.js
"draggingTool.horizontalGuidelineColor": "blue",
"draggingTool.verticalGuidelineColor": "blue",
"draggingTool.centerGuidelineColor": "green",
"draggingTool.guidelineWidth": 1,
"undoManager.isEnabled": true // enable undo & redo
});
// define a simple Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the TextBlock
$(go.Shape, "RoundedRectangle", { strokeWidth: 0 },
// Shape.fill is bound to Node.data.color
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 }, // some room around the text
// TextBlock.text is bound to Node.data.key
new go.Binding("text", "key"))
);
// but use the default Link template, by not setting Diagram.linkTemplate
// create the model data that will be represented by Nodes and Links
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" },
{ key: "Gamma", color: "lightgreen" },
{ key: "Delta", color: "pink" }
],
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Beta", to: "Beta" },
{ from: "Gamma", to: "Delta" },
{ from: "Delta", to: "Alpha" }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<!-- The DIV for the Diagram needs an explicit size or else we won't see anything.
Also add a border to help see the edges. -->
<div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:400px"></div>
<p>
This custom <a>DraggingTool</a> class makes guidelines visible as a Part is dragged around a Diagram and is nearly aligned with another Part.
If a locationObjectName is set, then this aligns <a>Part.locationObject</a>s instead.
The tool is defined in its own file, as <a href="GuidedDraggingTool.js">GuidedDraggingTool.js</a>.
</p>
<p>
Usage can also be seen in the <a href="FloorPlanEditor.html">Floor Plan Editor</a> sample.
</p>
</div>
</body>
</html>
+494
View File
@@ -0,0 +1,494 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends DraggingTool
* @class
* This draggingTool class makes guidelines visible as the parts are dragged around a diagram
* when the selected part is nearly aligned with another part.
*/
function GuidedDraggingTool() {
go.DraggingTool.call(this);
// temporary parts for horizonal guidelines
var $ = go.GraphObject.make;
var partProperties = { layerName: "Tool", isInDocumentBounds: false };
var shapeProperties = { stroke: "gray", isGeometryPositioned: true };
/** @ignore */
this.guidelineHtop =
$(go.Part, partProperties,
$(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" }));
/** @ignore */
this.guidelineHbottom =
$(go.Part, partProperties,
$(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" }));
/** @ignore */
this.guidelineHcenter =
$(go.Part, partProperties,
$(go.Shape, shapeProperties, { geometryString: "M0 0 100 0" }));
// temporary parts for vertical guidelines
/** @ignore */
this.guidelineVleft =
$(go.Part, partProperties,
$(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" }));
/** @ignore */
this.guidelineVright =
$(go.Part, partProperties,
$(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" }));
/** @ignore */
this.guidelineVcenter =
$(go.Part, partProperties,
$(go.Shape, shapeProperties, { geometryString: "M0 0 0 100" }));
// properties that the programmer can modify
/** @type {number} */
this._guidelineSnapDistance = 6;
/** @type {boolean} */
this._isGuidelineEnabled = true;
/** @type {string} */
this._horizontalGuidelineColor = "gray";
/** @type {string} */
this._verticalGuidelineColor = "gray";
/** @type {string} */
this._centerGuidelineColor = "gray";
/** @type {number} */
this._guidelineWidth = 1;
/** @type {number} */
this._searchDistance = 1000;
/** @type {boolean} */
this._isGuidelineSnapEnabled = true;
}
go.Diagram.inherit(GuidedDraggingTool, go.DraggingTool);
/**
* Removes all of the guidelines from the grid.
* @this {GuidedDraggingTool}
*/
GuidedDraggingTool.prototype.clearGuidelines = function() {
this.diagram.remove(this.guidelineHbottom);
this.diagram.remove(this.guidelineHcenter);
this.diagram.remove(this.guidelineHtop);
this.diagram.remove(this.guidelineVleft);
this.diagram.remove(this.guidelineVright);
this.diagram.remove(this.guidelineVcenter);
}
/**
* Calls the base method from {@link DraggingTool#doDeactivate}
* and removes the guidelines from the graph.
* @this {GuidedDraggingTool}
*/
GuidedDraggingTool.prototype.doDeactivate = function() {
go.DraggingTool.prototype.doDeactivate.call(this);
// clear any guidelines when dragging is done
this.clearGuidelines();
};
GuidedDraggingTool.prototype.doDragOver = function(pt, obj) {
// clear all existing guidelines in case either show... method decides to show a guideline
this.clearGuidelines();
// gets the selected part
var partItr = (this.copiedParts || this.draggedParts).iterator;
if (partItr.next()) {
var part = partItr.key;
this.showHorizontalMatches(part, this.isGuidelineEnabled, false);
this.showVerticalMatches(part, this.isGuidelineEnabled, false);
}
}
/**
* On a mouse-up, snaps the selected part to the nearest guideline.
* If not snapping, the part remains at its position.
* This calls {@link #guidelineSnap}.
* @this {GuidedDraggingTool}
*/
GuidedDraggingTool.prototype.doDropOnto = function(pt, obj) {
this.clearGuidelines();
// gets the selected (perhaps copied) Part
var partItr = (this.copiedParts || this.draggedParts).iterator;
if (partItr.next()) {
var part = partItr.key;
// snaps only when the mouse is released without shift modifier
var e = this.diagram.lastInput;
var snap = this.isGuidelineSnapEnabled && !e.shift;
this.showHorizontalMatches(part, false, snap); // false means don't show guidelines
this.showVerticalMatches(part, false, snap);
}
}
/**
* When nodes are shifted due to being guided upon a drop, make sure all connected link routes are invalidated,
* since the node is likely to have moved a different amount than all its connected links in the regular
* operation of the DraggingTool.
* @this {GuidedDraggingTool}
*/
GuidedDraggingTool.prototype.invalidateLinks = function(node) {
if (node instanceof go.Node) node.invalidateConnectedLinks();
}
/**
* This predicate decides whether or not the given Part should guide the dragged part.
* @param {Part} part a stationary Part to which the dragged part might be aligned
* @param {Part} guidedpart the Part being dragged
*/
GuidedDraggingTool.prototype.isGuiding = function(part, guidedpart) {
return part instanceof go.Part &&
!part.isSelected &&
!(part instanceof go.Link) &&
guidedpart instanceof go.Part &&
part.containingGroup === guidedpart.containingGroup &&
!part.layer.isTemporary;
}
/**
* This finds parts that are aligned near the selected part along horizontal lines. It compares the selected
* part to all parts within a rectangle approximately twice the {@link #searchDistance} wide.
* The guidelines appear when a part is aligned within a margin-of-error equal to {@link #guidelineSnapDistance}.
* The parameters used for {@link #guidelineSnap} are also set here.
* @this {GuidedDraggingTool}
* @param {Part} part
* @param {boolean} guideline if true, show guideline
* @param {boolean} snap if true, snap the part to where the guideline would be
*/
GuidedDraggingTool.prototype.showHorizontalMatches = function(part, guideline, snap) {
var objBounds = part.locationObject.getDocumentBounds();
var p0 = objBounds.y;
var p1 = objBounds.y + objBounds.height/2;
var p2 = objBounds.y + objBounds.height;
var marginOfError = this.guidelineSnapDistance;
var distance = this.searchDistance;
// compares with parts (or location objects) within narrow vertical area
var area = objBounds.copy();
area.inflate(distance, marginOfError + 1);
var tool = this;
var otherParts = this.diagram.findObjectsIn(area,
function(obj) { return obj.part; },
function(other) { return tool.isGuiding(other, part); },
true);
var bestDiff = marginOfError;
var bestPart = null;
var bestSpot;
var bestOtherSpot;
// horizontal line -- comparing y-values
otherParts.each(function(other) {
if (other === part) return; // ignore itself
var otherBounds = other.locationObject.getDocumentBounds();
var q0 = otherBounds.y;
var q1 = otherBounds.y + otherBounds.height/2;
var q2 = otherBounds.y + otherBounds.height;
// compare center with center of OTHER part
if (Math.abs(p1 - q1) < bestDiff) { bestDiff = Math.abs(p1 - q1); bestPart = other; bestSpot = go.Spot.Center; bestOtherSpot = go.Spot.Center; }
// compare top side with top and bottom sides of OTHER part
if (Math.abs(p0-q0) < bestDiff) { bestDiff = Math.abs(p0-q0); bestPart = other; bestSpot = go.Spot.Top; bestOtherSpot = go.Spot.Top; }
else if (Math.abs(p0-q2) < bestDiff) { bestDiff = Math.abs(p0-q2); bestPart = other; bestSpot = go.Spot.Top; bestOtherSpot = go.Spot.Bottom; }
// compare bottom side with top and bottom sides of OTHER part
if (Math.abs(p2-q0) < bestDiff) { bestDiff = Math.abs(p2-q0); bestPart = other; bestSpot = go.Spot.Bottom; bestOtherSpot = go.Spot.Top; }
else if (Math.abs(p2-q2) < bestDiff) { bestDiff = Math.abs(p2-q2); bestPart = other; bestSpot = go.Spot.Bottom; bestOtherSpot = go.Spot.Bottom; }
});
if (bestPart !== null) {
var offsetX = objBounds.x - part.actualBounds.x;
var offsetY = objBounds.y - part.actualBounds.y;
var bestBounds = bestPart.locationObject.getDocumentBounds();
// line extends from x0 to x2
var x0 = Math.min(objBounds.x, bestBounds.x) - 10;
var x2 = Math.max(objBounds.x + objBounds.width, bestBounds.x + bestBounds.width) + 10;
// find bestPart's desired Y
var bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot);
if (bestSpot === go.Spot.Center) {
if (snap) {
// call Part.move in order to automatically move member Parts of Groups
part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - objBounds.height / 2 - offsetY));
this.invalidateLinks(part);
}
if (guideline) {
this.guidelineHcenter.position = new go.Point(x0, bestPoint.y);
this.guidelineHcenter.elt(0).width = x2 - x0;
this.diagram.add(this.guidelineHcenter);
}
} else if (bestSpot === go.Spot.Top) {
if (snap) {
part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - offsetY));
this.invalidateLinks(part);
}
if (guideline) {
this.guidelineHtop.position = new go.Point(x0, bestPoint.y);
this.guidelineHtop.elt(0).width = x2 - x0;
this.diagram.add(this.guidelineHtop);
}
} else if (bestSpot === go.Spot.Bottom) {
if (snap) {
part.move(new go.Point(objBounds.x - offsetX, bestPoint.y - objBounds.height - offsetY));
this.invalidateLinks(part);
}
if (guideline) {
this.guidelineHbottom.position = new go.Point(x0, bestPoint.y);
this.guidelineHbottom.elt(0).width = x2 - x0;
this.diagram.add(this.guidelineHbottom);
}
}
}
}
/**
* This finds parts that are aligned near the selected part along vertical lines. It compares the selected
* part to all parts within a rectangle approximately twice the {@link #searchDistance} tall.
* The guidelines appear when a part is aligned within a margin-of-error equal to {@link #guidelineSnapDistance}.
* The parameters used for {@link #guidelineSnap} are also set here.
* @this {GuidedDraggingTool}
* @param {Part} part
* @param {boolean} guideline if true, show guideline
* @param {boolean} snap if true, don't show guidelines but just snap the part to where the guideline would be
*/
GuidedDraggingTool.prototype.showVerticalMatches = function(part, guideline, snap) {
var objBounds = part.locationObject.getDocumentBounds();
var p0 = objBounds.x;
var p1 = objBounds.x + objBounds.width/2;
var p2 = objBounds.x + objBounds.width;
var marginOfError = this.guidelineSnapDistance;
var distance = this.searchDistance;
// compares with parts within narrow vertical area
var area = objBounds.copy();
area.inflate(marginOfError + 1, distance);
var tool = this;
var otherParts = this.diagram.findObjectsIn(area,
function(obj) { return obj.part; },
function(other) { return tool.isGuiding(other, part); },
true);
var bestDiff = marginOfError;
var bestPart = null;
var bestSpot;
var bestOtherSpot;
// vertical line -- comparing x-values
otherParts.each(function(other) {
if (other === part) return; // ignore itself
var otherBounds = other.locationObject.getDocumentBounds();
var q0 = otherBounds.x;
var q1 = otherBounds.x + otherBounds.width/2;
var q2 = otherBounds.x + otherBounds.width;
// compare center with center of OTHER part
if (Math.abs(p1 - q1) < bestDiff) { bestDiff = Math.abs(p1 - q1); bestPart = other; bestSpot = go.Spot.Center; bestOtherSpot = go.Spot.Center; }
// compare left side with left and right sides of OTHER part
if (Math.abs(p0-q0) < bestDiff) { bestDiff = Math.abs(p0-q0); bestPart = other; bestSpot = go.Spot.Left; bestOtherSpot = go.Spot.Left; }
else if (Math.abs(p0-q2) < bestDiff) { bestDiff = Math.abs(p0-q2); bestPart = other; bestSpot = go.Spot.Left; bestOtherSpot = go.Spot.Right; }
// compare right side with left and right sides of OTHER part
if (Math.abs(p2-q0) < bestDiff) { bestDiff = Math.abs(p2-q0); bestPart = other; bestSpot = go.Spot.Right; bestOtherSpot = go.Spot.Left; }
else if (Math.abs(p2-q2) < bestDiff) { bestDiff = Math.abs(p2-q2); bestPart = other; bestSpot = go.Spot.Right; bestOtherSpot = go.Spot.Right; }
});
if (bestPart !== null) {
var offsetX = objBounds.x - part.actualBounds.x;
var offsetY = objBounds.y - part.actualBounds.y;
var bestBounds = bestPart.locationObject.getDocumentBounds();
// line extends from y0 to y2
var y0 = Math.min(objBounds.y, bestBounds.y) - 10;
var y2 = Math.max(objBounds.y + objBounds.height, bestBounds.y + bestBounds.height) + 10;
// find bestPart's desired X
var bestPoint = new go.Point().setRectSpot(bestBounds, bestOtherSpot);
if (bestSpot === go.Spot.Center) {
if (snap) {
// call Part.move in order to automatically move member Parts of Groups
part.move(new go.Point(bestPoint.x - objBounds.width / 2 - offsetX, objBounds.y - offsetY));
this.invalidateLinks(part);
}
if (guideline) {
this.guidelineVcenter.position = new go.Point(bestPoint.x, y0);
this.guidelineVcenter.elt(0).height = y2 - y0;
this.diagram.add(this.guidelineVcenter);
}
} else if (bestSpot === go.Spot.Left) {
if (snap) {
part.move(new go.Point(bestPoint.x - offsetX, objBounds.y - offsetY));
this.invalidateLinks(part);
}
if (guideline) {
this.guidelineVleft.position = new go.Point(bestPoint.x, y0);
this.guidelineVleft.elt(0).height = y2 - y0;
this.diagram.add(this.guidelineVleft);
}
} else if (bestSpot === go.Spot.Right) {
if (snap) {
part.move(new go.Point(bestPoint.x - objBounds.width - offsetX, objBounds.y - offsetY));
this.invalidateLinks(part);
}
if (guideline) {
this.guidelineVright.position = new go.Point(bestPoint.x, y0);
this.guidelineVright.elt(0).height = y2 - y0;
this.diagram.add(this.guidelineVright);
}
}
}
}
/**
* Gets or sets the margin of error for which guidelines show up.
* The default value is 6.
* Guidelines will show up when the aligned nods are ± 6px away from perfect alignment.
* @name GuidedDraggingTool#guidelineSnapDistance
* @return {number}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "guidelineSnapDistance", {
get: function() { return this._guidelineSnapDistance; },
set: function(val) {
if (typeof val !== "number" || isNaN(val) || val < 0) throw new Error("new value for GuidedDraggingTool.guidelineSnapDistance must be a non-negative number.");
if (this._guidelineSnapDistance !== val) {
this._guidelineSnapDistance = val;
}
}
});
/**
* Gets or sets whether the guidelines are enabled or disable.
* The default value is true.
* @name GuidedDraggingTool#isGuidelineEnabled
* @return {boolean}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "isGuidelineEnabled", {
get: function() { return this._isGuidelineEnabled; },
set: function(val) {
if (typeof val !== "boolean") throw new Error("new value for GuidedDraggingTool.isGuidelineEnabled must be a boolean value.");
if (this._isGuidelineEnabled !== val) {
this._isGuidelineEnabled = val;
}
}
});
/**
* Gets or sets the color of horizontal guidelines.
* The default value is "gray".
* @name GuidedDraggingTool#horizontalGuidelineColor
* @return {string}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "horizontalGuidelineColor", {
get: function() { return this._horizontalGuidelineColor; },
set: function(val) {
if (this._horizontalGuidelineColor !== val) {
this._horizontalGuidelineColor = val;
this.guidelineHbottom.elements.first().stroke = this._horizontalGuidelineColor;
this.guidelineHtop.elements.first().stroke = this._horizontalGuidelineColor;
}
}
});
/**
* Gets or sets the color of vertical guidelines.
* The default value is "gray".
* @name GuidedDraggingTool#verticalGuidelineColor
* @return {string}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "verticalGuidelineColor", {
get: function() { return this._verticalGuidelineColor; },
set: function(val) {
if (this._verticalGuidelineColor !== val) {
this._verticalGuidelineColor = val;
this.guidelineVleft.elements.first().stroke = this._verticalGuidelineColor;
this.guidelineVright.elements.first().stroke = this._verticalGuidelineColor;
}
}
});
/**
* Gets or sets the color of center guidelines.
* The default value is "gray".
* @name GuidedDraggingTool#centerGuidelineColor
* @return {string}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "centerGuidelineColor", {
get: function() { return this._centerGuidelineColor; },
set: function(val) {
if (this._centerGuidelineColor !== val) {
this._centerGuidelineColor = val;
this.guidelineVcenter.elements.first().stroke = this._centerGuidelineColor;
this.guidelineHcenter.elements.first().stroke = this._centerGuidelineColor;
}
}
});
/**
* Gets or sets the width guidelines.
* The default value is 1.
* @name GuidedDraggingTool#guidelineWidth
* @return {number}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "guidelineWidth", {
get: function() { return this._guidelineWidth; },
set: function(val) {
if (typeof val !== "number" || isNaN(val) || val < 0) throw new Error("New value for GuidedDraggingTool.guidelineWidth must be a non-negative number.");
if (this._guidelineWidth !== val) {
this._guidelineWidth = val;
this.guidelineVcenter.elements.first().strokeWidth = val;
this.guidelineHcenter.elements.first().strokeWidth = val;
this.guidelineVleft.elements.first().strokeWidth = val;
this.guidelineVright.elements.first().strokeWidth = val;
this.guidelineHbottom.elements.first().strokeWidth = val;
this.guidelineHtop.elements.first().strokeWidth = val;
}
}
});
/**
* Gets or sets the distance around the selected part to search for aligned parts.
* The default value is 1000.
* Set this to Infinity if you want to search the entire diagram no matter how far away.
* @name GuidedDraggingTool#searchDistance
* @return {number}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "searchDistance", {
get: function() { return this._searchDistance; },
set: function(val) {
if (typeof val !== "number" || isNaN(val) || val <= 0) throw new Error("new value for GuidedDraggingTool.searchDistance must be a positive number.");
if (this._searchDistance !== val) {
this._searchDistance = val;
}
}
});
/**
* Gets or sets whether snapping to guidelines is enabled.
* The default value is true.
* @name GuidedDraggingTool#isGuidelineSnapEnabled
* @return {Boolean}
*/
Object.defineProperty(GuidedDraggingTool.prototype, "isGuidelineSnapEnabled", {
get: function() { return this._isGuidelineSnapEnabled; },
set: function(val) {
if (typeof val !== "boolean") throw new Error("new value for GuidedDraggingTool.isGuidelineSnapEnabled must be a boolean.");
if (this._isGuidelineSnapEnabled !== val) {
this._isGuidelineSnapEnabled = val;
}
}
});
+52
View File
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<title>Demo of HyperlinkText Builder</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Implement a 'hyperlink' that allows the user to open a page by clicking." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="HyperlinkText.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv");
myDiagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape, "Ellipse", { fill: "lightskyblue" }),
$("HyperlinkText",
function(node) { return "https://gojs.net/" + node.data.version; },
function(node) { return "Visit GoJS " + node.data.version; },
{ margin: 10 }
)
);
myDiagram.model = new go.GraphLinksModel([
{ key: 1, version: "beta" },
{ key: 2, version: "latest" },
], [
{ from: 1, to: 2 }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<p>
This uses the "HyperlinkText" builder defined in <a href="HyperlinkText.js">HyperlinkText.js</a>.
</p>
<p>
Click on the text to open a window to a computed URL.
A mouse-over on the text will underline the text.
Hover on the text and you will see a tooltip showing the destination URL.
</p>
</div>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A "HyperlinkText" is either a TextBlock or a Panel containing a TextBlock that when clicked
// opens a new browser window with a given or computed URL.
// When the user's mouse passes over a "HyperlinkText", the text is underlined.
// When the mouse hovers over a "HyperlinkText", it shows a tooltip that displays the URL.
// This "HyperlinkText" builder is not pre-defined in the GoJS library, so you will need to load this definition.
// Typical usages:
// $("HyperlinkText", "https://gojs.net", "Visit GoJS")
//
// $("HyperlinkText",
// function(node) { return "https://gojs.net/" + node.data.version; },
// function(node) { return "Visit GoJS version " + node.data.version; })
//
// $("HyperlinkText",
// function(node) { return "https://gojs.net/" + node.data.version; },
// $(go.Panel, "Auto",
// $(go.Shape, ...),
// $(go.TextBlock, ...)
// )
// )
// The first argument to the "HyperlinkText" builder should be either the URL string or a function
// that takes the data-bound Panel and returns the URL string.
// If the URL string is empty or if the function returns an empty string,
// the text will not be underlined on a mouse-over and a click has no effect.
// The second argument to the "HyperlinkText" builder may be either a string to display in a TextBlock,
// or a function that takes the data-bound Panel and returns the string to display in a TextBlock.
// If no text string or function is provided, it assumes all of the arguments are used to
// define the visual tree for the "HyperlinkText", in the normal fashion for a Panel.
// The result is either a TextBlock or a Panel.
go.GraphObject.defineBuilder("HyperlinkText", function(args) {
// the URL is required as the first argument, either a string or a side-effect-free function returning a string
var url = go.GraphObject.takeBuilderArgument(args, undefined, function(x) { return typeof x === "string" || typeof x === "function"; });
// the text for the HyperlinkText is the optional second argument, either a string or a side-effect-free function returning a string
var text = go.GraphObject.takeBuilderArgument(args, null, function(x) { return typeof x === "string" || typeof x === "function"; });
// see if the visual tree is supplied in the arguments to the "HyperlinkText"
var anyGraphObjects = false;
for (var i = 0; i < args.length; i++) {
var a = args[i];
if (a && a instanceof go.GraphObject) anyGraphObjects = true;
}
// define the click behavior
var click =
function(e, obj) {
var u = obj._url;
if (typeof u === "function") u = u(obj.findTemplateBinder());
if (u) window.open(u, "_blank");
};
// define the tooltip
var tooltip =
go.GraphObject.make("ToolTip",
go.GraphObject.make(go.TextBlock,
{ name: "TB", margin: 4 },
new go.Binding("text", "", function(obj) {
// here OBJ will be in the Adornment, need to get the HyperlinkText/TextBlock
obj = obj.part.adornedObject;
var u = obj._url;
if (typeof u === "function") u = u(obj.findTemplateBinder());
return u;
}).ofObject()
),
new go.Binding("visible", "text", function(t) { return !!t; }).ofObject("TB")
);
// if the text is provided, use a new TextBlock; otherwise assume the TextBlock is provided
if (typeof (text) === "string" || typeof (text) === "function" || !anyGraphObjects) {
if (text === null && typeof (url) === "string") text = url;
var tb = go.GraphObject.make(go.TextBlock,
{
"_url": url,
cursor: "pointer",
mouseEnter: function(e, obj) {
var u = obj._url;
if (typeof u === "function") u = u(obj.findTemplateBinder());
if (u) obj.isUnderline = true;
},
mouseLeave: function(e, obj) { obj.isUnderline = false; },
click: click, // defined above
toolTip: tooltip // shared by all HyperlinkText textblocks
}
);
if (typeof(text) === "string") {
tb.text = text;
} else if (typeof(text) === "function") {
tb.bind(new go.Binding("text", "", text).ofObject())
} else if (typeof (url) === "function") {
tb.bind(new go.Binding("text", "", url).ofObject())
}
return tb;
} else {
function findTextBlock(obj) {
if (obj instanceof go.TextBlock) return obj;
if (obj instanceof go.Panel) {
var it = obj.elements;
while (it.next()) {
var result = findTextBlock(it.value);
if (result !== null) return result;
}
}
return null;
}
return go.GraphObject.make(go.Panel,
{
"_url": url,
cursor: "pointer",
mouseEnter: function(e, panel) {
var tb = findTextBlock(panel);
var u = panel._url;
if (typeof u === "function") u = u(panel.findTemplateBinder());
if (tb !== null && u) tb.isUnderline = true;
},
mouseLeave: function(e, panel) {
var tb = findTextBlock(panel);
if (tb !== null) tb.isUnderline = false;
},
click: click, // defined above
toolTip: tooltip // shared by all HyperlinkText panels
}
);
}
});
+45
View File
@@ -0,0 +1,45 @@
/* CSS for the lightbox context menu */
/* see also LightBoxContextMenu.js and samples/htmlLightBoxContextMenu.html */
#contextMenuDIV {
}
#cmLight {
top: 0px;
z-index:10002;
position: fixed;
text-align: center;
left: 25%;
width: 50%;
background-color: #F5F5F5;
padding: 16px;
border: 16px solid #444;
border-radius: 10px;
margin-top: 10px;
}
#cmDark {
z-index:10001;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
opacity: 0.8;
}
#cmLight ul { list-style: none; }
#cmLight li {
font:700 1.5em Helvetica, Arial, sans-serif;
position: relative;
min-width: 60px; }
#cmLight a {
color: #444;
display: inline-block;
padding: 4px;
text-decoration: none;
margin: 2px;
border: 1px solid gray;
border-radius: 10px;
}
+142
View File
@@ -0,0 +1,142 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// HTML + JavaScript context menu, made with HTMLInfo
// This file exposes one instance of HTMLInfo, window.myHTMLLightBox
// see also LightBoxContextMenu.css and /samples/htmlLightBoxContextMenu.html
(function(window) {
/* HTML for context menu:
<div id="contextMenuDIV">
<div id="cmLight"></div>
<div id="cmDark"></div>
</div>
*/
var contextMenuDIV = document.createElement("div");
contextMenuDIV.id = "contextMenuDIV";
// This is the actual HTML LightBox-style context menu, composed of buttons and a background:
var cmLight = document.createElement("div");
cmLight.id = "cmLight";
cmLight.className = "goCXforeground";
var cmDark = document.createElement("div");
cmDark.id = "cmDark";
cmDark.className = "goCXbackground";
contextMenuDIV.appendChild(cmLight);
contextMenuDIV.appendChild(cmDark);
var cxMenuButtons = [
{
text: 'Copy',
command: function(diagram) { diagram.commandHandler.copySelection(); },
isVisible: function(diagram) { return diagram.commandHandler.canCopySelection(); }
}, {
text: 'Cut',
command: function(diagram) { diagram.commandHandler.cutSelection(); },
isVisible: function(diagram) { return diagram.commandHandler.canCutSelection(); }
}, {
text: 'Delete',
command: function(diagram) { diagram.commandHandler.deleteSelection(); },
isVisible: function(diagram) { return diagram.commandHandler.canDeleteSelection(); }
}, {
text: 'Paste',
command: function (diagram) { diagram.commandHandler.pasteSelection(diagram.toolManager.contextMenuTool.mouseDownPoint); },
isVisible: function (diagram) { return diagram.commandHandler.canPasteSelection(diagram.toolManager.contextMenuTool.mouseDownPoint); }
}, {
text: 'Select All',
command: function(diagram) { diagram.commandHandler.selectAll(); },
isVisible: function(diagram) { return diagram.commandHandler.canSelectAll(); }
}, {
text: 'Undo',
command: function(diagram) { diagram.commandHandler.undo(); },
isVisible: function(diagram) { return diagram.commandHandler.canUndo(); }
}, {
text: 'Redo',
command: function(diagram) { diagram.commandHandler.redo(); },
isVisible: function(diagram) { return diagram.commandHandler.canRedo(); }
}, {
text: 'Scroll To Part',
command: function(diagram) { diagram.commandHandler.scrollToPart(); },
isVisible: function(diagram) { return diagram.commandHandler.canScrollToPart(); }
}, {
text: 'Zoom To Fit',
command: function(diagram) { diagram.commandHandler.zoomToFit(); },
isVisible: function(diagram) { return diagram.commandHandler.canZoomToFit(); }
}, {
text: 'Reset Zoom',
command: function(diagram) { diagram.commandHandler.resetZoom(); },
isVisible: function(diagram) { return diagram.commandHandler.canResetZoom(); }
}, {
text: 'Group Selection',
command: function(diagram) { diagram.commandHandler.groupSelection(); },
isVisible: function(diagram) { return diagram.commandHandler.canGroupSelection(); }
}, {
text: 'Ungroup Selection',
command: function(diagram) { diagram.commandHandler.ungroupSelection(); },
isVisible: function(diagram) { return diagram.commandHandler.canUngroupSelection(); }
}, {
text: 'Edit Text',
command: function(diagram) { diagram.commandHandler.editTextBlock(); },
isVisible: function(diagram) { return diagram.commandHandler.canEditTextBlock(); }
}
];
var $ = go.GraphObject.make;
var myContextMenu = $(go.HTMLInfo, {
show: showContextMenu,
hide: hideContextMenu
});
var firstTime = true;
function showContextMenu(obj, diagram, tool) {
if (firstTime) {
// We don't want the div acting as a context menu to have a (browser) context menu!
cmLight.addEventListener('contextmenu', function(e) { e.preventDefault(); return false; }, false);
cmLight.addEventListener('selectstart', function(e) { e.preventDefault(); return false; }, false);
contextMenuDIV.addEventListener('contextmenu', function(e) { e.preventDefault(); return false; }, false);
// Stop the context menu tool if you click on the dark part:
contextMenuDIV.addEventListener('click', function(e) { diagram.currentTool.stopTool(); return false; }, false);
firstTime = false;
}
// Empty the context menu and only show buttons that are relevant
cmLight.innerHTML = '';
var ul = document.createElement('ul');
cmLight.appendChild(ul);
for (var i = 0; i < cxMenuButtons.length; i++) {
var button = cxMenuButtons[i];
var command = button.command;
var isVisible = button.isVisible;
if (!(typeof command === 'function')) continue;
// Only show buttons that have isVisible = true
if (typeof isVisible === 'function' && !isVisible(diagram)) continue;
var li = document.createElement('li');
var ahref = document.createElement('a');
ahref.href = '#';
ahref._command = button.command;
ahref.addEventListener('click', function(e) {
this._command(diagram);
tool.stopTool();
e.preventDefault();
return false;
}, false);
ahref.textContent = button.text;
li.appendChild(ahref);
ul.appendChild(li);
}
// show the whole LightBox context menu
document.body.appendChild(contextMenuDIV);
};
function hideContextMenu(diagram, tool) {
document.body.removeChild(contextMenuDIV);
}
window.myHTMLLightBox = myContextMenu;
})(window);
+221
View File
@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html>
<head>
<title>State Chart with Draggable Link Labels</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow the user to shift link labels; useful when the user wants to adjust the position of the label relative to the link path to avoid overlapping the link path or any nodes." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="LinkLabelDraggingTool.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // must name or refer to the DIV HTML element
{
// have mouse wheel events zoom in and out instead of scroll up and down
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,
// support double-click in background creating a new node
"clickCreatingTool.archetypeNodeData": { text: "new node" },
// enable undo & redo
"undoManager.isEnabled": true
});
// install the LinkLabelDraggingTool as a "mouse move" tool
myDiagram.toolManager.mouseMoveTools.insertAt(0, new LinkLabelDraggingTool());
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("SaveButton");
if (button) button.disabled = !myDiagram.isModified;
var idx = document.title.indexOf("*");
if (myDiagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
// define the Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// define the node's outer shape, which will surround the TextBlock
$(go.Shape, "RoundedRectangle",
{
parameter1: 20, // the corner has a large radius
fill: $(go.Brush, "Linear", { 0: "rgb(254, 201, 0)", 1: "rgb(254, 162, 0)" }),
stroke: "black",
portId: "",
fromLinkable: true,
fromLinkableSelfNode: true,
fromLinkableDuplicates: true,
toLinkable: true,
toLinkableSelfNode: true,
toLinkableDuplicates: true,
cursor: "pointer"
}),
$(go.TextBlock,
{
font: "bold 11pt helvetica, bold arial, sans-serif",
editable: true // editing the text automatically updates the model data
},
new go.Binding("text", "text").makeTwoWay())
);
// unlike the normal selection Adornment, this one includes a Button
myDiagram.nodeTemplate.selectionAdornmentTemplate =
$(go.Adornment, "Spot",
$(go.Panel, "Auto",
$(go.Shape, { fill: null, stroke: "blue", strokeWidth: 2 }),
$(go.Placeholder) // this represents the selected Node
),
// the button to create a "next" node, at the top-right corner
$("Button",
{
alignment: go.Spot.TopRight,
click: addNodeAndLink // this function is defined below
},
$(go.Shape, "PlusLine", { desiredSize: new go.Size(6, 6) })
) // end button
); // end Adornment
// clicking the button inserts a new node to the right of the selected node,
// and adds a link to that new node
function addNodeAndLink(e, obj) {
var adorn = obj.part;
e.handled = true;
var diagram = adorn.diagram;
diagram.startTransaction("Add State");
// get the node data for which the user clicked the button
var fromNode = adorn.adornedPart;
var fromData = fromNode.data;
// create a new "State" data object, positioned off to the right of the adorned Node
var toData = { text: "new" };
var p = fromNode.location.copy();
p.x += 200;
toData.loc = go.Point.stringify(p); // the "loc" property is a string, not a Point object
// add the new node data to the model
var model = diagram.model;
model.addNodeData(toData);
// create a link data from the old node data to the new node data
var linkdata = {
from: model.getKeyForNodeData(fromData), // or just: fromData.id
to: model.getKeyForNodeData(toData),
text: "transition"
};
// and add the link data to the model
model.addLinkData(linkdata);
// select the new Node
var newnode = diagram.findNodeForData(toData);
diagram.select(newnode);
diagram.commitTransaction("Add State");
// if the new node is off-screen, scroll the diagram to show the new node
diagram.scrollToRect(newnode.actualBounds);
}
// replace the default Link template in the linkTemplateMap
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{ curve: go.Link.Bezier, adjusting: go.Link.Stretch, reshapable: true },
new go.Binding("points").makeTwoWay(),
new go.Binding("curviness", "curviness"),
$(go.Shape, // the link shape
{ strokeWidth: 1.5 }),
$(go.Shape, // the arrowhead
{ toArrow: "standard", stroke: null }),
$(go.Panel, "Auto",
{ cursor: "move" }, // visual hint that the user can do something with this link label
$(go.Shape, // the label background, which becomes transparent around the edges
{
fill: $(go.Brush, "Radial",
{ 0: "rgb(240, 240, 240)", 0.3: "rgb(240, 240, 240)", 1: "rgba(240, 240, 240, 0)" }),
stroke: null
}),
$(go.TextBlock, "transition", // the label text
{
textAlign: "center",
font: "10pt helvetica, arial, sans-serif",
stroke: "black",
margin: 4,
editable: true // editing the text automatically updates the model data
},
new go.Binding("text", "text").makeTwoWay()),
// The GraphObject.segmentOffset property is what the LinkLabelDraggingTool modifies.
// This TwoWay binding saves any changes to the same named property on the link data.
new go.Binding("segmentOffset", "segmentOffset", go.Point.parse).makeTwoWay(go.Point.stringify)
)
);
// read in the JSON-format data from the "mySavedModel" element
load();
}
// Show the diagram's model in JSON format
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: whitesmoke; border: solid 1px black; width: 100%; height: 400px"></div>
<p>
This sample is a modification of the <a href="../samples/stateChart.html">State Chart</a> sample
that makes use of the LinkLabelDraggingTool that is defined in its own file, as <a href="LinkLabelDraggingTool.js">LinkLabelDraggingTool.js</a>.
</p>
<p>
Note that after dragging a link label you can move a node connected by that link and the label maintains the same position relative to the link route.
That relative position is specified by the <a>GraphObject.segmentOffset</a> property.
This sample also saves any changes to that property by means of a TwoWay <a>Binding</a>.
</p>
<p>
See also the similar <a href="LinkLabelOnPathDragging.html" target="_blank">Link Label On Path Dragging sample</a>,
where the label is constrained to remain on the path of the link.
</p>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
Diagram Model saved in JSON format:
<br />
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"nodeKeyProperty": "id",
"nodeDataArray": [
{ "id": 0, "loc": "120 120", "text": "Initial" },
{ "id": 1, "loc": "330 120", "text": "First down" },
{ "id": 2, "loc": "226 376", "text": "First up" },
{ "id": 3, "loc": "60 276", "text": "Second down" },
{ "id": 4, "loc": "226 226", "text": "Wait" }
],
"linkDataArray": [
{ "from": 0, "to": 0, "text": "up or timer", "curviness": -20 },
{ "from": 0, "to": 1, "text": "down", "curviness": 20 },
{ "from": 1, "to": 0, "text": "up (moved)\nPOST", "curviness": 20 },
{ "from": 1, "to": 1, "text": "down", "curviness": -20 },
{ "from": 1, "to": 2, "text": "up (no move)" },
{ "from": 1, "to": 4, "text": "timer" },
{ "from": 2, "to": 0, "text": "timer\nPOST" },
{ "from": 2, "to": 3, "text": "down" },
{ "from": 3, "to": 0, "text": "up\nPOST\n(dblclick\nif no move)" },
{ "from": 3, "to": 3, "text": "down or timer", "curviness": 20 },
{ "from": 4, "to": 0, "text": "up\nPOST" },
{ "from": 4, "to": 4, "text": "down" }
]
}
</textarea>
</div>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for moving a label on a Link
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* This tool only works when the Link has a label
* that is positioned at the Link.midPoint plus some offset.
* It does not work for labels that have a particular segmentIndex.
*/
function LinkLabelDraggingTool() {
go.Tool.call(this);
this.name = "LinkLabelDragging";
/** @type {GraphObject} */
this.label = null;
/** @type {Point} */
this._offset = new go.Point(); // of the mouse relative to the center of the label object
/** @type {Point} */
this._originalOffset = null;
}
go.Diagram.inherit(LinkLabelDraggingTool, go.Tool);
/**
* This tool can only start if the mouse has moved enough so that it is not a click,
* and if the mouse down point is on a GraphObject "label" in a Link Panel,
* as determined by findLabel().
* @this {LinkLabelDraggingTool}
* @return {boolean}
*/
LinkLabelDraggingTool.prototype.canStart = function() {
if (!go.Tool.prototype.canStart.call(this)) return false;
var diagram = this.diagram;
if (diagram === null) return false;
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
var e = diagram.lastInput;
if (!e.left) return false;
if (!this.isBeyondDragSize()) return false;
return this.findLabel() !== null;
}
/**
* From the GraphObject at the mouse point, search up the visual tree until we get to
* an object that is a label of a Link.
* @this {LinkLabelDraggingTool}
* @return {GraphObject} This returns null if no such label is at the mouse down point.
*/
LinkLabelDraggingTool.prototype.findLabel = function() {
var diagram = this.diagram;
var e = diagram.lastInput;
var elt = diagram.findObjectAt(e.documentPoint, null, null);
if (elt === null || !(elt.part instanceof go.Link)) return null;
while (elt.panel !== elt.part) {
elt = elt.panel;
}
// If it's at an arrowhead segment index, don't consider it a label:
if (elt.segmentIndex === 0 || elt.segmentIndex === -1) return null;
return elt;
};
/**
* Start a transaction, call findLabel and remember it as the "label" property,
* and remember the original value for the label's segmentOffset property.
* @this {LinkLabelDraggingTool}
*/
LinkLabelDraggingTool.prototype.doActivate = function() {
this.startTransaction("Shifted Label");
this.label = this.findLabel();
if (this.label !== null) {
// compute the offset of the mouse-down point relative to the center of the label
this._offset = this.diagram.firstInput.documentPoint.copy().subtract(this.label.getDocumentPoint(go.Spot.Center));
this._originalOffset = this.label.segmentOffset.copy();
}
go.Tool.prototype.doActivate.call(this);
}
/**
* Stop any ongoing transaction.
* @this {LinkLabelDraggingTool}
*/
LinkLabelDraggingTool.prototype.doDeactivate = function() {
go.Tool.prototype.doDeactivate.call(this);
this.stopTransaction();
}
/**
* Clear any reference to a label element.
* @this {LinkLabelDraggingTool}
*/
LinkLabelDraggingTool.prototype.doStop = function() {
this.label = null;
go.Tool.prototype.doStop.call(this);
}
/**
* Restore the label's original value for GraphObject.segmentOffset.
* @this {LinkLabelDraggingTool}
*/
LinkLabelDraggingTool.prototype.doCancel = function() {
if (this.label !== null) {
this.label.segmentOffset = this._originalOffset;
}
go.Tool.prototype.doCancel.call(this);
}
/**
* During the drag, call updateSegmentOffset in order to set
* the GraphObject.segmentOffset of the label.
* @this {LinkLabelDraggingTool}
*/
LinkLabelDraggingTool.prototype.doMouseMove = function() {
if (!this.isActive) return;
this.updateSegmentOffset();
}
/**
* At the end of the drag, update the segment offset of the label and finish the tool,
* completing a transaction.
* @this {LinkLabelDraggingTool}
*/
LinkLabelDraggingTool.prototype.doMouseUp = function() {
if (!this.isActive) return;
this.updateSegmentOffset();
this.transactionResult = "Shifted Label";
this.stopTool();
}
/**
* Save the label's GraphObject.segmentOffset as a rotated offset from the midpoint of the
* Link that the label is in.
* @this {LinkLabelDraggingTool}
*/
LinkLabelDraggingTool.prototype.updateSegmentOffset = function() {
var lab = this.label;
if (lab === null) return;
var link = lab.part;
if (!(link instanceof go.Link)) return;
var last = this.diagram.lastInput.documentPoint;
var idx = lab.segmentIndex;
var numpts = link.pointsCount;
// if the label is a "mid" label, assume it is positioned differently from a label at a particular segment
if (idx < -numpts || idx >= numpts) {
var mid = link.midPoint;
// need to rotate this point to account for the angle of the link segment at the mid-point
var p = new go.Point(last.x - this._offset.x - mid.x, last.y - this._offset.y - mid.y);
lab.segmentOffset = p.rotate(-link.midAngle);
} else { // handle the label point being on a partiular segment with a given fraction
var frac = lab.segmentFraction;
var a, b;
if (idx >= 0) { // indexing forwards
a = link.getPoint(idx);
b = (idx < numpts - 1) ? link.getPoint(idx + 1) : a;
} else { // or backwards if segmentIndex is negative
var i = numpts + idx;
a = link.getPoint(i);
b = (i > 0) ? link.getPoint(i - 1) : a;
}
var labx = a.x + (b.x - a.x) * frac;
var laby = a.y + (b.y - a.y) * frac;
var p = new go.Point(last.x - this._offset.x - labx, last.y - this._offset.y - laby);
var segangle = (idx >= 0) ? a.directionPoint(b) : b.directionPoint(a);
lab.segmentOffset = p.rotate(-segangle);
}
}
+112
View File
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html>
<head>
<title>Draggable Link Labels That Stay On Path</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="This variation on the LinkLabelDraggingTool extension restricts link labels to stay on the link's route while the user is dragging it." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="LinkLabelOnPathDraggingTool.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv", // the ID of the DIV HTML element
{
layout: $(go.ForceDirectedLayout,
{ defaultSpringLength: 50, defaultElectricalCharge: 50 }),
"undoManager.isEnabled": true
});
// install the LinkLabelDraggingTool as a "mouse move" tool
myDiagram.toolManager.mouseMoveTools.insertAt(0, new LinkLabelOnPathDraggingTool());
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
{ locationSpot: go.Spot.Center },
$(go.Shape,
{
fill: "orange", // default fill color
portId: "",
fromLinkable: true,
fromSpot: go.Spot.AllSides,
toLinkable: true,
toSpot: go.Spot.AllSides,
cursor: "pointer"
},
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 10, font: "bold 12pt sans-serif" },
new go.Binding("text"))
);
myDiagram.linkTemplate =
$(go.Link,
{
routing: go.Link.AvoidsNodes,
corner: 5,
relinkableFrom: true,
relinkableTo: true,
reshapable: true,
resegmentable: true
},
$(go.Shape),
$(go.Shape, { toArrow: "OpenTriangle" }),
$(go.Panel, "Auto",
// mark this Panel as being a draggable label, and set default segment props
{ _isLinkLabel: true, segmentIndex: NaN, segmentFraction: .5 },
$(go.Shape, { fill: "white" }),
$(go.TextBlock, "?", { margin: 3 },
new go.Binding("text", "color")),
// remember any modified segment properties in the link data object
new go.Binding("segmentFraction").makeTwoWay()
)
);
// create a few nodes and links
myDiagram.model = new go.GraphLinksModel([
{ key: 1, text: "one", color: "lightyellow" },
{ key: 2, text: "two", color: "brown" },
{ key: 3, text: "three", color: "green" },
{ key: 4, text: "four", color: "slateblue" },
{ key: 5, text: "five", color: "aquamarine" },
{ key: 6, text: "six", color: "lightgreen" },
{ key: 7, text: "seven" }
], [
{ from: 5, to: 6, color: "orange" },
{ from: 1, to: 2, color: "red" },
{ from: 1, to: 3, color: "blue" },
{ from: 1, to: 4, color: "goldenrod" },
{ from: 2, to: 5, color: "fuchsia" },
{ from: 3, to: 5, color: "green" },
{ from: 4, to: 5, color: "black" },
{ from: 6, to: 7 }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: whitesmoke; border: solid 1px black; width: 100%; height: 600px"></div>
<p>
This sample demonstrates a custom Tool, LinkLabelOnPathDraggingTool, that allows the user to drag the label of a Link,
but that keeps the label exactly on the path of the link.
The tool is defined at <a href="LinkLabelOnPathDraggingTool.js">LinkLabelOnPathDraggingTool.js</a>.
</p>
<p>
The label on the link can be any arbitrarily complex object.
It is positioned by the <a>GraphObject.segmentIndex</a> and <a>GraphObject.segmentFraction</a> properties.
The segmentIndex is set to NaN such that the whole link path acts as the segment, and the segmentFraction is set by the LinkLabelOnPathDraggingTool.
A two-way data binding on segmentFraction automatically remembers any modified value on the link data object in the model.
</p>
<p>
The tool is derived from a similar tool, <a href="LinkLabelDraggingTool.js">LinkLabelDraggingTool.js</a>,
that allows the user to drag the label in any direction from the mid-point of the Link path.
</p>
</div>
</body>
</html>
@@ -0,0 +1,151 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for moving a label on a Link that keeps the label on the link's path.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* This tool only works when the Link has a label marked by the "_isLinkLabel" property.
*/
function LinkLabelOnPathDraggingTool() {
go.Tool.call(this);
this.name = "LinkLabelOnPathDragging";
/** @type {GraphObject} */
this.label = null;
/** @type {number} */
this._originalFraction = null;
}
go.Diagram.inherit(LinkLabelOnPathDraggingTool, go.Tool);
/**
* This tool can only start if the mouse has moved enough so that it is not a click,
* and if the mouse down point is on a GraphObject "label" in a Link Panel,
* as determined by findLabel().
* @this {LinkLabelOnPathDraggingTool}
* @return {boolean}
*/
LinkLabelOnPathDraggingTool.prototype.canStart = function() {
if (!go.Tool.prototype.canStart.call(this)) return false;
var diagram = this.diagram;
if (diagram === null) return false;
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
var e = diagram.lastInput;
if (!e.left) return false;
if (!this.isBeyondDragSize()) return false;
return this.findLabel() !== null;
}
/**
* From the GraphObject at the mouse point, search up the visual tree until we get to
* an object that has the "_isLinkLabel" property set to true and that is an immediate child of a Link Panel.
* @this {LinkLabelOnPathDraggingTool}
* @return {GraphObject} This returns null if no such label is at the mouse down point.
*/
LinkLabelOnPathDraggingTool.prototype.findLabel = function() {
var diagram = this.diagram;
var e = diagram.lastInput;
var elt = diagram.findObjectAt(e.documentPoint, null, null);
if (elt === null || !(elt.part instanceof go.Link)) return null;
while (elt.panel !== elt.part) {
elt = elt.panel;
}
// If it's not marked as "_isLinkLabel", don't consider it a label:
if (!elt._isLinkLabel) return null;
return elt;
};
/**
* Start a transaction, call findLabel and remember it as the "label" property,
* and remember the original values for the label's segment properties.
* @this {LinkLabelOnPathDraggingTool}
*/
LinkLabelOnPathDraggingTool.prototype.doActivate = function() {
this.startTransaction("Shifted Label");
this.label = this.findLabel();
if (this.label !== null) {
this._originalFraction = this.label.segmentFraction;
}
go.Tool.prototype.doActivate.call(this);
}
/**
* Stop any ongoing transaction.
* @this {LinkLabelOnPathDraggingTool}
*/
LinkLabelOnPathDraggingTool.prototype.doDeactivate = function() {
go.Tool.prototype.doDeactivate.call(this);
this.stopTransaction();
}
/**
* Clear any reference to a label element.
* @this {LinkLabelOnPathDraggingTool}
*/
LinkLabelOnPathDraggingTool.prototype.doStop = function() {
this.label = null;
go.Tool.prototype.doStop.call(this);
}
/**
* Restore the label's original value for GraphObject.segment... properties.
* @this {LinkLabelOnPathDraggingTool}
*/
LinkLabelOnPathDraggingTool.prototype.doCancel = function() {
if (this.label !== null) {
this.label.segmentFraction = this._originalFraction;
}
go.Tool.prototype.doCancel.call(this);
}
/**
* During the drag, call updateSegmentOffset in order to set the segment... properties of the label.
* @this {LinkLabelOnPathDraggingTool}
*/
LinkLabelOnPathDraggingTool.prototype.doMouseMove = function() {
if (!this.isActive) return;
this.updateSegmentOffset();
}
/**
* At the end of the drag, update the segment properties of the label and finish the tool,
* completing a transaction.
* @this {LinkLabelOnPathDraggingTool}
*/
LinkLabelOnPathDraggingTool.prototype.doMouseUp = function() {
if (!this.isActive) return;
this.updateSegmentOffset();
this.transactionResult = "Shifted Label";
this.stopTool();
}
/**
* Save the label's GraphObject.segmentFraction at the closest point to the mouse.
* @this {LinkLabelOnPathDraggingTool}
*/
LinkLabelOnPathDraggingTool.prototype.updateSegmentOffset = function() {
var lab = this.label;
if (lab === null) return;
var link = lab.part;
if (!(link instanceof go.Link)) return;
var last = this.diagram.lastInput.documentPoint;
// find the fractional distance along the link path closest to this point
var path = link.path;
var localpt = path.getLocalPoint(last);
lab.segmentFraction = path.geometry.getFractionForPoint(localpt);
}
+84
View File
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<title>Link Shifting Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow the user to shift the end of a link that is connected with a rectangular node." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="LinkShiftingTool.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"undoManager.isEnabled": true
});
myDiagram.toolManager.mouseDownTools.add($(LinkShiftingTool));
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{
fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides,
fromLinkable: true, toLinkable: true,
locationSpot: go.Spot.Center
},
new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape, { fill: "lightgray" }),
$(go.TextBlock, { margin: 10 },
{ fromLinkable: false, toLinkable: false },
new go.Binding("text", "key"))
);
myDiagram.linkTemplate =
$(go.Link,
{
reshapable: true, resegmentable: true,
relinkableFrom: true, relinkableTo: true,
adjusting: go.Link.Stretch
},
// remember the (potentially) user-modified route
new go.Binding("points").makeTwoWay(),
// remember any spots modified by LinkShiftingTool
new go.Binding("fromSpot", "fromSpot", go.Spot.parse).makeTwoWay(go.Spot.stringify),
new go.Binding("toSpot", "toSpot", go.Spot.parse).makeTwoWay(go.Spot.stringify),
$(go.Shape),
$(go.Shape, { toArrow: "Standard" })
);
myDiagram.model = new go.GraphLinksModel([
{ key: "Alpha", location: "0 0" },
{ key: "Beta", location: "0 100" }
], [
{ from: "Alpha", to: "Beta" }
]);
myDiagram.addDiagramListener("InitialLayoutCompleted", function(e) {
// select the Link in order to show its two additional Adornments, for shifting the ends
myDiagram.links.first().isSelected = true;
});
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<p>
This sample demonstrates the LinkShiftingTool, which is an extra tool
that can be installed in the ToolManager to allow users to shift the end
point of the link to be anywhere along the sides of the port with which it
remains connected.
</p>
<p>
This only looks good for ports that occupy the whole of a rectangular node.
If you want to restrict the user's permitted sides, you can adapt the
<code>LinkShiftingTool.doReshape</code> method to do what you want.
</p>
</div>
</body>
</html>
+264
View File
@@ -0,0 +1,264 @@
"use strict"
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for shifting the end point of a Link to be anywhere along the edges of the port.
/**
* This constructor produces a tool for shifting the end of a link;
* use it in a diagram.toolManager.mouseDownTools list:
* <pre>myDiagram.toolManager.mouseDownTools.add(new LinkShiftingTool());</pre>
* @constructor
* @extends Tool
* @class
*/
function LinkShiftingTool() {
go.Tool.call(this);
this.name = "LinkShifting";
// these are archetypes for the two shift handles, one at each end of the Link:
var h = new go.Shape();
h.geometryString = "F1 M0 0 L8 0 M8 4 L0 4";
h.fill = null;
h.stroke = "dodgerblue";
h.background = "lightblue";
h.cursor = "pointer";
h.segmentIndex = 0;
h.segmentFraction = 1;
h.segmentOrientation = go.Link.OrientAlong;
/** @type {GraphObject} */
this._fromHandleArchetype = h;
h = new go.Shape();
h.geometryString = "F1 M0 0 L8 0 M8 4 L0 4";
h.fill = null;
h.stroke = "dodgerblue";
h.background = "lightblue";
h.cursor = "pointer";
h.segmentIndex = -1;
h.segmentFraction = 1;
h.segmentOrientation = go.Link.OrientAlong;
/** @type {GraphObject} */
this._toHandleArchetype = h;
// transient state
/** @type {GraphObject} */
this._handle = null;
/** @type {List} */
this._originalPoints = null;
}
go.Diagram.inherit(LinkShiftingTool, go.Tool);
/*
* A small GraphObject used as a shifting handle.
* @name LinkShiftingTool#fromHandleArchetype
* @return {GraphObject}
*/
Object.defineProperty(LinkShiftingTool.prototype, "fromHandleArchetype", {
get: function() { return this._fromHandleArchetype; },
set: function(value) { this._fromHandleArchetype = value; }
});
/*
* A small GraphObject used as a shifting handle.
* @name LinkShiftingTool#toHandleArchetype
* @return {GraphObject}
*/
Object.defineProperty(LinkShiftingTool.prototype, "toHandleArchetype", {
get: function() { return this._toHandleArchetype; },
set: function(value) { this._toHandleArchetype = value; }
});
/**
* @this {LinkShiftingTool}
* @param {Part} part
*/
LinkShiftingTool.prototype.updateAdornments = function(part) {
if (part === null || !(part instanceof go.Link)) return; // this tool only applies to Links
var link = part;
// show handles if link is selected, remove them if no longer selected
var category = "LinkShiftingFrom";
var adornment = null;
if (link.isSelected && !this.diagram.isReadOnly) {
var selelt = link.selectionObject;
if (selelt !== null && link.actualBounds.isReal() && link.isVisible() &&
selelt.actualBounds.isReal() && selelt.isVisibleObject()) {
var spot = link.computeSpot(true);
if (spot.isSide() || spot.isSpot()) {
adornment = link.findAdornment(category);
if (adornment === null) {
adornment = this.makeAdornment(selelt, false);
adornment.category = category;
link.addAdornment(category, adornment);
} else {
// This is just to invalidate the measure, so it recomputes itself based on the adorned link
adornment.segmentFraction = Math.random();
}
}
}
}
if (adornment === null) link.removeAdornment(category);
category = "LinkShiftingTo";
adornment = null;
if (link.isSelected && !this.diagram.isReadOnly) {
var selelt = link.selectionObject;
if (selelt !== null && link.actualBounds.isReal() && link.isVisible() &&
selelt.actualBounds.isReal() && selelt.isVisibleObject()) {
var spot = link.computeSpot(false);
if (spot.isSide() || spot.isSpot()) {
adornment = link.findAdornment(category);
if (adornment === null) {
adornment = this.makeAdornment(selelt, true);
adornment.category = category;
link.addAdornment(category, adornment);
} else {
// This is just to invalidate the measure, so it recomputes itself based on the adorned link
adornment.segmentFraction = Math.random();
}
}
}
}
if (adornment === null) link.removeAdornment(category);
};
/**
* @this {LinkShiftingTool}
* @param {GraphObject} selelt the {@link GraphObject} of the {@link Link} being shifted.
* @param {boolean} toend
* @return {Adornment}
*/
LinkShiftingTool.prototype.makeAdornment = function(selelt, toend) {
var adornment = new go.Adornment();
adornment.type = go.Panel.Link;
var h = (toend ? this.toHandleArchetype : this.fromHandleArchetype);
if (h !== null) {
// add a single handle for shifting at one end
adornment.add(h.copy());
}
adornment.adornedObject = selelt;
return adornment;
};
/**
* @this {LinkShiftingTool}
* @return {boolean}
*/
LinkShiftingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly || diagram.isModelReadOnly) return false;
if (!diagram.lastInput.left) return false;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, "LinkShiftingFrom");
if (h === null) h = this.findToolHandleAt(diagram.firstInput.documentPoint, "LinkShiftingTo");
return (h !== null);
}
/**
* @this {LinkShiftingTool}
*/
LinkShiftingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, "LinkShiftingFrom");
if (h === null) h = this.findToolHandleAt(diagram.firstInput.documentPoint, "LinkShiftingTo");
if (h === null) return;
var ad = h.part;
var link = ad.adornedObject.part;
if (!(link instanceof go.Link)) return;
this._handle = h;
this._originalPoints = link.points.copy();
this.startTransaction(this.name);
diagram.isMouseCaptured = true;
diagram.currentCursor = 'pointer';
this.isActive = true;
};
/**
* @this {LinkShiftingTool}
*/
LinkShiftingTool.prototype.doDeactivate = function() {
this.isActive = false;
var diagram = this.diagram;
if (diagram === null) return;
diagram.isMouseCaptured = false;
diagram.currentCursor = '';
this.stopTransaction();
};
/**
* Clean up tool state.
* @this {LinkShiftingTool}
*/
LinkShiftingTool.prototype.doStop = function() {
this._handle = null;
this._originalPoints = null;
};
/**
* Clean up tool state.
* @this {LinkShiftingTool}
*/
LinkShiftingTool.prototype.doCancel = function() {
var ad = this._handle.part;
var link = ad.adornedObject.part;
link.points = this._originalPoints;
this.stopTool();
};
/**
* @this {LinkShiftingTool}
*/
LinkShiftingTool.prototype.doMouseMove = function() {
if (this.isActive) {
this.doReshape(this.diagram.lastInput.documentPoint);
}
};
/**
* @this {LinkShiftingTool}
*/
LinkShiftingTool.prototype.doMouseUp = function() {
if (this.isActive) {
this.doReshape(this.diagram.lastInput.documentPoint);
this.transactionResult = this.name;
}
this.stopTool();
};
/**
* @this {LinkShiftingTool}
* @param {Point} pt
*/
LinkShiftingTool.prototype.doReshape = function(pt) {
var ad = this._handle.part;
var link = ad.adornedObject.part;
var fromend = ad.category === "LinkShiftingFrom";
var port = null;
if (fromend) {
port = link.fromPort;
} else {
port = link.toPort;
}
if (port === null) return;
// support rotated ports
var portang = port.getDocumentAngle();
var center = port.getDocumentPoint(go.Spot.Center);
var portb = new go.Rect(port.getDocumentPoint(go.Spot.TopLeft).subtract(center).rotate(-portang).add(center),
port.getDocumentPoint(go.Spot.BottomRight).subtract(center).rotate(-portang).add(center));
var lp = link.getLinkPointFromPoint(port.part, port, center, pt, fromend);
lp = lp.copy().subtract(center).rotate(-portang).add(center);
var spot = new go.Spot(Math.max(0, Math.min(1, (lp.x - portb.x) / (portb.width || 1))),
Math.max(0, Math.min(1, (lp.y - portb.y) / (portb.height || 1))));
if (fromend) {
link.fromSpot = spot;
} else {
link.toSpot = spot;
}
};
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html>
<head>
<title>Local Storage Commands</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="The LocalStorageCommandHandler extension enhances the copy and paste commands to use 'localStorage' as the storage mechanism." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="LocalStorageCommandHandler.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
commandHandler: new LocalStorageCommandHandler(), // defined in DrawCommandHandler.js
"undoManager.isEnabled": true // enable undo & redo
});
myDiagram2 = $(go.Diagram, "myDiagramDiv2", // create a Diagram for the DIV HTML element
{
commandHandler: new LocalStorageCommandHandler(), // defined in DrawCommandHandler.js
"undoManager.isEnabled": true // enable undo & redo
});
// define a simple Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the TextBlock
$(go.Shape, "RoundedRectangle", { strokeWidth: 0 },
// Shape.fill is bound to Node.data.color
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 }, // some room around the text
// TextBlock.text is bound to Node.data.key
new go.Binding("text", "key"))
);
myDiagram2.nodeTemplate = myDiagram.nodeTemplate;
// create the model data that will be represented by Nodes and Links
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" },
{ key: "Gamma", color: "lightgreen" },
{ key: "Delta", color: "pink" }
],
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Beta", to: "Beta" },
{ from: "Gamma", to: "Delta" },
{ from: "Delta", to: "Alpha" }
]);
myDiagram2.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" },
{ key: "Gamma", color: "lightgreen" },
{ key: "Delta", color: "pink" }
],
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Beta", to: "Beta" },
{ from: "Gamma", to: "Delta" },
{ from: "Delta", to: "Alpha" }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<span style="display: inline-block; vertical-align: top;">
<div id="myDiagramDiv" style="border: solid 1px black; width:300px; height:300px"></div>
</span>
<span style="display: inline-block; vertical-align: top;">
<div id="myDiagramDiv2" style="border: solid 1px black; width:300px; height:300px"></div>
</span>
<p>
This example demonstrates a custom <a>CommandHandler</a>.
It uses localStorage as the repository for the clipboard, rather than an in-memory global variable.
It is defined in its own file, as <a href="LocalStorageCommandHandler.js">LocalStorageCommandHandler.js</a>.
</p>
<p>
Try copying and pasting between the above Diagrams, or between tabs/windows that contain Diagrams using LocalStorageCommandHandler.
Note that when copying and pasting between Diagrams, it will work best if they have similar templates.
</p>
</div>
</body>
</html>
+120
View File
@@ -0,0 +1,120 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends CommandHandler
* @class
* This CommandHandler class uses localStorage as the repository for the clipboard,
* rather than an in-memory global variable.
* It requires that the {@link Diagram#model} be serializable and deserializable using {@link Model#toJson} and {@link Model.fromJson}.
* <p>
* The {@link #copyToClipboard} and {@link #pasteFromClipboard} functions fall back to using the standard definitions
* if there are any errors calling <code>Storage.getItem</code> or <code>Storage.setItem</code>.
* <p>
* Typical usage:
* <pre>
* $(go.Diagram, "myDiagramDiv",
* {
* commandHandler: $(LocalStorageCommandHandler),
* . . .
* }
* )
* </pre>
* or:
* <pre>
* myDiagram.commandHandler = new LocalStorageCommandHandler();
* </pre>
*/
function LocalStorageCommandHandler() {
go.CommandHandler.call(this);
this.StorageKey = "go._clipboard";
this.FormatKey = "go._clipboardFormat";
}
go.Diagram.inherit(LocalStorageCommandHandler, go.CommandHandler);
/**
* @this {LocalStorageCommandHandler}
* @param {Iterable.<Part>} coll a collection of {@link Part}s.
*/
LocalStorageCommandHandler.prototype.copyToClipboard = function(coll) {
try {
if (coll === null) {
window.localStorage.setItem(this.StorageKey, "");
window.localStorage.setItem(this.FormatKey, "");
} else {
var clipdiag = new go.Diagram(); // create a temporary Diagram
// copy from this diagram to the temporary diagram some properties that affects copying:
clipdiag.isTreePathToChildren = this.diagram.isTreePathToChildren;
clipdiag.toolManager.draggingTool.dragsLink = this.diagram.toolManager.draggingTool.dragsLink;
// create a model like this one but with no data
clipdiag.model = this.diagram.model.copy();
// copy the given Parts into this temporary Diagram
this.diagram.copyParts(coll, clipdiag, false);
window.localStorage.setItem(this.StorageKey, clipdiag.model.toJson());
window.localStorage.setItem(this.FormatKey, clipdiag.model.dataFormat);
}
} catch (ex) {
// fallback implementation
go.CommandHandler.prototype.copyToClipboard.call(this, coll);
}
};
/**
* @this {LocalStorageCommandHandler}
* @return {Set.<Part>} a collection of newly pasted {@link Part}s
*/
LocalStorageCommandHandler.prototype.pasteFromClipboard = function() {
var coll = new go.Set(/*go.Part*/);
try {
var clipstr = window.localStorage.getItem(this.StorageKey);
var clipfrmt = window.localStorage.getItem(this.FormatKey);
if (clipstr === null || clipstr === "" || clipfrmt !== this.diagram.model.dataFormat) {
return coll;
} else {
var clipdiag = new go.Diagram(); // create a temporary Diagram
// recover the model from the clipboard rendering
clipdiag.model = go.Model.fromJson(clipstr);
// copy all the CLIPDIAG Parts into this Diagram
var all = new go.List().addAll(clipdiag.parts).addAll(clipdiag.nodes).addAll(clipdiag.links);
var copymap = this.diagram.copyParts(all, this.diagram, false);
// return a Set of the copied Parts
return new go.Set(/*go.Part*/).addAll(copymap.iteratorValues);
}
} catch (ex) {
// fallback implementation
return go.CommandHandler.prototype.pasteFromClipboard.call(this);
}
};
/**
* @this {LocalStorageCommandHandler}
* @param {Point?} pos
* @return {boolean}
*/
LocalStorageCommandHandler.prototype.canPasteSelection = function(pos) {
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly || diagram.isModelReadOnly) return false;
if (!diagram.allowInsert || !diagram.allowClipboard) return false;
try {
var clipstr = window.localStorage.getItem(this.StorageKey);
var clipfrmt = window.localStorage.getItem(this.FormatKey);
if (clipstr === null || clipstr === "") return false;
if (clipfrmt !== diagram.model.dataFormat) return false;
return true;
} catch (ex) {
// fallback implementation
return go.CommandHandler.prototype.canPasteSelection(pos);
}
};
+225
View File
@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html>
<head>
<title>State Chart with Draggable Node Labels</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow the user to shift the label of a node." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="NodeLabelDraggingTool.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // must name or refer to the DIV HTML element
{
// have mouse wheel events zoom in and out instead of scroll up and down
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,
// support double-click in background creating a new node
"clickCreatingTool.archetypeNodeData": { text: "new node" },
// enable undo & redo
"undoManager.isEnabled": true
});
// install the NodeLabelDraggingTool as a "mouse move" tool
myDiagram.toolManager.mouseMoveTools.insertAt(0, new NodeLabelDraggingTool());
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("SaveButton");
if (button) button.disabled = !myDiagram.isModified;
var idx = document.title.indexOf("*");
if (myDiagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
// define the Node template
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{ locationObjectName: "ICON", locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{ selectionObjectName: "ICON" },
// define the node primary shape
$(go.Shape, "RoundedRectangle",
{
name: "ICON",
parameter1: 10, // the corner has a medium radius
desiredSize: new go.Size(40, 40),
fill: $(go.Brush, "Linear", { 0: "rgb(254, 201, 0)", 1: "rgb(254, 162, 0)" }),
stroke: "black",
portId: "",
fromLinkable: true,
fromLinkableSelfNode: true,
fromLinkableDuplicates: true,
toLinkable: true,
toLinkableSelfNode: true,
toLinkableDuplicates: true,
cursor: "pointer"
}),
$(go.Shape, // provide interior area where the user can grab the node
{ fill: "transparent", stroke: null, desiredSize: new go.Size(30, 30) }),
$(go.TextBlock,
{
font: "bold 11pt helvetica, bold arial, sans-serif",
editable: true, // editing the text automatically updates the model data
_isNodeLabel: true,
cursor: "move" // visual hint that the user can do something with this node label
},
new go.Binding("text", "text").makeTwoWay(),
// The GraphObject.alignment property is what the NodeLabelDraggingTool modifies.
// This TwoWay binding saves any changes to the same named property on the node data.
new go.Binding("alignment", "alignment", go.Spot.parse).makeTwoWay(go.Spot.stringify)
)
);
// unlike the normal selection Adornment, this one includes a Button
myDiagram.nodeTemplate.selectionAdornmentTemplate =
$(go.Adornment, "Spot",
$(go.Panel, "Auto",
$(go.Shape, { fill: null, stroke: "blue", strokeWidth: 2 }),
$(go.Placeholder) // this represents the selected Node
),
// the button to create a "next" node, at the top-right corner
$("Button",
{
alignment: go.Spot.TopRight,
click: addNodeAndLink // this function is defined below
},
$(go.Shape, "PlusLine", { desiredSize: new go.Size(6, 6) })
) // end button
); // end Adornment
// clicking the button inserts a new node to the right of the selected node,
// and adds a link to that new node
function addNodeAndLink(e, obj) {
var adorn = obj.part;
e.handled = true;
var diagram = adorn.diagram;
diagram.startTransaction("Add State");
// get the node data for which the user clicked the button
var fromNode = adorn.adornedPart;
var fromData = fromNode.data;
// create a new "State" data object, positioned off to the right of the adorned Node
var toData = { text: "new" };
var p = fromNode.location.copy();
p.x += 200;
toData.loc = go.Point.stringify(p); // the "loc" property is a string, not a Point object
// add the new node data to the model
var model = diagram.model;
model.addNodeData(toData);
// create a link data from the old node data to the new node data
var linkdata = {
from: model.getKeyForNodeData(fromData), // or just: fromData.id
to: model.getKeyForNodeData(toData),
text: "transition"
};
// and add the link data to the model
model.addLinkData(linkdata);
// select the new Node
var newnode = diagram.findNodeForData(toData);
diagram.select(newnode);
diagram.commitTransaction("Add State");
// if the new node is off-screen, scroll the diagram to show the new node
diagram.scrollToRect(newnode.actualBounds);
}
// replace the default Link template in the linkTemplateMap
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{ curve: go.Link.Bezier, adjusting: go.Link.Stretch, reshapable: true },
new go.Binding("points").makeTwoWay(),
new go.Binding("curviness", "curviness"),
$(go.Shape, // the link shape
{ strokeWidth: 1.5 }),
$(go.Shape, // the arrowhead
{ toArrow: "standard", stroke: null }),
$(go.Panel, "Auto",
$(go.Shape, // the label background, which becomes transparent around the edges
{
fill: $(go.Brush, "Radial",
{ 0: "rgb(240, 240, 240)", 0.3: "rgb(240, 240, 240)", 1: "rgba(240, 240, 240, 0)" }),
stroke: null
}),
$(go.TextBlock, "transition", // the label text
{
textAlign: "center",
font: "10pt helvetica, arial, sans-serif",
stroke: "black",
margin: 4,
editable: true // editing the text automatically updates the model data
},
new go.Binding("text", "text").makeTwoWay())
)
);
// read in the JSON-format data from the "mySavedModel" element
load();
}
// Show the diagram's model in JSON format
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: whitesmoke; border: solid 1px black; width: 100%; height: 400px"></div>
<p>
This sample is a modification of the <a href="../samples/stateChart.html">State Chart</a> sample
that makes use of the NodeLabelDraggingTool that is defined in its own file, as <a href="NodeLabelDraggingTool.js">NodeLabelDraggingTool.js</a>.
</p>
<p>
Note that after dragging a node label you can move that node and the label maintains the same position relative to the node.
That relative position is specified by the <a>GraphObject.alignment</a> property, used by the "Spot" <a>Panel</a>.
This sample also saves any changes to that property by means of a TwoWay <a>Binding</a>.
</p>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
Diagram Model saved in JSON format:
<br />
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"nodeKeyProperty": "id",
"nodeDataArray": [
{ "id": 0, "loc": "120 120", "text": "Initial" },
{ "id": 1, "loc": "330 120", "text": "First down" },
{ "id": 2, "loc": "226 376", "text": "First up" },
{ "id": 3, "loc": "60 276", "text": "Second down" },
{ "id": 4, "loc": "226 226", "text": "Wait" }
],
"linkDataArray": [
{ "from": 0, "to": 0, "text": "up or timer", "curviness": -20 },
{ "from": 0, "to": 1, "text": "down", "curviness": 20 },
{ "from": 1, "to": 0, "text": "up (moved)\nPOST", "curviness": 20 },
{ "from": 1, "to": 1, "text": "down", "curviness": -20 },
{ "from": 1, "to": 2, "text": "up (no move)" },
{ "from": 1, "to": 4, "text": "timer" },
{ "from": 2, "to": 0, "text": "timer\nPOST" },
{ "from": 2, "to": 3, "text": "down" },
{ "from": 3, "to": 0, "text": "up\nPOST\n(dblclick\nif no move)" },
{ "from": 3, "to": 3, "text": "down or timer", "curviness": 20 },
{ "from": 4, "to": 0, "text": "up\nPOST" },
{ "from": 4, "to": 4, "text": "down" }
]
}
</textarea>
</div>
</body>
</html>
+157
View File
@@ -0,0 +1,157 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for moving a label on a Node
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* This tool only works when the Node has a label (any GraphObject) marked with
* { _isNodeLabel: true } that is positioned in a Spot Panel.
* It works by modifying that label's GraphObject.alignment property to have an
* offset from the center of the panel.
*/
function NodeLabelDraggingTool() {
go.Tool.call(this);
this.name = "NodeLabelDragging";
/** @type {GraphObject} */
this.label = null;
/** @type {Point} */
this._offset = new go.Point(); // of the mouse relative to the center of the label object
/** @type {Point} */
this._originalAlignment = null;
/** @type {Point} */
this._originalCenter = null;
}
go.Diagram.inherit(NodeLabelDraggingTool, go.Tool);
/**
* This tool can only start if the mouse has moved enough so that it is not a click,
* and if the mouse down point is on a GraphObject "label" in a Spot Panel,
* as determined by findLabel().
* @this {NodeLabelDraggingTool}
* @return {boolean}
*/
NodeLabelDraggingTool.prototype.canStart = function() {
if (!go.Tool.prototype.canStart.call(this)) return false;
var diagram = this.diagram;
if (diagram === null) return false;
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
var e = diagram.lastInput;
if (!e.left) return false;
if (!this.isBeyondDragSize()) return false;
return this.findLabel() !== null;
}
/**
* From the GraphObject at the mouse point, search up the visual tree until we get to
* an object that has the "_isNodeLabel" property set to true, that is in a Spot Panel,
* and that is not the first element of that Panel (i.e. not the main element of the panel).
* @this {NodeLabelDraggingTool}
* @return {GraphObject} This returns null if no such label is at the mouse down point.
*/
NodeLabelDraggingTool.prototype.findLabel = function() {
var diagram = this.diagram;
var e = diagram.firstInput;
var elt = diagram.findObjectAt(e.documentPoint, null, null);
if (elt === null || !(elt.part instanceof go.Node)) return null;
while (elt.panel !== null) {
if (elt._isNodeLabel && elt.panel.type === go.Panel.Spot && elt.panel.findMainElement() !== elt) return elt;
elt = elt.panel;
}
return null;
};
/**
* Start a transaction, call findLabel and remember it as the "label" property,
* and remember the original value for the label's alignment property.
* @this {NodeLabelDraggingTool}
*/
NodeLabelDraggingTool.prototype.doActivate = function() {
this.startTransaction("Shifted Label");
this.label = this.findLabel();
if (this.label !== null) {
// compute the offset of the mouse-down point relative to the center of the label
this._offset = this.diagram.firstInput.documentPoint.copy().subtract(this.label.getDocumentPoint(go.Spot.Center));
this._originalAlignment = this.label.alignment.copy();
var main = this.label.panel.findMainElement();
this._originalCenter = main.getDocumentPoint(go.Spot.Center);
}
go.Tool.prototype.doActivate.call(this);
}
/**
* Stop any ongoing transaction.
* @this {NodeLabelDraggingTool}
*/
NodeLabelDraggingTool.prototype.doDeactivate = function() {
go.Tool.prototype.doDeactivate.call(this);
this.stopTransaction();
}
/**
* Clear any reference to a label element.
* @this {NodeLabelDraggingTool}
*/
NodeLabelDraggingTool.prototype.doStop = function() {
this.label = null;
go.Tool.prototype.doStop.call(this);
}
/**
* Restore the label's original value for GraphObject.alignment.
* @this {NodeLabelDraggingTool}
*/
NodeLabelDraggingTool.prototype.doCancel = function() {
if (this.label !== null) {
this.label.alignment = this._originalAlignment;
}
go.Tool.prototype.doCancel.call(this);
}
/**
* During the drag, call updateAlignment in order to set the GraphObject.alignment of the label.
* @this {NodeLabelDraggingTool}
*/
NodeLabelDraggingTool.prototype.doMouseMove = function() {
if (!this.isActive) return;
this.updateAlignment();
}
/**
* At the end of the drag, update the alignment of the label and finish the tool,
* completing a transaction.
* @this {NodeLabelDraggingTool}
*/
NodeLabelDraggingTool.prototype.doMouseUp = function() {
if (!this.isActive) return;
this.updateAlignment();
this.transactionResult = "Shifted Label";
this.stopTool();
}
/**
* Save the label's GraphObject.alignment as an absolute offset from the center of the Spot Panel
* that the label is in.
* @this {NodeLabelDraggingTool}
*/
NodeLabelDraggingTool.prototype.updateAlignment = function() {
if (this.label === null) return;
var last = this.diagram.lastInput.documentPoint;
var cntr = this._originalCenter;
this.label.alignment = new go.Spot(0.5, 0.5, last.x - this._offset.x - cntr.x, last.y - this._offset.y - cntr.y);
}
+76
View File
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<title>Non-Realtime Dragging</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A modification of DraggingTool to show a ghost image of what is being moved, rather than moving the nodes and links in realtime." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="NonRealtimeDraggingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{ // install the replacement DraggingTool:
draggingTool: $(NonRealtimeDraggingTool, { duration: 600 }),
"undoManager.isEnabled": true
});
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{ locationSpot: go.Spot.Center },
$(go.Shape, "Circle",
{
fill: "white", // the default fill, if there is no data-binding
portId: "", cursor: "pointer", // the Shape is the port, not the whole Node
// allow all kinds of links from and to this port
fromLinkable: true, fromLinkableSelfNode: true, fromLinkableDuplicates: true,
toLinkable: true, toLinkableSelfNode: true, toLinkableDuplicates: true
},
new go.Binding("fill", "color")),
$(go.TextBlock,
{
font: "bold 14px sans-serif",
stroke: '#333',
margin: 6, // make some extra space for the shape around the text
isMultiline: false, // don't allow newlines in text
editable: true // allow in-place editing by user
},
new go.Binding("text", "text").makeTwoWay()) // the label shows the node data's text
);
myDiagram.model = new go.GraphLinksModel([
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen", group: 5 },
{ key: 4, text: "Delta", color: "pink", group: 5 },
{ key: 5, text: "Epsilon", color: "green", isGroup: true }
], [
{ from: 1, to: 2, color: "blue" },
{ from: 2, to: 2 },
{ from: 3, to: 4, color: "green" },
{ from: 3, to: 1, color: "purple" }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<p>
This custom <a>DraggingTool</a> class causes the user to drag around a translucent image of the Nodes and Links being moved,
leaving the selected Parts in place, rather than actually moving those Nodes and Links in realtime.
Only when the mouse up occurs does the move happen.
</p>
<p>
This tool is defined in its own file, as <a href="NonRealtimeDraggingTool.js">NonRealtimeDraggingTool.js</a>
</p>
</div>
</body>
</html>
+146
View File
@@ -0,0 +1,146 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom DraggingTool for dragging an image instead of actually moving any selected nodes,
// until the mouse-up event.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends DraggingTool
* @class
*/
function NonRealtimeDraggingTool() {
go.DraggingTool.call(this);
this._duration = 0; // duration of movement animation; <= 0 to disable
/** @type {Part} */
this._imagePart = null; // a Part holding a translucent image of what would be dragged
/** @type {Map.<Part,DraggingInfo>} */
this._ghostDraggedParts = null; // a Map of the _imagePart and its dragging information
/** @type {Map.<Part,DraggingInfo>} */
this._originalDraggedParts = null; // the saved normal value of DraggingTool.draggedParts
}
go.Diagram.inherit(NonRealtimeDraggingTool, go.DraggingTool);
/**
* Gets or sets how long the movement animation should be to move the actual parts upon a mouse-up.
* The default value is zero -- there is no animation of the movement.
* @this {NonRealtimeDraggingTool}
* @return {number}
*/
Object.defineProperty(NonRealtimeDraggingTool.prototype, "duration", {
get: function() { return this._duration; },
set: function(val) { this._duration = val; }
});
/**
* Call the base method, and then make an image of the returned collection,
* show it using a Picture, and hold the Picture in a temporary Part, as _imagePart.
* @this {NonRealtimeDraggingTool}
* @param {Iterable.<Part>} parts A {@link Set} or {@link List} of {@link Part}s.
* @return {Map.<Part,DraggingInfo>}
*/
NonRealtimeDraggingTool.prototype.computeEffectiveCollection = function(coll) {
var map = go.DraggingTool.prototype.computeEffectiveCollection.call(this, coll);
if (this.isActive && this._imagePart === null) {
var bounds = this.diagram.computePartsBounds(map.toKeySet());
var offset = this.diagram.lastInput.documentPoint.copy().subtract(bounds.position);
var $ = go.GraphObject.make;
this._imagePart =
$(go.Part,
{ layerName: "Tool", opacity: 0.5, locationSpot: new go.Spot(0, 0, offset.x, offset.y) },
$(go.Picture,
{ element: this.diagram.makeImage({ parts: map.toKeySet() }) })
);
}
return map;
};
/**
* When activated, replace the DraggingTool.draggedParts with the _ghostDraggedParts, which
* consists of just one Part, the _imagePart, added to the Diagram at the current mouse point.
* @this {NonRealtimeDraggingTool}
*/
NonRealtimeDraggingTool.prototype.doActivate = function() {
go.DraggingTool.prototype.doActivate.call(this);
if (this._imagePart !== null) {
this._imagePart.location = this.diagram.lastInput.documentPoint;
this.diagram.add(this._imagePart);
this._originalDraggedParts = this.draggedParts;
this._ghostDraggedParts = go.DraggingTool.prototype.computeEffectiveCollection.call(this,
new go.List().add(this._imagePart));
this.draggedParts = this._ghostDraggedParts;
}
};
/**
* When deactivated, make sure any _imagePart is removed from the Diagram and all references are cleared out.
* @this {NonRealtimeDraggingTool}
*/
NonRealtimeDraggingTool.prototype.doDeactivate = function() {
if (this._imagePart !== null) {
this.diagram.remove(this._imagePart);
}
this._imagePart = null;
this._ghostDraggedParts = null;
this._originalDraggedParts = null;
go.DraggingTool.prototype.doDeactivate.call(this);
};
/**
* Do the normal mouse-up behavior, but only after restoring DraggingTool.draggedParts.
* @this {NonRealtimeDraggingTool}
*/
NonRealtimeDraggingTool.prototype.doMouseUp = function() {
var partsmap = this._originalDraggedParts;
if (partsmap !== null) {
this.draggedParts = partsmap;
}
go.DraggingTool.prototype.doMouseUp.call(this);
if (partsmap !== null && this.duration > 0) {
var anim = new go.Animation();
anim.duration = this.duration;
partsmap.each(function(kvp) {
var part = kvp.key;
anim.add(part, "location", kvp.value.point, part.location);
});
anim.start();
}
};
/**
* If the user changes to "copying" mode by holding down the Control key,
* return to the regular behavior and remove the _imagePart.
* @this {NonRealtimeDraggingTool}
*/
NonRealtimeDraggingTool.prototype.doKeyDown = function() {
if (this._imagePart !== null && this._originalDraggedParts !== null &&
(this.diagram.lastInput.control || this.diagram.lastInput.meta) && this.mayCopy()) {
this.draggedParts = this._originalDraggedParts;
this.diagram.remove(this._imagePart);
}
go.DraggingTool.prototype.doKeyDown.call(this);
};
/**
* If the user changes back to "moving" mode,
* show the _imagePart again and go back to dragging the _ghostDraggedParts.
* @this {NonRealtimeDraggingTool}
*/
NonRealtimeDraggingTool.prototype.doKeyUp = function() {
if (this._imagePart !== null && this._ghostDraggedParts !== null && this.mayMove()) {
this._imagePart.location = this.diagram.lastInput.documentPoint;
this.diagram.add(this._imagePart);
this.draggedParts = this._ghostDraggedParts;
}
go.DraggingTool.prototype.doKeyUp.call(this);
};
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<title>Orthogonal Link Reshaping Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="An elaboration of the standard LinkReshapingTool that adds a broad handle to allow the user to easily drag a segment." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="OrthogonalLinkReshapingTool.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"undoManager.isEnabled": true,
"linkReshapingTool": new OrthogonalLinkReshapingTool()
});
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{
width: 80,
height: 50,
locationSpot: go.Spot.Center
},
new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape, { fill: "lightgray" }),
$(go.TextBlock, { margin: 10 },
new go.Binding("text", "key"))
);
myDiagram.linkTemplate =
$(go.Link,
{
routing: go.Link.AvoidsNodes,
reshapable: true,
resegmentable: true
},
new go.Binding("points").makeTwoWay(),
$(go.Shape, { strokeWidth: 2 })
);
myDiagram.model = new go.GraphLinksModel([
{ key: "Alpha", location: "0 0" },
{ key: "Beta", location: "200 0" },
{ key: "Gamma", location: "100 0" }
], [
{ from: "Alpha", to: "Beta" }
]);
myDiagram.addDiagramListener("InitialLayoutCompleted", function(e) {
// select the Link in order to show its two additional Adornments, for shifting the ends
myDiagram.links.first().isSelected = true;
});
}
function updateRouting() {
var routing = getRadioValue("routing");
var newRouting = (routing === "orthogonal") ? go.Link.Orthogonal : go.Link.AvoidsNodes;
myDiagram.startTransaction("update routing");
myDiagram.linkTemplate.routing = newRouting;
myDiagram.links.each(function(l) {
l.routing = newRouting;
});
myDiagram.commitTransaction("update routing");
}
function getRadioValue(name) {
var radio = document.getElementsByName(name);
for (var i = 0; i < radio.length; i++)
if (radio[i].checked) return radio[i].value;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
Routing:
<input type="radio" name="routing" onclick="updateRouting()" value="orthogonal" />Orthogonal
<input type="radio" name="routing" onclick="updateRouting()" value="avoidsnodes" checked="checked" />AvoidsNodes
<p>
This sample demonstrates the OrthogonalLinkReshapingTool that is defined in its own file, as <a href="OrthogonalLinkReshapingTool.js">OrthogonalLinkReshapingTool.js</a>.
This tool allow users to shift the sections of orthogonal links in addition to resegmenting them.
The Diagram's <a>ToolManager.linkReshapingTool</a> and link template's <a>Part.reshapable</a> properties must be set to use this tool.
The <a>Link.resegmentable</a> property can still optionally be used.
</p>
</div>
</body>
</html>
@@ -0,0 +1,161 @@
"use strict"
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for dragging a segment of an orthogonal link.
/**
* This OrthogonalLinkReshapingTool class allows for a Link's path to be modified by the user
* via the dragging of a tool handle along the link segment, which will move the whole segment.
* @constructor
* @extends LinkReshapingTool
* @class
*/
function OrthogonalLinkReshapingTool() {
go.LinkReshapingTool.call(this);
this.name = "OrthogonalLinkReshaping";
this._alreadyAddedPoint = false;
}
go.Diagram.inherit(OrthogonalLinkReshapingTool, go.LinkReshapingTool);
/**
* For orthogonal, straight links, create the handles and set reshaping behavior.
* @param {Shape} pathshape
* @return {Adornment}
* @this {OrthogonalLinkReshapingTool}
*/
OrthogonalLinkReshapingTool.prototype.makeAdornment = function(pathshape) {
var link = pathshape.part;
// add all normal handles first
var adornment = go.LinkReshapingTool.prototype.makeAdornment.call(this, pathshape);
// add long reshaping handles for orthogonal, straight links
if (link !== null && link.isOrthogonal && link.curve !== go.Link.Bezier) {
var firstindex = link.firstPickIndex + (link.resegmentable ? 0 : 1);
var lastindex = link.lastPickIndex - (link.resegmentable ? 0 : 1);
for (var i = firstindex; i < lastindex; i++) {
this.makeSegmentDragHandle(link, adornment, i);
}
}
return adornment;
};
/**
* Once we finish a reshape, make sure any handles are properly updated.
* @this {OrthogonalLinkReshapingTool}
*/
OrthogonalLinkReshapingTool.prototype.doDeactivate = function() {
this._alreadyAddedPoint = false;
// when we finish, recreate adornment to ensure proper reshaping behavior/cursor
var link = this.adornedLink;
if (link !== null && link.isOrthogonal && link.curve !== go.Link.Bezier) {
var pathshape = link.path;
var adornment = this.makeAdornment(pathshape);
if (adornment !== null) {
link.addAdornment(this.name, adornment);
adornment.location = link.position;
}
}
go.LinkReshapingTool.prototype.doDeactivate.call(this);
};
/**
* Set the reshaping behavior for segment dragging handles.
* @param {Point} newpt
* @this {OrthogonalLinkReshapingTool}
*/
OrthogonalLinkReshapingTool.prototype.reshape = function(newpt) {
var link = this.adornedLink;
// identify if the handle being dragged is a segment dragging handle
if (link !== null && link.isOrthogonal && link.curve !== go.Link.Bezier && this.handle.toMaxLinks === 999) {
link.startRoute();
var index = this.handle.segmentIndex; // for these handles, firstPickIndex <= index < lastPickIndex
if (!this._alreadyAddedPoint && link.resegmentable) { // only change the number of points if Link.resegmentable
this._alreadyAddedPoint = true;
if (index === link.firstPickIndex) {
link.insertPoint(index, link.getPoint(index).copy());
index++;
this.handle.segmentIndex = index;
} else if (index === link.lastPickIndex - 1) {
link.insertPoint(index, link.getPoint(index).copy());
}
}
var behavior = this.getReshapingBehavior(this.handle);
if (behavior === go.LinkReshapingTool.Vertical) {
// move segment vertically
link.setPointAt(index, link.getPoint(index - 1).x, newpt.y);
link.setPointAt(index + 1, link.getPoint(index + 2).x, newpt.y);
} else if (behavior === go.LinkReshapingTool.Horizontal) {
// move segment horizontally
link.setPointAt(index, newpt.x, link.getPoint(index - 1).y);
link.setPointAt(index + 1, newpt.x, link.getPoint(index + 2).y);
}
link.commitRoute();
} else {
go.LinkReshapingTool.prototype.reshape.call(this, newpt);
}
};
/**
* Create the segment dragging handles.
* There are two parts: one invisible handle that spans the segment, and a visible handle at the middle of the segment.
* These are inserted at the front of the adornment such that the normal handles have priority.
* @param {Link} link
* @param {Adornment} adornment
* @param {number} index
* @this {OrthogonalLinkReshapingTool}
*/
OrthogonalLinkReshapingTool.prototype.makeSegmentDragHandle = function(link, adornment, index) {
var a = link.getPoint(index);
var b = link.getPoint(index + 1);
var seglength = Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
// determine segment orientation
var orient = "";
if (OrthogonalLinkReshapingTool.isApprox(a.x, b.x) && OrthogonalLinkReshapingTool.isApprox(a.y, b.y)) {
b = link.getPoint(index - 1);
if (OrthogonalLinkReshapingTool.isApprox(a.x, b.x)) {
orient = "vertical";
} else if (OrthogonalLinkReshapingTool.isApprox(a.y, b.y)) {
orient = "horizontal";
}
} else {
if (OrthogonalLinkReshapingTool.isApprox(a.x, b.x)) {
orient = "vertical";
} else if (OrthogonalLinkReshapingTool.isApprox(a.y, b.y)) {
orient = "horizontal";
}
}
// first, make an invisible handle along the whole segment
var h = new go.Shape();
h.strokeWidth = 6;
h.opacity = 0.0;
h.segmentOrientation = go.Link.OrientAlong;
h.segmentIndex = index;
h.segmentFraction = 0.5;
h.toMaxLinks = 999; // set this unsused property to easily identify that we have a segment dragging handle
if (orient === "horizontal") {
this.setReshapingBehavior(h, go.LinkReshapingTool.Vertical);
h.cursor = 'n-resize';
} else {
this.setReshapingBehavior(h, go.LinkReshapingTool.Horizontal);
h.cursor = 'w-resize';
}
h.geometryString = "M 0 0 L " + seglength + " 0";
adornment.insertAt(0, h);
};
/**
* Compare two numbers to ensure they are almost equal.
* Used in this class for comparing coordinates of Points.
* @param {number} x
* @param {number} y
* @return {boolean}
*/
OrthogonalLinkReshapingTool.isApprox = function(x, y) {
var d = x - y;
return d < 0.5 && d > -0.5;
}
+165
View File
@@ -0,0 +1,165 @@
<!DOCTYPE html>
<html>
<head>
<title>Overview Resizing</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="The OverviewResizingTool extension allows the user to change the viewport of the observed Diagram by resizing the box representing the viewport in an Overview." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="OverviewResizingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, 'myDiagramDiv', // create a Diagram for the DIV HTML element
{
layout: $(go.ForceDirectedLayout),
'undoManager.isEnabled': true // enable undo & redo
});
// Define the Node template.
// This uses a Spot Panel to position a button relative
// to the ellipse surrounding the text.
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{
selectionObjectName: "PANEL",
isTreeExpanded: false,
isTreeLeaf: false
},
// the node's outer shape, which will surround the text
$(go.Panel, "Auto",
{ name: "PANEL" },
$(go.Shape, "Circle",
{ fill: "#03A9F4", stroke: "black" }
),
$(go.TextBlock,
{ font: "12pt sans-serif", margin: 5 },
new go.Binding("text", "key"))
),
// the expand/collapse button, at the top-right corner
$("TreeExpanderButton",
{
name: 'TREEBUTTON',
width: 20, height: 20,
alignment: go.Spot.TopRight,
alignmentFocus: go.Spot.Center,
// customize the expander behavior to
// create children if the node has never been expanded
click: function(e, obj) { // OBJ is the Button
var node = obj.part; // get the Node containing this Button
if (node === null) return;
e.handled = true;
expandNode(node);
}
}
) // end TreeExpanderButton
); // end Node
// create the model with a root node data
myDiagram.model = new go.TreeModel([
{ key: 0, everExpanded: false }
]);
// Overview
myOverview =
$(go.Overview, 'myOverviewDiv', // the HTML DIV element for the Overview
{
observed: myDiagram,
contentAlignment: go.Spot.Center,
'box.resizable': true,
'resizingTool': new OverviewResizingTool()
});
document.getElementById('zoomToFit').addEventListener('click', function() {
myDiagram.zoomToFit();
});
document.getElementById('expandAtRandom').addEventListener('click', function() {
expandAtRandom();
});
}
function expandNode(node) {
var diagram = node.diagram;
diagram.startTransaction("CollapseExpandTree");
// this behavior is specific to this incrementalTree sample:
var data = node.data;
if (!data.everExpanded) {
// only create children once per node
diagram.model.setDataProperty(data, "everExpanded", true);
var numchildren = createSubTree(data);
if (numchildren === 0) { // now known no children: don't need Button!
node.findObject('TREEBUTTON').visible = false;
}
}
// this behavior is generic for most expand/collapse tree buttons:
if (node.isTreeExpanded) {
diagram.commandHandler.collapseTree(node);
} else {
diagram.commandHandler.expandTree(node);
}
diagram.commitTransaction("CollapseExpandTree");
}
// This dynamically creates the immediate children for a node.
// The sample assumes that we have no idea of whether there are any children
// for a node until we look for them the first time, which happens
// upon the first tree-expand of a node.
function createSubTree(parentdata) {
var numchildren = Math.floor(Math.random() * 10);
if (myDiagram.nodes.count <= 1) {
numchildren += 1; // make sure the root node has at least one child
}
// create several node data objects and add them to the model
var model = myDiagram.model;
var parent = myDiagram.findNodeForData(parentdata);
var degrees = 1;
var grandparent = parent.findTreeParentNode();
while (grandparent) {
degrees++;
grandparent = grandparent.findTreeParentNode();
}
for (var i = 0; i < numchildren; i++) {
var childdata = {
key: model.nodeDataArray.length,
parent: parentdata.key,
rootdistance: degrees
};
// add to model.nodeDataArray and create a Node
model.addNodeData(childdata);
// position the new child node close to the parent
var child = myDiagram.findNodeForData(childdata);
child.location = parent.location;
}
return numchildren;
}
function expandAtRandom() {
var eligibleNodes = [];
myDiagram.nodes.each(function(n) {
if (!n.isTreeExpanded) eligibleNodes.push(n);
})
var node = eligibleNodes[Math.floor(Math.random() * (eligibleNodes.length))];
expandNode(node);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<div id="myOverviewDiv" style="border: solid 1px black; width: 250px; height: 200px"></div>
<p><button id="zoomToFit">Zoom to Fit</button><button id="expandAtRandom">Expand random Node</button></p>
<p>
This sample demonstrates a custom <a>ResizingTool</a> which allows the user to resize the overview box.
It is defined in its own file, as <a href="OverviewResizingTool.ts">OverviewResizingTool.ts</a>.
</p>
</div>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/**
* The OverviewResizingTool class lets the user resize the box within an overview.
*
* If you want to experiment with this extension, try the <a href="../../extensionsTS/OverviewResizing.html">Overview Resizing</a> sample.
* @constructor
* @extends ResizingTool
* @class
*/
function OverviewResizingTool() {
go.ResizingTool.call(this);
this.name = 'OverviewResizing';
this._handleSize = new go.Size(6, 6);
}
go.Diagram.inherit(OverviewResizingTool, go.ResizingTool);
/**
* @this {OverviewResizingTool}
* @param {Shape} resizeBox the overview box which may be resized
* @return {Adornment}
*/
OverviewResizingTool.prototype.makeAdornment = function(resizeBox) {
this._handleSize.setTo(resizeBox.strokeWidth * 3, resizeBox.strokeWidth * 3);
// Set up the resize adornment
var ad = new go.Adornment();
ad.type = go.Panel.Spot;
ad.locationSpot = go.Spot.Center;
var ph = new go.Placeholder();
ph.isPanelMain = true;
ad.add(ph);
var hnd = new go.Shape();
hnd.name = 'RSZHND';
hnd.figure = 'Rectangle';
hnd.desiredSize = this._handleSize;
hnd.cursor = 'se-resize';
hnd.alignment = go.Spot.BottomRight;
hnd.alignmentFocus = go.Spot.Center;
ad.add(hnd);
ad.adornedObject = resizeBox;
return ad;
};
/**
* @hidden @internal
* Keep the resize handle properly sized as the scale is changing.
* This overrides an undocumented method on the ResizingTool.
* @this {OverviewResizingTool}
* @param {GraphObject} elt
* @param {number} angle
*/
OverviewResizingTool.prototype.updateResizeHandles = function(elt, angle) {
if (elt === null) return;
var handle = elt.findObject('RSZHND');
var box = elt.adornedObject;
this._handleSize.setTo(box.strokeWidth * 3, box.strokeWidth * 3);
handle.desiredSize = this._handleSize;
}
/**
* Overrides {@link ResizingTool#resize} to resize the overview box via setting the observed diagram's scale.
* @this {OverviewResizingTool}
* @param {Rect} newr the intended new rectangular bounds the overview box.
*/
OverviewResizingTool.prototype.resize = function(newr) {
var overview = this.diagram;
var observed = overview.observed;
if (observed === null)
return;
var oldr = observed.viewportBounds.copy();
var oldscale = observed.scale;
if (oldr.width !== newr.width || oldr.height !== newr.height) {
if (newr.width > 0 && newr.height > 0) {
observed.scale = Math.min(oldscale * oldr.width / newr.width, oldscale * oldr.height / newr.height);
}
}
};
+242
View File
@@ -0,0 +1,242 @@
<!DOCTYPE html>
<html>
<head>
<title>GoJS Packed Class Hierarchy</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="The JavaScript class hierarchy defined by the GoJS library, arranged in nested circles." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../samples/assets/require.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
require(["../extensionsTS/PackedLayout"], function(app) {
var PackedLayout = app.PackedLayout;
// subclass PackedLayout to change default properties and override commitLayout function
function HierarchyLayout() {
PackedLayout.call(this);
this.packShape = PackedLayout.Spiral;
this.hasCircularNodes = true;
this.sortMode = PackedLayout.Area,
this.comparer = function(na, nb) {
// ensure label is placed last
if (na.data.isLabel) {
return 1;
}
if (nb.data.isLabel) {
return -1;
}
// otherwise sort in ascending order by size (all nodes are circular, so using width or height here doesn't matter)
return (na.actualBounds.width - nb.actualBounds.width);
}
}
go.Diagram.inherit(HierarchyLayout, PackedLayout);
/* after each group has had its layout applied, size and position it according
* to the smallest enclosing circle which goes around all of its nodes */
HierarchyLayout.prototype.commitLayout = function() {
if (this.group !== null) {
var groupData = keyToNodeDataMap.get(this.group.key);
var enclosingCircle = this.enclosingCircle;
var actualBounds = this.actualBounds;
var size = new go.Size(enclosingCircle.width, enclosingCircle.width);
this.diagram.model.setDataProperty(groupData, "size", size);
var dx = enclosingCircle.centerX - actualBounds.centerX;
var dy = enclosingCircle.centerY - actualBounds.centerY;
var position = new go.Point((actualBounds.width - enclosingCircle.width) / 2 + dx, (actualBounds.height - enclosingCircle.height) / 2 + dy);
this.diagram.model.setDataProperty(groupData, "position", position);
}
}
var $ = go.GraphObject.make; // for conciseness in defining templates
var myDiagram =
$(go.Diagram, "myDiagramDiv", // must be the ID or reference to div
{
layout: $(HierarchyLayout), // defined above
"animationManager.isEnabled": false,
isReadOnly: true,
initialAutoScale: go.Diagram.Uniform
});
// common definitions for both Nodes and Groups
var toolTipTemplate =
$(go.Adornment, "Auto",
$(go.Shape, { fill: "white" }),
$(go.TextBlock, { margin: 4 },
new go.Binding("text", "tooltip"))
);
var selectionAdornmentTemplate =
$(go.Adornment, "Auto",
$(go.Shape, "Circle",
{ fill: null, stroke: "dodgerblue", strokeWidth: 3,
spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight }),
$(go.Placeholder)
);
function commonStyle() {
return [
{
toolTip: toolTipTemplate,
selectionAdornmentTemplate: selectionAdornmentTemplate,
doubleClick: function(e, node) {
var url = "../../api/symbols/" + node.data.key + ".html";
window.open(url, "_blank");
}
}
];
}
myDiagram.nodeTemplate =
$(go.Node, "Auto", commonStyle(),
new go.Binding("width", "diameter"),
new go.Binding("height", "diameter"),
$(go.Shape,
{ figure: "Circle", fill: "#1F4963", strokeWidth: 0, spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight },
new go.Binding("fill")),
$(go.TextBlock,
{ font: "12px Helvetica, Arial, sans-serif", stroke: "white", maxLines: 1 },
new go.Binding("text"),
new go.Binding("font"),
new go.Binding("stroke", "fill", function(f) { return go.Brush.isDark(f) ? "white" : "black"; }))
);
myDiagram.groupTemplate =
$(go.Group, commonStyle(),
{ layout: $(HierarchyLayout) }, // defined above
$(go.Shape, "Circle",
{ fill: "rgba(128,128,128,0.33)" },
new go.Binding("desiredSize", "size"),
new go.Binding("position")),
$(go.Placeholder) // represents area for all member parts
);
// Collect all of the data for the model of the class hierarchy
var nodeDataArray = [
{ key: "GoJS", text: "GoJS", children: [] },
// large label to be placed at the very end
{
key: "GoJS label",
group: "GoJS",
text: "GoJS",
tooltip: "GoJS",
fill: null,
font: "64px Helvetica, bold Arial, sans-serif",
isLabel: true,
children: []
}
];
var keyToNodeDataMap = new go.Map();
keyToNodeDataMap.add("GoJS", nodeDataArray[0]);
// Iterate over all of the classes in "go"
for (var k in go) {
var cls = go[k];
if (!cls) continue;
var proto = cls.prototype;
if (!proto) continue;
proto.constructor.className = k; // remember name
var propCount = 0; // count number of properties on the class
for (var prop in proto) {
if (proto.hasOwnProperty(prop)) {
propCount++;
}
}
var data = { key: k, text: k, propCount: propCount, children: [] };
if (keyToNodeDataMap.has(k)) {
data.children = keyToNodeDataMap.get(k).children;
}
keyToNodeDataMap.add(k, data); // will replace existing key if there is one
// find base class constructor
var base = Object.getPrototypeOf(proto).constructor;
if (base === Object) { // "root" node?
data.group = "GoJS";
keyToNodeDataMap.get("GoJS").children.push(data);
nodeDataArray.push(data);
} else {
// add a node for this class and set its group to its parent
data.group = base.className;
if (keyToNodeDataMap.has(base.className)) {
keyToNodeDataMap.get(base.className).children.push(data);
} else {
keyToNodeDataMap.add(base.className, {children: [data]});
}
nodeDataArray.push(data);
}
}
// create groups and add labels to groups with only 1 child
for (var i = nodeDataArray.length - 1; i >= 0; i--) {
var n = nodeDataArray[i];
if (n.children.length > 0) {
n.isGroup = true;
}
// add tooltip and/or size node using the total number of properties it has and has inherited
var totalCount = n.propCount;
var parentKey = n.group;
var parentData;
while ((parentData = keyToNodeDataMap.get(parentKey)) !== null && parentData.propCount !== undefined) {
totalCount += parentData.propCount;
parentKey = parentData.group;
}
if (totalCount === undefined) { // applies to the root GoJS group only
n.tooltip = n.text;
} else {
n.tooltip = n.text + ": " + totalCount; // add tooltip
}
if (!n.isGroup && !n.isLabel) { // only set size of node if it is not a group
// calculate size by scaling totalCount logarithmically to produce more visually appealing results
n.diameter = 20 * Math.log(0.5 * (totalCount + 5.5));
}
// add label to groups that only have one child
if (n.children.length === 1) {
nodeDataArray.push({
text: n.text,
tooltip: n.tooltip,
group: n.key,
fill: null,
font: "20px Helvetica, bold Arial, sans-serif"
});
}
delete n.children;
}
myDiagram.model = new go.GraphLinksModel(nodeDataArray);
});
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px"></div>
<p>
Circle packing can be a useful way to visualize hierarchical data, as demonstrated here
with a visualization of the class hierarchy of the GoJS library. This layout is performed
automatically by the <a href="../extensions/PackedLayout.html">PackedLayout</a> extension. Nodes
are sized according to how many properties their corresponding class has, or has inherited.
As a result, larger nodes generally represent more complex classes. Mouse over nodes to see
their full name and the number of properties on their corresponding class.
</p>
<p>
This sample is very similar to the <a href="../samples/classHierarchy.html" target="_blank">Class Hierarchy</a> sample,
except that instead of showing the class hierarchy as a tree, it is displayed using nested circles.
Opening the API page is achieved by double-clicking on a node, rather than using a "HyperlinkText".
</p>
</div>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<title>Packed Layout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Arrange nodes into rectangular or elliptical areas, ignoring any links." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../samples/assets/require.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<style type="text/css">
input[type="number"] {
width: 60px;
margin-left: 3px;
}
input[type=checkbox], input[type=radio] {
vertical-align: middle;
position: relative;
bottom: 2px;
}
</style>
<script id="code">
var rebuildGraph;
function init() {
require(["../extensionsTS/PackedScript"], function(app) {
rebuildGraph = app.rebuildGraph;
app.init();
document.getElementById("randomizeGraph").onclick = app.randomize;
});
}
function layout() {
rebuildGraph();
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div style="margin-bottom: 5px; padding: 5px; background-color: aliceblue">
<span style="display: inline-block; vertical-align: top; padding: 5px">
<b>General Properties</b><br />
PackShape:<br /> <input type="radio" name="packShape" onclick="layout()" value="Elliptical" checked> Elliptical<br />
<input type="radio" name="packShape" onclick="layout()" value="Rectangular"> Rectangular<br />
PackMode:<br /> <input type="radio" name="packMode" onclick="layout()" value="AspectOnly" checked> Aspect Ratio<br />
<input type="radio" name="packMode" onclick="layout()" value="ExpandToFit"> Expand to Fit<br />
<input type="radio" name="packMode" onclick="layout()" value="Fit"> Fit<br />
<table>
<tr>
<td>Aspect ratio: </td>
<td><input type="number" size="2" id="aspectRatio" value="1" onchange="layout()"></td>
</tr>
<tr>
<td>Layout width: </td>
<td><input type="number" size="2" id="width" value="600" onchange="layout()"></td>
</tr>
<tr>
<td>Layout height: </td>
<td><input type="number" size="2" id="height" value="600" onchange="layout()"></td>
</table>
</span>
<span style="display: inline-block; vertical-align: top; padding: 5px">
<b>Node Sorting Properties</b><br />
SortOrder: <input type="radio" name="sortOrder" onclick="layout()" value="Descending" checked> Descending
<input type="radio" name="sortOrder" onclick="layout()" value="Ascending"> Ascending<br />
SortMode: <br />
<input type="radio" name="sortMode" onclick="layout()" id="modeNone" value="None"> None (do not sort nodes)<br />
<input type="radio" name="sortMode" onclick="layout()" id="modeMaxSide" value="MaxSide"> Max Side Length<br />
<input type="radio" name="sortMode" onclick="layout()" id="modeArea" value="Area" checked> Area<br />
<b>Padding between nodes</b><br />
Spacing: <input type="number" id="nodeSpacing" value="0" onchange="layout()"><br />
<b>Circle Packing</b><br />
hasCircularNodes <input type="checkbox" id="hasCircularNodes" onclick="layout()"><br />
isSpiralPacked <input type="checkbox" id="isSpiralPacked" onclick="layout()">
</span>
<span style="display: inline-block; vertical-align: top; padding: 5px">
<b>Node Generation</b><br />
<table>
<tr>
<td>Number of nodes: </td>
<td><input type="number" id="numNodes" value="100" onchange="layout()"><br /></td>
</tr>
<tr>
<td>Node shape:<br /><input type="radio" name="shapeToPack" onclick="layout()" value="Rectangle" checked> Rectangles<br />
<input type="radio" name="shapeToPack" onclick="layout()" value="Ellipse"> Ellipses<br />
</tr>
<tr>
<td>Minimum side length: </td>
<td><input type="number" id="nodeMinSide" value="30" onchange="layout()"><br /></td>
</tr>
<tr>
<td>Maximum side length: </td>
<td><input type="number" id="nodeMaxSide" value="50" onchange="layout()"><br /></td>
</tr>
</table>
Same width/height <input type="checkbox" id="sameSides" onclick="layout()"><br />
<button type="button" id="randomizeGraph" style="margin-top: 5px;">Randomize Graph</button>
</span>
</div>
<div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 500px"></div>
<p>
This sample demonstrates a custom Layout, PackedLayout, which attempts to pack nodes as close together as possible without overlap.
Each node is assumed to be either rectangular or circular (dictated by the 'nodesAreCircles' property). This layout supports packing
nodes into either a rectangle or an ellipse, with the shape determined by the PackShape and the aspect ratio determined by either the
aspectRatio property, or the specified width and height (depending on the PackMode).
</p>
<p>
This extension's code is TypeScript-only and the source files can be found in the <code>extensionsTS</code> directory.
The layout is defined in its own file, as <a href="../extensionsTS/PackedLayout.ts">extensionsTS/PackedLayout.ts</a>, with an additional dependency on <a href="../extensionsTS/Quadtree.ts">extensionsTS/Quadtree.ts</a>.
</p>
</div>
</body>
</html>
+161
View File
@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html>
<head>
<title>Parallel Layout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A custom Layout that arranges a collection of nodes and links where there is a single 'split' node and a single 'merge' node, and all nodes are in paths of links that come from the 'split' node and go to the 'merge' node." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="ParallelLayout.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // must be the ID or reference to div
{
allowCopy: false, // would need to merge copied nodes and links to
allowDelete: false, // use the single "Split" and "Merge" nodes
layout: $(ParallelLayout, { layerSpacing: 20, nodeSpacing: 10 })
});
// define the Node templates
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{ locationSpot: go.Spot.Center },
$(go.Shape, "Rectangle",
{ fill: "wheat", stroke: null, strokeWidth: 0 }),
$(go.TextBlock, { margin: 3 },
new go.Binding("text"))
);
myDiagram.nodeTemplateMap.add("Split",
$(go.Node, "Auto",
{ locationSpot: go.Spot.Center },
$(go.Shape, "Diamond",
{
fill: "deepskyblue", stroke: null, strokeWidth: 0,
desiredSize: new go.Size(28, 28)
}),
$(go.TextBlock,
new go.Binding("text"))
));
myDiagram.nodeTemplateMap.add("Merge",
$(go.Node, "Auto",
{ locationSpot: go.Spot.Center },
$(go.Shape, "Circle",
{
fill: "deepskyblue", stroke: null, strokeWidth: 0,
desiredSize: new go.Size(28, 28)
}),
$(go.TextBlock,
new go.Binding("text"))
));
// define the Link template to be minimal
myDiagram.linkTemplate =
$(go.Link,
{ routing: go.Link.Orthogonal, corner: 5 },
$(go.Shape,
{ stroke: "gray", strokeWidth: 1.5 })
);
// define the Group template to be fairly simple
myDiagram.groupTemplate =
$(go.Group, "Auto",
{
layout: $(ParallelLayout, { layerSpacing: 20, nodeSpacing: 10 })
},
$(go.Shape, { fill: "transparent", stroke: "darkgoldenrod" }),
$(go.Placeholder, { padding: 10 }),
$("SubGraphExpanderButton", { alignment: go.Spot.TopLeft, "ButtonBorder.figure": "Rectangle" })
);
var model = $(go.GraphLinksModel);
model.nodeDataArray = [
{ key: -1, isGroup: true },
{ key: -2, isGroup: true },
{ key: -3, isGroup: true },
{ key: 1, text: "S", category: "Split", group: -1 },
{ key: 2, text: "C", group: -1 },
{ key: 3, text: "Longer Node", group: -1 },
{ key: 4, text: "A", group: -1 },
{ key: 5, text: "B\nB", group: -1 },
{ key: 6, text: "Another", group: -1 },
{ key: 9, text: "J", category: "Merge", group: -1 },
{ key: 11, text: "T", category: "Split", group: -2 },
{ key: 12, text: "C", group: -2 },
{ key: 13, text: "Here", group: -2 },
{ key: 14, text: "D", group: -2 },
{ key: 15, text: "Everywhere", group: -2 },
{ key: 16, text: "EEEEE", group: -2 },
{ key: 19, text: "K", category: "Merge", group: -2 },
{ key: 21, text: "U", category: "Split", group: -3 },
{ key: 22, text: "F", group: -3 },
{ key: 23, text: "Medium\nTall\nNode", group: -3 },
{ key: 24, text: "G", group: -3 },
{ key: 25, text: "AS", group: -3 },
{ key: 26, text: "H\nHH\nHHH", group: -3 },
{ key: 27, text: "I", group: -3 },
{ key: 29, text: "L", category: "Merge", group: -3 },
{ key: 101, text: "0", category: "Split" },
{ key: 107, text: "ABCDEFG" },
{ key: 109, text: "*", category: "Merge" }
];
model.linkDataArray = [
{ from: 1, to: 2 },
{ from: 2, to: 3 },
{ from: 3, to: 4 },
{ from: 4, to: 9 },
{ from: 1, to: 5 },
{ from: 5, to: 6 },
{ from: 6, to: 9 },
{ from: 9, to: 11 },
{ from: 9, to: 21 },
{ from: 11, to: 12 },
{ from: 12, to: 13 },
{ from: 13, to: 14 },
{ from: 14, to: 19 },
{ from: 11, to: 15 },
{ from: 15, to: 16 },
{ from: 16, to: 19 },
{ from: 21, to: 22 },
{ from: 22, to: 24 },
{ from: 24, to: 26 },
{ from: 23, to: 29 },
{ from: 21, to: 25 },
{ from: 25, to: 23 },
{ from: 21, to: 27 },
{ from: 26, to: 29 },
{ from: 27, to: 29 },
{ from: 101, to: 1 },
{ from: 19, to: 109 },
{ from: 29, to: 107 },
{ from: 107, to: 109 }
];
myDiagram.model = model;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; background: white; width: 100%; height: 500px"></div>
<p>
This sample demonstrates a custom <a>TreeLayout</a>, ParallelLayout,
which assumes that there is a single "Split" node that is the root of a tree,
other than links that connect with a single "Merge" node.
The layout is defined in its own file, as <a href="ParallelLayout.js">ParallelLayout.js</a>.
</p>
<p>
Both the <a>Diagram.layout</a> and the <a>Group.layout</a> are instances of ParallelLayout,
allowing for nested layouts that appear in parallel.
</p>
</div>
</body>
</html>
+281
View File
@@ -0,0 +1,281 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom {@link TreeLayout} that can be used for laying out stylized flowcharts.
// Each layout requires a single "Split" node and a single "Merge" node.
// The "Split" node should be the root of a tree-like structure if one excludes links to the "Merge" node.
// This will position the "Merge" node to line up with the "Split" node.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends TreeLayout
* @class
* You can set all of the TreeLayout properties that you like,
* except that for simplicity this code just works for angle === 0 or angle === 90.
*/
function ParallelLayout() {
go.TreeLayout.call(this);
this._splitNode = null;
this._mergeNode = null;
// these are desired for the Parallel Layout:
this.isRealtime = false;
this.alignment = go.TreeLayout.AlignmentCenterChildren;
this.compaction = go.TreeLayout.CompactionNone;
this.alternateAlignment = go.TreeLayout.AlignmentCenterChildren;
this.alternateCompaction = go.TreeLayout.CompactionNone;
}
go.Diagram.inherit(ParallelLayout, go.TreeLayout);
/**
* Overridable predicate for deciding if a Node is a Split node.
* By default this checks the node's {@link Part#category} to see if it is
* "Split", "Start", "For", "While", "If", or "Switch".
* @param {Node} node
* @return {boolean}
*/
ParallelLayout.prototype.isSplit = function(node) {
if (!(node instanceof go.Node)) return false;
var cat = node.category;
return (cat === "Split" || cat === "Start" || cat === "For" || cat === "While" || cat === "If" || cat === "Switch");
}
/**
* Overridable predicate for deciding if a Node is a Merge node.
* By default this checks the node's {@link Part#category} to see if it is
* "Merge", "End", "EndFor", "EndWhile", "EndIf", or "EndSwitch".
* @param {Node} node
* @return {boolean}
*/
ParallelLayout.prototype.isMerge = function(node) {
if (!(node instanceof go.Node)) return false;
var cat = node.category;
return (cat === "Merge" || cat === "End" || cat === "EndFor" || cat === "EndWhile" || cat === "EndIf" || cat === "EndSwitch");
}
/**
* Overridable predicate for deciding if a Node is a conditional or "If" type of Split Node
* expecting to have two links coming out of the sides.
* @param {Node} node
* @return {boolean}
*/
ParallelLayout.prototype.isConditional = function(node) {
if (!(node instanceof go.Node)) return false;
return node.category === "If";
}
/**
* Overridable predicate for deciding if a Node is a "Switch" type of Split Node
* expecting to have three links coming out of the bottom/right side.
* @param {Node} node
* @return {boolean}
*/
ParallelLayout.prototype.isSwitch = function(node) {
if (!(node instanceof go.Node)) return false;
return node.category === "Switch";
}
/**
* Return an Array holding the Split node and the Merge node for this layout.
* This signals an error if there is not exactly one Node that {@link #isSplit}
* and exactly one Node that {@link #isMerge}.
* This can be overridden; any override must set {@link #splitNode} and {@link #mergeNode}.
* @param {*} vertexes
*/
ParallelLayout.prototype.findSplitMerge = function(vertexes) {
var split = null;
var merge = null;
var it = vertexes.iterator;
while (it.next()) {
var v = it.value;
if (!v.node) continue;
if (this.isSplit(v.node)) {
if (split) throw new Error("Split node already exists in " + this + " -- existing: " + split + " new: " + v.node);
split = v.node;
} else if (this.isMerge(v.node)) {
if (merge) throw new Error("Merge node already exists in " + this + " -- existing: " + merge + " new: " + v.node);
merge = v.node;
}
}
if (!split) throw new Error("Missing Split node in " + this);
if (!merge) throw new Error("Missing Merge node in " + this);
this._splitNode = split;
this._mergeNode = merge;
}
/**
* @hidden @internal
* @param {*} coll
*/
ParallelLayout.prototype.makeNetwork = function(coll) {
var net = go.TreeLayout.prototype.makeNetwork.call(this, coll);
// Groups might be unbalanced -- position them so that the Split node is centered under the parent node.
var it = net.vertexes.iterator;
while (it.next()) {
var v = it.value;
var g = v.node;
if (g instanceof go.Group && g.isSubGraphExpanded && g.placeholder !== null && g.layout instanceof ParallelLayout) {
var split = g.layout.splitNode;
if (split) {
if (this.angle === 0) {
v.focusY = split.location.y - g.position.y;
} else if (this.angle === 90) {
v.focusX = split.location.x - g.position.x;
}
}
}
}
if (this.group && !this.group.isSubGraphExpanded) return net;
// look for and remember the one Split node and the one Merge node
this.findSplitMerge(net.vertexes);
// don't have TreeLayout lay out the Merge node; commitNodes will do it
if (this.mergeNode) net.deleteNode(this.mergeNode);
return net;
};
/**
* @hidden @internal
*/
ParallelLayout.prototype.commitNodes = function() {
go.TreeLayout.prototype.commitNodes.call(this);
// Line up the Merge node to the center of the Split node
var mergeNode = this.mergeNode;
var splitNode = this.splitNode;
if (mergeNode === null || splitNode === null || this.network === null) return;
var splitVertex = this.network.findVertex(splitNode);
if (splitVertex === null) return;
if (this.angle === 0) {
mergeNode.location = new go.Point(splitVertex.x + splitVertex.subtreeSize.width + this.layerSpacing + mergeNode.actualBounds.width/2,
splitVertex.centerY);
} else if (this.angle === 90) {
mergeNode.location = new go.Point(splitVertex.centerX,
splitVertex.y + splitVertex.subtreeSize.height + this.layerSpacing + mergeNode.actualBounds.height/2);
}
mergeNode.ensureBounds();
};
/**
* @hidden @internal
*/
ParallelLayout.prototype.commitLinks = function() {
var splitNode = this.splitNode;
var mergeNode = this.mergeNode;
if (splitNode === null || mergeNode === null || this.network === null) return;
// set default link spots based on this.angle
var it = this.network.edges.iterator;
while (it.next()) {
var e = it.value;
var link = e.link;
if (!link) continue;
if (this.angle === 0) {
if (this.setsPortSpot) link.fromSpot = go.Spot.Right;
if (this.setsChildPortSpot) link.toSpot = go.Spot.Left;
} else if (this.angle === 90) {
if (this.setsPortSpot) link.fromSpot = go.Spot.Bottom;
if (this.setsChildPortSpot) link.toSpot = go.Spot.Top;
}
}
// Make sure links coming into and going out of a Split node come in the correct way
if (splitNode) {
// Handle links coming into the Split node
var cond = this.isConditional(splitNode);
var swtch = this.isSwitch(splitNode);
// Handle links going out of the Split node
var first = true; // handle "If" nodes specially
var lit = splitNode.findLinksOutOf();
while (lit.next()) {
var link = lit.value;
if (this.angle === 0) {
if (this.setsPortSpot) link.fromSpot = cond ? (first ? go.Spot.Top : go.Spot.Bottom) : (swtch ? go.Spot.RightSide : go.Spot.Right);
if (this.setsChildPortSpot) link.toSpot = go.Spot.Left;
} else if (this.angle === 90) {
if (this.setsPortSpot) link.fromSpot = cond ? (first ? go.Spot.Left : go.Spot.Right) : (swtch ? go.Spot.BottomSide : go.Spot.Bottom);
if (this.setsChildPortSpot) link.toSpot = go.Spot.Top;
}
first = false;
}
}
if (mergeNode) {
// Handle links going into the Merge node
var it = mergeNode.findLinksInto();
while (it.next()) {
var link = it.value;
if (!this.isSplit(link.fromNode)) { // if link connects Split with Merge directly, only set fromSpot once
if (this.angle === 0) {
if (this.setsPortSpot) link.fromSpot = go.Spot.Right;
if (this.setsChildPortSpot) link.toSpot = go.Spot.Left;
} else if (this.angle === 90) {
if (this.setsPortSpot) link.fromSpot = go.Spot.Bottom;
if (this.setsChildPortSpot) link.toSpot = go.Spot.Top;
}
}
if (!link.isOrthogonal) continue;
// have all of the links coming into the Merge node have segments
// that share a common X (or if angle==90, Y) coordinate
link.updateRoute();
if (link.pointsCount >= 6) {
var pts = link.points.copy();
var p2 = pts.elt(pts.length - 4);
var p3 = pts.elt(pts.length - 3);
if (this.angle === 0 && p2.x === p3.x) {
var x = mergeNode.position.x - this.layerSpacing / 2;
pts.setElt(pts.length - 4, new go.Point(x, p2.y));
pts.setElt(pts.length - 3, new go.Point(x, p3.y));
} else if (this.angle === 90 && p2.y === p3.y) {
var y = mergeNode.position.y - this.layerSpacing / 2;
pts.setElt(pts.length - 4, new go.Point(p2.x, y));
pts.setElt(pts.length - 3, new go.Point(p3.x, y));
}
link.points = pts;
}
}
// handle links coming out of the Merge node, looping back left/up
var it = mergeNode.findLinksOutOf();
while (it.next()) {
var link = it.value;
// if connects internal with external node, it isn't a loop-back link
if (link.toNode.containingGroup !== mergeNode.containingGroup) continue;
if (this.angle === 0) {
if (this.setsPortSpot) link.fromSpot = go.Spot.TopBottomSides;
if (this.setsChildPortSpot) link.toSpot = go.Spot.TopBottomSides;
} else if (this.angle === 90) {
if (this.setsPortSpot) link.fromSpot = go.Spot.LeftRightSides;
if (this.setsChildPortSpot) link.toSpot = go.Spot.LeftRightSides;
}
link.routing = go.Link.AvoidsNodes;
}
}
};
/**
* This read-only property returns the Split Node of the Diagram or Group.
* The value is only available once the layout has been performed.
* @name ParallelLayout#splitNode
* @return {Node}
*/
Object.defineProperty(ParallelLayout.prototype, "splitNode", {
get: function() { return this._splitNode; },
set: function(n) { this._splitNode = n; }
});
/**
* This read-only property returns the Merge Node of the Diagram or Group.
* The value is only available once the layout has been performed.
* @name ParallelLayout#splitNode
* @return {Node}
*/
Object.defineProperty(ParallelLayout.prototype, "mergeNode", {
get: function() { return this._mergeNode; },
set: function(n) { this._mergeNode = n; }
})
+80
View File
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
<title>Parallel Route Links</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Custom non-Orthogonal non-Bezier Links that have parallel routings for multiple links connecting the same pair of ports." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="ParallelRouteLink.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"undoManager.isEnabled": true
});
myDiagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "loc", go.Point.parse),
$(go.Shape,
{
portId: "",
fromLinkable: true, toLinkable: true,
fromLinkableDuplicates: true, toLinkableDuplicates: true,
cursor: "pointer"
},
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 },
new go.Binding("text"))
);
myDiagram.linkTemplate =
$(ParallelRouteLink,
{
relinkableFrom: true, relinkableTo: true,
reshapable: true //, resegmentable: true
},
$(go.Shape, { strokeWidth: 2 },
new go.Binding("stroke", "fromNode", function(node) { return node.port.fill; }).ofObject()),
$(go.Shape, { toArrow: "OpenTriangle", strokeWidth: 1.5 },
new go.Binding("stroke", "fromNode", function(node) { return node.port.fill; }).ofObject())
);
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, text: "Alpha", color: "lightblue", loc: "0 0" },
{ key: 2, text: "Beta", color: "orange", loc: "130 70" },
{ key: 3, text: "Gamma", color: "lightgreen", loc: "0 130" }
],
[
{ from: 1, to: 2 },
{ from: 2, to: 1 },
{ from: 1, to: 3 },
{ from: 1, to: 3 },
{ from: 3, to: 1 },
{ from: 1, to: 3 },
{ from: 1, to: 3 }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<p>
A <b>ParallelRouteLink</b> is a custom <a>Link</a> that overrides <a>Link.computePoints</a>
in order to produce a middle segment that is parallel to the routes of other <b>ParallelRouteLink</b>s
connecting the same two ports.
</p>
</div>
</body>
</html>
+83
View File
@@ -0,0 +1,83 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Link whose routing is parallel to other links connecting the same pair of ports
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Link
* @class
* This custom Link class customizes its route to go parallel to other links connecting the same ports,
* if the link is not orthogonal and is not Bezier curved.
*/
function ParallelRouteLink() {
go.Link.call(this);
}
go.Diagram.inherit(ParallelRouteLink, go.Link);
/**
* @this {ParallelRouteLink}
* @return {boolean}
*/
ParallelRouteLink.prototype.computePoints = function() {
var result = go.Link.prototype.computePoints.call(this);
if (!this.isOrthogonal && this.curve !== go.Link.Bezier && this.hasCurviness()) {
var curv = this.computeCurviness();
if (curv !== 0) {
var num = this.pointsCount;
var pidx = 0;
var qidx = num-1;
if (num >= 4) {
pidx++;
qidx--;
}
var frompt = this.getPoint(pidx);
var topt = this.getPoint(qidx);
var dx = topt.x - frompt.x;
var dy = topt.y - frompt.y;
var mx = frompt.x + dx * 1 / 8;
var my = frompt.y + dy * 1 / 8;
var px = mx;
var py = my;
if (-0.01 < dy && dy < 0.01) {
if (dx > 0) py -= curv; else py += curv;
} else {
var slope = -dx / dy;
var e = Math.sqrt(curv * curv / (slope * slope + 1));
if (curv < 0) e = -e;
px = (dy < 0 ? -1 : 1) * e + mx;
py = slope * (px - mx) + my;
}
mx = frompt.x + dx * 7 / 8;
my = frompt.y + dy * 7 / 8;
var qx = mx;
var qy = my;
if (-0.01 < dy && dy < 0.01) {
if (dx > 0) qy -= curv; else qy += curv;
} else {
var slope = -dx / dy;
var e = Math.sqrt(curv * curv / (slope * slope + 1));
if (curv < 0) e = -e;
qx = (dy < 0 ? -1 : 1) * e + mx;
qy = slope * (qx - mx) + my;
}
this.insertPointAt(pidx+1, px, py);
this.insertPointAt(qidx+1, qx, qy);
}
}
return result;
};
+157
View File
@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html>
<head>
<title>Polygon Drawing Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="The user can draw a new polygon by clicking where its points should go." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="PolygonDrawingTool.js"></script>
<script src="GeometryReshapingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv");
myDiagram.toolManager.mouseDownTools.insertAt(3, new GeometryReshapingTool());
myDiagram.nodeTemplateMap.add("PolygonDrawing",
$(go.Node,
{ locationSpot: go.Spot.Center }, // to support rotation about the center
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{
selectionAdorned: true, selectionObjectName: "SHAPE",
selectionAdornmentTemplate: // custom selection adornment: a blue rectangle
$(go.Adornment, "Auto",
$(go.Shape, { stroke: "dodgerblue", fill: null }),
$(go.Placeholder, { margin: -1 }))
},
{ resizable: true, resizeObjectName: "SHAPE" },
{ rotatable: true, rotateObjectName: "SHAPE" },
{ reshapable: true }, // GeometryReshapingTool assumes nonexistent Part.reshapeObjectName would be "SHAPE"
$(go.Shape,
{ name: "SHAPE", fill: "lightgray", strokeWidth: 1.5 },
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
new go.Binding("angle").makeTwoWay(),
new go.Binding("geometryString", "geo").makeTwoWay(),
new go.Binding("fill"),
new go.Binding("stroke"),
new go.Binding("strokeWidth"))
));
// create polygon drawing tool for myDiagram, defined in PolygonDrawingTool.js
var tool = new PolygonDrawingTool();
// provide the default JavaScript object for a new polygon in the model
tool.archetypePartData =
{ fill: "yellow", stroke: "blue", strokeWidth: 3, category: "PolygonDrawing" };
tool.isPolygon = true; // for a polyline drawing tool set this property to false
// install as first mouse-down-tool
myDiagram.toolManager.mouseDownTools.insertAt(0, tool);
load(); // load a simple diagram from the textarea
}
function mode(draw, polygon) {
// assume PolygonDrawingTool is the first tool in the mouse-down-tools list
var tool = myDiagram.toolManager.mouseDownTools.elt(0);
tool.isEnabled = draw;
tool.isPolygon = polygon;
tool.archetypePartData.fill = (polygon ? "yellow" : null);
tool.temporaryShape.fill = (polygon ? "yellow" : null);
}
// this command ends the PolygonDrawingTool
function finish(commit) {
var tool = myDiagram.currentTool;
if (commit && tool instanceof PolygonDrawingTool) {
var lastInput = myDiagram.lastInput;
if (lastInput.event instanceof window.MouseEvent) tool.removeLastPoint(); // remove point from last mouse-down
tool.finishShape();
} else {
tool.doCancel();
}
}
// this command removes the last clicked point from the temporary Shape
function undo() {
var tool = myDiagram.currentTool;
if (tool instanceof PolygonDrawingTool) {
var lastInput = myDiagram.lastInput;
if (lastInput.event instanceof window.MouseEvent) tool.removeLastPoint(); // remove point from last mouse-down
tool.undo();
}
}
function updateAllAdornments() { // called after checkboxes change Diagram.allow...
myDiagram.selection.each(function(p) { p.updateAdornments(); });
}
// save a model to and load a model from Json text, displayed below the Diagram
function save() {
var str = '{ "position": "' + go.Point.stringify(myDiagram.position) + '",\n "model": ' + myDiagram.model.toJson() + ' }';
document.getElementById("mySavedDiagram").value = str;
}
function load() {
var str = document.getElementById("mySavedDiagram").value;
try {
var json = JSON.parse(str);
myDiagram.initialPosition = go.Point.parse(json.position || "0 0");
myDiagram.model = go.Model.fromJson(json.model);
myDiagram.model.undoManager.isEnabled = true;
} catch (ex) {
alert(ex);
}
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 350px"></div>
<div id="buttons">
<button onclick="mode(false)">Select</button>
<button onclick="mode(true, true)">Draw Polygon</button>
<button onclick="mode(true, false)">Draw Polyline</button>
<button onclick="finish(true)">Finish Drawing</button>
<button onclick="finish(false)">Cancel Drawing</button>
<button onclick="undo()">Undo Last Point</button>
<br/>
<label><input type="checkbox" onclick="myDiagram.allowResize = !myDiagram.allowResize; updateAllAdornments()" checked="checked" />Allow Resizing</label>
<label><input type="checkbox" onclick="myDiagram.allowReshape = !myDiagram.allowReshape; updateAllAdornments()" checked="checked" />Allow Reshaping</label>
<label><input type="checkbox" onclick="myDiagram.allowRotate = !myDiagram.allowRotate; updateAllAdornments()" checked="checked" />Allow Rotating</label>
</div>
<p>
This sample demonstrates the PolygonDrawingTool, a custom <a>Tool</a> added to the Diagram's mouseDownTools.
It is defined in its own file, as <a href="PolygonDrawingTool.js">PolygonDrawingTool.js</a>.
It also demonstrates the GeometryReshapingTool, another custom tool,
defined in <a href="GeometryReshapingTool.js">GeometryReshapingTool.js</a>.
</p>
<p>
These extensions serve as examples of features that can be added to GoJS by writing new classes.
With the PolygonDrawingTool, a new mode is supported that allows the user to draw custom shapes.
With the GeometryReshapingTool, users can change the geometry (i.e. the "shape") of a <a>Shape</a>s in a selected <a>Node</a>.
<br/>
Click a "Draw" button and then click in the diagram to place a new point in a polygon or polyline shape.
Right-click, double-click, or Enter to finish. Press <b>Escape</b> to cancel, or <b>Z</b> to remove the last point.
Click the "Select" button to switch back to the normal selection behavior, so that you can select, resize, and rotate the shapes.
The checkboxes control whether you can resize, reshape, and/or rotate selected shapes.
</p>
<div>
<button onclick="save()">Save</button>
<button onclick="load()">Load</button>
</div>
<textarea id="mySavedDiagram" style="width:100%;height:300px">
{ "position": "0 0",
"model": { "class": "go.GraphLinksModel",
"nodeDataArray": [ {"loc":"183 148", "category": "PolygonDrawing", "geo":"F M0 145 L75 2 L131 87 L195 0 L249 143z", "key":-1} ],
"linkDataArray": [ ]
} }
</textarea>
</div>
</body>
</html>
+412
View File
@@ -0,0 +1,412 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for drawing polygons or polylines
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* This tool allows the user to draw a new polygon or polyline shape by clicking where the corners should go.
* Right click or type ENTER to finish the operation.
* <p/>
* Set {@link #isPolygon} to false if you want this tool to draw open unfilled polyline shapes.
* Set {@link #archetypePartData} to customize the node data object that is added to the model.
* Data-bind to those properties in your node template to customize the appearance and behavior of the part.
* <p/>
* This tool uses a temporary {@link Shape}, {@link #temporaryShape}, held by a {@link Part} in the "Tool" layer,
* to show interactively what the user is drawing.
*/
function PolygonDrawingTool() {
go.Tool.call(this);
this.name = "PolygonDrawing";
this._isPolygon = true;
this._hasArcs = false;
this._isOrthoOnly = false;
this._isGridSnapEnabled = false;
this._archetypePartData = {}; // the data to copy for a new polygon Part
// this is the Shape that is shown during a drawing operation
this._temporaryShape = go.GraphObject.make(go.Shape, { name: "SHAPE", fill: "lightgray", strokeWidth: 1.5 });
// the Shape has to be inside a temporary Part that is used during the drawing operation
go.GraphObject.make(go.Part, { layerName: "Tool" }, this._temporaryShape);
}
go.Diagram.inherit(PolygonDrawingTool, go.Tool);
/**
* Don't start this tool in a mode-less fashion when the user's mouse-down is on an existing Part.
* When this tool is a mouse-down tool, it requires using the left mouse button in the background of a modifiable Diagram.
* Modal uses of this tool will not call this canStart predicate.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly || diagram.isModelReadOnly) return false;
var model = diagram.model;
if (model === null) return false;
// require left button
if (!diagram.firstInput.left) return false;
// can't start when mouse-down on an existing Part
var obj = diagram.findObjectAt(diagram.firstInput.documentPoint, null, null);
return (obj === null);
};
/**
* Start a transaction, capture the mouse, use a "crosshair" cursor,
* and start accumulating points in the geometry of the {@link #temporaryShape}.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.doActivate = function() {
go.Tool.prototype.doActivate.call(this);
var diagram = this.diagram;
this.startTransaction(this.name);
if (!diagram.lastInput.isTouchEvent) diagram.isMouseCaptured = true;
diagram.currentCursor = "crosshair";
// the first point
if (!diagram.lastInput.isTouchEvent) this.addPoint(diagram.lastInput.documentPoint);
};
/**
* Stop the transaction and clean up.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.doDeactivate = function() {
go.Tool.prototype.doDeactivate.call(this);
var diagram = this.diagram;
if (this.temporaryShape !== null) {
diagram.remove(this.temporaryShape.part);
}
diagram.currentCursor = "";
if (diagram.isMouseCaptured) diagram.isMouseCaptured = false;
this.stopTransaction();
};
/**
* Given a potential Point for the next segment, return a Point it to snap to the grid, and remain orthogonal, if either is applicable.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.modifyPointForGrid = function(p) {
var pregrid = p.copy();
var grid = this.diagram.grid;
if (grid !== null && grid.visible && this.isGridSnapEnabled) {
var cell = grid.gridCellSize;
var orig = grid.gridOrigin;
p = p.copy();
p.snapToGrid(orig.x, orig.y, cell.width, cell.height); // compute the closest grid point (modifies p)
}
if (this.temporaryShape.geometry === null) return p;
var fig = this.temporaryShape.geometry.figures.first();
var segments = fig.segments;
if (this.isOrthoOnly && segments.count > 0) {
var lastPt = null;
if (segments.count === 1) {
lastPt = new go.Point(fig.startX, fig.startY);
} else if (segments.count > 1) {
// the last segment is the current temporary segment, which we might be altering. We want the segment before
var secondLastSegment = (segments.elt(segments.count - 2));
lastPt = new go.Point(secondLastSegment.endX, secondLastSegment.endY);
}
if (pregrid.distanceSquared(lastPt.x, pregrid.y) < pregrid.distanceSquared(pregrid.x, lastPt.y)) { // closer to X coord
return new go.Point(lastPt.x, p.y);
} else { // closer to Y coord
return new go.Point(p.x, lastPt.y);
}
}
return p;
}
/**
* This internal method adds a segment to the geometry of the {@link #temporaryShape}.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.addPoint = function(p) {
var shape = this.temporaryShape;
if (shape === null) return;
// for the temporary Shape, normalize the geometry to be in the viewport
var viewpt = this.diagram.viewportBounds.position;
var q = this.modifyPointForGrid(new go.Point(p.x - viewpt.x, p.y - viewpt.y));
var part = shape.part;
// if it's not in the Diagram, re-initialize the Shape's geometry and add the Part to the Diagram
if (part.diagram === null) {
var fig = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill
var geo = new go.Geometry().add(fig); // the Shape.geometry consists of a single PathFigure
this.temporaryShape.geometry = geo;
// position the Shape's Part, accounting for the stroke width
part.position = viewpt.copy().offset(-shape.strokeWidth / 2, -shape.strokeWidth / 2);
this.diagram.add(part);
} else {
// must copy whole Geometry in order to add a PathSegment
var geo = shape.geometry.copy();
var fig = geo.figures.first();
if (this.hasArcs) {
var lastseg = fig.segments.last();
if (lastseg === null) {
fig.add(new go.PathSegment(go.PathSegment.QuadraticBezier, q.x, q.y, (fig.startX + q.x) / 2, (fig.startY + q.y) / 2));
} else {
fig.add(new go.PathSegment(go.PathSegment.QuadraticBezier, q.x, q.y, (lastseg.endX + q.x) / 2, (lastseg.endY + q.y) / 2));
}
} else {
fig.add(new go.PathSegment(go.PathSegment.Line, q.x, q.y));
}
}
shape.geometry = geo;
};
/**
* This internal method changes the last segment of the geometry of the {@link #temporaryShape} to end at the given point.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.moveLastPoint = function(p) {
p = this.modifyPointForGrid(p);
// must copy whole Geometry in order to change a PathSegment
var shape = this.temporaryShape;
var geo = shape.geometry.copy();
var fig = geo.figures.first();
var segs = fig.segments;
if (segs.count > 0) {
// for the temporary Shape, normalize the geometry to be in the viewport
var viewpt = this.diagram.viewportBounds.position;
var seg = segs.elt(segs.count - 1);
// modify the last PathSegment to be the given Point p
seg.endX = p.x - viewpt.x;
seg.endY = p.y - viewpt.y;
if (seg.type === go.PathSegment.QuadraticBezier) {
var prevx = 0.0;
var prevy = 0.0;
if (segs.count > 1) {
var prevseg = segs.elt(segs.count - 2);
prevx = prevseg.endX;
prevy = prevseg.endY;
} else {
prevx = fig.startX;
prevy = fig.startY;
}
seg.point1X = (seg.endX + prevx)/2;
seg.point1Y = (seg.endY + prevy)/2;
}
shape.geometry = geo;
}
};
/**
* This internal method removes the last segment of the geometry of the {@link #temporaryShape}.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.removeLastPoint = function() {
// must copy whole Geometry in order to remove a PathSegment
var shape = this.temporaryShape;
var geo = shape.geometry.copy();
var segs = geo.figures.first().segments;
if (segs.count > 0) {
segs.removeAt(segs.count-1);
shape.geometry = geo;
}
};
/**
* Add a new node data JavaScript object to the model and initialize the Part's
* position and its Shape's geometry by copying the {@link #temporaryShape}'s {@link Shape#geometry}.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.finishShape = function() {
var diagram = this.diagram;
var shape = this.temporaryShape;
if (shape !== null && this.archetypePartData !== null) {
// remove the temporary point, which is last, except on touch devices
if (!diagram.lastInput.isTouchEvent) this.removeLastPoint();
var tempgeo = shape.geometry;
// require 3 points (2 segments) if polygon; 2 points (1 segment) if polyline
if (tempgeo.figures.first().segments.count >= (this.isPolygon ? 2 : 1)) {
// normalize geometry and node position
var viewpt = diagram.viewportBounds.position;
var geo = tempgeo.copy();
if (this.isPolygon) {
// if polygon, close the last segment
var segs = geo.figures.first().segments;
var seg = segs.elt(segs.count-1);
seg.isClosed = true;
}
// create the node data for the model
var d = diagram.model.copyNodeData(this.archetypePartData);
// adding data to model creates the actual Part
diagram.model.addNodeData(d);
var part = diagram.findPartForData(d);
// assign the position for the whole Part
var pos = geo.normalize();
pos.x = viewpt.x - pos.x - shape.strokeWidth / 2;
pos.y = viewpt.y - pos.y - shape.strokeWidth / 2;
part.position = pos;
// assign the Shape.geometry
var shape = part.findObject("SHAPE");
if (shape !== null) shape.geometry = geo;
this.transactionResult = this.name;
}
}
this.stopTool();
};
/**
* Add another point to the geometry of the {@link #temporaryShape}.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.doMouseDown = function() {
if (!this.isActive) {
this.doActivate();
}
// a new temporary end point, the previous one is now "accepted"
this.addPoint(this.diagram.lastInput.documentPoint);
if (!this.diagram.lastInput.left) { // e.g. right mouse down
this.finishShape();
} else if (this.diagram.lastInput.clickCount > 1) { // e.g. double-click
this.removeLastPoint();
this.finishShape();
}
};
/**
* Move the last point of the {@link #temporaryShape}'s geometry to follow the mouse point.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.doMouseMove = function() {
if (this.isActive) {
this.moveLastPoint(this.diagram.lastInput.documentPoint);
}
};
/**
* Do not stop this tool, but continue to accumulate Points via mouse-down events.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.doMouseUp = function() {
// don't stop this tool (the default behavior is to call stopTool)
};
/**
* Typing the "ENTER" key accepts the current geometry (excluding the current mouse point)
* and creates a new part in the model by calling {@link #finishShape}.
* <p/>
* Typing the "Z" key causes the previous point to be discarded.
* <p/>
* Typing the "ESCAPE" key causes the temporary Shape and its geometry to be discarded and this tool to be stopped.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.doKeyDown = function() {
if (!this.isActive) return;
var e = this.diagram.lastInput;
if (e.key === '\r') { // accept
this.finishShape(); // all done!
} else if (e.key === 'Z') { // undo
this.undo();
} else {
go.Tool.prototype.doKeyDown.call(this);
}
};
/**
* Undo: remove the last point and continue the drawing of new points.
* @this {PolygonDrawingTool}
*/
PolygonDrawingTool.prototype.undo = function() {
// remove a point, and then treat the last one as a temporary one
this.removeLastPoint();
var lastInput = this.diagram.lastInput;
if (lastInput.event instanceof window.MouseEvent) this.moveLastPoint(lastInput.documentPoint);
};
// Public properties
/**
* Gets or sets whether this tools draws a filled polygon or an unfilled open polyline.
* The default value is true.
* @name PolygonDrawingTool#isPolygon
* @return {boolean}
*/
Object.defineProperty(PolygonDrawingTool.prototype, "isPolygon", {
get: function() { return this._isPolygon; },
set: function(val) { this._isPolygon = val; }
});
/**
* Gets or sets whether this tool draws shapes with quadratic bezier curves for each segment, or just straight lines.
* The default value is false -- only use straight lines.
* @name PolygonDrawingTool#hasArcs
* @return {boolean}
*/
Object.defineProperty(PolygonDrawingTool.prototype, "hasArcs", {
get: function() { return this._hasArcs; },
set: function(val) { this._hasArcs = val; }
});
/**
* Gets or sets whether this tool draws shapes with only orthogonal segments, or segments in any direction.
* The default value is false -- draw segments in any direction. This does not restrict the closing segment, which may not be orthogonal.
* @name PolygonDrawingTool#isOrthoOnly
* @return {boolean}
*/
Object.defineProperty(PolygonDrawingTool.prototype, "isOrthoOnly", {
get: function() { return this._isOrthoOnly; },
set: function(val) { this._isOrthoOnly = val; }
});
/**
* Gets or sets whether this tool only places the shape's corners on the Diagram's visible grid.
* The default value is false.
* @name PolygonDrawingTool#isGridSnapEnabled
* @return {boolean}
*/
Object.defineProperty(PolygonDrawingTool.prototype, "isGridSnapEnabled", {
get: function() { return this._isGridSnapEnabled; },
set: function(val) { this._isGridSnapEnabled = val; }
});
/**
* Gets or sets the Shape that is used to hold the line as it is being drawn.
* The default value is a simple Shape drawing an unfilled open thin black line.
* @name PolygonDrawingTool#temporaryShape
* @return {Shape}
*/
Object.defineProperty(PolygonDrawingTool.prototype, "temporaryShape", {
get: function() { return this._temporaryShape; },
set: function(val) {
if (this._temporaryShape !== val && val !== null) {
val.name = "SHAPE";
var panel = this._temporaryShape.panel;
panel.remove(this._temporaryShape);
this._temporaryShape = val;
panel.add(this._temporaryShape);
}
}
});
/**
* Gets or sets the node data object that is copied and added to the model
* when the drawing operation completes.
* @name PolygonDrawingTool#archetypePartData
* @return {Object}
*/
Object.defineProperty(PolygonDrawingTool.prototype, "archetypePartData", {
get: function() { return this._archetypePartData; },
set: function(val) { this._archetypePartData = val; }
});
+109
View File
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<head>
<title>Polyline Linking Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Let the user draw a new link by clicking consecutive points through which the link's route must pass." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="PolylineLinkingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv");
// install custom linking tool, defined in PolylineLinkingTool.js
var tool = new PolylineLinkingTool();
//tool.temporaryLink.routing = go.Link.Orthogonal; // optional, but need to keep link template in sync, below
myDiagram.toolManager.linkingTool = tool;
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{ locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape,
{
width: 100, height: 100, fill: "lightgray",
portId: "", cursor: "pointer",
fromLinkable: true,
fromLinkableSelfNode: true, fromLinkableDuplicates: true, // optional
toLinkable: true,
toLinkableSelfNode: true, toLinkableDuplicates: true // optional
},
new go.Binding("fill")),
$(go.Shape, { width: 70, height: 70, fill: "transparent", stroke: null }),
$(go.TextBlock,
new go.Binding("text")));
myDiagram.linkTemplate =
$(go.Link,
{ reshapable: true, resegmentable: true },
//{ routing: go.Link.Orthogonal }, // optional, but need to keep LinkingTool.temporaryLink in sync, above
{ adjusting: go.Link.Stretch }, // optional
new go.Binding("points", "points").makeTwoWay(),
$(go.Shape, { strokeWidth: 1.5 }),
$(go.Shape, { toArrow: "OpenTriangle" }));
load(); // load a simple diagram from the textarea
}
// save a model to and load a model from Json text, displayed below the Diagram
function save() {
var str = myDiagram.model.toJson();
document.getElementById("mySavedModel").value = str;
}
function load() {
var str = document.getElementById("mySavedModel").value;
myDiagram.model = go.Model.fromJson(str);
myDiagram.model.undoManager.isEnabled = true;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 600px"></div>
<div id="buttons">
<button onclick="save()">Save</button>
<button onclick="load()">Load</button>
</div>
<p>
This sample demonstrates the PolylineLinkingTool, which replaces the standard LinkingTool.
The tool is defined in its own file, as <a href="PolylineLinkingTool.js">PolylineLinkingTool.js</a>.
</p>
<p>
The user starts drawing a new link from a node in the normal manner, by dragging from a port,
which for feedback purposes has a "pointer" cursor.
Normally the user would have to release the mouse near the target port/node.
However with the PolylineLinkingTool the user may click at various points to cause the new link
to be routed along those points.
Clicking on the target port completes the new link.
Press <b>Escape</b> to cancel, or <b>Z</b> to remove the last point.
</p>
<p>
Furthermore, because <a>Link.resegmentable</a> is true, the user can easily add or remove segments
from the route of a selected link. To insert a segment, the user can start dragging the small
diamond resegmenting handle. To remove a segment, the user needs to move a regular reshaping handle
to cause the adjacent two segments to be in a straight line.
</p>
<p>
The PolylineLinkingTool also works with orthogonally routed links.
To demonstrate this, uncomment the two lines that initialize <a>Link.routing</a> to be <a>Link,Orthogonal</a>.
</p>
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"nodeDataArray": [
{ "key": 1, "text": "Node 1", "fill": "blueviolet", "loc": "100 100" },
{ "key": 2, "text": "Node 2", "fill": "orange", "loc": "400 100" }
],
"linkDataArray": [ ]
}
</textarea>
</div>
</body>
</html>
+241
View File
@@ -0,0 +1,241 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom LinkingTool for manually routing a new link
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends LinkingTool
* @class
* This tool allows the user to draw a new link by clicking where the route should go,
* until clicking on a valid target port.
* <p/>
* This tool supports routing both orthogonal and straight links.
* You can customize the {@link LinkingBaseTool#temporaryLink} as needed to affect the
* appearance and behavior of the temporary link that is shown during the linking operation.
* You can customize the {@link LinkingTool#archetypeLinkData} to specify property values
* that can be data-bound by your link template for the Links that are actually created.
*/
function PolylineLinkingTool() {
go.LinkingTool.call(this);
this.name = "PolylineLinking";
this.portGravity = 0; // gotta click on a target port in order to complete the link
}
go.Diagram.inherit(PolylineLinkingTool, go.LinkingTool);
/**
* Use a "crosshair" cursor.
* @this {PolylineLinkingTool}
*/
PolylineLinkingTool.prototype.doActivate = function() {
go.LinkingTool.prototype.doActivate.call(this);
this.diagram.currentCursor = "crosshair";
// until a mouse down occurs, allow the temporary link to be routed to the temporary node/port
this._firstMouseDown = true;
};
/**
* This internal method adds a point to the route.
* During the operation of this tool, the very last point changes to follow the mouse point.
* This method is called by {@link #doMouseDown} in order to add a new "last" point.
* @this {PolylineLinkingTool}
* @param {Point} p
*/
PolylineLinkingTool.prototype.addPoint = function(p) {
if (this._firstMouseDown) return;
var pts = this.temporaryLink.points.copy();
this._horizontal = !this._horizontal;
pts.add(p.copy());
this.temporaryLink.points = pts;
};
/**
* This internal method moves the last point of the temporary Link's route.
* This is called by {@link #doMouseMove} and other methods that want to adjust the end of the route.
* @this {PolylineLinkingTool}
* @param {Point} p
*/
PolylineLinkingTool.prototype.moveLastPoint = function(p) {
if (this._firstMouseDown) return;
var pts = this.temporaryLink.points.copy();
if (this.temporaryLink.isOrthogonal) {
var q = pts.elt(pts.length - 3).copy();
if (this._horizontal) {
q.y = p.y;
} else {
q.x = p.x;
}
pts.setElt(pts.length - 2, q);
}
pts.setElt(pts.length - 1, p.copy());
this.temporaryLink.points = pts;
};
/**
* This internal method removes the last point of the temporary Link's route.
* This is called by the "Z" command in {@link #doKeyDown}
* and by {@link #doMouseUp} when a valid target port is found and we want to
* discard the current mouse point from the route.
* @this {PolylineLinkingTool}
*/
PolylineLinkingTool.prototype.removeLastPoint = function() {
if (this._firstMouseDown) return;
var pts = this.temporaryLink.points.copy();
if (pts.length === 0) return;
pts.removeAt(pts.length - 1);
this.temporaryLink.points = pts;
this._horizontal = !this._horizontal;
};
/**
* Add a point to the route that the temporary Link is accumulating.
* @this {PolylineLinkingTool}
*/
PolylineLinkingTool.prototype.doMouseDown = function() {
if (!this.isActive) {
this.doActivate();
}
if (this.diagram.lastInput.left) {
if (this._firstMouseDown) {
this._firstMouseDown = false;
// disconnect the temporary node/port from the temporary link
// so that it doesn't lose the points that are accumulating
if (this.isForwards) {
this.temporaryLink.toNode = null;
} else {
this.temporaryLink.fromNode = null;
}
var pts = this.temporaryLink.points;
var ult = pts.elt(pts.length - 1);
var penult = pts.elt(pts.length - 2);
this._horizontal = (ult.x === penult.x);
}
// a new temporary end point, the previous one is now "accepted"
this.addPoint(this.diagram.lastInput.documentPoint);
} else { // e.g. right mouse down
this.doCancel();
}
};
/**
* Have the temporary link reach to the last mouse point.
* @this {PolylineLinkingTool}
*/
PolylineLinkingTool.prototype.doMouseMove = function() {
if (this.isActive) {
this.moveLastPoint(this.diagram.lastInput.documentPoint);
go.LinkingTool.prototype.doMouseMove.call(this);
}
};
/**
* If this event happens on a valid target port (as determined by {@link LinkingBaseTool#findTargetPort}),
* we complete the link drawing operation. {@link #insertLink} is overridden to transfer the accumulated
* route drawn by user clicks to the new {@link Link} that was created.
* <p/>
* If this event happens elsewhere in the diagram, this tool is not stopped: the drawing of the route continues.
* @this {PolylineLinkingTool}
*/
PolylineLinkingTool.prototype.doMouseUp = function() {
if (!this.isActive) return;
var target = this.findTargetPort(this.isForwards);
if (target !== null) {
if (this._firstMouseDown) {
go.LinkingTool.prototype.doMouseUp.call(this);
} else {
var pts;
this.removeLastPoint(); // remove temporary point
var spot = this.isForwards ? target.toSpot : target.fromSpot;
if (spot.equals(go.Spot.None)) {
var pt = this.temporaryLink.getLinkPointFromPoint(target.part, target,
target.getDocumentPoint(go.Spot.Center),
this.temporaryLink.points.elt(this.temporaryLink.points.length - 2),
!this.isForwards);
this.moveLastPoint(pt);
pts = this.temporaryLink.points.copy();
if (this.temporaryLink.isOrthogonal) {
pts.insertAt(pts.length - 2, pts.elt(pts.length - 2));
}
} else {
// copy the route of saved points, because we're about to recompute it
pts = this.temporaryLink.points.copy();
// terminate the link in the expected manner by letting the
// temporary link connect with the temporary node/port and letting the
// normal route computation take place
if (this.isForwards) {
this.copyPortProperties(target.part, target, this.temporaryToNode, this.temporaryToPort, true);
this.temporaryLink.toNode = target.part;
} else {
this.copyPortProperties(target.part, target, this.temporaryFromNode, this.temporaryFromPort, false);
this.temporaryLink.fromNode = target.part;
}
this.temporaryLink.updateRoute();
// now copy the final one or two points of the temporary link's route
// into the route built up in the PTS List.
var natpts = this.temporaryLink.points;
var numnatpts = natpts.length;
if (numnatpts >= 2) {
if (numnatpts >= 3) {
var penult = natpts.elt(numnatpts - 2);
pts.insertAt(pts.length - 1, penult);
if (this.temporaryLink.isOrthogonal) {
pts.insertAt(pts.length - 1, penult);
}
}
var ult = natpts.elt(numnatpts - 1);
pts.setElt(pts.length - 1, ult);
}
}
// save desired route in temporary link;
// insertLink will copy the route into the new real Link
this.temporaryLink.points = pts;
go.LinkingTool.prototype.doMouseUp.call(this);
}
}
};
/**
* This method overrides the standard link creation method by additionally
* replacing the default link route with the custom one laid out by the user.
* @this {PolylineLinkingTool}
* @this {Node} fromnode
* @this {GraphObject} fromport
* @this {Node} tonode
* @this {GraphObject} toport
* @return {Link}
*/
PolylineLinkingTool.prototype.insertLink = function(fromnode, fromport, tonode, toport) {
var link = go.LinkingTool.prototype.insertLink.call(this, fromnode, fromport, tonode, toport);
if (link !== null && !this._firstMouseDown) {
// ignore natural route by replacing with route accumulated by this tool
link.points = this.temporaryLink.points;
}
return link;
};
/**
* This supports the "Z" command during this tool's operation to remove the last added point of the route.
* Type ESCAPE to completely cancel the operation of the tool.
* @this {PolylineLinkingTool}
*/
PolylineLinkingTool.prototype.doKeyDown = function() {
if (!this.isActive) return;
var e = this.diagram.lastInput;
if (e.key === 'Z' && this.temporaryLink.points.length > (this.temporaryLink.isOrthogonal ? 4 : 3)) { // undo
// remove a point, and then treat the last one as a temporary one
this.removeLastPoint();
this.moveLastPoint(e.documentPoint);
} else {
go.Tool.prototype.doKeyDown.call(this);
}
};
+398
View File
@@ -0,0 +1,398 @@
<!DOCTYPE html>
<html>
<head>
<title>Logic Circuit with shiftable ports</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow the user to shift ports that are in a node." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="Figures.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="PortShiftingTool.js"></script>
<script id="code">
var red = "orangered"; // 0 or false
var green = "forestgreen"; // 1 or true
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a new Diagram in the HTML DIV element "myDiagramDiv"
{
"draggingTool.isGridSnapEnabled": true, // dragged nodes will snap to a grid of 10x10 cells
"undoManager.isEnabled": true
});
// install the PortShiftingTool as a "mouse move" tool
myDiagram.toolManager.mouseMoveTools.insertAt(0, new PortShiftingTool());
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("saveModel");
if (button) button.disabled = !myDiagram.isModified;
var idx = document.title.indexOf("*");
if (myDiagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
var palette = new go.Palette("palette"); // create a new Palette in the HTML DIV element "palette"
// creates relinkable Links that will avoid crossing Nodes when possible and will jump over other Links in their paths
myDiagram.linkTemplate =
$(go.Link,
{
routing: go.Link.AvoidsNodes,
curve: go.Link.JumpOver,
corner: 3,
relinkableFrom: true, relinkableTo: true,
selectionAdorned: false, // Links are not adorned when selected so that their color remains visible.
shadowOffset: new go.Point(0, 0), shadowBlur: 5, shadowColor: "blue",
},
new go.Binding("isShadowed", "isSelected").ofObject(),
$(go.Shape,
{ name: "SHAPE", strokeWidth: 2, stroke: red }));
// node template helpers
var sharedToolTip =
$("ToolTip",
{ "Border.figure": "RoundedRectangle" },
$(go.TextBlock, { margin: 2 },
new go.Binding("text", "", function(d) { return d.category; })));
// define some common property settings
function nodeStyle() {
return [new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("isShadowed", "isSelected").ofObject(),
{
selectionAdorned: false,
shadowOffset: new go.Point(0, 0),
shadowBlur: 15,
shadowColor: "blue",
resizable: true,
resizeObjectName: "NODESHAPE",
toolTip: sharedToolTip
}];
}
function shapeStyle() {
return {
name: "NODESHAPE",
fill: "lightgray",
stroke: "darkslategray",
desiredSize: new go.Size(40, 40),
strokeWidth: 2
};
}
function portStyle(input) {
return {
desiredSize: new go.Size(6, 6),
fill: "black",
fromSpot: go.Spot.Right,
fromLinkable: !input,
toSpot: go.Spot.Left,
toLinkable: input,
toMaxLinks: 1,
cursor: "pointer"
};
}
// define templates for each type of node
var inputTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "Circle", shapeStyle(),
{ fill: red }), // override the default fill (from shapeStyle()) to be red
$(go.Shape, "Rectangle", portStyle(false), // the only port
{ portId: "", alignment: new go.Spot(1, 0.5) }),
{ // if double-clicked, an input node will change its value, represented by the color.
doubleClick: function(e, obj) {
e.diagram.startTransaction("Toggle Input");
var shp = obj.findObject("NODESHAPE");
shp.fill = (shp.fill === green) ? red : green;
updateStates();
e.diagram.commitTransaction("Toggle Input");
}
}
);
var outputTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "Rectangle", shapeStyle(),
{ fill: green }), // override the default fill (from shapeStyle()) to be green
$(go.Shape, "Rectangle", portStyle(true), // the only port
{ portId: "", alignment: new go.Spot(0, 0.5) })
);
var andTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "AndGate", shapeStyle()),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in1", alignment: new go.Spot(0, 0.3) }),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in2", alignment: new go.Spot(0, 0.7) }),
$(go.Shape, "Rectangle", portStyle(false),
{ portId: "out", alignment: new go.Spot(1, 0.5) })
);
var orTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "OrGate", shapeStyle()),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in1", alignment: new go.Spot(0.16, 0.3) }),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in2", alignment: new go.Spot(0.16, 0.7) }),
$(go.Shape, "Rectangle", portStyle(false),
{ portId: "out", alignment: new go.Spot(1, 0.5) })
);
var xorTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "XorGate", shapeStyle()),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in1", alignment: new go.Spot(0.26, 0.3) }),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in2", alignment: new go.Spot(0.26, 0.7) }),
$(go.Shape, "Rectangle", portStyle(false),
{ portId: "out", alignment: new go.Spot(1, 0.5) })
);
var norTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "NorGate", shapeStyle()),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in1", alignment: new go.Spot(0.16, 0.3) }),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in2", alignment: new go.Spot(0.16, 0.7) }),
$(go.Shape, "Rectangle", portStyle(false),
{ portId: "out", alignment: new go.Spot(1, 0.5) })
);
var xnorTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "XnorGate", shapeStyle()),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in1", alignment: new go.Spot(0.26, 0.3) }),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in2", alignment: new go.Spot(0.26, 0.7) }),
$(go.Shape, "Rectangle", portStyle(false),
{ portId: "out", alignment: new go.Spot(1, 0.5) })
);
var nandTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "NandGate", shapeStyle()),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in1", alignment: new go.Spot(0, 0.3) }),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in2", alignment: new go.Spot(0, 0.7) }),
$(go.Shape, "Rectangle", portStyle(false),
{ portId: "out", alignment: new go.Spot(1, 0.5) })
);
var notTemplate =
$(go.Node, "Spot", nodeStyle(),
$(go.Shape, "Inverter", shapeStyle()),
$(go.Shape, "Rectangle", portStyle(true),
{ portId: "in", alignment: new go.Spot(0, 0.5) }),
$(go.Shape, "Rectangle", portStyle(false),
{ portId: "out", alignment: new go.Spot(1, 0.5) })
);
// add the templates created above to myDiagram and palette
myDiagram.nodeTemplateMap.add("input", inputTemplate);
myDiagram.nodeTemplateMap.add("output", outputTemplate);
myDiagram.nodeTemplateMap.add("and", andTemplate);
myDiagram.nodeTemplateMap.add("or", orTemplate);
myDiagram.nodeTemplateMap.add("xor", xorTemplate);
myDiagram.nodeTemplateMap.add("not", notTemplate);
myDiagram.nodeTemplateMap.add("nand", nandTemplate);
myDiagram.nodeTemplateMap.add("nor", norTemplate);
myDiagram.nodeTemplateMap.add("xnor", xnorTemplate);
// share the template map with the Palette
palette.nodeTemplateMap = myDiagram.nodeTemplateMap;
palette.model.nodeDataArray = [
{ category: "input" },
{ category: "output" },
{ category: "and" },
{ category: "or" },
{ category: "xor" },
{ category: "not" },
{ category: "nand" },
{ category: "nor" },
{ category: "xnor" }
];
// load the initial diagram
load();
// continually update the diagram
loop();
}
// update the diagram every 250 milliseconds
function loop() {
setTimeout(function() { updateStates(); loop(); }, 250);
}
// update the value and appearance of each node according to its type and input values
function updateStates() {
var oldskip = myDiagram.skipsUndoManager;
myDiagram.skipsUndoManager = true;
// do all "input" nodes first
myDiagram.nodes.each(function(node) {
if (node.category === "input") {
doInput(node);
}
});
// now we can do all other kinds of nodes
myDiagram.nodes.each(function(node) {
switch (node.category) {
case "and": doAnd(node); break;
case "or": doOr(node); break;
case "xor": doXor(node); break;
case "not": doNot(node); break;
case "nand": doNand(node); break;
case "nor": doNor(node); break;
case "xnor": doXnor(node); break;
case "output": doOutput(node); break;
case "input": break; // doInput already called, above
}
});
myDiagram.skipsUndoManager = oldskip;
}
// helper predicate
function linkIsTrue(link) { // assume the given Link has a Shape named "SHAPE"
return link.findObject("SHAPE").stroke === green;
}
// helper function for propagating results
function setOutputLinks(node, color) {
node.findLinksOutOf().each(function(link) { link.findObject("SHAPE").stroke = color; });
}
// update nodes by the specific function for its type
// determine the color of links coming out of this node based on those coming in and node type
function doInput(node) {
// the output is just the node's Shape.fill
setOutputLinks(node, node.findObject("NODESHAPE").fill);
}
function doAnd(node) {
var color = node.findLinksInto().all(linkIsTrue) ? green : red;
setOutputLinks(node, color);
}
function doNand(node) {
var color = !node.findLinksInto().all(linkIsTrue) ? green : red;
setOutputLinks(node, color);
}
function doNot(node) {
var color = !node.findLinksInto().all(linkIsTrue) ? green : red;
setOutputLinks(node, color);
}
function doOr(node) {
var color = node.findLinksInto().any(linkIsTrue) ? green : red;
setOutputLinks(node, color);
}
function doNor(node) {
var color = !node.findLinksInto().any(linkIsTrue) ? green : red;
setOutputLinks(node, color);
}
function doXor(node) {
var truecount = 0;
node.findLinksInto().each(function(link) { if (linkIsTrue(link)) truecount++; });
var color = truecount % 2 !== 0 ? green : red;
setOutputLinks(node, color);
}
function doXnor(node) {
var truecount = 0;
node.findLinksInto().each(function(link) { if (linkIsTrue(link)) truecount++; });
var color = truecount % 2 === 0 ? green : red;
setOutputLinks(node, color);
}
function doOutput(node) {
// assume there is just one input link
// we just need to update the node's Shape.fill
node.linksConnected.each(function(link) { node.findObject("NODESHAPE").fill = link.findObject("SHAPE").stroke; });
}
// save a model to and load a model from Json text, displayed below the Diagram
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div style="width: 100%; display: flex; justify-content: space-between">
<div id="palette" style="width: 100px; height: 500px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
<div id="myDiagramDiv" style="flex-grow: 1; height: 500px; border: solid 1px black"></div>
</div>
<div id="description">
<p>
This is exactly like the <a href="../samples/LogicCircuit.html">Logic Circuit sample</a>
but also makes use of the PortShiftingTool,
which is defined in <a href="PortShiftingTool.js">PortShiftingTool.js</a>
</p>
<p>
When the user wants to shift the position of a port on a node,
the user can hold down the Shift key during a mouse-down on a port element.
Dragging then will move the port within the node.
</p>
<p>
Note how the relative position of the port within the node is maintained as you resize the node.
</p>
<p>
If you want to persist the port's spot, you should add a TwoWay Binding of the <a>GraphObject.alignment</a>
property with a property that you define on the node data for each port.
</p>
<p>
This sample does not constrain the position of the port within the node,
but you could adapt the PortShiftingTool.updateAlignment method to do so.
For example if you wanted, you could keep a port stuck along one edge of the node.
</p>
</div>
<div id="buttons">
<button id="saveModel" onclick="save()">Save</button>
<button id="loadModel" onclick="load()">Load</button>
</div>
<textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "go.GraphLinksModel",
"linkFromPortIdProperty": "fromPort",
"linkToPortIdProperty": "toPort",
"nodeDataArray": [
{"category":"input", "key":"input1", "loc":"-150 -80" },
{"category":"or", "key":"or1", "loc":"-70 0" },
{"category":"not", "key":"not1", "loc":"10 0" },
{"category":"xor", "key":"xor1", "loc":"100 0" },
{"category":"or", "key":"or2", "loc":"200 0" },
{"category":"output", "key":"output1", "loc":"200 -100" }
],
"linkDataArray": [
{"from":"input1", "fromPort":"out", "to":"or1", "toPort":"in1"},
{"from":"or1", "fromPort":"out", "to":"not1", "toPort":"in"},
{"from":"not1", "fromPort":"out", "to":"or1", "toPort":"in2"},
{"from":"not1", "fromPort":"out", "to":"xor1", "toPort":"in1"},
{"from":"xor1", "fromPort":"out", "to":"or2", "toPort":"in1"},
{"from":"or2", "fromPort":"out", "to":"xor1", "toPort":"in2"},
{"from":"xor1", "fromPort":"out", "to":"output1", "toPort":""}
]}
</textarea>
</div>
</body>
</html>
+158
View File
@@ -0,0 +1,158 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for moving a port on a Node
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* This tool only works when the Node has a port (any GraphObject) marked with
* a non-null and non-empty portId that is positioned in a Spot Panel,
* and the user holds down the Shift key.
* It works by modifying that port's GraphObject.alignment property.
*/
function PortShiftingTool() {
go.Tool.call(this);
this.name = "PortShifting";
/** @type {GraphObject} */
this.port = null;
/** @type {Point} */
this._originalAlignment = null;
}
go.Diagram.inherit(PortShiftingTool, go.Tool);
/**
* This tool can only start if the mouse has moved enough so that it is not a click,
* and if the mouse down point is on a GraphObject "port" in a Spot Panel,
* as determined by findPort().
* @this {PortShiftingTool}
* @return {boolean}
*/
PortShiftingTool.prototype.canStart = function() {
if (!go.Tool.prototype.canStart.call(this)) return false;
var diagram = this.diagram;
if (diagram === null) return false;
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
var e = diagram.lastInput;
if (!e.left || !e.shift) return false;
if (!this.isBeyondDragSize()) return false;
return this.findPort() !== null;
}
/**
* From the GraphObject at the mouse point, search up the visual tree until we get to
* an object that has the portId property set to a non-empty string, that is in a Spot Panel,
* and that is not the main element of the panel (typically the first element).
* @this {PortShiftingTool}
* @return {GraphObject} This returns null if no such port is at the mouse down point.
*/
PortShiftingTool.prototype.findPort = function() {
var diagram = this.diagram;
var e = diagram.firstInput;
var elt = diagram.findObjectAt(e.documentPoint, null, null);
if (elt === null || !(elt.part instanceof go.Node)) return null;
while (elt !== null && elt.panel !== null) {
if (elt.panel.type === go.Panel.Spot && elt.panel.findMainElement() !== elt &&
elt.portId !== null && elt.portId !== "") return elt;
elt = elt.panel;
}
return null;
};
/**
* Start a transaction, call findPort and remember it as the "port" property,
* and remember the original value for the port's alignment property.
* @this {PortShiftingTool}
*/
PortShiftingTool.prototype.doActivate = function() {
this.startTransaction("Shifted Label");
this.port = this.findPort();
if (this.port !== null) {
this._originalAlignment = this.port.alignment.copy();
var main = this.port.panel.findMainElement();
}
go.Tool.prototype.doActivate.call(this);
}
/**
* Stop any ongoing transaction.
* @this {PortShiftingTool}
*/
PortShiftingTool.prototype.doDeactivate = function() {
go.Tool.prototype.doDeactivate.call(this);
this.stopTransaction();
}
/**
* Clear any reference to a port element.
* @this {PortShiftingTool}
*/
PortShiftingTool.prototype.doStop = function() {
this.port = null;
go.Tool.prototype.doStop.call(this);
}
/**
* Restore the port's original value for GraphObject.alignment.
* @this {PortShiftingTool}
*/
PortShiftingTool.prototype.doCancel = function() {
if (this.port !== null) {
this.port.alignment = this._originalAlignment;
}
go.Tool.prototype.doCancel.call(this);
}
/**
* During the drag, call updateAlignment in order to set the GraphObject.alignment of the port.
* @this {PortShiftingTool}
*/
PortShiftingTool.prototype.doMouseMove = function() {
if (!this.isActive) return;
this.updateAlignment();
}
/**
* At the end of the drag, update the alignment of the port and finish the tool,
* completing a transaction.
* @this {PortShiftingTool}
*/
PortShiftingTool.prototype.doMouseUp = function() {
if (!this.isActive) return;
this.updateAlignment();
this.transactionResult = "Shifted Label";
this.stopTool();
}
/**
* Save the port's GraphObject.alignment as a fractional Spot in the Spot Panel
* that the port is in. Thus if the main element changes size, the relative positions
* of the ports will be maintained. But that does assume that the port must remain
* inside the main element -- it cannot wander away from the node.
* This does not modify the port's GraphObject.alignmentFocus property.
* @this {PortShiftingTool}
*/
PortShiftingTool.prototype.updateAlignment = function() {
if (this.port === null) return;
var last = this.diagram.lastInput.documentPoint;
var main = this.port.panel.findMainElement();
var tl = main.getDocumentPoint(go.Spot.TopLeft);
var br = main.getDocumentPoint(go.Spot.BottomRight);
var x = Math.max(0, Math.min((last.x - tl.x) / (br.x - tl.x), 1));
var y = Math.max(0, Math.min((last.y - tl.y) / (br.y - tl.y), 1));
this.port.alignment = new go.Spot(x, y);
}
+325
View File
@@ -0,0 +1,325 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Layout
* @class
* Given a root Node this arranges connected nodes in concentric rings,
* layered by the minimum link distance from the root.
*/
function RadialLayout() {
go.Layout.call(this);
this._root = null;
this._layerThickness = 100; // how thick each ring should be
this._maxLayers = Infinity;
}
go.Diagram.inherit(RadialLayout, go.Layout);
/**
* @ignore
* Copies properties to a cloned Layout.
* @this {RadialLayout}
* @param {Layout} copy
*/
RadialLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
// don't copy .root
copy._layerThickness = this._layerThickness;
copy._maxLayers = this._maxLayers;
};
/*
* The Node to act as the root or central node of the radial layout.
* @name RadialLayout#root
* @return {Node}
*/
Object.defineProperty(RadialLayout.prototype, "root", {
get: function() { return this._root; },
set: function(value) {
if (this._root !== value) {
this._root = value;
this.invalidateLayout();
}
}
});
/*
* The thickness of each ring representing a layer.
* @name RadialLayout#layerThickness
* @return {number}
*/
Object.defineProperty(RadialLayout.prototype, "layerThickness", {
get: function() { return this._layerThickness; },
set: function(value) {
if (this._layerThickness !== value) {
this._layerThickness = value;
this.invalidateLayout();
}
}
});
/*
* The maximum number of layers to be shown, in addition to the root node at layer zero.
* The default value is Infinity.
* @name RadialLayout#maxLayers
* @return {number}
*/
Object.defineProperty(RadialLayout.prototype, "maxLayers", {
get: function() { return this._maxLayers; },
set: function(value) {
if (this._maxLayers !== value) {
this._maxLayers = value;
this.invalidateLayout();
}
}
});
/**
* Use a LayoutNetwork that always creates RadialVertexes.
* @this {RadialLayout}
* @return {LayoutNetwork}
*/
RadialLayout.prototype.createNetwork = function() {
var net = new go.LayoutNetwork(this);
net.createVertex = function() { return new RadialVertex(net); };
return net;
}
/**
* @this {RadialLayout}
* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
*/
RadialLayout.prototype.doLayout = function(coll) {
if (this.network === null) {
this.network = this.makeNetwork(coll);
}
if (this.network.vertexes.count === 0) return;
if (this.root === null) {
// If no root supplied, choose one without any incoming edges
var it = this.network.vertexes.iterator;
while (it.next()) {
var v = it.value;
if (v.node !== null && v.sourceEdges.count === 0) {
this.root = v.node;
break;
}
}
}
if (this.root === null) {
// If could not find any default root, choose a random one
this.root = this.network.vertexes.first().node;
}
if (this.root === null) return; // nothing to do
var rootvert = this.network.findVertex(this.root);
if (rootvert === null) throw new Error("RadialLayout.root must be a Node in the LayoutNetwork that the RadialLayout is operating on")
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
this.findDistances(rootvert);
// sort all results into Arrays of RadialVertexes with the same distance
var verts = [];
var maxlayer = 0;
var it = this.network.vertexes.iterator;
while (it.next()) {
var v = it.value;
v.laid = false;
var layer = v.distance;
if (layer === Infinity) continue; // Infinity used as init value (set in findDistances())
if (layer > maxlayer) maxlayer = layer;
var layerverts = verts[layer];
if (layerverts === undefined) {
layerverts = [];
verts[layer] = layerverts;
}
layerverts.push(v);
}
// now recursively position nodes (using radlay1()), starting with the root
rootvert.centerX = this.arrangementOrigin.x;
rootvert.centerY = this.arrangementOrigin.y;
this.radlay1(rootvert, 1, 0, 360);
// Update the "physical" positions of the nodes and links.
this.updateParts();
this.network = null;
}
/**
* @ignore
* recursively position vertexes in a radial layout
* @this {RadialLayout}
* @param {RadialVertex} vert
* @param {number} layer
* @param {number} angle
* @param {number} sweep
*/
RadialLayout.prototype.radlay1 = function(vert, layer, angle, sweep) {
if (layer > this.maxLayers) return; // no need to position nodes outside of maxLayers
var verts = []; // array of all RadialVertexes connected to 'vert' in layer 'layer'
vert.vertexes.each(function(v) {
if (v.laid) return;
if (v.distance === layer) verts.push(v);
});
var found = verts.length;
if (found === 0) return;
var radius = layer * this.layerThickness;
var separator = sweep / found; // distance between nodes in their sweep portion
var start = angle - sweep / 2 + separator / 2;
// for each vertex in this layer, place it in its correct layer and position
for (var i = 0; i < found; i++) {
var v = verts[i];
var a = start + i * separator; // the angle to rotate the node to
if (a < 0) a += 360; else if (a > 360) a -= 360;
// the point to place the node at -- this corresponds with the layer the node is in
// all nodes in the same layer are placed at a constant point, then rotated accordingly
var p = new go.Point(radius, 0);
p.rotate(a);
v.centerX = p.x + this.arrangementOrigin.x;
v.centerY = p.y + this.arrangementOrigin.y;
v.laid = true;
v.angle = a;
v.sweep = separator;
v.radius = radius;
// keep going for all layers
this.radlay1(v, layer + 1, a, sweep / found);
}
};
/**
* @ignore
* Update RadialVertex.distance for every vertex.
* @this {RadialLayout}
* @param {RadialVertex} source
*/
RadialLayout.prototype.findDistances = function(source) {
var diagram = this.diagram;
// keep track of distances from the source node
this.network.vertexes.each(function(v) { v.distance = Infinity; });
// the source node starts with distance 0
source.distance = 0;
// keep track of nodes for we have set a non-Infinity distance,
// but which we have not yet finished examining
var seen = new go.Set(/*go.RadialVertex*/);
seen.add(source);
// local function for finding a vertex with the smallest distance in a given collection
function leastVertex(coll) {
var bestdist = Infinity;
var bestvert = null;
var it = coll.iterator;
while (it.next()) {
var v = it.value;
var dist = v.distance;
if (dist < bestdist) {
bestdist = dist;
bestvert = v;
}
}
return bestvert;
}
// keep track of vertexes we have finished examining;
// this avoids unnecessary traversals and helps keep the SEEN collection small
var finished = new go.Set(/*go.RadialVertex*/);
while (seen.count > 0) {
// look at the unfinished vertex with the shortest distance so far
var least = leastVertex(seen);
var leastdist = least.distance;
// by the end of this loop we will have finished examining this LEAST vertex
seen.remove(least);
finished.add(least);
// look at all edges connected with this vertex
least.edges.each(function(e) {
var neighbor = e.getOtherVertex(least);
// skip vertexes that we have finished
if (finished.contains(neighbor)) return;
var neighbordist = neighbor.distance;
// assume "distance" along a link is unitary, but could be any non-negative number.
var dist = leastdist + 1;
if (dist < neighbordist) {
// if haven't seen that vertex before, add it to the SEEN collection
if (neighbordist == Infinity) {
seen.add(neighbor);
}
// record the new best distance so far to that node
neighbor.distance = dist;
}
});
}
}
/**
* This override positions each Node and also calls {@link #rotateNode}.
* @this {RadialLayout}
*/
RadialLayout.prototype.commitLayout = function() {
go.Layout.prototype.commitLayout.call(this);
var it = this.network.vertexes.iterator;
while (it.next()) {
var v = it.value;
var n = v.node;
if (n !== null) {
n.visible = (v.distance <= this.maxLayers);
this.rotateNode(n, v.angle, v.sweep, v.radius);
}
}
this.commitLayers();
};
/**
* Override this method in order to modify each node as it is laid out.
* By default this method does nothing.
* @this {RadialLayout}
* @param {Node} node
* @param {number} angle in degrees relative to the center point
* @param {number} sweep in degrees
* @param {number} radius the inner radius for this node's layer
*/
RadialLayout.prototype.rotateNode = function(node, angle, sweep, radius) {
};
/**
* Override this method in order to create background circles indicating the layers of the radial layout.
* By default this method does nothing.
* @this {RadialLayout}
*/
RadialLayout.prototype.commitLayers = function() {
};
// end RadialLayout
/**
* @ignore
* @constructor
* @extends LayoutVertex
* @class
*/
function RadialVertex(network) {
go.LayoutVertex.call(this, network);
this.distance = Infinity; // number of layers from the root, non-negative integers
this.laid = false; // used internally to keep track
this.angle = 0; // the direction at which the node is placed relative to the root node
this.sweep = 0; // the angle subtended by the vertex
this.radius = 0; // the inner radius of the layer containing this vertex
}
go.Diagram.inherit(RadialVertex, go.LayoutVertex);
+102
View File
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html>
<head>
<title>Realtime Drag Selecting Tool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="This customized DragSelectingTool selects and deselects parts continuously while the user is dragging a box, rather than when the tool finishes." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="RealtimeDragSelectingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
initialDocumentSpot: go.Spot.Center,
initialViewportSpot: go.Spot.Center,
// replace the standard DragSelectingTool with one that selects while dragging,
// and also only requires overlapping bounds with the dragged box to be selected
dragSelectingTool:
$(RealtimeDragSelectingTool,
{ isPartialInclusion: true, delay: 50 },
{
box: $(go.Part, // replace the magenta box with a red one
{ layerName: "Tool", selectable: false },
$(go.Shape,
{
name: "SHAPE", fill: "rgba(255,0,0,0.1)",
stroke: "red", strokeWidth: 2
}))
}
),
// Define the template for Nodes, just some text inside a colored rectangle
nodeTemplate:
$(go.Node, "Spot",
{ width: 70, height: 20 },
$(go.Shape, "Rectangle",
new go.Binding("fill", "c")),
$(go.TextBlock,
{ margin: 2 },
new go.Binding("text", "c"))),
// Define the template for Links, just a simple line
linkTemplate:
$(go.Link,
$(go.Shape, { stroke: "black" })),
layout: $(go.TreeLayout)
});
myDiagram.model = loadTree();
}
function loadTree() {
// create some tree data
var total = 49;
var treedata = [];
for (var i = 0; i < total; i++) {
// these property names are also specified when creating the TreeModel
var d = {
key: i, // this node data's key
c: go.Brush.randomColor(), // the node's color
parent: (i > 0 ? Math.floor(Math.random() * i / 2) : undefined) // the random parent's key
};
treedata.push(d);
}
return new go.TreeModel(treedata);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%;height: 600px"></div>
<p>
This sample demonstrates the RealtimeDragSelectingTool, which replaces the standard <a>DragSelectingTool</a>.
Press in the background, wait briefly, and then drag to start selecting Nodes or Links that intersect with the box.
You can press or release Control (Command on Mac) or Shift while dragging to see how the selection changes.
</p>
<p>
Load it in your own app by including <a href="RealtimeDragSelectingTool.js">RealtimeDragSelectingTool.js</a>.
Initialize your Diagram by setting <a>ToolManager.dragSelectingTool</a> to a new instance of this tool.
For example:
</p>
<pre>
myDiagram.toolManager.dragSelectingTool = new RealtimeDragSelectingTool();
</pre>
or
<pre>
$(go.Diagram, { . . .,
"toolManager.dragSelectingTool": $(RealtimeDragSelectingTool, { isPartialInclusion: true }),
. . . })
</pre>
</div>
</body>
</html>
+126
View File
@@ -0,0 +1,126 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom DragSelectingTool for selecting and deselecting Parts during a drag.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends DragSelectingTool
* @class
* The RealtimeDragSelectingTool selects and deselects Parts within the {@link DragSelectingTool#box}
* during a drag, not just at the end of the drag.
*/
function RealtimeDragSelectingTool() {
go.DragSelectingTool.call(this);
this._originalSelection = null;
this._temporarySelection = null;
}
go.Diagram.inherit(RealtimeDragSelectingTool, go.DragSelectingTool);
/**
* Remember the original collection of selected Parts.
* @this {RealtimeDragSelectingTool}
*/
RealtimeDragSelectingTool.prototype.doActivate = function() {
go.DragSelectingTool.prototype.doActivate.call(this);
// keep a copy of the original Set of selected Parts
this._originalSelection = this.diagram.selection.copy();
// these Part.isSelected may have been temporarily modified
this._temporarySelection = new go.Set(/*go.Part*/);
this.diagram.raiseDiagramEvent("ChangingSelection");
};
/**
* Release any references to selected Parts.
* @this {RealtimeDragSelectingTool}
*/
RealtimeDragSelectingTool.prototype.doDeactivate = function() {
this.diagram.raiseDiagramEvent("ChangedSelection");
this._originalSelection = null;
this._temporarySelection = null;
go.DragSelectingTool.prototype.doDeactivate.call(this);
};
/**
* Restore the selection which may have been modified during a drag.
* @this {RealtimeDragSelectingTool}
*/
RealtimeDragSelectingTool.prototype.doCancel = function() {
var orig = this._originalSelection;
if (orig !== null) {
orig.each(function(p) { p.isSelected = true; });
this._temporarySelection.each(function(p) { if (!orig.contains(p)) p.isSelected = false; });
}
go.DragSelectingTool.prototype.doCancel.call(this);
};
/**
* @this {RealtimeDragSelectingTool}
*/
RealtimeDragSelectingTool.prototype.doMouseMove = function() {
if (this.isActive) {
go.DragSelectingTool.prototype.doMouseMove.call(this);
this.selectInRect(this.computeBoxBounds());
}
};
/**
* @this {RealtimeDragSelectingTool}
*/
RealtimeDragSelectingTool.prototype.doKeyDown = function() {
if (this.isActive) {
go.DragSelectingTool.prototype.doKeyDown.call(this);
this.selectInRect(this.computeBoxBounds());
}
};
/**
* @this {RealtimeDragSelectingTool}
*/
RealtimeDragSelectingTool.prototype.doKeyUp = function() {
if (this.isActive) {
go.DragSelectingTool.prototype.doKeyUp.call(this);
this.selectInRect(this.computeBoxBounds());
}
};
/**
* @expose
* @this {RealtimeDragSelectingTool}
* @param {Rect} r a rectangular bounds in document coordinates.
*/
RealtimeDragSelectingTool.prototype.selectInRect = function(r) {
var diagram = this.diagram;
var orig = this._originalSelection;
var temp = this._temporarySelection;
if (diagram === null || orig === null) return;
var e = diagram.lastInput;
var found = diagram.findPartsIn(r, this.isPartialInclusion, true, new go.Set(/*go.Part*/));
if (e.control || e.meta) { // toggle or deselect
if (e.shift) { // deselect only
temp.each(function(p) { if (!found.contains(p)) p.isSelected = orig.contains(p); });
found.each(function(p) { p.isSelected = false; temp.add(p); });
} else { // toggle selectedness of parts based on _originalSelection
temp.each(function(p) { if (!found.contains(p)) p.isSelected = orig.contains(p); });
found.each(function(p) { p.isSelected = !orig.contains(p); temp.add(p); });
}
} else if (e.shift) { // extend selection only
temp.each(function(p) { if (!found.contains(p)) p.isSelected = orig.contains(p); });
found.each(function(p) { p.isSelected = true; temp.add(p); });
} else { // select found parts, and unselect all other previously selected parts
temp.each(function(p) { if (!found.contains(p)) p.isSelected = false; });
orig.each(function(p) { if (!found.contains(p)) p.isSelected = false; });
found.each(function(p) { p.isSelected = true; temp.add(p); });
}
};
+68
View File
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<title>Rescaling GraphObjects using the RescalingTool</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A demonstration of the RescalingTool extension." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="RescalingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
layout: $(go.TreeLayout),
"undoManager.isEnabled": true // enable undo & redo
});
// install the RescalingTool as a mouse-down tool
myDiagram.toolManager.mouseDownTools.add(new RescalingTool());
myDiagram.nodeTemplate =
$(go.Node, "Auto",
{ locationSpot: go.Spot.Center },
new go.Binding("scale").makeTwoWay(),
$(go.Shape, "RoundedRectangle", { strokeWidth: 0 },
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 },
new go.Binding("text"))
);
// but use the default Link template, by not setting Diagram.linkTemplate
// create the model data that will be represented by Nodes and Links
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" }
],
[
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 3, to: 4 }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<p>
Selecting a node will show a rescaling handle that when dragged will modify the node's <a>GraphObject.scale</a> property.
</p>
<p>
Just as the <a>ResizingTool</a> changes the <a>GraphObject.desiredSize</a> of an object,
and just as the <a>RotatingTool</a> changes the <a>GraphObject.angle</a> of an object,
the <a>RescalingTool</a> changes the <a>GraphObject.scale</a> of an object.
</p>
</div>
</body>
</html>
+270
View File
@@ -0,0 +1,270 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool to change the scale of an object in a Part.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
* A custom tool for rescaling an object.
*
* Install the RescalingTool as a mouse-down tool by calling:
* myDiagram.toolManager.mouseDownTools.add(new RescalingTool());
*
* Note that there is no <code>Part.rescaleObjectName</code> property and there is no <code>Part.rescalable</code> property.
* So although you cannot customize any Node to affect this tool, you can set
* <a>RescalingTool.rescaleObjectName</a> and set <a>RescalingTool.isEnabled</a> to control
* whether objects are rescalable and when.
*/
function RescalingTool() {
go.Tool.call(this);
this.name = "Rescaling";
this._rescaleObjectName = "";
var h = new go.Shape();
h.desiredSize = new go.Size(8, 8);
h.fill = "lightblue";
h.stroke = "dodgerblue";
h.strokeWidth = 1;
h.cursor = "nwse-resize";
this._handleArchetype = h;
// internal state
this._adornedObject = null;
this._handle = null;
this.originalPoint = new go.Point();
this.originalTopLeft = new go.Point();
this.originalScale = 1.0;
}
go.Diagram.inherit(RescalingTool, go.Tool);
/**
* Gets the {@link GraphObject} that is being rescaled.
* This may be the same object as the selected {@link Part} or it may be contained within that Part.
*
* This property is also settable, but should only be set when overriding functions
* in RescalingTool, and not during normal operation.
*/
Object.defineProperty(RescalingTool.prototype, "adornedObject", {
get: function() { return this._adornedObject; },
set: function(val) { this._adornedObject = val; }
});
/**
* Gets or sets a small GraphObject that is copied as a rescale handle for the selected part.
* By default this is a {@link Shape} that is a small blue square.
* Setting this property does not raise any events.
*
* Here is an example of changing the default handle to be green "X":
* ```js
* tool.handleArchetype =
* $(go.Shape, "XLine",
* { width: 8, height: 8, stroke: "green", fill: "transparent" });
* ```
*/
Object.defineProperty(RescalingTool.prototype, "handleArchetype", {
get: function() { return this._handleArchetype; },
set: function(val) { this._handleArchetype = val; }
});
/**
* This property returns the {@link GraphObject} that is the tool handle being dragged by the user.
* This will be contained by an {@link Adornment} whose category is "RescalingTool".
* Its {@link Adornment#adornedObject} is the same as the {@link #adornedObject}.
*
* This property is also settable, but should only be set either within an override of {@link #doActivate}
* or prior to calling {@link #doActivate}.
*/
Object.defineProperty(RescalingTool.prototype, "handle", {
get: function() { return this._handle; },
set: function(val) { this._handle = val; }
});
/**
* This property returns the name of the GraphObject that identifies the object to be rescaled by this tool.
*
* The default value is the empty string, resulting in the whole Node being rescaled.
* This property is used by findRescaleObject when calling {@link Panel#findObject}.
*/
Object.defineProperty(RescalingTool.prototype, "rescaleObjectName", {
get: function() { return this._rescaleObjectName; },
set: function(val) { this._rescaleObjectName = val; }
});
/**
* @this {RescalingTool}
* @param {Part} part
*/
RescalingTool.prototype.updateAdornments = function(part) {
if (part === null || part instanceof go.Link) return;
if (part.isSelected && !this.diagram.isReadOnly) {
var rescaleObj = this.findRescaleObject(part);
if (rescaleObj !== null && part.actualBounds.isReal() && part.isVisible() &&
rescaleObj.actualBounds.isReal() && rescaleObj.isVisibleObject()) {
var adornment = part.findAdornment(this.name);
if (adornment === null || adornment.adornedObject !== rescaleObj) {
adornment = this.makeAdornment(rescaleObj);
}
if (adornment !== null) {
adornment.location = rescaleObj.getDocumentPoint(go.Spot.BottomRight);
part.addAdornment(this.name, adornment);
return;
}
}
}
part.removeAdornment(this.name);
}
/**
* @this {RescalingTool}
* @param {GraphObject} rescaleObj
* @return {Adornment}
*/
RescalingTool.prototype.makeAdornment = function(rescaleObj) {
var adornment = new go.Adornment();
adornment.type = go.Panel.Position;
adornment.locationSpot = go.Spot.Center;
adornment.add(this._handleArchetype.copy());
adornment.adornedObject = rescaleObj;
return adornment;
}
/**
* Return the GraphObject to be rescaled by the user.
* @this {RescalingTool}
* @return {GraphObject}
*/
RescalingTool.prototype.findRescaleObject = function(part) {
var obj = part.findObject(this.rescaleObjectName);
if (obj) return obj;
return part;
}
/**
* This tool can start running if the mouse-down happens on a "Rescaling" handle.
* @this {RescalingTool}
* @return {boolean}
*/
RescalingTool.prototype.canStart = function() {
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly) return false;
if (!diagram.lastInput.left) return false;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
return (h !== null);
}
/**
* Activating this tool remembers the {@link #handle} that was dragged,
* the {@link #adornedObject} that is being rescaled,
* starts a transaction, and captures the mouse.
* @this {RescalingTool}
*/
RescalingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
if (this._handle === null) return;
this._adornedObject = this._handle.part.adornedObject;
this.originalPoint = this._handle.getDocumentPoint(go.Spot.Center);
this.originalTopLeft = this._adornedObject.getDocumentPoint(go.Spot.TopLeft);
this.originalScale = this._adornedObject.scale;
diagram.isMouseCaptured = true;
diagram.delaysLayout = true;
this.startTransaction(this.name);
this.isActive = true;
}
/**
* Stop the current transaction, forget the {@link #handle} and {@link #adornedObject}, and release the mouse.
* @this {RescalingTool}
*/
RescalingTool.prototype.doDeactivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this.stopTransaction();
this._handle = null;
this._adornedObject = null;
diagram.isMouseCaptured = false;
this.isActive = false;
};
/**
* Restore the original {@link GraphObject#scale} of the adorned object.
* @this {RescalingTool}
*/
RescalingTool.prototype.doCancel = function() {
var diagram = this.diagram;
if (diagram !== null) diagram.delaysLayout = false;
this.scale(this.originalScale);
this.stopTool();
}
/**
* Call {@link #scale} with a new scale determined by the current mouse point.
* This determines the new scale by calling {@link #computeScale}.
* @this {RescalingTool}
*/
RescalingTool.prototype.doMouseMove = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newScale = this.computeScale(diagram.lastInput.documentPoint);
this.scale(newScale);
}
}
/**
* Call {@link #scale} with a new scale determined by the most recent mouse point,
* and commit the transaction.
* @this {RescalingTool}
*/
RescalingTool.prototype.doMouseUp = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
diagram.delaysLayout = false;
var newScale = this.computeScale(diagram.lastInput.documentPoint);
this.scale(newScale);
this.transactionResult = this.name;
}
this.stopTool();
}
/**
* Set the {@link GraphObject#scale} of the {@link #findRescaleObject}.
* @this {RescalingTool}
* @param {number} newScale
*/
RescalingTool.prototype.scale = function(newScale) {
if (this._adornedObject !== null) {
this._adornedObject.scale = newScale;
}
}
/**
* Compute the new scale given a point.
*
* This method is called by both {@link #doMouseMove} and {@link #doMouseUp}.
* This method may be overridden.
* Please read the Introduction page on <a href="../../intro/extensions.html">Extensions</a> for how to override methods and how to call this base method.
* @this {RescalingTool}
* @param {Point} newPoint in document coordinates
*/
RescalingTool.prototype.computeScale = function(newPoint) {
var scale = this.originalScale;
var origdist = Math.sqrt(this.originalPoint.distanceSquaredPoint(this.originalTopLeft));
var newdist = Math.sqrt(newPoint.distanceSquaredPoint(this.originalTopLeft));
return scale * (newdist/origdist);
};
+74
View File
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<title>Resize Multiple</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow the user to resize multiple nodes at once by using the ResizeMultipleTool extension." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="ResizeMultipleTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
resizingTool: new ResizeMultipleTool(), // defined in ResizeMultipleTool.js
"undoManager.isEnabled": true // enable undo & redo
});
// define a simple Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the TextBlock
{ resizable: true },
new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
// save the modified size in the model node data
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
$(go.Shape, "RoundedRectangle", { strokeWidth: 0 },
// Shape.fill is bound to Node.data.color
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 }, // some room around the text
// TextBlock.text is bound to Node.data.key
new go.Binding("text", "key"))
);
// but use the default Link template, by not setting Diagram.linkTemplate
// create the model data that will be represented by Nodes and Links
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" },
{ key: "Gamma", color: "lightgreen" },
{ key: "Delta", color: "pink" }
],
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Beta", to: "Beta" },
{ from: "Gamma", to: "Delta" },
{ from: "Delta", to: "Alpha" }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<!-- The DIV for the Diagram needs an explicit size or else we won't see anything.
Also add a border to help see the edges. -->
<div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:400px"></div>
<p>
This sample demonstrates a custom <a>ResizingTool</a> which allows the user to resize many selected objects at once.
It is defined in its own file, as <a href="ResizeMultipleTool.js">ResizeMultipleTool.js</a>.
</p>
<p>
Usage can also be seen in the <a href="FloorPlanEditor.html">Floor Plan Editor</a> sample.
</p>
</div>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends ResizingTool
* @class
* A custom tool for resizing multiple objects at once.
*/
function ResizeMultipleTool() {
go.ResizingTool.call(this);
this.name = "ResizeMultiple";
}
go.Diagram.inherit(ResizeMultipleTool, go.ResizingTool);
/**
* Overrides ResizingTool.resize to resize all selected objects to the same size.
* @this {ResizeMultipleTool}
* @param {Rect} newr the intended new rectangular bounds for each Part's {@link Part#resizeObject}.
*/
ResizeMultipleTool.prototype.resize = function(newr) {
var diagram = this.diagram;
if (diagram === null) return;
diagram.selection.each(function(part) {
if (part instanceof go.Link || part instanceof go.Group) return; // only Nodes and simple Parts
var obj = part.resizeObject;
// calculate new location
var pos = part.position.copy();
var angle = obj.getDocumentAngle();
var sc = obj.getDocumentScale();
var radAngle = Math.PI * angle / 180;
var angleCos = Math.cos(radAngle);
var angleSin = Math.sin(radAngle);
var deltaWidth = newr.width - obj.naturalBounds.width;
var deltaHeight = newr.height - obj.naturalBounds.height;
var angleRight = (angle > 270 || angle < 90) ? 1 : 0;
var angleBottom = (angle > 0 && angle < 180) ? 1 : 0;
var angleLeft = (angle > 90 && angle < 270) ? 1 : 0;
var angleTop = (angle > 180 && angle < 360) ? 1 : 0;
pos.x += sc * ((newr.x + deltaWidth * angleLeft) * angleCos - (newr.y + deltaHeight * angleBottom) * angleSin);
pos.y += sc * ((newr.x + deltaWidth * angleTop) * angleSin + (newr.y + deltaHeight * angleLeft) * angleCos);
obj.desiredSize = newr.size;
part.position = pos;
});
}
+224
View File
@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html>
<head>
<title>Simulating Input Events</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Test a diagram by simulating abstract input events." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="Robot.js"></script>
<script id="code">
var robot; // this global variable will hold an instance of the Robot class for myDiagram
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
function showProperties(e, obj) { // executed by ContextMenuButton
var node = obj.part.adornedPart;
var msg = "Context clicked: " + node.data.key + ". ";
msg += "Selection includes:";
myDiagram.selection.each(function(part) {
msg += " " + part.toString();
});
document.getElementById("myStatus").textContent = msg;
}
function nodeClicked(e, obj) { // executed by click and doubleclick handlers
var evt = e.copy();
var node = obj.part;
var type = evt.clickCount === 2 ? "Double-Clicked: " : "Clicked: ";
var msg = type + node.data.key + ". ";
document.getElementById("myStatus").textContent = msg;
}
myDiagram =
$(go.Diagram, "myDiagramDiv", // must name or refer to the DIV HTML element
{
nodeTemplate:
$(go.Node, "Auto",
{
click: nodeClicked,
doubleClick: nodeClicked,
contextMenu:
$("ContextMenu",
$("ContextMenuButton",
$(go.TextBlock, "Properties"),
{ click: showProperties })
)
},
$(go.Shape, "Rectangle",
{ fill: "lightgray" },
{ portId: "", fromLinkable: true, toLinkable: true, cursor: "pointer" }),
$(go.TextBlock,
{ margin: 3 },
new go.Binding("text", "key"))),
model: new go.GraphLinksModel([
{ key: "Lambda" },
{ key: "Mu" }
], [
{ from: "Lambda", to: "Mu" }
]),
"undoManager.isEnabled": true
});
// a shared Robot that can be used by all commands for this one Diagram
robot = new Robot(myDiagram); // defined in Robot.js
// initialize the Palette that is on the left side of the page
myPalette =
$(go.Palette, "myPaletteDiv", // must name or refer to the DIV HTML element
{
nodeTemplate: myDiagram.nodeTemplate,
model: new go.GraphLinksModel([ // specify the contents of the Palette
{ key: "Alpha" },
{ key: "Beta" },
{ key: "Gamma" },
{ key: "Delta" }
])
});
}
function dragFromPalette() {
// simulate a drag-and-drop between Diagrams:
var dragdrop = { sourceDiagram: myPalette, targetDiagram: myDiagram };
robot.mouseDown(5, 5, 0, dragdrop); // this should be where the Alpha node is in the source myPalette
robot.mouseMove(60, 60, 100, dragdrop);
robot.mouseUp(100, 100, 200, dragdrop); // this is where the node will be dropped in the target myDiagram
// If successful in dragging a node from the Palette into the Diagram,
// the DraggingTool will perform a transaction.
}
function copyNode() {
var alpha = myDiagram.findNodeForKey("Alpha");
if (alpha === null) return;
var loc = alpha.actualBounds.center;
var options = { control: true, alt: true };
// Simulate a mouse drag to move the Alpha node:
robot.mouseDown(loc.x, loc.y, 0, options);
robot.mouseMove(loc.x + 80, loc.y + 50, 50, options);
robot.mouseMove(loc.x + 20, loc.y + 100, 100, options);
robot.mouseUp(loc.x + 20, loc.y + 100, 150, options);
// If successful, will have made a copy of the "Alpha" node below it.
// Alternatively you could copy the Node using commands:
// myDiagram.commandHandler.copySelection();
// myDiagram.commandHandler.pasteSelection(new go.Point(loc.x+20, loc.y+100));
}
function dragSelectNodes() {
var alpha = myDiagram.findNodeForKey("Alpha");
if (alpha === null) return;
var alpha2 = myDiagram.findNodeForKey("Alpha2");
if (alpha2 === null) return;
var coll = new go.Set();
coll.add(alpha);
coll.add(alpha2);
var area = myDiagram.computePartsBounds(coll);
area.inflate(30, 30);
// Simulate dragging in the background around the two Alpha nodes.
// This uses timestamps to pretend to wait a while to avoid activating the PanningTool.
// Hopefully this mouse down does not hit any Part, but in the Diagram's background:
robot.mouseDown(area.x, area.y, 0);
// NOTE that this mouseMove timestamp needs to be > myDiagram.toolManager.dragSelectingTool.delay:
robot.mouseMove(area.centerX, area.centerY, 200);
robot.mouseUp(area.right, area.bottom, 250);
// Now should have selected both "Alpha" and "Alpha2" using the DragSelectingTool.
// Alternatively you could select the Nodes programmatically:
// alpha.isSelected = true;
// alpha2.isSelected = true;
}
function clickContextMenu() {
var alpha = myDiagram.findNodeForKey("Alpha");
if (alpha === null) return;
var loc = alpha.location;
// right click on Alpha
robot.mouseDown(loc.x + 10, loc.y + 10, 0, { right: true });
robot.mouseUp(loc.x + 10, loc.y + 10, 100, { right: true });
// Alternatively you could invoke the Show Context Menu command directly:
// myDiagram.commandHandler.showContextMenu(alpha);
// move mouse over first context menu button
robot.mouseMove(loc.x + 20, loc.y + 20, 200);
// and click that button
robot.mouseDown(loc.x + 20, loc.y + 20, 300);
robot.mouseUp(loc.x + 20, loc.y + 20, 350);
// This should have invoked the ContextMenuButton's click function, showProperties,
// which should have put a green message in the myStatus DIV.
}
function deleteSelection() {
// Simulate clicking the "Del" key:
robot.keyDown("Del");
robot.keyUp("Del");
// Now the selected Nodes are deleted.
// Alternatively you could invoke the Delete command directly:
// myDiagram.commandHandler.deleteSelection();
}
function clickLambda() {
var lambda = myDiagram.findNodeForKey("Lambda");
if (lambda === null) return;
var loc = lambda.location;
// click on Lambda
robot.mouseDown(loc.x + 10, loc.y + 10, 0, {});
robot.mouseUp(loc.x + 10, loc.y + 10, 100, {});
// Clicking is just a sequence of input events.
// There is no command in CommandHandler for such a basic gesture.
}
function doubleClickLambda() {
var lambda = myDiagram.findNodeForKey("Lambda");
if (lambda === null) return;
var loc = lambda.location;
// double-click on Lambda
robot.mouseDown(loc.x + 10, loc.y + 10, 0, {});
robot.mouseUp(loc.x + 10, loc.y + 10, 100, {});
robot.mouseDown(loc.x + 10, loc.y + 10, 200, { clickCount: 2 });
robot.mouseUp(loc.x + 10, loc.y + 10, 300, { clickCount: 2 });
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div style="width: 100%; display: flex; justify-content: space-between">
<div id="myPaletteDiv" style="width: 80px; height: 400px; margin-right: 2px; border: solid 1px black"></div>
<div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
</div>
<p>
To simulate mouse events the buttons below use the <b>Robot</b> class that is defined in <a href="Robot.js">Robot.js</a>.
</p>
<p>
Click these buttons in order from top to bottom:<br />
<button onclick="dragFromPalette()">Drag From Palette</button><br />
<button onclick="copyNode()">Copy Node</button><br />
<button onclick="dragSelectNodes()">Drag Select Nodes</button><br />
<button onclick="clickContextMenu()">Context Menu Click Alpha</button><br />
<button onclick="deleteSelection()">Delete</button><br />
</p>
<p>Clicking operations:<br />
<button onclick="clickLambda()">Click Lambda</button><br />
<button onclick="doubleClickLambda()">Double Click Lambda</button><br />
</p>
<p>
The <a>UndoManager</a> has been enabled in the main Diagram.
Give focus to the Diagram and you can undo everything and then redo everything to confirm what was executed by the Robot.
</p>
<div id="myStatus" style="color:green"></div>
</div>
</body>
</html>
+216
View File
@@ -0,0 +1,216 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A class for simulating mouse and keyboard input.
// As a special hack, this supports limited simulation of drag-and-drop between Diagrams,
// by setting on the EVENTPROPS argument of the mouseDown/mouseMove/mouseUp methods
// both the "sourceDiagram" and "targetDiagram" properties.
// Although InputEvent.targetDiagram is a real property,
// the "sourceDiagram" property is only used by these Robot methods.
var isMac = (this.navigator !== undefined && this.navigator.platform !== undefined && this.navigator.platform.toUpperCase().indexOf('MAC') >= 0);
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @class
* @param {Diagram=} dia the Diagram on which the Robot simulates input events
*/
function Robot(dia) {
if (dia === undefined) dia = null;
this.diagram = dia;
}
/**
* @ignore
* Transfer property settings from a JavaScript Object to an InputEvent.
* @this {Robot}
* @param {InputEvent} e
* @param {Object} props
*/
Robot.prototype.initializeEvent = function(e, props) {
if (!props) return;
for (var p in props) {
if (p !== "sourceDiagram") e[p] = props[p];
// If people write control: true, switch to alt: true on macs, for ctrl+copy
if (p === "control") {
if (isMac) {
e['alt'] = props[p];
} else {
e[p] = props[p];
}
}
}
};
/**
* Simulate a mouse down event.
* @this {Robot}
* @param {number} x the X-coordinate of the mouse point in document coordinates.
* @param {number} y the Y-coordinate of the mouse point in document coordinates.
* @param {number=} time the timestamp of the simulated event, in milliseconds; default zero
* @param {object=} eventprops an optional argument providing properties for the InputEvent.
*/
Robot.prototype.mouseDown = function(x, y, time, eventprops) {
if (typeof x !== "number" || typeof y !== "number") throw new Error("Robot.mouseDown first two args must be X,Y numbers");
if (time === undefined) time = 0;
var diagram = this.diagram;
if (eventprops && eventprops.sourceDiagram) diagram = eventprops.sourceDiagram;
if (!diagram.isEnabled) return;
var n = new go.InputEvent();
n.diagram = diagram;
n.documentPoint = new go.Point(x, y);
n.viewPoint = diagram.transformDocToView(n.documentPoint);
n.timestamp = time;
n.down = true;
this.initializeEvent(n, eventprops);
diagram.lastInput = n;
diagram.firstInput = n.copy();
diagram.currentTool.doMouseDown();
};
/**
* Simulate a mouse move event.
* @this {Robot}
* @param {number} x the X-coordinate of the mouse point in document coordinates.
* @param {number} y the Y-coordinate of the mouse point in document coordinates.
* @param {number=} time the timestamp of the simulated event, in milliseconds; default zero
* @param {object=} eventprops an optional argument providing properties for the InputEvent.
*/
Robot.prototype.mouseMove = function(x, y, time, eventprops) {
if (typeof x !== "number" || typeof y !== "number") throw new Error("Robot.mouseMove first two args must be X,Y numbers");
if (time === undefined) time = 0;
var diagram = this.diagram;
if (eventprops && eventprops.sourceDiagram) diagram = eventprops.sourceDiagram;
if (!diagram.isEnabled) return;
var n = new go.InputEvent();
n.diagram = diagram;
n.documentPoint = new go.Point(x, y);
n.viewPoint = diagram.transformDocToView(n.documentPoint);
n.timestamp = time;
this.initializeEvent(n, eventprops);
diagram.lastInput = n;
diagram.currentTool.doMouseMove();
};
/**
* Simulate a mouse up event.
* @this {Robot}
* @param {number} x the X-coordinate of the mouse point in document coordinates.
* @param {number} y the Y-coordinate of the mouse point in document coordinates.
* @param {number=} time the timestamp of the simulated event, in milliseconds; default zero
* @param {object=} eventprops an optional argument providing properties for the InputEvent.
*/
Robot.prototype.mouseUp = function(x, y, time, eventprops) {
if (typeof x !== "number" || typeof y !== "number") throw new Error("Robot.mouseUp first two args must be X,Y numbers");
if (time === undefined) time = 0;
var diagram = this.diagram;
if (eventprops && eventprops.sourceDiagram) diagram = eventprops.sourceDiagram;
if (!diagram.isEnabled) return;
var n = new go.InputEvent();
n.diagram = diagram;
n.documentPoint = new go.Point(x, y);
n.viewPoint = diagram.transformDocToView(n.documentPoint);
n.timestamp = time;
n.up = true;
if (diagram.firstInput.documentPoint.equals(n.documentPoint)) n.clickCount = 1; // at least??
this.initializeEvent(n, eventprops);
diagram.lastInput = n;
// if (diagram.simulatedMouseUp(null, (n as any).sourceDiagram, n.documentPoint, n.targetDiagram)) return;
diagram.currentTool.doMouseUp();
};
/**
* Simulate a mouse wheel event.
* @this {Robot}
* @param {number} delta non-zero turn
* @param {number=} time the timestamp of the simulated event, in milliseconds; default zero
* @param {object=} eventprops an optional argument providing properties for the InputEvent.
*/
Robot.prototype.mouseWheel = function(delta, time, eventprops) {
if (typeof delta !== "number") throw new Error("Robot.mouseWheel first arg must be DELTA number");
if (time === undefined) time = 0;
var diagram = this.diagram;
if (!diagram.isEnabled) return;
var n = diagram.lastInput.copy();
n.diagram = diagram;
n.delta = delta;
n.timestamp = time;
this.initializeEvent(n, eventprops);
diagram.lastInput = n;
diagram.currentTool.doMouseWheel();
};
/**
* Simulate a key down event.
* @this {Robot}
* @param {string|number} keyorcode
* @param {number=} time the timestamp of the simulated event, in milliseconds; default zero
* @param {object=} eventprops an optional argument providing properties for the InputEvent.
*/
Robot.prototype.keyDown = function(keyorcode, time, eventprops) {
if (typeof keyorcode !== "string" && typeof keyorcode !== "number") throw new Error("Robot.keyDown first arg must be a string or a number");
if (time === undefined) time = 0;
var diagram = this.diagram;
if (!diagram.isEnabled) return;
var n = diagram.lastInput.copy();
n.diagram = diagram;
if (typeof (keyorcode) === 'string') {
n.key = keyorcode;
} else if (typeof (keyorcode) === 'number') {
n.key = String.fromCharCode(keyorcode);
}
n.timestamp = time;
n.down = true;
this.initializeEvent(n, eventprops);
diagram.lastInput = n;
diagram.currentTool.doKeyDown();
};
/**
* Simulate a key up event.
* @this {Robot}
* @param {string|number} keyorcode
* @param {number=} time the timestamp of the simulated event, in milliseconds; default zero
* @param {object=} eventprops an optional argument providing properties for the InputEvent.
*/
Robot.prototype.keyUp = function(keyorcode, time, eventprops) {
if (typeof keyorcode !== "string" && typeof keyorcode !== "number") throw new Error("Robot.keyUp first arg must be a string or a number");
if (time === undefined) time = 0;
var diagram = this.diagram;
if (!diagram.isEnabled) return;
var n = diagram.lastInput.copy();
n.diagram = diagram;
if (typeof (keyorcode) === 'string') {
n.key = keyorcode;
} else if (typeof (keyorcode) === 'number') {
n.key = String.fromCharCode(keyorcode);
}
n.timestamp = time;
n.up = true;
this.initializeEvent(n, eventprops);
diagram.lastInput = n;
diagram.currentTool.doKeyUp();
};
+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Rotate Multiple</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow the user to rotate multiple nodes at the same time by using the RotateMultipleTool extension." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="RotateMultipleTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
rotatingTool: new RotateMultipleTool(), // defined in RotateMultipleTool.js
"undoManager.isEnabled": true // enable undo & redo
});
// define a simple Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the TextBlock
{ locationSpot: go.Spot.Center, rotatable: true },
new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("angle").makeTwoWay(), // save the modified Node.angle in the model data
$(go.Shape, "RoundedRectangle", { strokeWidth: 0 },
// Shape.fill is bound to Node.data.color
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8 }, // some room around the text
// TextBlock.text is bound to Node.data.key
new go.Binding("text", "key"))
);
// but use the default Link template, by not setting Diagram.linkTemplate
// create the model data that will be represented by Nodes and Links
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" },
{ key: "Gamma", color: "lightgreen" },
{ key: "Delta", color: "pink" }
],
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Beta", to: "Beta" },
{ from: "Gamma", to: "Delta" },
{ from: "Delta", to: "Alpha" }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<!-- The DIV for the Diagram needs an explicit size or else we won't see anything.
Also add a border to help see the edges. -->
<div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:400px"></div>
<p>
This sample demonstrates a custom <a>RotatingTool</a> which allows the user to rotate many selected objects at once.
It is defined in its own file, as <a href="RotateMultipleTool.js">RotateMultipleTool.js</a>.
</p>
<p>
Hold down the control key in order to rotate each selected node individually, rather than all of them collectively.
</p>
</div>
</body>
</html>
+167
View File
@@ -0,0 +1,167 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends RotatingTool
* @class
* A custom tool for rotating multiple objects at a time. When more than one
* part is selected, rotates all parts, revolving them about their collective center.
* If the control key is held down during rotation, rotates all parts individually.
* <p>
* Caution: this only works for Groups that do *not* have a Placeholder.
*/
function RotateMultipleTool() {
go.RotatingTool.call(this);
this.name = "RotateMultiple";
// holds references to all selected non-Link Parts and their offset & angles
this._initialInfo = null;
// initial angle when rotating as a whole
this._initialAngle = 0;
// rotation point of selection
this._centerPoint = null;
}
go.Diagram.inherit(RotateMultipleTool, go.RotatingTool);
/**
* Calls RotatingTool.doActivate, and then remembers the center point of the collection,
* and the initial distances and angles of selected parts to the center.
* @this {RotateMultipleTool}
*/
RotateMultipleTool.prototype.doActivate = function() {
go.RotatingTool.prototype.doActivate.call(this);
var diagram = this.diagram;
// center point of the collection
this._centerPoint = diagram.computePartsBounds(diagram.selection).center;
// remember the angle relative to the center point when rotating the whole collection
this._initialAngle = this._centerPoint.directionPoint(diagram.lastInput.documentPoint);
// remember initial angle and distance for each Part
var infos = new go.Map(/*go.Part, PartInfo*/);
var tool = this;
diagram.selection.each(function(part) {
tool.walkTree(part, infos);
});
this._initialInfo = infos;
}
/**
* @ignore
* @param {Part} part
* @param {Map} infos
*/
RotateMultipleTool.prototype.walkTree = function(part, infos) {
if (part === null || part instanceof go.Link) return;
// distance from _centerPoint to locationSpot of part
var dist = Math.sqrt(this._centerPoint.distanceSquaredPoint(part.location));
// calculate initial relative angle
var dir = this._centerPoint.directionPoint(part.location);
// saves part-angle combination in array
infos.add(part, new PartInfo(dir, dist, part.rotateObject.angle));
// recurse into Groups
if (part instanceof go.Group) {
var it = part.memberParts.iterator;
while (it.next()) this.walkTree(it.value, infos);
}
};
/**
* @ignore
* Internal class that remembers a Part's offset & angle.
*/
function PartInfo(placementAngle, distance, rotationAngle) {
this.placementAngle = placementAngle * (Math.PI / 180); // in radians
this.distance = distance;
this.rotationAngle = rotationAngle; // in degrees
}
/**
* Clean up any references to Parts.
* @this {RotateMultipleTool}
*/
RotateMultipleTool.prototype.doDeactivate = function() {
this._initialInfo = null;
go.RotatingTool.prototype.doDeactivate.call(this);
};
/**
* Overrides rotatingTool.rotate to rotate all selected objects about their collective center.
* When the control key is held down while rotating, all selected objects are rotated individually.
* @this {RotateMultipleTool}
* @param {number} newangle
*/
RotateMultipleTool.prototype.rotate = function(newangle) {
var diagram = this.diagram;
var e = diagram.lastInput;
// when rotating individual parts, remember the original angle difference
var angleDiff = newangle - this.adornedObject.part.rotateObject.angle;
var tool = this;
this._initialInfo.each(function(kvp) {
var part = kvp.key;
if (part instanceof go.Link) return; // only Nodes and simple Parts
var partInfo = kvp.value;
// rotate every selected non-Link Part
// find information about the part set in RotateMultipleTool.initialInformation
if (e.control || e.meta) {
if (tool.adornedObject.part === part) {
part.rotateObject.angle = newangle;
} else {
part.rotateObject.angle += angleDiff;
}
} else {
var radAngle = newangle * (Math.PI / 180); // converts the angle traveled from degrees to radians
// calculate the part's x-y location relative to the central rotation point
var offsetX = partInfo.distance * Math.cos(radAngle + partInfo.placementAngle);
var offsetY = partInfo.distance * Math.sin(radAngle + partInfo.placementAngle);
// move part
part.location = new go.Point(tool._centerPoint.x + offsetX, tool._centerPoint.y + offsetY);
// rotate part
part.rotateObject.angle = partInfo.rotationAngle + newangle;
}
});
}
/**
* This override needs to calculate the desired angle with different rotation points,
* depending on whether we are rotating the whole selection as one, or Parts individually.
* @this {RotateMultipleTool}
* @param {Point} newPoint in document coordinates
*/
RotateMultipleTool.prototype.computeRotate = function(newPoint) {
var diagram = this.diagram;
var angle;
var e = diagram.lastInput;
if (e.control || e.meta) { // relative to the center of the Node whose handle we are rotating
var part = this.adornedObject.part;
var rotationPoint = part.getDocumentPoint(part.locationSpot);
angle = rotationPoint.directionPoint(newPoint);
} else { // relative to the center of the whole selection
angle = this._centerPoint.directionPoint(newPoint) - this._initialAngle;
}
if (angle >= 360) angle -= 360;
else if (angle < 0) angle += 360;
var interval = Math.min(Math.abs(this.snapAngleMultiple), 180);
var epsilon = Math.min(Math.abs(this.snapAngleEpsilon), interval / 2);
// if it's close to a multiple of INTERVAL degrees, make it exactly so
if (!diagram.lastInput.shift && interval > 0 && epsilon > 0) {
if (angle % interval < epsilon) {
angle = Math.floor(angle / interval) * interval;
} else if (angle % interval > interval - epsilon) {
angle = (Math.floor(angle / interval) + 1) * interval;
}
if (angle >= 360) angle -= 360;
else if (angle < 0) angle += 360;
}
return angle;
};
+53
View File
@@ -0,0 +1,53 @@
'use strict';
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// This file holds the definitions of two useful figures: "RoundedTopRectangle" and "RoundedBottomRectangle".
// These are demonstrated at ../samples/twoHalves.html and ../samples/roundedGroups.html.
go.Shape.defineFigureGenerator("RoundedTopRectangle", function (shape, w, h) {
// this figure takes one parameter, the size of the corner
var p1 = 5; // default corner size
if (shape !== null) {
var param1 = shape.parameter1;
if (!isNaN(param1) && param1 >= 0) p1 = param1; // can't be negative or NaN
}
p1 = Math.min(p1, w / 2);
p1 = Math.min(p1, h / 2); // limit by whole height or by half height?
var geo = new go.Geometry();
// a single figure consisting of straight lines and quarter-circle arcs
geo.add(new go.PathFigure(0, p1)
.add(new go.PathSegment(go.PathSegment.Arc, 180, 90, p1, p1, p1, p1))
.add(new go.PathSegment(go.PathSegment.Line, w - p1, 0))
.add(new go.PathSegment(go.PathSegment.Arc, 270, 90, w - p1, p1, p1, p1))
.add(new go.PathSegment(go.PathSegment.Line, w, h))
.add(new go.PathSegment(go.PathSegment.Line, 0, h).close()));
// don't intersect with two top corners when used in an "Auto" Panel
geo.spot1 = new go.Spot(0, 0, 0.3 * p1, 0.3 * p1);
geo.spot2 = new go.Spot(1, 1, -0.3 * p1, 0);
return geo;
});
go.Shape.defineFigureGenerator("RoundedBottomRectangle", function (shape, w, h) {
// this figure takes one parameter, the size of the corner
var p1 = 5; // default corner size
if (shape !== null) {
var param1 = shape.parameter1;
if (!isNaN(param1) && param1 >= 0) p1 = param1; // can't be negative or NaN
}
p1 = Math.min(p1, w / 2);
p1 = Math.min(p1, h / 2); // limit by whole height or by half height?
var geo = new go.Geometry();
// a single figure consisting of straight lines and quarter-circle arcs
geo.add(new go.PathFigure(0, 0)
.add(new go.PathSegment(go.PathSegment.Line, w, 0))
.add(new go.PathSegment(go.PathSegment.Line, w, h - p1))
.add(new go.PathSegment(go.PathSegment.Arc, 0, 90, w - p1, h - p1, p1, p1))
.add(new go.PathSegment(go.PathSegment.Line, p1, h))
.add(new go.PathSegment(go.PathSegment.Arc, 90, 90, p1, h - p1, p1, p1).close()));
// don't intersect with two bottom corners when used in an "Auto" Panel
geo.spot1 = new go.Spot(0, 0, 0.3 * p1, 0);
geo.spot2 = new go.Spot(1, 1, -0.3 * p1, -0.3 * p1);
return geo;
});
+301
View File
@@ -0,0 +1,301 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Tool for resizing each row of a named Table Panel in a selected Part.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Tool
* @class
*/
function RowResizingTool() {
go.Tool.call(this);
this.name = "RowResizing";
var h = new go.Shape();
h.geometryString = "M0 0 H14 M0 2 H14";
h.desiredSize = new go.Size(14, 2);
h.cursor = "row-resize";
h.geometryStretch = go.GraphObject.None;
h.background = "rgba(255,255,255,0.5)";
h.stroke = "rgba(30,144,255,0.5)";
/** @type {GraphObject} */
this._handleArchetype = h;
/** @type {string} */
this._tableName = "TABLE";
// internal state
/** @type {GraphObject} */
this._handle = null;
/** @type {Panel} */
this._adornedTable = null;
}
go.Diagram.inherit(RowResizingTool, go.Tool);
/*
* A small GraphObject used as a resize handle for each row.
* This tool expects that this object's {@link GraphObject#desiredSize} (a.k.a width and height) has been set to real numbers.
* @name RowResizingTool#handleArchetype
* @return {GraphObject}
*/
Object.defineProperty(RowResizingTool.prototype, "handleArchetype", {
get: function() { return this._handleArchetype; },
set: function(value) { this._handleArchetype = value; }
});
/*
* The name of the Table Panel to be resized, by default the name "TABLE".
* @name RowResizingTool#tableName
* @return {string}
*/
Object.defineProperty(RowResizingTool.prototype, "tableName", {
get: function() { return this._tableName; },
set: function(value) { this._tableName = value; }
});
/*
* This read-only property returns the {@link GraphObject} that is the tool handle being dragged by the user.
* This will be contained by an {@link Adornment} whose category is "RowResizing".
* Its {@link Adornment#adornedObject} is the same as the {@link #adornedTable}.
* @name RowResizingTool#handle
* @return {GraphObject}
*/
Object.defineProperty(RowResizingTool.prototype, "handle", {
get: function() { return this._handle; }
});
/*
* Gets the {@link Panel} of type {@link Panel#Table} whose rows may be resized.
* This must be contained within the selected Part.
* @name RowResizingTool#adornedTable
* @return {Panel}
*/
Object.defineProperty(RowResizingTool.prototype, "adornedTable", {
get: function() { return this._adornedTable; }
});
/**
* Show an {@link Adornment} with a resize handle at each row.
* Don't show anything if {@link #tableName} doesn't identify a {@link Panel}
* that has a {@link Panel#type} of type {@link Panel#Table}.
* @this {RowResizingTool}
* @param {Part} part the part.
*/
RowResizingTool.prototype.updateAdornments = function(part) {
if (part === null || part instanceof go.Link) return; // this tool never applies to Links
if (part.isSelected && !this.diagram.isReadOnly) {
var selelt = part.findObject(this.tableName);
if (selelt instanceof go.Panel && selelt.actualBounds.isReal() && selelt.isVisibleObject() &&
part.actualBounds.isReal() && part.isVisible() &&
selelt.type === go.Panel.Table) {
var table = selelt;
var adornment = part.findAdornment(this.name);
if (adornment === null) {
adornment = this.makeAdornment(table);
part.addAdornment(this.name, adornment);
}
if (adornment !== null) {
var pad = table.padding;
var numrows = table.rowCount;
// update the position/alignment of each handle
adornment.elements.each(function(h) {
if (!h.pickable) return;
var rowdef = table.getRowDefinition(h.row);
var hgt = rowdef.actual;
if (hgt > 0) hgt = rowdef.total;
var sep = 0;
// find next non-zero-height row's separatorStrokeWidth
var idx = h.row + 1;
while (idx < numrows && table.getRowDefinition(idx).actual === 0) idx++;
if (idx < numrows) {
sep = table.getRowDefinition(idx).separatorStrokeWidth;
if (isNaN(sep)) sep = table.defaultRowSeparatorStrokeWidth;
}
h.alignment = new go.Spot(0, 0, pad.left + h.width/2, pad.top + rowdef.position + hgt + sep/2);
});
adornment.locationObject.desiredSize = table.actualBounds.size;
adornment.location = table.getDocumentPoint(adornment.locationSpot);
adornment.angle = table.getDocumentAngle();
return;
}
}
}
part.removeAdornment(this.name);
};
/*
* @this {RowResizingTool}
* @param {Panel} table the Table Panel whose rows may be resized
* @return {Adornment}
*/
RowResizingTool.prototype.makeAdornment = function(table) {
// the Adornment is a Spot Panel holding resize handles
var adornment = new go.Adornment();
adornment.category = this.name;
adornment.adornedObject = table;
adornment.type = go.Panel.Spot;
adornment.locationObjectName = "BLOCK";
// create the "main" element of the Spot Panel
var block = new go.TextBlock(); // doesn't matter much what this is
block.name = "BLOCK";
block.pickable = false; // it's transparent and not pickable
adornment.add(block);
// now add resize handles for each row
for (var i = 0; i < table.rowCount; i++) {
var rowdef = table.getRowDefinition(i);
adornment.add(this.makeHandle(table, rowdef));
}
return adornment;
};
/*
* @this {RowResizingTool}
* @param {Panel} table the Table Panel whose rows may be resized
* @param {RowRowDefinition} rowdef the row definition to be resized
* @return a copy of the {@link #handleArchetype}
*/
RowResizingTool.prototype.makeHandle = function(table, rowdef) {
var h = this.handleArchetype;
if (h === null) return null;
var c = h.copy();
c.row = rowdef.index;
return c;
};
/*
* This predicate is true when there is a resize handle at the mouse down point.
* @this {RowResizingTool}
* @return {boolean}
*/
RowResizingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly) return false;
if (!diagram.lastInput.left) return false;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
return (h !== null);
};
/**
* @this {RowResizingTool}
*/
RowResizingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
if (this.handle === null) return;
var panel = this.handle.part.adornedObject;
if (!panel || panel.type !== go.Panel.Table) return;
this._adornedTable = panel;
diagram.isMouseCaptured = true;
this.startTransaction(this.name);
this.isActive = true;
};
/**
* @this {RowResizingTool}
*/
RowResizingTool.prototype.doDeactivate = function() {
this.stopTransaction();
this._handle = null;
this._adornedTable = null;
var diagram = this.diagram;
if (diagram !== null) diagram.isMouseCaptured = false;
this.isActive = false;
};
/**
* @this {RowResizingTool}
*/
RowResizingTool.prototype.doMouseMove = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newpt = this.computeResize(diagram.lastInput.documentPoint);
this.resize(newpt);
}
};
/**
* @this {RowResizingTool}
*/
RowResizingTool.prototype.doMouseUp = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newpt = this.computeResize(diagram.lastInput.documentPoint);
this.resize(newpt);
this.transactionResult = this.name; // success
}
this.stopTool();
};
/**
* This should change the {@link RowRowDefinition#height} of the row being resized
* to a value corresponding to the given mouse point.
* @expose
* @this {RowResizingTool}
* @param {Point} newPoint the value of the call to {@link #computeResize}.
*/
RowResizingTool.prototype.resize = function(newPoint) {
var table = this.adornedTable;
var pad = table.padding;
var numrows = table.rowCount;
var locpt = table.getLocalPoint(newPoint);
var h = this.handle;
var rowdef = table.getRowDefinition(h.row);
var sep = 0;
var idx = h.row + 1;
while (idx < numrows && table.getRowDefinition(idx).actual === 0) idx++;
if (idx < numrows) {
sep = table.getRowDefinition(idx).separatorStrokeWidth;
if (isNaN(sep)) sep = table.defaultRowSeparatorStrokeWidth;
}
rowdef.height = Math.max(0, locpt.y - pad.top - rowdef.position - (rowdef.total - rowdef.actual) - sep/2);
};
/**
* This can be overridden in order to customize the resizing process.
* @expose
* @this {RowResizingTool}
* @param {Point} p the point where the handle is being dragged.
* @return {Point}
*/
RowResizingTool.prototype.computeResize = function(p) {
return p;
};
/**
* Pressing the Delete key removes any row width setting and stops this tool.
* @this {RowResizingTool}
*/
RowResizingTool.prototype.doKeyDown = function() {
if (!this.isActive) return;
var e = this.diagram.lastInput;
if (e.key === 'Del' || e.key === '\t') { // remove height setting
var rowdef = this.adornedTable.getRowDefinition(this.handle.row);
rowdef.height = NaN;
this.transactionResult = this.name; // success
this.stopTool();
} else {
go.Tool.prototype.doKeyDown.call(this);
}
};
+138
View File
@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html>
<head>
<title>Scrolling Table</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Allow users to scroll the items in a Table Panel." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="ScrollingTable.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"PartResized": function(e) {
var node = e.subject;
var scroller = node.findObject("SCROLLER");
if (scroller !== null) scroller._updateScrollBar(scroller.findObject("TABLE"));
}
});
myDiagram.nodeTemplate =
$(go.Node, "Vertical",
{
selectionObjectName: "SCROLLER",
resizable: true, resizeObjectName: "SCROLLER",
portSpreading: go.Node.SpreadingNone
},
new go.Binding("location").makeTwoWay(),
$(go.TextBlock,
{ font: "bold 14px sans-serif" },
new go.Binding("text", "key")),
$(go.Panel, "Auto",
$(go.Shape, { fill: "white" }),
$("ScrollingTable",
{
name: "SCROLLER",
desiredSize: new go.Size(NaN, 60), // fixed width
stretch: go.GraphObject.Fill, // but stretches vertically
defaultColumnSeparatorStroke: "gray",
defaultColumnSeparatorStrokeWidth: 0.5
},
new go.Binding("TABLE.itemArray", "items"),
new go.Binding("TABLE.column", "left", function(left) { return left ? 2 : 0; }),
new go.Binding("desiredSize", "size").makeTwoWay(),
{
"TABLE.itemTemplate":
$(go.Panel, "TableRow",
{
defaultStretch: go.GraphObject.Horizontal,
fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides,
fromLinkable: true, toLinkable: true
},
new go.Binding("portId", "name"),
$(go.TextBlock, { column: 0 }, new go.Binding("text", "name")),
$(go.TextBlock, { column: 1 }, new go.Binding("text", "value"))
),
"TABLE.defaultColumnSeparatorStroke": "gray",
"TABLE.defaultColumnSeparatorStrokeWidth": 0.5,
"TABLE.defaultRowSeparatorStroke": "gray",
"TABLE.defaultRowSeparatorStrokeWidth": 0.5,
"TABLE.defaultSeparatorPadding": new go.Margin(1, 3, 0, 3)
}
)
)
);
myDiagram.model = $(go.GraphLinksModel,
{
linkFromPortIdProperty: "fromPort",
linkToPortIdProperty: "toPort",
nodeDataArray: [
{
key: "Alpha", left: true, location: new go.Point(0, 0), size: new go.Size(100, 50),
items:
[
{ name: "A", value: 1 },
{ name: "B", value: 2 },
{ name: "C", value: 3 },
{ name: "D", value: 4 },
{ name: "E", value: 5 },
{ name: "F", value: 6 },
{ name: "G", value: 7 }
]
},
{
key: "Beta", location: new go.Point(150, 0),
items:
[
{ name: "Aa", value: 1 },
{ name: "Bb", value: 2 },
{ name: "Cc", value: 3 },
{ name: "Dd", value: 4 },
{ name: "Ee", value: 5 },
{ name: "Ff", value: 6 },
{ name: "Gg", value: 7 },
{ name: "Hh", value: 8 },
{ name: "Ii", value: 9 },
{ name: "Jj", value: 10 },
{ name: "Kk", value: 11 },
{ name: "Ll", value: 12 },
{ name: "Mm", value: 13 },
{ name: "Nn", value: 14 }
]
}
],
linkDataArray: [
{ from: "Alpha", fromPort: "D", to: "Beta", toPort: "Ff" },
{ from: "Alpha", fromPort: "A", to: "Beta", toPort: "Nn" },
{ from: "Alpha", fromPort: "G", to: "Beta", toPort: "Aa" }
]
});
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<p>
This makes use of the "ScrollingTable" Panel defined in <a href="ScrollingTable.js">ScrollingTable.js</a>.
The "AutoRepeatButton" Panel is also defined in that file.
Each node is resizable.
</p>
<p>
Note how links connect particular port elements on each node.
When an element has a <a>GraphObject.index</a> less than the <a>Panel.topIndex</a>,
the panel arranges it be zero sized at the top of the panel.
Similarly, elements beyond the last item in the panel are arranged to be at the end of the list,
which may be at the bottom of the panel.
</p>
</div>
</body>
</html>
+170
View File
@@ -0,0 +1,170 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A "ScrollingTable" Panel
// This also defines an "AutoRepeatButton" Panel,
// which is used by the scrollbar in the "ScrollingTable" Panel.
// This defines a custom "Button" that automatically repeats its click
// action when the user holds down the mouse.
// The first optional argument may be a number indicating the number of milliseconds
// to wait between calls to the click function. Default is 50.
// The second optional argument may be a number indicating the number of milliseconds
// to delay before starting calls to the click function. Default is 500.
// Example:
// $("AutoRepeatButton", 150, // slower than the default 50 milliseconds between calls
// {
// click: function(e, button) { doSomething(button.part); }
// },
// $(go.Shape, "Circle", { width: 8, height: 8 })
// )
go.GraphObject.defineBuilder("AutoRepeatButton", function(args) {
var repeat = go.GraphObject.takeBuilderArgument(args, 50, function(x) { return typeof x === "number"; });
var delay = go.GraphObject.takeBuilderArgument(args, 500, function(x) { return typeof x === "number"; });
var $ = go.GraphObject.make;
// some internal helper functions for auto-repeating
function delayClicking(e, obj) {
endClicking(e, obj);
if (obj.click) {
obj._timer =
setTimeout(function() { repeatClicking(e, obj); },
delay); // wait milliseconds before starting clicks
}
}
function repeatClicking(e, obj) {
if (obj._timer) clearTimeout(obj._timer);
if (obj.click) {
obj._timer =
setTimeout(function() {
if (obj.click) {
(obj.click)(e, obj);
repeatClicking(e, obj);
}
},
repeat); // milliseconds between clicks
}
}
function endClicking(e, obj) {
if (obj._timer) {
clearTimeout(obj._timer);
obj._timer = undefined;
}
}
return $("Button",
{
actionDown: delayClicking, actionUp: endClicking,
"ButtonBorder.figure": "Rectangle",
"ButtonBorder.fill": null,
"ButtonBorder.stroke": null,
"_buttonFillOver": "rgba(0, 0, 0, .25)",
"_buttonStrokeOver": null,
cursor: "auto"
});
});
// Create a scrolling Table Panel, whose name is given as the optional first argument.
// If not given the name defaults to "TABLE".
// Example use:
// $("ScrollingTable", "TABLE",
// new go.Binding("TABLE.itemArray", "someArrayProperty"),
// ...)
// Note that if you have more than one of these in a Part,
// you'll want to make sure each one has a unique name.
go.GraphObject.defineBuilder("ScrollingTable", function(args) {
var $ = go.GraphObject.make;
var tablename = go.GraphObject.takeBuilderArgument(args, "TABLE");
// an internal helper function for actually performing a scrolling operation
function incrTableIndex(obj, i) {
var diagram = obj.diagram;
var table = obj.panel.panel.panel.findObject(tablename);
if (i === +Infinity || i === -Infinity) { // page up or down
var tabh = table.actualBounds.height;
var rowh = table.elt(table.topIndex).actualBounds.height; // assume each row has same height?
if (i === +Infinity) {
i = Math.max(1, Math.ceil(tabh / rowh) - 1);
} else {
i = -Math.max(1, Math.ceil(tabh / rowh) - 1);
}
}
var idx = table.topIndex + i;
if (idx < 0) idx = 0;
else if (idx >= table.rowCount - 1) idx = table.rowCount - 1;
if (table.topIndex !== idx) {
if (diagram !== null) diagram.startTransaction("scroll");
table.topIndex = idx;
var node = table.part; // may need to reroute links if the table contains any ports
if (node instanceof go.Node) node.invalidateConnectedLinks();
updateScrollBar(table);
if (diagram !== null) diagram.commitTransaction("scroll");
}
}
function updateScrollBar(table) {
var bar = table.panel.elt(1); // the scrollbar is a sibling of the table
if (!bar) return;
var idx = table.topIndex;
var up = bar.findObject("UP");
if (up) up.opacity = (idx > 0) ? 1.0 : 0.3;
var down = bar.findObject("DOWN");
if (down) down.opacity = (idx < table.rowCount - 1) ? 1.0 : 0.3;
var tabh = bar.actualBounds.height;
var rowh = table.elt(idx).actualBounds.height; //?? assume each row has same height?
if (rowh === 0 && idx < table.rowCount-2) rowh = table.elt(idx + 1).actualBounds.height;
var numVisibleRows = Math.max(1, Math.ceil(tabh / rowh) - 1);
var needed = idx > 0 || idx + numVisibleRows <= table.rowCount;
bar.opacity = needed ? 1.0 : 0.0;
}
return $(go.Panel, "Table",
{
_updateScrollBar: updateScrollBar
},
// this actually holds the item elements
$(go.Panel, "Table",
{
name: tablename,
column: 0,
stretch: go.GraphObject.Fill,
background: "whitesmoke",
rowSizing: go.RowColumnDefinition.None,
defaultAlignment: go.Spot.Top
}),
// this is the scrollbar
$(go.RowColumnDefinition,
{ column: 1, sizing: go.RowColumnDefinition.None }),
$(go.Panel, "Table",
{ column: 1, stretch: go.GraphObject.Vertical, background: "#DDDDDD" },
// the scroll up button
$("AutoRepeatButton",
{
name: "UP",
row: 0,
alignment: go.Spot.Top,
click: function(e, obj) { incrTableIndex(obj, -1); }
},
$(go.Shape, "TriangleUp",
{ stroke: null, desiredSize: new go.Size(6, 6) })),
// (someday implement a thumb here and support dragging to scroll)
// the scroll down button
$("AutoRepeatButton",
{
name: "DOWN",
row: 2,
alignment: go.Spot.Bottom,
click: function(e, obj) { incrTableIndex(obj, +1); }
},
$(go.Shape, "TriangleDown",
{ stroke: null, desiredSize: new go.Size(6, 6) }))
)
);
});
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<title>Tool for Reshaping a Sector of a Circle</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A demonstration of the SectorReshapingTool extension." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="SectorReshapingTool.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"animationManager.isEnabled": false,
"undoManager.isEnabled": true
});
// install the SectorReshapingTool as a mouse-down tool
myDiagram.toolManager.mouseDownTools.add(new SectorReshapingTool());
function makeSector(data) { // Geometry converter for the node's "LAMP" Shape
var radius = SectorReshapingTool.getRadius(data);
var angle = SectorReshapingTool.getAngle(data);
var sweep = SectorReshapingTool.getSweep(data);
var start = new go.Point(radius, 0).rotate(angle);
// this is much more efficient than calling go.GraphObject.make:
var geo = new go.Geometry()
.add(new go.PathFigure(radius + start.x, radius + start.y) // start point
.add(new go.PathSegment(go.PathSegment.Arc,
angle, sweep, // angles
radius, radius, // center
radius, radius)) // radius
.add(new go.PathSegment(go.PathSegment.Line, radius, radius).close()))
.add(new go.PathFigure(0, 0)) // make sure the Geometry always includes the whole circle
.add(new go.PathFigure(2 * radius, 2 * radius)); // even if only a small sector is "lit"
return geo;
}
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{
locationSpot: go.Spot.Center, locationObjectName: "LAMP",
selectionObjectName: "LAMP", selectionAdorned: false
},
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// selecting a Node brings it forward in the z-order
new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
$(go.Panel, "Spot",
{ name: "LAMP" },
$(go.Shape, // arc
{ fill: "yellow", stroke: "lightgray", strokeWidth: 0.5 },
new go.Binding("geometry", "", makeSector)),
$(go.Shape, "Circle",
{ name: "SHAPE", width: 6, height: 6 })
),
$(go.TextBlock,
{
alignment: new go.Spot(0.5, 0.5, 0, 3), alignmentFocus: go.Spot.Top,
stroke: "blue", background: "rgba(255,255,255,0.3)"
},
new go.Binding("alignment", "spot", go.Spot.parse).makeTwoWay(go.Spot.stringify),
new go.Binding("text", "name"))
);
myDiagram.model = new go.GraphLinksModel([
{ name: "Alpha", radius: 70, sweep: 120 },
{ name: "Beta", radius: 70, sweep: 80, angle: 200 }
]);
myDiagram.commandHandler.selectAll(); // to show the tool handles
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<p>
Two of the handles permit changing the angles of the sector; one handle permits changing the radius of the sector.
</p>
<p>
Note that the <a>Geometry</a> returned by <code>makeSector</code> always returns a Geometry that
occupies the area that would be occupied by a full circle. That Geometry-creating function also
depends on three data properties, <code>radius</code>, <code>angle</code>, and <code>sweep</code>.
</p>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
"use strict"
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// The SectorReshapingTool shows three handles:
// two for changing the angles of the sides of the filled sector,
// and one for controlling the diameter of the sector.
// This depends on there being three data properties, "angle", "sweep", and "radius",
// that hold the needed information to be able to reproduce the sector.
/**
* This SectorReshapingTool class allows for the user to interactively modify the angles of a "pie"-shaped sector of a circle.
* When a node is selected, this shows two handles for changing the angles of the sides of the sector and one handle for changing the radius.
* @constructor
* @extends Tool
* @class
*/
function SectorReshapingTool() {
go.Tool.call(this);
this.name = "SectorReshaping";
this._handle = null;
this._originalRadius = 0;
this._originalAngle = 0;
this._originalSweep = 0;
}
go.Diagram.inherit(SectorReshapingTool, go.Tool);
// these are the names of the data properties to read and write
SectorReshapingTool.radiusProperty = "radius";
SectorReshapingTool.angleProperty = "angle";
SectorReshapingTool.sweepProperty = "sweep";
/**
* This tool can only start if Diagram.allowReshape is true and the mouse-down event
* is at a tool handle created by this tool.
* @this {SectorReshapingTool}
*/
SectorReshapingTool.prototype.canStart = function() {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly) return false;
if (!diagram.allowReshape) return false;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
return (h !== null);
};
/**
* If the Part is selected, show two angle-changing tool handles and one radius-changing tool handle.
* @this {SectorReshapingTool}
* @param {Part} part
*/
SectorReshapingTool.prototype.updateAdornments = function(part) {
var data = part.data;
if (part.isSelected && data !== null && this.diagram !== null && !this.diagram.isReadOnly) {
var ad = part.findAdornment(this.name);
if (ad === null) {
var $ = go.GraphObject.make;
ad =
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, "Diamond",
{ name: "RADIUS", fill: "lime", width: 10, height: 10, cursor: "move" },
new go.Binding("alignment", "", function(data) {
var angle = SectorReshapingTool.getAngle(data);
var sweep = SectorReshapingTool.getSweep(data);
var p = new go.Point(0.5, 0).rotate(angle + sweep / 2);
return new go.Spot(0.5 + p.x, 0.5 + p.y);
})),
$(go.Shape, "Circle",
{ name: "ANGLE", fill: "lime", width: 8, height: 8, cursor: "move" },
new go.Binding("alignment", "", function(data) {
var angle = SectorReshapingTool.getAngle(data);
var p = new go.Point(0.5, 0).rotate(angle);
return new go.Spot(0.5 + p.x, 0.5 + p.y);
})),
$(go.Shape, "Circle",
{ name: "SWEEP", fill: "lime", width: 8, height: 8, cursor: "move" },
new go.Binding("alignment", "", function(data) {
var angle = SectorReshapingTool.getAngle(data);
var sweep = SectorReshapingTool.getSweep(data);
var p = new go.Point(0.5, 0).rotate(angle + sweep);
return new go.Spot(0.5 + p.x, 0.5 + p.y);
}))
);
ad.adornedObject = part.locationObject;
part.addAdornment(this.name, ad);
} else {
ad.location = part.position;
var ns = part.naturalBounds;
ad.placeholder.desiredSize = new go.Size((ns.width) * part.scale, (ns.height) * part.scale);
ad.updateTargetBindings();
}
} else {
part.removeAdornment(this.name);
}
}
/**
* Remember the original angles and radius and start a transaction.
* @this {SectorReshapingTool}
*/
SectorReshapingTool.prototype.doActivate = function() {
var diagram = this.diagram;
if (diagram === null) return;
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
if (this._handle === null) return;
var part = this._handle.part.adornedPart;
if (part === null || part.data === null) return;
var data = part.data;
this._originalRadius = SectorReshapingTool.getRadius(data);
this._originalAngle = SectorReshapingTool.getAngle(data);
this._originalSweep = SectorReshapingTool.getSweep(data);
this.startTransaction(this.name);
this.isActive = true;
}
/**
* Stop the transaction.
* @this {SectorReshapingTool}
*/
SectorReshapingTool.prototype.doDeactivate = function() {
this.stopTransaction();
this._handle = null;
this.isActive = false;
};
/**
* Restore the original angles and radius and then stop this tool.
* @this {SectorReshapingTool}
*/
SectorReshapingTool.prototype.doCancel = function() {
if (this._handle !== null && this.diagram !== null) {
var part = this._handle.part.adornedPart;
if (part !== null) {
var model = this.diagram.model;
model.setDataProperty(part.data, SectorReshapingTool.radiusProperty, this._originalRadius);
model.setDataProperty(part.data, SectorReshapingTool.angleProperty, this._originalAngle);
model.setDataProperty(part.data, SectorReshapingTool.sweepProperty, this._originalSweep);
}
}
this.stopTool();
};
/**
* Depending on the current handle being dragged, update the "radius", the "angle", or the "sweep"
* properties on the model data.
* Those property names are currently parameterized as static members of SectorReshapingTool.
* @this {SectorReshapingTool}
*/
SectorReshapingTool.prototype.doMouseMove = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var h = this._handle;
var center = h.part.adornedObject.getDocumentPoint(go.Spot.Center);
var node = h.part.adornedPart;
var mouse = diagram.lastInput.documentPoint;
if (h.name === "RADIUS") {
var dst = Math.sqrt(center.distanceSquaredPoint(mouse));
diagram.model.setDataProperty(node.data, SectorReshapingTool.radiusProperty, dst);
} else if (h.name === "ANGLE") {
var dir = center.directionPoint(mouse);
diagram.model.setDataProperty(node.data, SectorReshapingTool.angleProperty, dir);
} else if (h.name === "SWEEP") {
var dir = center.directionPoint(mouse);
var ang = SectorReshapingTool.getAngle(node.data);
var swp = (dir - ang + 360) % 360;
if (swp > 359) swp = 360; // make it easier to get a full circle
diagram.model.setDataProperty(node.data, SectorReshapingTool.sweepProperty, swp);
}
}
};
/**
* Finish the transaction and stop the tool.
* @this {SectorReshapingTool}
*/
SectorReshapingTool.prototype.doMouseUp = function() {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
this.transactionResult = this.name; // successful finish
}
this.stopTool();
};
// static functions for getting data
SectorReshapingTool.getRadius = function(data) {
var radius = data[SectorReshapingTool.radiusProperty];
if (!(typeof radius === "number") || isNaN(radius) || radius <= 0) radius = 50;
return radius;
}
SectorReshapingTool.getAngle = function(data) {
var angle = data[SectorReshapingTool.angleProperty];
if (!(typeof angle === "number") || isNaN(angle)) angle = 0; else angle = angle % 360;
return angle;
}
SectorReshapingTool.getSweep = function(data) {
var sweep = data[SectorReshapingTool.sweepProperty];
if (!(typeof sweep === "number") || isNaN(sweep)) sweep = 360;
return sweep;
}
+82
View File
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<title>Serpentine Layout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Arrange a chain of nodes in rows, alternating directions, back and forth." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="SerpentineLayout.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
isTreePathToChildren: false, // links go from child to parent
layout: $(SerpentineLayout) // defined in SerpentineLayout.js
});
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
$(go.Shape, { figure: "RoundedRectangle", fill: "white" },
new go.Binding("fill", "color")),
$(go.TextBlock, { margin: 4 },
new go.Binding("text", "key")));
myDiagram.linkTemplate =
$(go.Link, go.Link.Orthogonal,
{ corner: 5 },
$(go.Shape),
$(go.Shape, { toArrow: "Standard" }));
var model = new go.TreeModel();
model.nodeParentKeyProperty = "next";
model.nodeDataArray = [
{ key: "Alpha", next: "Beta", color: "coral" },
{ key: "Beta", next: "Gamma", color: "tomato" },
{ key: "Gamma", next: "Delta", color: "goldenrod" },
{ key: "Delta", next: "Epsilon", color: "orange" },
{ key: "Epsilon", next: "Zeta", color: "coral" },
{ key: "Zeta", next: "Eta", color: "tomato" },
{ key: "Eta", next: "Theta", color: "goldenrod" },
{ key: "Theta", next: "Iota", color: "orange" },
{ key: "Iota", next: "Kappa", color: "coral" },
{ key: "Kappa", next: "Lambda", color: "tomato" },
{ key: "Lambda", next: "Mu", color: "goldenrod" },
{ key: "Mu", next: "Nu", color: "orange" },
{ key: "Nu", next: "Xi", color: "coral" },
{ key: "Xi", next: "Omicron", color: "tomato" },
{ key: "Omicron", next: "Pi", color: "goldenrod" },
{ key: "Pi", next: "Rho", color: "orange" },
{ key: "Rho", next: "Sigma", color: "coral" },
{ key: "Sigma", next: "Tau", color: "tomato" },
{ key: "Tau", next: "Upsilon", color: "goldenrod" },
{ key: "Upsilon", next: "Phi", color: "orange" },
{ key: "Phi", next: "Chi", color: "coral" },
{ key: "Chi", next: "Psi", color: "tomato" },
{ key: "Psi", next: "Omega", color: "goldenrod" },
{ key: "Omega", color: "orange" }
];
myDiagram.model = model;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:500px; min-width: 200px"></div>
<p>
This sample demonstrates a custom Layout, SerpentineLayout, which assumes the graph consists of a chain of nodes.
The layout is defined in its own file, as <a href="SerpentineLayout.js">SerpentineLayout.js</a>.
</p>
<p>
It also has <a>Layout.isViewportSized</a> set to true, so that resizing the Diagram DIV will automatically re-layout.
</p>
</div>
</body>
</html>
+187
View File
@@ -0,0 +1,187 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Layout that lays out a chain of nodes in a snake-like fashion
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Layout
* @class
* This layout assumes the graph is a chain of Nodes,
* positioning nodes in horizontal rows back and forth, alternating between left-to-right
* and right-to-left within the {@link #wrap} limit.
* {@link #spacing} controls the distance between nodes.
* <p/>
* When this layout is the Diagram.layout, it is automatically invalidated when the viewport changes size.
*/
function SerpentineLayout() {
go.Layout.call(this);
this.isViewportSized = true;
this._spacing = new go.Size(30, 30);
this._wrap = NaN;
}
go.Diagram.inherit(SerpentineLayout, go.Layout);
/**
* @ignore
* Copies properties to a cloned Layout.
* @this {SerpentineLayout}
* @param {Layout} copy
*/
SerpentineLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
copy._spacing = this._spacing;
copy._wrap = this._wrap;
};
/**
* This method actually positions all of the Nodes, assuming that the ordering of the nodes
* is given by a single link from one node to the next.
* This respects the {@link #spacing} and {@link #wrap} properties to affect the layout.
* @this {SerpentineLayout}
* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
*/
SerpentineLayout.prototype.doLayout = function(coll) {
var diagram = this.diagram;
coll = this.collectParts(coll);
var root = null;
// find a root node -- one without any incoming links
var it = coll.iterator;
while (it.next()) {
var n = it.value;
if (!(n instanceof go.Node)) continue;
if (root === null) root = n;
if (n.findLinksInto().count === 0) {
root = n;
break;
}
}
// couldn't find a root node
if (root === null) return;
var spacing = this.spacing;
// calculate the width at which we should start a new row
var wrap = this.wrap;
if (diagram !== null && isNaN(wrap)) {
if (this.group === null) { // for a top-level layout, use the Diagram.viewportBounds
var pad = diagram.padding;
wrap = Math.max(spacing.width * 2, diagram.viewportBounds.width - 24 - pad.left - pad.right);
} else {
wrap = 1000; // provide a better default value?
}
}
// implementations of doLayout that do not make use of a LayoutNetwork
// need to perform their own transactions
if (diagram !== null) diagram.startTransaction("Serpentine Layout");
// start on the left, at Layout.arrangementOrigin
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
var x = this.arrangementOrigin.x;
var rowh = 0;
var y = this.arrangementOrigin.y;
var increasing = true;
var node = root;
while (node !== null) {
var b = this.getLayoutBounds(node);
// get the next node, if any
var nextlink = node.findLinksOutOf().first();
var nextnode = (nextlink !== null ? nextlink.toNode : null);
var nb = (nextnode !== null ? this.getLayoutBounds(nextnode) : new go.Rect());
if (increasing) {
node.move(new go.Point(x, y));
x += b.width;
rowh = Math.max(rowh, b.height);
if (x + spacing.width + nb.width > wrap) {
y += rowh + spacing.height;
x = wrap - spacing.width;
rowh = 0;
increasing = false;
if (nextlink !== null) {
nextlink.fromSpot = go.Spot.Right;
nextlink.toSpot = go.Spot.Right;
}
} else {
x += spacing.width;
if (nextlink !== null) {
nextlink.fromSpot = go.Spot.Right;
nextlink.toSpot = go.Spot.Left;
}
}
} else {
x -= b.width;
node.move(new go.Point(x, y));
rowh = Math.max(rowh, b.height);
if (x - spacing.width - nb.width < 0) {
y += rowh + spacing.height;
x = 0;
rowh = 0;
increasing = true;
if (nextlink !== null) {
nextlink.fromSpot = go.Spot.Left;
nextlink.toSpot = go.Spot.Left;
}
} else {
x -= spacing.width;
if (nextlink !== null) {
nextlink.fromSpot = go.Spot.Left;
nextlink.toSpot = go.Spot.Right;
}
}
}
node = nextnode;
}
if (diagram !== null) diagram.commitTransaction("Serpentine Layout");
};
// Public properties
/**
* Gets or sets the {@link Size} whose width specifies the horizontal space between nodes
* and whose height specifies the minimum vertical space between nodes.
* The default value is 30x30.
* @name SerpentineLayout#spacing
* @return {Size}
*/
Object.defineProperty(SerpentineLayout.prototype, "spacing", {
get: function() { return this._spacing; },
set: function(val) {
if (!(val instanceof go.Size)) throw new Error("new value for SerpentineLayout.spacing must be a Size, not: " + val);
if (!this._spacing.equals(val)) {
this._spacing = val;
this.invalidateLayout();
}
}
});
/**
* Gets or sets the total width of the layout.
* The default value is NaN, which for {@link Diagram#layout}s means that it uses
* the {@link Diagram#viewportBounds}.
* @name SerpentineLayout#wrap
* @return {number}
*/
Object.defineProperty(SerpentineLayout.prototype, "wrap", {
get: function() { return this._wrap; },
set: function(val) {
if (this._wrap !== val) {
this._wrap = val;
this.invalidateLayout();
}
}
});
+237
View File
@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html>
<head>
<title>Snap Link Reshaping</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="When reshaping an orthogonal link, make sure the points are moved onto a grid." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="Figures.js"></script>
<script src="SnapLinkReshapingTool.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // must name or refer to the DIV HTML element
{
// supply a simple narrow grid that manually reshaped link routes will follow
grid: $(go.Panel, "Grid",
{ gridCellSize: new go.Size(8, 8) },
$(go.Shape, "LineH", { stroke: "lightgray", strokeWidth: 0.5 }),
$(go.Shape, "LineV", { stroke: "lightgray", strokeWidth: 0.5 })
),
"draggingTool.isGridSnapEnabled": true,
linkReshapingTool: $(SnapLinkReshapingTool),
// when the user reshapes a Link, change its Link.routing from AvoidsNodes to Orthogonal,
// so that combined with Link.adjusting == End the link will retain its reshaped mid points
// even after nodes are moved
"LinkReshaped": function(e) { e.subject.routing = go.Link.Orthogonal; },
"animationManager.isEnabled": false,
"undoManager.isEnabled": true
});
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("SaveButton");
if (button) button.disabled = !myDiagram.isModified;
var idx = document.title.indexOf("*");
if (myDiagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
// Define a function for creating a "port" that is normally transparent.
// The "name" is used as the GraphObject.portId, the "spot" is used to control how links connect
// and where the port is positioned on the node, and the boolean "output" and "input" arguments
// control whether the user can draw links from or to the port.
function makePort(name, spot, output, input) {
// the port is basically just a small transparent square
return $(go.Shape, "Circle",
{
fill: null, // not seen, by default; set to a translucent gray by showSmallPorts, defined below
stroke: null,
desiredSize: new go.Size(7, 7),
alignment: spot, // align the port on the main Shape
alignmentFocus: spot, // just inside the Shape
portId: name, // declare this object to be a "port"
fromSpot: spot, toSpot: spot, // declare where links may connect at this port
fromLinkable: output, toLinkable: input, // declare whether the user may draw links to/from here
cursor: "pointer" // show a different cursor to indicate potential link point
});
}
myDiagram.nodeTemplate =
$(go.Node, "Spot",
{ locationSpot: go.Spot.Center },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
{ selectable: true },
{ resizable: true, resizeObjectName: "PANEL" },
// the main object is a Panel that surrounds a TextBlock with a Shape
$(go.Panel, "Auto",
{ name: "PANEL" },
new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
$(go.Shape, "Rectangle", // default figure
{
portId: "", // the default port: if no spot on link data, use closest side
fromLinkable: true, toLinkable: true, cursor: "pointer",
fill: "white" // default color
},
new go.Binding("figure"),
new go.Binding("fill")),
$(go.TextBlock,
{
font: "bold 11pt Helvetica, Arial, sans-serif",
margin: 8,
maxSize: new go.Size(160, NaN),
wrap: go.TextBlock.WrapFit,
editable: true
},
new go.Binding("text").makeTwoWay())
),
// four small named ports, one on each side:
makePort("T", go.Spot.Top, false, true),
makePort("L", go.Spot.Left, true, true),
makePort("R", go.Spot.Right, true, true),
makePort("B", go.Spot.Bottom, true, false),
{ // handle mouse enter/leave events to show/hide the ports
mouseEnter: function(e, node) { showSmallPorts(node, true); },
mouseLeave: function(e, node) { showSmallPorts(node, false); }
}
);
function showSmallPorts(node, show) {
node.ports.each(function(port) {
if (port.portId !== "") { // don't change the default port, which is the big shape
port.fill = show ? "rgba(0,0,0,.3)" : null;
}
});
}
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{ relinkableFrom: true, relinkableTo: true, reshapable: true, resegmentable: true },
{
routing: go.Link.AvoidsNodes, // but this is changed to go.Link.Orthgonal when the Link is reshaped
adjusting: go.Link.End,
curve: go.Link.JumpOver,
corner: 5,
toShortLength: 4
},
new go.Binding("points").makeTwoWay(),
// remember the Link.routing too
new go.Binding("routing", "routing", go.Binding.parseEnum(go.Link, go.Link.AvoidsNodes))
.makeTwoWay(go.Binding.toString),
$(go.Shape, // the link path shape
{ isPanelMain: true, strokeWidth: 2 }),
$(go.Shape, // the arrowhead
{ toArrow: "Standard", stroke: null })
);
load(); // load an initial diagram from some JSON text
var link = myDiagram.links.first();
if (link) link.isSelected = true;
// initialize the Palette that is on the left side of the page
myPalette =
$(go.Palette, "myPaletteDiv", // must name or refer to the DIV HTML element
{
maxSelectionCount: 1,
nodeTemplateMap: myDiagram.nodeTemplateMap, // share the templates used by myDiagram
model: new go.GraphLinksModel([ // specify the contents of the Palette
{ text: "Start", figure: "Circle", fill: "green" },
{ text: "Step" },
{ text: "DB", figure: "Database", fill: "lightgray" },
{ text: "???", figure: "Diamond", fill: "lightskyblue" },
{ text: "End", figure: "Circle", fill: "red" },
{ text: "Comment", figure: "RoundedRectangle", fill: "lightyellow" }
])
});
document.getElementById("AvoidsNodesCheckBox").onclick = function(e) {
myDiagram.toolManager.linkReshapingTool.avoidsNodes = e.target.checked;
}
}
// Show the diagram's model in JSON format that the user may edit
function save() {
saveDiagramProperties(); // do this first, before writing to JSON
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
loadDiagramProperties();
}
function saveDiagramProperties() {
myDiagram.model.modelData.position = go.Point.stringify(myDiagram.position);
}
// Called by "InitialLayoutCompleted" DiagramEvent listener, NOT directly by load()!
function loadDiagramProperties(e) {
// set Diagram.initialPosition, not Diagram.position, to handle initialization side-effects
var pos = myDiagram.model.modelData.position;
if (pos) myDiagram.initialPosition = go.Point.parse(pos);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div style="width: 100%; display: flex; justify-content: space-between">
<div id="myPaletteDiv" style="width: 105px; height: 620px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
<div id="myDiagramDiv" style="flex-grow: 1; height: 620px; border: solid 1px black"></div>
</div>
<label><input type="checkbox" id="AvoidsNodesCheckBox" checked="checked"/>avoidsNodes</label>
<p>
This sample is a simplified version of the <a href="../samples/draggableLink.html">Draggable Link</a> sample.
Links are not draggable, there are no custom <a>Adornment</a>s, nodes are not rotatable, and links
do not have text labels.
</p>
<p>
Its purpose is to demonstrate the <a href="SnapLinkReshapingTool.js">SnapLinkReshapingTool</a>,
an extension of <a>LinkReshapingTool</a> that snaps each dragged reshape handle of selected Links to
the nearest grid point. If the <a>SnapLinkReshapingTool.avoidsNodes</a> option is true,
as it is by default, then the reshaping is limited to points where the adjacent segments would not
be crossing over any avoidable nodes.
</p>
<p>
Note how the "LinkReshaped" DiagramEvent listener changes the <a>Link.routing</a> of the reshaped Link,
so that it is no longer AvoidsNodes routing but simple Orthogonal routing.
This combined with <a>Link.adjusting</a> being End permits the middle points of the link route to be
retained even after the user moves or resizes nodes.
Furthermore there is a TwoWay <a>Binding</a> on <a>Link.routing</a>, so that the model remembers
whether the link route had ever been reshaped manually.
</p>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
<textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
"linkFromPortIdProperty": "fromPort",
"linkToPortIdProperty": "toPort",
"modelData": {"position":"0 0"},
"nodeDataArray": [
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-3, "loc":"184 176"},
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-2, "loc":"248 248"},
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-4, "loc":"424 192"},
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-5, "loc":"320 152"},
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-6, "loc":"424 320"},
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-7, "loc":"352 256"},
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-8, "loc":"176 296"},
{"text":"DB", "figure":"Database", "fill":"lightgray", "key":-9, "loc":"288 344"},
{"text":"Step", "key":-10, "loc":"96 240"},
{"text":"Step", "key":-11, "loc":"536 280"}
],
"linkDataArray": [
{"from":-10, "to":-11, "fromPort":"R", "toPort":"L", "points":[121,240,131,240,132,240,132,240,216,240,216,176,264,176,264,104,392,104,392,240,480,240,480,280,501,280,511,280], "routing":"Orthogonal"}
]}
</textarea>
</div>
</body>
</html>
+211
View File
@@ -0,0 +1,211 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom LinkReshapingTool that snaps dragged reshaping handles to grid points.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends LinkReshapingTool
* @class
* This SnapLinkReshapingTool class supports snapping reshaping handles to go to the nearest grid point.
* If {@link #avoidsNodes} is true and the link is orthogonal,
* it also avoids reshaping the link so that any adjacent segments cross over any avoidable nodes.
*/
function SnapLinkReshapingTool() {
go.LinkReshapingTool.call(this);
/** @type {Size} */
this._gridCellSize = new go.Size(NaN, NaN);
/** @type {Point} */
this._gridOrigin = new go.Point(NaN, NaN);
/** @type {boolean} */
this._isGridSnapEnabled = true;
/** @type {boolean} */
this._avoidsNodes = true;
// internal state
this._safePoint = new go.Point(NaN, NaN);
this._prevSegHoriz = false;
this._nextSegHoriz = false;
}
go.Diagram.inherit(SnapLinkReshapingTool, go.LinkReshapingTool);
/**
* Gets or sets the {@link Size} of each grid cell to which link points will be snapped.
* The default value is NaNxNaN, which means use the {@link Diagram#grid}'s {@link Panel#gridCellSize}.
* @name SnapLinkReshapingTool#gridCellSize
* @return {Size}
*/
Object.defineProperty(SnapLinkReshapingTool.prototype, "gridCellSize", {
get: function() { return this._gridCellSize; },
set: function(val) {
if (!(val instanceof go.Size)) throw new Error("new value for SnapLinkReshapingTool.gridCellSize must be a Size, not: " + val);
this._gridCellSize = val.copy();
}
});
/**
* Gets or sets the {@link Point} origin for the grid to which link points will be snapped.
* The default value is NaN,NaN, which means use the {@link Diagram#grid}'s {@link Panel#gridOrigin}.
* @name SnapLinkReshapingTool#gridOrigin
* @return {Point}
*/
Object.defineProperty(SnapLinkReshapingTool.prototype, "gridOrigin", {
get: function() { return this._gridOrigin; },
set: function(val) {
if (!(val instanceof go.Point)) throw new Error("new value for SnapLinkReshapingTool.gridOrigin must be a Point, not: " + val);
this._gridOrigin = val.copy();
}
});
/**
* Gets or sets whether a reshape handle's position should be snapped to a grid point.
* The default value is true.
* This affects the behavior of {@link #computeReshape}.
*/
Object.defineProperty(SnapLinkReshapingTool.prototype, "isGridSnapEnabled", {
get: function() { return this._isGridSnapEnabled; },
set: function(val) {
if (typeof val !== "boolean") throw new Error("new value for SnapLinkReshapingTool.isGridSnapEnabled must be a boolean, not: " + val);
this._isGridSnapEnabled = val;
}
});
/**
* Gets or sets whether a reshape handle's position should only be dragged where the
* adjacent segments do not cross over any nodes.
* The default value is true.
* This affects the behavior of {@link #computeReshape}.
*/
Object.defineProperty(SnapLinkReshapingTool.prototype, "avoidsNodes", {
get: function() { return this._avoidsNodes; },
set: function(val) {
if (typeof val !== "boolean") throw new Error("new value for SnapLinkReshapingTool.avoidsNodes must be a boolean, not: " + val);
this._avoidsNodes = val;
}
});
/**
* This override records information about the original point of the handle being dragged,
* if the {@link #adornedLink} is Orthogonal and if {@link #avoidsNodes} is true.
*/
SnapLinkReshapingTool.prototype.doActivate = function() {
go.LinkReshapingTool.prototype.doActivate.call(this);
if (this.isActive && this.avoidsNodes && this.adornedLink.isOrthogonal) {
// assume the Link's route starts off correctly avoiding all nodes
this._safePoint = this.diagram.lastInput.documentPoint.copy();
var link = this.adornedLink;
var idx = this.handle.segmentIndex;
this._prevSegHoriz = Math.abs(link.getPoint(idx-1).y - link.getPoint(idx).y) < 0.5;
this._nextSegHoriz = Math.abs(link.getPoint(idx+1).y - link.getPoint(idx).y) < 0.5;
}
};
/**
* Pretend while dragging a reshape handle the mouse point is at the nearest grid point,
* if {@link #isGridSnapEnabled} is true.
* This uses {@link #gridCellSize} and {@link #gridOrigin}, unless those are not real values,
* in which case this uses the {@link Diagram#grid}'s {@link Panel#gridCellSize} and {@link Panel#gridOrigin}.
*
* If {@link #avoidsNodes} is true and the adorned Link is {@link Link#isOrthogonal},
* this method also avoids returning a Point that causes the adjacent segments, both before and after
* the current handle's index, to cross over any Nodes that are {@link Node#avoidable}.
* @this {SnapLinkReshapingTool}
* @param {Point} p
* @return {Point}
*/
SnapLinkReshapingTool.prototype.computeReshape = function(p) {
var pt = p;
if (this.isGridSnapEnabled) {
// first, find the grid to which we should snap
var cell = this.gridCellSize;
var orig = this.gridOrigin;
if (!cell.isReal() || cell.width === 0 || cell.height === 0) cell = this.diagram.grid.gridCellSize;
if (!orig.isReal()) orig = this.diagram.grid.gridOrigin;
// second, compute the closest grid point
pt = p.copy().snapToGrid(orig.x, orig.y, cell.width, cell.height);
}
if (this.avoidsNodes && this.adornedLink.isOrthogonal) {
if (this._checkSegmentsOverlap(pt)) {
this._safePoint = pt.copy();
} else {
pt = this._safePoint.copy();
}
}
// then do whatever LinkReshapingTool would normally do as if the mouse were at that point
return go.LinkReshapingTool.prototype.computeReshape.call(this, pt);
};
/**
* @hidden @internal
* Internal method for seeing whether a moved handle will cause any
* adjacent orthogonal segments to cross over any avoidable nodes.
* Returns true if everything would be OK.
*/
SnapLinkReshapingTool.prototype._checkSegmentsOverlap = function(pt) {
var index = this.handle.segmentIndex;
if (index >= 1) {
var p1 = this.adornedLink.getPoint(index-1);
var r = new go.Rect(pt.x, pt.y, 0, 0);
var q1 = p1.copy();
if (this._prevSegHoriz) {
q1.y = pt.y;
} else {
q1.x = pt.x;
}
r.unionPoint(q1);
var overlaps = this.diagram.findPartsIn(r, true, false);
if (overlaps.any(function(p) { return p instanceof go.Node && p.avoidable; })) return false;
if (index >= 2) {
var p0 = this.adornedLink.getPoint(index-2);
var r = new go.Rect(q1.x, q1.y, 0, 0);
if (this._prevSegHoriz) {
r.unionPoint(new go.Point(q1.x, p0.y));
} else {
r.unionPoint(new go.Point(p0.x, q1.y));
}
var overlaps = this.diagram.findPartsIn(r, true, false);
if (overlaps.any(function(p) { return p instanceof go.Node && p.avoidable; })) return false;
}
}
if (index < this.adornedLink.pointsCount-1) {
var p2 = this.adornedLink.getPoint(index+1);
var r = new go.Rect(pt.x, pt.y, 0, 0);
var q2 = p2.copy();
if (this._nextSegHoriz) {
q2.y = pt.y;
} else {
q2.x = pt.x;
}
r.unionPoint(q2);
var overlaps = this.diagram.findPartsIn(r, true, false);
if (overlaps.any(function(p) { return p instanceof go.Node && p.avoidable; })) return false;
if (index < this.adornedLink.pointsCount-2) {
var p3 = this.adornedLink.getPoint(index+2);
var r = new go.Rect(q2.x, q2.y, 0, 0);
if (this._nextSegHoriz) {
r.unionPoint(new go.Point(q2.x, p3.y));
} else {
r.unionPoint(new go.Point(p3.x, q2.y));
}
var overlaps = this.diagram.findPartsIn(r, true, false);
if (overlaps.any(function(p) { return p instanceof go.Node && p.avoidable; })) return false;
}
}
return true;
};
+104
View File
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<title>Spiral Layout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A custom layout that arranges a chain of nodes in a spiral manner." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="SpiralLayout.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
initialAutoScale: go.Diagram.Uniform,
isTreePathToChildren: false, // links go from child to parent
layout: $(SpiralLayout) // defined in SpiralLayout.js
});
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
{ locationSpot: go.Spot.Center },
$(go.Shape, { figure: "Circle", fill: "white" },
new go.Binding("fill", "color")),
$(go.TextBlock, { margin: 4 },
new go.Binding("text", "key")));
myDiagram.linkTemplate =
$(go.Link,
{ curve: go.Link.Bezier, curviness: 10 },
$(go.Shape),
$(go.Shape, { toArrow: "Standard" }));
var model = new go.TreeModel();
model.nodeParentKeyProperty = "next";
model.nodeDataArray = [
{ key: "Alpha", next: "Beta", color: "coral" },
{ key: "Beta", next: "Gamma", color: "tomato" },
{ key: "Gamma", next: "Delta", color: "goldenrod" },
{ key: "Delta", next: "Epsilon", color: "orange" },
{ key: "Epsilon", next: "Zeta", color: "coral" },
{ key: "Zeta", next: "Eta", color: "tomato" },
{ key: "Eta", next: "Theta", color: "goldenrod" },
{ key: "Theta", next: "Iota", color: "orange" },
{ key: "Iota", next: "Kappa", color: "coral" },
{ key: "Kappa", next: "Lambda", color: "tomato" },
{ key: "Lambda", next: "Mu", color: "goldenrod" },
{ key: "Mu", next: "Nu", color: "orange" },
{ key: "Nu", next: "Xi", color: "coral" },
{ key: "Xi", next: "Omicron", color: "tomato" },
{ key: "Omicron", next: "Pi", color: "goldenrod" },
{ key: "Pi", next: "Rho", color: "orange" },
{ key: "Rho", next: "Sigma", color: "coral" },
{ key: "Sigma", next: "Tau", color: "tomato" },
{ key: "Tau", next: "Upsilon", color: "goldenrod" },
{ key: "Upsilon", next: "Phi", color: "orange" },
{ key: "Phi", next: "Chi", color: "coral" },
{ key: "Chi", next: "Psi", color: "tomato" },
{ key: "Psi", next: "Omega", color: "goldenrod" },
{ key: "Omega", next: "Alpha2", color: "orange" },
{ key: "Alpha2", next: "Beta2", color: "coral" },
{ key: "Beta2", next: "Gamma2", color: "tomato" },
{ key: "Gamma2", next: "Delta2", color: "goldenrod" },
{ key: "Delta2", next: "Epsilon2", color: "orange" },
{ key: "Epsilon2", next: "Zeta2", color: "coral" },
{ key: "Zeta2", next: "Eta2", color: "tomato" },
{ key: "Eta2", next: "Theta2", color: "goldenrod" },
{ key: "Theta2", next: "Iota2", color: "orange" },
{ key: "Iota2", next: "Kappa2", color: "coral" },
{ key: "Kappa2", next: "Lambda2", color: "tomato" },
{ key: "Lambda2", next: "Mu2", color: "goldenrod" },
{ key: "Mu2", next: "Nu2", color: "orange" },
{ key: "Nu2", next: "Xi2", color: "coral" },
{ key: "Xi2", next: "Omicron2", color: "tomato" },
{ key: "Omicron2", next: "Pi2", color: "goldenrod" },
{ key: "Pi2", next: "Rho2", color: "orange" },
{ key: "Rho2", next: "Sigma2", color: "coral" },
{ key: "Sigma2", next: "Tau2", color: "tomato" },
{ key: "Tau2", next: "Upsilon2", color: "goldenrod" },
{ key: "Upsilon2", next: "Phi2", color: "orange" },
{ key: "Phi2", next: "Chi2", color: "coral" },
{ key: "Chi2", next: "Psi2", color: "tomato" },
{ key: "Psi2", next: "Omega2", color: "goldenrod" },
{ key: "Omega2", color: "orange" }];
myDiagram.model = model;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px; min-width: 200px"></div>
<p>
This sample demonstrates a custom Layout, SpiralLayout, which assumes the graph consists of a chain of nodes.
The layout is defined in its own file, as <a href="SpiralLayout.js">SpiralLayout.js</a>.
</p>
</div>
</body>
</html>
+196
View File
@@ -0,0 +1,196 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Layout that lays out a chain of nodes in a spiral
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Layout
* @class
* This layout assumes the graph is a chain of Nodes,
* {@link #spacing} controls the spacing between nodes.
*/
function SpiralLayout() {
go.Layout.call(this);
this._radius = NaN;
this._spacing = 10;
this._clockwise = true;
}
go.Diagram.inherit(SpiralLayout, go.Layout);
/**
* @ignore
* Copies properties to a cloned Layout.
* @this {SpiralLayout}
* @param {Layout} copy
*/
SpiralLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
copy._radius = this._radius;
copy._spacing = this._spacing;
copy._clockwise = this._clockwise;
};
/**
* This method actually positions all of the Nodes, assuming that the ordering of the nodes
* is given by a single link from one node to the next.
* This respects the {@link #spacing} property to affect the layout.
* @this {SpiralLayout}
* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
*/
SpiralLayout.prototype.doLayout = function(coll) {
if (this.network === null) {
this.network = this.makeNetwork(coll);
}
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
var originx = this.arrangementOrigin.x;
var originy = this.arrangementOrigin.y;
var root = null;
// find a root vertex -- one without any incoming edges
var it = this.network.vertexes.iterator;
while (it.next()) {
var v = it.value;
if (root === null) root = v; // in case there are only circles
if (v.sourceEdges.count === 0) {
root = v;
break;
}
}
// couldn't find a root vertex
if (root === null) {
this.network = null;
return;
}
var space = this.spacing;
var cw = (this.clockwise ? 1 : -1);
var rad = this.radius;
if (rad <= 0 || isNaN(rad) || !isFinite(rad)) rad = this.diameter(root) / 4;
// treat the root node specially: it goes in the center
var dia = this.diameter(root);
var angle = cw * Math.PI;
root.centerX = originx;
root.centerY = originy;
var edge = root.destinationEdges.first();
if (edge !== null && edge.link !== null) edge.link.curviness = cw * rad;
// now locate each of the following nodes, in order, along a spiral
var vert = (edge !== null ? edge.toVertex : null);
while (vert !== null) {
// involute spiral
var cos = Math.cos(angle);
var sin = Math.sin(angle);
var x = rad * (cos + angle * sin);
var y = rad * (sin - angle * cos);
// the link might connect to a member node of a group
if (vert.node instanceof go.Group && edge.link.toNode !== vert.node) {
var offset = edge.link.toNode.location.copy().subtract(vert.node.location);
x -= offset.x;
y -= offset.y;
}
vert.centerX = x + originx;
vert.centerY = y + originy;
var nextedge = vert.destinationEdges.first();
var nextvert = (nextedge !== null ? nextedge.toVertex : null);
// clockwise curves want positive Link.curviness
if (this.isRouting && nextedge !== null && nextedge.link !== null) {
if (!isNaN(nextedge.link.curviness)) {
var c = nextedge.link.curviness;
nextedge.link.curviness = cw * Math.abs(c);
}
}
// determine next node's angle
var dia = this.diameter(vert)/2 + this.diameter(nextvert)/2;
angle += cw * Math.atan((dia + space) / Math.sqrt(x * x + y * y));
edge = nextedge;
vert = nextvert;
}
this.updateParts();
this.network = null;
};
/**
* @ignore
* Compute the effective diameter of a Node.
* @this {SpiralLayout}
* @param {LayoutVertex} v
* @return {number}
*/
SpiralLayout.prototype.diameter = function(v) {
if (!v) return 0;
var b = v.bounds;
return Math.sqrt(b.width*b.width + b.height*b.height);
};
// Public properties
/**
* Gets or sets the radius distance.
* The default value is NaN.
* @name SpiralLayout#radius
* @return {number}
*/
Object.defineProperty(SpiralLayout.prototype, "radius", {
get: function() { return this._radius; },
set: function(val) {
if (typeof val !== "number") throw new Error("new value for SpiralLayout.radius must be a number, not: " + val);
if (this._radius !== val) {
this._radius = val;
this.invalidateLayout();
}
}
});
/**
* Gets or sets the spacing between nodes.
* The default value is 100.
* @name SpiralLayout#spacing
* @return {number}
*/
Object.defineProperty(SpiralLayout.prototype, "spacing", {
get: function() { return this._spacing; },
set: function(val) {
if (typeof val !== "number") throw new Error("new value for SpiralLayout.spacing must be a number, not: " + val);
if (this._spacing !== val) {
this._spacing = val;
this.invalidateLayout();
}
}
});
/**
* Gets or sets whether the spiral should go clockwise or counter-clockwise.
* The default value is true.
* @name SpiralLayout#clockwise
* @return {boolean}
*/
Object.defineProperty(SpiralLayout.prototype, "clockwise", {
get: function() { return this._clockwise; },
set: function(val) {
if (typeof val !== "boolean") throw new Error("new value for SpiralLayout.clockwise must be a boolean, not: " + val);
if (this._clockwise !== val) {
this._clockwise = val;
this.invalidateLayout();
}
}
});
+249
View File
@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html>
<head>
<title>Beat Paths with Lanes for Divisions Using SwimLaneLayout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="TypeScript: SwimLaneLayout, laying out the whole graph while assigning nodes to stay in lanes/groups." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="SwimLaneLayout.js"></script>
<script id="code">
var DIRECTION = 90; // used to customize the layout and the templates, only upon first initialization
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv",
{ // automatically scale the diagram to fit the viewport's size
initialAutoScale: go.Diagram.Uniform,
// disable user copying of parts
allowCopy: false,
// position all of the nodes and route all of the links
layout:
$(SwimLaneLayout,
{
laneProperty: "group", // needs to know how to assign vertexes/nodes into lanes/groups
direction: DIRECTION, // Group template also depends on DIRECTION
setsPortSpots: false,
layerSpacing: 20,
columnSpacing: 5,
commitLayers: function(layerRects, offset) {
if (layerRects.length === 0) return;
var horiz = (this.direction === 0 || this.direction === 180);
var forwards = (this.direction === 0 || this.direction === 90);
var rect = layerRects[forwards ? layerRects.length - 1 : 0];
var totallength = horiz ? rect.right : rect.bottom;
for (var i = 0; i < this.laneNames.length; i++) {
var lane = this.laneNames[i];
// assume lane names do not conflict with node names
var group = this.diagram.findNodeForKey(lane);
if (group === null) {
this.diagram.model.addNodeData({ key: lane, isGroup: true });
group = this.diagram.findNodeForKey(lane);
}
if (horiz) {
group.location = new go.Point(-this.layerSpacing / 2, this.lanePositions.get(lane) * this.columnSpacing + offset.y);
} else {
group.location = new go.Point(this.lanePositions.get(lane) * this.columnSpacing + offset.x, -this.layerSpacing / 2);
}
var ph = group.findObject("PLACEHOLDER"); // won't be a go.Placeholder, but just a regular Shape
if (ph === null) ph = group;
if (horiz) {
ph.desiredSize = new go.Size(totallength, this.laneBreadths.get(lane) * this.columnSpacing);
} else {
ph.desiredSize = new go.Size(this.laneBreadths.get(lane) * this.columnSpacing, totallength);
}
}
}
})
});
// replace the default Node template in the nodeTemplateMap
myDiagram.nodeTemplate =
$(go.Node, "Vertical", // the whole node panel
{ // when the DIRECTION is vertical, use the whole Node as the port
fromSpot: go.Spot.TopBottomSides,
toSpot: go.Spot.TopBottomSides
},
$(go.TextBlock, // the text label
new go.Binding("text", "key")),
$(go.Picture, // the icon showing the logo
// You should set the desiredSize (or width and height)
// whenever you know what size the Picture should be.
{
desiredSize: new go.Size(50, 50),
// when the DIRECTION is horizontal, use this icon as the port
portId: (DIRECTION === 0 || DIRECTION === 180) ? "" : null,
fromSpot: go.Spot.LeftRightSides,
toSpot: go.Spot.LeftRightSides
},
new go.Binding("source", "key", convertKeyImage))
);
function convertKeyImage(key) {
if (!key) key = "NE";
return "https://www.nwoods.com/go/beatpaths/" + key + "_logo-50x50.png";
}
// replace the default Link template in the linkTemplateMap
myDiagram.linkTemplate =
$(go.Link, // the whole link panel
{ routing: go.Link.AvoidsNodes, corner: 10 },
$(go.Shape, // the link shape
{ strokeWidth: 1.5 }),
$(go.Shape, // the arrowhead
{ toArrow: "Standard", stroke: null })
);
myDiagram.groupTemplate = // assumes SwimLaneLayout.direction === 0
$(go.Group, (DIRECTION === 0 || DIRECTION === 180) ? "Horizontal" : "Vertical",
{
layerName: "Background", // always behind all regular nodes and links
movable: false, // user cannot move or copy any lanes
copyable: false,
locationObjectName: "PLACEHOLDER", // this object will be sized and located by SwimLaneLayout
layout: null, // no lane lays out its member nodes
avoidable: false // don't affect any AvoidsNodes link routes
},
$(go.TextBlock, { font: "bold 12pt sans-serif", angle: (DIRECTION === 0 || DIRECTION === 180) ? 270 : 0 },
new go.Binding("text", "key")),
$(go.Panel, "Auto",
$(go.Shape, { fill: "transparent", stroke: "orange" }),
$(go.Shape, { name: "PLACEHOLDER", fill: null, stroke: null, strokeWidth: 0 })
),
$(go.TextBlock, { font: "bold 12pt sans-serif", angle: (DIRECTION === 0 || DIRECTION === 180) ? 90 : 0 },
new go.Binding("text", "key"))
);
partitionBy('d');
}
// the array of node data describing each team, each division, and each conference
const nodeDataArray = [
{ key: "AFC", isGroup: true },
{ key: "NFC", isGroup: true },
{ key: "AFCE", isGroup: true },
{ key: "AFCN", isGroup: true },
{ key: "AFCS", isGroup: true },
{ key: "AFCW", isGroup: true },
{ key: "NFCE", isGroup: true },
{ key: "NFCN", isGroup: true },
{ key: "NFCS", isGroup: true },
{ key: "NFCW", isGroup: true },
{ key: "NE", conf: "AFC", div: "AFCE" },
{ key: "PIT", conf: "AFC", div: "AFCN" },
{ key: "DAL", conf: "NFC", div: "NFCE" },
{ key: "CLE", conf: "AFC", div: "AFCN" },
{ key: "NYG", conf: "NFC", div: "NFCE" },
{ key: "GB", conf: "NFC", div: "NFCN" },
{ key: "SEA", conf: "NFC", div: "NFCW" },
{ key: "IND", conf: "AFC", div: "AFCS" },
{ key: "MIN", conf: "NFC", div: "NFCN" },
{ key: "PHI", conf: "NFC", div: "NFCE" },
{ key: "DET", conf: "NFC", div: "NFCN" },
{ key: "JAC", conf: "AFC", div: "AFCS" },
{ key: "SD", conf: "AFC", div: "AFCW" },
{ key: "CHI", conf: "NFC", div: "NFCN" },
{ key: "TB", conf: "NFC", div: "NFCS" },
{ key: "KC", conf: "AFC", div: "AFCW" },
{ key: "DEN", conf: "AFC", div: "AFCW" },
{ key: "TEN", conf: "AFC", div: "AFCS" },
{ key: "BUF", conf: "AFC", div: "AFCE" },
{ key: "OAK", conf: "AFC", div: "AFCW" },
{ key: "HOU", conf: "AFC", div: "AFCS" },
{ key: "ATL", conf: "NFC", div: "NFCS" },
{ key: "WAS", conf: "NFC", div: "NFCE" },
{ key: "CIN", conf: "AFC", div: "AFCN" },
{ key: "NYJ", conf: "AFC", div: "AFCE" },
{ key: "CAR", conf: "NFC", div: "NFCS" },
{ key: "NO", conf: "NFC", div: "NFCS" },
{ key: "BAL", conf: "AFC", div: "AFCN" },
{ key: "MIA", conf: "AFC", div: "AFCE" },
{ key: "ARI", conf: "NFC", div: "NFCW" },
{ key: "STL", conf: "NFC", div: "NFCW" },
{ key: "SF", conf: "NFC", div: "NFCW" }
];
// the array of link data objects: the relationships between the nodes
var linkDataArray = [
{ from: "NE", to: "CLE" },
{ from: "NE", to: "DAL" },
{ from: "NE", to: "IND" },
{ from: "PIT", to: "CLE" },
{ from: "DAL", to: "NYG" },
{ from: "DAL", to: "GB" },
{ from: "CLE", to: "SEA" },
{ from: "NYG", to: "DET" },
{ from: "GB", to: "MIN" },
{ from: "GB", to: "PHI" },
{ from: "SEA", to: "PHI" },
{ from: "SEA", to: "CIN" },
{ from: "IND", to: "TB" },
{ from: "IND", to: "JAC" },
{ from: "MIN", to: "SD" },
{ from: "PHI", to: "NYJ" },
{ from: "DET", to: "CHI" },
{ from: "DET", to: "DEN" },
{ from: "JAC", to: "DEN" },
{ from: "SD", to: "DEN" },
{ from: "CHI", to: "OAK" },
{ from: "TB", to: "TEN" },
{ from: "DEN", to: "TEN" },
{ from: "DEN", to: "KC" },
{ from: "DEN", to: "BUF" },
{ from: "TEN", to: "OAK" },
{ from: "TEN", to: "ATL" },
{ from: "TEN", to: "HOU" },
{ from: "BUF", to: "WAS" },
{ from: "OAK", to: "MIA" },
{ from: "HOU", to: "MIA" },
{ from: "HOU", to: "CAR" },
{ from: "WAS", to: "NYJ" },
{ from: "WAS", to: "ARI" },
{ from: "CIN", to: "BAL" },
{ from: "NYJ", to: "MIA" },
{ from: "CAR", to: "ARI" },
{ from: "CAR", to: "STL" },
{ from: "CAR", to: "SF" },
{ from: "NO", to: "SF" },
{ from: "BAL", to: "STL" },
{ from: "BAL", to: "SF" }
];
function partitionBy(a) {
// create the model and assign it to the Diagram
var model = new go.GraphLinksModel();
// depending on how we are partitioning the graph, each node belongs either
// to a conference group or to a division group
model.nodeGroupKey = (a === 'c') ? "conf" : "div";
model.nodeDataArray = nodeDataArray;
model.linkDataArray = linkDataArray;
// each node's lane information is the same as the group information
myDiagram.layout.laneProperty = model.nodeGroupKey;
// optionally, specify the order of known lane names, without setting laneComparer
myDiagram.layout.laneNames = (a === 'c') ?
["AFC", "NFC"] :
["AFCE", "AFCN", "AFCS", "AFCW", "NFCE", "NFCN", "NFCS", "NFCW"];
myDiagram.model = model;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<p><b>Beat Paths</b>: The 2007 NFL Season, divided by conference or by division</p>
<div id="myDiagramDiv" style="border: solid 1px gray; margin: 10px; height: 700px"></div>
<input type="radio" name="A" onclick="partitionBy('c')" id="conferenceButton" />
<label for="conferenceButton">Conferences</label><br />
<input type="radio" name="A" onclick="partitionBy('d')" id="divisionButton" checked />
<label for="divisionButton">Divisions</label><br />
</div>
</body>
</html>
+479
View File
@@ -0,0 +1,479 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom LayeredDigraphLayout that knows about "lanes"
// and that positions each node in its respective lane.
// This assumes that each Node.data.lane property is a string that names the lane the node should be in.
// You can set the SwimLaneLayout.laneProperty property to use a different data property name.
// It is commonplace to set this property to be the same as the GraphLinksModel.nodeGroupKeyProperty,
// so that the one property indicates that a particular node data is a member of a particular group
// and thus that that group represents a lane.
// The lanes can be sorted by specifying the SwimLaneLayout.laneComparer function.
// You can add extra space between the lanes by increasing SwimLaneLayout.laneSpacing from its default of zero.
// That number's unit is columns, LayeredDigraphLayout.columnSpacing, not in document coordinates.
function SwimLaneLayout() {
go.LayeredDigraphLayout.call(this);
// settable properties
this._laneProperty = "lane"; // how to get lane identifier string from node data
this._laneNames = [];
this._laneComparer = null;
this._laneSpacing = 0; // in columns
this._router = { linkSpacing: 4 };
this._reducer = null;
// computed, read-only state
this.lanePositions = new go.Map(); // lane names --> start columns, left to right
this.laneBreadths = new go.Map(); // lane names --> needed width in columns
// internal state
this._layers = null;
this._neededSpaces = null;
}
go.Diagram.inherit(SwimLaneLayout, go.LayeredDigraphLayout);
Object.defineProperty(SwimLaneLayout.prototype, "laneProperty", {
get: function() { return this._laneProperty; },
set: function(val) {
if (typeof val !== 'string' && typeof val !== 'function') throw new Error("new value for SwimLaneLayout.laneProperty must be a property name, not: " + val);
if (this._laneProperty !== val) {
this._laneProperty = val;
this.invalidateLayout();
}
}
});
Object.defineProperty(SwimLaneLayout.prototype, "laneNames", {
get: function() { return this._laneNames; },
set: function(val) {
if (!Array.isArray(val)) throw new Error("new value for SwimLaneLayout.laneNames must be an Array, not: " + val);
if (this._laneNames !== val) {
this._laneNames = val;
this.invalidateLayout();
}
}
});
Object.defineProperty(SwimLaneLayout.prototype, "laneComparer", {
get: function() { return this._laneComparer; },
set: function(val) {
if (typeof val !== 'function') throw new Error("new value for SwimLaneLayout.laneComparer must be a function, not: " + val);
if (this._laneComparer !== val) {
this._laneComparer = val;
this.invalidateLayout();
}
}
});
Object.defineProperty(SwimLaneLayout.prototype, "laneSpacing", { // unit is columns, not in document coordinates
get: function() { return this._laneSpacing; },
set: function(val) {
if (typeof val !== 'number') throw new Error("new value for SwimLaneLayout.laneSpacing must be a number, not: " + val);
if (this._laneSpacing !== val) {
this._laneSpacing = val;
this.invalidateLayout();
}
}
});
Object.defineProperty(SwimLaneLayout.prototype, "router", {
get: function() { return this._router; },
set: function(val) {
if (this._router !== val) {
this._router = val;
this.invalidateLayout();
}
}
});
Object.defineProperty(SwimLaneLayout.prototype, "reducer", {
get: function() { return this._reducer; },
set: function(val) {
if (this._reducer !== val) {
this._reducer = val;
if (val) {
var lay = this;
val.findLane = function(v) { return lay.getLane(v); }
val.getIndex = function(v) { return v.index; }
val.getBary = function(v) { return v.bary || 0; }
val.setBary = function(v, val) { v.bary = val; }
val.getConnectedNodesIterator = function(v) { return v.vertexes; }
}
this.invalidateLayout();
}
}
});
SwimLaneLayout.prototype.doLayout = function(coll) {
this.lanePositions.clear(); // lane names --> start columns, left to right
this.laneBreadths.clear(); // lane names --> needed width in columns
this._layers = null;
this._neededSpaces = null;
go.LayeredDigraphLayout.prototype.doLayout.call(this, coll);
this.lanePositions.clear();
this.laneBreadths.clear();
this._layers = null;
this._neededSpaces = null;
this.laneNames = []; // clear out for next layout
}
SwimLaneLayout.prototype.nodeMinLayerSpace = function(v, topleft) {
if (!this._neededSpaces) this._neededSpaces = this.computeNeededLayerSpaces(this.network);
if (v.node === null) return 0;
var lay = v.layer;
if (!topleft) {
if (lay > 0) lay--;
}
var overlaps = (this._neededSpaces[lay] || 0)/2;
var edges = this.countEdgesForDirection(v, (this.direction > 135) ? !topleft : topleft);
var needed = Math.max(overlaps, edges) * this.router.linkSpacing * 1.5;
if (this.direction === 90 || this.direction === 270) {
if (topleft) {
return v.focus.y + 10 + needed;
} else {
return v.bounds.height - v.focus.y + 10 + needed;
}
} else {
if (topleft) {
return v.focus.x + 10 + needed;
} else {
return v.bounds.width - v.focus.x + 10 + needed;
}
}
}
SwimLaneLayout.prototype.countEdgesForDirection = function(vertex, topleft) {
var c = 0;
var lay = vertex.layer;
vertex.edges.each(function(e) {
if (topleft) {
if (e.getOtherVertex(vertex).layer >= lay) c++;
} else {
if (e.getOtherVertex(vertex).layer <= lay) c++;
}
});
return c;
}
SwimLaneLayout.prototype.computeNeededLayerSpaces = function(net) {
// group all edges by their connected vertexes' least layer
var layerMinEdges = [];
net.edges.each(function(e) {
// consider all edges, including dummy ones!
var f = e.fromVertex;
var t = e.toVertex;
if (f.column === t.column) return; // skip edges that don't go between columns
if (Math.abs(f.layer-t.layer) > 1) return; // skip edges that don't go between adjacent layers
var lay = Math.min(f.layer, t.layer);
var arr = layerMinEdges[lay];
if (!arr) arr = layerMinEdges[lay] = [];
arr.push(e);
});
// sort each array of edges by their lowest connected vertex column
// for edges with the same minimum column, sort by their maximum column
var layerMaxEdges = []; // same as layerMinEdges, but sorted by maximum column
layerMinEdges.forEach(function(arr, lay) {
if (!arr) return;
arr.sort(function(e1, e2) {
var f1c = e1.fromVertex.column;
var t1c = e1.toVertex.column;
var f2c = e2.fromVertex.column;
var t2c = e2.toVertex.column;
var e1mincol = Math.min(f1c, t1c);
var e2mincol = Math.min(f2c, t2c);
if (e1mincol > e2mincol) return 1;
if (e1mincol < e2mincol) return -1;
var e1maxcol = Math.max(f1c, t1c);
var e2maxcol = Math.max(f2c, t2c);
if (e1maxcol > e2maxcol) return 1;
if (e1maxcol < e2maxcol) return -1;
return 0;
});
layerMaxEdges[lay] = arr.slice(0);
layerMaxEdges[lay].sort(function(e1, e2) {
var f1c = e1.fromVertex.column;
var t1c = e1.toVertex.column;
var f2c = e2.fromVertex.column;
var t2c = e2.toVertex.column;
var e1maxcol = Math.max(f1c, t1c);
var e2maxcol = Math.max(f2c, t2c);
if (e1maxcol > e2maxcol) return 1;
if (e1maxcol < e2maxcol) return -1;
var e1mincol = Math.min(f1c, t1c);
var e2mincol = Math.min(f2c, t2c);
if (e1mincol > e2mincol) return 1;
if (e1mincol < e2mincol) return -1;
return 0;
});
});
// run through each array of edges to count how many overlaps there might be
var layerOverlaps = [];
layerMinEdges.forEach(function(arr, lay) {
var mins = arr; // sorted by min column
var maxs = layerMaxEdges[lay]; // sorted by max column
var maxoverlap = 0; // maximum count for this layer
if (mins && maxs && mins.length > 1 && maxs.length > 1) {
var mini = 0;
var min = null;
var maxi = 0;
var max = null;
while (mini < mins.length || maxi < maxs.length) {
if (mini < mins.length) min = mins[mini];
var mincol = min ? Math.min(min.fromVertex.column, min.toVertex.column) : 0;
if (maxi < maxs.length) max = maxs[maxi];
var maxcol = max ? Math.max(max.fromVertex.column, max.toVertex.column) : Infinity;
maxoverlap = Math.max(maxoverlap, Math.abs(mini-maxi));
if (mincol <= maxcol && mini < mins.length) {
mini++;
} else if (maxi < maxs.length) {
maxi++;
}
}
}
layerOverlaps[lay] = maxoverlap * 1.5; // # of parallel links
});
return layerOverlaps;
}
SwimLaneLayout.prototype.setupLanes = function() {
// set up some data structures
var layout = this;
var laneNameSet = new go.Set().addAll(this.laneNames);
var laneIndexes = new go.Map(); // lane names --> index when sorted
var layers = [];
this._layers = layers;
var vit = this.network.vertexes.iterator;
while (vit.next()) {
var v = vit.value;
var lane = this.getLane(v); // cannot call findLane yet
if (lane !== null && !laneNameSet.has(lane)) {
laneNameSet.add(lane);
this.laneNames.push(lane);
}
var layer = v.layer;
if (layer >= 0) {
var arr = layers[layer];
if (!arr) {
layers[layer] = [v];
} else {
arr.push(v);
}
}
}
// sort laneNames and initialize laneIndexes
if (typeof laneComparer === "function") this.laneNames.sort(laneComparer);
for (var i = 0; i < this.laneNames.length; i++) {
laneIndexes.add(this.laneNames[i], i);
}
// now OK to call findLane
// sort vertexes so that vertexes are grouped by lane
for (var i = 0; i <= this.maxLayer; i++) {
layers[i].sort(function(a, b) { return layout.compareVertexes(a, b); });
}
}
/**
* Replace the standard reduceCrossings behavior so that it respects lanes.
*/
SwimLaneLayout.prototype.reduceCrossings = function() {
this.setupLanes();
// this just cares about the .index and ignores .column
var layers = this._layers;
var red = this.reducer;
if (red) {
for (var i = 0; i < layers.length-1; i++) {
red.reduceCrossings(layers[i], layers[i+1]);
layers[i].forEach(function(v, j) { v.index = j; })
}
for (var i = layers.length-1; i > 0; i--) {
red.reduceCrossings(layers[i], layers[i-1]);
layers[i].forEach(function(v, j) { v.index = j; })
}
}
this.computeLanes(); // and recompute all vertex.column values
}
SwimLaneLayout.prototype.computeLanes = function() {
// compute needed width for each lane, in columns
for (var i = 0; i < this.laneNames.length; i++) {
var lane = this.laneNames[i];
this.laneBreadths.add(lane, this.computeMinLaneWidth(lane));
}
var lwidths = new go.Map(); // reused for each layer
for (var i = 0; i <= this.maxLayer; i++) {
var arr = this._layers[i];
if (arr) {
var layout = this;
// now run through Array finding width (in columns) of each lane
// and max with this.laneBreadths.get(lane)
for (var j = 0; j < arr.length; j++) {
var v = arr[j];
var w = this.nodeMinColumnSpace(v, true) + 1 + this.nodeMinColumnSpace(v, false);
var ln = this.findLane(v);
var totw = lwidths.get(ln)
if (totw === null) {
lwidths.set(ln, w);
} else {
lwidths.set(ln, totw + w);
}
}
lwidths.each(function(kvp) {
var lane = kvp.key;
var colsInLayer = kvp.value;
var colsMax = layout.laneBreadths.get(lane);
if (colsInLayer > colsMax) layout.laneBreadths.set(lane, colsInLayer);
})
lwidths.clear();
}
}
// compute starting positions for each lane
var x = 0;
for (var i = 0; i < this.laneNames.length; i++) {
var lane = this.laneNames[i];
this.lanePositions.set(lane, x);
var w = this.laneBreadths.get(lane);
x += w + this.laneSpacing;
}
this.renormalizeColumns();
}
SwimLaneLayout.prototype.renormalizeColumns = function() {
// set new column and index on each vertex
for (var i = 0; i < this._layers.length; i++) {
var prevlane = null;
var c = 0;
var arr = this._layers[i];
for (var j = 0; j < arr.length; j++) {
var v = arr[j];
v.index = j;
var l = this.findLane(v);
if (prevlane !== l) {
c = this.lanePositions.get(l);
var w = this.laneBreadths.get(l);
// compute needed breadth within lane, in columns
var z = this.nodeMinColumnSpace(v, true) + 1 + this.nodeMinColumnSpace(v, false);
var k = j+1;
while (k < arr.length && this.findLane(arr[k]) === l) {
var vz = arr[k];
z += this.nodeMinColumnSpace(vz, true) + 1 + this.nodeMinColumnSpace(vz, false);
k++;
}
// if there is extra space, shift the vertexes to the middle of the lane
if (z < w) {
c += Math.floor((w-z)/2);
}
}
c += this.nodeMinColumnSpace(v, true);
v.column = c;
c += 1;
c += this.nodeMinColumnSpace(v, false);
prevlane = l;
}
}
}
/**
* Return the minimum lane width, in columns
* @param lane
*/
SwimLaneLayout.prototype.computeMinLaneWidth = function(lane) { return 0; }
/**
* Disable normal straightenAndPack behavior, which would mess up the columns.
*/
SwimLaneLayout.prototype.straightenAndPack = function() {}
/**
* Given a vertex, get the lane (name) that its node belongs in.
* If the lane appears to be undefined, this returns the empty string.
* For dummy vertexes (with no node) this will return null.
* @param v
*/
SwimLaneLayout.prototype.getLane = function(v) {
if (v === null) return null;
var node = v.node;
if (node !== null) {
var data = node.data;
if (data !== null) {
var lane = null;
if (typeof this.laneProperty === "function") {
lane = this.laneProperty(data);
} else {
lane = data[this.laneProperty];
}
if (typeof lane === "string") return lane;
return "";
}
}
return null;
}
/**
* This is just like {@link #getLane} but handles dummy vertexes
* for which the {@link #getLane} returns null by returning the
* lane of the edge's source or destination vertex.
* This can only be called after the lanes have been set up internally.
* @param v
*/
SwimLaneLayout.prototype.findLane = function(v) {
if (v !== null) {
var lane = this.getLane(v);
if (lane !== null) {
return lane;
} else {
var srcv = this.findRealSource(v.sourceEdges.first());
var dstv = this.findRealDestination(v.destinationEdges.first());
var srcLane = this.getLane(srcv);
var dstLane = this.getLane(dstv);
if (srcLane !== null || dstLane !== null) {
if (srcLane === dstLane) return srcLane;
if (srcLane !== null) return srcLane;
if (dstLane !== null) return dstLane;
}
}
}
return null;
}
SwimLaneLayout.prototype.findRealSource = function(e) {
if (e === null) return null;
if (e.fromVertex.node) return e.fromVertex;
return this.findRealSource(e.fromVertex.sourceEdges.first());
}
SwimLaneLayout.prototype.findRealDestination = function(e) {
if (e === null) return null;
if (e.toVertex.node) return e.toVertex;
return this.findRealDestination(e.toVertex.destinationEdges.first());
}
SwimLaneLayout.prototype.compareVertexes = function(v, w) {
var laneV = this.findLane(v);
if (laneV === null) laneV = "";
var laneW = this.findLane(w);
if (laneW === null) laneW = "";
if (laneV < laneW) return -1;
if (laneV > laneW) return 1;
return 0;
};
+311
View File
@@ -0,0 +1,311 @@
<!DOCTYPE html>
<html>
<head>
<title>Table Layout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Use the TableLayout extension to arrange nodes in a tabular or grid-like form." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="TableLayout.js"></script>
<script id="code">
// define a custom ResizingTool to limit how far one can shrink a row or column
function LaneResizingTool() {
go.ResizingTool.call(this);
}
go.Diagram.inherit(LaneResizingTool, go.ResizingTool);
LaneResizingTool.prototype.computeMinSize = function() {
var diagram = this.diagram;
var lane = this.adornedObject.part; // might be row or column
var horiz = (lane.category === "Column Header"); // or "Row Header"
var margin = diagram.nodeTemplate.margin;
var bounds = new go.Rect();
diagram.findTopLevelGroups().each(function(g) {
if (horiz ? (g.column === lane.column) : (g.row === lane.row)) {
var b = diagram.computePartsBounds(g.memberParts);
if (b.isEmpty()) return; // nothing in there? ignore it
b.unionPoint(g.location); // keep any empty space on the left and top
b.addMargin(margin); // assume the same node margin applies to all nodes
if (bounds.isEmpty()) {
bounds = b;
} else {
bounds.unionRect(b);
}
}
});
// limit the result by the standard value of computeMinSize
var msz = go.ResizingTool.prototype.computeMinSize.call(this);
if (bounds.isEmpty()) return msz;
return new go.Size(Math.max(msz.width, bounds.width), Math.max(msz.height, bounds.height));
};
LaneResizingTool.prototype.resize = function(newr) {
var lane = this.adornedObject.part;
var horiz = (lane.category === "Column Header");
var lay = this.diagram.layout; // the TableLayout
if (horiz) {
var col = lane.column;
var coldef = lay.getColumnDefinition(col);
coldef.width = newr.width;
} else {
var row = lane.row;
var rowdef = lay.getRowDefinition(row);
rowdef.height = newr.height;
}
lay.invalidateLayout();
};
// end LaneResizingTool class
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
layout: $(TableLayout,
$(go.RowColumnDefinition, { row: 1, height: 22 }), // fixed size column headers
$(go.RowColumnDefinition, { column: 1, width: 22 }) // fixed size row headers
),
"SelectionMoved": function(e) { e.diagram.layoutDiagram(true); },
"resizingTool": new LaneResizingTool(),
// feedback that dropping in the background is not allowed
mouseDragOver: function(e) { e.diagram.currentCursor = "not-allowed"; },
// when dropped in the background, not on a Node or a Group, cancel the drop
mouseDrop: function(e) { e.diagram.currentTool.doCancel(); },
"animationManager.isInitial": false,
"undoManager.isEnabled": true
});
myDiagram.nodeTemplateMap.add("Header", // an overall table header, at the top
$(go.Part, "Auto",
{
row: 0, column: 1, columnSpan: 9999,
stretch: go.GraphObject.Horizontal,
selectable: false, pickable: false
},
$(go.Shape, { fill: "transparent", strokeWidth: 0 }),
$(go.TextBlock, { alignment: go.Spot.Center, font: "bold 12pt sans-serif" },
new go.Binding("text"))
));
myDiagram.nodeTemplateMap.add("Sider", // an overall table header, on the left side
$(go.Part, "Auto",
{
row: 1, rowSpan: 9999, column: 0,
stretch: go.GraphObject.Vertical,
selectable: false, pickable: false
},
$(go.Shape, { fill: "transparent", strokeWidth: 0 }),
$(go.TextBlock, { alignment: go.Spot.Center, font: "bold 12pt sans-serif", angle: 270 },
new go.Binding("text"))
));
myDiagram.nodeTemplateMap.add("Column Header", // for each column header
$(go.Part, "Spot",
{
row: 1, rowSpan: 9999, column: 2,
minSize: new go.Size(100, NaN),
stretch: go.GraphObject.Fill,
movable: false,
resizable: true,
resizeAdornmentTemplate:
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, // for changing the length of a lane
{
alignment: go.Spot.Right,
desiredSize: new go.Size(7, 50),
fill: "lightblue", stroke: "dodgerblue",
cursor: "col-resize"
})
)
},
new go.Binding("column", "col"),
$(go.Shape, { fill: null },
new go.Binding("fill", "color")),
$(go.Panel, "Auto",
{ // this is positioned above the Shape, in row 1
alignment: go.Spot.Top, alignmentFocus: go.Spot.Bottom,
stretch: go.GraphObject.Horizontal,
height: myDiagram.layout.getRowDefinition(1).height
},
$(go.Shape, { fill: "transparent", strokeWidth: 0 }),
$(go.TextBlock,
{
font: "bold 10pt sans-serif", isMultiline: false,
wrap: go.TextBlock.None, overflow: go.TextBlock.OverflowEllipsis
},
new go.Binding("text"))
)
));
myDiagram.nodeTemplateMap.add("Row Sider", // for each row header
$(go.Part, "Spot",
{
row: 2, column: 1, columnSpan: 9999,
minSize: new go.Size(NaN, 100),
stretch: go.GraphObject.Fill,
movable: false,
resizable: true,
resizeAdornmentTemplate:
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, // for changing the breadth of a lane
{
alignment: go.Spot.Bottom,
desiredSize: new go.Size(50, 7),
fill: "lightblue", stroke: "dodgerblue",
cursor: "row-resize"
})
)
},
new go.Binding("row"),
$(go.Shape, { fill: null },
new go.Binding("fill", "color")),
$(go.Panel, "Auto",
{ // this is positioned to the left of the Shape, in column 1
alignment: go.Spot.Left, alignmentFocus: go.Spot.Right,
stretch: go.GraphObject.Vertical, angle: 270,
height: myDiagram.layout.getColumnDefinition(1).width
},
$(go.Shape, { fill: "transparent", strokeWidth: 0 }),
$(go.TextBlock,
{
font: "bold 10pt sans-serif", isMultiline: false,
wrap: go.TextBlock.None, overflow: go.TextBlock.OverflowEllipsis
},
new go.Binding("text"))
)
));
myDiagram.nodeTemplate = // for regular nodes within cells (groups); you'll want to extend this
$(go.Node, "Auto",
{ width: 100, height: 50, margin: 4 }, // assume uniform Margin, all around
new go.Binding("row"),
new go.Binding("column", "col"),
$(go.Shape, { fill: "white" },
new go.Binding("fill", "color")),
$(go.TextBlock,
new go.Binding("text", "key"))
);
myDiagram.groupTemplate = // for cells
$(go.Group, "Auto",
{
layerName: "Background",
stretch: go.GraphObject.Fill,
selectable: false,
computesBoundsAfterDrag: true,
computesBoundsIncludingLocation: true,
handlesDragDropForMembers: true, // don't need to define handlers on member Nodes and Links
mouseDragEnter: function(e, group, prev) { group.isHighlighted = true; },
mouseDragLeave: function(e, group, next) { group.isHighlighted = false; },
mouseDrop: function(e, group) {
// if any dropped part wasn't already a member of this group, we'll want to let the group's row
// column allow themselves to be resized automatically, in case the row height or column width
// had been set manually by the LaneResizingTool
var anynew = e.diagram.selection.any(function(p) { return p.containingGroup !== group; });
// Don't allow headers/siders to be dropped
var anyHeadersSiders = e.diagram.selection.any(function(p) {
return p.category === "Column Header" || p.category === "Row Sider";
});
if (!anyHeadersSiders && group.addMembers(e.diagram.selection, true)) {
if (anynew) {
e.diagram.layout.getRowDefinition(group.row).height = NaN;
e.diagram.layout.getColumnDefinition(group.column).width = NaN;
}
} else { // failure upon trying to add parts to this group
e.diagram.currentTool.doCancel();
}
}
},
new go.Binding("row"),
new go.Binding("column", "col"),
// the group is normally unseen -- it is completely transparent except when given a color or when highlighted
$(go.Shape,
{
fill: "transparent", stroke: "transparent",
strokeWidth: myDiagram.nodeTemplate.margin.left,
stretch: go.GraphObject.Fill
},
new go.Binding("fill", "color"),
new go.Binding("stroke", "isHighlighted", function(h) { return h ? "red" : "transparent"; }).ofObject()),
$(go.Placeholder,
{ // leave a margin around the member nodes of the group which is the same as the member node margin
alignment: (function(m) { return new go.Spot(0, 0, m.top, m.left); })(myDiagram.nodeTemplate.margin),
padding: (function(m) { return new go.Margin(0, m.right, m.bottom, 0); })(myDiagram.nodeTemplate.margin)
})
);
myDiagram.model = new go.GraphLinksModel([
// headers
{ key: "Header", text: "Vacation Procedures", category: "Header" },
{ key: "Sider", text: "Personnel", category: "Sider" },
// column and row headers
{ key: "Request", text: "Request", col: 2, category: "Column Header" },
{ key: "Approval", text: "Approval", col: 3, category: "Column Header" },
{ key: "Employee", text: "Employee", row: 2, category: "Row Sider" },
{ key: "Manager", text: "Manager", row: 3, category: "Row Sider" },
{ key: "Administrator", text: "Administrator", row: 4, category: "Row Sider" },
// cells, each a group assigned to a row and column
{ key: "EmpReq", row: 2, col: 2, isGroup: true, color: "lightyellow" },
{ key: "EmpApp", row: 2, col: 3, isGroup: true, color: "lightgreen" },
{ key: "ManReq", row: 3, col: 2, isGroup: true, color: "lightgreen" },
{ key: "ManApp", row: 3, col: 3, isGroup: true, color: "lightyellow" },
{ key: "AdmReq", row: 4, col: 2, isGroup: true, color: "lightyellow" },
{ key: "AdmApp", row: 4, col: 3, isGroup: true, color: "lightgreen" },
// nodes, each assigned to a group/cell
{ key: "Delta", color: "orange", size: "100 100", group: "EmpReq" },
{ key: "Epsilon", color: "coral", size: "100 50", group: "EmpReq" },
{ key: "Zeta", color: "tomato", size: "50 70", group: "ManReq" },
{ key: "Eta", color: "coral", size: "50 50", group: "ManApp" },
{ key: "Theta", color: "tomato", size: "100 50", group: "AdmApp" }
]);
myPalette =
$(go.Palette, "myPaletteDiv",
{
nodeTemplateMap: myDiagram.nodeTemplateMap,
"model.nodeDataArray": [
{ key: "Alpha", color: "orange" },
{ key: "Beta", color: "tomato" },
{ key: "Gamma", color: "goldenrod" }
]
});
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div style="width: 100%; display: flex; justify-content: space-between">
<div id="myPaletteDiv" style="width: 120px; height: 600px; margin-right: 2px; border: solid 1px black"></div>
<div id="myDiagramDiv" style="flex-grow: 1; height: 600px; border: solid 1px black"></div>
</div>
<p>
This sample demonstrates a custom Layout, TableLayout, that is very much like a simplified "Table" Panel layout,
but working on non-Link Parts in a Diagram or a Group rather than on GraphObjects in a Panel.
The layout is defined in its own file, as <a href="TableLayout.js">TableLayout.js</a>.
</p>
<p>
You can drag-and-drop nodes from the Palette into any Group.
Dragging into a Group highlights the Group.
Drops must occur inside Groups; otherwise the action is cancelled.
</p>
<p>
Each row and each column is <a>Part.resizable</a> and has a custom <a>Part.resizeAdornmentTemplate</a>
showing a single resize handle on the right side or on the bottom.
There is a custom LaneResizingTool to provide a minimum width or height based on the contents of all of the
groups (cells) in that column or row.
</p>
<p>
This example assumes the Groups are predefined and exist in each cell at a particular row/column,
but this sample could be extended to allow adding and removing rows and/or columns.
</p>
</div>
</body>
</html>
+857
View File
@@ -0,0 +1,857 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// This layout is patterned after the "Table" Panel layout.
/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* @constructor
* @extends Layout
* @class
* This Layout positions non-Link Parts into a table according to the values of
* GraphObject.row, GraphObject.column, GraphObject.rowSpan, GraphObject.columnSpan,
* GraphObject.alignment, GraphObject.stretch.
* If the value of GraphObject.stretch is not go.GraphObject.None, the Part will be sized
* according to the available space in the cell(s).
* <p>
* You can specify constraints for whole rows or columns by calling
* getRowDefinition(row) or getColumnDefinition(col) and setting one of the following properties:
* RowColumnDefinition.alignment, RowColumnDefinition.height, RowColumnDefinition.width,
* RowColumnDefinition.maximum, RowColumnDefinition.minimum, RowColumnDefinition.stretch.
* <p>
* The defaultAlignment and defaultStretch properties apply to all parts if not specified
* on the individual Part or in the corresponding row or column definition.
* <p>
* At the current time, there is no support for separator lines
* (RowColumnDefinition.separatorStroke, separatorStrokeWidth, and separatorDashArray properties)
* nor background (RowColumnDefinition.background and coversSeparators properties).
* There is no support for RowColumnDefinition.sizing, either.
*/
function TableLayout() {
go.Layout.call(this);
/** @type {Spot} */
this._defaultAlignment = go.Spot.Default;
/** @type {EnumValue} */
this._defaultStretch = go.GraphObject.Default;
/** @type {Array} */
this._rowDefs = [];
/** @type {Array} */
this._colDefs = [];
}
go.Diagram.inherit(TableLayout, go.Layout);
/** @ignore */
TableLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
copy._defaultAlignment = this._defaultAlignment;
copy._defaultStretch = this._defaultStretch;
for (var i = 0; i < this._rowDefs.length; i++) {
var def = this._rowDefs[i];
copy._rowDefs.push(def !== undefined ? def.copy() : def);
}
for (var i = 0; i < this._colDefs.length; i++) {
var def = this._colDefs[i];
copy._colDefs.push(def !== undefined ? def.copy() : def);
}
};
/**
* Gets or sets the alignment to use by default for Parts in rows (vertically) and in columns (horizontally).
* The default value is {@link Spot#Default}.
* Setting this property does not raise any events.
* @name TableLayout#defaultAlignment
* @return {Spot}
*/
Object.defineProperty(TableLayout.prototype, "defaultAlignment", {
get: function() { return this._defaultAlignment; },
set: function(val) { this._defaultAlignment = val; }
});
/**
* Gets or sets whether Parts should be stretched in rows (vertically) and in columns (horizontally).
* The default value is {@link GraphObject#Default}.
* Setting this property does not raise any events.
* @name TableLayout#defaultStretch
* @return {EnumValue}
*/
Object.defineProperty(TableLayout.prototype, "defaultStretch", {
get: function() { return this._defaultStretch; },
set: function(val) { this._defaultStretch = val; }
});
/**
* Gets the {@link RowColumnDefinition} for a particular row in this TableLayout.
* If you ask for the definition of a row at or beyond the {@link #rowCount},
* it will automatically create one and return it.
* @this {TableLayout}
* @param {number} idx the non-negative zero-based integer row index.
* @return {RowColumnDefinition}
*/
TableLayout.prototype.getRowDefinition = function(idx) {
if (idx < 0) throw new Error("Row index must be non-negative, not: " + idx);
idx = Math.round(idx);
var defs = this._rowDefs;
var d = defs[idx];
if (d === undefined) {
d = new go.RowColumnDefinition();
// .panel remains null
d.isRow = true;
d.index = idx;
defs[idx] = d;
}
return d;
};
/**
* This read-only property returns the number of rows in this TableLayout.
* This value is only valid after the layout has been performed.
* @name TableLayout#rowCount
* @return {number}
*/
Object.defineProperty(TableLayout.prototype, "rowCount", {
get: function() { return this._rowDefs.length; }
});
/**
* Returns the row at a given y-coordinate in document coordinates.
* This information is only valid when this layout has been performed and {#Layout.isValidLayout}.
* <p>
* If the point is above row 0, this method returns -1.
* If the point below the last row, this returns the last row + 1.
* @this {TableLayout}
* @param {number} y
* @return {number} a zero-based integer
* @see #findColumnForDocumentX
*/
TableLayout.prototype.findRowForDocumentY = function(y) {
y -= this.arrangementOrigin.y;
if (y < 0) return -1;
var total = 0.0;
var it = this._rowDefs;
var l = it.length;
for (var i = 0; i < l; i++) {
var def = it[i];
if (def === undefined) continue;
total += def.total;
if (y < total) {
return i;
}
}
return i;
};
/**
* Gets the {@link RowColumnDefinition} for a particular column in this TableLayout.
* If you ask for the definition of a column at or beyond the {@link #columnCount},
* it will automatically create one and return it.
* @this {TableLayout}
* @param {number} idx the non-negative zero-based integer column index.
* @return {RowColumnDefinition}
*/
TableLayout.prototype.getColumnDefinition = function(idx) {
if (idx < 0) throw new Error("Column index must be non-negative, not: " + idx);
idx = Math.round(idx);
var defs = this._colDefs;
var d = defs[idx];
if (d === undefined) {
d = new go.RowColumnDefinition();
// .panel remains null
d.isRow = false;
d.index = idx;
defs[idx] = d;
}
return d;
};
/**
* This read-only property returns the number of columns in this TableLayout.
* This value is only valid after the layout has been performed.
* @name TableLayout#rowCount
* @return {number}
*/
Object.defineProperty(TableLayout.prototype, "columnCount", {
get: function() { return this._colDefs.length; }
});
/**
* Returns the cell at a given x-coordinate in document coordinates.
* This information is only valid when this layout has been performed and {#Layout.isValidLayout}.
* <p>
* If the point is to left of the column 0, this method returns -1.
* If the point to to the right of the last column, this returns the last column + 1.
* @this {TableLayout}
* @param {number} x
* @return {number} a zero-based integer
* @see #findRowForDocumentY
*/
TableLayout.prototype.findColumnForDocumentX = function(x) {
x -= this.arrangementOrigin.x;
if (x < 0) return -1;
var total = 0.0;
var it = this._colDefs;
var l = it.length;
for (var i = 0; i < l; i++) {
var def = it[i];
if (def === undefined) continue;
total += def.total;
if (x < total) {
return i;
}
}
return i;
};
/**
* @ignore
* @this {TableLayout}
* @param {Part} child
* @param {number} row
* @param {number} col
* @return {EnumValue}
*/
TableLayout.prototype.getEffectiveTableStretch = function(child, row, col) {
var effectivestretch = child.stretch;
if (effectivestretch !== go.GraphObject.Default) return effectivestretch;
// which directions are we stretching?
// undefined = default
var horizontal = undefined;
var vertical = undefined;
switch (row.stretch) {
case go.GraphObject.Default:
case go.GraphObject.Horizontal: break;
case go.GraphObject.Vertical: vertical = true; break;
case go.GraphObject.Fill: vertical = true; break;
}
switch (col.stretch) {
case go.GraphObject.Default:
case go.GraphObject.Vertical: break;
case go.GraphObject.Horizontal: horizontal = true; break;
case go.GraphObject.Fill: horizontal = true; break;
}
var str = this.defaultStretch;
if (horizontal === undefined && (str === go.GraphObject.Horizontal || str === go.GraphObject.Fill)) {
horizontal = true;
} else {
horizontal = false;
}
if (vertical === undefined && (str === go.GraphObject.Vertical || str === go.GraphObject.Fill)) {
vertical = true;
} else {
vertical = false;
}
if (horizontal === true && vertical === true) return go.GraphObject.Fill;
if (horizontal === true) return go.GraphObject.Horizontal;
if (vertical === true) return go.GraphObject.Vertical;
return go.GraphObject.None; // Everything else is none by default
};
/**
* @ignore
* @this {TableLayout}
*/
TableLayout.prototype.doLayout = function(coll) {
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
// put all eligible Parts that are not Links into an Array
var parts = new go.List(/*go.Part*/);
this.collectParts(coll).each(function(p) {
if (!(p instanceof go.Link)) {
parts.add(p);
}
});
this.diagram.startTransaction("TableLayout");
var union = new go.Size();
// this calls .beforeMeasure(parts, rowcol)
var rowcol = this.measureTable(Infinity, Infinity, parts, union, 0, 0);
this.arrangeTable(parts, union, rowcol);
this.afterArrange(parts, rowcol);
this.diagram.commitTransaction("TableLayout");
};
/**
* @ignore
* @this {TableLayout}
* @param {List} parts
* @param {Array.<Array.<Array>>} rowcol [row][col][cell]
*/
TableLayout.prototype.beforeMeasure = function(parts, rowcol) { };
/**
* @ignore
* @this {TableLayout}
* @param {List} parts
* @param {Array.<Array.<Array>>} rowcol [row][col][cell]
*/
TableLayout.prototype.afterArrange = function(parts, rowcol) { };
/**
* @ignore
* @this {TableLayout}
*/
TableLayout.prototype.measureTable = function(width, height, children, union, minw, minh) {
var l = children.length;
// Make the array that holds [rows][cols] of the table
var rowcol = []; // saved (so no temp array) starts as an array of rows, will end up [row][col][cell]
for (var i = 0; i < l; i++) {
var child = children.elt(i);
if (!rowcol[child.row]) {
rowcol[child.row] = []; // make new column for this row
}
if (!rowcol[child.row][child.column]) {
rowcol[child.row][child.column] = []; // new list for this cell
}
rowcol[child.row][child.column].push(child); // push child into right cell
}
this.beforeMeasure(children, rowcol);
// Reset the row/col definitions because the ones from last measure are irrelevant
var resetCols = []; // keep track of which columns we've already reset
// Objects that span multiple columns and
var spanners = [];
var nosize = [];
// These hashes are used to tally the number of rows and columns that do not have a size
var nosizeCols = { 'count': 0 };
var nosizeRows = { 'count': 0 };
var colleft = width;
var rowleft = height;
var defs = this._rowDefs;
l = defs.length;
for (var i = 0; i < l; i++) {
var def = defs[i];
if (def !== undefined) def.actual = 0;
}
defs = this._colDefs;
l = defs.length;
for (var i = 0; i < l; i++) {
var def = defs[i];
if (def !== undefined) def.actual = 0;
}
var lrow = rowcol.length; //number of rows
var lcol = 0;
for (var i = 0; i < lrow; i++) {
if (!rowcol[i]) continue;
lcol = Math.max(lcol, rowcol[i].length); // column length in this row
}
// Go through each cell (first pass)
var amt = 0.0;
lrow = rowcol.length; //number of rows
for (var i = 0; i < lrow; i++) {
if (!rowcol[i]) continue;
lcol = rowcol[i].length; // column length in this row
var rowHerald = this.getRowDefinition(i);
rowHerald.actual = 0; // Reset rows (only on first pass)
for (var j = 0; j < lcol; j++) {
//foreach column j in row i...
if (!rowcol[i][j]) continue;
var colHerald = this.getColumnDefinition(j);
if (resetCols[j] === undefined) { // make sure we only reset these once
colHerald.actual = 0;
resetCols[j] = true;
}
var cell = rowcol[i][j];
var len = cell.length;
for (var k = 0; k < len; k++) {
//foreach element in cell, measure
var child = cell[k];
// Skip children that span more than one row or column or do not have a set size
var spanner = (child.rowSpan > 1 || child.columnSpan > 1);
if (spanner) {
spanners.push(child);
// We used to not measure spanners twice, but now we do
// The reason is that there may be a row whose size
// is dictated by an object with columnSpan 2+ and vice versa
// continue;
}
var marg = child.margin;
var margw = marg.right + marg.left;
var margh = marg.top + marg.bottom;
var stretch = this.getEffectiveTableStretch(child, rowHerald, colHerald);
var dsize = child.resizeObject.desiredSize;
var realwidth = !(isNaN(dsize.width));
var realheight = !(isNaN(dsize.height));
var realsize = realwidth && realheight;
if (!spanner && stretch !== go.GraphObject.None && !realsize) {
if (nosizeCols[j] === undefined && (stretch === go.GraphObject.Fill || stretch === go.GraphObject.Horizontal)) {
nosizeCols[j] = -1; nosizeCols.count++;
}
if (nosizeRows[i] === undefined && (stretch === go.GraphObject.Fill || stretch === go.GraphObject.Vertical)) {
nosizeRows[i] = -1; nosizeRows.count++;
}
nosize.push(child);
}
if (stretch !== go.GraphObject.None) {
var unrestrictedSize = new go.Size(NaN, NaN);
//if (stretch !== go.GraphObject.Horizontal) unrestrictedSize.height = rowHerald.minimum;
//if (stretch !== go.GraphObject.Vertical) unrestrictedSize.width = colHerald.minimum;
//??? allow resizing during measure phase
child.resizeObject.desiredSize = unrestrictedSize;
child.ensureBounds();
}
var m = this.getLayoutBounds(child);
var mwidth = Math.max(m.width + margw, 0);
var mheight = Math.max(m.height + margh, 0);
// Make sure the heralds have the right layout size
// the row/column should use the largest meausured size of any
// GraphObject contained, constrained by mins and maxes
if (child.rowSpan === 1 && (realheight || stretch === go.GraphObject.None || stretch === go.GraphObject.Horizontal)) {
var def = this.getRowDefinition(i);
amt = Math.max(mheight - def.actual, 0);
if (amt > rowleft) amt = rowleft;
def.actual = def.actual + amt;
rowleft = Math.max(rowleft - amt, 0);
}
if (child.columnSpan === 1 && (realwidth || stretch === go.GraphObject.None || stretch === go.GraphObject.Vertical)) {
var def = this.getColumnDefinition(j);
amt = Math.max(mwidth - def.actual, 0);
if (amt > colleft) amt = colleft;
def.actual = def.actual + amt;
colleft = Math.max(colleft - amt, 0);
}
} // end cell
} // end col
} //end row
// For objects of no desired size we allocate what is left as we go,
// or else what is already in the column
var totalColWidth = 0.0;
var totalRowHeight = 0.0;
l = this.columnCount;
for (var i = 0; i < l; i++) {
if (this._colDefs[i] === undefined) continue;
totalColWidth += this.getColumnDefinition(i).actual;
}
l = this.rowCount;
for (var i = 0; i < l; i++) {
if (this._rowDefs[i] === undefined) continue;
totalRowHeight += this.getRowDefinition(i).actual;
}
colleft = Math.max(width - totalColWidth, 0);
rowleft = Math.max(height - totalRowHeight, 0);
var originalrowleft = rowleft;
var originalcolleft = colleft;
// Determine column sizes for the yet-to-be-sized columns
l = nosize.length;
for (var i = 0; i < l; i++) {
var child = nosize[i];
var rowHerald = this.getRowDefinition(child.row);
var colHerald = this.getColumnDefinition(child.column);
// We want to gather the largest difference between desired and expected col/row sizes
var mb = this.getLayoutBounds(child);
var marg = child.margin;
var margw = marg.right + marg.left;
var margh = marg.top + marg.bottom;
if (colHerald.actual === 0 && nosizeCols[child.column] !== undefined) {
nosizeCols[child.column] = Math.max(mb.width + margw, nosizeCols[child.column]);
} else {
nosizeCols[child.column] = null; // obey the column herald
}
if (rowHerald.actual === 0 && nosizeRows[child.row]!== undefined) {
nosizeRows[child.row] = Math.max(mb.height + margh, nosizeRows[child.row]);
} else {
nosizeRows[child.row] = null; // obey the row herald
}
}
// we now have the size that all these columns prefer to be
// we also have the amount left over
var desiredRowTotal = 0.0;
var desiredColTotal = 0.0;
for (i in nosizeRows) { if (i !== 'count') desiredRowTotal += nosizeRows[i] }
for (i in nosizeCols) { if (i !== 'count') desiredColTotal += nosizeCols[i] }
var allowedSize = new go.Size(); // used in stretch and span loops
// Deal with objects that have a stretch
for (var i = 0; i < l; i++) {
var child = nosize[i];
var rowHerald = this.getRowDefinition(child.row);
var colHerald = this.getColumnDefinition(child.column);
var w = 0.0;
if (isFinite(colHerald.width)) {
w = colHerald.width;
} else {
if (isFinite(colleft) && nosizeCols[child.column] !== null) {
if (desiredColTotal === 0) w = colHerald.actual + colleft;
else w = /*colHerald.actual +*/ ((nosizeCols[child.column] / desiredColTotal) * originalcolleft);
} else {
// Only use colHerald.actual if it was nonzero before this loop
if (nosizeCols[child.column] !== null) w = colleft;
else w = colHerald.actual || colleft;
//w = nosizeCols[child.column] || colleft; // Older, less correct way
}
w = Math.max(0, w - colHerald.computeEffectiveSpacing());
}
var h = 0.0;
if (isFinite(rowHerald.height)) {
h = rowHerald.height;
} else {
if (isFinite(rowleft) && nosizeRows[child.row] !== null) {
if (desiredRowTotal === 0) h = rowHerald.actual + rowleft;
else h = /*rowHerald.actual +*/ ((nosizeRows[child.row] / desiredRowTotal) * originalrowleft);
} else {
// Only use rowHerald.actual if it was nonzero before this loop
if (nosizeRows[child.row] !== null) h = rowleft;
else h = rowHerald.actual || rowleft;
//h = nosizeRows[child.row] || rowleft; // Older, less correct way
}
h = Math.max(0, h - rowHerald.computeEffectiveSpacing());
}
allowedSize.setTo(
Math.max(colHerald.minimum, Math.min(w, colHerald.maximum)),
Math.max(rowHerald.minimum, Math.min(h, rowHerald.maximum)));
// Which way do we care about fill:
var stretch = this.getEffectiveTableStretch(child, rowHerald, colHerald);
// This used to set allowedSize height/width to Infinity,
// but we can only set it to the current row/column space, plus rowleft/colleft values, at most.
switch (stretch) {
case go.GraphObject.Horizontal: // H stretch means it can be as large as its wants vertically
allowedSize.height = Math.max(allowedSize.height, rowHerald.actual + rowleft);
break;
case go.GraphObject.Vertical: // vice versa
allowedSize.width = Math.max(allowedSize.width, colHerald.actual + colleft);
break;
}
var marg = child.margin;
var margw = marg.right + marg.left;
var margh = marg.top + marg.bottom;
var m = this.getLayoutBounds(child);
var mwidth = Math.max(m.width + margw, 0);
var mheight = Math.max(m.height + margh, 0);
if (isFinite(colleft)) mwidth = Math.min(mwidth, allowedSize.width);
if (isFinite(rowleft)) mheight = Math.min(mheight, allowedSize.height);
var oldAmount = 0.0;
oldAmount = rowHerald.actual;
rowHerald.actual = Math.max(rowHerald.actual, mheight);
amt = rowHerald.actual - oldAmount;
rowleft = Math.max(rowleft - amt, 0);
oldAmount = colHerald.actual;
colHerald.actual = Math.max(colHerald.actual, mwidth);
amt = colHerald.actual - oldAmount;
colleft = Math.max(colleft - amt, 0);
} // end no fixed size objects
// Go through each object that spans multiple rows or columns
var additionalSpan = new go.Size();
l = spanners.length;
if (l !== 0) {
// record the actual sizes of every row/column before measuring spanners
// because they will change during the loop and we want to use their 'before' values
var actualSizeRows = [];
var actualSizeColumns = [];
for (var i = 0; i < lrow; i++) {
if (!rowcol[i]) continue;
lcol = rowcol[i].length; // column length in this row
var rowHerald = this.getRowDefinition(i);
actualSizeRows[i] = rowHerald.actual;
for (var j = 0; j < lcol; j++) {
//foreach column j in row i...
if (!rowcol[i][j]) continue;
var colHerald = this.getColumnDefinition(j);
actualSizeColumns[j] = colHerald.actual;
}
}
}
for (var i = 0; i < l; i++) {
var child = spanners[i];
var rowHerald = this.getRowDefinition(child.row);
var colHerald = this.getColumnDefinition(child.column);
// If there's a set column width/height we don't care about the given width/height
allowedSize.setTo(
Math.max(colHerald.minimum, Math.min(width, colHerald.maximum)),
Math.max(rowHerald.minimum, Math.min(height, rowHerald.maximum)));
// If it is a spanner and has a fill:
var stretch = this.getEffectiveTableStretch(child, rowHerald, colHerald);
switch (stretch) {
case go.GraphObject.Fill:
if (actualSizeColumns[colHerald.index] !== 0) allowedSize.width = Math.min(allowedSize.width, actualSizeColumns[colHerald.index]);
if (actualSizeRows[rowHerald.index] !== 0) allowedSize.height = Math.min(allowedSize.height, actualSizeRows[rowHerald.index]);
break;
case go.GraphObject.Horizontal:
if (actualSizeColumns[colHerald.index] !== 0) allowedSize.width = Math.min(allowedSize.width, actualSizeColumns[colHerald.index]);
break;
case go.GraphObject.Vertical:
if (actualSizeRows[rowHerald.index] !== 0) allowedSize.height = Math.min(allowedSize.height, actualSizeRows[rowHerald.index]);
break;
}
// If there's a set column width/height we don't care about any of the above:
if (isFinite(colHerald.width)) allowedSize.width = colHerald.width;
if (isFinite(rowHerald.height)) allowedSize.height = rowHerald.height;
var marg = child.margin;
var margw = marg.right + marg.left;
var margh = marg.top + marg.bottom;
var m = this.getLayoutBounds(child);
var mwidth = Math.max(m.width + margw, 0);
var mheight = Math.max(m.height + margh, 0);
var totalRow = 0.0;
for (var n = 0; n < child.rowSpan; n++) {
if (child.row + n >= this.rowCount) break; // if the row exists at all
def = this.getRowDefinition(child.row + n);
totalRow += def.total || 0;
}
// def is the last row definition
if (totalRow < mheight) {
var roomLeft = mheight - totalRow;
while (roomLeft > 0) { // Add the extra to the first row that allows us to
var act = def.actual || 0;
if (isNaN(def.height) && def.maximum > act) {
def.actual = Math.min(def.maximum, act + roomLeft);
if (def.actual !== act) roomLeft -= def.actual - act;
}
if (def.index - 1 === -1) break;
def = this.getRowDefinition(def.index - 1);
}
}
var totalCol = 0.0;
for (var n = 0; n < child.columnSpan; n++) {
if (child.column + n >= this.columnCount) break; // if the col exists at all
def = this.getColumnDefinition(child.column + n);
totalCol += def.total || 0;
}
// def is the last col definition
if (totalCol < mwidth) {
var roomLeft = mwidth - totalCol;
while (roomLeft > 0) { // Add the extra to the first row that allows us to
var act = def.actual || 0;
if (isNaN(def.width) && def.maximum > act) {
def.actual = Math.min(def.maximum, act + roomLeft);
if (def.actual !== act) roomLeft -= def.actual - act;
}
if (def.index - 1 === -1) break;
def = this.getColumnDefinition(def.index - 1);
}
}
} // end spanning objects
l = this.columnCount;
for (var i = 0; i < l; i++) {
if (this._colDefs[i] === undefined) continue;
def = this.getColumnDefinition(i);
def.position = union.width;
if (def.actual !== 0) {
union.width += def.actual;
union.width += def.computeEffectiveSpacing();
}
}
l = this.rowCount;
for (var i = 0; i < l; i++) {
if (this._rowDefs[i] === undefined) continue;
def = this.getRowDefinition(i);
def.position = union.height;
if (def.actual !== 0) {
union.height += def.actual;
union.height += def.computeEffectiveSpacing();
}
}
// save these for arrange (destroy them or not? Possibly needed for drawing spacers)
return rowcol;
}; // end measureTable
/**
* @ignore
* @this {TableLayout}
*/
TableLayout.prototype.arrangeTable = function(children, union, rowcol) {
var l = children.length;
var originx = this.arrangementOrigin.x;
var originy = this.arrangementOrigin.y;
var x = 0.0;
var y = 0.0;
var lrow = rowcol.length; //number of rows
var lcol = 0;
for (var i = 0; i < lrow; i++) {
if (!rowcol[i]) continue;
lcol = Math.max(lcol, rowcol[i].length); // column length in this row
}
var additionalSpan = new go.Size();
// Find cell space and arrange objects:
for (var i = 0; i < lrow; i++) {
if (!rowcol[i]) continue;
lcol = rowcol[i].length; // column length in this row
var rowHerald = this.getRowDefinition(i);
y = originy + rowHerald.position + rowHerald.computeEffectiveSpacingTop();
for (var j = 0; j < lcol; j++) {
//foreach column j in row i...
if (!rowcol[i][j]) continue;
var colHerald = this.getColumnDefinition(j);
x = originx + colHerald.position + colHerald.computeEffectiveSpacingTop();
var cell = rowcol[i][j];
var len = cell.length;
for (var k = 0; k < len; k++) {
//foreach element in cell
var child = cell[k];
// add to layoutWidth/Height any additional span
additionalSpan.setTo(0, 0);
for (var n = 1; n < child.rowSpan; n++) {
// if the row exists at all
if (i + n >= this.rowCount) break;
var rh = this.getRowDefinition(i + n);
additionalSpan.height += rh.total;
}
for (var n = 1; n < child.columnSpan; n++) {
// if the col exists at all
if (j + n >= this.columnCount) break;
var ch = this.getColumnDefinition(j + n);
additionalSpan.width += ch.total;
}
// Construct containing rect (cell):
// total width and height of the cell that an object could possibly be created in
var colwidth = colHerald.actual + additionalSpan.width;
var rowheight = rowHerald.actual + additionalSpan.height;
// construct a rect that represents the total cell size allowed for this object
var ar = new go.Rect();
ar.x = x;
ar.y = y;
ar.width = colwidth;
ar.height = rowheight;
// Also keep them for clip values
var cellx = x;
var celly = y;
var cellw = colwidth;
var cellh = rowheight;
// Ending rows/col might have actual spaces that are larger than the remaining space
// Modify them for clipping regions
if (x + colwidth > union.width) cellw = Math.max(union.width - x, 0);
if (y + rowheight > union.height) cellh = Math.max(union.height - y, 0);
// Construct alignment:
var align = child.alignment;
var alignx = 0.0;
var aligny = 0.0;
var alignoffsetX = 0.0;
var alignoffsetY = 0.0;
if (align.isDefault()) {
align = this.defaultAlignment;
if (!align.isSpot()) align = go.Spot.Center;
alignx = align.x;
aligny = align.y;
alignoffsetX = align.offsetX;
alignoffsetY = align.offsetY;
var ca = colHerald.alignment;
var ra = rowHerald.alignment;
if (ca.isSpot()) {
alignx = ca.x;
alignoffsetX = ca.offsetX;
}
if (ra.isSpot()) {
aligny = ra.y;
alignoffsetY = ra.offsetY;
}
} else {
alignx = align.x;
aligny = align.y;
alignoffsetX = align.offsetX;
alignoffsetY = align.offsetY;
}
// same as if (!align.isSpot()) align = go.Spot.Center;
if (isNaN(alignx) || isNaN(aligny)) {
alignx = 0.5;
aligny = 0.5;
alignoffsetX = 0;
alignoffsetY = 0;
}
var width = 0.0;
var height = 0.0;
var marg = child.margin;
var margw = marg.left + marg.right;
var margh = marg.top + marg.bottom;
var stretch = this.getEffectiveTableStretch(child, rowHerald, colHerald);
if (/* isNaN(child.resizeObject.desiredSize.width) && */ (stretch === go.GraphObject.Fill || stretch === go.GraphObject.Horizontal))
width = Math.max(colwidth - margw, 0);
else
width = this.getLayoutBounds(child).width;
if (/* isNaN(child.resizeObject.desiredSize.height) && */ (stretch === go.GraphObject.Fill || stretch === go.GraphObject.Vertical))
height = Math.max(rowheight - margh, 0);
else
height = this.getLayoutBounds(child).height;
// min and max override any stretch values
var max = child.maxSize;
var min = child.minSize;
width = Math.min(max.width, width);
height = Math.min(max.height, height);
width = Math.max(min.width, width);
height = Math.max(min.height, height);
var widthmarg = width + margw;
var heightmarg = height + margh;
ar.x += (ar.width * alignx) - (widthmarg * alignx) + alignoffsetX + marg.left;
ar.y += (ar.height * aligny) - (heightmarg * aligny) + alignoffsetY + marg.top;
child.moveTo(ar.x, ar.y);
if (stretch !== go.GraphObject.None) {
child.resizeObject.desiredSize = new go.Size(width, height);
}
} // end cell
} // end col
} //end row
}; // end arrangeTable
// end TableLayout class
+385
View File
@@ -0,0 +1,385 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// These are the definitions for all of the predefined templates and tool archetypes.
// You do not need to load this file in order to use the default templates and archetypes.
// Although we have tried to provide definitions here that are faithful to how they
// are actually implemented, there may be some differences from what is in the library.
// Caution: these may change in a future version.
// Set up the default templates that each Diagram starts off with.
function setupDiagramTemplates(diagram /* : go.Diagram */) {
// Node Templates
var nodeTemplateMap = new go.Map(/*'string', go.Part*/);
// create the default Node template
var archnode = new go.Node();
var nodet = new go.TextBlock();
nodet.bind(new go.Binding('text', '', go.Binding.toString));
archnode.add(nodet);
nodeTemplateMap.add('', archnode);
// create the default Comment Node template
var archcmnt = new go.Node();
var nodec = new go.TextBlock();
nodec.stroke = 'brown';
nodec.bind(new go.Binding('text', '', go.Binding.toString));
archcmnt.add(nodec);
nodeTemplateMap.add('Comment', archcmnt);
// create the default Link Label Node template
var archllab = new go.Node();
archllab.selectable = false;
archllab.avoidable = false;
var nodel = new go.Shape();
nodel.figure = 'Ellipse';
nodel.fill = 'black';
nodel.stroke = null;
nodel.desiredSize = new go.Size(3, 3);
archllab.add(nodel);
nodeTemplateMap.add('LinkLabel', archllab);
diagram.nodeTemplateMap = nodeTemplateMap;
// Group Templates
var groupTemplateMap = new go.Map(/*'string', go.Group*/);
// create the default Group template
var archgrp = new go.Group();
archgrp.selectionObjectName = 'GROUPPANEL';
archgrp.type = go.Panel.Vertical;
var grpt = new go.TextBlock();
grpt.font = 'bold 12pt sans-serif';
grpt.bind(new go.Binding('text', '', go.Binding.toString));
archgrp.add(grpt);
var grppan = new go.Panel(go.Panel.Auto);
grppan.name = 'GROUPPANEL';
var grpbord = new go.Shape();
grpbord.figure = 'Rectangle';
grpbord.fill = 'rgba(128,128,128,0.2)';
grpbord.stroke = 'black';
grppan.add(grpbord);
var phold = new go.Placeholder();
phold.padding = new go.Margin(5, 5, 5, 5);
grppan.add(phold);
archgrp.add(grppan);
groupTemplateMap.add('', archgrp);
diagram.groupTemplateMap = groupTemplateMap;
// Link Templates
var linkTemplateMap = new go.Map(/*'string', go.Link*/);
// create the default Link template
var archlink = new go.Link();
var archpath = new go.Shape();
archpath.isPanelMain = true;
archlink.add(archpath);
var archarrow = new go.Shape();
archarrow.toArrow = 'Standard';
archarrow.fill = 'black';
archarrow.stroke = null;
archarrow.strokeWidth = 0;
archlink.add(archarrow);
linkTemplateMap.add('', archlink);
// create the default Comment Link template
var archcmntlink = new go.Link();
var archcmntpath = new go.Shape();
archcmntpath.isPanelMain = true;
archcmntpath.stroke = 'brown';
archcmntlink.add(archcmntpath);
linkTemplateMap.add('Comment', archcmntlink);
diagram.linkTemplateMap = linkTemplateMap;
}
// Set up the default Panel.itemTemplate.
function setupDefaultItemTemplate(panel /* : go.Panel */) {
var architem = new go.Panel();
var itemtxt = new go.TextBlock();
itemtxt.bind(new go.Binding('text', '', go.Binding.toString));
architem.add(itemtxt);
panel.itemTemplate = architem;
}
// Set up the diagram's selection Adornments
function setupSelectionAdornments(diagram /* : go.Diagram */) {
// create the default Adornment for selection
var selad = new go.Adornment();
selad.type = go.Panel.Auto;
var seladhandle = new go.Shape();
seladhandle.fill = null;
seladhandle.stroke = 'dodgerblue';
seladhandle.strokeWidth = 3;
selad.add(seladhandle);
var selplace = new go.Placeholder();
selplace.margin = new go.Margin(1.5, 1.5, 1.5, 1.5);
selad.add(selplace);
diagram.nodeSelectionAdornmentTemplate = selad;
// reuse the default Node Adornment for selection
diagram.groupSelectionAdornmentTemplate = selad;
// create the default Link Adornment for selection
selad = new go.Adornment();
selad.type = go.Panel.Link;
seladhandle = new go.Shape();
seladhandle.isPanelMain = true;
seladhandle.fill = null;
seladhandle.stroke = 'dodgerblue';
seladhandle.strokeWidth = 3; //?? zero to use selection object's strokeWidth is often not wide enough
selad.add(seladhandle);
diagram.linkSelectionAdornmentTemplate = selad;
}
// Set up the background Grid Panel.
function setupDefaultBackgroundGrid(diagram /* : go.Diagram */) {
var grid = new go.Panel(go.Panel.Grid);
grid.name = 'GRID';
var hlines = new go.Shape();
hlines.figure = 'LineH';
hlines.stroke = 'lightgray';
hlines.strokeWidth = 0.5;
hlines.interval = 1;
grid.add(hlines);
hlines = new go.Shape();
hlines.figure = 'LineH';
hlines.stroke = 'gray';
hlines.strokeWidth = 0.5;
hlines.interval = 5;
grid.add(hlines);
hlines = new go.Shape();
hlines.figure = 'LineH';
hlines.stroke = 'gray';
hlines.strokeWidth = 1;
hlines.interval = 10;
grid.add(hlines);
var vlines = new go.Shape();
vlines.figure = 'LineV';
vlines.stroke = 'lightgray';
vlines.strokeWidth = 0.5;
vlines.interval = 1;
grid.add(vlines);
vlines = new go.Shape();
vlines.figure = 'LineV';
vlines.stroke = 'gray';
vlines.strokeWidth = 0.5;
vlines.interval = 5;
grid.add(vlines);
vlines = new go.Shape();
vlines.figure = 'LineV';
vlines.stroke = 'gray';
vlines.strokeWidth = 1;
vlines.interval = 10;
grid.add(vlines);
grid.visible = false; // by default the grid is not visible
// Create the Part that holds the grid.
//var gridpart = new go.Part();
//gridpart.add(grid);
//gridpart.layerName = 'Grid'; // goes in the "Grid" layer
//gridpart.zOrder = 0; // to make it easier for other background parts to be behind the grid
//gridpart.isInDocumentBounds = false; // never part of the document bounds
//gridpart.isAnimated = false; // not animated
//gridpart.pickable = false; // user cannot pick it with mouse/touch/stylus
//gridpart.locationObjectName = 'GRID';
//diagram.add(gridpart);
// So then: diagram.grid === grid
// BUT, the gridpart is not actually in the Diagram.parts collection,
// and that Part cannot be replaced; so the above code is commented out.
// Instead, this works in an existing GoJS environment:
diagram.grid = grid;
}
// Set up the "viewport" box part that is the initial value of Overview.box.
function setupOverviewBox(overview /* : go.Overview */) {
var box = new go.Part();
var s = new go.Shape();
s.stroke = 'magenta';
s.strokeWidth = 2;
s.fill = 'transparent';
s.name = 'BOXSHAPE';
box.selectable = true;
box.selectionObjectName = 'BOXSHAPE';
box.locationObjectName = 'BOXSHAPE';
//box.resizable = true;
box.resizeObjectName = 'BOXSHAPE';
box.cursor = 'move';
box.add(s);
// only resize the bottom-right corner
var ad = new go.Adornment();
ad.type = go.Panel.Spot;
ad.locationSpot = go.Spot.Center;
var ph = new go.Placeholder();
ph.isPanelMain = true;
ad.add(ph);
var hnd = new go.Shape();
hnd.alignmentFocus = go.Spot.Center;
hnd.figure = 'Rectangle';
hnd.desiredSize = new go.Size(64, 64);
hnd.cursor = 'se-resize';
hnd.alignment = go.Spot.BottomRight;
ad.add(hnd);
box.resizeAdornmentTemplate = ad;
overview.box = box;
}
// Set up LinkingBaseTool's default temporary nodes and link.
function setupLinkingToolTemporaryNodesAndLink(tool /* : go.LinkingBaseTool */) {
// LinkingTool.temporaryLink
var link = new go.Link();
var path = new go.Shape();
path.isPanelMain = true;
path.stroke = 'blue';
link.add(path);
var arrow = new go.Shape();
arrow.toArrow = 'Standard';
arrow.fill = 'blue';
arrow.stroke = 'blue';
link.add(arrow);
link.layerName = 'Tool';
tool.temporaryLink = link;
// LinkingTool.temporaryFromNode and .temporaryFromPort
var fromNode = new go.Node();
var fromPort = new go.Shape();
fromPort.portId = '';
fromPort.figure = 'Rectangle';
fromPort.fill = null;
fromPort.stroke = 'magenta';
fromPort.strokeWidth = 2;
fromPort.desiredSize = new go.Size(1, 1);
fromNode.add(fromPort);
fromNode.selectable = false;
fromNode.layerName = 'Tool';
tool.temporaryFromNode = fromNode;
tool.temporaryFromPort = fromPort;
// LinkingTool.temporaryToNode and .temporaryToPort
var toNode = new go.Node();
var toPort = new go.Shape();
toPort.portId = '';
toPort.figure = 'Rectangle';
toPort.fill = null;
toPort.stroke = 'magenta';
toPort.strokeWidth = 2;
toPort.desiredSize = new go.Size(1, 1);
toNode.add(toPort);
toNode.selectable = false;
toNode.layerName = 'Tool';
tool.temporaryToNode = toNode;
tool.temporaryToPort = toPort;
}
// Set up RelinkingTool's default handle archetypes
function setupRelinkingToolHandles(tool /* : go.RelinkingTool */) {
var h = new go.Shape();
h.figure = 'Diamond';
h.desiredSize = new go.Size(8, 8);
h.fill = 'lightblue';
h.stroke = 'dodgerblue';
h.cursor = 'pointer';
h.segmentIndex = 0;
tool.fromHandleArchetype = h;
h = new go.Shape();
h.figure = 'Diamond';
h.desiredSize = new go.Size(8, 8);
h.fill = 'lightblue';
h.stroke = 'dodgerblue';
h.cursor = 'pointer';
h.segmentIndex = -1;
tool.toHandleArchetype = h;
}
// Set up LinkReshapingTool's default handle archetypes
function setupLinkReshapingToolHandles(tool /* : go.LinkReshapingTool */) {
var h = new go.Shape();
h.figure = 'Rectangle';
h.desiredSize = new go.Size(6, 6);
h.fill = 'lightblue';
h.stroke = 'dodgerblue';
tool.handleArchetype = h;
h = new go.Shape();
h.figure = 'Diamond';
h.desiredSize = new go.Size(8, 8);
h.fill = 'lightblue';
h.stroke = 'dodgerblue';
h.cursor = 'move';
tool.midHandleArchetype = h;
}
// Set up ResizingTool's default handle archetype
function setupResizingToolHandles(tool /* : go.ResizingTool */) {
var h = new go.Shape();
h.alignmentFocus = go.Spot.Center;
h.figure = 'Rectangle';
h.desiredSize = new go.Size(6, 6);
h.fill = 'lightblue';
h.stroke = 'dodgerblue';
h.strokeWidth = 1;
h.cursor = 'pointer';
tool.handleArchetype = h;
}
// Set up RotatingTool's default handle archetype
function setupRotatingToolHandles(tool /* : go.RotatingTool */) {
var h = new go.Shape();
h.figure = 'Ellipse';
h.desiredSize = new go.Size(8, 8);
h.fill = 'lightblue';
h.stroke = 'dodgerblue';
h.strokeWidth = 1;
h.cursor = 'pointer';
tool.handleArchetype = h;
}
// Set up DragSelectingTool's default box
function setupDragSelectingToolBox(tool /* : go.DragSelectingTool */) {
var b = new go.Part();
b.layerName = 'Tool';
b.selectable = false;
var r = new go.Shape();
r.name = 'SHAPE';
r.figure = 'Rectangle';
r.fill = null;
r.stroke = 'magenta';
b.add(r);
tool.box = b;
}
+75
View File
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<title>HTMLInfo Text Editor</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="A re-implementation of the default text editor, implemented by the TextEditor extension." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="TextEditor.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = $(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
"undoManager.isEnabled": true // enable undo & redo
});
myDiagram.toolManager.textEditingTool.defaultTextEditor = window.TextEditor;
// this predicate is true if the new string has at least three characters
// and has a vowel in it
function okName(textblock, oldstr, newstr) {
return newstr.length >= 3 && /[aeiouy]/i.test(newstr);
};
// define a simple Node template
myDiagram.nodeTemplate =
$(go.Node, "Auto", // the Shape will go around the TextBlock
$(go.Shape, "RoundedRectangle", { strokeWidth: 0 },
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 8, editable: true, textValidation: okName }, // some room around the text
new go.Binding("text", "key"))
);
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Alpha", color: "lightblue" },
{ key: "Beta", color: "orange" },
{ key: "Gamma", color: "lightgreen" },
{ key: "Delta", color: "pink" }
],
[
{ from: "Alpha", to: "Beta" },
{ from: "Alpha", to: "Gamma" },
{ from: "Beta", to: "Beta" },
{ from: "Gamma", to: "Delta" },
{ from: "Delta", to: "Alpha" }
]);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<!-- The DIV for the Diagram needs an explicit size or else we won't see anything.
Also add a border to help see the edges. -->
<div id="myDiagramDiv" style="border: solid 1px black; width:400px; height:400px"></div>
<p>This sample constructs an <a>HTMLInfo</a> that acts as a re-implementation of the default text editor.
<p>The implementation is contained in the file <a href="TextEditor.js" target="_blank">TextEditor.js</a>
and exposes <code>window.TextEditor</code>,
which is used in this file as the value of <code>myDiagram.toolManager.textEditingTool.defaultTextEditor</code>.
<p>This also adds a text validation predicate, <code>okName</code>, as the <a>TextBlock.textValidation</a> property.
That predicate makes sure that the new string has at least three characters and contains a vowel.
<p>You can see additional custom text editors in the <a href="../samples/customTextEditingTool.html">Custom TextEditingTool sample</a>.
</div>
</body>
</html>
+154
View File
@@ -0,0 +1,154 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// This is the definitions of the predefined text editor used by TextEditingTool
// when you set or bind TextBlock.editable to true.
// You do not need to load this file in order to use in-place text editing.
// HTML + JavaScript text editor menu, made with HTMLInfo
// This is a re-implementation of the default text editor
// This file exposes one instance of HTMLInfo, window.TextEditor
// See also TextEditor.html
(function(window) {
var textarea = document.createElement('textarea');
textarea.id = "myTextArea";
textarea.addEventListener('input', function(e) {
var tool = TextEditor.tool;
if (tool.textBlock === null) return;
var tempText = tool.measureTemporaryTextBlock(this.value);
var scale = this.textScale;
this.style.width = 20 + tempText.measuredBounds.width * scale + 'px';
this.rows = tempText.lineCount;
}, false);
textarea.addEventListener('keydown', function(e) {
var tool = TextEditor.tool;
if (tool.textBlock === null) return;
var keynum = e.which;
if (keynum === 13) { // Enter
if (tool.textBlock.isMultiline === false) e.preventDefault();
tool.acceptText(go.TextEditingTool.Enter);
return;
} else if (keynum === 9) { // Tab
tool.acceptText(go.TextEditingTool.Tab);
e.preventDefault();
return;
} else if (keynum === 27) { // Esc
tool.doCancel();
if (tool.diagram !== null) tool.diagram.doFocus();
}
}, false);
// handle focus:
textarea.addEventListener('focus', function(e) {
var tool = TextEditor.tool;
if (!tool || tool.currentTextEditor === null) return;
if (tool.state === go.TextEditingTool.StateActive) {
tool.state = go.TextEditingTool.StateEditing;
}
if (tool.selectsTextOnActivate) {
textarea.select();
textarea.setSelectionRange(0, 9999);
}
}, false);
// Disallow blur.
// If the textEditingTool blurs and the text is not valid,
// we do not want focus taken off the element just because a user clicked elsewhere.
textarea.addEventListener('blur', function(e) {
var tool = TextEditor.tool;
if (!tool || tool.currentTextEditor === null || tool.state === go.TextEditingTool.StateNone) return;
textarea.focus();
if (tool.selectsTextOnActivate) {
textarea.select();
textarea.setSelectionRange(0, 9999);
}
}, false);
var TextEditor = new go.HTMLInfo();
TextEditor.valueFunction = function() { return textarea.value; }
TextEditor.mainElement = textarea; // to reference it more easily
TextEditor.tool = null; // Initialize
// used to be in doActivate
TextEditor.show = function(textBlock, diagram, tool) {
if (!(textBlock instanceof go.TextBlock)) return;
if (TextEditor.tool !== null) return; // Only one at a time.
TextEditor.tool = tool; // remember the TextEditingTool for use by listeners
// This is called during validation, if validation failed:
if (tool.state === go.TextEditingTool.StateInvalid) {
textarea.style.border = '3px solid red';
textarea.focus();
return;
}
// This part is called during initalization:
var loc = textBlock.getDocumentPoint(go.Spot.Center);
var pos = diagram.position;
var sc = diagram.scale;
var textscale = textBlock.getDocumentScale() * sc;
if (textscale < tool.minimumEditorScale) textscale = tool.minimumEditorScale;
// Add slightly more width/height to stop scrollbars and line wrapping on some browsers
// +6 is firefox minimum, otherwise lines will be wrapped improperly
var textwidth = (textBlock.naturalBounds.width * textscale) + 6;
var textheight = (textBlock.naturalBounds.height * textscale) + 2;
var left = (loc.x - pos.x) * sc;
var top = (loc.y - pos.y) * sc;
textarea.value = textBlock.text;
// the only way you can mix font and fontSize is if the font inherits and the fontSize overrides
// in the future maybe have textarea contained in its own div
diagram.div.style['font'] = textBlock.font;
var paddingsize = 1;
textarea.style['position'] = 'absolute';
textarea.style['zIndex'] = '100';
textarea.style['font'] = 'inherit';
textarea.style['fontSize'] = (textscale * 100) + '%';
textarea.style['lineHeight'] = 'normal';
textarea.style['width'] = (textwidth) + 'px';
textarea.style['left'] = ((left - (textwidth / 2) | 0) - paddingsize) + 'px';
textarea.style['top'] = ((top - (textheight / 2) | 0) - paddingsize) + 'px';
textarea.style['textAlign'] = textBlock.textAlign;
textarea.style['margin'] = '0';
textarea.style['padding'] = paddingsize + 'px';
textarea.style['border'] = '0';
textarea.style['outline'] = 'none';
textarea.style['whiteSpace'] = 'pre-wrap';
textarea.style['overflow'] = 'hidden'; // for proper IE wrap
textarea.rows = textBlock.lineCount;
textarea.textScale = textscale; // attach a value to the textarea, for convenience
textarea.className = 'goTXarea';
// Show:
diagram.div.appendChild(textarea);
// After adding, focus:
textarea.focus();
if (tool.selectsTextOnActivate) {
textarea.select();
textarea.setSelectionRange(0, 9999);
}
};
TextEditor.hide = function(diagram, tool) {
diagram.div.removeChild(textarea);
TextEditor.tool = null; // forget reference to TextEditingTool
}
window.TextEditor = TextEditor;
})(window);
+116
View File
@@ -0,0 +1,116 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// HTML + JavaScript text editor menu, using HTML radio inputs and HTMLInfo.
// This file exposes one instance of HTMLInfo, window.TextEditorRadioButtons
// see /samples/customTextEditingTool.html
// see also textEditorSelectBox.js for another custom editor
// see also textEditor.html for a re-implementation of the default text editor
(function(window) {
// Use the following HTML:
var customText = document.createElement("div");
customText.id = "customTextEditor";
customText.style.cssText = "border: 1px solid black; background-color: white;";
customText.innerHTML =
' <label for="One">One</label> <input type="radio" name="group1" id="One" value="One"> <br/>' +
' <label for="Two">Two</label> <input type="radio" name="group1" id="Two" value="Two"> <br/>' +
' <label for="Three">Three</label> <input type="radio" name="group1" id="Three" value="Three"> <br/>' +
' <label for="Four">Four</label> <input type="radio" name="group1" id="Four" value="Four">';
var customEditor = new go.HTMLInfo();
customEditor.show = function(textBlock, diagram, tool) {
if (!(textBlock instanceof go.TextBlock)) return;
var startingValue = textBlock.text;
// Populate the select box:
customText.innerHTML = "";
var list = textBlock.choices;
// Perhaps give some default choices if textBlock.choices is null
if (list === null) list = ["Default A", "Default B", "Default C"];
var l = list.length;
for (var i = 0; i < l; i++) {
var value = list[i];
var label = document.createElement("label");
var input = document.createElement("input");
label.htmlFor = value;
label.textContent = value;
input.type = "radio";
input.name = "group1";
input.id = value;
input.value = value;
customText.appendChild(label);
customText.appendChild(input);
if (i !== l-1) customText.appendChild(document.createElement("br"));
}
// consider also adding the current value, if it is not in the choices list
var children = customText.children
var l = children.length;
for (var i = 0; i < l; i++) {
var child = children[i];
if (!(child instanceof HTMLInputElement)) continue;
// Make sure the radio button that equals the text is checked
if (child.value == startingValue) {
child.checked = true;
}
// Finish immediately when a radio button is pressed
customText.addEventListener("change", function(e) {
tool.acceptText(go.TextEditingTool.Tab);
}, false);
}
// Do a few different things when a user presses a key
customText.addEventListener("keydown", function(e) {
var keynum = e.which;
if (keynum == 13) { // Accept on Enter
tool.acceptText(go.TextEditingTool.Enter);
return;
} else if (keynum == 9) { // Accept on Tab
tool.acceptText(go.TextEditingTool.Tab);
e.preventDefault();
return false;
} else if (keynum === 27) { // Cancel on Esc
tool.doCancel();
if (tool.diagram) tool.diagram.focus();
}
}, false);
var loc = textBlock.getDocumentPoint(go.Spot.TopLeft);
var pos = diagram.transformDocToView(loc);
customText.style.left = pos.x + "px";
customText.style.top = pos.y + "px";
customText.style.position = 'absolute';
customText.style.zIndex = 100; // place it in front of the Diagram
diagram.div.appendChild(customText);
}
customEditor.hide = function(diagram, tool) {
diagram.div.removeChild(customText);
}
// customText is a div and doesn't have a "value" field
// So we will make value into a function that will return
// the "value" of the checked radio button
customEditor.valueFunction = function() {
var children = customText.children
var l = children.length;
for (var i = 0; i < l; i++) {
var child = children[i];
if (!(child instanceof HTMLInputElement)) continue;
if (child.checked) {
return child.value;
}
}
return "";
}
window.TextEditorRadioButtons = customEditor;
})(window);
+71
View File
@@ -0,0 +1,71 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// HTML + JavaScript text editor using an HTML Select Element and HTMLInfo.
// This file exposes one instance of HTMLInfo, window.TextEditorSelectBox
// see /samples/customTextEditingTool.html
// see also textEditorRadioButton.js for another custom editor
// see also textEditor.html for a re-implementation of the default text editor
(function(window) {
var customEditor = new go.HTMLInfo();
var customSelectBox = document.createElement("select");
customEditor.show = function(textBlock, diagram, tool) {
if (!(textBlock instanceof go.TextBlock)) return;
// Populate the select box:
customSelectBox.innerHTML = "";
var list = textBlock.choices;
// Perhaps give some default choices if textBlock.choices is null
if (list === null) list = ["Default A", "Default B", "Default C"];
var l = list.length;
for (var i = 0; i < l; i++) {
var op = document.createElement("option");
op.text = list[i];
op.value = list[i];
customSelectBox.add(op, null);
// consider also adding the current value, if it is not in the choices list
}
// After the list is populated, set the value:
customSelectBox.value = textBlock.text;
// Do a few different things when a user presses a key
customSelectBox.addEventListener("keydown", function(e) {
var keynum = e.which;
if (keynum == 13) { // Accept on Enter
tool.acceptText(go.TextEditingTool.Enter);
return;
} else if (keynum == 9) { // Accept on Tab
tool.acceptText(go.TextEditingTool.Tab);
e.preventDefault();
return false;
} else if (keynum === 27) { // Cancel on Esc
tool.doCancel();
if (tool.diagram) tool.diagram.focus();
}
}, false);
var loc = textBlock.getDocumentPoint(go.Spot.TopLeft);
var pos = diagram.transformDocToView(loc);
customSelectBox.style.left = pos.x + "px";
customSelectBox.style.top = pos.y + "px";
customSelectBox.style.position = 'absolute';
customSelectBox.style.zIndex = 100; // place it in front of the Diagram
diagram.div.appendChild(customSelectBox);
}
customEditor.hide = function(diagram, tool) {
diagram.div.removeChild(customSelectBox);
}
customEditor.valueFunction = function() { return customSelectBox.value; }
window.TextEditorSelectBox = customEditor;
})(window);
+209
View File
@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html>
<head>
<title>Tree Map</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta name="description" content="Display hierarchical data by nesting, where the area of each node is proportional to some value for the node. Clicking consecutively results in selecting containing Groups." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="TreeMapLayout.js"></script>
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram =
$(go.Diagram, "myDiagramDiv", // must be the ID or reference to div
{
initialAutoScale: go.Diagram.Uniform,
"animationManager.isEnabled": false,
layout: $(TreeMapLayout, { isTopLevelHorizontal: false }),
allowMove: false, allowCopy: false, allowDelete: false
});
// change selection behavior to cycle up the chain of containing Groups
myDiagram.toolManager.clickSelectingTool.standardMouseSelect = function() {
var diagram = this.diagram;
if (diagram === null || !diagram.allowSelect) return;
var e = diagram.lastInput;
if (!(e.control || e.meta) && !e.shift) {
var part = diagram.findPartAt(e.documentPoint, false);
if (part !== null) {
var firstselected = null; // is this or any containing Group selected?
var node = part;
while (node !== null) {
if (node.isSelected) {
firstselected = node;
break;
} else {
node = node.containingGroup;
}
}
if (firstselected !== null) { // deselect this and select its containing Group
firstselected.isSelected = false;
var group = firstselected.containingGroup;
if (group !== null) group.isSelected = true;
return;
}
}
}
go.ClickSelectingTool.prototype.standardMouseSelect.call(this);
};
// Nodes and Groups are the absolute minimum template: no elements at all!
myDiagram.nodeTemplate =
$(go.Node,
{ background: "rgba(99,99,99,0.2)" },
new go.Binding("background", "fill"),
{
toolTip: $("ToolTip",
$(go.TextBlock, new go.Binding("text", "", tooltipString).ofObject())
)
}
);
myDiagram.groupTemplate =
$(go.Group, "Auto",
{ layout: null },
{ background: "rgba(99,99,99,0.2)" },
new go.Binding("background", "fill"),
{
toolTip: $("ToolTip",
$(go.TextBlock, new go.Binding("text", "", tooltipString).ofObject())
)
}
);
function tooltipString(part) {
if (part instanceof go.Adornment) part = part.adornedPart;
var msg = createPath(part);
msg += "\nsize: " + part.data.size;
if (part instanceof go.Group) {
var group = part;
msg += "\n# children: " + group.memberParts.count;
msg += "\nsubtotal size: " + group.data.total;
}
return msg;
}
function createPath(part) {
var parent = part.containingGroup;
return (parent !== null ? createPath(parent) + "/" : "") + part.data.text;
}
// generate a tree with the default values
rebuildGraph();
}
function rebuildGraph() {
var minNodes = document.getElementById("minNodes").value;
minNodes = parseInt(minNodes, 10);
var maxNodes = document.getElementById("maxNodes").value;
maxNodes = parseInt(maxNodes, 10);
var minChil = document.getElementById("minChil").value;
minChil = parseInt(minChil, 10);
var maxChil = document.getElementById("maxChil").value;
maxChil = parseInt(maxChil, 10);
// create and assign a new model
var model = new go.GraphLinksModel();
model.nodeGroupKeyProperty = "parent";
model.nodeDataArray = generateNodeData(minNodes, maxNodes, minChil, maxChil);
myDiagram.model = model;
}
// Creates a random number (between MIN and MAX) of randomly colored nodes.
function generateNodeData(minNodes, maxNodes, minChil, maxChil) {
var nodeArray = [];
if (minNodes === undefined || isNaN(minNodes) || minNodes < 1) minNodes = 1;
if (maxNodes === undefined || isNaN(maxNodes) || maxNodes < minNodes) maxNodes = minNodes;
// Create a bunch of node data
var numNodes = Math.floor(Math.random() * (maxNodes - minNodes + 1)) + minNodes;
for (var i = 0; i < numNodes; i++) {
var size = Math.random() * Math.random() * 10000; // non-uniform distribution
nodeArray.push({
key: i, // the unique identifier
isGroup: false, // many of these turn into groups, by code below
parent: undefined, // is set by code below that assigns children
text: i.toString(), // some text to be shown by the node template
fill: go.Brush.randomColor(), // a color to be shown by the node template
size: size,
total: -1 // use a negative value to indicate that the total for the group has not been computed
});
}
// Takes the random collection of node data and creates a random tree with them.
// Respects the minimum and maximum number of links from each node.
// The minimum can be disregarded if we run out of nodes to link to.
if (nodeArray.length > 1) {
if (minChil === undefined || isNaN(minChil) || minChil < 0) minChil = 0;
if (maxChil === undefined || isNaN(maxChil) || maxChil < minChil) maxChil = minChil;
// keep the Set of node data that do not yet have a parent
var available = new go.Set();
available.addAll(nodeArray);
for (var i = 0; i < nodeArray.length; i++) {
var parent = nodeArray[i];
available.remove(parent);
// assign some number of node data as children of this parent node data
var children = Math.floor(Math.random() * (maxChil - minChil + 1)) + minChil;
for (var j = 0; j < children; j++) {
var child = available.first();
if (child === null) break; // oops, ran out already
available.remove(child);
// have the child node data refer to the parent node data by its key
child.parent = parent.key;
if (!parent.isGroup) { // make sure PARENT is a group
parent.isGroup = true;
}
var par = parent;
while (par !== null) {
par.total += child.total; // sum up sizes of all children
if (par.parent !== undefined) {
par = nodeArray[par.parent];
} else {
break;
}
}
}
}
}
return nodeArray;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div style="margin-bottom: 5px; padding: 5px; background-color: aliceblue">
<span style="display: inline-block; vertical-align: top; padding: 5px">
<b>New Tree</b><br />
MinNodes: <input type="number" width="2" id="minNodes" value="300" /><br />
MaxNodes: <input type="number" width="2" id="maxNodes" value="500" /><br />
MinChildren: <input type="number" width="2" id="minChil" value="2" /><br />
MaxChildren: <input type="number" width="2" id="maxChil" value="5" /><br />
<button type="button" onclick="rebuildGraph()">Generate Tree</button>
</span>
</div>
<div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 500px"></div>
<p>
This sample demonstrates a custom Layout, TreeMapLayout, which assumes that the diagram consists of nested Groups and simple Nodes.
Each node is positioned and sized to fill an area of the viewport proportionate to its "size", as determined by its Node.data.size property.
Each Group gets a size that is the sum of all of its member Nodes.
</p>
<p>
The layout is defined in its own file, as <a href="TreeMapLayout.js">TreeMapLayout.js</a>.
</p>
<p>
Clicking repeatedly at the same point will initially select the Node at that point, and then its containing Group, and so on up the chain of containers.
</p>
</div>
</body>
</html>
+146
View File
@@ -0,0 +1,146 @@
"use strict";
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/
// A custom Layout that lays out nested Groups according to how much area they should have
// within the viewport as a proportion of the total area.
// A simple layout for positioning and sizing all nodes in a diagram to make a tree map.
// Assume that all Group.layout == null and that it's OK to set the Node.desiredSize for all nodes, including groups.
// Also assume that there is a number property named "size" on the node data;
// this computes the "total" property for each node as the sum of the group's member nodes.
// This layout ignores all Links.
function TreeMapLayout() {
go.Layout.call(this);
this._isTopLevelHorizontal = false;
}
go.Diagram.inherit(TreeMapLayout, go.Layout);
/**
* @ignore
* Copies properties to a cloned Layout.
* @this {TreeMapLayout}
* @param {Layout} copy
*/
TreeMapLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
copy._isTopLevelHorizontal = this._isTopLevelHorizontal;
};
// First call computeTotals to make sure all of the node data have values for data.total.
// Then do a top-down walk through the diagram's structure of group relationships,
// positioning everything to fit in the viewport.
TreeMapLayout.prototype.doLayout = function(coll) {
if (!(coll instanceof go.Diagram)) throw new Error("TreeMapLayout only works as the Diagram.layout");
var diagram = coll;
this.computeTotals(diagram); // make sure data.total has been computed for every node
// figure out how large an area to cover;
// perhaps this should be a property that could be set, rather than depending on the current viewport
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
var x = this.arrangementOrigin.x;
var y = this.arrangementOrigin.y;
var w = diagram.viewportBounds.width;
var h = diagram.viewportBounds.height;
if (isNaN(w)) w = 1000;
if (isNaN(h)) h = 1000;
// collect all top-level nodes, and sum their totals
var tops = new go.Set();
var total = 0;
diagram.nodes.each(function(n) {
if (n.isTopLevel) {
tops.add(n);
total += n.data.total;
}
});
var horiz = this.isTopLevelHorizontal; // initially horizontal layout?
// the following was copied from the layoutNode method
var gx = x;
var gy = y;
var lay = this;
tops.each(function(n) {
var tot = n.data.total;
if (horiz) {
var pw = w * tot / total;
lay.layoutNode(!horiz, n, gx, gy, pw, h);
gx += pw;
} else {
var ph = h * tot / total;
lay.layoutNode(!horiz, n, gx, gy, w, ph);
gy += ph;
}
})
};
// Position and size the given node, and recurse if the node is a group
TreeMapLayout.prototype.layoutNode = function(horiz, n, x, y, w, h) {
n.position = new go.Point(x, y);
n.desiredSize = new go.Size(w, h);
if (n instanceof go.Group) {
var g = n;
var total = g.data.total;
var gx = x;
var gy = y;
var lay = this;
g.memberParts.each(function(p) {
if (p instanceof go.Link) return;
var tot = p.data.total;
if (horiz) {
var pw = w * tot / total;
lay.layoutNode(!horiz, p, gx, gy, pw, h);
gx += pw;
} else {
var ph = h * tot / total;
lay.layoutNode(!horiz, p, gx, gy, w, ph);
gy += ph;
}
})
}
};
// Make sure all nodes have initialized data.total property
TreeMapLayout.prototype.computeTotals = function(diagram) {
if (!diagram.nodes.all(function(g) { return !(g instanceof go.Group) || g.data.total >= 0; })) {
var groups = new go.Set();
diagram.nodes.each(function(n) {
if (n instanceof go.Group) { // collect all groups
groups.add(n);
} else { // regular nodes just have their total == size
n.data.total = n.data.size;
}
});
// keep looking for groups whose total can be computed, until all groups have been processed
while (groups.count > 0) {
var grps = new go.Set();
groups.each(function(g) {
// for a group all of whose member nodes have an initialized data.total,
if (g.memberParts.all(function(m) { return !(m instanceof go.Group) || m.data.total >= 0; })) {
// compute the group's total as the sum of the sizes of all of the member nodes
g.data.total = 0;
g.memberParts.each(function(m) { if (m instanceof go.Node) g.data.total += m.data.total; });
} else { // remember for the next iteration
grps.add(g);
}
});
groups = grps;
}
}
};
/**
* Gets or sets whether the top-level organization is horizontal or vertical.
* The default value is false.
* @name TreeMapLayout#isTopLevelHorizontal
* @return {boolean}
*/
Object.defineProperty(TreeMapLayout.prototype, "isTopLevelHorizontal", {
get: function() { return this._isTopLevelHorizontal; },
set: function(val) {
if (this._isTopLevelHorizontal !== val) {
this._isTopLevelHorizontal = val;
this.invalidateLayout();
}
}
});
// end TreeMapLayout
+89
View File
@@ -0,0 +1,89 @@
.zoomSlider {
position: absolute;
padding: 0;
opacity: .75;
z-index: 99;
width: 125px;
height: 25px;
top: 0px;
left: 0px;
}
.zoomButton {
display: inline-block;
vertical-align: top;
text-align: center;
padding: 0;
transition: opacity .2s;
}
.zoomRangeContainer {
display: inline-block;
vertical-align: top;
padding: 0;
}
.zoomRangeInput {
margin: 0;
padding: 0;
outline: none;
transition: opacity .2s;
background: transparent;
-webkit-appearance: none;
}
/* Set up additional styling to ensure consistenty across browsers */
.zoomRangeInput::-webkit-slider-runnable-track {
box-sizing: border-box;
border: none;
width: 100%;
height: 3px;
background: #ccc;
}
.zoomRangeInput::-moz-range-track {
box-sizing: border-box;
border: none;
width: 100%;
height: 3px;
background: #ccc;
}
.zoomRangeInput::-ms-track {
box-sizing: border-box;
border: none;
width: 100%;
height: 3px;
background: #ccc;
color: transparent;
}
.zoomRangeInput::-webkit-slider-thumb {
-webkit-appearance: none;
margin-top: -3.33px;
box-sizing: border-box;
border: none;
width: 10px;
height: 10px;
border-radius: 50%;
background: #444;
}
.zoomRangeInput::-moz-range-thumb {
box-sizing: border-box;
border: none;
width: 10px;
height: 10px;
border-radius: 50%;
background: #444;
}
.zoomRangeInput::-ms-thumb {
margin-top: 0;
box-sizing: border-box;
border: none;
width: 10px;
height: 10px;
border-radius: 50%;
background: #444;
}
.zoomRangeInput::-ms-tooltip,
.zoomRangeInput::-ms-fill-lower,
.zoomRangeInput::-ms-fill-upper {
display: none;
}

Some files were not shown because too many files have changed in this diff Show More