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:
+144
@@ -0,0 +1,144 @@
|
||||
<!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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { ArrangingLayout } from "./ArrangingLayout.js";
|
||||
import { PackedLayout } from "./PackedLayout.js";
|
||||
|
||||
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
|
||||
var $ = go.GraphObject.make;
|
||||
|
||||
const 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 (let 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);
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Arranging.html">Arranging Layout</a> sample.
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class ArrangingLayout extends go.Layout {
|
||||
constructor() {
|
||||
super();
|
||||
this._filter = null;
|
||||
this._side = go.Spot.BottomSide;
|
||||
this._spacing = new go.Size(20, 20);
|
||||
const play = new go.GridLayout();
|
||||
play.cellSize = new go.Size(1, 1);
|
||||
this._primaryLayout = play;
|
||||
const alay = new go.GridLayout();
|
||||
alay.cellSize = new go.Size(1, 1);
|
||||
this._arrangingLayout = alay;
|
||||
const slay = new go.GridLayout();
|
||||
slay.cellSize = new go.Size(1, 1);
|
||||
this._sideLayout = slay;
|
||||
}
|
||||
/**
|
||||
* @ignore @hidden @internal
|
||||
* Copies properties to a cloned Layout.
|
||||
*/
|
||||
cloneProtected(copy) {
|
||||
super.cloneProtected(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
|
||||
* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
|
||||
*/
|
||||
doLayout(coll) {
|
||||
const coll2 = this.collectParts(coll);
|
||||
const 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");
|
||||
const maincoll = new go.Set();
|
||||
const sidecoll = new go.Set();
|
||||
this.splitParts(coll2, maincoll, sidecoll);
|
||||
let mainnet = null;
|
||||
let subnets = null;
|
||||
if (this.arrangingLayout !== null) {
|
||||
mainnet = this.makeNetwork(maincoll);
|
||||
subnets = mainnet.splitIntoSubNetworks();
|
||||
}
|
||||
let bounds = null;
|
||||
if (this.arrangingLayout !== null && mainnet !== null && subnets !== null && subnets.count > 1) {
|
||||
const groups = new go.Map();
|
||||
const it = subnets.iterator;
|
||||
while (it.next()) {
|
||||
const net = it.value;
|
||||
const subcoll = net.findAllParts();
|
||||
this.preparePrimaryLayout(this.primaryLayout, subcoll);
|
||||
this.primaryLayout.doLayout(subcoll);
|
||||
this._addMainNode(groups, subcoll, diagram);
|
||||
}
|
||||
const mit = mainnet.vertexes.iterator;
|
||||
while (mit.next()) {
|
||||
const v = mit.value;
|
||||
if (v.node) {
|
||||
const subcoll = new go.Set();
|
||||
subcoll.add(v.node);
|
||||
this.preparePrimaryLayout(this.primaryLayout, subcoll);
|
||||
this.primaryLayout.doLayout(subcoll);
|
||||
this._addMainNode(groups, subcoll, diagram);
|
||||
}
|
||||
}
|
||||
this.arrangingLayout.doLayout(groups.toKeySet());
|
||||
const git = groups.iterator;
|
||||
while (git.next()) {
|
||||
const grp = git.key;
|
||||
const ginfo = git.value;
|
||||
this.moveSubgraph(ginfo.parts, ginfo.bounds, new go.Rect(grp.position, grp.desiredSize));
|
||||
}
|
||||
bounds = diagram.computePartsBounds(groups.toKeySet()); // 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);
|
||||
let 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
|
||||
*/
|
||||
_addMainNode(groups, subcoll, diagram) {
|
||||
const grp = new go.Node();
|
||||
grp.locationSpot = go.Spot.Center;
|
||||
const grpb = diagram.computePartsBounds(subcoll);
|
||||
grp.desiredSize = grpb.size;
|
||||
grp.position = grpb.position;
|
||||
groups.add(grp, { parts: subcoll, bounds: grpb });
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
splitParts(coll, maincoll, sidecoll) {
|
||||
// first consider all Nodes
|
||||
const pred = this.filter;
|
||||
coll.each(function (p) {
|
||||
if (p instanceof go.Link)
|
||||
return;
|
||||
let 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 (!p.fromNode || !p.toNode)
|
||||
return;
|
||||
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
|
||||
*/
|
||||
preparePrimaryLayout(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
|
||||
*/
|
||||
moveSubgraph(subColl, subbounds, bounds) {
|
||||
const diagram = this.diagram;
|
||||
if (!diagram)
|
||||
return;
|
||||
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
|
||||
*/
|
||||
prepareSideLayout(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
|
||||
*/
|
||||
moveSideCollection(sidecoll, mainbounds, sidebounds) {
|
||||
const diagram = this.diagram;
|
||||
if (!diagram)
|
||||
return;
|
||||
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.
|
||||
*/
|
||||
get filter() { return this._filter; }
|
||||
set filter(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}
|
||||
*/
|
||||
get side() { return this._side; }
|
||||
set side(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}
|
||||
*/
|
||||
get spacing() { return this._spacing; }
|
||||
set spacing(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.
|
||||
*/
|
||||
get primaryLayout() { return this._primaryLayout; }
|
||||
set primaryLayout(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.
|
||||
*/
|
||||
get arrangingLayout() { return this._arrangingLayout; }
|
||||
set arrangingLayout(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.
|
||||
*/
|
||||
get sideLayout() { return this._sideLayout; }
|
||||
set sideLayout(val) {
|
||||
if (!(val instanceof go.Layout))
|
||||
throw new Error("layout does not inherit from go.Layout: " + val);
|
||||
this._sideLayout = val;
|
||||
this.invalidateLayout();
|
||||
}
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Arranging.html">Arranging Layout</a> sample.
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class ArrangingLayout extends go.Layout {
|
||||
private _filter: ((part: go.Part) => boolean) | null = null;
|
||||
private _primaryLayout: go.Layout;
|
||||
private _arrangingLayout: go.Layout;
|
||||
private _sideLayout: go.Layout;
|
||||
private _side: go.Spot = go.Spot.BottomSide;
|
||||
private _spacing: go.Size = new go.Size(20, 20);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const play = new go.GridLayout();
|
||||
play.cellSize = new go.Size(1, 1);
|
||||
this._primaryLayout = play;
|
||||
const alay = new go.GridLayout();
|
||||
alay.cellSize = new go.Size(1, 1);
|
||||
this._arrangingLayout = alay;
|
||||
const slay = new go.GridLayout();
|
||||
slay.cellSize = new go.Size(1, 1);
|
||||
this._sideLayout = slay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore @hidden @internal
|
||||
* Copies properties to a cloned Layout.
|
||||
*/
|
||||
cloneProtected(copy: this): void {
|
||||
super.cloneProtected(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
|
||||
* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
|
||||
*/
|
||||
doLayout(coll: go.Diagram | go.Group | go.Iterable<go.Part>) {
|
||||
const coll2 = this.collectParts(coll);
|
||||
|
||||
const 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");
|
||||
|
||||
const maincoll = new go.Set<go.Part>();
|
||||
const sidecoll = new go.Set<go.Part>();
|
||||
this.splitParts(coll2, maincoll, sidecoll);
|
||||
|
||||
let mainnet = null;
|
||||
let subnets = null;
|
||||
if (this.arrangingLayout !== null) {
|
||||
mainnet = this.makeNetwork(maincoll);
|
||||
subnets = mainnet.splitIntoSubNetworks();
|
||||
}
|
||||
let bounds = null;
|
||||
if (this.arrangingLayout !== null && mainnet !== null && subnets !== null && subnets.count > 1) {
|
||||
const groups = new go.Map<go.Part, { parts: go.Set<go.Part>, bounds: go.Rect }>();
|
||||
const it = subnets.iterator;
|
||||
while (it.next()) {
|
||||
const net = it.value;
|
||||
const subcoll = net.findAllParts();
|
||||
this.preparePrimaryLayout(this.primaryLayout, subcoll);
|
||||
this.primaryLayout.doLayout(subcoll);
|
||||
this._addMainNode(groups, subcoll, diagram);
|
||||
}
|
||||
const mit = mainnet.vertexes.iterator;
|
||||
while (mit.next()) {
|
||||
const v = mit.value;
|
||||
if (v.node) {
|
||||
const subcoll = new go.Set<go.Part>();
|
||||
subcoll.add(v.node);
|
||||
this.preparePrimaryLayout(this.primaryLayout, subcoll);
|
||||
this.primaryLayout.doLayout(subcoll);
|
||||
this._addMainNode(groups, subcoll, diagram);
|
||||
}
|
||||
}
|
||||
|
||||
this.arrangingLayout.doLayout(groups.toKeySet());
|
||||
const git = groups.iterator;
|
||||
while (git.next()) {
|
||||
const grp = git.key;
|
||||
const ginfo = git.value;
|
||||
this.moveSubgraph(ginfo.parts, ginfo.bounds, new go.Rect(grp.position, grp.desiredSize));
|
||||
}
|
||||
bounds = diagram.computePartsBounds(groups.toKeySet()); // 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);
|
||||
let 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
|
||||
*/
|
||||
_addMainNode(groups: go.Map<go.Part, { parts: go.Set<go.Part>, bounds: go.Rect }>, subcoll: go.Set<go.Part>, diagram: go.Diagram) {
|
||||
const grp = new go.Node();
|
||||
grp.locationSpot = go.Spot.Center;
|
||||
const grpb = diagram.computePartsBounds(subcoll);
|
||||
grp.desiredSize = grpb.size;
|
||||
grp.position = grpb.position;
|
||||
groups.add(grp, { parts: subcoll, bounds: grpb });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
splitParts(coll: go.Set<go.Part>, maincoll: go.Set<go.Part>, sidecoll: go.Set<go.Part>) {
|
||||
// first consider all Nodes
|
||||
const pred = this.filter;
|
||||
coll.each(function(p) {
|
||||
if (p instanceof go.Link) return;
|
||||
let 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 (!p.fromNode || !p.toNode) return;
|
||||
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
|
||||
*/
|
||||
preparePrimaryLayout(primaryLayout: go.Layout, mainColl: go.Set<go.Part>) {
|
||||
// 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
|
||||
*/
|
||||
moveSubgraph(subColl: go.Set<go.Part>, subbounds: go.Rect, bounds: go.Rect) {
|
||||
const diagram = this.diagram;
|
||||
if (!diagram) return;
|
||||
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
|
||||
*/
|
||||
prepareSideLayout(sideLayout: go.Layout, sideColl: go.Set<go.Part>, mainBounds: go.Rect) {
|
||||
// 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
|
||||
*/
|
||||
moveSideCollection(sidecoll: go.Set<go.Part>, mainbounds: go.Rect, sidebounds: go.Rect) {
|
||||
const diagram = this.diagram;
|
||||
if (!diagram) return;
|
||||
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.
|
||||
*/
|
||||
get filter(): ((part: go.Part) => boolean) | null { return this._filter; }
|
||||
set filter(val: ((part: go.Part) => boolean) | null) {
|
||||
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}
|
||||
*/
|
||||
get side(): go.Spot { return this._side; }
|
||||
set side(val: go.Spot) {
|
||||
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}
|
||||
*/
|
||||
get spacing(): go.Size { return this._spacing; }
|
||||
set spacing(val: go.Size) {
|
||||
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.
|
||||
*/
|
||||
get primaryLayout(): go.Layout { return this._primaryLayout; }
|
||||
set primaryLayout(val: go.Layout) {
|
||||
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.
|
||||
*/
|
||||
get arrangingLayout(): go.Layout { return this._arrangingLayout; }
|
||||
set arrangingLayout(val: go.Layout) {
|
||||
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.
|
||||
*/
|
||||
get sideLayout(): go.Layout { return this._sideLayout; }
|
||||
set sideLayout(val: go.Layout) {
|
||||
if (!(val instanceof go.Layout)) throw new Error("layout does not inherit from go.Layout: " + val);
|
||||
this._sideLayout = val;
|
||||
this.invalidateLayout();
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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" })
|
||||
// );
|
||||
import * as go from '../release/go-module.js';
|
||||
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');
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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" })
|
||||
// );
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
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
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Balloon Link</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="TypeScript: A demonstration of the BalloonLink extension for implementing word balloons or speech bubbles as comments in diagrams about particular objects." />
|
||||
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">BalloonLink.ts</a>.
|
||||
</p>
|
||||
<p>
|
||||
Usage can also be seen in the <a href="../samples/comments.html">Comments</a> sample.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { BalloonLink } from "./BalloonLink.js";
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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' }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* This custom {@link Link} class customizes its {@link Shape} to surround the comment node (the from 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".
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/BalloonLink.html">Balloon Links</a> sample.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class BalloonLink extends go.Link {
|
||||
/**
|
||||
* Constructs a BalloonLink and sets the {@link Part#layerName} property to "Background".
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._base = 10;
|
||||
this.layerName = 'Background';
|
||||
}
|
||||
/**
|
||||
* Copies properties to a cloned BalloonLink.
|
||||
*/
|
||||
cloneProtected(copy) {
|
||||
super.cloneProtected(copy);
|
||||
copy._base = this._base;
|
||||
}
|
||||
/**
|
||||
* Gets or sets width of the base of the triangle at the center point of the {@link Link#fromNode}.
|
||||
*
|
||||
* The default value is 10.
|
||||
*/
|
||||
get base() { return this._base; }
|
||||
set base(value) { this._base = value; }
|
||||
/**
|
||||
* Produce a Geometry from the Link's route that draws a "balloon" shape around the {@link Link#fromNode}
|
||||
* and has a triangular shape with the base at the fromNode and the top at the toNode.
|
||||
*/
|
||||
makeGeometry() {
|
||||
const fromnode = this.fromNode;
|
||||
const tonode = this.toNode;
|
||||
if (fromnode === null || tonode === null)
|
||||
return super.makeGeometry();
|
||||
// assume the fromNode is the comment and the toNode is the commented-upon node
|
||||
const bb = fromnode.actualBounds;
|
||||
const nb = tonode.actualBounds;
|
||||
const p0 = bb.center;
|
||||
let pn = this.getPoint(this.pointsCount - 1);
|
||||
if (bb.intersectsRect(nb)) {
|
||||
pn = nb.center;
|
||||
}
|
||||
const pos = this.routeBounds;
|
||||
// compute the intersection points for the triangular arrow
|
||||
const ang = pn.directionPoint(p0);
|
||||
const L = new go.Point(this.base, 0).rotate(ang - 90).add(p0);
|
||||
const R = new go.Point(this.base, 0).rotate(ang + 90).add(p0);
|
||||
this.getLinkPointFromPoint(fromnode, fromnode, L, pn, true, L);
|
||||
this.getLinkPointFromPoint(fromnode, fromnode, R, pn, true, R);
|
||||
// form a triangular arrow from the comment to the commented node
|
||||
const 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
|
||||
let 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);
|
||||
}
|
||||
/**
|
||||
* Draw a line to a corner, but not if the comment arrow encompasses that corner.
|
||||
*/
|
||||
pathToCorner(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* This custom {@link Link} class customizes its {@link Shape} to surround the comment node (the from 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".
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/BalloonLink.html">Balloon Links</a> sample.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class BalloonLink extends go.Link {
|
||||
private _base: number = 10;
|
||||
|
||||
/**
|
||||
* Constructs a BalloonLink and sets the {@link Part#layerName} property to "Background".
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.layerName = 'Background';
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies properties to a cloned BalloonLink.
|
||||
*/
|
||||
protected cloneProtected(copy: this): void {
|
||||
super.cloneProtected(copy);
|
||||
copy._base = this._base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets width of the base of the triangle at the center point of the {@link Link#fromNode}.
|
||||
*
|
||||
* The default value is 10.
|
||||
*/
|
||||
get base(): number { return this._base; }
|
||||
set base(value: number) { this._base = value; }
|
||||
|
||||
/**
|
||||
* Produce a Geometry from the Link's route that draws a "balloon" shape around the {@link Link#fromNode}
|
||||
* and has a triangular shape with the base at the fromNode and the top at the toNode.
|
||||
*/
|
||||
public makeGeometry(): go.Geometry {
|
||||
const fromnode = this.fromNode;
|
||||
const tonode = this.toNode;
|
||||
if (fromnode === null || tonode === null) return super.makeGeometry();
|
||||
// assume the fromNode is the comment and the toNode is the commented-upon node
|
||||
const bb = fromnode.actualBounds;
|
||||
const nb = tonode.actualBounds;
|
||||
|
||||
const p0 = bb.center;
|
||||
let pn = this.getPoint(this.pointsCount - 1);
|
||||
if (bb.intersectsRect(nb)) {
|
||||
pn = nb.center;
|
||||
}
|
||||
const pos = this.routeBounds;
|
||||
|
||||
// compute the intersection points for the triangular arrow
|
||||
const ang = pn.directionPoint(p0);
|
||||
const L = new go.Point(this.base, 0).rotate(ang - 90).add(p0);
|
||||
const R = new go.Point(this.base, 0).rotate(ang + 90).add(p0);
|
||||
this.getLinkPointFromPoint(fromnode, fromnode, L, pn, true, L);
|
||||
this.getLinkPointFromPoint(fromnode, fromnode, R, pn, true, R);
|
||||
|
||||
// form a triangular arrow from the comment to the commented node
|
||||
const 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
|
||||
let 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a line to a corner, but not if the comment arrow encompasses that corner.
|
||||
*/
|
||||
public pathToCorner(side: number, bb: go.Rect, fig: go.PathFigure, pos: go.Rect, L: go.Point, R: go.Point): void {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+522
@@ -0,0 +1,522 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
// 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', (args) => {
|
||||
// default colors for 'Button' shape
|
||||
const buttonFillNormal = '#F5F5F5';
|
||||
const buttonStrokeNormal = '#BDBDBD';
|
||||
const buttonFillOver = '#E0E0E0';
|
||||
const buttonStrokeOver = '#9E9E9E';
|
||||
const buttonFillPressed = '#BDBDBD'; // set to null for no button pressed effects
|
||||
const buttonStrokePressed = '#9E9E9E';
|
||||
const buttonFillDisabled = '#E5E5E5';
|
||||
// padding inside the ButtonBorder to match sizing from previous versions
|
||||
const paddingHorizontal = 2.76142374915397;
|
||||
const paddingVertical = 2.761423749153969;
|
||||
const button = /** @type {Panel} */ (go.GraphObject.make(go.Panel, 'Auto', {
|
||||
isActionable: true,
|
||||
enabledChanged: (btn, enabled) => {
|
||||
if (btn instanceof go.Panel) {
|
||||
const 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 = (e, btn, prev) => {
|
||||
if (!btn.isEnabledObject())
|
||||
return;
|
||||
if (!(btn instanceof go.Panel))
|
||||
return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
let brush = btn['_buttonFillOver'];
|
||||
btn['_buttonFillNormal'] = shape.fill;
|
||||
shape.fill = brush;
|
||||
brush = btn['_buttonStrokeOver'];
|
||||
btn['_buttonStrokeNormal'] = shape.stroke;
|
||||
shape.stroke = brush;
|
||||
}
|
||||
};
|
||||
button.mouseLeave = (e, btn, prev) => {
|
||||
if (!btn.isEnabledObject())
|
||||
return;
|
||||
if (!(btn instanceof go.Panel))
|
||||
return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
shape.fill = btn['_buttonFillNormal'];
|
||||
shape.stroke = btn['_buttonStrokeNormal'];
|
||||
}
|
||||
};
|
||||
button.actionDown = (e, btn) => {
|
||||
if (!btn.isEnabledObject())
|
||||
return;
|
||||
if (!(btn instanceof go.Panel))
|
||||
return;
|
||||
if (btn['_buttonFillPressed'] === null)
|
||||
return;
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
const diagram = e.diagram;
|
||||
const oldskip = diagram.skipsUndoManager;
|
||||
diagram.skipsUndoManager = true;
|
||||
let 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 = (e, btn) => {
|
||||
if (!btn.isEnabledObject())
|
||||
return;
|
||||
if (!(btn instanceof go.Panel))
|
||||
return;
|
||||
if (btn['_buttonFillPressed'] === null)
|
||||
return;
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
const diagram = e.diagram;
|
||||
const 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 = (e, btn) => {
|
||||
if (!btn.isEnabledObject())
|
||||
return;
|
||||
if (!(btn instanceof go.Panel))
|
||||
return;
|
||||
if (btn['_buttonFillPressed'] === null)
|
||||
return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
const diagram = e.diagram;
|
||||
const 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 = (e, btn) => {
|
||||
if (!btn.isEnabledObject())
|
||||
return;
|
||||
if (!(btn instanceof go.Panel))
|
||||
return;
|
||||
if (btn['_buttonFillPressed'] === null)
|
||||
return;
|
||||
const diagram = e.diagram;
|
||||
if (diagram.firstInput.button !== 0)
|
||||
return;
|
||||
diagram.currentTool.standardMouseOver();
|
||||
if (overButton(e, btn)) {
|
||||
const shape = btn.findObject('ButtonBorder');
|
||||
if (shape instanceof go.Shape) {
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
const overButton = (e, btn) => {
|
||||
const over = e.diagram.findObjectAt(e.documentPoint, (x) => {
|
||||
while (x.panel !== null) {
|
||||
if (x.isActionable)
|
||||
return x;
|
||||
x = x.panel;
|
||||
}
|
||||
return x;
|
||||
}, (x) => 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', (args) => {
|
||||
const button = /** @type {Panel} */ (go.GraphObject.make('Button', {
|
||||
'_treeExpandedFigure': 'MinusLine',
|
||||
'_treeCollapsedFigure': 'PlusLine'
|
||||
}, go.GraphObject.make(go.Shape, // the icon
|
||||
{
|
||||
name: 'ButtonIcon',
|
||||
figure: 'MinusLine',
|
||||
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', (exp, shape) => {
|
||||
const 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', (leaf) => !leaf).ofObject()));
|
||||
// tree expand/collapse behavior
|
||||
button.click = (e, btn) => {
|
||||
let node = btn.part;
|
||||
if (node instanceof go.Adornment)
|
||||
node = node.adornedPart;
|
||||
if (!(node instanceof go.Node))
|
||||
return;
|
||||
const diagram = node.diagram;
|
||||
if (diagram === null)
|
||||
return;
|
||||
const 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', (args) => {
|
||||
const button = /** @type {Panel} */ (go.GraphObject.make('Button', {
|
||||
'_subGraphExpandedFigure': 'MinusLine',
|
||||
'_subGraphCollapsedFigure': 'PlusLine'
|
||||
}, go.GraphObject.make(go.Shape, // the icon
|
||||
{
|
||||
name: 'ButtonIcon',
|
||||
figure: 'MinusLine',
|
||||
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', (exp, shape) => {
|
||||
const but = shape.panel;
|
||||
return exp ? but['_subGraphExpandedFigure'] : but['_subGraphCollapsedFigure'];
|
||||
}).ofObject())));
|
||||
// subgraph expand/collapse behavior
|
||||
button.click = (e, btn) => {
|
||||
let group = btn.part;
|
||||
if (group instanceof go.Adornment)
|
||||
group = group.adornedPart;
|
||||
if (!(group instanceof go.Group))
|
||||
return;
|
||||
const diagram = group.diagram;
|
||||
if (diagram === null)
|
||||
return;
|
||||
const 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', (args) => {
|
||||
const 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', (args) => {
|
||||
const 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', '', (obj) => {
|
||||
const 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', (args) => {
|
||||
const button = /** @type {Panel} */ (go.GraphObject.make('Button'));
|
||||
button.stretch = go.GraphObject.Horizontal;
|
||||
const 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', (args) => {
|
||||
const eltname = (go.GraphObject.takeBuilderArgument(args, 'COLLAPSIBLE'));
|
||||
const button = (go.GraphObject.make('Button', {
|
||||
'_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', (vis) => vis ? button['_buttonExpandedFigure'] : button['_buttonCollapsedFigure']).ofObject(eltname))));
|
||||
const border = button.findObject('ButtonBorder');
|
||||
if (border instanceof go.Shape) {
|
||||
border.stroke = null;
|
||||
border.fill = 'rgba(0, 0, 0, 0)';
|
||||
}
|
||||
button.click = (e, btn) => {
|
||||
if (!(btn instanceof go.Panel))
|
||||
return;
|
||||
const diagram = btn.diagram;
|
||||
if (diagram === null)
|
||||
return;
|
||||
if (diagram.isReadOnly)
|
||||
return;
|
||||
let elt = btn.findTemplateBinder();
|
||||
if (elt === null)
|
||||
elt = btn.part;
|
||||
if (elt !== null) {
|
||||
const 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', (args) => {
|
||||
// process the one required string argument for this kind of button
|
||||
const propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
|
||||
const 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',
|
||||
strokeWidth: 2,
|
||||
stretch: go.GraphObject.Fill,
|
||||
geometryStretch: go.GraphObject.Uniform,
|
||||
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 = (e, btn) => {
|
||||
const diagram = e.diagram;
|
||||
if (diagram === null || diagram.isReadOnly)
|
||||
return;
|
||||
if (propname !== '' && diagram.model.isReadOnly)
|
||||
return;
|
||||
e.handled = true;
|
||||
const shape = btn.findObject('ButtonIcon');
|
||||
diagram.startTransaction('checkbox');
|
||||
if (shape !== null)
|
||||
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', (args) => {
|
||||
// process the one required string argument for this kind of button
|
||||
const propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
|
||||
const button = /** @type {Panel} */ (go.GraphObject.make('CheckBoxButton', propname, // bound to this data property
|
||||
{
|
||||
name: 'Button',
|
||||
isActionable: false,
|
||||
margin: new go.Margin(0, 1, 0, 0)
|
||||
}));
|
||||
const 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;
|
||||
});
|
||||
Executable
+596
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
// 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', (args: any): go.Panel => {
|
||||
// default colors for 'Button' shape
|
||||
const buttonFillNormal = '#F5F5F5';
|
||||
const buttonStrokeNormal = '#BDBDBD';
|
||||
const buttonFillOver = '#E0E0E0';
|
||||
const buttonStrokeOver = '#9E9E9E';
|
||||
const buttonFillPressed = '#BDBDBD'; // set to null for no button pressed effects
|
||||
const buttonStrokePressed = '#9E9E9E';
|
||||
const buttonFillDisabled = '#E5E5E5';
|
||||
|
||||
// padding inside the ButtonBorder to match sizing from previous versions
|
||||
const paddingHorizontal = 2.76142374915397;
|
||||
const paddingVertical = 2.761423749153969;
|
||||
|
||||
const button = /** @type {Panel} */ (
|
||||
go.GraphObject.make(go.Panel, 'Auto',
|
||||
{
|
||||
isActionable: true, // needed so that the ActionTool intercepts mouse events
|
||||
enabledChanged: (btn: go.GraphObject, enabled: boolean): void => {
|
||||
if (btn instanceof go.Panel) {
|
||||
const shape = btn.findObject('ButtonBorder') as go.Shape;
|
||||
if (shape !== null) {
|
||||
shape.fill = enabled ? (btn as any)['_buttonFillNormal'] : (btn as any)['_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
|
||||
}
|
||||
)
|
||||
)
|
||||
) as go.Panel;
|
||||
|
||||
// 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 = (e: go.InputEvent, btn: go.GraphObject, prev: go.GraphObject): void => {
|
||||
if (!btn.isEnabledObject()) return;
|
||||
if (!(btn instanceof go.Panel)) return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
let brush = (btn as any)['_buttonFillOver'];
|
||||
(btn as any)['_buttonFillNormal'] = shape.fill;
|
||||
shape.fill = brush;
|
||||
brush = (btn as any)['_buttonStrokeOver'];
|
||||
(btn as any)['_buttonStrokeNormal'] = shape.stroke;
|
||||
shape.stroke = brush;
|
||||
}
|
||||
};
|
||||
|
||||
button.mouseLeave = (e: go.InputEvent, btn: go.GraphObject, prev: go.GraphObject): void => {
|
||||
if (!btn.isEnabledObject()) return;
|
||||
if (!(btn instanceof go.Panel)) return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
shape.fill = (btn as any)['_buttonFillNormal'];
|
||||
shape.stroke = (btn as any)['_buttonStrokeNormal'];
|
||||
}
|
||||
};
|
||||
|
||||
button.actionDown = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
if (!btn.isEnabledObject()) return;
|
||||
if (!(btn instanceof go.Panel)) return;
|
||||
if ((btn as any)['_buttonFillPressed'] === null) return;
|
||||
if (e.button !== 0) return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
const diagram = e.diagram;
|
||||
const oldskip = diagram.skipsUndoManager;
|
||||
diagram.skipsUndoManager = true;
|
||||
let brush = (btn as any)['_buttonFillPressed'];
|
||||
(btn as any)['_buttonFillOver'] = shape.fill;
|
||||
shape.fill = brush;
|
||||
brush = (btn as any)['_buttonStrokePressed'];
|
||||
(btn as any)['_buttonStrokeOver'] = shape.stroke;
|
||||
shape.stroke = brush;
|
||||
diagram.skipsUndoManager = oldskip;
|
||||
}
|
||||
};
|
||||
|
||||
button.actionUp = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
if (!btn.isEnabledObject()) return;
|
||||
if (!(btn instanceof go.Panel)) return;
|
||||
if ((btn as any)['_buttonFillPressed'] === null) return;
|
||||
if (e.button !== 0) return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
const diagram = e.diagram;
|
||||
const oldskip = diagram.skipsUndoManager;
|
||||
diagram.skipsUndoManager = true;
|
||||
if (overButton(e, btn)) {
|
||||
shape.fill = (btn as any)['_buttonFillOver'];
|
||||
shape.stroke = (btn as any)['_buttonStrokeOver'];
|
||||
} else {
|
||||
shape.fill = (btn as any)['_buttonFillNormal'];
|
||||
shape.stroke = (btn as any)['_buttonStrokeNormal'];
|
||||
}
|
||||
diagram.skipsUndoManager = oldskip;
|
||||
}
|
||||
};
|
||||
|
||||
button.actionCancel = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
if (!btn.isEnabledObject()) return;
|
||||
if (!(btn instanceof go.Panel)) return;
|
||||
if ((btn as any)['_buttonFillPressed'] === null) return;
|
||||
const shape = btn.findObject('ButtonBorder'); // the border Shape
|
||||
if (shape instanceof go.Shape) {
|
||||
const diagram = e.diagram;
|
||||
const oldskip = diagram.skipsUndoManager;
|
||||
diagram.skipsUndoManager = true;
|
||||
if (overButton(e, btn)) {
|
||||
shape.fill = (btn as any)['_buttonFillOver'];
|
||||
shape.stroke = (btn as any)['_buttonStrokeOver'];
|
||||
} else {
|
||||
shape.fill = (btn as any)['_buttonFillNormal'];
|
||||
shape.stroke = (btn as any)['_buttonStrokeNormal'];
|
||||
}
|
||||
diagram.skipsUndoManager = oldskip;
|
||||
}
|
||||
};
|
||||
|
||||
button.actionMove = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
if (!btn.isEnabledObject()) return;
|
||||
if (!(btn instanceof go.Panel)) return;
|
||||
if ((btn as any)['_buttonFillPressed'] === null) return;
|
||||
const diagram = e.diagram;
|
||||
if (diagram.firstInput.button !== 0) return;
|
||||
diagram.currentTool.standardMouseOver();
|
||||
if (overButton(e, btn)) {
|
||||
const shape = btn.findObject('ButtonBorder');
|
||||
if (shape instanceof go.Shape) {
|
||||
const oldskip = diagram.skipsUndoManager;
|
||||
diagram.skipsUndoManager = true;
|
||||
let brush = (btn as any)['_buttonFillPressed'];
|
||||
if (shape.fill !== brush) shape.fill = brush;
|
||||
brush = (btn as any)['_buttonStrokePressed'];
|
||||
if (shape.stroke !== brush) shape.stroke = brush;
|
||||
diagram.skipsUndoManager = oldskip;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const overButton = (e: go.InputEvent, btn: go.Panel): boolean => {
|
||||
const over = e.diagram.findObjectAt(
|
||||
e.documentPoint,
|
||||
(x: go.GraphObject): go.GraphObject => {
|
||||
while (x.panel !== null) {
|
||||
if (x.isActionable) return x;
|
||||
x = x.panel;
|
||||
}
|
||||
return x;
|
||||
},
|
||||
(x: go.GraphObject): boolean => 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', (args: any): go.Panel => {
|
||||
const 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',
|
||||
(exp: boolean, shape: go.Shape): string => {
|
||||
const but = shape.panel;
|
||||
return exp ? (but as any)['_treeExpandedFigure'] : (but as any)['_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',
|
||||
(leaf: boolean): boolean => !leaf
|
||||
).ofObject()
|
||||
)
|
||||
) as go.Panel;
|
||||
|
||||
// tree expand/collapse behavior
|
||||
button.click = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
let node = btn.part;
|
||||
if (node instanceof go.Adornment) node = node.adornedPart;
|
||||
if (!(node instanceof go.Node)) return;
|
||||
const diagram = node.diagram;
|
||||
if (diagram === null) return;
|
||||
const 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', (args: any): go.Panel => {
|
||||
const 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',
|
||||
(exp: boolean, shape: go.Shape): string => {
|
||||
const but = shape.panel;
|
||||
return exp ? (but as any)['_subGraphExpandedFigure'] : (but as any)['_subGraphCollapsedFigure'];
|
||||
}
|
||||
).ofObject()
|
||||
)
|
||||
)
|
||||
) as go.Panel;
|
||||
|
||||
// subgraph expand/collapse behavior
|
||||
button.click = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
let group = btn.part;
|
||||
if (group instanceof go.Adornment) group = group.adornedPart;
|
||||
if (!(group instanceof go.Group)) return;
|
||||
const diagram = group.diagram;
|
||||
if (diagram === null) return;
|
||||
const 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', (args: any): go.Adornment => {
|
||||
const 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', (args: any): go.Adornment => {
|
||||
const 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', '', (obj: go.Adornment) => {
|
||||
const 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', (args: any): go.Panel => {
|
||||
const button = /** @type {Panel} */ (go.GraphObject.make('Button')) as go.Panel;
|
||||
button.stretch = go.GraphObject.Horizontal;
|
||||
const 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', (args: any): go.Panel => {
|
||||
const eltname: string = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args, 'COLLAPSIBLE'));
|
||||
|
||||
const button: go.Panel = /** @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',
|
||||
(vis: boolean): string => vis ? (button as any)['_buttonExpandedFigure'] : (button as any)['_buttonCollapsedFigure']
|
||||
).ofObject(eltname)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const border = button.findObject('ButtonBorder');
|
||||
if (border instanceof go.Shape) {
|
||||
border.stroke = null;
|
||||
border.fill = 'rgba(0, 0, 0, 0)';
|
||||
}
|
||||
|
||||
button.click = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
if (!(btn instanceof go.Panel)) return;
|
||||
const diagram = btn.diagram;
|
||||
if (diagram === null) return;
|
||||
if (diagram.isReadOnly) return;
|
||||
let elt = btn.findTemplateBinder();
|
||||
if (elt === null) elt = btn.part;
|
||||
if (elt !== null) {
|
||||
const 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', (args: any): go.Panel => {
|
||||
// process the one required string argument for this kind of button
|
||||
const propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
|
||||
|
||||
const 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() : [])
|
||||
)
|
||||
)
|
||||
) as go.Panel;
|
||||
|
||||
button.click = (e: go.InputEvent, btn: go.GraphObject): void => {
|
||||
const diagram = e.diagram;
|
||||
if (diagram === null || diagram.isReadOnly) return;
|
||||
if (propname !== '' && diagram.model.isReadOnly) return;
|
||||
e.handled = true;
|
||||
const shape = (btn as go.Panel).findObject('ButtonIcon');
|
||||
diagram.startTransaction('checkbox');
|
||||
if (shape !== null) 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 as any)['_doClick'] === 'function') (btn as any)['_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', (args: any): go.Panel => {
|
||||
// process the one required string argument for this kind of button
|
||||
const propname = /** @type {string} */ (go.GraphObject.takeBuilderArgument(args));
|
||||
|
||||
const 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)
|
||||
}
|
||||
)
|
||||
) as go.Panel;
|
||||
|
||||
const 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 as any)['_buttonFillNormal'],
|
||||
'_buttonStrokeNormal': (button as any)['_buttonStrokeNormal'],
|
||||
'_buttonFillOver': (button as any)['_buttonFillOver'],
|
||||
'_buttonStrokeOver': (button as any)['_buttonStrokeOver'],
|
||||
'_buttonFillPressed': (button as any)['_buttonFillPressed'],
|
||||
'_buttonStrokePressed': (button as any)['_buttonStrokePressed'],
|
||||
'_buttonFillDisabled': (button as any)['_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
|
||||
}
|
||||
)
|
||||
) as go.Panel;
|
||||
// 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;
|
||||
});
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CheckBoxes</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">Buttons.ts</a>.
|
||||
</p>
|
||||
<textarea id="mySavedModel" style="width:100%;height:300px"></textarea>
|
||||
</div>
|
||||
<script type="module" id="code">
|
||||
import * as go from '../release/go-module.js';
|
||||
// don't need to import any extension -- CheckBox is already defined in GoJS
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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': function (e, obj) {
|
||||
if (obj.part !== null) 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) {
|
||||
const elt = document.getElementById('mySavedModel');
|
||||
if (elt !== null) elt.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' }
|
||||
]
|
||||
});
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
<!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="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">ColumnResizingTool.ts</a> and <a href="RowResizingTool.ts">RowResizingTool.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { ColumnResizingTool } from './ColumnResizingTool.js';
|
||||
import { RowResizingTool } from './RowResizingTool.js';
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const myDiagram = $(go.Diagram, 'myDiagramDiv', {
|
||||
validCycle: go.Diagram.CycleNotDirected,
|
||||
'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.
|
||||
const 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',
|
||||
fromSpot: go.Spot.Right,
|
||||
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) {
|
||||
let arr = data.widths;
|
||||
if (!arr)
|
||||
arr = [];
|
||||
if (idx >= arr.length) {
|
||||
for (let 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, {
|
||||
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() {
|
||||
const elt = document.getElementById('mySavedModel');
|
||||
if (elt !== null)
|
||||
elt.textContent = myDiagram.model.toJson();
|
||||
}
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The ColumnResizingTool class lets the user resize each column of a named Table Panel in a selected Part.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/ColumnResizing.html">Column Resizing</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class ColumnResizingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a ColumnResizingTool and sets the handle and name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._tableName = 'TABLE';
|
||||
// internal state
|
||||
this._handle = null;
|
||||
this._adornedTable = null;
|
||||
const 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)';
|
||||
this._handleArchetype = h;
|
||||
this.name = 'ColumnResizing';
|
||||
}
|
||||
/**
|
||||
* Gets or sets small GraphObject that is copied 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.
|
||||
*
|
||||
* The default value is a {@link Shape} that is a narrow rectangle.
|
||||
*/
|
||||
get handleArchetype() { return this._handleArchetype; }
|
||||
set handleArchetype(val) { this._handleArchetype = val; }
|
||||
/**
|
||||
* Gets or sets the name of the Table Panel to be resized.
|
||||
*
|
||||
* The default value is the name "TABLE".
|
||||
*/
|
||||
get tableName() { return this._tableName; }
|
||||
set tableName(val) { this._tableName = val; }
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
get handle() { return this._handle; }
|
||||
/**
|
||||
* This read-only property returns the {@link Panel} of type {@link Panel.Table} whose columns are being resized.
|
||||
* This must be contained within the selected {@link Part}.
|
||||
*/
|
||||
get adornedTable() { 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}.
|
||||
*/
|
||||
updateAdornments(part) {
|
||||
if (part === null || part instanceof go.Link)
|
||||
return; // this tool never applies to Links
|
||||
if (part.isSelected && !this.diagram.isReadOnly) {
|
||||
const 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) {
|
||||
const table = selelt;
|
||||
let adornment = part.findAdornment(this.name);
|
||||
if (adornment === null) {
|
||||
adornment = this.makeAdornment(table);
|
||||
part.addAdornment(this.name, adornment);
|
||||
}
|
||||
if (adornment !== null) {
|
||||
const pad = table.padding;
|
||||
const numcols = table.columnCount;
|
||||
// update the position/alignment of each handle
|
||||
adornment.elements.each((h) => {
|
||||
if (!h.pickable)
|
||||
return;
|
||||
const coldef = table.getColumnDefinition(h.column);
|
||||
let wid = coldef.actual;
|
||||
if (wid > 0)
|
||||
wid = coldef.total;
|
||||
let sep = 0;
|
||||
// find next non-zero-width column's separatorStrokeWidth
|
||||
let 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);
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @param {Panel} table the Table Panel whose columns may be resized
|
||||
* @return {Adornment}
|
||||
*/
|
||||
makeAdornment(table) {
|
||||
// the Adornment is a Spot Panel holding resize handles
|
||||
const 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
|
||||
const 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 (let i = 0; i < table.columnCount; i++) {
|
||||
const coldef = table.getColumnDefinition(i);
|
||||
const h = this.makeHandle(table, coldef);
|
||||
if (h !== null)
|
||||
adornment.add(h);
|
||||
}
|
||||
return adornment;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @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}
|
||||
*/
|
||||
makeHandle(table, coldef) {
|
||||
const h = this.handleArchetype;
|
||||
if (h === null)
|
||||
return null;
|
||||
const c = h.copy();
|
||||
c.column = coldef.index;
|
||||
return c;
|
||||
}
|
||||
/**
|
||||
* This tool may run when there is a mouse-down event on a "ColumnResizing" handle,
|
||||
* the diagram is not read-only, the left mouse button is being used,
|
||||
* and this tool's adornment's resize handle is at the current mouse point.
|
||||
*/
|
||||
canStart() {
|
||||
if (!this.isEnabled)
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly)
|
||||
return false;
|
||||
if (!diagram.lastInput.left)
|
||||
return false;
|
||||
const h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
return (h !== null);
|
||||
}
|
||||
/**
|
||||
* Find the {@link #handle}, ensure type {@link Panel.Table}, capture the mouse, and start a transaction.
|
||||
*
|
||||
* If the call to {@link Tool#findToolHandleAt} finds no "ColumnResizing" tool handle, this method returns without activating this tool.
|
||||
*/
|
||||
doActivate() {
|
||||
const diagram = this.diagram;
|
||||
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
if (this.handle === null)
|
||||
return;
|
||||
const 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;
|
||||
}
|
||||
/**
|
||||
* Stop the current transaction and release the mouse.
|
||||
*/
|
||||
doDeactivate() {
|
||||
this.stopTransaction();
|
||||
this._handle = null;
|
||||
this._adornedTable = null;
|
||||
const diagram = this.diagram;
|
||||
diagram.isMouseCaptured = false;
|
||||
this.isActive = false;
|
||||
}
|
||||
/**
|
||||
* Call {@link #resize} with a new size determined by the current mouse point.
|
||||
* This determines the new bounds by calling {@link #computeResize}.
|
||||
*/
|
||||
doMouseMove() {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeResize(diagram.lastInput.documentPoint);
|
||||
this.resize(newpt);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Call {@link #resize} with the final bounds based on the most recent mouse point, and commit the transaction.
|
||||
* This determines the new bounds by calling {@link #computeResize}.
|
||||
*/
|
||||
doMouseUp() {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeResize(diagram.lastInput.documentPoint);
|
||||
this.resize(newpt);
|
||||
this.transactionResult = this.name; // success
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Change the {@link RowColumnDefinition#width} of the column being resized
|
||||
* to a value corresponding to the given mouse point.
|
||||
* @param {Point} newPoint the value returned by the call to {@link #computeResize}
|
||||
*/
|
||||
resize(newPoint) {
|
||||
const table = this.adornedTable;
|
||||
if (table === null)
|
||||
return;
|
||||
const h = this.handle;
|
||||
if (h === null)
|
||||
return;
|
||||
const pad = table.padding;
|
||||
const numcols = table.columnCount;
|
||||
const locpt = table.getLocalPoint(newPoint);
|
||||
const coldef = table.getColumnDefinition(h.column);
|
||||
let sep = 0;
|
||||
let 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
|
||||
* @param {Point} p the point where the handle is being dragged
|
||||
* @return {Point}
|
||||
*/
|
||||
computeResize(p) {
|
||||
return p;
|
||||
}
|
||||
/**
|
||||
* Pressing the Delete key removes any column width setting and stops this tool.
|
||||
*/
|
||||
doKeyDown() {
|
||||
if (!this.isActive)
|
||||
return;
|
||||
const e = this.diagram.lastInput;
|
||||
if (e.key === 'Del' || e.key === '\t') { // remove width setting
|
||||
if (this.adornedTable !== null && this.handle !== null) {
|
||||
const coldef = this.adornedTable.getColumnDefinition(this.handle.column);
|
||||
coldef.width = NaN;
|
||||
this.transactionResult = this.name; // success
|
||||
this.stopTool();
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.doKeyDown();
|
||||
}
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The ColumnResizingTool class lets the user resize each column of a named Table Panel in a selected Part.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/ColumnResizing.html">Column Resizing</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class ColumnResizingTool extends go.Tool {
|
||||
private _handleArchetype: go.Shape;
|
||||
private _tableName: string = 'TABLE';
|
||||
|
||||
// internal state
|
||||
private _handle: go.GraphObject | null = null;
|
||||
private _adornedTable: go.Panel | null = null;
|
||||
|
||||
/**
|
||||
* Constructs a ColumnResizingTool and sets the handle and name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
const h: go.Shape = 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)';
|
||||
this._handleArchetype = h;
|
||||
this.name = 'ColumnResizing';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets small GraphObject that is copied 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.
|
||||
*
|
||||
* The default value is a {@link Shape} that is a narrow rectangle.
|
||||
*/
|
||||
get handleArchetype(): go.Shape { return this._handleArchetype; }
|
||||
set handleArchetype(val: go.Shape) { this._handleArchetype = val; }
|
||||
|
||||
/**
|
||||
* Gets or sets the name of the Table Panel to be resized.
|
||||
*
|
||||
* The default value is the name "TABLE".
|
||||
*/
|
||||
get tableName(): string { return this._tableName; }
|
||||
set tableName(val: string) { this._tableName = val; }
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
get handle(): go.GraphObject | null { return this._handle; }
|
||||
|
||||
/**
|
||||
* This read-only property returns the {@link Panel} of type {@link Panel.Table} whose columns are being resized.
|
||||
* This must be contained within the selected {@link Part}.
|
||||
*/
|
||||
get adornedTable(): go.Panel | null { 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}.
|
||||
*/
|
||||
public updateAdornments(part: go.Part): void {
|
||||
if (part === null || part instanceof go.Link) return; // this tool never applies to Links
|
||||
if (part.isSelected && !this.diagram.isReadOnly) {
|
||||
const 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) {
|
||||
const table = selelt;
|
||||
let adornment = part.findAdornment(this.name);
|
||||
if (adornment === null) {
|
||||
adornment = this.makeAdornment(table);
|
||||
part.addAdornment(this.name, adornment);
|
||||
}
|
||||
if (adornment !== null) {
|
||||
const pad = table.padding as go.Margin;
|
||||
const numcols = table.columnCount;
|
||||
// update the position/alignment of each handle
|
||||
adornment.elements.each((h: go.GraphObject) => {
|
||||
if (!h.pickable) return;
|
||||
const coldef = table.getColumnDefinition(h.column);
|
||||
let wid = coldef.actual;
|
||||
if (wid > 0) wid = coldef.total;
|
||||
let sep = 0;
|
||||
// find next non-zero-width column's separatorStrokeWidth
|
||||
let 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @param {Panel} table the Table Panel whose columns may be resized
|
||||
* @return {Adornment}
|
||||
*/
|
||||
public makeAdornment(table: go.Panel): go.Adornment {
|
||||
// the Adornment is a Spot Panel holding resize handles
|
||||
const 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
|
||||
const 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 (let i = 0; i < table.columnCount; i++) {
|
||||
const coldef = table.getColumnDefinition(i);
|
||||
const h = this.makeHandle(table, coldef);
|
||||
if (h !== null) adornment.add(h);
|
||||
}
|
||||
return adornment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @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}
|
||||
*/
|
||||
public makeHandle(table: go.Panel, coldef: go.RowColumnDefinition): go.GraphObject | null {
|
||||
const h = this.handleArchetype;
|
||||
if (h === null) return null;
|
||||
const c = h.copy();
|
||||
c.column = coldef.index;
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This tool may run when there is a mouse-down event on a "ColumnResizing" handle,
|
||||
* the diagram is not read-only, the left mouse button is being used,
|
||||
* and this tool's adornment's resize handle is at the current mouse point.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!this.isEnabled) return false;
|
||||
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly) return false;
|
||||
if (!diagram.lastInput.left) return false;
|
||||
const h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
return (h !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the {@link #handle}, ensure type {@link Panel.Table}, capture the mouse, and start a transaction.
|
||||
*
|
||||
* If the call to {@link Tool#findToolHandleAt} finds no "ColumnResizing" tool handle, this method returns without activating this tool.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
const diagram = this.diagram;
|
||||
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
if (this.handle === null) return;
|
||||
const panel = (this.handle.part as go.Adornment).adornedObject as go.Adornment;
|
||||
if (!panel || panel.type !== go.Panel.Table) return;
|
||||
this._adornedTable = panel;
|
||||
diagram.isMouseCaptured = true;
|
||||
this.startTransaction(this.name);
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current transaction and release the mouse.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
this.stopTransaction();
|
||||
this._handle = null;
|
||||
this._adornedTable = null;
|
||||
const diagram = this.diagram;
|
||||
diagram.isMouseCaptured = false;
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@link #resize} with a new size determined by the current mouse point.
|
||||
* This determines the new bounds by calling {@link #computeResize}.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeResize(diagram.lastInput.documentPoint);
|
||||
this.resize(newpt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@link #resize} with the final bounds based on the most recent mouse point, and commit the transaction.
|
||||
* This determines the new bounds by calling {@link #computeResize}.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeResize(diagram.lastInput.documentPoint);
|
||||
this.resize(newpt);
|
||||
this.transactionResult = this.name; // success
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the {@link RowColumnDefinition#width} of the column being resized
|
||||
* to a value corresponding to the given mouse point.
|
||||
* @param {Point} newPoint the value returned by the call to {@link #computeResize}
|
||||
*/
|
||||
public resize(newPoint: go.Point): void {
|
||||
const table = this.adornedTable;
|
||||
if (table === null) return;
|
||||
const h = this.handle;
|
||||
if (h === null) return;
|
||||
const pad = table.padding as go.Margin;
|
||||
const numcols = table.columnCount;
|
||||
const locpt = table.getLocalPoint(newPoint);
|
||||
const coldef = table.getColumnDefinition(h.column);
|
||||
let sep = 0;
|
||||
let 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
|
||||
* @param {Point} p the point where the handle is being dragged
|
||||
* @return {Point}
|
||||
*/
|
||||
public computeResize(p: go.Point): go.Point {
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pressing the Delete key removes any column width setting and stops this tool.
|
||||
*/
|
||||
public doKeyDown(): void {
|
||||
if (!this.isActive) return;
|
||||
const e = this.diagram.lastInput;
|
||||
if (e.key === 'Del' || e.key === '\t') { // remove width setting
|
||||
if (this.adornedTable !== null && this.handle !== null) {
|
||||
const coldef = this.adornedTable.getColumnDefinition(this.handle.column);
|
||||
coldef.width = NaN;
|
||||
this.transactionResult = this.name; // success
|
||||
this.stopTool();
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.doKeyDown();
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>State Chart with Simple Curved Link Reshaping</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">CurvedLinkReshapingTool.ts</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 id="LoadButton" onclick="load()">Load</button> Diagram Model saved in JSON format:
|
||||
<br />
|
||||
<textarea id="mySavedModel" style="width:100%;height:300px">
|
||||
{ "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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { CurvedLinkReshapingTool } from './CurvedLinkReshapingTool.js';
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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', (e) => {
|
||||
const button = document.getElementById('SaveButton');
|
||||
if (button)
|
||||
button.disabled = !myDiagram.isModified;
|
||||
const idx = document.title.indexOf('*');
|
||||
if (myDiagram.isModified) {
|
||||
if (idx < 0)
|
||||
document.title += '*';
|
||||
}
|
||||
else {
|
||||
if (idx >= 0)
|
||||
document.title = document.title.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,
|
||||
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) {
|
||||
const adorn = obj.part;
|
||||
const fromNode = adorn.adornedPart;
|
||||
if (fromNode === null)
|
||||
return;
|
||||
e.handled = true;
|
||||
const diagram = e.diagram;
|
||||
diagram.startTransaction('Add State');
|
||||
// get the node data for which the user clicked the button
|
||||
const fromData = fromNode.data;
|
||||
// create a new "State" data object, positioned off to the right of the adorned Node
|
||||
const toData = { text: 'new' };
|
||||
const 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
|
||||
const model = diagram.model;
|
||||
model.addNodeData(toData);
|
||||
// create a link data from the old node data to the new node data
|
||||
const linkdata = {
|
||||
from: model.getKeyForNodeData(fromData),
|
||||
to: model.getKeyForNodeData(toData),
|
||||
text: 'transition'
|
||||
};
|
||||
// and add the link data to the model
|
||||
model.addLinkData(linkdata);
|
||||
// select the new Node
|
||||
const 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
|
||||
if (newnode !== null)
|
||||
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())));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// read in the JSON-format data from the "mySavedModel" element
|
||||
load();
|
||||
document.getElementById("SaveButton").onclick = save;
|
||||
document.getElementById("LoadButton").onclick = load;
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* This CurvedLinkReshapingTool class allows for a {@link Link}'s path to be modified by the user
|
||||
* via the dragging of a single tool handle at the middle of the link.
|
||||
* Dragging the handle changes the value of {@link Link#curviness}.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/CurvedLinkReshaping.html">Curved Link Reshaping</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class CurvedLinkReshapingTool extends go.LinkReshapingTool {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._originalCurviness = NaN;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
makeAdornment(pathshape) {
|
||||
const link = pathshape.part;
|
||||
if (link !== null && link.curve === go.Link.Bezier && link.pointsCount === 4) {
|
||||
const adornment = new go.Adornment();
|
||||
adornment.type = go.Panel.Link;
|
||||
const h = this.makeHandle(pathshape, 0);
|
||||
this.setReshapingBehavior(h, go.LinkReshapingTool.All);
|
||||
h.cursor = 'move';
|
||||
adornment.add(h);
|
||||
adornment.category = this.name;
|
||||
adornment.adornedObject = pathshape;
|
||||
return adornment;
|
||||
}
|
||||
else {
|
||||
return super.makeAdornment(pathshape);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Start reshaping, if {@link #findToolHandleAt} finds a reshape handle at the mouse down point.
|
||||
*
|
||||
* If successful this sets {@link #handle} to be the reshape handle that it finds
|
||||
* and {@link #adornedLink} to be the {@link Link} being routed.
|
||||
* It also remembers the original link route (a list of Points) and curviness in case this tool is cancelled.
|
||||
* And it starts a transaction.
|
||||
*/
|
||||
doActivate() {
|
||||
super.doActivate();
|
||||
if (this.adornedLink !== null)
|
||||
this._originalCurviness = this.adornedLink.curviness;
|
||||
}
|
||||
/**
|
||||
* Restore the link route to be the original points and curviness and stop this tool.
|
||||
*/
|
||||
doCancel() {
|
||||
if (this.adornedLink !== null)
|
||||
this.adornedLink.curviness = this._originalCurviness;
|
||||
super.doCancel();
|
||||
}
|
||||
/**
|
||||
* Change the route of the {@link #adornedLink} by moving the point corresponding to the current
|
||||
* {@link #handle} to be at the given {@link Point}.
|
||||
* This is called by {@link #doMouseMove} and {@link #doMouseUp} with the result of calling
|
||||
* {@link #computeReshape} to constrain the input point.
|
||||
* @param {Point} newpt the value of the call to {@link #computeReshape}.
|
||||
*/
|
||||
reshape(newpt) {
|
||||
const link = this.adornedLink;
|
||||
if (link !== null && link.curve === go.Link.Bezier && link.pointsCount === 4) {
|
||||
const start = link.getPoint(0);
|
||||
const end = link.getPoint(3);
|
||||
const ang = start.directionPoint(end);
|
||||
const mid = new go.Point((start.x + end.x) / 2, (start.y + end.y) / 2);
|
||||
const a = new go.Point(9999, 0).rotate(ang + 90).add(mid);
|
||||
const b = new go.Point(9999, 0).rotate(ang - 90).add(mid);
|
||||
const q = newpt.copy().projectOntoLineSegmentPoint(a, b);
|
||||
let curviness = Math.sqrt(mid.distanceSquaredPoint(q));
|
||||
const port = link.fromPort;
|
||||
if (port === link.toPort && port !== null) {
|
||||
if (newpt.y < port.getDocumentPoint(go.Spot.Center).y)
|
||||
curviness = -curviness;
|
||||
}
|
||||
else {
|
||||
const diff = mid.directionPoint(q) - ang;
|
||||
if ((diff > 0 && diff < 180) || (diff < -180))
|
||||
curviness = -curviness;
|
||||
}
|
||||
link.curviness = curviness;
|
||||
}
|
||||
else {
|
||||
super.reshape(newpt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* This CurvedLinkReshapingTool class allows for a {@link Link}'s path to be modified by the user
|
||||
* via the dragging of a single tool handle at the middle of the link.
|
||||
* Dragging the handle changes the value of {@link Link#curviness}.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/CurvedLinkReshaping.html">Curved Link Reshaping</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class CurvedLinkReshapingTool extends go.LinkReshapingTool {
|
||||
private _originalCurviness: number = NaN;
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
public makeAdornment(pathshape: go.GraphObject): go.Adornment {
|
||||
const link = pathshape.part as go.Link;
|
||||
if (link !== null && link.curve === go.Link.Bezier && link.pointsCount === 4) {
|
||||
const adornment = new go.Adornment();
|
||||
adornment.type = go.Panel.Link;
|
||||
const h = this.makeHandle(pathshape, 0);
|
||||
this.setReshapingBehavior(h, go.LinkReshapingTool.All);
|
||||
h.cursor = 'move';
|
||||
adornment.add(h);
|
||||
adornment.category = this.name;
|
||||
adornment.adornedObject = pathshape;
|
||||
return adornment;
|
||||
} else {
|
||||
return super.makeAdornment(pathshape);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start reshaping, if {@link #findToolHandleAt} finds a reshape handle at the mouse down point.
|
||||
*
|
||||
* If successful this sets {@link #handle} to be the reshape handle that it finds
|
||||
* and {@link #adornedLink} to be the {@link Link} being routed.
|
||||
* It also remembers the original link route (a list of Points) and curviness in case this tool is cancelled.
|
||||
* And it starts a transaction.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
super.doActivate();
|
||||
if (this.adornedLink !== null) this._originalCurviness = this.adornedLink.curviness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the link route to be the original points and curviness and stop this tool.
|
||||
*/
|
||||
public doCancel(): void {
|
||||
if (this.adornedLink !== null) this.adornedLink.curviness = this._originalCurviness;
|
||||
super.doCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the route of the {@link #adornedLink} by moving the point corresponding to the current
|
||||
* {@link #handle} to be at the given {@link Point}.
|
||||
* This is called by {@link #doMouseMove} and {@link #doMouseUp} with the result of calling
|
||||
* {@link #computeReshape} to constrain the input point.
|
||||
* @param {Point} newpt the value of the call to {@link #computeReshape}.
|
||||
*/
|
||||
public reshape(newpt: go.Point): void {
|
||||
const link = this.adornedLink;
|
||||
if (link !== null && link.curve === go.Link.Bezier && link.pointsCount === 4) {
|
||||
const start = link.getPoint(0);
|
||||
const end = link.getPoint(3);
|
||||
const ang = start.directionPoint(end);
|
||||
const mid = new go.Point((start.x + end.x) / 2, (start.y + end.y) / 2);
|
||||
const a = new go.Point(9999, 0).rotate(ang + 90).add(mid);
|
||||
const b = new go.Point(9999, 0).rotate(ang - 90).add(mid);
|
||||
const q = newpt.copy().projectOntoLineSegmentPoint(a, b);
|
||||
let curviness = Math.sqrt(mid.distanceSquaredPoint(q));
|
||||
const port = link.fromPort;
|
||||
if (port === link.toPort && port !== null) {
|
||||
if (newpt.y < port.getDocumentPoint(go.Spot.Center).y) curviness = -curviness;
|
||||
} else {
|
||||
const diff = mid.directionPoint(q) - ang;
|
||||
if ((diff > 0 && diff < 180) || (diff < -180)) curviness = -curviness;
|
||||
}
|
||||
link.curviness = curviness;
|
||||
} else {
|
||||
super.reshape(newpt);
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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;
|
||||
}
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Data Inspector</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
<link rel='stylesheet' href='DataInspector.css' />
|
||||
</head>
|
||||
<body>
|
||||
<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="myInspectorDiv1" 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.ts">DataInspector.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { Inspector } from './DataInspector.js';
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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) {
|
||||
const elt = document.getElementById('savedModel');
|
||||
if (elt !== null)
|
||||
elt.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',
|
||||
portId: '', cursor: 'pointer',
|
||||
// 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,
|
||||
isMultiline: false,
|
||||
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',
|
||||
ungroupable: true
|
||||
}, // enable Ctrl-Shift-G to ungroup a selected Group
|
||||
$(go.TextBlock, {
|
||||
font: 'bold 19px sans-serif',
|
||||
isMultiline: false,
|
||||
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:
|
||||
const 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 }
|
||||
];
|
||||
const 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);
|
||||
// myDiagram.model = go.Model.fromJson((document.getElementById('mySavedModel') as any).value);
|
||||
// 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:
|
||||
const inspector1 = new Inspector('myInspectorDiv1', 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
|
||||
showLimit: 4,
|
||||
// when multipleSelection is true, when showUnionProperties is true it takes the union of properties
|
||||
// otherwise it takes the intersection of properties
|
||||
showUnionProperties: 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 },
|
||||
// an example of specifying the <input> type
|
||||
'password': { show: Inspector.showIfPresent, type: 'password' }
|
||||
}
|
||||
});
|
||||
// Always show the first Node:
|
||||
const 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(object)
|
||||
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(myDiagram.selection.first(), 'key') }
|
||||
}
|
||||
});
|
||||
// If not inspecting a selection, you can programatically decide what to inspect (a Part, or a JavaScript object)
|
||||
// Here, we inspect the first node, if available
|
||||
const firstnode = myDiagram.nodes.first();
|
||||
if (firstnode !== null)
|
||||
inspector2.inspectObject(firstnode.data);
|
||||
// Always show the model.modelData:
|
||||
const inspector3 = new Inspector('myInspectorDiv3', myDiagram, {
|
||||
inspectSelection: false
|
||||
});
|
||||
inspector3.inspectObject(myDiagram.model.modelData);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
window.inspector1 = inspector1;
|
||||
window.inspector2 = inspector2;
|
||||
window.inspector3 = inspector3;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+906
@@ -0,0 +1,906 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* This class implements an inspector for GoJS model data objects.
|
||||
* The constructor takes three arguments:
|
||||
* - `divid` ***string*** a string referencing the HTML ID of the to-be inspector's div
|
||||
* - `diagram` ***Diagram*** a reference to a GoJS Diagram
|
||||
* - `options` ***Object*** an optional JS Object describing options for the inspector
|
||||
*
|
||||
* Options:
|
||||
* - `inspectSelection` ***boolean*** see {@link #inspectSelection}
|
||||
* - `includesOwnProperties` ***boolean*** see {@link #includesOwnProperties}
|
||||
* - `properties` ***Object*** see {@link #properties}
|
||||
* - `propertyModified` ***function(propertyName, newValue, inspector)*** see {@link #propertyModified}
|
||||
* - `multipleSelection` ***boolean*** see {@link #multipleSelection}
|
||||
* - `showUnionProperties` ***boolean*** see {@link #showUnionProperties}
|
||||
* - `showLimit` ***number*** see {@link #showLimit}
|
||||
*
|
||||
* 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` ***any*** 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:
|
||||
* ```js
|
||||
* 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:
|
||||
* ```html
|
||||
* <div id="divid" class="inspector">
|
||||
* <tr>
|
||||
* <td>propertyName</td>
|
||||
* <td><input value=propertyValue /></td>
|
||||
* </tr>
|
||||
* ...
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DataInspector.html">Data Inspector</a> sample.
|
||||
* @category Extension
|
||||
*/
|
||||
export class Inspector {
|
||||
/**
|
||||
* Constructs an Inspector and sets up properties based on the options provided.
|
||||
* Also sets up change listeners on the Diagram so the Inspector stays up-to-date.
|
||||
* @param {string} divid a string referencing the HTML ID of the to-be Inspector's div
|
||||
* @param {Diagram} diagram a reference to a GoJS Diagram
|
||||
* @param {Object=} options an optional JS Object describing options for the inspector
|
||||
*/
|
||||
constructor(divid, diagram, options) {
|
||||
this._inspectedObject = null;
|
||||
// Inspector options defaults:
|
||||
this._inspectSelection = true;
|
||||
this._includesOwnProperties = true;
|
||||
this._properties = {};
|
||||
this._propertyModified = null;
|
||||
this._multipleSelection = false;
|
||||
this._showUnionProperties = false;
|
||||
this._showLimit = 0;
|
||||
// Private variables used to keep track of internal state
|
||||
this.inspectedProperties = {};
|
||||
this.multipleProperties = {};
|
||||
const mainDiv = document.getElementById(divid);
|
||||
mainDiv.className = 'inspector';
|
||||
mainDiv.innerHTML = '';
|
||||
this._div = mainDiv;
|
||||
this._diagram = diagram;
|
||||
this.tabIndex = 0;
|
||||
// Set properties based on options
|
||||
if (options !== undefined) {
|
||||
if (options.inspectSelection !== undefined)
|
||||
this._inspectSelection = options.inspectSelection;
|
||||
if (options.includesOwnProperties !== undefined)
|
||||
this._includesOwnProperties = options.includesOwnProperties;
|
||||
if (options.properties !== undefined)
|
||||
this._properties = options.properties;
|
||||
if (options.propertyModified !== undefined)
|
||||
this._propertyModified = options.propertyModified;
|
||||
if (options.multipleSelection !== undefined)
|
||||
this._multipleSelection = options.multipleSelection;
|
||||
if (options.showUnionProperties !== undefined)
|
||||
this._showUnionProperties = options.showUnionProperties;
|
||||
if (options.showLimit !== undefined)
|
||||
this._showLimit = options.showLimit;
|
||||
}
|
||||
// Prepare change listeners
|
||||
const self = this;
|
||||
this.inspectOnModelChanged = (e) => {
|
||||
if (e.isTransactionFinished)
|
||||
self.inspectObject();
|
||||
};
|
||||
this.inspectOnSelectionChanged = (e) => { self.inspectObject(); };
|
||||
this._diagram.addModelChangedListener(this.inspectOnModelChanged);
|
||||
if (this._inspectSelection) {
|
||||
this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This read-only property returns the HTMLElement containing the Inspector.
|
||||
*/
|
||||
get div() { return this._div; }
|
||||
/**
|
||||
* Gets or sets the {@link Diagram} associated with this Inspector.
|
||||
*/
|
||||
get diagram() { return this._diagram; }
|
||||
set diagram(val) {
|
||||
if (val !== this._diagram) {
|
||||
// First, unassociate change listeners with current inspected diagram
|
||||
this._diagram.removeModelChangedListener(this.inspectOnModelChanged);
|
||||
this._diagram.removeDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
// Now set the diagram and add the necessary change listeners
|
||||
this._diagram = val;
|
||||
this._diagram.addModelChangedListener(this.inspectOnModelChanged);
|
||||
if (this._inspectSelection) {
|
||||
this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
this.inspectObject();
|
||||
}
|
||||
else {
|
||||
this.inspectObject(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This read-only property returns the object currently being inspected.
|
||||
*
|
||||
* To set the inspected object, call {@link #inspectObject}.
|
||||
*/
|
||||
get inspectedObject() { return this._inspectedObject; }
|
||||
/**
|
||||
* Gets or sets whether the Inspector automatically inspects the associated Diagram's selection.
|
||||
* When set to false, the Inspector won't show anything until {@link #inspectObject} is called.
|
||||
*
|
||||
* The default value is true.
|
||||
*/
|
||||
get inspectSelection() { return this._inspectSelection; }
|
||||
set inspectSelection(val) {
|
||||
if (val !== this._inspectSelection) {
|
||||
this._inspectSelection = val;
|
||||
if (this._inspectSelection) {
|
||||
this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
this.inspectObject();
|
||||
}
|
||||
else {
|
||||
this._diagram.removeDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
this.inspectObject(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets or sets whether the Inspector includes all properties currently on the inspected object.
|
||||
*
|
||||
* The default value is true.
|
||||
*/
|
||||
get includesOwnProperties() { return this._includesOwnProperties; }
|
||||
set includesOwnProperties(val) {
|
||||
if (val !== this._includesOwnProperties) {
|
||||
this._includesOwnProperties = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets or sets the properties that the Inspector will inspect, maybe setting options for those properties.
|
||||
* The object should contain string: Object pairs represnting propertyName: propertyOptions.
|
||||
* Can be used to include or exclude additional properties.
|
||||
*
|
||||
* The default value is an empty object.
|
||||
*/
|
||||
get properties() { return this._properties; }
|
||||
set properties(val) {
|
||||
if (val !== this._properties) {
|
||||
this._properties = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets or sets the function to be called when a property is modified by the Inspector.
|
||||
* The first paremeter will be the property name, the second will be the new value, and the third will be a reference to this Inspector.
|
||||
*
|
||||
* The default value is null, meaning nothing will be done.
|
||||
*/
|
||||
get propertyModified() { return this._propertyModified; }
|
||||
set propertyModified(val) {
|
||||
if (val !== this._propertyModified) {
|
||||
this._propertyModified = val;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets or sets whether the Inspector displays properties for multiple selected objects or just the first.
|
||||
*
|
||||
* The default value is false, meaning only the first item in the {@link Diagram#selection} is inspected.
|
||||
*/
|
||||
get multipleSelection() { return this._multipleSelection; }
|
||||
set multipleSelection(val) {
|
||||
if (val !== this._multipleSelection) {
|
||||
this._multipleSelection = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets or sets whether the Inspector displays the union or intersection of properties for multiple selected objects.
|
||||
*
|
||||
* The default value is false, meaning the intersection of properties is inspected.
|
||||
*/
|
||||
get showUnionProperties() { return this._showUnionProperties; }
|
||||
set showUnionProperties(val) {
|
||||
if (val !== this._showUnionProperties) {
|
||||
this._showUnionProperties = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets or sets how many objects will be displayed when {@link #multipleSelection} is true.
|
||||
*
|
||||
* The default value is 0, meaning all selected objects will be displayed for a given property.
|
||||
*/
|
||||
get showLimit() { return this._showLimit; }
|
||||
set showLimit(val) {
|
||||
if (val !== this._showLimit) {
|
||||
this._showLimit = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown when inspecting a {@link Node}.
|
||||
* @param {Part} part the Part being inspected
|
||||
* @return {boolean}
|
||||
*/
|
||||
static showIfNode(part) { return part instanceof go.Node; }
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown when inspecting a {@link Link}.
|
||||
* @param {Part} part the Part being inspected
|
||||
* @return {boolean}
|
||||
*/
|
||||
static showIfLink(part) { return part instanceof go.Link; }
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown when inspecting a {@link Group}.
|
||||
* @param {Part} part the Part being inspected
|
||||
* @return {boolean}
|
||||
*/
|
||||
static showIfGroup(part) { return part instanceof go.Group; }
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown if present.
|
||||
* Useful for properties such as `key`, which will be shown on Nodes and Groups, but normally not on Links
|
||||
* @param {Part|null} part the Part being inspected
|
||||
* @param {string} propname the property to check presence of
|
||||
* @return {boolean}
|
||||
*/
|
||||
static showIfPresent(data, propname) {
|
||||
if (data instanceof go.Part)
|
||||
data = data.data;
|
||||
return typeof data === 'object' && data[propname] !== undefined;
|
||||
}
|
||||
/**
|
||||
* Update the HTML state of this Inspector with the given object.
|
||||
*
|
||||
* If passed an object, the Inspector will inspect that object.
|
||||
* If passed null, this will do nothing.
|
||||
* If no parameter is supplied, the {@link #inspectedObject} will be set based on the value of {@link #inspectSelection}.
|
||||
* @param {Object=} object an optional argument, used when {@link #inspectSelection} is false to
|
||||
* set {@link #inspectedObject} and show and edit that object's properties.
|
||||
*/
|
||||
inspectObject(object) {
|
||||
let inspectedObject = null;
|
||||
let inspectedObjects = null;
|
||||
if (object === null)
|
||||
return;
|
||||
if (object === undefined) {
|
||||
if (this._inspectSelection) {
|
||||
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 && inspectedObject) {
|
||||
inspectedObjects = new go.Set();
|
||||
inspectedObjects.add(inspectedObject);
|
||||
}
|
||||
if (!inspectedObjects || inspectedObjects.count < 1) { // if nothing is selected
|
||||
this.updateAllHTML();
|
||||
return;
|
||||
}
|
||||
if (inspectedObjects) {
|
||||
const mainDiv = this._div;
|
||||
mainDiv.innerHTML = '';
|
||||
const shared = new go.Map(); // for properties that the nodes have in common
|
||||
const properties = new go.Map(); // for adding properties
|
||||
const all = new go.Map(); // used later to prevent changing properties when unneeded
|
||||
const it = inspectedObjects.iterator;
|
||||
let nodecount = 2;
|
||||
// Build table:
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
this.inspectedProperties = {};
|
||||
this.tabIndex = 0;
|
||||
const declaredProperties = this._properties;
|
||||
it.next();
|
||||
inspectedObject = it.value;
|
||||
this._inspectedObject = inspectedObject;
|
||||
let 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 (const name in declaredProperties) {
|
||||
const desc = declaredProperties[name];
|
||||
if (!this.canShowProperty(name, desc, inspectedObject))
|
||||
continue;
|
||||
const val = this.findValue(name, desc, data);
|
||||
if (val === '' && this._properties[name] && this._properties[name].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 (const 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (it.next() && (this._showLimit < 1 || nodecount <= this._showLimit)) { // 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 (const name in declaredProperties) {
|
||||
const desc = declaredProperties[name];
|
||||
if (!this.canShowProperty(name, desc, inspectedObject))
|
||||
continue;
|
||||
const val = this.findValue(name, desc, data);
|
||||
if (val === '' && this._properties[name] && this._properties[name].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 (const 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._showUnionProperties) {
|
||||
// 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
|
||||
const addIt = shared.iterator;
|
||||
const toRemove = [];
|
||||
while (addIt.next()) {
|
||||
if (properties.has(addIt.key)) {
|
||||
let 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 (let i = 0; i < toRemove.length; i++) { // removes anything that doesn't showUnionProperties
|
||||
shared.remove(toRemove[i]);
|
||||
all.remove(toRemove[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Adds missing properties to all with the correct amount of seperators
|
||||
let 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
|
||||
const newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
|
||||
all.set(addIt.key, newVal);
|
||||
}
|
||||
}
|
||||
else {
|
||||
let newVal = '';
|
||||
for (let 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
|
||||
const newVal = all.get(addIt.key) + '|';
|
||||
all.set(addIt.key, newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nodecount++;
|
||||
}
|
||||
// builds the table property rows and sets multipleProperties to help with updateall
|
||||
let mapIt;
|
||||
if (!this._showUnionProperties)
|
||||
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);
|
||||
const allIt = all.iterator;
|
||||
while (allIt.next()) {
|
||||
this.multipleProperties[allIt.key] = allIt.value; // used for updateall to know which properties to change
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
canShowProperty(propertyName, propertyDesc, inspectedObject) {
|
||||
const prop = propertyDesc;
|
||||
if (prop.show === false)
|
||||
return false;
|
||||
// if "show" is a predicate, make sure it passes or do not show this property
|
||||
if (typeof prop.show === 'function')
|
||||
return prop.show(inspectedObject, propertyName);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
canEditProperty(propertyName, propertyDesc, inspectedObject) {
|
||||
if (this._diagram.isReadOnly || this._diagram.isModelReadOnly)
|
||||
return false;
|
||||
if (inspectedObject === null)
|
||||
return false;
|
||||
// assume property values that are functions of Objects cannot be edited
|
||||
const data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
|
||||
const valtype = typeof data[propertyName];
|
||||
if (valtype === 'function')
|
||||
return false;
|
||||
if (propertyDesc) {
|
||||
const prop = propertyDesc;
|
||||
if (prop.readOnly === true)
|
||||
return false;
|
||||
// if "readOnly" is a predicate, make sure it passes or do not show this property
|
||||
if (typeof prop.readOnly === 'function')
|
||||
return !prop.readOnly(inspectedObject, propertyName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* @ignore
|
||||
* @param propName
|
||||
* @param propDesc
|
||||
* @param data
|
||||
*/
|
||||
findValue(propName, propDesc, data) {
|
||||
let val = '';
|
||||
if (propDesc && propDesc.defaultValue !== undefined)
|
||||
val = propDesc.defaultValue;
|
||||
if (data[propName] !== undefined)
|
||||
val = data[propName];
|
||||
if (val === undefined)
|
||||
return '';
|
||||
return val;
|
||||
}
|
||||
/**
|
||||
* This sets `inspectedProperties[propertyName]` and creates the HTML table row for a given property:
|
||||
* ```html
|
||||
* <tr>
|
||||
* <td>propertyName</td>
|
||||
* <td><input value=propertyValue /></td>
|
||||
* </tr>
|
||||
* ```
|
||||
*
|
||||
* This method can be customized to change how an Inspector row is rendered.
|
||||
* @param {string} propertyName the property name
|
||||
* @param {*} propertyValue the property value
|
||||
* @return {HTMLTableRowElement} the table row
|
||||
*/
|
||||
buildPropertyRow(propertyName, propertyValue) {
|
||||
const tr = document.createElement('tr');
|
||||
const td1 = document.createElement('td');
|
||||
let displayName;
|
||||
if (this._properties[propertyName] && this._properties[propertyName].name !== undefined) { // name changes the dispaly name shown on inspector
|
||||
displayName = this._properties[propertyName].name;
|
||||
}
|
||||
else {
|
||||
displayName = propertyName;
|
||||
}
|
||||
td1.textContent = displayName;
|
||||
tr.appendChild(td1);
|
||||
const td2 = document.createElement('td');
|
||||
const decProp = this._properties[propertyName];
|
||||
let input = null;
|
||||
const self = this;
|
||||
function updateall() {
|
||||
if (self._diagram.selection.count === 1 || !self.multipleSelection) {
|
||||
self.updateAllProperties();
|
||||
}
|
||||
else {
|
||||
self.updateAllObjectsProperties();
|
||||
}
|
||||
}
|
||||
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) {
|
||||
const 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); // removed with multi select
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* HTML5 color input will only take hex,
|
||||
* so let HTML5 canvas convert the color into hex format.
|
||||
* This converts "rgb(255, 0, 0)" into "#FF0000", etc.
|
||||
*/
|
||||
convertToColor(propertyValue) {
|
||||
const ctx = document.createElement('canvas').getContext('2d');
|
||||
if (ctx === null)
|
||||
return '#000000';
|
||||
ctx.fillStyle = propertyValue;
|
||||
return ctx.fillStyle;
|
||||
}
|
||||
/**
|
||||
* @hidden @ignore
|
||||
*/
|
||||
convertToArrayOfNumber(propertyValue) {
|
||||
if (propertyValue === 'null')
|
||||
return null;
|
||||
const split = propertyValue.split(' ');
|
||||
const arr = [];
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
const str = split[i];
|
||||
if (!str)
|
||||
continue;
|
||||
arr.push(parseFloat(str));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
/**
|
||||
* @hidden @ignore
|
||||
*/
|
||||
convertToString(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)) {
|
||||
let str = '';
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
if (i > 0)
|
||||
str += ' ';
|
||||
const v = x[i];
|
||||
str += this.convertToString(v);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
return x.toString();
|
||||
}
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update all of the HTML in this Inspector.
|
||||
*/
|
||||
updateAllHTML() {
|
||||
const inspectedProps = this.inspectedProperties;
|
||||
const isPart = this._inspectedObject instanceof go.Part;
|
||||
const data = isPart ? this._inspectedObject.data : this._inspectedObject;
|
||||
if (!data) { // clear out all of the fields
|
||||
for (const name in inspectedProps) {
|
||||
const 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 (const name in inspectedProps) {
|
||||
const input = inspectedProps[name];
|
||||
const propertyValue = data[name];
|
||||
if (input instanceof HTMLSelectElement) {
|
||||
const decProp = this._properties[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update an HTMLSelectElement with an appropriate list of choices, given the propertyName
|
||||
*/
|
||||
updateSelect(decProp, select, propertyName, propertyValue) {
|
||||
select.innerHTML = ''; // clear out anything that was there
|
||||
let 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 (let i = 0; i < choices.length; i++) {
|
||||
const choice = choices[i];
|
||||
const opt = document.createElement('option');
|
||||
opt.text = this.convertToString(choice);
|
||||
select.add(opt);
|
||||
}
|
||||
select.value = this.convertToString(propertyValue);
|
||||
}
|
||||
parseValue(decProp, value, input, oldval) {
|
||||
// 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
|
||||
let type = '';
|
||||
if (decProp !== undefined && decProp.type !== undefined) {
|
||||
type = decProp.type;
|
||||
}
|
||||
if (type === '') {
|
||||
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;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update all of the data properties of all the objects in {@link #inspectedObjects} according to the
|
||||
* current values held in the HTML input elements.
|
||||
*/
|
||||
updateAllObjectsProperties() {
|
||||
const inspectedProps = this.inspectedProperties;
|
||||
const diagram = this._diagram;
|
||||
diagram.startTransaction('set all properties');
|
||||
for (const name in inspectedProps) {
|
||||
const input = inspectedProps[name];
|
||||
let value = input.value;
|
||||
const arr1 = value.split('|');
|
||||
let arr2 = [];
|
||||
if (this.multipleProperties[name]) {
|
||||
// don't split if it is union and its checkbox type
|
||||
if (this._properties[name] && this._properties[name].type === 'checkbox' && this._showUnionProperties) {
|
||||
arr2.push(this.multipleProperties[name]);
|
||||
}
|
||||
else if (this._properties[name]) {
|
||||
arr2 = this.multipleProperties[name].toString().split('|');
|
||||
}
|
||||
}
|
||||
const it = diagram.selection.iterator;
|
||||
let change = false;
|
||||
if (this._properties[name] && this._properties[name].type === 'checkbox')
|
||||
change = true; // always change checkbox
|
||||
if (arr1.length < arr2.length // i.e Alpha|Beta -> Alpha procs the change
|
||||
&& (!this._properties[name] // from and to links
|
||||
|| !(this._properties[name] // do not change color checkbox and choices due to them always having less
|
||||
&& (this._properties[name].type === 'color' || this._properties[name].type === 'checkbox' || this._properties[name].type === 'choices')))) {
|
||||
change = true;
|
||||
}
|
||||
else { // standard detection in change in properties
|
||||
for (let j = 0; j < arr1.length && j < arr2.length; j++) {
|
||||
if (!(arr1[j] === arr2[j])
|
||||
&& !(this._properties[name] && this._properties[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 (let i = 0; i < diagram.selection.count; i++) {
|
||||
it.next();
|
||||
const isPart = it.value instanceof go.Part;
|
||||
const 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
|
||||
const decProp = this._properties[name];
|
||||
if (!this.canEditProperty(name, decProp, it.value))
|
||||
continue;
|
||||
const oldval = data[name];
|
||||
value = this.parseValue(decProp, value, input, oldval);
|
||||
// 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');
|
||||
}
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update all of the data properties of {@link #inspectedObject} according to the
|
||||
* current values held in the HTML input elements.
|
||||
*/
|
||||
updateAllProperties() {
|
||||
const inspectedProps = this.inspectedProperties;
|
||||
const diagram = this._diagram;
|
||||
const isPart = this.inspectedObject instanceof go.Part;
|
||||
const 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 (const name in inspectedProps) {
|
||||
const input = inspectedProps[name];
|
||||
let value = input.value;
|
||||
// don't update "readOnly" data properties
|
||||
const decProp = this._properties[name];
|
||||
if (!this.canEditProperty(name, decProp, this.inspectedObject))
|
||||
continue;
|
||||
const oldval = data[name];
|
||||
value = this.parseValue(decProp, value, input, oldval);
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
+862
@@ -0,0 +1,862 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* This class implements an inspector for GoJS model data objects.
|
||||
* The constructor takes three arguments:
|
||||
* - `divid` ***string*** a string referencing the HTML ID of the to-be inspector's div
|
||||
* - `diagram` ***Diagram*** a reference to a GoJS Diagram
|
||||
* - `options` ***Object*** an optional JS Object describing options for the inspector
|
||||
*
|
||||
* Options:
|
||||
* - `inspectSelection` ***boolean*** see {@link #inspectSelection}
|
||||
* - `includesOwnProperties` ***boolean*** see {@link #includesOwnProperties}
|
||||
* - `properties` ***Object*** see {@link #properties}
|
||||
* - `propertyModified` ***function(propertyName, newValue, inspector)*** see {@link #propertyModified}
|
||||
* - `multipleSelection` ***boolean*** see {@link #multipleSelection}
|
||||
* - `showUnionProperties` ***boolean*** see {@link #showUnionProperties}
|
||||
* - `showLimit` ***number*** see {@link #showLimit}
|
||||
*
|
||||
* 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` ***any*** 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:
|
||||
* ```js
|
||||
* 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:
|
||||
* ```html
|
||||
* <div id="divid" class="inspector">
|
||||
* <tr>
|
||||
* <td>propertyName</td>
|
||||
* <td><input value=propertyValue /></td>
|
||||
* </tr>
|
||||
* ...
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DataInspector.html">Data Inspector</a> sample.
|
||||
* @category Extension
|
||||
*/
|
||||
export class Inspector {
|
||||
private _div: HTMLDivElement;
|
||||
private _diagram: go.Diagram;
|
||||
private _inspectedObject: go.ObjectData | null = null;
|
||||
// Inspector options defaults:
|
||||
private _inspectSelection: boolean = true;
|
||||
private _includesOwnProperties: boolean = true;
|
||||
private _properties: { [index: string]: any } = {};
|
||||
private _propertyModified: ((a: string, b: string, c: Inspector) => void) | null = null;
|
||||
private _multipleSelection: boolean = false;
|
||||
private _showUnionProperties: boolean = false;
|
||||
private _showLimit: number = 0;
|
||||
|
||||
// Private variables used to keep track of internal state
|
||||
private inspectedProperties: { [index: string]: any } = {};
|
||||
private multipleProperties: { [index: string]: any } = {};
|
||||
private tabIndex: number;
|
||||
// Functions used to keep the Inspector up-to-date
|
||||
private inspectOnModelChanged: ((e: go.ChangedEvent) => void);
|
||||
private inspectOnSelectionChanged: ((e: go.DiagramEvent) => void);
|
||||
|
||||
/**
|
||||
* Constructs an Inspector and sets up properties based on the options provided.
|
||||
* Also sets up change listeners on the Diagram so the Inspector stays up-to-date.
|
||||
* @param {string} divid a string referencing the HTML ID of the to-be Inspector's div
|
||||
* @param {Diagram} diagram a reference to a GoJS Diagram
|
||||
* @param {Object=} options an optional JS Object describing options for the inspector
|
||||
*/
|
||||
constructor(divid: string, diagram: go.Diagram, options?: { [index: string]: any }) {
|
||||
const mainDiv = document.getElementById(divid) as HTMLDivElement;
|
||||
mainDiv.className = 'inspector';
|
||||
mainDiv.innerHTML = '';
|
||||
this._div = mainDiv;
|
||||
this._diagram = diagram;
|
||||
this.tabIndex = 0;
|
||||
// Set properties based on options
|
||||
if (options !== undefined) {
|
||||
if (options.inspectSelection !== undefined) this._inspectSelection = options.inspectSelection;
|
||||
if (options.includesOwnProperties !== undefined) this._includesOwnProperties = options.includesOwnProperties;
|
||||
if (options.properties !== undefined) this._properties = options.properties;
|
||||
if (options.propertyModified !== undefined) this._propertyModified = options.propertyModified;
|
||||
if (options.multipleSelection !== undefined) this._multipleSelection = options.multipleSelection;
|
||||
if (options.showUnionProperties !== undefined) this._showUnionProperties = options.showUnionProperties;
|
||||
if (options.showLimit !== undefined) this._showLimit = options.showLimit;
|
||||
}
|
||||
// Prepare change listeners
|
||||
const self = this;
|
||||
this.inspectOnModelChanged = (e: go.ChangedEvent) => {
|
||||
if (e.isTransactionFinished) self.inspectObject();
|
||||
};
|
||||
this.inspectOnSelectionChanged = (e: go.DiagramEvent) => { self.inspectObject(); };
|
||||
this._diagram.addModelChangedListener(this.inspectOnModelChanged);
|
||||
if (this._inspectSelection) {
|
||||
this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This read-only property returns the HTMLElement containing the Inspector.
|
||||
*/
|
||||
get div(): HTMLDivElement { return this._div; }
|
||||
|
||||
/**
|
||||
* Gets or sets the {@link Diagram} associated with this Inspector.
|
||||
*/
|
||||
get diagram(): go.Diagram { return this._diagram; }
|
||||
set diagram(val: go.Diagram) {
|
||||
if (val !== this._diagram) {
|
||||
// First, unassociate change listeners with current inspected diagram
|
||||
this._diagram.removeModelChangedListener(this.inspectOnModelChanged);
|
||||
this._diagram.removeDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
// Now set the diagram and add the necessary change listeners
|
||||
this._diagram = val;
|
||||
this._diagram.addModelChangedListener(this.inspectOnModelChanged);
|
||||
if (this._inspectSelection) {
|
||||
this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
this.inspectObject();
|
||||
} else {
|
||||
this.inspectObject(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This read-only property returns the object currently being inspected.
|
||||
*
|
||||
* To set the inspected object, call {@link #inspectObject}.
|
||||
*/
|
||||
get inspectedObject(): go.ObjectData | null { return this._inspectedObject; }
|
||||
|
||||
/**
|
||||
* Gets or sets whether the Inspector automatically inspects the associated Diagram's selection.
|
||||
* When set to false, the Inspector won't show anything until {@link #inspectObject} is called.
|
||||
*
|
||||
* The default value is true.
|
||||
*/
|
||||
get inspectSelection(): boolean { return this._inspectSelection; }
|
||||
set inspectSelection(val: boolean) {
|
||||
if (val !== this._inspectSelection) {
|
||||
this._inspectSelection = val;
|
||||
if (this._inspectSelection) {
|
||||
this._diagram.addDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
this.inspectObject();
|
||||
} else {
|
||||
this._diagram.removeDiagramListener('ChangedSelection', this.inspectOnSelectionChanged);
|
||||
this.inspectObject(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets whether the Inspector includes all properties currently on the inspected object.
|
||||
*
|
||||
* The default value is true.
|
||||
*/
|
||||
get includesOwnProperties(): boolean { return this._includesOwnProperties; }
|
||||
set includesOwnProperties(val: boolean) {
|
||||
if (val !== this._includesOwnProperties) {
|
||||
this._includesOwnProperties = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the properties that the Inspector will inspect, maybe setting options for those properties.
|
||||
* The object should contain string: Object pairs represnting propertyName: propertyOptions.
|
||||
* Can be used to include or exclude additional properties.
|
||||
*
|
||||
* The default value is an empty object.
|
||||
*/
|
||||
get properties(): go.ObjectData { return this._properties; }
|
||||
set properties(val: go.ObjectData) {
|
||||
if (val !== this._properties) {
|
||||
this._properties = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the function to be called when a property is modified by the Inspector.
|
||||
* The first paremeter will be the property name, the second will be the new value, and the third will be a reference to this Inspector.
|
||||
*
|
||||
* The default value is null, meaning nothing will be done.
|
||||
*/
|
||||
get propertyModified(): ((a: string, b: string, c: Inspector) => void) | null { return this._propertyModified; }
|
||||
set propertyModified(val: ((a: string, b: string, c: Inspector) => void) | null) {
|
||||
if (val !== this._propertyModified) {
|
||||
this._propertyModified = val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets whether the Inspector displays properties for multiple selected objects or just the first.
|
||||
*
|
||||
* The default value is false, meaning only the first item in the {@link Diagram#selection} is inspected.
|
||||
*/
|
||||
get multipleSelection(): boolean { return this._multipleSelection; }
|
||||
set multipleSelection(val: boolean) {
|
||||
if (val !== this._multipleSelection) {
|
||||
this._multipleSelection = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets whether the Inspector displays the union or intersection of properties for multiple selected objects.
|
||||
*
|
||||
* The default value is false, meaning the intersection of properties is inspected.
|
||||
*/
|
||||
get showUnionProperties(): boolean { return this._showUnionProperties; }
|
||||
set showUnionProperties(val: boolean) {
|
||||
if (val !== this._showUnionProperties) {
|
||||
this._showUnionProperties = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets how many objects will be displayed when {@link #multipleSelection} is true.
|
||||
*
|
||||
* The default value is 0, meaning all selected objects will be displayed for a given property.
|
||||
*/
|
||||
get showLimit(): number { return this._showLimit; }
|
||||
set showLimit(val: number) {
|
||||
if (val !== this._showLimit) {
|
||||
this._showLimit = val;
|
||||
this.inspectObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown when inspecting a {@link Node}.
|
||||
* @param {Part} part the Part being inspected
|
||||
* @return {boolean}
|
||||
*/
|
||||
public static showIfNode(part: go.Part): boolean { return part instanceof go.Node; }
|
||||
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown when inspecting a {@link Link}.
|
||||
* @param {Part} part the Part being inspected
|
||||
* @return {boolean}
|
||||
*/
|
||||
public static showIfLink(part: go.Part): boolean { return part instanceof go.Link; }
|
||||
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown when inspecting a {@link Group}.
|
||||
* @param {Part} part the Part being inspected
|
||||
* @return {boolean}
|
||||
*/
|
||||
public static showIfGroup(part: go.Part): boolean { return part instanceof go.Group; }
|
||||
|
||||
/**
|
||||
* This predicate function can be used as a value for the `show` option for properties.
|
||||
* When used, the property will only be shown if present.
|
||||
* Useful for properties such as `key`, which will be shown on Nodes and Groups, but normally not on Links
|
||||
* @param {Part|null} part the Part being inspected
|
||||
* @param {string} propname the property to check presence of
|
||||
* @return {boolean}
|
||||
*/
|
||||
public static showIfPresent(data: go.Part | null, propname: string): boolean {
|
||||
if (data instanceof go.Part) data = data.data;
|
||||
return typeof data === 'object' && (data as any)[propname] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the HTML state of this Inspector with the given object.
|
||||
*
|
||||
* If passed an object, the Inspector will inspect that object.
|
||||
* If passed null, this will do nothing.
|
||||
* If no parameter is supplied, the {@link #inspectedObject} will be set based on the value of {@link #inspectSelection}.
|
||||
* @param {Object=} object an optional argument, used when {@link #inspectSelection} is false to
|
||||
* set {@link #inspectedObject} and show and edit that object's properties.
|
||||
*/
|
||||
public inspectObject(object?: go.ObjectData | null): void {
|
||||
let inspectedObject: go.ObjectData | null = null;
|
||||
let inspectedObjects: go.Set<go.ObjectData> | null = null;
|
||||
if (object === null) return;
|
||||
if (object === undefined) {
|
||||
if (this._inspectSelection) {
|
||||
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 && inspectedObject) {
|
||||
inspectedObjects = new go.Set<go.ObjectData>();
|
||||
inspectedObjects.add(inspectedObject);
|
||||
}
|
||||
if (!inspectedObjects || inspectedObjects.count < 1) { // if nothing is selected
|
||||
this.updateAllHTML();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inspectedObjects) {
|
||||
const mainDiv = this._div;
|
||||
mainDiv.innerHTML = '';
|
||||
const shared: go.Map<string, any> = new go.Map<string, any>(); // for properties that the nodes have in common
|
||||
const properties: go.Map<string, any> = new go.Map<string, any>(); // for adding properties
|
||||
const all: go.Map<string, any> = new go.Map<string, any>(); // used later to prevent changing properties when unneeded
|
||||
const it = inspectedObjects.iterator;
|
||||
let nodecount = 2;
|
||||
// Build table:
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
this.inspectedProperties = {};
|
||||
this.tabIndex = 0;
|
||||
const declaredProperties = this._properties;
|
||||
it.next();
|
||||
inspectedObject = it.value;
|
||||
this._inspectedObject = inspectedObject;
|
||||
let 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 (const name in declaredProperties) {
|
||||
const desc = declaredProperties[name];
|
||||
if (!this.canShowProperty(name, desc, inspectedObject)) continue;
|
||||
const val = this.findValue(name, desc, data);
|
||||
if (val === '' && this._properties[name] && this._properties[name].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 (const 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (it.next() && (this._showLimit < 1 || nodecount <= this._showLimit)) { // 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 (const name in declaredProperties) {
|
||||
const desc = declaredProperties[name];
|
||||
if (!this.canShowProperty(name, desc, inspectedObject)) continue;
|
||||
const val = this.findValue(name, desc, data);
|
||||
if (val === '' && this._properties[name] && this._properties[name].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 (const 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._showUnionProperties) {
|
||||
// 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
|
||||
const addIt = shared.iterator;
|
||||
const toRemove: Array<string> = [];
|
||||
while (addIt.next()) {
|
||||
if (properties.has(addIt.key)) {
|
||||
let 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 (let i = 0; i < toRemove.length; i++) { // removes anything that doesn't showUnionProperties
|
||||
shared.remove(toRemove[i]);
|
||||
all.remove(toRemove[i]);
|
||||
}
|
||||
} else {
|
||||
// Adds missing properties to all with the correct amount of seperators
|
||||
let 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
|
||||
const newVal = all.get(addIt.key) + '|' + properties.get(addIt.key);
|
||||
all.set(addIt.key, newVal);
|
||||
}
|
||||
} else {
|
||||
let newVal = '';
|
||||
for (let 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
|
||||
const newVal = all.get(addIt.key) + '|';
|
||||
all.set(addIt.key, newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nodecount++;
|
||||
}
|
||||
// builds the table property rows and sets multipleProperties to help with updateall
|
||||
let mapIt;
|
||||
if (!this._showUnionProperties) 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);
|
||||
const allIt = all.iterator;
|
||||
while (allIt.next()) {
|
||||
this.multipleProperties[allIt.key] = allIt.value; // used for updateall to know which properties to change
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public canShowProperty(propertyName: string, propertyDesc: go.ObjectData, inspectedObject: go.ObjectData): boolean {
|
||||
const prop = propertyDesc as any;
|
||||
if (prop.show === false) return false;
|
||||
// if "show" is a predicate, make sure it passes or do not show this property
|
||||
if (typeof prop.show === 'function') return prop.show(inspectedObject, propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public canEditProperty(propertyName: string, propertyDesc: go.ObjectData, inspectedObject: go.ObjectData | null): boolean {
|
||||
if (this._diagram.isReadOnly || this._diagram.isModelReadOnly) return false;
|
||||
if (inspectedObject === null) return false;
|
||||
// assume property values that are functions of Objects cannot be edited
|
||||
const data = (inspectedObject instanceof go.Part) ? inspectedObject.data : inspectedObject;
|
||||
const valtype = typeof data[propertyName];
|
||||
if (valtype === 'function') return false;
|
||||
if (propertyDesc) {
|
||||
const prop = propertyDesc as any;
|
||||
if (prop.readOnly === true) return false;
|
||||
// if "readOnly" is a predicate, make sure it passes or do not show this property
|
||||
if (typeof prop.readOnly === 'function') return !prop.readOnly(inspectedObject, propertyName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @param propName
|
||||
* @param propDesc
|
||||
* @param data
|
||||
*/
|
||||
private findValue(propName: string, propDesc: any, data: any): any {
|
||||
let val = '';
|
||||
if (propDesc && propDesc.defaultValue !== undefined) val = propDesc.defaultValue;
|
||||
if (data[propName] !== undefined) val = data[propName];
|
||||
if (val === undefined) return '';
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets `inspectedProperties[propertyName]` and creates the HTML table row for a given property:
|
||||
* ```html
|
||||
* <tr>
|
||||
* <td>propertyName</td>
|
||||
* <td><input value=propertyValue /></td>
|
||||
* </tr>
|
||||
* ```
|
||||
*
|
||||
* This method can be customized to change how an Inspector row is rendered.
|
||||
* @param {string} propertyName the property name
|
||||
* @param {*} propertyValue the property value
|
||||
* @return {HTMLTableRowElement} the table row
|
||||
*/
|
||||
public buildPropertyRow(propertyName: string, propertyValue: any): HTMLTableRowElement {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const td1 = document.createElement('td');
|
||||
let displayName;
|
||||
if (this._properties[propertyName] && this._properties[propertyName].name !== undefined) { // name changes the dispaly name shown on inspector
|
||||
displayName = this._properties[propertyName].name;
|
||||
} else {
|
||||
displayName = propertyName;
|
||||
}
|
||||
td1.textContent = displayName;
|
||||
|
||||
tr.appendChild(td1);
|
||||
|
||||
const td2 = document.createElement('td');
|
||||
const decProp = this._properties[propertyName];
|
||||
let input = null;
|
||||
const self = this;
|
||||
function updateall() {
|
||||
if (self._diagram.selection.count === 1 || !self.multipleSelection) {
|
||||
self.updateAllProperties();
|
||||
} else {
|
||||
self.updateAllObjectsProperties();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const 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); // removed with multi select
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* HTML5 color input will only take hex,
|
||||
* so let HTML5 canvas convert the color into hex format.
|
||||
* This converts "rgb(255, 0, 0)" into "#FF0000", etc.
|
||||
*/
|
||||
public convertToColor(propertyValue: string): string {
|
||||
const ctx: CanvasRenderingContext2D | null = document.createElement('canvas').getContext('2d');
|
||||
if (ctx === null) return '#000000';
|
||||
ctx.fillStyle = propertyValue;
|
||||
return ctx.fillStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @ignore
|
||||
*/
|
||||
public convertToArrayOfNumber(propertyValue: string): Array<number> | null {
|
||||
if (propertyValue === 'null') return null;
|
||||
const split = propertyValue.split(' ');
|
||||
const arr = [];
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
const str = split[i];
|
||||
if (!str) continue;
|
||||
arr.push(parseFloat(str));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @ignore
|
||||
*/
|
||||
public convertToString(x: any): string {
|
||||
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)) {
|
||||
let str = '';
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
if (i > 0) str += ' ';
|
||||
const v = x[i];
|
||||
str += this.convertToString(v);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
return x.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update all of the HTML in this Inspector.
|
||||
*/
|
||||
public updateAllHTML(): void {
|
||||
const inspectedProps = this.inspectedProperties;
|
||||
const isPart = this._inspectedObject instanceof go.Part;
|
||||
const data = isPart ? (this._inspectedObject as any).data : this._inspectedObject;
|
||||
if (!data) { // clear out all of the fields
|
||||
for (const name in inspectedProps) {
|
||||
const 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 (const name in inspectedProps) {
|
||||
const input = inspectedProps[name];
|
||||
const propertyValue = data[name];
|
||||
if (input instanceof HTMLSelectElement) {
|
||||
const decProp = this._properties[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update an HTMLSelectElement with an appropriate list of choices, given the propertyName
|
||||
*/
|
||||
public updateSelect(decProp: any, select: HTMLSelectElement, propertyName: string, propertyValue: any): void {
|
||||
select.innerHTML = ''; // clear out anything that was there
|
||||
let 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 (let i = 0; i < choices.length; i++) {
|
||||
const choice = choices[i];
|
||||
const opt = document.createElement('option');
|
||||
opt.text = this.convertToString(choice);
|
||||
select.add(opt);
|
||||
}
|
||||
select.value = this.convertToString(propertyValue);
|
||||
}
|
||||
|
||||
private parseValue(decProp: any, value: any, input: any, oldval: any) {
|
||||
// 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
|
||||
let type = '';
|
||||
if (decProp !== undefined && decProp.type !== undefined) {
|
||||
type = decProp.type;
|
||||
}
|
||||
if (type === '') {
|
||||
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;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update all of the data properties of all the objects in {@link #inspectedObjects} according to the
|
||||
* current values held in the HTML input elements.
|
||||
*/
|
||||
private updateAllObjectsProperties() {
|
||||
const inspectedProps = this.inspectedProperties;
|
||||
const diagram = this._diagram;
|
||||
diagram.startTransaction('set all properties');
|
||||
for (const name in inspectedProps) {
|
||||
const input = inspectedProps[name];
|
||||
let value = input.value;
|
||||
const arr1: Array<string> = value.split('|');
|
||||
let arr2: Array<string> = [];
|
||||
if (this.multipleProperties[name]) {
|
||||
// don't split if it is union and its checkbox type
|
||||
if (this._properties[name] && this._properties[name].type === 'checkbox' && this._showUnionProperties) {
|
||||
arr2.push(this.multipleProperties[name]);
|
||||
} else if (this._properties[name]) {
|
||||
arr2 = this.multipleProperties[name].toString().split('|');
|
||||
}
|
||||
}
|
||||
const it = diagram.selection.iterator;
|
||||
let change = false;
|
||||
if (this._properties[name] && this._properties[name].type === 'checkbox') change = true; // always change checkbox
|
||||
if (arr1.length < arr2.length // i.e Alpha|Beta -> Alpha procs the change
|
||||
&& (!this._properties[name] // from and to links
|
||||
|| !(this._properties[name] // do not change color checkbox and choices due to them always having less
|
||||
&& (this._properties[name].type === 'color' || this._properties[name].type === 'checkbox' || this._properties[name].type === 'choices')))) {
|
||||
change = true;
|
||||
} else { // standard detection in change in properties
|
||||
for (let j = 0; j < arr1.length && j < arr2.length; j++) {
|
||||
if (!(arr1[j] === arr2[j])
|
||||
&& !(this._properties[name] && this._properties[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 (let i = 0; i < diagram.selection.count; i++) {
|
||||
it.next();
|
||||
const isPart = it.value instanceof go.Part;
|
||||
const 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
|
||||
const decProp = this._properties[name];
|
||||
if (!this.canEditProperty(name, decProp, it.value)) continue;
|
||||
|
||||
const oldval = data[name];
|
||||
value = this.parseValue(decProp, value, input, oldval);
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @ignore
|
||||
* Update all of the data properties of {@link #inspectedObject} according to the
|
||||
* current values held in the HTML input elements.
|
||||
*/
|
||||
private updateAllProperties() {
|
||||
const inspectedProps = this.inspectedProperties;
|
||||
const diagram = this._diagram;
|
||||
const isPart = this.inspectedObject instanceof go.Part;
|
||||
const data = isPart ? (this.inspectedObject as any).data : this.inspectedObject;
|
||||
if (!data) return; // must not try to update data when there's no data!
|
||||
|
||||
diagram.startTransaction('set all properties');
|
||||
for (const name in inspectedProps) {
|
||||
const input = inspectedProps[name];
|
||||
let value = input.value;
|
||||
|
||||
// don't update "readOnly" data properties
|
||||
const decProp = this._properties[name];
|
||||
if (!this.canEditProperty(name, decProp, this.inspectedObject)) continue;
|
||||
|
||||
const oldval = data[name];
|
||||
value = this.parseValue(decProp, value, input, oldval);
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Using Dimensioning Links</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">DimensioningLink.ts</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { DimensioningLink } from './DimensioningLink.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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) {
|
||||
const numpts = link.pointsCount;
|
||||
if (numpts < 2)
|
||||
return '';
|
||||
const p0 = link.getPoint(0);
|
||||
const pn = link.getPoint(numpts - 1);
|
||||
const ang = link.direction;
|
||||
if (isNaN(ang))
|
||||
return Math.floor(Math.sqrt(p0.distanceSquaredPoint(pn))) + '';
|
||||
const 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
|
||||
}
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* A custom routed {@link 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 {@link Node}s, not simple {@link Part}s.
|
||||
* The exact point on each Node is determined by the {@link Link#fromSpot} and {@link Link#toSpot}.
|
||||
*
|
||||
* Several properties of the DimensioningLink customize the appearance of the dimensioning:
|
||||
* {@link #direction}, for orientation of the dimension line and which side it is on,
|
||||
* {@link #extension}, for how far the dimension line is from the measured points,
|
||||
* {@link #inset}, for leaving room for a text label, and
|
||||
* {@link #gap}, for distance that the extension line starts from the measured points.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Dimensioning.html">Dimensioning</a> sample.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class DimensioningLink extends go.Link {
|
||||
/**
|
||||
* Constructs a DimensioningLink and sets the following properties:
|
||||
* - {@link #isLayoutPositioned} = false
|
||||
* - {@link #isTreeLink} = false
|
||||
* - {@link #routing} = {@link Link.Orthogonal}
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._direction = 0;
|
||||
this._extension = 30;
|
||||
this._inset = 10;
|
||||
this._gap = 10;
|
||||
this.isLayoutPositioned = false;
|
||||
this.isTreeLink = false;
|
||||
this.routing = go.Link.Orthogonal;
|
||||
}
|
||||
/**
|
||||
* Copies properties to a cloned DimensioningLink.
|
||||
*/
|
||||
cloneProtected(copy) {
|
||||
super.cloneProtected(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.
|
||||
*/
|
||||
get direction() { return this._direction; }
|
||||
set direction(val) {
|
||||
if (isNaN(val) || val === 0 || val === 90 || val === 180 || val === 270) {
|
||||
this._direction = val;
|
||||
}
|
||||
else {
|
||||
throw new Error('DimensioningLink: invalid new direction: ' + val);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get extension() { return this._extension; }
|
||||
set extension(val) { this._extension = val; }
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get inset() { return this._inset; }
|
||||
set inset(val) {
|
||||
if (val >= 0) {
|
||||
this._inset = val;
|
||||
}
|
||||
else {
|
||||
throw new Error('DimensionLink: invalid new inset: ' + val);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The distance that the extension lines should come short of the measured points.
|
||||
*
|
||||
* The default value is 10.
|
||||
*/
|
||||
get gap() { return this._gap; }
|
||||
set gap(val) {
|
||||
if (val >= 0) {
|
||||
this._gap = val;
|
||||
}
|
||||
else {
|
||||
throw new Error('DimensionLink: invalid new gap: ' + val);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Constructs the link's route by modifying {@link #points}.
|
||||
* @return {boolean} true if it computed a route of points
|
||||
*/
|
||||
computePoints() {
|
||||
const fromnode = this.fromNode;
|
||||
if (!fromnode)
|
||||
return false;
|
||||
const fromport = this.fromPort;
|
||||
const fromspot = this.computeSpot(true);
|
||||
const tonode = this.toNode;
|
||||
if (!tonode)
|
||||
return false;
|
||||
const toport = this.toPort;
|
||||
const tospot = this.computeSpot(false);
|
||||
const frompoint = this.getLinkPoint(fromnode, fromport, fromspot, true, true, tonode, toport);
|
||||
if (!frompoint.isReal())
|
||||
return false;
|
||||
const topoint = this.getLinkPoint(tonode, toport, tospot, false, true, fromnode, fromport);
|
||||
if (!topoint.isReal())
|
||||
return false;
|
||||
this.clearPoints();
|
||||
let ang = this.direction;
|
||||
if (isNaN(ang)) {
|
||||
ang = frompoint.directionPoint(topoint);
|
||||
const p = new go.Point(this.extension, 0);
|
||||
p.rotate(ang + 90);
|
||||
const q = new go.Point(this.extension - this.inset, 0);
|
||||
q.rotate(ang + 90);
|
||||
const 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 {
|
||||
const dist = this.extension;
|
||||
let r = 0.0;
|
||||
let s = 0.0;
|
||||
let t0 = 0.0;
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* A custom routed {@link 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 {@link Node}s, not simple {@link Part}s.
|
||||
* The exact point on each Node is determined by the {@link Link#fromSpot} and {@link Link#toSpot}.
|
||||
*
|
||||
* Several properties of the DimensioningLink customize the appearance of the dimensioning:
|
||||
* {@link #direction}, for orientation of the dimension line and which side it is on,
|
||||
* {@link #extension}, for how far the dimension line is from the measured points,
|
||||
* {@link #inset}, for leaving room for a text label, and
|
||||
* {@link #gap}, for distance that the extension line starts from the measured points.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Dimensioning.html">Dimensioning</a> sample.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class DimensioningLink extends go.Link {
|
||||
private _direction: number = 0;
|
||||
private _extension: number = 30;
|
||||
private _inset: number = 10;
|
||||
private _gap: number = 10;
|
||||
|
||||
/**
|
||||
* Constructs a DimensioningLink and sets the following properties:
|
||||
* - {@link #isLayoutPositioned} = false
|
||||
* - {@link #isTreeLink} = false
|
||||
* - {@link #routing} = {@link Link.Orthogonal}
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.isLayoutPositioned = false;
|
||||
this.isTreeLink = false;
|
||||
this.routing = go.Link.Orthogonal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies properties to a cloned DimensioningLink.
|
||||
*/
|
||||
public cloneProtected(copy: this): void {
|
||||
super.cloneProtected(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.
|
||||
*/
|
||||
get direction(): number { return this._direction; }
|
||||
set direction(val: number) {
|
||||
if (isNaN(val) || val === 0 || val === 90 || val === 180 || val === 270) {
|
||||
this._direction = val;
|
||||
} else {
|
||||
throw new Error('DimensioningLink: invalid new direction: ' + val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get extension(): number { return this._extension; }
|
||||
set extension(val: number) { this._extension = val; }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get inset(): number { return this._inset; }
|
||||
set inset(val: number) {
|
||||
if (val >= 0) {
|
||||
this._inset = val;
|
||||
} else {
|
||||
throw new Error('DimensionLink: invalid new inset: ' + val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The distance that the extension lines should come short of the measured points.
|
||||
*
|
||||
* The default value is 10.
|
||||
*/
|
||||
get gap(): number { return this._gap; }
|
||||
set gap(val: number) {
|
||||
if (val >= 0) {
|
||||
this._gap = val;
|
||||
} else {
|
||||
throw new Error('DimensionLink: invalid new gap: ' + val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the link's route by modifying {@link #points}.
|
||||
* @return {boolean} true if it computed a route of points
|
||||
*/
|
||||
public computePoints(): boolean {
|
||||
const fromnode = this.fromNode;
|
||||
if (!fromnode) return false;
|
||||
const fromport = this.fromPort;
|
||||
const fromspot = this.computeSpot(true);
|
||||
const tonode = this.toNode;
|
||||
if (!tonode) return false;
|
||||
const toport = this.toPort;
|
||||
const tospot = this.computeSpot(false);
|
||||
const frompoint = this.getLinkPoint(fromnode, fromport, fromspot, true, true, tonode, toport);
|
||||
if (!frompoint.isReal()) return false;
|
||||
const topoint = this.getLinkPoint(tonode, toport, tospot, false, true, fromnode, fromport);
|
||||
if (!topoint.isReal()) return false;
|
||||
|
||||
this.clearPoints();
|
||||
|
||||
let ang = this.direction;
|
||||
if (isNaN(ang)) {
|
||||
ang = frompoint.directionPoint(topoint);
|
||||
const p = new go.Point(this.extension, 0);
|
||||
p.rotate(ang + 90);
|
||||
const q = new go.Point(this.extension - this.inset, 0);
|
||||
q.rotate(ang + 90);
|
||||
const 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 {
|
||||
const dist = this.extension;
|
||||
let r = 0.0;
|
||||
let s = 0.0;
|
||||
let t0 = 0.0;
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* Perform two TreeLayouts, one going rightwards and one going leftwards.
|
||||
* The choice of direction is determined by the mandatory predicate {@link #directionFunction},
|
||||
* which is called on each child Node of the root Node.
|
||||
*
|
||||
* You can also set {@link #vertical} to true if you want the DoubleTreeLayout to
|
||||
* perform TreeLayouts both downwards and upwards.
|
||||
*
|
||||
* Normally there should be a single root node. Hoewver if there are multiple root nodes
|
||||
* found in the nodes and links that this layout is responsible for, this will pretend that
|
||||
* there is a real root node and make all of the apparent root nodes children of that pretend root.
|
||||
*
|
||||
* If there is no root node, all nodes are involved in cycles, so the first given node is chosen.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../samples/doubleTree.html">Double Tree</a> sample.
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class DoubleTreeLayout extends go.Layout {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._vertical = false;
|
||||
this._directionFunction = function (node) { return true; };
|
||||
this._bottomRightOptions = null;
|
||||
this._topLeftOptions = null;
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get vertical() { return this._vertical; }
|
||||
set vertical(value) {
|
||||
if (typeof value !== "boolean")
|
||||
throw new Error("new value for DoubleTreeLayout.vertical must be a boolean value.");
|
||||
if (this._vertical !== value) {
|
||||
this._vertical = value;
|
||||
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 {@link #isPositiveDirection} should return true; otherwise it should return false.
|
||||
*/
|
||||
get directionFunction() { return this._directionFunction; }
|
||||
set directionFunction(value) {
|
||||
if (typeof value !== "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 !== value) {
|
||||
this._directionFunction = 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}.
|
||||
*/
|
||||
get bottomRightOptions() { return this._bottomRightOptions; }
|
||||
set bottomRightOptions(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}.
|
||||
*/
|
||||
get topLeftOptions() { return this._topLeftOptions; }
|
||||
set topLeftOptions(value) {
|
||||
if (this._topLeftOptions !== value) {
|
||||
this._topLeftOptions = value;
|
||||
this.invalidateLayout();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @ignore
|
||||
* Copies properties to a cloned Layout.
|
||||
*/
|
||||
cloneProtected(copy) {
|
||||
super.cloneProtected(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
|
||||
*/
|
||||
doLayout(coll) {
|
||||
const coll2 = this.collectParts(coll);
|
||||
if (coll2.count === 0)
|
||||
return;
|
||||
const diagram = this.diagram;
|
||||
if (diagram !== null)
|
||||
diagram.startTransaction("Double Tree Layout");
|
||||
// split the nodes and links into two Sets, depending on direction
|
||||
const leftParts = new go.Set();
|
||||
const rightParts = new go.Set();
|
||||
this.separatePartsForLayout(coll2, 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
|
||||
const layout1 = this.createTreeLayout(false);
|
||||
layout1.angle = this.vertical ? 270 : 180;
|
||||
layout1.arrangement = go.TreeLayout.ArrangementFixedRoots;
|
||||
const 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}
|
||||
*/
|
||||
createTreeLayout(positive) {
|
||||
const lay = new go.TreeLayout();
|
||||
let opts = this.topLeftOptions;
|
||||
if (positive)
|
||||
opts = this.bottomRightOptions;
|
||||
if (opts)
|
||||
for (const p in opts) {
|
||||
lay[p] = opts[p];
|
||||
}
|
||||
return lay;
|
||||
}
|
||||
/**
|
||||
* This is called by {@link #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.
|
||||
*/
|
||||
separatePartsForLayout(coll, leftParts, rightParts) {
|
||||
let root = null; // the one root
|
||||
const 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
|
||||
const 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);
|
||||
const forwards = (this.diagram ? this.diagram.isTreePathToChildren : true);
|
||||
// now make dummy links from the one root node to each node
|
||||
roots.each(function (child) {
|
||||
const link = new go.Link();
|
||||
if (forwards) {
|
||||
link.fromNode = root;
|
||||
link.toNode = child;
|
||||
}
|
||||
else {
|
||||
link.fromNode = child;
|
||||
link.toNode = root;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (root === null)
|
||||
return;
|
||||
// the ROOT node is shared by both subtrees
|
||||
leftParts.add(root);
|
||||
rightParts.add(root);
|
||||
const 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?
|
||||
const bottomright = lay.isPositiveDirection(child);
|
||||
const parts = bottomright ? rightParts : leftParts;
|
||||
// add the whole subtree starting with this child node
|
||||
parts.addAll(child.findTreeParts());
|
||||
// and also add the link from the ROOT node to this child node
|
||||
const plink = child.findTreeParentLink();
|
||||
if (plink !== null)
|
||||
parts.add(plink);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
isPositiveDirection(child) {
|
||||
const f = this.directionFunction;
|
||||
if (!f)
|
||||
throw new Error("No DoubleTreeLayout.directionFunction supplied on the layout");
|
||||
return f(child);
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* Perform two TreeLayouts, one going rightwards and one going leftwards.
|
||||
* The choice of direction is determined by the mandatory predicate {@link #directionFunction},
|
||||
* which is called on each child Node of the root Node.
|
||||
*
|
||||
* You can also set {@link #vertical} to true if you want the DoubleTreeLayout to
|
||||
* perform TreeLayouts both downwards and upwards.
|
||||
*
|
||||
* Normally there should be a single root node. Hoewver if there are multiple root nodes
|
||||
* found in the nodes and links that this layout is responsible for, this will pretend that
|
||||
* there is a real root node and make all of the apparent root nodes children of that pretend root.
|
||||
*
|
||||
* If there is no root node, all nodes are involved in cycles, so the first given node is chosen.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../samples/doubleTree.html">Double Tree</a> sample.
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class DoubleTreeLayout extends go.Layout {
|
||||
private _vertical: boolean = false;
|
||||
private _directionFunction: ((node: go.Node) => boolean) = function(node: go.Node): boolean { return true; };
|
||||
private _bottomRightOptions: Partial<go.TreeLayout> | null = null;
|
||||
private _topLeftOptions: Partial<go.TreeLayout> | null = null;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get vertical(): boolean { return this._vertical; }
|
||||
set vertical(value: boolean) {
|
||||
if (typeof value !== "boolean") throw new Error("new value for DoubleTreeLayout.vertical must be a boolean value.");
|
||||
if (this._vertical !== value) {
|
||||
this._vertical = value;
|
||||
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 {@link #isPositiveDirection} should return true; otherwise it should return false.
|
||||
*/
|
||||
get directionFunction(): ((node: go.Node) => boolean) { return this._directionFunction; }
|
||||
set directionFunction(value: ((node: go.Node) => boolean)) {
|
||||
if (typeof value !== "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 !== value) {
|
||||
this._directionFunction = 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}.
|
||||
*/
|
||||
get bottomRightOptions(): Partial<go.TreeLayout> | null { return this._bottomRightOptions; }
|
||||
set bottomRightOptions(value: Partial<go.TreeLayout> | null) {
|
||||
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}.
|
||||
*/
|
||||
get topLeftOptions(): Partial<go.TreeLayout> | null { return this._topLeftOptions; }
|
||||
set topLeftOptions(value: Partial<go.TreeLayout> | null) {
|
||||
if (this._topLeftOptions !== value) {
|
||||
this._topLeftOptions = value;
|
||||
this.invalidateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* Copies properties to a cloned Layout.
|
||||
*/
|
||||
protected cloneProtected(copy: this): void {
|
||||
super.cloneProtected(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
|
||||
*/
|
||||
public doLayout(coll: (go.Diagram | go.Group | go.Iterable<go.Part>)): void {
|
||||
const coll2: go.Set<go.Part> = this.collectParts(coll);
|
||||
if (coll2.count === 0) return;
|
||||
const diagram = this.diagram;
|
||||
if (diagram !== null) diagram.startTransaction("Double Tree Layout");
|
||||
|
||||
// split the nodes and links into two Sets, depending on direction
|
||||
const leftParts = new go.Set<go.Part>();
|
||||
const rightParts = new go.Set<go.Part>();
|
||||
this.separatePartsForLayout(coll2, 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
|
||||
const layout1 = this.createTreeLayout(false);
|
||||
layout1.angle = this.vertical ? 270 : 180;
|
||||
layout1.arrangement = go.TreeLayout.ArrangementFixedRoots;
|
||||
|
||||
const 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}
|
||||
*/
|
||||
protected createTreeLayout(positive: boolean): go.TreeLayout {
|
||||
const lay = new go.TreeLayout();
|
||||
let opts = this.topLeftOptions;
|
||||
if (positive) opts = this.bottomRightOptions;
|
||||
if (opts) for (const p in opts) { (<any>lay)[p] = (<any>opts)[p]; }
|
||||
return lay;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called by {@link #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.
|
||||
*/
|
||||
protected separatePartsForLayout(coll: go.Set<go.Part>, leftParts: go.Set<go.Part>, rightParts: go.Set<go.Part>): void {
|
||||
let root: go.Node | null = null; // the one root
|
||||
const roots = new go.Set<go.Node>(); // in case there are multiple roots
|
||||
coll.each(function(node: go.Part) {
|
||||
if (node instanceof go.Node && node.findTreeParentNode() === null) roots.add(node);
|
||||
});
|
||||
if (roots.count === 0) { // just choose the first node as the root
|
||||
const 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);
|
||||
const forwards = (this.diagram ? this.diagram.isTreePathToChildren : true);
|
||||
// now make dummy links from the one root node to each node
|
||||
roots.each(function(child) {
|
||||
const link = new go.Link();
|
||||
if (forwards) {
|
||||
link.fromNode = root;
|
||||
link.toNode = child;
|
||||
} else {
|
||||
link.fromNode = child;
|
||||
link.toNode = root;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (root === null) return;
|
||||
|
||||
// the ROOT node is shared by both subtrees
|
||||
leftParts.add(root);
|
||||
rightParts.add(root);
|
||||
const 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?
|
||||
const bottomright = lay.isPositiveDirection(child);
|
||||
const parts = bottomright ? rightParts : leftParts;
|
||||
// add the whole subtree starting with this child node
|
||||
parts.addAll(child.findTreeParts());
|
||||
// and also add the link from the ROOT node to this child node
|
||||
const plink = child.findTreeParentLink();
|
||||
if (plink !== null) parts.add(plink);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected isPositiveDirection(child: go.Node): boolean {
|
||||
const f = this.directionFunction;
|
||||
if (!f) throw new Error("No DoubleTreeLayout.directionFunction supplied on the layout");
|
||||
return f(child);
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Drag Creating Tool</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: Create nodes by dragging, thereby specifying their initial size." />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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"/>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.ts">DragCreatingTool.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { DragCreatingTool } from './DragCreatingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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 ForegFround layer
|
||||
new go.Binding('layerName', 'isSelected', (s) => 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' })));
|
||||
|
||||
class CustomDragCreatingTool extends DragCreatingTool {
|
||||
insertPart(bounds) {
|
||||
if (this.archetypeNodeData === null)
|
||||
return null;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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, $(CustomDragCreatingTool, {
|
||||
isEnabled: true,
|
||||
delay: 0,
|
||||
box: $(go.Part, { layerName: 'Tool' }, $(go.Shape, { name: 'SHAPE', fill: null, stroke: 'cyan', strokeWidth: 2 })),
|
||||
archetypeNodeData: { color: 'white' } // initial properties shared by all nodes
|
||||
}));
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
|
||||
function toolEnabled() {
|
||||
const enable = document.getElementById('ToolEnabled').checked;
|
||||
const tool = myDiagram.toolManager.findTool('DragCreating');
|
||||
if (tool !== null)
|
||||
tool.isEnabled = enable;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The DragCreatingTool lets the user create a new node by dragging in the background
|
||||
* to indicate its size and position.
|
||||
*
|
||||
* The default drag selection box is a magenta rectangle.
|
||||
* You can modify the {@link #box} to customize its appearance.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You can use this tool in a modal manner by executing:
|
||||
* ```js
|
||||
* diagram.currentTool = new DragCreatingTool();
|
||||
* ```
|
||||
*
|
||||
* Use this tool in a mode-less manner by executing:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseMoveTools.insertAt(2, new DragCreatingTool());
|
||||
* ```
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DragCreating.html">Drag Creating</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class DragCreatingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a DragCreatingTool, sets {@link #box} to a magenta rectangle, and sets name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._archetypeNodeData = null;
|
||||
this._delay = 175;
|
||||
const b = new go.Part();
|
||||
const r = new go.Shape();
|
||||
b.layerName = 'Tool';
|
||||
b.selectable = false;
|
||||
r.name = 'SHAPE';
|
||||
r.figure = 'Rectangle';
|
||||
r.fill = null;
|
||||
r.stroke = 'magenta';
|
||||
r.position = new go.Point(0, 0);
|
||||
b.add(r);
|
||||
this._box = b;
|
||||
this.name = 'DragCreating';
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Modifying this property while this tool {@link Tool#isActive} might have no effect.
|
||||
*/
|
||||
get box() { return this._box; }
|
||||
set box(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.
|
||||
*/
|
||||
get delay() { return this._delay; }
|
||||
set delay(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.
|
||||
*/
|
||||
get archetypeNodeData() { return this._archetypeNodeData; }
|
||||
set archetypeNodeData(val) { this._archetypeNodeData = val; }
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
canStart() {
|
||||
if (!this.isEnabled)
|
||||
return false;
|
||||
// gotta have some node data that can be copied
|
||||
if (this.archetypeNodeData === null)
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
// heed IsReadOnly & AllowInsert
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly)
|
||||
return false;
|
||||
if (!diagram.allowInsert)
|
||||
return false;
|
||||
const 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}.
|
||||
*/
|
||||
doActivate() {
|
||||
const diagram = this.diagram;
|
||||
this.isActive = true;
|
||||
diagram.isMouseCaptured = true;
|
||||
diagram.add(this.box);
|
||||
this.doMouseMove();
|
||||
}
|
||||
/**
|
||||
* Release the mouse and remove any {@link #box}.
|
||||
*/
|
||||
doDeactivate() {
|
||||
const diagram = this.diagram;
|
||||
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}.
|
||||
*/
|
||||
doMouseMove() {
|
||||
if (this.isActive && this.box !== null) {
|
||||
const r = this.computeBoxBounds();
|
||||
let shape = this.box.findObject('SHAPE');
|
||||
if (shape === null)
|
||||
shape = this.box.findMainElement();
|
||||
if (shape !== null)
|
||||
shape.desiredSize = r.size;
|
||||
this.box.position = r.position;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Call {@link #insertPart} with the value of a call to {@link #computeBoxBounds}.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (this.isActive) {
|
||||
const 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.
|
||||
* @return {Rect} a {@link Rect} in document coordinates.
|
||||
*/
|
||||
computeBoxBounds() {
|
||||
const diagram = this.diagram;
|
||||
const start = diagram.firstInput.documentPoint;
|
||||
const 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.
|
||||
*
|
||||
* 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.
|
||||
* @param {Rect} bounds a Point in document coordinates.
|
||||
* @return {Part} the newly created Part, or null if it failed.
|
||||
*/
|
||||
insertPart(bounds) {
|
||||
const diagram = this.diagram;
|
||||
const arch = this.archetypeNodeData;
|
||||
if (arch === null)
|
||||
return null;
|
||||
diagram.raiseDiagramEvent('ChangingSelection', diagram.selection);
|
||||
this.startTransaction(this.name);
|
||||
let part = null;
|
||||
if (arch !== null) {
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The DragCreatingTool lets the user create a new node by dragging in the background
|
||||
* to indicate its size and position.
|
||||
*
|
||||
* The default drag selection box is a magenta rectangle.
|
||||
* You can modify the {@link #box} to customize its appearance.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You can use this tool in a modal manner by executing:
|
||||
* ```js
|
||||
* diagram.currentTool = new DragCreatingTool();
|
||||
* ```
|
||||
*
|
||||
* Use this tool in a mode-less manner by executing:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseMoveTools.insertAt(2, new DragCreatingTool());
|
||||
* ```
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DragCreating.html">Drag Creating</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class DragCreatingTool extends go.Tool {
|
||||
private _box: go.Part;
|
||||
private _archetypeNodeData: go.ObjectData | null = null;
|
||||
private _delay: number = 175;
|
||||
|
||||
/**
|
||||
* Constructs a DragCreatingTool, sets {@link #box} to a magenta rectangle, and sets name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
const b: go.Part = new go.Part();
|
||||
const r: go.Shape = new go.Shape();
|
||||
b.layerName = 'Tool';
|
||||
b.selectable = false;
|
||||
r.name = 'SHAPE';
|
||||
r.figure = 'Rectangle';
|
||||
r.fill = null;
|
||||
r.stroke = 'magenta';
|
||||
r.position = new go.Point(0, 0);
|
||||
b.add(r);
|
||||
this._box = b;
|
||||
this.name = 'DragCreating';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Modifying this property while this tool {@link Tool#isActive} might have no effect.
|
||||
*/
|
||||
get box(): go.Part { return this._box; }
|
||||
set box(val: go.Part) { 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.
|
||||
*/
|
||||
get delay(): number { return this._delay; }
|
||||
set delay(val: number) { 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.
|
||||
*/
|
||||
get archetypeNodeData(): go.ObjectData | null { return this._archetypeNodeData; }
|
||||
set archetypeNodeData(val: go.ObjectData | null) { this._archetypeNodeData = val; }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!this.isEnabled) return false;
|
||||
|
||||
// gotta have some node data that can be copied
|
||||
if (this.archetypeNodeData === null) return false;
|
||||
|
||||
const diagram = this.diagram;
|
||||
// heed IsReadOnly & AllowInsert
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
|
||||
if (!diagram.allowInsert) return false;
|
||||
|
||||
const 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}.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
const diagram = this.diagram;
|
||||
this.isActive = true;
|
||||
diagram.isMouseCaptured = true;
|
||||
diagram.add(this.box);
|
||||
this.doMouseMove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the mouse and remove any {@link #box}.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
const diagram = this.diagram;
|
||||
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}.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
if (this.isActive && this.box !== null) {
|
||||
const r = this.computeBoxBounds();
|
||||
let shape = this.box.findObject('SHAPE');
|
||||
if (shape === null) shape = this.box.findMainElement();
|
||||
if (shape !== null) shape.desiredSize = r.size;
|
||||
this.box.position = r.position;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@link #insertPart} with the value of a call to {@link #computeBoxBounds}.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (this.isActive) {
|
||||
const 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.
|
||||
* @return {Rect} a {@link Rect} in document coordinates.
|
||||
*/
|
||||
public computeBoxBounds(): go.Rect {
|
||||
const diagram = this.diagram;
|
||||
const start = diagram.firstInput.documentPoint;
|
||||
const 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.
|
||||
*
|
||||
* 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.
|
||||
* @param {Rect} bounds a Point in document coordinates.
|
||||
* @return {Part} the newly created Part, or null if it failed.
|
||||
*/
|
||||
public insertPart(bounds: go.Rect): go.Part | null {
|
||||
const diagram = this.diagram;
|
||||
const arch = this.archetypeNodeData;
|
||||
if (arch === null) return null;
|
||||
|
||||
diagram.raiseDiagramEvent('ChangingSelection', diagram.selection);
|
||||
this.startTransaction(this.name);
|
||||
let part = null;
|
||||
if (arch !== null) {
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Drag Zooming Tool</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">DragZoomingTool.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { DragZoomingTool } from './DragZoomingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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, {
|
||||
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
|
||||
const 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
|
||||
const total = 99;
|
||||
const treedata = [];
|
||||
for (let i = 0; i < total; i++) {
|
||||
// these property names are also specified when creating the TreeModel
|
||||
const d = {
|
||||
k: i,
|
||||
c: go.Brush.randomColor(),
|
||||
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);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The default drag selection box is a magenta rectangle.
|
||||
* You can modify the {@link #box} to customize its appearance.
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* You can use this tool in a modal manner by executing:
|
||||
* ```js
|
||||
* diagram.currentTool = new DragZoomingTool();
|
||||
* ```
|
||||
*
|
||||
* Use this tool in a mode-less manner by executing:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseMoveTools.insertAt(2, new DragZoomingTool());
|
||||
* ```
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DragZooming.html">Drag Zooming</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class DragZoomingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a DragZoomingTool, sets {@link #box} to a magenta rectangle, and sets name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._delay = 175;
|
||||
this._zoomedDiagram = null;
|
||||
const b = new go.Part();
|
||||
const r = new go.Shape();
|
||||
b.layerName = 'Tool';
|
||||
b.selectable = false;
|
||||
r.name = 'SHAPE';
|
||||
r.figure = 'Rectangle';
|
||||
r.fill = null;
|
||||
r.stroke = 'magenta';
|
||||
r.position = new go.Point(0, 0);
|
||||
b.add(r);
|
||||
this._box = b;
|
||||
this.name = 'DragZooming';
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Modifying this property while this tool {@link Tool#isActive} might have no effect.
|
||||
*/
|
||||
get box() { return this._box; }
|
||||
set box(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.
|
||||
*/
|
||||
get delay() { return this._delay; }
|
||||
set delay(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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
get zoomedDiagram() { return this._zoomedDiagram; }
|
||||
set zoomedDiagram(val) { this._zoomedDiagram = val; }
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
canStart() {
|
||||
if (!this.isEnabled)
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
const 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}.
|
||||
*/
|
||||
doActivate() {
|
||||
const diagram = this.diagram;
|
||||
this.isActive = true;
|
||||
diagram.isMouseCaptured = true;
|
||||
diagram.skipsUndoManager = true;
|
||||
diagram.add(this.box);
|
||||
this.doMouseMove();
|
||||
}
|
||||
/**
|
||||
* Release the mouse and remove any {@link #box}.
|
||||
*/
|
||||
doDeactivate() {
|
||||
const diagram = this.diagram;
|
||||
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}.
|
||||
*/
|
||||
doMouseMove() {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive && this.box !== null) {
|
||||
const r = this.computeBoxBounds();
|
||||
let shape = this.box.findObject('SHAPE');
|
||||
if (shape === null)
|
||||
shape = this.box.findMainElement();
|
||||
if (shape !== null)
|
||||
shape.desiredSize = r.size;
|
||||
this.box.position = r.position;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Call {@link #zoomToRect} with the value of a call to {@link #computeBoxBounds}.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (this.isActive) {
|
||||
const 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}.
|
||||
* @return {Rect} a {@link Rect} in document coordinates.
|
||||
*/
|
||||
computeBoxBounds() {
|
||||
const diagram = this.diagram;
|
||||
const start = diagram.firstInput.documentPoint;
|
||||
const latest = diagram.lastInput.documentPoint;
|
||||
const adx = latest.x - start.x;
|
||||
const ady = latest.y - start.y;
|
||||
let observed = this.zoomedDiagram;
|
||||
if (observed === null)
|
||||
observed = diagram;
|
||||
if (observed === null) {
|
||||
return new go.Rect(start, latest);
|
||||
}
|
||||
const vrect = observed.viewportBounds;
|
||||
if (vrect.height === 0 || ady === 0) {
|
||||
return new go.Rect(start, latest);
|
||||
}
|
||||
const vratio = vrect.width / vrect.height;
|
||||
let lx;
|
||||
let 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.
|
||||
* @param {Rect} r a rectangular bounds in document coordinates.
|
||||
*/
|
||||
zoomToRect(r) {
|
||||
if (r.width < 0.1)
|
||||
return;
|
||||
const diagram = this.diagram;
|
||||
let observed = this.zoomedDiagram;
|
||||
if (observed === null)
|
||||
observed = diagram;
|
||||
if (observed === null)
|
||||
return;
|
||||
// zoom out when using the Shift modifier
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The default drag selection box is a magenta rectangle.
|
||||
* You can modify the {@link #box} to customize its appearance.
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* You can use this tool in a modal manner by executing:
|
||||
* ```js
|
||||
* diagram.currentTool = new DragZoomingTool();
|
||||
* ```
|
||||
*
|
||||
* Use this tool in a mode-less manner by executing:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseMoveTools.insertAt(2, new DragZoomingTool());
|
||||
* ```
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DragZooming.html">Drag Zooming</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class DragZoomingTool extends go.Tool {
|
||||
private _box: go.Part;
|
||||
private _delay: number = 175;
|
||||
private _zoomedDiagram: go.Diagram | null = null;
|
||||
|
||||
/**
|
||||
* Constructs a DragZoomingTool, sets {@link #box} to a magenta rectangle, and sets name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
const b: go.Part = new go.Part();
|
||||
const r: go.Shape = new go.Shape();
|
||||
b.layerName = 'Tool';
|
||||
b.selectable = false;
|
||||
r.name = 'SHAPE';
|
||||
r.figure = 'Rectangle';
|
||||
r.fill = null;
|
||||
r.stroke = 'magenta';
|
||||
r.position = new go.Point(0, 0);
|
||||
b.add(r);
|
||||
this._box = b;
|
||||
this.name = 'DragZooming';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Modifying this property while this tool {@link Tool#isActive} might have no effect.
|
||||
*/
|
||||
get box(): go.Part { return this._box; }
|
||||
set box(val: go.Part) { 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.
|
||||
*/
|
||||
get delay(): number { return this._delay; }
|
||||
set delay(val: number) { 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
get zoomedDiagram(): go.Diagram | null { return this._zoomedDiagram; }
|
||||
set zoomedDiagram(val: go.Diagram | null) { this._zoomedDiagram = val; }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!this.isEnabled) return false;
|
||||
const diagram = this.diagram;
|
||||
const 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}.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
const diagram = this.diagram;
|
||||
this.isActive = true;
|
||||
diagram.isMouseCaptured = true;
|
||||
diagram.skipsUndoManager = true;
|
||||
diagram.add(this.box);
|
||||
this.doMouseMove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the mouse and remove any {@link #box}.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
const diagram = this.diagram;
|
||||
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}.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive && this.box !== null) {
|
||||
const r = this.computeBoxBounds();
|
||||
let shape = this.box.findObject('SHAPE');
|
||||
if (shape === null) shape = this.box.findMainElement();
|
||||
if (shape !== null) shape.desiredSize = r.size;
|
||||
this.box.position = r.position;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@link #zoomToRect} with the value of a call to {@link #computeBoxBounds}.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (this.isActive) {
|
||||
const 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}.
|
||||
* @return {Rect} a {@link Rect} in document coordinates.
|
||||
*/
|
||||
public computeBoxBounds(): go.Rect {
|
||||
const diagram = this.diagram;
|
||||
const start = diagram.firstInput.documentPoint;
|
||||
const latest = diagram.lastInput.documentPoint;
|
||||
const adx = latest.x - start.x;
|
||||
const ady = latest.y - start.y;
|
||||
|
||||
let observed = this.zoomedDiagram;
|
||||
if (observed === null) observed = diagram;
|
||||
if (observed === null) {
|
||||
return new go.Rect(start, latest);
|
||||
}
|
||||
const vrect = observed.viewportBounds;
|
||||
if (vrect.height === 0 || ady === 0) {
|
||||
return new go.Rect(start, latest);
|
||||
}
|
||||
|
||||
const vratio = vrect.width / vrect.height;
|
||||
let lx;
|
||||
let 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.
|
||||
* @param {Rect} r a rectangular bounds in document coordinates.
|
||||
*/
|
||||
public zoomToRect(r: go.Rect): void {
|
||||
if (r.width < 0.1) return;
|
||||
const diagram = this.diagram;
|
||||
let observed = this.zoomedDiagram;
|
||||
if (observed === null) observed = diagram;
|
||||
if (observed === null) return;
|
||||
|
||||
// zoom out when using the Shift modifier
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Drawing Commands</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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 id="leftsides">Left Sides</button>
|
||||
<button id="rightsides">Right Sides</button>
|
||||
<button id="tops">Tops</button>
|
||||
<button id="bottoms">Bottoms</button>
|
||||
<button id="cenX">Center X</button>
|
||||
<button id="cenY">Center Y</button>
|
||||
<button id="row">Row</button>
|
||||
<button id="column">Column</button>
|
||||
</br>
|
||||
Rotate:
|
||||
<button id="45">45°</button>
|
||||
<button id="-45">-45°</button>
|
||||
<button id="90">90°</button>
|
||||
<button id="-90">-90°</button>
|
||||
<button id="180">180°</button>
|
||||
</br>
|
||||
Z-Order:
|
||||
<button id="front">Pull to Front</button>
|
||||
<button id="back">Push to Back</button>
|
||||
</br>
|
||||
Arrow Mode:
|
||||
<input type="radio" name="arrow" id="move" checked="checked">Move</input>
|
||||
<input type="radio" name="arrow" id="select">Select</input>
|
||||
<input type="radio" name="arrow" id="scroll">Scroll</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.ts">DrawCommandHandler.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { DrawCommandHandler } from './DrawCommandHandler.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const myDiagram = $(go.Diagram, 'myDiagramDiv', // create a Diagram for the DIV HTML element
|
||||
{
|
||||
commandHandler: new DrawCommandHandler(),
|
||||
"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: 'Beta', to: 'Beta' },
|
||||
{ from: 'Gamma', to: 'Delta' },
|
||||
{ from: 'Delta', to: 'Alpha' }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
|
||||
function askSpace() {
|
||||
const space = parseInt(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
|
||||
const move = document.getElementById('move');
|
||||
const select = document.getElementById('select');
|
||||
const scroll = document.getElementById('scroll');
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("leftsides").onclick = function () { myDiagram.commandHandler.alignLeft(); }
|
||||
document.getElementById("rightsides").onclick = function () { myDiagram.commandHandler.alignRight(); }
|
||||
document.getElementById("tops").onclick = function () { myDiagram.commandHandler.alignTop(); }
|
||||
document.getElementById("bottoms").onclick = function () { myDiagram.commandHandler.alignBottom(); }
|
||||
document.getElementById("cenX").onclick = function () { myDiagram.commandHandler.alignCenterX(); }
|
||||
document.getElementById("cenY").onclick = function () { myDiagram.commandHandler.alignCenterY(); }
|
||||
document.getElementById("row").onclick = function () { myDiagram.commandHandler.alignRow(askSpace()); }
|
||||
document.getElementById("column").onclick = function () { myDiagram.commandHandler.alignColumn(askSpace()); }
|
||||
document.getElementById("45").onclick = function () { myDiagram.commandHandler.rotate(45); }
|
||||
document.getElementById("-45").onclick = function () { myDiagram.commandHandler.rotate(-45); }
|
||||
document.getElementById("90").onclick = function () { myDiagram.commandHandler.rotate(90); }
|
||||
document.getElementById("-90").onclick = function () { myDiagram.commandHandler.rotate(-90); }
|
||||
document.getElementById("180").onclick = function () { myDiagram.commandHandler.rotate(180); }
|
||||
document.getElementById("front").onclick = function () { myDiagram.commandHandler.pullToFront(); }
|
||||
document.getElementById("back").onclick = function () { myDiagram.commandHandler.pushToBack(); }
|
||||
document.getElementById("move").onclick = arrowMode;
|
||||
document.getElementById("select").onclick = arrowMode;
|
||||
document.getElementById("scroll").onclick = arrowMode;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+534
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Typical usage:
|
||||
* ```js
|
||||
* $(go.Diagram, "myDiagramDiv",
|
||||
* {
|
||||
* commandHandler: $(DrawCommandHandler),
|
||||
* . . .
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
* or:
|
||||
* ```js
|
||||
* myDiagram.commandHandler = new DrawCommandHandler();
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DrawCommandHandler.html">Drawing Commands</a> sample.
|
||||
* @category Extension
|
||||
*/
|
||||
export class DrawCommandHandler extends go.CommandHandler {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._arrowKeyBehavior = 'move';
|
||||
this._pasteOffset = new go.Point(10, 10);
|
||||
this._lastPasteOffset = new go.Point(0, 0);
|
||||
}
|
||||
/**
|
||||
* Gets or sets the arrow key behavior. Possible values are "move", "select", and "scroll".
|
||||
*
|
||||
* The default value is "move".
|
||||
*/
|
||||
get arrowKeyBehavior() { return this._arrowKeyBehavior; }
|
||||
set arrowKeyBehavior(val) {
|
||||
if (val !== 'move' && val !== 'select' && val !== 'scroll' && val !== 'none') {
|
||||
throw new Error('DrawCommandHandler.arrowKeyBehavior must be either "move", "select", "scroll", or "none", not: ' + val);
|
||||
}
|
||||
this._arrowKeyBehavior = val;
|
||||
}
|
||||
/**
|
||||
* Gets or sets the offset at which each repeated {@link #pasteSelection} puts the new copied parts from the clipboard.
|
||||
*/
|
||||
get pasteOffset() { return this._pasteOffset; }
|
||||
set pasteOffset(val) {
|
||||
if (!(val instanceof go.Point))
|
||||
throw new Error('DrawCommandHandler.pasteOffset must be a Point, not: ' + val);
|
||||
this._pasteOffset.set(val);
|
||||
}
|
||||
/**
|
||||
* This controls whether or not the user can invoke the {@link #alignLeft}, {@link #alignRight},
|
||||
* {@link #alignTop}, {@link #alignBottom}, {@link #alignCenterX}, {@link #alignCenterY} commands.
|
||||
* @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.
|
||||
*/
|
||||
canAlignSelection() {
|
||||
const diagram = this.diagram;
|
||||
if (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.
|
||||
*/
|
||||
alignLeft() {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('aligning left');
|
||||
let minPosition = Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link)
|
||||
return; // skips over go.Link
|
||||
minPosition = Math.min(current.position.x, minPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
alignRight() {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('aligning right');
|
||||
let maxPosition = -Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link)
|
||||
return; // skips over go.Link
|
||||
const rightSideLoc = current.actualBounds.x + current.actualBounds.width;
|
||||
maxPosition = Math.max(rightSideLoc, maxPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
alignTop() {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('alignTop');
|
||||
let minPosition = Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link)
|
||||
return; // skips over go.Link
|
||||
minPosition = Math.min(current.position.y, minPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
alignBottom() {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('aligning bottom');
|
||||
let maxPosition = -Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link)
|
||||
return; // skips over go.Link
|
||||
const bottomSideLoc = current.actualBounds.y + current.actualBounds.height;
|
||||
maxPosition = Math.max(bottomSideLoc, maxPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
alignCenterX() {
|
||||
const diagram = this.diagram;
|
||||
const firstSelection = diagram.selection.first();
|
||||
if (!firstSelection)
|
||||
return;
|
||||
diagram.startTransaction('aligning Center X');
|
||||
const centerX = firstSelection.actualBounds.x + firstSelection.actualBounds.width / 2;
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
alignCenterY() {
|
||||
const diagram = this.diagram;
|
||||
const firstSelection = diagram.selection.first();
|
||||
if (!firstSelection)
|
||||
return;
|
||||
diagram.startTransaction('aligning Center Y');
|
||||
const centerY = firstSelection.actualBounds.y + firstSelection.actualBounds.height / 2;
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
alignColumn(distance) {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('align Column');
|
||||
if (distance === undefined)
|
||||
distance = 0; // for aligning edge to edge
|
||||
distance = parseFloat(distance.toString());
|
||||
const selectedParts = new Array();
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link)
|
||||
return; // skips over go.Link
|
||||
selectedParts.push(current);
|
||||
});
|
||||
for (let i = 0; i < selectedParts.length - 1; i++) {
|
||||
const current = selectedParts[i];
|
||||
// adds distance specified between parts
|
||||
const curBottomSideLoc = current.actualBounds.y + current.actualBounds.height + distance;
|
||||
const 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.
|
||||
*/
|
||||
alignRow(distance) {
|
||||
if (distance === undefined)
|
||||
distance = 0; // for aligning edge to edge
|
||||
distance = parseFloat(distance.toString());
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('align Row');
|
||||
const selectedParts = new Array();
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link)
|
||||
return; // skips over go.Link
|
||||
selectedParts.push(current);
|
||||
});
|
||||
for (let i = 0; i < selectedParts.length - 1; i++) {
|
||||
const current = selectedParts[i];
|
||||
// adds distance specified between parts
|
||||
const curRightSideLoc = current.actualBounds.x + current.actualBounds.width + distance;
|
||||
const 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.
|
||||
* @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}.
|
||||
*/
|
||||
canRotate() {
|
||||
const diagram = this.diagram;
|
||||
if (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.
|
||||
* @param {number} angle the positive (clockwise) or negative (counter-clockwise) change in the rotation angle of each Part, in degrees.
|
||||
*/
|
||||
rotate(angle) {
|
||||
if (angle === undefined)
|
||||
angle = 90;
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('rotate ' + angle.toString());
|
||||
diagram.selection.each((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}
|
||||
*/
|
||||
pullToFront() {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction("pullToFront");
|
||||
// find the affected Layers
|
||||
const layers = new go.Map();
|
||||
diagram.selection.each(function (part) {
|
||||
if (part.layer !== null)
|
||||
layers.set(part.layer, 0);
|
||||
});
|
||||
// find the maximum zOrder in each Layer
|
||||
layers.iteratorKeys.each(function (layer) {
|
||||
let max = 0;
|
||||
layer.parts.each(function (part) {
|
||||
if (part.isSelected)
|
||||
return;
|
||||
const 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) {
|
||||
const z = layers.get(part.layer) || 0;
|
||||
DrawCommandHandler._assignZOrder(part, z + 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}
|
||||
*/
|
||||
pushToBack() {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction("pushToBack");
|
||||
// find the affected Layers
|
||||
const layers = new go.Map();
|
||||
diagram.selection.each(function (part) {
|
||||
if (part.layer !== null)
|
||||
layers.set(part.layer, 0);
|
||||
});
|
||||
// find the minimum zOrder in each Layer
|
||||
layers.iteratorKeys.each(function (layer) {
|
||||
let min = 0;
|
||||
layer.parts.each(function (part) {
|
||||
if (part.isSelected)
|
||||
return;
|
||||
const 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) {
|
||||
const z = layers.get(part.layer) || 0;
|
||||
DrawCommandHandler._assignZOrder(part,
|
||||
// make sure a group's nested nodes are also behind everything else
|
||||
z - 1 - DrawCommandHandler._findGroupDepth(part));
|
||||
});
|
||||
diagram.commitTransaction("pushToBack");
|
||||
}
|
||||
static _assignZOrder(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
static _findGroupDepth(part) {
|
||||
if (part instanceof go.Group) {
|
||||
let 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.
|
||||
*/
|
||||
doKeyDown() {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
// determines the function of the arrow keys
|
||||
if (e.key === 'Up' || e.key === 'Down' || e.key === 'Left' || e.key === 'Right') {
|
||||
const behavior = this.arrowKeyBehavior;
|
||||
if (behavior === 'none') {
|
||||
// no-op
|
||||
return;
|
||||
}
|
||||
else if (behavior === 'select') {
|
||||
this._arrowKeySelect();
|
||||
return;
|
||||
}
|
||||
else if (behavior === 'move') {
|
||||
this._arrowKeyMove();
|
||||
return;
|
||||
}
|
||||
// otherwise drop through to get the default scrolling behavior
|
||||
}
|
||||
// otherwise still does all standard commands
|
||||
super.doKeyDown();
|
||||
}
|
||||
/**
|
||||
* Collects in an Array all of the non-Link Parts currently in the Diagram.
|
||||
*/
|
||||
_getAllParts() {
|
||||
const allParts = new Array();
|
||||
this.diagram.nodes.each((node) => { allParts.push(node); });
|
||||
this.diagram.parts.each((part) => { allParts.push(part); });
|
||||
// note that this ignores Links
|
||||
return allParts;
|
||||
}
|
||||
/**
|
||||
* To be called when arrow keys should move the Diagram.selection.
|
||||
*/
|
||||
_arrowKeyMove() {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
// moves all selected parts in the specified direction
|
||||
let vdistance = 0;
|
||||
let 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) {
|
||||
const cellsize = diagram.grid.gridCellSize;
|
||||
hdistance = cellsize.width;
|
||||
vdistance = cellsize.height;
|
||||
}
|
||||
diagram.startTransaction('arrowKeyMove');
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
_arrowKeySelect() {
|
||||
const diagram = this.diagram;
|
||||
const 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
|
||||
let 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.
|
||||
* @param {number} dir the direction, in degrees
|
||||
* @return {Part} the closest Part found in the given direction
|
||||
*/
|
||||
_findNearestPartTowards(dir) {
|
||||
const originalPart = this.diagram.selection.first();
|
||||
if (originalPart === null)
|
||||
return null;
|
||||
const originalPoint = originalPart.actualBounds.center;
|
||||
const allParts = this._getAllParts();
|
||||
let closestDistance = Infinity;
|
||||
let closest = originalPart; // if no parts meet the criteria, the same part remains selected
|
||||
for (let i = 0; i < allParts.length; i++) {
|
||||
const nextPart = allParts[i];
|
||||
if (nextPart === originalPart)
|
||||
continue; // skips over currently selected part
|
||||
const nextPoint = nextPart.actualBounds.center;
|
||||
const angle = originalPoint.directionPoint(nextPoint);
|
||||
const anglediff = this._angleCloseness(angle, dir);
|
||||
if (anglediff <= 45) { // if this part's center is within the desired direction's sector,
|
||||
let 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;
|
||||
}
|
||||
_angleCloseness(a, dir) {
|
||||
return Math.min(Math.abs(dir - a), Math.min(Math.abs(dir + 360 - a), Math.abs(dir - 360 - a)));
|
||||
}
|
||||
/**
|
||||
* Reset the last offset for pasting.
|
||||
* @param {Iterable.<Part>} coll a collection of {@link Part}s.
|
||||
*/
|
||||
copyToClipboard(coll) {
|
||||
super.copyToClipboard(coll);
|
||||
this._lastPasteOffset.set(this.pasteOffset);
|
||||
}
|
||||
/**
|
||||
* Paste from the clipboard with an offset incremented on each paste, and reset when copied.
|
||||
* @return {Set.<Part>} a collection of newly pasted {@link Part}s
|
||||
*/
|
||||
pasteFromClipboard() {
|
||||
const coll = super.pasteFromClipboard();
|
||||
this.diagram.moveParts(coll, this._lastPasteOffset, false);
|
||||
this._lastPasteOffset.add(this.pasteOffset);
|
||||
return coll;
|
||||
}
|
||||
}
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Typical usage:
|
||||
* ```js
|
||||
* $(go.Diagram, "myDiagramDiv",
|
||||
* {
|
||||
* commandHandler: $(DrawCommandHandler),
|
||||
* . . .
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
* or:
|
||||
* ```js
|
||||
* myDiagram.commandHandler = new DrawCommandHandler();
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/DrawCommandHandler.html">Drawing Commands</a> sample.
|
||||
* @category Extension
|
||||
*/
|
||||
export class DrawCommandHandler extends go.CommandHandler {
|
||||
private _arrowKeyBehavior: string = 'move';
|
||||
private _pasteOffset: go.Point = new go.Point(10, 10);
|
||||
private _lastPasteOffset: go.Point = new go.Point(0, 0);
|
||||
|
||||
/**
|
||||
* Gets or sets the arrow key behavior. Possible values are "move", "select", and "scroll".
|
||||
*
|
||||
* The default value is "move".
|
||||
*/
|
||||
get arrowKeyBehavior(): string { return this._arrowKeyBehavior; }
|
||||
set arrowKeyBehavior(val: string) {
|
||||
if (val !== 'move' && val !== 'select' && val !== 'scroll' && val !== 'none') {
|
||||
throw new Error('DrawCommandHandler.arrowKeyBehavior must be either "move", "select", "scroll", or "none", not: ' + val);
|
||||
}
|
||||
this._arrowKeyBehavior = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the offset at which each repeated {@link #pasteSelection} puts the new copied parts from the clipboard.
|
||||
*/
|
||||
get pasteOffset(): go.Point { return this._pasteOffset; }
|
||||
set pasteOffset(val: go.Point) {
|
||||
if (!(val instanceof go.Point)) throw new Error('DrawCommandHandler.pasteOffset must be a Point, not: ' + val);
|
||||
this._pasteOffset.set(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* This controls whether or not the user can invoke the {@link #alignLeft}, {@link #alignRight},
|
||||
* {@link #alignTop}, {@link #alignBottom}, {@link #alignCenterX}, {@link #alignCenterY} commands.
|
||||
* @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.
|
||||
*/
|
||||
public canAlignSelection(): boolean {
|
||||
const diagram = this.diagram;
|
||||
if (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.
|
||||
*/
|
||||
public alignLeft(): void {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('aligning left');
|
||||
let minPosition = Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link) return; // skips over go.Link
|
||||
minPosition = Math.min(current.position.x, minPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
public alignRight(): void {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('aligning right');
|
||||
let maxPosition = -Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link) return; // skips over go.Link
|
||||
const rightSideLoc = current.actualBounds.x + current.actualBounds.width;
|
||||
maxPosition = Math.max(rightSideLoc, maxPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
public alignTop(): void {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('alignTop');
|
||||
let minPosition = Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link) return; // skips over go.Link
|
||||
minPosition = Math.min(current.position.y, minPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
public alignBottom(): void {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('aligning bottom');
|
||||
let maxPosition = -Infinity;
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link) return; // skips over go.Link
|
||||
const bottomSideLoc = current.actualBounds.y + current.actualBounds.height;
|
||||
maxPosition = Math.max(bottomSideLoc, maxPosition);
|
||||
});
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
public alignCenterX(): void {
|
||||
const diagram = this.diagram;
|
||||
const firstSelection = diagram.selection.first();
|
||||
if (!firstSelection) return;
|
||||
diagram.startTransaction('aligning Center X');
|
||||
const centerX = firstSelection.actualBounds.x + firstSelection.actualBounds.width / 2;
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
public alignCenterY(): void {
|
||||
const diagram = this.diagram;
|
||||
const firstSelection = diagram.selection.first();
|
||||
if (!firstSelection) return;
|
||||
diagram.startTransaction('aligning Center Y');
|
||||
const centerY = firstSelection.actualBounds.y + firstSelection.actualBounds.height / 2;
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
public alignColumn(distance: number): void {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('align Column');
|
||||
if (distance === undefined) distance = 0; // for aligning edge to edge
|
||||
distance = parseFloat(distance.toString());
|
||||
const selectedParts = new Array();
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link) return; // skips over go.Link
|
||||
selectedParts.push(current);
|
||||
});
|
||||
for (let i = 0; i < selectedParts.length - 1; i++) {
|
||||
const current = selectedParts[i];
|
||||
// adds distance specified between parts
|
||||
const curBottomSideLoc = current.actualBounds.y + current.actualBounds.height + distance;
|
||||
const 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.
|
||||
*/
|
||||
public alignRow(distance: number): void {
|
||||
if (distance === undefined) distance = 0; // for aligning edge to edge
|
||||
distance = parseFloat(distance.toString());
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('align Row');
|
||||
const selectedParts = new Array();
|
||||
diagram.selection.each((current) => {
|
||||
if (current instanceof go.Link) return; // skips over go.Link
|
||||
selectedParts.push(current);
|
||||
});
|
||||
for (let i = 0; i < selectedParts.length - 1; i++) {
|
||||
const current = selectedParts[i];
|
||||
// adds distance specified between parts
|
||||
const curRightSideLoc = current.actualBounds.x + current.actualBounds.width + distance;
|
||||
const 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.
|
||||
* @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}.
|
||||
*/
|
||||
public canRotate(): boolean {
|
||||
const diagram = this.diagram;
|
||||
if (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.
|
||||
* @param {number} angle the positive (clockwise) or negative (counter-clockwise) change in the rotation angle of each Part, in degrees.
|
||||
*/
|
||||
public rotate(angle: number): void {
|
||||
if (angle === undefined) angle = 90;
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction('rotate ' + angle.toString());
|
||||
diagram.selection.each((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}
|
||||
*/
|
||||
public pullToFront(): void {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction("pullToFront");
|
||||
// find the affected Layers
|
||||
const layers = new go.Map<go.Layer, number>();
|
||||
diagram.selection.each(function(part) {
|
||||
if (part.layer !== null) layers.set(part.layer, 0);
|
||||
});
|
||||
// find the maximum zOrder in each Layer
|
||||
layers.iteratorKeys.each(function(layer) {
|
||||
let max = 0;
|
||||
layer.parts.each(function(part) {
|
||||
if (part.isSelected) return;
|
||||
const 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) {
|
||||
const z = layers.get(part.layer as go.Layer) || 0;
|
||||
DrawCommandHandler._assignZOrder(part, z + 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}
|
||||
*/
|
||||
public pushToBack(): void {
|
||||
const diagram = this.diagram;
|
||||
diagram.startTransaction("pushToBack");
|
||||
// find the affected Layers
|
||||
const layers = new go.Map<go.Layer, number>();
|
||||
diagram.selection.each(function(part) {
|
||||
if (part.layer !== null) layers.set(part.layer, 0);
|
||||
});
|
||||
// find the minimum zOrder in each Layer
|
||||
layers.iteratorKeys.each(function(layer) {
|
||||
let min = 0;
|
||||
layer.parts.each(function(part) {
|
||||
if (part.isSelected) return;
|
||||
const 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) {
|
||||
const z = layers.get(part.layer as go.Layer) || 0;
|
||||
DrawCommandHandler._assignZOrder(part,
|
||||
// make sure a group's nested nodes are also behind everything else
|
||||
z - 1 - DrawCommandHandler._findGroupDepth(part));
|
||||
});
|
||||
diagram.commitTransaction("pushToBack");
|
||||
}
|
||||
|
||||
private static _assignZOrder(part: go.Part, z: number, root?: go.Part): void {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static _findGroupDepth(part: go.Part): number {
|
||||
if (part instanceof go.Group) {
|
||||
let 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.
|
||||
*/
|
||||
public doKeyDown(): void {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
|
||||
// determines the function of the arrow keys
|
||||
if (e.key === 'Up' || e.key === 'Down' || e.key === 'Left' || e.key === 'Right') {
|
||||
const behavior = this.arrowKeyBehavior;
|
||||
if (behavior === 'none') {
|
||||
// no-op
|
||||
return;
|
||||
} else if (behavior === 'select') {
|
||||
this._arrowKeySelect();
|
||||
return;
|
||||
} else if (behavior === 'move') {
|
||||
this._arrowKeyMove();
|
||||
return;
|
||||
}
|
||||
// otherwise drop through to get the default scrolling behavior
|
||||
}
|
||||
|
||||
// otherwise still does all standard commands
|
||||
super.doKeyDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects in an Array all of the non-Link Parts currently in the Diagram.
|
||||
*/
|
||||
private _getAllParts(): Array<any> {
|
||||
const allParts = new Array();
|
||||
this.diagram.nodes.each((node) => { allParts.push(node); });
|
||||
this.diagram.parts.each((part) => { allParts.push(part); });
|
||||
// note that this ignores Links
|
||||
return allParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called when arrow keys should move the Diagram.selection.
|
||||
*/
|
||||
private _arrowKeyMove(): void {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
// moves all selected parts in the specified direction
|
||||
let vdistance = 0;
|
||||
let 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) {
|
||||
const cellsize = diagram.grid.gridCellSize;
|
||||
hdistance = cellsize.width;
|
||||
vdistance = cellsize.height;
|
||||
}
|
||||
diagram.startTransaction('arrowKeyMove');
|
||||
diagram.selection.each((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.
|
||||
*/
|
||||
private _arrowKeySelect(): void {
|
||||
const diagram = this.diagram;
|
||||
const 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
|
||||
let 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.
|
||||
* @param {number} dir the direction, in degrees
|
||||
* @return {Part} the closest Part found in the given direction
|
||||
*/
|
||||
private _findNearestPartTowards(dir: number): go.Part | null {
|
||||
const originalPart = this.diagram.selection.first();
|
||||
if (originalPart === null) return null;
|
||||
const originalPoint = originalPart.actualBounds.center;
|
||||
const allParts = this._getAllParts();
|
||||
let closestDistance = Infinity;
|
||||
let closest = originalPart; // if no parts meet the criteria, the same part remains selected
|
||||
|
||||
for (let i = 0; i < allParts.length; i++) {
|
||||
const nextPart = allParts[i];
|
||||
if (nextPart === originalPart) continue; // skips over currently selected part
|
||||
const nextPoint = nextPart.actualBounds.center;
|
||||
const angle = originalPoint.directionPoint(nextPoint);
|
||||
const anglediff = this._angleCloseness(angle, dir);
|
||||
if (anglediff <= 45) { // if this part's center is within the desired direction's sector,
|
||||
let 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;
|
||||
}
|
||||
|
||||
private _angleCloseness(a: number, dir: number): number {
|
||||
return Math.min(Math.abs(dir - a), Math.min(Math.abs(dir + 360 - a), Math.abs(dir - 360 - a)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the last offset for pasting.
|
||||
* @param {Iterable.<Part>} coll a collection of {@link Part}s.
|
||||
*/
|
||||
public copyToClipboard(coll: go.Iterable<go.Part>): void {
|
||||
super.copyToClipboard(coll);
|
||||
this._lastPasteOffset.set(this.pasteOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste from the clipboard with an offset incremented on each paste, and reset when copied.
|
||||
* @return {Set.<Part>} a collection of newly pasted {@link Part}s
|
||||
*/
|
||||
public pasteFromClipboard(): go.Set<go.Part> {
|
||||
const coll = super.pasteFromClipboard();
|
||||
this.diagram.moveParts(coll, this._lastPasteOffset, false);
|
||||
this._lastPasteOffset.add(this.pasteOffset);
|
||||
return coll;
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+5064
File diff suppressed because it is too large
Load Diff
Executable
+5919
File diff suppressed because it is too large
Load Diff
+223
@@ -0,0 +1,223 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Fishbone Layout</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="sample">
|
||||
<div id="myDiagramDiv" style="height:550px;width:100%;border:1px solid black;"></div>
|
||||
<div id="buttons">
|
||||
<label>Layout:</label>
|
||||
<button id="fishbone">Fishbone</button>
|
||||
<button id="branching">Branching</button>
|
||||
<button id="normal">Normal</button>
|
||||
</div>
|
||||
<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.ts">FishboneLayout.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { FishboneLayout, FishboneLink } from './FishboneLayout.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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) {
|
||||
let size = data.size;
|
||||
if (size === undefined)
|
||||
size = 13;
|
||||
let 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
|
||||
const 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) {
|
||||
const key = arr.length;
|
||||
obj.key = key;
|
||||
arr.push(obj);
|
||||
const children = obj.causes;
|
||||
if (children) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const o = children[i];
|
||||
o.parent = key; // reference to parent node data
|
||||
walkJson(o, arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build the tree model
|
||||
const nodeDataArray = [];
|
||||
walkJson(json, nodeDataArray);
|
||||
myDiagram.model = new go.TreeModel(nodeDataArray);
|
||||
layoutFishbone();
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
|
||||
// use FishboneLayout and FishboneLink
|
||||
function layoutFishbone() {
|
||||
myDiagram.startTransaction('fishbone layout');
|
||||
myDiagram.linkTemplate = myDiagram.linkTemplateMap.getValue('fishbone');
|
||||
myDiagram.layout = go.GraphObject.make(FishboneLayout, {
|
||||
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.getValue('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.getValue('normal');
|
||||
myDiagram.layout = go.GraphObject.make(go.TreeLayout, {
|
||||
angle: 180,
|
||||
breadthLimit: 1000,
|
||||
alignment: go.TreeLayout.AlignmentStart
|
||||
});
|
||||
myDiagram.commitTransaction('normal layout');
|
||||
}
|
||||
|
||||
document.getElementById("fishbone").onclick = layoutFishbone;
|
||||
document.getElementById("branching").onclick = layoutBranching;
|
||||
document.getElementById("normal").onclick = layoutNormal;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* FishboneLayout is a custom {@link Layout} derived from {@link TreeLayout} for creating "fishbone" diagrams.
|
||||
* A fishbone diagram also requires a {@link Link} class that implements custom routing, {@link FishboneLink}.
|
||||
*
|
||||
* This only works for angle === 0 or angle === 180.
|
||||
*
|
||||
* This layout assumes Links are automatically routed in the way needed by fishbone diagrams,
|
||||
* by using the FishboneLink class instead of go.Link.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Fishbone.html">Fishbone Layout</a> sample.
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class FishboneLayout extends go.TreeLayout {
|
||||
/**
|
||||
* Constructs a FishboneLayout and sets the following properties:
|
||||
* - {@link #alignment} = {@link TreeLayout.AlignmentBusBranching}
|
||||
* - {@link #setsPortSpot} = false
|
||||
* - {@link #setsChildPortSpot} = false
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.alignment = go.TreeLayout.AlignmentBusBranching;
|
||||
this.setsPortSpot = false;
|
||||
this.setsChildPortSpot = false;
|
||||
}
|
||||
/**
|
||||
* Create and initialize a {@link LayoutNetwork} with the given nodes and links.
|
||||
* This override creates dummy vertexes, when necessary, to allow for proper positioning within the fishbone.
|
||||
* @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
|
||||
* @return {LayoutNetwork}
|
||||
*/
|
||||
makeNetwork(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
|
||||
const net = super.makeNetwork(coll);
|
||||
// make a copy of the collection of TreeVertexes
|
||||
// because we will be modifying the TreeNetwork.vertexes collection in the loop
|
||||
const verts = new go.List().addAll(net.vertexes.iterator);
|
||||
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
|
||||
const 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
|
||||
const dummy2 = net.createVertex();
|
||||
dummy2.bounds = v.bounds;
|
||||
dummy2.focus = v.focus;
|
||||
net.addVertex(dummy2);
|
||||
net.linkVertexes(v, dummy2, null);
|
||||
});
|
||||
return net;
|
||||
}
|
||||
/**
|
||||
* Add a direction property to each vertex and modify {@link TreeVertex#layerSpacing}.
|
||||
*/
|
||||
assignTreeVertexValues(v) {
|
||||
super.assignTreeVertexValues(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Assigns {@link Link#fromSpot}s and {@link Link#toSpot}s based on branching and angle
|
||||
* and moves vertexes based on dummy locations.
|
||||
*/
|
||||
commitNodes() {
|
||||
if (this.network === null)
|
||||
return;
|
||||
// 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) {
|
||||
const link = e.link;
|
||||
if (link === null)
|
||||
return;
|
||||
link.fromSpot = go.Spot.None;
|
||||
link.toSpot = go.Spot.None;
|
||||
const v = e.fromVertex;
|
||||
const 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
|
||||
let vit = this.network.vertexes.iterator;
|
||||
while (vit.next()) {
|
||||
const v = vit.value;
|
||||
const len = v.children.length;
|
||||
if (len === 0)
|
||||
continue; // ignore leaf nodes
|
||||
if (v.parent === null)
|
||||
continue; // don't move root node
|
||||
const dummy2 = v.children[len - 1];
|
||||
v.centerX = dummy2.centerX;
|
||||
v.centerY = dummy2.centerY;
|
||||
}
|
||||
const layout = this;
|
||||
vit = this.network.vertexes.iterator;
|
||||
while (vit.next()) {
|
||||
const v = vit.value;
|
||||
if (v.parent === null) {
|
||||
layout.shift(v);
|
||||
}
|
||||
}
|
||||
// now actually change the Node.location of all nodes
|
||||
super.commitNodes();
|
||||
}
|
||||
/**
|
||||
* This override stops links from being committed since the work is done by the {@link FishboneLink} class.
|
||||
*/
|
||||
commitLinks() { }
|
||||
/**
|
||||
* Shifts subtrees within the fishbone based on angle and node spacing.
|
||||
*/
|
||||
shift(v) {
|
||||
const p = v.parent;
|
||||
if (p !== null && (v.angle === 90 || v.angle === 270)) {
|
||||
const g = p.parent;
|
||||
if (g !== null) {
|
||||
const 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
|
||||
const dir = ((p.angle === 0) ? 1 : -1);
|
||||
v['_direction'] = dir;
|
||||
this.shiftAll(dir, 0, p, v);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < v.children.length; i++) {
|
||||
const c = v.children[i];
|
||||
this.shift(c);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Shifts a subtree.
|
||||
*/
|
||||
shiftAll(direction, absolute, root, v) {
|
||||
// assert(root.angle === 0 || root.angle === 180);
|
||||
let locx = v.centerX;
|
||||
locx += direction * Math.abs(root.centerY - v.centerY) / 2;
|
||||
locx += absolute;
|
||||
v.centerX = locx;
|
||||
for (let i = 0; i < v.children.length; i++) {
|
||||
const c = v.children[i];
|
||||
this.shiftAll(direction, absolute, root, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Custom {@link Link} class for {@link FishboneLayout}.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class FishboneLink extends go.Link {
|
||||
computeAdjusting() { return this.adjusting; }
|
||||
/**
|
||||
* Determines the points for this link based on spots and maintains horizontal lines.
|
||||
*/
|
||||
computePoints() {
|
||||
const result = super.computePoints();
|
||||
if (result) {
|
||||
// insert middle point to maintain horizontal lines
|
||||
if (this.fromSpot.equals(go.Spot.Right) || this.fromSpot.equals(go.Spot.Left)) {
|
||||
let p1;
|
||||
// deal with root node being on the "wrong" side
|
||||
const fromnode = this.fromNode;
|
||||
const fromport = this.fromPort;
|
||||
if (fromnode !== null && fromport !== null && fromnode.findLinksInto().count === 0) {
|
||||
// pretend the link is coming from the opposite direction than the declared FromSpot
|
||||
const fromctr = fromport.getDocumentPoint(go.Spot.Center);
|
||||
const 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);
|
||||
let 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
|
||||
}
|
||||
const tonode = this.toNode;
|
||||
const toport = this.toPort;
|
||||
if (tonode !== null && toport !== null) {
|
||||
const toctr = toport.getDocumentPoint(go.Spot.Center);
|
||||
const far = toctr.copy();
|
||||
far.x += (this.fromSpot.equals(go.Spot.Left)) ? -99999 / 2 : 99999 / 2;
|
||||
far.y += (toctr.y < p1.y) ? 99999 : -99999;
|
||||
const p2 = this.getLinkPointFromPoint(tonode, toport, toctr, far, false);
|
||||
this.setPoint(2, p2);
|
||||
let 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)) {
|
||||
const p1 = this.getPoint(1); // points 1 & 2 should be OK already
|
||||
const fromnode = this.fromNode;
|
||||
const fromport = this.fromPort;
|
||||
if (fromnode !== null && fromport !== null) {
|
||||
const parentlink = fromnode.findLinksInto().first();
|
||||
const fromctr = fromport.getDocumentPoint(go.Spot.Center);
|
||||
const 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;
|
||||
const p0 = this.getLinkPointFromPoint(fromnode, fromport, fromctr, far, true);
|
||||
this.setPoint(0, p0);
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* FishboneLayout is a custom {@link Layout} derived from {@link TreeLayout} for creating "fishbone" diagrams.
|
||||
* A fishbone diagram also requires a {@link Link} class that implements custom routing, {@link FishboneLink}.
|
||||
*
|
||||
* This only works for angle === 0 or angle === 180.
|
||||
*
|
||||
* This layout assumes Links are automatically routed in the way needed by fishbone diagrams,
|
||||
* by using the FishboneLink class instead of go.Link.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Fishbone.html">Fishbone Layout</a> sample.
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class FishboneLayout extends go.TreeLayout {
|
||||
/**
|
||||
* Constructs a FishboneLayout and sets the following properties:
|
||||
* - {@link #alignment} = {@link TreeLayout.AlignmentBusBranching}
|
||||
* - {@link #setsPortSpot} = false
|
||||
* - {@link #setsChildPortSpot} = false
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.alignment = go.TreeLayout.AlignmentBusBranching;
|
||||
this.setsPortSpot = false;
|
||||
this.setsChildPortSpot = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and initialize a {@link LayoutNetwork} with the given nodes and links.
|
||||
* This override creates dummy vertexes, when necessary, to allow for proper positioning within the fishbone.
|
||||
* @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
|
||||
* @return {LayoutNetwork}
|
||||
*/
|
||||
public makeNetwork(coll: go.Diagram | go.Group | go.Iterable<go.Part>): go.LayoutNetwork {
|
||||
// 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
|
||||
const net = super.makeNetwork(coll);
|
||||
// make a copy of the collection of TreeVertexes
|
||||
// because we will be modifying the TreeNetwork.vertexes collection in the loop
|
||||
const verts = new go.List<go.TreeVertex>().addAll(net.vertexes.iterator as go.Iterator<go.TreeVertex>);
|
||||
verts.each(function(v: go.TreeVertex) {
|
||||
// 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
|
||||
const 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
|
||||
const dummy2 = net.createVertex();
|
||||
dummy2.bounds = v.bounds;
|
||||
dummy2.focus = v.focus;
|
||||
net.addVertex(dummy2);
|
||||
net.linkVertexes(v, dummy2, null);
|
||||
});
|
||||
return net;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a direction property to each vertex and modify {@link TreeVertex#layerSpacing}.
|
||||
*/
|
||||
public assignTreeVertexValues(v: go.TreeVertex): void {
|
||||
super.assignTreeVertexValues(v);
|
||||
(v as any)['_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns {@link Link#fromSpot}s and {@link Link#toSpot}s based on branching and angle
|
||||
* and moves vertexes based on dummy locations.
|
||||
*/
|
||||
public commitNodes(): void {
|
||||
if (this.network === null) return;
|
||||
// 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) {
|
||||
const link = e.link;
|
||||
if (link === null) return;
|
||||
link.fromSpot = go.Spot.None;
|
||||
link.toSpot = go.Spot.None;
|
||||
|
||||
const v: go.TreeVertex = e.fromVertex as go.TreeVertex;
|
||||
const w: go.TreeVertex = e.toVertex as go.TreeVertex;
|
||||
|
||||
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
|
||||
let vit = this.network.vertexes.iterator;
|
||||
while (vit.next()) {
|
||||
const v = vit.value as go.TreeVertex;
|
||||
const len = v.children.length;
|
||||
if (len === 0) continue; // ignore leaf nodes
|
||||
if (v.parent === null) continue; // don't move root node
|
||||
const dummy2 = v.children[len - 1];
|
||||
v.centerX = dummy2.centerX;
|
||||
v.centerY = dummy2.centerY;
|
||||
}
|
||||
|
||||
const layout = this;
|
||||
vit = this.network.vertexes.iterator;
|
||||
while (vit.next()) {
|
||||
const v = vit.value as go.TreeVertex;
|
||||
if (v.parent === null) {
|
||||
layout.shift(v);
|
||||
}
|
||||
}
|
||||
|
||||
// now actually change the Node.location of all nodes
|
||||
super.commitNodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* This override stops links from being committed since the work is done by the {@link FishboneLink} class.
|
||||
*/
|
||||
public commitLinks(): void { }
|
||||
|
||||
/**
|
||||
* Shifts subtrees within the fishbone based on angle and node spacing.
|
||||
*/
|
||||
public shift(v: go.TreeVertex): void {
|
||||
const p = v.parent;
|
||||
if (p !== null && (v.angle === 90 || v.angle === 270)) {
|
||||
const g = p.parent;
|
||||
if (g !== null) {
|
||||
const shift = v.nodeSpacing;
|
||||
if ((g as any)['_direction'] > 0) {
|
||||
if (g.angle === 90) {
|
||||
if (p.angle === 0) {
|
||||
(v as any)['_direction'] = 1;
|
||||
if (v.angle === 270) this.shiftAll(2, -shift, p, v);
|
||||
} else if (p.angle === 180) {
|
||||
(v as any)['_direction'] = -1;
|
||||
if (v.angle === 90) this.shiftAll(-2, shift, p, v);
|
||||
}
|
||||
} else if (g.angle === 270) {
|
||||
if (p.angle === 0) {
|
||||
(v as any)['_direction'] = 1;
|
||||
if (v.angle === 90) this.shiftAll(2, -shift, p, v);
|
||||
} else if (p.angle === 180) {
|
||||
(v as any)['_direction'] = -1;
|
||||
if (v.angle === 270) this.shiftAll(-2, shift, p, v);
|
||||
}
|
||||
}
|
||||
} else if ((g as any)['_direction'] < 0) {
|
||||
if (g.angle === 90) {
|
||||
if (p.angle === 0) {
|
||||
(v as any)['_direction'] = 1;
|
||||
if (v.angle === 90) this.shiftAll(2, -shift, p, v);
|
||||
} else if (p.angle === 180) {
|
||||
(v as any)['_direction'] = -1;
|
||||
if (v.angle === 270) this.shiftAll(-2, shift, p, v);
|
||||
}
|
||||
} else if (g.angle === 270) {
|
||||
if (p.angle === 0) {
|
||||
(v as any)['_direction'] = 1;
|
||||
if (v.angle === 270) this.shiftAll(2, -shift, p, v);
|
||||
} else if (p.angle === 180) {
|
||||
(v as any)['_direction'] = -1;
|
||||
if (v.angle === 90) this.shiftAll(-2, shift, p, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // g === null: V is a child of the tree ROOT
|
||||
const dir = ((p.angle === 0) ? 1 : -1);
|
||||
(v as any)['_direction'] = dir;
|
||||
this.shiftAll(dir, 0, p, v);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < v.children.length; i++) {
|
||||
const c = v.children[i];
|
||||
this.shift(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts a subtree.
|
||||
*/
|
||||
public shiftAll(direction: number, absolute: number, root: go.TreeVertex, v: go.TreeVertex): void {
|
||||
// assert(root.angle === 0 || root.angle === 180);
|
||||
let locx = v.centerX;
|
||||
locx += direction * Math.abs(root.centerY - v.centerY) / 2;
|
||||
locx += absolute;
|
||||
v.centerX = locx;
|
||||
for (let i = 0; i < v.children.length; i++) {
|
||||
const c = v.children[i];
|
||||
this.shiftAll(direction, absolute, root, c);
|
||||
}
|
||||
}
|
||||
// end FishboneLayout
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom {@link Link} class for {@link FishboneLayout}.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class FishboneLink extends go.Link {
|
||||
public computeAdjusting(): go.EnumValue { return this.adjusting; }
|
||||
/**
|
||||
* Determines the points for this link based on spots and maintains horizontal lines.
|
||||
*/
|
||||
public computePoints(): boolean {
|
||||
const result = super.computePoints();
|
||||
if (result) {
|
||||
// insert middle point to maintain horizontal lines
|
||||
if (this.fromSpot.equals(go.Spot.Right) || this.fromSpot.equals(go.Spot.Left)) {
|
||||
let p1: go.Point;
|
||||
// deal with root node being on the "wrong" side
|
||||
const fromnode = this.fromNode;
|
||||
const fromport = this.fromPort;
|
||||
if (fromnode !== null && fromport !== null && fromnode.findLinksInto().count === 0) {
|
||||
// pretend the link is coming from the opposite direction than the declared FromSpot
|
||||
const fromctr = fromport.getDocumentPoint(go.Spot.Center);
|
||||
const 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);
|
||||
let 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
|
||||
}
|
||||
const tonode = this.toNode;
|
||||
const toport = this.toPort;
|
||||
if (tonode !== null && toport !== null) {
|
||||
const toctr = toport.getDocumentPoint(go.Spot.Center);
|
||||
const far = toctr.copy();
|
||||
far.x += (this.fromSpot.equals(go.Spot.Left)) ? -99999 / 2 : 99999 / 2;
|
||||
far.y += (toctr.y < p1.y) ? 99999 : -99999;
|
||||
const p2 = this.getLinkPointFromPoint(tonode, toport, toctr, far, false);
|
||||
this.setPoint(2, p2);
|
||||
let 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)) {
|
||||
const p1: go.Point = this.getPoint(1); // points 1 & 2 should be OK already
|
||||
const fromnode = this.fromNode;
|
||||
const fromport = this.fromPort;
|
||||
if (fromnode !== null && fromport !== null) {
|
||||
const parentlink = fromnode.findLinksInto().first();
|
||||
const fromctr = fromport.getDocumentPoint(go.Spot.Center);
|
||||
const 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;
|
||||
const p0 = this.getLinkPointFromPoint(fromnode, fromport, fromctr, far, true);
|
||||
this.setPoint(0, p0);
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Freehand Drawing Tool</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script><!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="sample">
|
||||
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 600px"></div>
|
||||
<div id="buttons">
|
||||
<button id="select">Select</button>
|
||||
<button id="drawMode">Draw Mode</button>
|
||||
<button id="save">Save</button>
|
||||
<button id="load">Load</button>
|
||||
<label><input type="checkbox" id = "allowResizing" checked="checked" />Allow Resizing</label>
|
||||
<label><input type="checkbox" id = "allowReshaping" checked="checked" />Allow Reshaping</label>
|
||||
<label><input type="checkbox" id = "allowRotating" checked="checked" />Allow Rotating</label>
|
||||
</div>
|
||||
<p>
|
||||
This sample demonstrates the FreehandDrawingTool. It is defined in its own file, as <a href="FreehandDrawingTool.ts">FreehandDrawingTool.ts</a>.
|
||||
It also demonstrates the GeometryReshapingTool, another custom tool, defined in <a href="GeometryReshapingTool.ts">GeometryReshapingTool.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { FreehandDrawingTool } from './FreehandDrawingTool.js';
|
||||
import { GeometryReshapingTool } from './GeometryReshapingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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
|
||||
const 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
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
|
||||
function mode(draw) {
|
||||
const tool = myDiagram.toolManager.findTool('FreehandDrawing');
|
||||
if (tool !== null)
|
||||
tool.isEnabled = draw;
|
||||
}
|
||||
function updateAllAdornments() {
|
||||
myDiagram.selection.each(function (p) { p.updateAdornments(); });
|
||||
}
|
||||
// save a model to and load a model from Json text, displayed below the Diagram
|
||||
function save() {
|
||||
const str = '{ "position": "' + go.Point.stringify(myDiagram.position) + '",\n "model": ' + myDiagram.model.toJson() + ' }';
|
||||
document.getElementById('mySavedDiagram').value = str;
|
||||
}
|
||||
function load() {
|
||||
const str = document.getElementById('mySavedDiagram').value;
|
||||
try {
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
function select() {
|
||||
mode(false);
|
||||
}
|
||||
function drawMode() {
|
||||
mode(true);
|
||||
}
|
||||
function allowResizing() {
|
||||
myDiagram.allowResize = !myDiagram.allowResize;
|
||||
updateAllAdornments();
|
||||
}
|
||||
function allowReshaping() {
|
||||
myDiagram.allowReshape = !myDiagram.allowReshape;
|
||||
updateAllAdornments();
|
||||
}
|
||||
function allowRotating() {
|
||||
myDiagram.allowRotate = !myDiagram.allowRotate;
|
||||
updateAllAdornments();
|
||||
}
|
||||
|
||||
document.getElementById("select").onclick = select;
|
||||
document.getElementById("drawMode").onclick = drawMode;
|
||||
document.getElementById("save").onclick = save;
|
||||
document.getElementById("load").onclick = load;
|
||||
document.getElementById("allowResizing").onclick = allowResizing;
|
||||
document.getElementById("allowReshaping").onclick = allowReshaping;
|
||||
document.getElementById("allowRotating").onclick = allowRotating;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The FreehandDrawingTool 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.
|
||||
*
|
||||
* This tool may be installed as the first mouse down tool:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseDownTools.insertAt(0, new FreehandDrawingTool());
|
||||
* ```
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/FreehandDrawing.html">Freehand Drawing</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class FreehandDrawingTool extends go.Tool {
|
||||
constructor() {
|
||||
super();
|
||||
// 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 });
|
||||
this._archetypePartData = {}; // the data to copy for a new polyline Part
|
||||
this._isBackgroundOnly = true; // affects canStart()
|
||||
// the Shape has to be inside a temporary Part that is used during the drawing operation
|
||||
this.temp = go.GraphObject.make(go.Part, { layerName: 'Tool' }, this._temporaryShape);
|
||||
this.name = 'FreehandDrawing';
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get temporaryShape() { return this._temporaryShape; }
|
||||
set temporaryShape(val) {
|
||||
if (this._temporaryShape !== val && val !== null) {
|
||||
val.name = 'SHAPE';
|
||||
const panel = this._temporaryShape.panel;
|
||||
if (panel !== null) {
|
||||
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.
|
||||
*/
|
||||
get archetypePartData() { return this._archetypePartData; }
|
||||
set archetypePartData(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.
|
||||
*/
|
||||
get isBackgroundOnly() { return this._isBackgroundOnly; }
|
||||
set isBackgroundOnly(val) { this._isBackgroundOnly = val; }
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
canStart() {
|
||||
if (!this.isEnabled)
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
if (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
|
||||
const part = diagram.findPartAt(diagram.lastInput.documentPoint, true);
|
||||
if (part !== null)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Capture the mouse and use a "crosshair" cursor.
|
||||
*/
|
||||
doActivate() {
|
||||
super.doActivate();
|
||||
this.diagram.isMouseCaptured = true;
|
||||
this.diagram.currentCursor = 'crosshair';
|
||||
}
|
||||
/**
|
||||
* Release the mouse and reset the cursor.
|
||||
*/
|
||||
doDeactivate() {
|
||||
super.doDeactivate();
|
||||
if (this.temporaryShape !== null && this.temporaryShape.part !== null) {
|
||||
this.diagram.remove(this.temporaryShape.part);
|
||||
}
|
||||
this.diagram.currentCursor = '';
|
||||
this.diagram.isMouseCaptured = false;
|
||||
}
|
||||
/**
|
||||
* This adds a Point to the {@link #temporaryShape}'s geometry.
|
||||
*
|
||||
* If the Shape is not yet in the Diagram, its geometry is initialized and
|
||||
* its parent Part is added to the Diagram.
|
||||
*
|
||||
* If the point is less than half a pixel away from the previous point, it is ignored.
|
||||
*/
|
||||
addPoint(p) {
|
||||
const shape = this.temporaryShape;
|
||||
if (shape === null)
|
||||
return;
|
||||
const part = shape.part;
|
||||
if (part === null)
|
||||
return;
|
||||
// for the temporary Shape, normalize the geometry to be in the viewport
|
||||
const viewpt = this.diagram.viewportBounds.position;
|
||||
const q = new go.Point(p.x - viewpt.x, p.y - viewpt.y);
|
||||
if (part.diagram === null) {
|
||||
const f = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill
|
||||
const g = new go.Geometry().add(f); // the Shape.geometry consists of a single PathFigure
|
||||
shape.geometry = g;
|
||||
// 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
|
||||
const geo = shape.geometry;
|
||||
if (geo !== null) {
|
||||
const fig = geo.figures.first();
|
||||
if (fig !== null) {
|
||||
const segs = fig.segments;
|
||||
const idx = segs.count - 1;
|
||||
if (idx >= 0) {
|
||||
const 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
|
||||
const geo2 = geo.copy();
|
||||
const fig2 = geo2.figures.first();
|
||||
if (fig2 !== null) {
|
||||
fig2.add(new go.PathSegment(go.PathSegment.Line, q.x, q.y));
|
||||
shape.geometry = geo2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Start drawing the line by starting to accumulate points in the {@link #temporaryShape}'s geometry.
|
||||
*/
|
||||
doMouseDown() {
|
||||
if (!this.isActive) {
|
||||
this.doActivate();
|
||||
// the first point
|
||||
this.addPoint(this.diagram.lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Keep accumulating points in the {@link #temporaryShape}'s geometry.
|
||||
*/
|
||||
doMouseMove() {
|
||||
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.
|
||||
*/
|
||||
doMouseUp() {
|
||||
const diagram = this.diagram;
|
||||
let started = false;
|
||||
if (this.isActive) {
|
||||
started = true;
|
||||
// the last point
|
||||
this.addPoint(diagram.lastInput.documentPoint);
|
||||
// normalize geometry and node position
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
if (this.temporaryShape.geometry !== null) {
|
||||
const geo = this.temporaryShape.geometry.copy();
|
||||
const 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
|
||||
const d = diagram.model.copyNodeData(this.archetypePartData);
|
||||
if (d !== null) {
|
||||
// adding data to model creates the actual Part
|
||||
diagram.model.addNodeData(d);
|
||||
const part = diagram.findPartForData(d);
|
||||
if (part !== null) {
|
||||
// assign the location
|
||||
part.location = new go.Point(pos.x + geo.bounds.width / 2, pos.y + geo.bounds.height / 2);
|
||||
// assign the Shape.geometry
|
||||
const shape = part.findObject('SHAPE');
|
||||
if (shape !== null)
|
||||
shape.geometry = geo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stopTool();
|
||||
if (started)
|
||||
diagram.commitTransaction(this.name);
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The FreehandDrawingTool 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.
|
||||
*
|
||||
* This tool may be installed as the first mouse down tool:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseDownTools.insertAt(0, new FreehandDrawingTool());
|
||||
* ```
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/FreehandDrawing.html">Freehand Drawing</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class FreehandDrawingTool extends go.Tool {
|
||||
// this is the Shape that is shown during a drawing operation
|
||||
private _temporaryShape: go.GraphObject = go.GraphObject.make(go.Shape, { name: 'SHAPE', fill: null, strokeWidth: 1.5 });
|
||||
private _archetypePartData: go.ObjectData = {}; // the data to copy for a new polyline Part
|
||||
private _isBackgroundOnly: boolean = true; // affects canStart()
|
||||
|
||||
// the Shape has to be inside a temporary Part that is used during the drawing operation
|
||||
private temp: go.GraphObject = go.GraphObject.make(go.Part, { layerName: 'Tool' }, this._temporaryShape);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'FreehandDrawing';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get temporaryShape(): go.Shape { return this._temporaryShape as go.Shape; }
|
||||
set temporaryShape(val: go.Shape) {
|
||||
if (this._temporaryShape !== val && val !== null) {
|
||||
val.name = 'SHAPE';
|
||||
const panel = this._temporaryShape.panel;
|
||||
if (panel !== null) {
|
||||
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.
|
||||
*/
|
||||
get archetypePartData(): go.ObjectData { return this._archetypePartData; }
|
||||
set archetypePartData(val: go.ObjectData) { 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.
|
||||
*/
|
||||
get isBackgroundOnly(): boolean { return this._isBackgroundOnly; }
|
||||
set isBackgroundOnly(val: boolean) { this._isBackgroundOnly = val; }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!this.isEnabled) return false;
|
||||
const diagram = this.diagram;
|
||||
if (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
|
||||
const part = diagram.findPartAt(diagram.lastInput.documentPoint, true);
|
||||
if (part !== null) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture the mouse and use a "crosshair" cursor.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
super.doActivate();
|
||||
this.diagram.isMouseCaptured = true;
|
||||
this.diagram.currentCursor = 'crosshair';
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the mouse and reset the cursor.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
super.doDeactivate();
|
||||
if (this.temporaryShape !== null && this.temporaryShape.part !== null) {
|
||||
this.diagram.remove(this.temporaryShape.part);
|
||||
}
|
||||
this.diagram.currentCursor = '';
|
||||
this.diagram.isMouseCaptured = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds a Point to the {@link #temporaryShape}'s geometry.
|
||||
*
|
||||
* If the Shape is not yet in the Diagram, its geometry is initialized and
|
||||
* its parent Part is added to the Diagram.
|
||||
*
|
||||
* If the point is less than half a pixel away from the previous point, it is ignored.
|
||||
*/
|
||||
public addPoint(p: go.Point): void {
|
||||
const shape = this.temporaryShape;
|
||||
if (shape === null) return;
|
||||
const part = shape.part;
|
||||
if (part === null) return;
|
||||
|
||||
// for the temporary Shape, normalize the geometry to be in the viewport
|
||||
const viewpt = this.diagram.viewportBounds.position;
|
||||
const q = new go.Point(p.x - viewpt.x, p.y - viewpt.y);
|
||||
|
||||
if (part.diagram === null) {
|
||||
const f = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill
|
||||
const g = new go.Geometry().add(f); // the Shape.geometry consists of a single PathFigure
|
||||
shape.geometry = g;
|
||||
// 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
|
||||
const geo = shape.geometry;
|
||||
if (geo !== null) {
|
||||
const fig = geo.figures.first();
|
||||
if (fig !== null) {
|
||||
const segs = fig.segments;
|
||||
const idx = segs.count - 1;
|
||||
if (idx >= 0) {
|
||||
const 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
|
||||
const geo2 = geo.copy();
|
||||
const fig2 = geo2.figures.first();
|
||||
if (fig2 !== null) {
|
||||
fig2.add(new go.PathSegment(go.PathSegment.Line, q.x, q.y));
|
||||
shape.geometry = geo2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start drawing the line by starting to accumulate points in the {@link #temporaryShape}'s geometry.
|
||||
*/
|
||||
public doMouseDown(): void {
|
||||
if (!this.isActive) {
|
||||
this.doActivate();
|
||||
// the first point
|
||||
this.addPoint(this.diagram.lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep accumulating points in the {@link #temporaryShape}'s geometry.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
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.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
const diagram = this.diagram;
|
||||
let started = false;
|
||||
if (this.isActive) {
|
||||
started = true;
|
||||
// the last point
|
||||
this.addPoint(diagram.lastInput.documentPoint);
|
||||
// normalize geometry and node position
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
if (this.temporaryShape.geometry !== null) {
|
||||
const geo = this.temporaryShape.geometry.copy();
|
||||
const 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
|
||||
const d = diagram.model.copyNodeData(this.archetypePartData);
|
||||
if (d !== null) {
|
||||
// adding data to model creates the actual Part
|
||||
diagram.model.addNodeData(d);
|
||||
const part = diagram.findPartForData(d);
|
||||
if (part !== null) {
|
||||
// assign the location
|
||||
part.location = new go.Point(pos.x + geo.bounds.width / 2, pos.y + geo.bounds.height / 2);
|
||||
// assign the Shape.geometry
|
||||
const shape = part.findObject('SHAPE') as go.Shape;
|
||||
if (shape !== null) shape.geometry = geo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stopTool();
|
||||
if (started) diagram.commitTransaction(this.name);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Geometry Reshaping</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">GeometryReshapingTool.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { GeometryReshapingTool } from './GeometryReshapingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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 }], []);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/GeometryReshaping.html">Geometry Reshaping</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class GeometryReshapingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a GeometryReshapingTool and sets the handle and name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._reshapeObjectName = 'SHAPE'; // ??? can't add Part.reshapeObjectName property
|
||||
// there's no Part.reshapeAdornmentTemplate either
|
||||
// internal state
|
||||
this._handle = null;
|
||||
this._adornedShape = null;
|
||||
this._originalGeometry = null; // in case the tool is cancelled and the UndoManager is not enabled
|
||||
const h = new go.Shape();
|
||||
h.figure = 'Diamond';
|
||||
h.desiredSize = new go.Size(7, 7);
|
||||
h.fill = 'lightblue';
|
||||
h.stroke = 'dodgerblue';
|
||||
h.cursor = 'move';
|
||||
this._handleArchetype = h;
|
||||
this.name = 'GeometryReshaping';
|
||||
}
|
||||
/**
|
||||
* A small GraphObject used as a reshape handle for each segment.
|
||||
* The default GraphObject is a small blue diamond.
|
||||
*/
|
||||
get handleArchetype() { return this._handleArchetype; }
|
||||
set handleArchetype(value) { this._handleArchetype = value; }
|
||||
/**
|
||||
* The name of the GraphObject to be reshaped.
|
||||
*/
|
||||
get reshapeObjectName() { return this._reshapeObjectName; }
|
||||
set reshapeObjectName(value) { 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}.
|
||||
*/
|
||||
get handle() { return this._handle; }
|
||||
/**
|
||||
* Gets the {@link Shape} that is being reshaped.
|
||||
* This must be contained within the selected Part.
|
||||
*/
|
||||
get adornedShape() { 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.
|
||||
*/
|
||||
get originalGeometry() { 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}.
|
||||
*/
|
||||
updateAdornments(part) {
|
||||
if (part === null || part instanceof go.Link)
|
||||
return; // this tool never applies to Links
|
||||
if (part.isSelected && !this.diagram.isReadOnly) {
|
||||
const selelt = part.findObject(this.reshapeObjectName);
|
||||
if (selelt instanceof go.Shape && selelt.geometry !== null &&
|
||||
selelt.actualBounds.isReal() && selelt.isVisibleObject() &&
|
||||
part.canReshape() && part.actualBounds.isReal() && part.isVisible() &&
|
||||
selelt.geometry.type === go.Geometry.Path) {
|
||||
let adornment = part.findAdornment(this.name);
|
||||
if (adornment === null) {
|
||||
adornment = this.makeAdornment(selelt);
|
||||
}
|
||||
if (adornment !== null) {
|
||||
// update the position/alignment of each handle
|
||||
const geo = selelt.geometry;
|
||||
const b = geo.bounds;
|
||||
// update the size of the adornment
|
||||
const body = adornment.findObject('BODY');
|
||||
if (body !== null)
|
||||
body.desiredSize = b.size;
|
||||
adornment.elements.each(function (h) {
|
||||
if (h._typ === undefined)
|
||||
return;
|
||||
const fig = geo.figures.elt(h._fig);
|
||||
const seg = fig.segments.elt(h._seg);
|
||||
let x = 0;
|
||||
let 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);
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
makeAdornment(selelt) {
|
||||
const 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);
|
||||
let h = new go.Shape();
|
||||
h.name = 'BODY';
|
||||
h.fill = null;
|
||||
h.stroke = null;
|
||||
h.strokeWidth = 0;
|
||||
adornment.add(h);
|
||||
const geo = selelt.geometry;
|
||||
if (geo !== null) {
|
||||
// requires Path Geometry, checked above in updateAdornments
|
||||
for (let f = 0; f < geo.figures.count; f++) {
|
||||
const fig = geo.figures.elt(f);
|
||||
for (let g = 0; g < fig.segments.count; g++) {
|
||||
const seg = fig.segments.elt(g);
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
makeHandle(selelt, fig, seg) {
|
||||
const h = this.handleArchetype;
|
||||
if (h === null)
|
||||
return null;
|
||||
return h.copy();
|
||||
}
|
||||
/**
|
||||
* This tool may run when there is a mouse-down event on a reshape handle.
|
||||
*/
|
||||
canStart() {
|
||||
if (!this.isEnabled)
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly)
|
||||
return false;
|
||||
if (!diagram.allowReshape)
|
||||
return false;
|
||||
if (!diagram.lastInput.left)
|
||||
return false;
|
||||
const h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
return (h !== null);
|
||||
}
|
||||
/**
|
||||
* Start reshaping, if {@link #findToolHandleAt} finds a reshape handle at the mouse down point.
|
||||
*
|
||||
* If successful this sets {@link #handle} to be the reshape handle that it finds
|
||||
* and {@link #adornedShape} to be the {@link Shape} being reshaped.
|
||||
* It also remembers the original geometry in case this tool is cancelled.
|
||||
* And it starts a transaction.
|
||||
*/
|
||||
doActivate() {
|
||||
const diagram = this.diagram;
|
||||
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
if (this._handle === null)
|
||||
return;
|
||||
const 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 stops the current reshaping operation with the Shape as it is.
|
||||
*/
|
||||
doDeactivate() {
|
||||
this.stopTransaction();
|
||||
this._handle = null;
|
||||
this._adornedShape = null;
|
||||
const diagram = this.diagram;
|
||||
diagram.isMouseCaptured = false;
|
||||
this.isActive = false;
|
||||
}
|
||||
/**
|
||||
* Restore the shape to be the original geometry and stop this tool.
|
||||
*/
|
||||
doCancel() {
|
||||
const shape = this._adornedShape;
|
||||
if (shape !== null) {
|
||||
// explicitly restore the original route, in case !UndoManager.isEnabled
|
||||
shape.geometry = this._originalGeometry;
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Call {@link #reshape} with a new point determined by the mouse
|
||||
* to change the geometry of the {@link #adornedShape}.
|
||||
*/
|
||||
doMouseMove() {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeReshape(diagram.lastInput.documentPoint);
|
||||
this.reshape(newpt);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reshape the Shape's geometry with a point based on the most recent mouse point by calling {@link #reshape},
|
||||
* and then stop this tool.
|
||||
*/
|
||||
doMouseUp() {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeReshape(diagram.lastInput.documentPoint);
|
||||
this.reshape(newpt);
|
||||
this.transactionResult = this.name; // success
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Change the geometry of the {@link #adornedShape} by moving the point corresponding to the current
|
||||
* {@link #handle} to be at the given {@link Point}.
|
||||
* This is called by {@link #doMouseMove} and {@link #doMouseUp} with the result of calling
|
||||
* {@link #computeReshape} to constrain the input point.
|
||||
* @param {Point} newPoint the value of the call to {@link #computeReshape}.
|
||||
*/
|
||||
reshape(newPoint) {
|
||||
const shape = this.adornedShape;
|
||||
if (shape === null || shape.geometry === null)
|
||||
return;
|
||||
const locpt = shape.getLocalPoint(newPoint);
|
||||
const geo = shape.geometry.copy();
|
||||
const type = this.handle._typ;
|
||||
if (type === undefined)
|
||||
return;
|
||||
const fig = geo.figures.elt(this.handle._fig);
|
||||
const 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;
|
||||
}
|
||||
const 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
|
||||
const part = shape.part; // move the Part holding the Shape
|
||||
if (part === null)
|
||||
return;
|
||||
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
|
||||
}
|
||||
/**
|
||||
* This is called by {@link #doMouseMove} and {@link #doMouseUp} to limit the input point
|
||||
* before calling {@link #reshape}.
|
||||
* By default, this doesn't limit the input point.
|
||||
* @param {Point} p the point where the handle is being dragged.
|
||||
* @return {Point}
|
||||
*/
|
||||
computeReshape(p) {
|
||||
return p; // no constraints on the points
|
||||
}
|
||||
}
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/GeometryReshaping.html">Geometry Reshaping</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class GeometryReshapingTool extends go.Tool {
|
||||
|
||||
private _handleArchetype: go.GraphObject;
|
||||
private _reshapeObjectName = 'SHAPE'; // ??? can't add Part.reshapeObjectName property
|
||||
// there's no Part.reshapeAdornmentTemplate either
|
||||
|
||||
// internal state
|
||||
private _handle: go.GraphObject | null = null;
|
||||
private _adornedShape: go.Shape | null = null;
|
||||
private _originalGeometry: go.Geometry | null = null; // in case the tool is cancelled and the UndoManager is not enabled
|
||||
|
||||
/**
|
||||
* Constructs a GeometryReshapingTool and sets the handle and name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
const h: go.Shape = new go.Shape();
|
||||
h.figure = 'Diamond';
|
||||
h.desiredSize = new go.Size(7, 7);
|
||||
h.fill = 'lightblue';
|
||||
h.stroke = 'dodgerblue';
|
||||
h.cursor = 'move';
|
||||
this._handleArchetype = h;
|
||||
this.name = 'GeometryReshaping';
|
||||
}
|
||||
|
||||
/**
|
||||
* A small GraphObject used as a reshape handle for each segment.
|
||||
* The default GraphObject is a small blue diamond.
|
||||
*/
|
||||
get handleArchetype(): go.GraphObject { return this._handleArchetype; }
|
||||
set handleArchetype(value: go.GraphObject) { this._handleArchetype = value; }
|
||||
|
||||
/**
|
||||
* The name of the GraphObject to be reshaped.
|
||||
*/
|
||||
get reshapeObjectName(): string { return this._reshapeObjectName; }
|
||||
set reshapeObjectName(value: string) { 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}.
|
||||
*/
|
||||
get handle(): go.GraphObject | null { return this._handle; }
|
||||
|
||||
/**
|
||||
* Gets the {@link Shape} that is being reshaped.
|
||||
* This must be contained within the selected Part.
|
||||
*/
|
||||
get adornedShape(): go.Shape | null { 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.
|
||||
*/
|
||||
get originalGeometry(): go.Geometry | null { 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}.
|
||||
*/
|
||||
public updateAdornments(part: go.Part): void {
|
||||
if (part === null || part instanceof go.Link) return; // this tool never applies to Links
|
||||
if (part.isSelected && !this.diagram.isReadOnly) {
|
||||
const selelt = part.findObject(this.reshapeObjectName);
|
||||
if (selelt instanceof go.Shape && selelt.geometry !== null &&
|
||||
selelt.actualBounds.isReal() && selelt.isVisibleObject() &&
|
||||
part.canReshape() && part.actualBounds.isReal() && part.isVisible() &&
|
||||
selelt.geometry.type === go.Geometry.Path) {
|
||||
let adornment = part.findAdornment(this.name);
|
||||
if (adornment === null) {
|
||||
adornment = this.makeAdornment(selelt);
|
||||
}
|
||||
if (adornment !== null) {
|
||||
// update the position/alignment of each handle
|
||||
const geo = selelt.geometry;
|
||||
const b = geo.bounds;
|
||||
// update the size of the adornment
|
||||
const body = adornment.findObject('BODY');
|
||||
if (body !== null) body.desiredSize = b.size;
|
||||
adornment.elements.each(function(h: any) {
|
||||
if (h._typ === undefined) return;
|
||||
const fig = geo.figures.elt(h._fig);
|
||||
const seg = fig.segments.elt(h._seg);
|
||||
let x = 0;
|
||||
let 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
public makeAdornment(selelt: go.Shape): go.Adornment {
|
||||
const 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);
|
||||
let h: any = new go.Shape();
|
||||
h.name = 'BODY';
|
||||
h.fill = null;
|
||||
h.stroke = null;
|
||||
h.strokeWidth = 0;
|
||||
adornment.add(h);
|
||||
|
||||
const geo = selelt.geometry;
|
||||
if (geo !== null) {
|
||||
// requires Path Geometry, checked above in updateAdornments
|
||||
for (let f = 0; f < geo.figures.count; f++) {
|
||||
const fig = geo.figures.elt(f);
|
||||
for (let g = 0; g < fig.segments.count; g++) {
|
||||
const seg = fig.segments.elt(g);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
public makeHandle(selelt: go.GraphObject, fig: go.PathFigure, seg: go.PathSegment): go.GraphObject | null {
|
||||
const h = this.handleArchetype;
|
||||
if (h === null) return null;
|
||||
return h.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* This tool may run when there is a mouse-down event on a reshape handle.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!this.isEnabled) return false;
|
||||
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly) return false;
|
||||
if (!diagram.allowReshape) return false;
|
||||
if (!diagram.lastInput.left) return false;
|
||||
const h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
return (h !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start reshaping, if {@link #findToolHandleAt} finds a reshape handle at the mouse down point.
|
||||
*
|
||||
* If successful this sets {@link #handle} to be the reshape handle that it finds
|
||||
* and {@link #adornedShape} to be the {@link Shape} being reshaped.
|
||||
* It also remembers the original geometry in case this tool is cancelled.
|
||||
* And it starts a transaction.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
const diagram = this.diagram;
|
||||
this._handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
|
||||
if (this._handle === null) return;
|
||||
const shape = (this._handle.part as go.Adornment).adornedObject as go.Shape;
|
||||
if (!shape) return;
|
||||
this._adornedShape = shape;
|
||||
diagram.isMouseCaptured = true;
|
||||
this.startTransaction(this.name);
|
||||
this._originalGeometry = shape.geometry;
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This stops the current reshaping operation with the Shape as it is.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
this.stopTransaction();
|
||||
|
||||
this._handle = null;
|
||||
this._adornedShape = null;
|
||||
const diagram = this.diagram;
|
||||
diagram.isMouseCaptured = false;
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the shape to be the original geometry and stop this tool.
|
||||
*/
|
||||
public doCancel(): void {
|
||||
const shape = this._adornedShape;
|
||||
if (shape !== null) {
|
||||
// explicitly restore the original route, in case !UndoManager.isEnabled
|
||||
shape.geometry = this._originalGeometry;
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@link #reshape} with a new point determined by the mouse
|
||||
* to change the geometry of the {@link #adornedShape}.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeReshape(diagram.lastInput.documentPoint);
|
||||
this.reshape(newpt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reshape the Shape's geometry with a point based on the most recent mouse point by calling {@link #reshape},
|
||||
* and then stop this tool.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
const newpt = this.computeReshape(diagram.lastInput.documentPoint);
|
||||
this.reshape(newpt);
|
||||
this.transactionResult = this.name; // success
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the geometry of the {@link #adornedShape} by moving the point corresponding to the current
|
||||
* {@link #handle} to be at the given {@link Point}.
|
||||
* This is called by {@link #doMouseMove} and {@link #doMouseUp} with the result of calling
|
||||
* {@link #computeReshape} to constrain the input point.
|
||||
* @param {Point} newPoint the value of the call to {@link #computeReshape}.
|
||||
*/
|
||||
public reshape(newPoint: go.Point): void {
|
||||
const shape = this.adornedShape;
|
||||
if (shape === null || shape.geometry === null) return;
|
||||
const locpt = shape.getLocalPoint(newPoint);
|
||||
const geo = shape.geometry.copy();
|
||||
const type = (this.handle as any)._typ;
|
||||
if (type === undefined) return;
|
||||
const fig = geo.figures.elt((this.handle as any)._fig);
|
||||
const seg = fig.segments.elt((this.handle as any)._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;
|
||||
}
|
||||
const 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
|
||||
const part = shape.part; // move the Part holding the Shape
|
||||
if (part === null) return;
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called by {@link #doMouseMove} and {@link #doMouseUp} to limit the input point
|
||||
* before calling {@link #reshape}.
|
||||
* By default, this doesn't limit the input point.
|
||||
* @param {Point} p the point where the handle is being dragged.
|
||||
* @return {Point}
|
||||
*/
|
||||
public computeReshape(p: go.Point): go.Point {
|
||||
return p; // no constraints on the points
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Guided Dragging</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: A demonstration of the GuidedDraggingTool extension." />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">GuidedDraggingTool.ts</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { GuidedDraggingTool } from './GuidedDraggingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const myDiagram = $(go.Diagram, 'myDiagramDiv', // create a Diagram for the DIV HTML element
|
||||
{
|
||||
draggingTool: new GuidedDraggingTool(),
|
||||
'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' }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+462
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The GuidedDraggingTool class makes guidelines visible as the parts are dragged around a diagram
|
||||
* when the selected part is nearly aligned with another part.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/GuidedDragging.html">Guided Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class GuidedDraggingTool extends go.DraggingTool {
|
||||
/**
|
||||
* Constructs a GuidedDraggingTool and sets up the temporary guideline parts.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
// properties that the programmer can modify
|
||||
this._guidelineSnapDistance = 6;
|
||||
this._isGuidelineEnabled = true;
|
||||
this._horizontalGuidelineColor = 'gray';
|
||||
this._verticalGuidelineColor = 'gray';
|
||||
this._centerGuidelineColor = 'gray';
|
||||
this._guidelineWidth = 1;
|
||||
this._searchDistance = 1000;
|
||||
this._isGuidelineSnapEnabled = true;
|
||||
const partProperties = { layerName: 'Tool', isInDocumentBounds: false };
|
||||
const shapeProperties = { stroke: 'gray', isGeometryPositioned: true };
|
||||
const $ = go.GraphObject.make;
|
||||
// temporary parts for horizonal guidelines
|
||||
this.guidelineHtop =
|
||||
$(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
||||
this.guidelineHbottom =
|
||||
$(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
||||
this.guidelineHcenter =
|
||||
$(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
||||
// temporary parts for vertical guidelines
|
||||
this.guidelineVleft =
|
||||
$(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
||||
this.guidelineVright =
|
||||
$(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
||||
this.guidelineVcenter =
|
||||
$(go.Part, partProperties, $(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get guidelineSnapDistance() { return this._guidelineSnapDistance; }
|
||||
set guidelineSnapDistance(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.
|
||||
*/
|
||||
get isGuidelineEnabled() { return this._isGuidelineEnabled; }
|
||||
set isGuidelineEnabled(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".
|
||||
*/
|
||||
get horizontalGuidelineColor() { return this._horizontalGuidelineColor; }
|
||||
set horizontalGuidelineColor(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".
|
||||
*/
|
||||
get verticalGuidelineColor() { return this._verticalGuidelineColor; }
|
||||
set verticalGuidelineColor(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".
|
||||
*/
|
||||
get centerGuidelineColor() { return this._centerGuidelineColor; }
|
||||
set centerGuidelineColor(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.
|
||||
*/
|
||||
get guidelineWidth() { return this._guidelineWidth; }
|
||||
set guidelineWidth(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.
|
||||
*/
|
||||
get searchDistance() { return this._searchDistance; }
|
||||
set searchDistance(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.
|
||||
*/
|
||||
get isGuidelineSnapEnabled() { return this._isGuidelineSnapEnabled; }
|
||||
set isGuidelineSnapEnabled(val) {
|
||||
if (typeof val !== 'boolean')
|
||||
throw new Error('new value for GuidedDraggingTool.isGuidelineSnapEnabled must be a boolean.');
|
||||
if (this._isGuidelineSnapEnabled !== val) {
|
||||
this._isGuidelineSnapEnabled = val;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes all of the guidelines from the grid.
|
||||
*/
|
||||
clearGuidelines() {
|
||||
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 and removes the guidelines from the graph.
|
||||
*/
|
||||
doDeactivate() {
|
||||
super.doDeactivate();
|
||||
// clear any guidelines when dragging is done
|
||||
this.clearGuidelines();
|
||||
}
|
||||
/**
|
||||
* Shows vertical and horizontal guidelines for the dragged part.
|
||||
*/
|
||||
doDragOver(pt, obj) {
|
||||
// clear all existing guidelines in case either show... method decides to show a guideline
|
||||
this.clearGuidelines();
|
||||
// gets the selected part
|
||||
const draggingParts = this.copiedParts || this.draggedParts;
|
||||
if (draggingParts === null)
|
||||
return;
|
||||
const partItr = draggingParts.iterator;
|
||||
if (partItr.next()) {
|
||||
const 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.
|
||||
*/
|
||||
doDropOnto(pt, obj) {
|
||||
this.clearGuidelines();
|
||||
// gets the selected (perhaps copied) Part
|
||||
const draggingParts = this.copiedParts || this.draggedParts;
|
||||
if (draggingParts === null)
|
||||
return;
|
||||
const partItr = draggingParts.iterator;
|
||||
if (partItr.next()) {
|
||||
const part = partItr.key;
|
||||
// snaps only when the mouse is released without shift modifier
|
||||
const e = this.diagram.lastInput;
|
||||
const 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.
|
||||
*/
|
||||
invalidateLinks(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
|
||||
*/
|
||||
isGuiding(part, guidedpart) {
|
||||
return part instanceof go.Part &&
|
||||
!part.isSelected &&
|
||||
!(part instanceof go.Link) &&
|
||||
guidedpart instanceof go.Part &&
|
||||
part.containingGroup === guidedpart.containingGroup &&
|
||||
part.layer !== null && !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}.
|
||||
* @param {Node} part
|
||||
* @param {boolean} guideline if true, show guideline
|
||||
* @param {boolean} snap if true, snap the part to where the guideline would be
|
||||
*/
|
||||
showHorizontalMatches(part, guideline, snap) {
|
||||
const objBounds = part.locationObject.getDocumentBounds();
|
||||
const p0 = objBounds.y;
|
||||
const p1 = objBounds.y + objBounds.height / 2;
|
||||
const p2 = objBounds.y + objBounds.height;
|
||||
const marginOfError = this.guidelineSnapDistance;
|
||||
const distance = this.searchDistance;
|
||||
// compares with parts within narrow vertical area
|
||||
const area = objBounds.copy();
|
||||
area.inflate(distance, marginOfError + 1);
|
||||
const otherObjs = this.diagram.findObjectsIn(area, (obj) => obj.part, (p) => this.isGuiding(p, part), true);
|
||||
let bestDiff = marginOfError;
|
||||
let bestObj = null; // TS 2.6 won't let this be go.Part | null
|
||||
let bestSpot = go.Spot.Default;
|
||||
let bestOtherSpot = go.Spot.Default;
|
||||
// horizontal line -- comparing y-values
|
||||
otherObjs.each((other) => {
|
||||
if (other === part)
|
||||
return; // ignore itself
|
||||
const otherBounds = other.locationObject.getDocumentBounds();
|
||||
const q0 = otherBounds.y;
|
||||
const q1 = otherBounds.y + otherBounds.height / 2;
|
||||
const q2 = otherBounds.y + otherBounds.height;
|
||||
// compare center with center of OTHER part
|
||||
if (Math.abs(p1 - q1) < bestDiff) {
|
||||
bestDiff = Math.abs(p1 - q1);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Top;
|
||||
bestOtherSpot = go.Spot.Top;
|
||||
}
|
||||
else if (Math.abs(p0 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p0 - q2);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Bottom;
|
||||
bestOtherSpot = go.Spot.Top;
|
||||
}
|
||||
else if (Math.abs(p2 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p2 - q2);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Bottom;
|
||||
bestOtherSpot = go.Spot.Bottom;
|
||||
}
|
||||
});
|
||||
if (bestObj !== null) {
|
||||
const offsetX = objBounds.x - part.actualBounds.x;
|
||||
const offsetY = objBounds.y - part.actualBounds.y;
|
||||
const bestBounds = bestObj.locationObject.getDocumentBounds();
|
||||
// line extends from x0 to x2
|
||||
const x0 = Math.min(objBounds.x, bestBounds.x) - 10;
|
||||
const x2 = Math.max(objBounds.x + objBounds.width, bestBounds.x + bestBounds.width) + 10;
|
||||
// find bestObj's desired Y
|
||||
const 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}.
|
||||
* @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
|
||||
*/
|
||||
showVerticalMatches(part, guideline, snap) {
|
||||
const objBounds = part.locationObject.getDocumentBounds();
|
||||
const p0 = objBounds.x;
|
||||
const p1 = objBounds.x + objBounds.width / 2;
|
||||
const p2 = objBounds.x + objBounds.width;
|
||||
const marginOfError = this.guidelineSnapDistance;
|
||||
const distance = this.searchDistance;
|
||||
// compares with parts within narrow vertical area
|
||||
const area = objBounds.copy();
|
||||
area.inflate(marginOfError + 1, distance);
|
||||
const otherObjs = this.diagram.findObjectsIn(area, (obj) => obj.part, (p) => this.isGuiding(p, part), true);
|
||||
let bestDiff = marginOfError;
|
||||
let bestObj = null; // TS 2.6 won't let this be go.Part | null
|
||||
let bestSpot = go.Spot.Default;
|
||||
let bestOtherSpot = go.Spot.Default;
|
||||
// vertical line -- comparing x-values
|
||||
otherObjs.each((other) => {
|
||||
if (other === part)
|
||||
return; // ignore itself
|
||||
const otherBounds = other.locationObject.getDocumentBounds();
|
||||
const q0 = otherBounds.x;
|
||||
const q1 = otherBounds.x + otherBounds.width / 2;
|
||||
const q2 = otherBounds.x + otherBounds.width;
|
||||
// compare center with center of OTHER part
|
||||
if (Math.abs(p1 - q1) < bestDiff) {
|
||||
bestDiff = Math.abs(p1 - q1);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Left;
|
||||
bestOtherSpot = go.Spot.Left;
|
||||
}
|
||||
else if (Math.abs(p0 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p0 - q2);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Right;
|
||||
bestOtherSpot = go.Spot.Left;
|
||||
}
|
||||
else if (Math.abs(p2 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p2 - q2);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Right;
|
||||
bestOtherSpot = go.Spot.Right;
|
||||
}
|
||||
});
|
||||
if (bestObj !== null) {
|
||||
const offsetX = objBounds.x - part.actualBounds.x;
|
||||
const offsetY = objBounds.y - part.actualBounds.y;
|
||||
const bestBounds = bestObj.locationObject.getDocumentBounds();
|
||||
// line extends from y0 to y2
|
||||
const y0 = Math.min(objBounds.y, bestBounds.y) - 10;
|
||||
const y2 = Math.max(objBounds.y + objBounds.height, bestBounds.y + bestBounds.height) + 10;
|
||||
// find bestObj's desired X
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The GuidedDraggingTool class makes guidelines visible as the parts are dragged around a diagram
|
||||
* when the selected part is nearly aligned with another part.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/GuidedDragging.html">Guided Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class GuidedDraggingTool extends go.DraggingTool {
|
||||
// horizontal guidelines
|
||||
private guidelineHtop: go.Part;
|
||||
private guidelineHbottom: go.Part;
|
||||
private guidelineHcenter: go.Part;
|
||||
// vertical guidelines
|
||||
private guidelineVleft: go.Part;
|
||||
private guidelineVright: go.Part;
|
||||
private guidelineVcenter: go.Part;
|
||||
|
||||
// properties that the programmer can modify
|
||||
private _guidelineSnapDistance: number = 6;
|
||||
private _isGuidelineEnabled: boolean = true;
|
||||
private _horizontalGuidelineColor: string = 'gray';
|
||||
private _verticalGuidelineColor: string = 'gray';
|
||||
private _centerGuidelineColor: string = 'gray';
|
||||
private _guidelineWidth: number = 1;
|
||||
private _searchDistance: number = 1000;
|
||||
private _isGuidelineSnapEnabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Constructs a GuidedDraggingTool and sets up the temporary guideline parts.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const partProperties = { layerName: 'Tool', isInDocumentBounds: false };
|
||||
const shapeProperties = { stroke: 'gray', isGeometryPositioned: true };
|
||||
|
||||
const $ = go.GraphObject.make;
|
||||
// temporary parts for horizonal guidelines
|
||||
this.guidelineHtop =
|
||||
$(go.Part, partProperties,
|
||||
$(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
||||
this.guidelineHbottom =
|
||||
$(go.Part, partProperties,
|
||||
$(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
||||
this.guidelineHcenter =
|
||||
$(go.Part, partProperties,
|
||||
$(go.Shape, shapeProperties, { geometryString: 'M0 0 100 0' }));
|
||||
// temporary parts for vertical guidelines
|
||||
this.guidelineVleft =
|
||||
$(go.Part, partProperties,
|
||||
$(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
||||
this.guidelineVright =
|
||||
$(go.Part, partProperties,
|
||||
$(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
||||
this.guidelineVcenter =
|
||||
$(go.Part, partProperties,
|
||||
$(go.Shape, shapeProperties, { geometryString: 'M0 0 0 100' }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get guidelineSnapDistance(): number { return this._guidelineSnapDistance; }
|
||||
set guidelineSnapDistance(val: number) {
|
||||
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.
|
||||
*/
|
||||
get isGuidelineEnabled(): boolean { return this._isGuidelineEnabled; }
|
||||
set isGuidelineEnabled(val: boolean) {
|
||||
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".
|
||||
*/
|
||||
get horizontalGuidelineColor(): string { return this._horizontalGuidelineColor; }
|
||||
set horizontalGuidelineColor(val: string) {
|
||||
if (this._horizontalGuidelineColor !== val) {
|
||||
this._horizontalGuidelineColor = val;
|
||||
(this.guidelineHbottom.elements.first() as go.Shape).stroke = this._horizontalGuidelineColor;
|
||||
(this.guidelineHtop.elements.first() as go.Shape).stroke = this._horizontalGuidelineColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the color of vertical guidelines.
|
||||
*
|
||||
* The default value is "gray".
|
||||
*/
|
||||
get verticalGuidelineColor(): string { return this._verticalGuidelineColor; }
|
||||
set verticalGuidelineColor(val: string) {
|
||||
if (this._verticalGuidelineColor !== val) {
|
||||
this._verticalGuidelineColor = val;
|
||||
(this.guidelineVleft.elements.first() as go.Shape).stroke = this._verticalGuidelineColor;
|
||||
(this.guidelineVright.elements.first() as go.Shape).stroke = this._verticalGuidelineColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the color of center guidelines.
|
||||
*
|
||||
* The default value is "gray".
|
||||
*/
|
||||
get centerGuidelineColor(): string { return this._centerGuidelineColor; }
|
||||
set centerGuidelineColor(val: string) {
|
||||
if (this._centerGuidelineColor !== val) {
|
||||
this._centerGuidelineColor = val;
|
||||
(this.guidelineVcenter.elements.first() as go.Shape).stroke = this._centerGuidelineColor;
|
||||
(this.guidelineHcenter.elements.first() as go.Shape).stroke = this._centerGuidelineColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the width guidelines.
|
||||
*
|
||||
* The default value is 1.
|
||||
*/
|
||||
get guidelineWidth(): number { return this._guidelineWidth; }
|
||||
set guidelineWidth(val: number) {
|
||||
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() as go.Shape).strokeWidth = val;
|
||||
(this.guidelineHcenter.elements.first() as go.Shape).strokeWidth = val;
|
||||
(this.guidelineVleft.elements.first() as go.Shape).strokeWidth = val;
|
||||
(this.guidelineVright.elements.first() as go.Shape).strokeWidth = val;
|
||||
(this.guidelineHbottom.elements.first() as go.Shape).strokeWidth = val;
|
||||
(this.guidelineHtop.elements.first() as go.Shape).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.
|
||||
*/
|
||||
get searchDistance(): number { return this._searchDistance; }
|
||||
set searchDistance(val: number) {
|
||||
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.
|
||||
*/
|
||||
get isGuidelineSnapEnabled(): boolean { return this._isGuidelineSnapEnabled; }
|
||||
set isGuidelineSnapEnabled(val: boolean) {
|
||||
if (typeof val !== 'boolean') throw new Error('new value for GuidedDraggingTool.isGuidelineSnapEnabled must be a boolean.');
|
||||
if (this._isGuidelineSnapEnabled !== val) {
|
||||
this._isGuidelineSnapEnabled = val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all of the guidelines from the grid.
|
||||
*/
|
||||
public clearGuidelines(): void {
|
||||
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 and removes the guidelines from the graph.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
super.doDeactivate();
|
||||
// clear any guidelines when dragging is done
|
||||
this.clearGuidelines();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows vertical and horizontal guidelines for the dragged part.
|
||||
*/
|
||||
public doDragOver(pt: go.Point, obj: go.GraphObject): void {
|
||||
// clear all existing guidelines in case either show... method decides to show a guideline
|
||||
this.clearGuidelines();
|
||||
|
||||
// gets the selected part
|
||||
const draggingParts = this.copiedParts || this.draggedParts;
|
||||
if (draggingParts === null) return;
|
||||
const partItr = draggingParts.iterator;
|
||||
if (partItr.next()) {
|
||||
const 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.
|
||||
*/
|
||||
public doDropOnto(pt: go.Point, obj: go.GraphObject): void {
|
||||
this.clearGuidelines();
|
||||
|
||||
// gets the selected (perhaps copied) Part
|
||||
const draggingParts = this.copiedParts || this.draggedParts;
|
||||
if (draggingParts === null) return;
|
||||
const partItr = draggingParts.iterator;
|
||||
if (partItr.next()) {
|
||||
const part = partItr.key;
|
||||
|
||||
// snaps only when the mouse is released without shift modifier
|
||||
const e = this.diagram.lastInput;
|
||||
const 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.
|
||||
*/
|
||||
public invalidateLinks(node: go.Part): void {
|
||||
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
|
||||
*/
|
||||
protected isGuiding(part: go.Part, guidedpart: go.Part): boolean {
|
||||
return part instanceof go.Part &&
|
||||
!part.isSelected &&
|
||||
!(part instanceof go.Link) &&
|
||||
guidedpart instanceof go.Part &&
|
||||
part.containingGroup === guidedpart.containingGroup &&
|
||||
part.layer !== null && !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}.
|
||||
* @param {Node} part
|
||||
* @param {boolean} guideline if true, show guideline
|
||||
* @param {boolean} snap if true, snap the part to where the guideline would be
|
||||
*/
|
||||
public showHorizontalMatches(part: go.Part, guideline: boolean, snap: boolean): void {
|
||||
const objBounds = part.locationObject.getDocumentBounds();
|
||||
const p0 = objBounds.y;
|
||||
const p1 = objBounds.y + objBounds.height / 2;
|
||||
const p2 = objBounds.y + objBounds.height;
|
||||
|
||||
const marginOfError = this.guidelineSnapDistance;
|
||||
const distance = this.searchDistance;
|
||||
// compares with parts within narrow vertical area
|
||||
const area = objBounds.copy();
|
||||
area.inflate(distance, marginOfError + 1);
|
||||
const otherObjs = this.diagram.findObjectsIn(area,
|
||||
(obj) => obj.part as go.Part,
|
||||
(p) => this.isGuiding(p as go.Part, part),
|
||||
true) as go.Set<go.Part>;
|
||||
|
||||
let bestDiff: number = marginOfError;
|
||||
let bestObj: any = null; // TS 2.6 won't let this be go.Part | null
|
||||
let bestSpot: go.Spot = go.Spot.Default;
|
||||
let bestOtherSpot: go.Spot = go.Spot.Default;
|
||||
// horizontal line -- comparing y-values
|
||||
otherObjs.each((other) => {
|
||||
if (other === part) return; // ignore itself
|
||||
|
||||
const otherBounds = other.locationObject.getDocumentBounds();
|
||||
const q0 = otherBounds.y;
|
||||
const q1 = otherBounds.y + otherBounds.height / 2;
|
||||
const q2 = otherBounds.y + otherBounds.height;
|
||||
|
||||
// compare center with center of OTHER part
|
||||
if (Math.abs(p1 - q1) < bestDiff) {
|
||||
bestDiff = Math.abs(p1 - q1);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Top;
|
||||
bestOtherSpot = go.Spot.Top;
|
||||
} else if (Math.abs(p0 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p0 - q2);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Bottom;
|
||||
bestOtherSpot = go.Spot.Top;
|
||||
} else if (Math.abs(p2 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p2 - q2);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Bottom;
|
||||
bestOtherSpot = go.Spot.Bottom;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestObj !== null) {
|
||||
const offsetX = objBounds.x - part.actualBounds.x;
|
||||
const offsetY = objBounds.y - part.actualBounds.y;
|
||||
const bestBounds = bestObj.locationObject.getDocumentBounds();
|
||||
// line extends from x0 to x2
|
||||
const x0 = Math.min(objBounds.x, bestBounds.x) - 10;
|
||||
const x2 = Math.max(objBounds.x + objBounds.width, bestBounds.x + bestBounds.width) + 10;
|
||||
// find bestObj's desired Y
|
||||
const 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}.
|
||||
* @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
|
||||
*/
|
||||
public showVerticalMatches(part: go.Part, guideline: boolean, snap: boolean): void {
|
||||
const objBounds = part.locationObject.getDocumentBounds();
|
||||
const p0 = objBounds.x;
|
||||
const p1 = objBounds.x + objBounds.width / 2;
|
||||
const p2 = objBounds.x + objBounds.width;
|
||||
|
||||
const marginOfError = this.guidelineSnapDistance;
|
||||
const distance = this.searchDistance;
|
||||
// compares with parts within narrow vertical area
|
||||
const area = objBounds.copy();
|
||||
area.inflate(marginOfError + 1, distance);
|
||||
const otherObjs = this.diagram.findObjectsIn(area,
|
||||
(obj) => obj.part as go.Part,
|
||||
(p) => this.isGuiding(p as go.Part, part),
|
||||
true) as go.Set<go.Part>;
|
||||
|
||||
let bestDiff: number = marginOfError;
|
||||
let bestObj: any = null; // TS 2.6 won't let this be go.Part | null
|
||||
let bestSpot: go.Spot = go.Spot.Default;
|
||||
let bestOtherSpot: go.Spot = go.Spot.Default;
|
||||
// vertical line -- comparing x-values
|
||||
otherObjs.each((other) => {
|
||||
if (other === part) return; // ignore itself
|
||||
|
||||
const otherBounds = other.locationObject.getDocumentBounds();
|
||||
const q0 = otherBounds.x;
|
||||
const q1 = otherBounds.x + otherBounds.width / 2;
|
||||
const q2 = otherBounds.x + otherBounds.width;
|
||||
|
||||
// compare center with center of OTHER part
|
||||
if (Math.abs(p1 - q1) < bestDiff) {
|
||||
bestDiff = Math.abs(p1 - q1);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Left;
|
||||
bestOtherSpot = go.Spot.Left;
|
||||
} else if (Math.abs(p0 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p0 - q2);
|
||||
bestObj = 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);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Right;
|
||||
bestOtherSpot = go.Spot.Left;
|
||||
} else if (Math.abs(p2 - q2) < bestDiff) {
|
||||
bestDiff = Math.abs(p2 - q2);
|
||||
bestObj = other;
|
||||
bestSpot = go.Spot.Right;
|
||||
bestOtherSpot = go.Spot.Right;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestObj !== null) {
|
||||
const offsetX = objBounds.x - part.actualBounds.x;
|
||||
const offsetY = objBounds.y - part.actualBounds.y;
|
||||
const bestBounds = bestObj.locationObject.getDocumentBounds();
|
||||
// line extends from y0 to y2
|
||||
const y0 = Math.min(objBounds.y, bestBounds.y) - 10;
|
||||
const y2 = Math.max(objBounds.y + objBounds.height, bestBounds.y + bestBounds.height) + 10;
|
||||
// find bestObj's desired X
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Demo of HyperlinkText Builder</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">HyperlinkText.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import "./HyperlinkText.js";
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const myDiagram = $(go.Diagram, 'myDiagramDiv');
|
||||
|
||||
myDiagram.nodeTemplate =
|
||||
$(go.Node, 'Auto',
|
||||
$(go.Shape, 'Ellipse', { fill: 'lightskyblue' }),
|
||||
$('HyperlinkText',
|
||||
(node) => 'https://gojs.net/' + node.data.version,
|
||||
(node) => 'Visit GoJS ' + node.data.version,
|
||||
{ margin: 10 })
|
||||
);
|
||||
|
||||
myDiagram.model = new go.GraphLinksModel([
|
||||
{ key: 1, version: 'beta' },
|
||||
{ key: 2, version: 'latest' }
|
||||
], [
|
||||
{ from: 1, to: 2 }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
// 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', (args) => {
|
||||
// the URL is required as the first argument, either a string or a side-effect-free function returning a string
|
||||
const url = go.GraphObject.takeBuilderArgument(args, undefined, (x) => 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
|
||||
let text = go.GraphObject.takeBuilderArgument(args, null, (x) => typeof x === 'string' || typeof x === 'function');
|
||||
// see if the visual tree is supplied in the arguments to the "HyperlinkText"
|
||||
let anyGraphObjects = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a && a instanceof go.GraphObject)
|
||||
anyGraphObjects = true;
|
||||
}
|
||||
// define the click behavior
|
||||
const click = (e, obj) => {
|
||||
let u = obj._url;
|
||||
if (typeof u === 'function')
|
||||
u = u(obj.findTemplateBinder());
|
||||
if (u)
|
||||
window.open(u, '_blank');
|
||||
};
|
||||
// define the tooltip
|
||||
const 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;
|
||||
let 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;
|
||||
const tb = go.GraphObject.make(go.TextBlock, {
|
||||
'_url': url,
|
||||
cursor: 'pointer',
|
||||
mouseEnter: function (e, obj) {
|
||||
let u = obj._url;
|
||||
if (typeof u === 'function')
|
||||
u = u(obj.findTemplateBinder());
|
||||
if (u && obj instanceof go.TextBlock)
|
||||
obj.isUnderline = true;
|
||||
},
|
||||
mouseLeave: (e, obj) => { if (obj instanceof go.TextBlock)
|
||||
obj.isUnderline = false; },
|
||||
click: click,
|
||||
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 {
|
||||
const findTextBlock = function (obj) {
|
||||
if (obj instanceof go.TextBlock)
|
||||
return obj;
|
||||
if (obj instanceof go.Panel) {
|
||||
const it = obj.elements;
|
||||
while (it.next()) {
|
||||
const result = findTextBlock(it.value);
|
||||
if (result !== null)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return go.GraphObject.make(go.Panel, {
|
||||
'_url': url,
|
||||
cursor: 'pointer',
|
||||
mouseEnter: (e, panel) => {
|
||||
const tb = findTextBlock(panel);
|
||||
let u = panel._url;
|
||||
if (typeof u === 'function')
|
||||
u = u(panel.findTemplateBinder());
|
||||
if (tb !== null && u)
|
||||
tb.isUnderline = true;
|
||||
},
|
||||
mouseLeave: (e, panel) => {
|
||||
const tb = findTextBlock(panel);
|
||||
if (tb !== null)
|
||||
tb.isUnderline = false;
|
||||
},
|
||||
click: click,
|
||||
toolTip: tooltip // shared by all HyperlinkText panels
|
||||
});
|
||||
}
|
||||
});
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
// 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', (args) => {
|
||||
// the URL is required as the first argument, either a string or a side-effect-free function returning a string
|
||||
const url = go.GraphObject.takeBuilderArgument(args, undefined, (x) => 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
|
||||
let text = go.GraphObject.takeBuilderArgument(args, null, (x) => typeof x === 'string' || typeof x === 'function');
|
||||
|
||||
// see if the visual tree is supplied in the arguments to the "HyperlinkText"
|
||||
let anyGraphObjects = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a && a instanceof go.GraphObject) anyGraphObjects = true;
|
||||
}
|
||||
|
||||
// define the click behavior
|
||||
const click =
|
||||
(e: go.InputEvent, obj: go.GraphObject) => {
|
||||
let u = (obj as any)._url;
|
||||
if (typeof u === 'function') u = u(obj.findTemplateBinder());
|
||||
if (u) window.open(u, '_blank');
|
||||
};
|
||||
|
||||
// define the tooltip
|
||||
const tooltip =
|
||||
go.GraphObject.make<go.Adornment>('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;
|
||||
let 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;
|
||||
const tb = go.GraphObject.make(go.TextBlock,
|
||||
{
|
||||
'_url': url,
|
||||
cursor: 'pointer',
|
||||
mouseEnter: function(e: go.InputEvent, obj: go.GraphObject) {
|
||||
let u = (obj as any)._url;
|
||||
if (typeof u === 'function') u = u(obj.findTemplateBinder());
|
||||
if (u && obj instanceof go.TextBlock) obj.isUnderline = true;
|
||||
},
|
||||
mouseLeave: (e: go.InputEvent, obj: go.GraphObject) => { if (obj instanceof go.TextBlock) 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 {
|
||||
const findTextBlock = function(obj: go.GraphObject): go.TextBlock | null {
|
||||
if (obj instanceof go.TextBlock) return obj;
|
||||
if (obj instanceof go.Panel) {
|
||||
const it = obj.elements;
|
||||
while (it.next()) {
|
||||
const result = findTextBlock(it.value);
|
||||
if (result !== null) return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return go.GraphObject.make(go.Panel,
|
||||
{
|
||||
'_url': url,
|
||||
cursor: 'pointer',
|
||||
mouseEnter: (e: go.InputEvent, panel: go.GraphObject) => {
|
||||
const tb = findTextBlock(panel);
|
||||
let u = (panel as any)._url;
|
||||
if (typeof u === 'function') u = u(panel.findTemplateBinder());
|
||||
if (tb !== null && u) tb.isUnderline = true;
|
||||
},
|
||||
mouseLeave: (e: go.InputEvent, panel: go.GraphObject) => {
|
||||
const tb = findTextBlock(panel);
|
||||
if (tb !== null) tb.isUnderline = false;
|
||||
},
|
||||
click: click, // defined above
|
||||
toolTip: tooltip // shared by all HyperlinkText panels
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
// 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>
|
||||
*/
|
||||
const contextMenuDIV = document.createElement('div');
|
||||
contextMenuDIV.id = 'contextMenuDIV';
|
||||
// This is the actual HTML LightBox-style context menu, composed of buttons and a background:
|
||||
const cmLight = document.createElement('div');
|
||||
cmLight.id = 'cmLight';
|
||||
cmLight.className = 'goCXforeground';
|
||||
const cmDark = document.createElement('div');
|
||||
cmDark.id = 'cmDark';
|
||||
cmDark.className = 'goCXbackground';
|
||||
contextMenuDIV.appendChild(cmLight);
|
||||
contextMenuDIV.appendChild(cmDark);
|
||||
const cxMenuButtons = [
|
||||
{
|
||||
text: 'Copy',
|
||||
command: (diagram) => { diagram.commandHandler.copySelection(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canCopySelection()
|
||||
}, {
|
||||
text: 'Cut',
|
||||
command: (diagram) => { diagram.commandHandler.cutSelection(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canCutSelection()
|
||||
}, {
|
||||
text: 'Delete',
|
||||
command: (diagram) => { diagram.commandHandler.deleteSelection(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canDeleteSelection()
|
||||
}, {
|
||||
text: 'Paste',
|
||||
command: (diagram) => { diagram.commandHandler.pasteSelection(diagram.toolManager.contextMenuTool.mouseDownPoint); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canPasteSelection(diagram.toolManager.contextMenuTool.mouseDownPoint)
|
||||
}, {
|
||||
text: 'Select All',
|
||||
command: (diagram) => { diagram.commandHandler.selectAll(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canSelectAll()
|
||||
}, {
|
||||
text: 'Undo',
|
||||
command: (diagram) => { diagram.commandHandler.undo(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canUndo()
|
||||
}, {
|
||||
text: 'Redo',
|
||||
command: (diagram) => { diagram.commandHandler.redo(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canRedo()
|
||||
}, {
|
||||
text: 'Scroll To Part',
|
||||
command: (diagram) => { diagram.commandHandler.scrollToPart(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canScrollToPart()
|
||||
}, {
|
||||
text: 'Zoom To Fit',
|
||||
command: (diagram) => { diagram.commandHandler.zoomToFit(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canZoomToFit()
|
||||
}, {
|
||||
text: 'Reset Zoom',
|
||||
command: (diagram) => { diagram.commandHandler.resetZoom(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canResetZoom()
|
||||
}, {
|
||||
text: 'Group Selection',
|
||||
command: (diagram) => { diagram.commandHandler.groupSelection(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canGroupSelection()
|
||||
}, {
|
||||
text: 'Ungroup Selection',
|
||||
command: (diagram) => { diagram.commandHandler.ungroupSelection(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canUngroupSelection()
|
||||
}, {
|
||||
text: 'Edit Text',
|
||||
command: (diagram) => { diagram.commandHandler.editTextBlock(); },
|
||||
isVisible: (diagram) => diagram.commandHandler.canEditTextBlock()
|
||||
}
|
||||
];
|
||||
const $ = go.GraphObject.make;
|
||||
const myContextMenu = $(go.HTMLInfo, {
|
||||
show: showContextMenu,
|
||||
hide: hideContextMenu
|
||||
});
|
||||
let 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', (e) => { e.preventDefault(); return false; }, false);
|
||||
cmLight.addEventListener('selectstart', (e) => { e.preventDefault(); return false; }, false);
|
||||
contextMenuDIV.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; }, false);
|
||||
// Stop the context menu tool if you click on the dark part:
|
||||
contextMenuDIV.addEventListener('click', (e) => { diagram.currentTool.stopTool(); return false; }, false);
|
||||
firstTime = false;
|
||||
}
|
||||
// Empty the context menu and only show buttons that are relevant
|
||||
cmLight.innerHTML = '';
|
||||
const ul = document.createElement('ul');
|
||||
cmLight.appendChild(ul);
|
||||
for (let i = 0; i < cxMenuButtons.length; i++) {
|
||||
const button = cxMenuButtons[i];
|
||||
const command = button.command;
|
||||
const isVisible = button.isVisible;
|
||||
if (!(typeof command === 'function'))
|
||||
continue;
|
||||
// Only show buttons that have isVisible = true
|
||||
if (typeof isVisible === 'function' && !isVisible(diagram))
|
||||
continue;
|
||||
const li = document.createElement('li');
|
||||
const ahref = document.createElement('a');
|
||||
ahref.href = '#';
|
||||
ahref._command = button.command;
|
||||
ahref.addEventListener('click', (e) => {
|
||||
ahref._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);
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
// 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>
|
||||
*/
|
||||
const contextMenuDIV = document.createElement('div');
|
||||
contextMenuDIV.id = 'contextMenuDIV';
|
||||
// This is the actual HTML LightBox-style context menu, composed of buttons and a background:
|
||||
const cmLight = document.createElement('div');
|
||||
cmLight.id = 'cmLight';
|
||||
cmLight.className = 'goCXforeground';
|
||||
const cmDark = document.createElement('div');
|
||||
cmDark.id = 'cmDark';
|
||||
cmDark.className = 'goCXbackground';
|
||||
contextMenuDIV.appendChild(cmLight);
|
||||
contextMenuDIV.appendChild(cmDark);
|
||||
|
||||
const cxMenuButtons = [
|
||||
{
|
||||
text: 'Copy',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.copySelection(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canCopySelection()
|
||||
}, {
|
||||
text: 'Cut',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.cutSelection(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canCutSelection()
|
||||
}, {
|
||||
text: 'Delete',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.deleteSelection(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canDeleteSelection()
|
||||
}, {
|
||||
text: 'Paste',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.pasteSelection(diagram.toolManager.contextMenuTool.mouseDownPoint); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canPasteSelection(diagram.toolManager.contextMenuTool.mouseDownPoint)
|
||||
}, {
|
||||
text: 'Select All',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.selectAll(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canSelectAll()
|
||||
}, {
|
||||
text: 'Undo',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.undo(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canUndo()
|
||||
}, {
|
||||
text: 'Redo',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.redo(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canRedo()
|
||||
}, {
|
||||
text: 'Scroll To Part',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.scrollToPart(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canScrollToPart()
|
||||
}, {
|
||||
text: 'Zoom To Fit',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.zoomToFit(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canZoomToFit()
|
||||
}, {
|
||||
text: 'Reset Zoom',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.resetZoom(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canResetZoom()
|
||||
}, {
|
||||
text: 'Group Selection',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.groupSelection(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canGroupSelection()
|
||||
}, {
|
||||
text: 'Ungroup Selection',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.ungroupSelection(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canUngroupSelection()
|
||||
}, {
|
||||
text: 'Edit Text',
|
||||
command: (diagram: go.Diagram) => { diagram.commandHandler.editTextBlock(); },
|
||||
isVisible: (diagram: go.Diagram) => diagram.commandHandler.canEditTextBlock()
|
||||
}
|
||||
];
|
||||
|
||||
const $ = go.GraphObject.make;
|
||||
const myContextMenu = $(go.HTMLInfo, {
|
||||
show: showContextMenu,
|
||||
hide: hideContextMenu
|
||||
});
|
||||
|
||||
let firstTime = true;
|
||||
|
||||
function showContextMenu(obj: go.GraphObject, diagram: go.Diagram, tool: go.Tool) {
|
||||
if (firstTime) {
|
||||
// We don't want the div acting as a context menu to have a (browser) context menu!
|
||||
cmLight.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; }, false);
|
||||
cmLight.addEventListener('selectstart', (e) => { e.preventDefault(); return false; }, false);
|
||||
contextMenuDIV.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; }, false);
|
||||
// Stop the context menu tool if you click on the dark part:
|
||||
contextMenuDIV.addEventListener('click', (e) => { diagram.currentTool.stopTool(); return false; }, false);
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
// Empty the context menu and only show buttons that are relevant
|
||||
cmLight.innerHTML = '';
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
cmLight.appendChild(ul);
|
||||
|
||||
for (let i = 0; i < cxMenuButtons.length; i++) {
|
||||
const button = cxMenuButtons[i];
|
||||
const command = button.command;
|
||||
const isVisible = button.isVisible;
|
||||
|
||||
if (!(typeof command === 'function')) continue;
|
||||
// Only show buttons that have isVisible = true
|
||||
if (typeof isVisible === 'function' && !isVisible(diagram)) continue;
|
||||
const li = document.createElement('li');
|
||||
const ahref = document.createElement('a');
|
||||
ahref.href = '#';
|
||||
(ahref as any)._command = button.command;
|
||||
ahref.addEventListener('click', (e) => {
|
||||
(ahref as any)._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: go.Diagram, tool: go.Tool) {
|
||||
document.body.removeChild(contextMenuDIV);
|
||||
}
|
||||
|
||||
(window as any).myHTMLLightBox = myContextMenu;
|
||||
})(window);
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>State Chart with Draggable Link Labels</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">LinkLabelDraggingTool.ts</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">Save</button>
|
||||
<button id="LoadButton">Load</button> Diagram Model saved in JSON format:
|
||||
<br />
|
||||
<textarea id="mySavedModel" style="width:100%;height:300px">
|
||||
{ "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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { LinkLabelDraggingTool } from './LinkLabelDraggingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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', (e) => {
|
||||
const button = document.getElementById('SaveButton');
|
||||
if (button) button.disabled = !myDiagram.isModified;
|
||||
|
||||
const idx = document.title.indexOf('*');
|
||||
if (myDiagram.isModified) {
|
||||
if (idx < 0) document.title += '*';
|
||||
} else {
|
||||
if (idx >= 0) document.title = document.title.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,
|
||||
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) {
|
||||
const adorn = obj.part;
|
||||
const fromNode = adorn.adornedPart;
|
||||
if (fromNode === null) return;
|
||||
e.handled = true;
|
||||
const diagram = e.diagram;
|
||||
diagram.startTransaction('Add State');
|
||||
// get the node data for which the user clicked the button
|
||||
const fromData = fromNode.data;
|
||||
// create a new "State" data object, positioned off to the right of the adorned Node
|
||||
const toData = { text: 'new' };
|
||||
const 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
|
||||
const model = diagram.model;
|
||||
model.addNodeData(toData);
|
||||
// create a link data from the old node data to the new node data
|
||||
const linkdata = {
|
||||
from: model.getKeyForNodeData(fromData),
|
||||
to: model.getKeyForNodeData(toData),
|
||||
text: 'transition'
|
||||
};
|
||||
// and add the link data to the model
|
||||
model.addLinkData(linkdata);
|
||||
// select the new Node
|
||||
const 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
|
||||
if (newnode !== null) 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();
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
|
||||
// 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);
|
||||
}
|
||||
document.getElementById("SaveButton").onclick = save;
|
||||
document.getElementById("LoadButton").onclick = load;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The LinkLabelDraggingTool class lets the user move a label on a {@link Link}.
|
||||
*
|
||||
* This tool only works when the Link has a label
|
||||
* that is positioned at the {@link Link#midPoint} plus some offset.
|
||||
* It does not work for labels that have a particular {@link GraphObject#segmentIndex}.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LinkLabelDragging.html">Link Label Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class LinkLabelDraggingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a LinkLabelDraggingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* The label being dragged.
|
||||
*/
|
||||
this.label = null;
|
||||
this._offset = new go.Point(); // of the mouse relative to the center of the label object
|
||||
this._originalOffset = null;
|
||||
this.name = 'LinkLabelDragging';
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
* @return {GraphObject} This returns null if no such label is at the mouse down point.
|
||||
*/
|
||||
findLabel() {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
let elt = diagram.findObjectAt(e.documentPoint, null, null);
|
||||
if (elt === null || !(elt.part instanceof go.Link))
|
||||
return null;
|
||||
while (elt !== null && elt.panel !== elt.part) {
|
||||
elt = elt.panel;
|
||||
}
|
||||
// If it's at an arrowhead segment index, don't consider it a label:
|
||||
if (elt !== null && (elt.segmentIndex === 0 || elt.segmentIndex === -1))
|
||||
return null;
|
||||
return elt;
|
||||
}
|
||||
/**
|
||||
* 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 {@link #findLabel}.
|
||||
*/
|
||||
canStart() {
|
||||
if (!super.canStart())
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left)
|
||||
return false;
|
||||
if (!this.isBeyondDragSize())
|
||||
return false;
|
||||
return this.findLabel() !== null;
|
||||
}
|
||||
/**
|
||||
* Start a transaction, call {@link #findLabel} and remember it as the "label" property,
|
||||
* and remember the original value for the label's {@link GraphObject#segmentOffset} property.
|
||||
*/
|
||||
doActivate() {
|
||||
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();
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
doDeactivate() {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
/**
|
||||
* Clear any reference to a label element.
|
||||
*/
|
||||
doStop() {
|
||||
this.label = null;
|
||||
super.doStop();
|
||||
}
|
||||
/**
|
||||
* Restore the label's original value for {@link GraphObject#segmentOffset}.
|
||||
*/
|
||||
doCancel() {
|
||||
if (this.label !== null && this._originalOffset !== null) {
|
||||
this.label.segmentOffset = this._originalOffset;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
/**
|
||||
* During the drag, call {@link #updateSegmentOffset} in order to set
|
||||
* the {@link GraphObject#segmentOffset} of the label.
|
||||
*/
|
||||
doMouseMove() {
|
||||
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.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (!this.isActive)
|
||||
return;
|
||||
this.updateSegmentOffset();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Save the label's {@link GraphObject#segmentOffset} as a rotated offset from the midpoint of the
|
||||
* Link that the label is in.
|
||||
*/
|
||||
updateSegmentOffset() {
|
||||
const lab = this.label;
|
||||
if (lab === null)
|
||||
return;
|
||||
const link = lab.part;
|
||||
if (!(link instanceof go.Link))
|
||||
return;
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
const idx = lab.segmentIndex;
|
||||
const 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) {
|
||||
const mid = link.midPoint;
|
||||
// need to rotate this point to account for the angle of the link segment at the mid-point
|
||||
const 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
|
||||
const frac = lab.segmentFraction;
|
||||
let a;
|
||||
let 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
|
||||
const i = numpts + idx;
|
||||
a = link.getPoint(i);
|
||||
b = (i > 0) ? link.getPoint(i - 1) : a;
|
||||
}
|
||||
const labx = a.x + (b.x - a.x) * frac;
|
||||
const laby = a.y + (b.y - a.y) * frac;
|
||||
const p = new go.Point(last.x - this._offset.x - labx, last.y - this._offset.y - laby);
|
||||
const segangle = (idx >= 0) ? a.directionPoint(b) : b.directionPoint(a);
|
||||
lab.segmentOffset = p.rotate(-segangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The LinkLabelDraggingTool class lets the user move a label on a {@link Link}.
|
||||
*
|
||||
* This tool only works when the Link has a label
|
||||
* that is positioned at the {@link Link#midPoint} plus some offset.
|
||||
* It does not work for labels that have a particular {@link GraphObject#segmentIndex}.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LinkLabelDragging.html">Link Label Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class LinkLabelDraggingTool extends go.Tool {
|
||||
/**
|
||||
* The label being dragged.
|
||||
*/
|
||||
public label: go.GraphObject | null = null;
|
||||
private _offset: go.Point = new go.Point(); // of the mouse relative to the center of the label object
|
||||
private _originalOffset: go.Point | null = null;
|
||||
|
||||
/**
|
||||
* Constructs a LinkLabelDraggingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'LinkLabelDragging';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {GraphObject} This returns null if no such label is at the mouse down point.
|
||||
*/
|
||||
public findLabel(): go.GraphObject | null {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
let elt = diagram.findObjectAt(e.documentPoint, null, null);
|
||||
|
||||
if (elt === null || !(elt.part instanceof go.Link)) return null;
|
||||
while (elt !== null && elt.panel !== elt.part) {
|
||||
elt = elt.panel;
|
||||
}
|
||||
// If it's at an arrowhead segment index, don't consider it a label:
|
||||
if (elt !== null && (elt.segmentIndex === 0 || elt.segmentIndex === -1)) return null;
|
||||
return elt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {@link #findLabel}.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!super.canStart()) return false;
|
||||
const diagram = this.diagram;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left) return false;
|
||||
if (!this.isBeyondDragSize()) return false;
|
||||
|
||||
return this.findLabel() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a transaction, call {@link #findLabel} and remember it as the "label" property,
|
||||
* and remember the original value for the label's {@link GraphObject#segmentOffset} property.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
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();
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any reference to a label element.
|
||||
*/
|
||||
public doStop(): void {
|
||||
this.label = null;
|
||||
super.doStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the label's original value for {@link GraphObject#segmentOffset}.
|
||||
*/
|
||||
public doCancel(): void {
|
||||
if (this.label !== null && this._originalOffset !== null) {
|
||||
this.label.segmentOffset = this._originalOffset;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* During the drag, call {@link #updateSegmentOffset} in order to set
|
||||
* the {@link GraphObject#segmentOffset} of the label.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
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.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (!this.isActive) return;
|
||||
this.updateSegmentOffset();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the label's {@link GraphObject#segmentOffset} as a rotated offset from the midpoint of the
|
||||
* Link that the label is in.
|
||||
*/
|
||||
public updateSegmentOffset(): void {
|
||||
const lab = this.label;
|
||||
if (lab === null) return;
|
||||
const link = lab.part;
|
||||
if (!(link instanceof go.Link)) return;
|
||||
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
const idx = lab.segmentIndex;
|
||||
const 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) {
|
||||
const mid = link.midPoint;
|
||||
// need to rotate this point to account for the angle of the link segment at the mid-point
|
||||
const 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
|
||||
const frac = lab.segmentFraction;
|
||||
let a: go.Point;
|
||||
let b: go.Point;
|
||||
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
|
||||
const i = numpts + idx;
|
||||
a = link.getPoint(i);
|
||||
b = (i > 0) ? link.getPoint(i - 1) : a;
|
||||
}
|
||||
const labx = a.x + (b.x - a.x) * frac;
|
||||
const laby = a.y + (b.y - a.y) * frac;
|
||||
const p = new go.Point(last.x - this._offset.x - labx, last.y - this._offset.y - laby);
|
||||
const segangle = (idx >= 0) ? a.directionPoint(b) : b.directionPoint(a);
|
||||
lab.segmentOffset = p.rotate(-segangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Draggable Link Labels That Stay On Path</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">LinkLabelOnPathDraggingTool.ts</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.ts">LinkLabelDraggingTool.ts</a>, that allows
|
||||
the user to drag the label in any direction from the mid-point of the Link path.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { LinkLabelOnPathDraggingTool } from './LinkLabelOnPathDraggingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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',
|
||||
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('segmentIndex').makeTwoWay(), 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 }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The LinkLabelOnPathDraggingTool class lets the user move a label on a {@link Link} while keeping the label on the link's path.
|
||||
* This tool only works when the Link has a label marked by the "_isLinkLabel" property.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LinkLabelOnPathDragging.html">Link Label On Path Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class LinkLabelOnPathDraggingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a LinkLabelOnPathDraggingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* The label being dragged.
|
||||
*/
|
||||
this.label = null;
|
||||
this._originalFraction = 0.0;
|
||||
this.name = 'LinkLabelOnPathDragging';
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
* @return {GraphObject} This returns null if no such label is at the mouse down point.
|
||||
*/
|
||||
findLabel() {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
let elt = diagram.findObjectAt(e.documentPoint, null, null);
|
||||
if (elt === null || !(elt.part instanceof go.Link))
|
||||
return null;
|
||||
while (elt !== null && 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;
|
||||
}
|
||||
/**
|
||||
* 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 {@link #findLabel}.
|
||||
*/
|
||||
canStart() {
|
||||
if (!super.canStart())
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left)
|
||||
return false;
|
||||
if (!this.isBeyondDragSize())
|
||||
return false;
|
||||
return this.findLabel() !== null;
|
||||
}
|
||||
/**
|
||||
* Start a transaction, call findLabel and remember it as the "label" property,
|
||||
* and remember the original values for the label's segment properties.
|
||||
*/
|
||||
doActivate() {
|
||||
this.startTransaction('Shifted Label');
|
||||
this.label = this.findLabel();
|
||||
if (this.label !== null) {
|
||||
this._originalFraction = this.label.segmentFraction;
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
doDeactivate() {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
/**
|
||||
* Clear any reference to a label element.
|
||||
*/
|
||||
doStop() {
|
||||
this.label = null;
|
||||
super.doStop();
|
||||
}
|
||||
/**
|
||||
* Restore the label's original value for GraphObject.segment... properties.
|
||||
*/
|
||||
doCancel() {
|
||||
if (this.label !== null) {
|
||||
this.label.segmentFraction = this._originalFraction;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
/**
|
||||
* During the drag, call {@link #updateSegmentOffset} in order to set the segment... properties of the label.
|
||||
*/
|
||||
doMouseMove() {
|
||||
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.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (!this.isActive)
|
||||
return;
|
||||
this.updateSegmentOffset();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Save the label's {@link GraphObject#segmentFraction}
|
||||
* at the closest point to the mouse.
|
||||
*/
|
||||
updateSegmentOffset() {
|
||||
const lab = this.label;
|
||||
if (lab === null)
|
||||
return;
|
||||
const link = lab.part;
|
||||
if (!(link instanceof go.Link) || link.path === null)
|
||||
return;
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
// find the fractional distance along the link path closest to this point
|
||||
const path = link.path;
|
||||
if (path.geometry === null)
|
||||
return;
|
||||
const localpt = path.getLocalPoint(last);
|
||||
lab.segmentFraction = path.geometry.getFractionForPoint(localpt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The LinkLabelOnPathDraggingTool class lets the user move a label on a {@link Link} while keeping the label on the link's path.
|
||||
* This tool only works when the Link has a label marked by the "_isLinkLabel" property.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LinkLabelOnPathDragging.html">Link Label On Path Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class LinkLabelOnPathDraggingTool extends go.Tool {
|
||||
/**
|
||||
* The label being dragged.
|
||||
*/
|
||||
public label: go.GraphObject | null = null;
|
||||
private _originalFraction: number = 0.0;
|
||||
|
||||
/**
|
||||
* Constructs a LinkLabelOnPathDraggingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'LinkLabelOnPathDragging';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {GraphObject} This returns null if no such label is at the mouse down point.
|
||||
*/
|
||||
public findLabel(): go.GraphObject | null {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.lastInput;
|
||||
let elt = diagram.findObjectAt(e.documentPoint, null, null);
|
||||
|
||||
if (elt === null || !(elt.part instanceof go.Link)) return null;
|
||||
while (elt !== null && elt.panel !== elt.part) {
|
||||
elt = elt.panel;
|
||||
}
|
||||
// If it's not marked as "_isLinkLabel", don't consider it a label:
|
||||
if (!(elt as any)['_isLinkLabel']) return null;
|
||||
return elt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {@link #findLabel}.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!super.canStart()) return false;
|
||||
const diagram = this.diagram;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left) return false;
|
||||
if (!this.isBeyondDragSize()) return false;
|
||||
|
||||
return this.findLabel() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a transaction, call findLabel and remember it as the "label" property,
|
||||
* and remember the original values for the label's segment properties.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
this.startTransaction('Shifted Label');
|
||||
this.label = this.findLabel();
|
||||
if (this.label !== null) {
|
||||
this._originalFraction = this.label.segmentFraction;
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any reference to a label element.
|
||||
*/
|
||||
public doStop(): void {
|
||||
this.label = null;
|
||||
super.doStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the label's original value for GraphObject.segment... properties.
|
||||
*/
|
||||
public doCancel(): void {
|
||||
if (this.label !== null) {
|
||||
this.label.segmentFraction = this._originalFraction;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* During the drag, call {@link #updateSegmentOffset} in order to set the segment... properties of the label.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
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.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (!this.isActive) return;
|
||||
this.updateSegmentOffset();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the label's {@link GraphObject#segmentFraction}
|
||||
* at the closest point to the mouse.
|
||||
*/
|
||||
public updateSegmentOffset(): void {
|
||||
const lab = this.label;
|
||||
if (lab === null) return;
|
||||
const link = lab.part;
|
||||
if (!(link instanceof go.Link) || link.path === null) return;
|
||||
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
// find the fractional distance along the link path closest to this point
|
||||
const path = link.path;
|
||||
if (path.geometry === null) return;
|
||||
const localpt = path.getLocalPoint(last);
|
||||
lab.segmentFraction = path.geometry.getFractionForPoint(localpt);
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Link Shifting Tool</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { LinkShiftingTool } from './LinkShiftingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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
|
||||
const firstlink = myDiagram.links.first();
|
||||
if (firstlink !== null)
|
||||
firstlink.isSelected = true;
|
||||
});
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The LinkShiftingTool class lets the user shift the end of a link to be anywhere along the edges of the port;
|
||||
* use it in a diagram.toolManager.mouseDownTools list:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseDownTools.add(new LinkShiftingTool());
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LinkShifting.html">Link Shifting</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class LinkShiftingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a LinkShiftingTool and sets the handles and name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
// transient state
|
||||
this._handle = null;
|
||||
const 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;
|
||||
const g = new go.Shape();
|
||||
g.geometryString = 'F1 M0 0 L8 0 M8 4 L0 4';
|
||||
g.fill = null;
|
||||
g.stroke = 'dodgerblue';
|
||||
g.background = 'lightblue';
|
||||
g.cursor = 'pointer';
|
||||
g.segmentIndex = -1;
|
||||
g.segmentFraction = 1;
|
||||
g.segmentOrientation = go.Link.OrientAlong;
|
||||
this._fromHandleArchetype = h;
|
||||
this._toHandleArchetype = g;
|
||||
this._originalPoints = null;
|
||||
this.name = 'LinkShifting';
|
||||
}
|
||||
/**
|
||||
* A small GraphObject used as a shifting handle.
|
||||
*/
|
||||
get fromHandleArchetype() { return this._fromHandleArchetype; }
|
||||
set fromHandleArchetype(value) { this._fromHandleArchetype = value; }
|
||||
/**
|
||||
* A small GraphObject used as a shifting handle.
|
||||
*/
|
||||
get toHandleArchetype() { return this._toHandleArchetype; }
|
||||
set toHandleArchetype(value) { this._toHandleArchetype = value; }
|
||||
/**
|
||||
* Show an {@link Adornment} with a reshape handle at each end of the link which allows for shifting of the end points.
|
||||
*/
|
||||
updateAdornments(part) {
|
||||
if (part === null || !(part instanceof go.Link))
|
||||
return; // this tool only applies to Links
|
||||
const link = part;
|
||||
// show handles if link is selected, remove them if no longer selected
|
||||
let category = 'LinkShiftingFrom';
|
||||
let adornment = null;
|
||||
if (link.isSelected && !this.diagram.isReadOnly) {
|
||||
const selelt = link.selectionObject;
|
||||
if (selelt !== null && link.actualBounds.isReal() && link.isVisible() &&
|
||||
selelt.actualBounds.isReal() && selelt.isVisibleObject()) {
|
||||
const 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) {
|
||||
const selelt = link.selectionObject;
|
||||
if (selelt !== null && link.actualBounds.isReal() && link.isVisible() &&
|
||||
selelt.actualBounds.isReal() && selelt.isVisibleObject()) {
|
||||
const 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);
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @param {GraphObject} selelt the {@link GraphObject} of the {@link Link} being shifted.
|
||||
* @param {boolean} toend
|
||||
* @return {Adornment}
|
||||
*/
|
||||
makeAdornment(selelt, toend) {
|
||||
const adornment = new go.Adornment();
|
||||
adornment.type = go.Panel.Link;
|
||||
const 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 tool may run when there is a mouse-down event on a reshaping handle.
|
||||
*/
|
||||
canStart() {
|
||||
if (!this.isEnabled)
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly)
|
||||
return false;
|
||||
if (!diagram.lastInput.left)
|
||||
return false;
|
||||
let h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingFrom');
|
||||
if (h === null)
|
||||
h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingTo');
|
||||
return (h !== null);
|
||||
}
|
||||
/**
|
||||
* Start shifting, if {@link #findToolHandleAt} finds a reshaping handle at the mouse down point.
|
||||
*
|
||||
* If successful this sets the handle to be the reshape handle that it finds.
|
||||
* It also remembers the original points in case this tool is cancelled.
|
||||
* And it starts a transaction.
|
||||
*/
|
||||
doActivate() {
|
||||
const diagram = this.diagram;
|
||||
let h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingFrom');
|
||||
if (h === null)
|
||||
h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingTo');
|
||||
if (h === null)
|
||||
return;
|
||||
const ad = h.part;
|
||||
if (ad === null || ad.adornedObject === null)
|
||||
return;
|
||||
const 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 stops the current shifting operation with the link as it is.
|
||||
*/
|
||||
doDeactivate() {
|
||||
this.isActive = false;
|
||||
const diagram = this.diagram;
|
||||
diagram.isMouseCaptured = false;
|
||||
diagram.currentCursor = '';
|
||||
this.stopTransaction();
|
||||
}
|
||||
/**
|
||||
* Perform cleanup of tool state.
|
||||
*/
|
||||
doStop() {
|
||||
this._handle = null;
|
||||
this._originalPoints = null;
|
||||
}
|
||||
/**
|
||||
* Restore the link route to be the original points and stop this tool.
|
||||
*/
|
||||
doCancel() {
|
||||
if (this._handle !== null) {
|
||||
const ad = this._handle.part;
|
||||
if (ad.adornedObject === null)
|
||||
return;
|
||||
const link = ad.adornedObject.part;
|
||||
if (this._originalPoints !== null)
|
||||
link.points = this._originalPoints;
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Call {@link #doReshape} with a new point determined by the mouse
|
||||
* to change the end point of the link.
|
||||
*/
|
||||
doMouseMove() {
|
||||
if (this.isActive) {
|
||||
this.doReshape(this.diagram.lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reshape the link's end with a point based on the most recent mouse point by calling {@link #doReshape},
|
||||
* and then stop this tool.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (this.isActive) {
|
||||
this.doReshape(this.diagram.lastInput.documentPoint);
|
||||
this.transactionResult = this.name;
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Find the closest point along the edge of the link's port and shift the end of the link to that point.
|
||||
*/
|
||||
doReshape(pt) {
|
||||
if (this._handle === null)
|
||||
return;
|
||||
const ad = this._handle.part;
|
||||
if (ad.adornedObject === null)
|
||||
return;
|
||||
const link = ad.adornedObject.part;
|
||||
const fromend = ad.category === 'LinkShiftingFrom';
|
||||
let port = null;
|
||||
if (fromend) {
|
||||
port = link.fromPort;
|
||||
}
|
||||
else {
|
||||
port = link.toPort;
|
||||
}
|
||||
if (port === null)
|
||||
return;
|
||||
// support rotated ports
|
||||
const portang = port.getDocumentAngle();
|
||||
const center = port.getDocumentPoint(go.Spot.Center);
|
||||
const 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));
|
||||
let lp = link.getLinkPointFromPoint(port.part, port, center, pt, fromend);
|
||||
lp = lp.copy().subtract(center).rotate(-portang).add(center);
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The LinkShiftingTool class lets the user shift the end of a link to be anywhere along the edges of the port;
|
||||
* use it in a diagram.toolManager.mouseDownTools list:
|
||||
* ```js
|
||||
* myDiagram.toolManager.mouseDownTools.add(new LinkShiftingTool());
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LinkShifting.html">Link Shifting</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class LinkShiftingTool extends go.Tool {
|
||||
// these are archetypes for the two shift handles, one at each end of the Link:
|
||||
private _fromHandleArchetype: go.GraphObject | null;
|
||||
private _toHandleArchetype: go.GraphObject | null;
|
||||
|
||||
// transient state
|
||||
private _handle: go.GraphObject | null = null;
|
||||
private _originalPoints: go.List<go.Point> | null;
|
||||
|
||||
/**
|
||||
* Constructs a LinkShiftingTool and sets the handles and name of the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
const h: go.Shape = 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;
|
||||
const g: go.Shape = new go.Shape();
|
||||
g.geometryString = 'F1 M0 0 L8 0 M8 4 L0 4';
|
||||
g.fill = null;
|
||||
g.stroke = 'dodgerblue';
|
||||
g.background = 'lightblue';
|
||||
g.cursor = 'pointer';
|
||||
g.segmentIndex = -1;
|
||||
g.segmentFraction = 1;
|
||||
g.segmentOrientation = go.Link.OrientAlong;
|
||||
|
||||
this._fromHandleArchetype = h;
|
||||
this._toHandleArchetype = g;
|
||||
this._originalPoints = null;
|
||||
this.name = 'LinkShifting';
|
||||
}
|
||||
|
||||
/**
|
||||
* A small GraphObject used as a shifting handle.
|
||||
*/
|
||||
get fromHandleArchetype(): go.GraphObject | null { return this._fromHandleArchetype; }
|
||||
set fromHandleArchetype(value: go.GraphObject | null) { this._fromHandleArchetype = value; }
|
||||
|
||||
/**
|
||||
* A small GraphObject used as a shifting handle.
|
||||
*/
|
||||
get toHandleArchetype(): go.GraphObject | null { return this._toHandleArchetype; }
|
||||
set toHandleArchetype(value: go.GraphObject | null) { this._toHandleArchetype = value; }
|
||||
|
||||
/**
|
||||
* Show an {@link Adornment} with a reshape handle at each end of the link which allows for shifting of the end points.
|
||||
*/
|
||||
public updateAdornments(part: go.Part): void {
|
||||
if (part === null || !(part instanceof go.Link)) return; // this tool only applies to Links
|
||||
const link: go.Link = part;
|
||||
// show handles if link is selected, remove them if no longer selected
|
||||
let category = 'LinkShiftingFrom';
|
||||
let adornment = null;
|
||||
if (link.isSelected && !this.diagram.isReadOnly) {
|
||||
const selelt = link.selectionObject;
|
||||
if (selelt !== null && link.actualBounds.isReal() && link.isVisible() &&
|
||||
selelt.actualBounds.isReal() && selelt.isVisibleObject()) {
|
||||
const spot = (link as any).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) {
|
||||
const selelt = link.selectionObject;
|
||||
if (selelt !== null && link.actualBounds.isReal() && link.isVisible() &&
|
||||
selelt.actualBounds.isReal() && selelt.isVisibleObject()) {
|
||||
const spot = (link as any).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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @param {GraphObject} selelt the {@link GraphObject} of the {@link Link} being shifted.
|
||||
* @param {boolean} toend
|
||||
* @return {Adornment}
|
||||
*/
|
||||
public makeAdornment(selelt: go.GraphObject, toend: boolean): go.Adornment {
|
||||
const adornment = new go.Adornment();
|
||||
adornment.type = go.Panel.Link;
|
||||
const 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 tool may run when there is a mouse-down event on a reshaping handle.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!this.isEnabled) return false;
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
|
||||
if (!diagram.lastInput.left) return false;
|
||||
let h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingFrom');
|
||||
if (h === null) h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingTo');
|
||||
return (h !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start shifting, if {@link #findToolHandleAt} finds a reshaping handle at the mouse down point.
|
||||
*
|
||||
* If successful this sets the handle to be the reshape handle that it finds.
|
||||
* It also remembers the original points in case this tool is cancelled.
|
||||
* And it starts a transaction.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
const diagram = this.diagram;
|
||||
let h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingFrom');
|
||||
if (h === null) h = this.findToolHandleAt(diagram.firstInput.documentPoint, 'LinkShiftingTo');
|
||||
if (h === null) return;
|
||||
const ad = h.part as go.Adornment;
|
||||
if (ad === null || ad.adornedObject === null) return;
|
||||
const 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 stops the current shifting operation with the link as it is.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
this.isActive = false;
|
||||
const diagram = this.diagram;
|
||||
diagram.isMouseCaptured = false;
|
||||
diagram.currentCursor = '';
|
||||
this.stopTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform cleanup of tool state.
|
||||
*/
|
||||
public doStop(): void {
|
||||
this._handle = null;
|
||||
this._originalPoints = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the link route to be the original points and stop this tool.
|
||||
*/
|
||||
public doCancel(): void {
|
||||
if (this._handle !== null) {
|
||||
const ad = this._handle.part as go.Adornment;
|
||||
if (ad.adornedObject === null) return;
|
||||
const link = ad.adornedObject.part as go.Link;
|
||||
if (this._originalPoints !== null) link.points = this._originalPoints;
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call {@link #doReshape} with a new point determined by the mouse
|
||||
* to change the end point of the link.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
if (this.isActive) {
|
||||
this.doReshape(this.diagram.lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reshape the link's end with a point based on the most recent mouse point by calling {@link #doReshape},
|
||||
* and then stop this tool.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (this.isActive) {
|
||||
this.doReshape(this.diagram.lastInput.documentPoint);
|
||||
this.transactionResult = this.name;
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest point along the edge of the link's port and shift the end of the link to that point.
|
||||
*/
|
||||
public doReshape(pt: go.Point): void {
|
||||
if (this._handle === null) return;
|
||||
const ad = this._handle.part as go.Adornment;
|
||||
if (ad.adornedObject === null) return;
|
||||
const link = ad.adornedObject.part as go.Link;
|
||||
const fromend = ad.category === 'LinkShiftingFrom';
|
||||
let port = null;
|
||||
if (fromend) {
|
||||
port = link.fromPort;
|
||||
} else {
|
||||
port = link.toPort;
|
||||
}
|
||||
if (port === null) return;
|
||||
// support rotated ports
|
||||
const portang = port.getDocumentAngle();
|
||||
const center = port.getDocumentPoint(go.Spot.Center);
|
||||
const 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));
|
||||
let lp = link.getLinkPointFromPoint(port.part as go.Node, port, center, pt, fromend);
|
||||
lp = lp.copy().subtract(center).rotate(-portang).add(center);
|
||||
const 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,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Local Storage Commands</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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.ts">LocalStorageCommandHandler.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { LocalStorageCommandHandler } from './LocalStorageCommandHandler.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const myDiagram = $(go.Diagram, 'myDiagramDiv', // create a Diagram for the DIV HTML element
|
||||
{
|
||||
commandHandler: new LocalStorageCommandHandler(),
|
||||
'undoManager.isEnabled': true // enable undo & redo
|
||||
});
|
||||
const myDiagram2 = $(go.Diagram, 'myDiagramDiv2', // create a Diagram for the DIV HTML element
|
||||
{
|
||||
commandHandler: new LocalStorageCommandHandler(),
|
||||
'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' }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
window.myDiagram2 = myDiagram2;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* The {@link #copyToClipboard} and {@link #pasteFromClipboard} functions fall back to using the standard definitions
|
||||
* if there are any errors calling `Storage.getItem` or `Storage.setItem`.
|
||||
*
|
||||
* Typical usage:
|
||||
* ```js
|
||||
* $(go.Diagram, "myDiagramDiv",
|
||||
* {
|
||||
* commandHandler: $(LocalStorageCommandHandler),
|
||||
* ...
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
* or:
|
||||
* ```js
|
||||
* myDiagram.commandHandler = new LocalStorageCommandHandler();
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LocalStorageCommandHandler.html">Local Storage Commands</a> sample.
|
||||
* @category Extension
|
||||
*/
|
||||
export class LocalStorageCommandHandler extends go.CommandHandler {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.StorageKey = 'go._clipboard';
|
||||
this.FormatKey = 'go._clipboardFormat';
|
||||
}
|
||||
/**
|
||||
* Makes a copy of the given collection of {@link Part}s
|
||||
* and stores it as JSON in LocalStorage.
|
||||
* @param {Iterable.<Part>} coll a collection of {@link Part}s.
|
||||
*/
|
||||
copyToClipboard(coll) {
|
||||
try {
|
||||
if (coll === null) {
|
||||
window.localStorage.setItem(this.StorageKey, '');
|
||||
window.localStorage.setItem(this.FormatKey, '');
|
||||
}
|
||||
else {
|
||||
const 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
|
||||
super.copyToClipboard(coll);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If LocalStorage holds JSON for a collection of {@link Part}s,
|
||||
* and if the {@link Model#dataFormat} matches that stored in the clipboard,
|
||||
* this makes a copy of the clipboard's parts and adds the copies to this {@link Diagram}.
|
||||
* @return {Set.<Part>} a collection of newly pasted {@link Part}s
|
||||
*/
|
||||
pasteFromClipboard() {
|
||||
const coll = new go.Set();
|
||||
try {
|
||||
const clipstr = window.localStorage.getItem(this.StorageKey);
|
||||
const clipfrmt = window.localStorage.getItem(this.FormatKey);
|
||||
if (clipstr === null || clipstr === '' || clipfrmt !== this.diagram.model.dataFormat) {
|
||||
return coll;
|
||||
}
|
||||
else {
|
||||
const 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
|
||||
const allparts = new go.Set();
|
||||
allparts.addAll(clipdiag.parts).addAll(clipdiag.nodes).addAll(clipdiag.links);
|
||||
const copymap = this.diagram.copyParts(allparts, this.diagram, false);
|
||||
// return a Set of the copied Parts
|
||||
return new go.Set().addAll(copymap.iteratorValues);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
// fallback implementation
|
||||
return super.pasteFromClipboard();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This predicate controls whether or not the user can invoke the {@link #pasteSelection} command.
|
||||
*
|
||||
* This works just like {@link CommandHandler#canPasteSelection}, but looks at LocalStorage instead of a static variable.
|
||||
*/
|
||||
canPasteSelection(pos) {
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly)
|
||||
return false;
|
||||
if (!diagram.allowInsert || !diagram.allowClipboard)
|
||||
return false;
|
||||
try {
|
||||
const clipstr = window.localStorage.getItem(this.StorageKey);
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* The {@link #copyToClipboard} and {@link #pasteFromClipboard} functions fall back to using the standard definitions
|
||||
* if there are any errors calling `Storage.getItem` or `Storage.setItem`.
|
||||
*
|
||||
* Typical usage:
|
||||
* ```js
|
||||
* $(go.Diagram, "myDiagramDiv",
|
||||
* {
|
||||
* commandHandler: $(LocalStorageCommandHandler),
|
||||
* ...
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
* or:
|
||||
* ```js
|
||||
* myDiagram.commandHandler = new LocalStorageCommandHandler();
|
||||
* ```
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/LocalStorageCommandHandler.html">Local Storage Commands</a> sample.
|
||||
* @category Extension
|
||||
*/
|
||||
export class LocalStorageCommandHandler extends go.CommandHandler {
|
||||
private StorageKey: string = 'go._clipboard';
|
||||
private FormatKey: string = 'go._clipboardFormat';
|
||||
|
||||
/**
|
||||
* Makes a copy of the given collection of {@link Part}s
|
||||
* and stores it as JSON in LocalStorage.
|
||||
* @param {Iterable.<Part>} coll a collection of {@link Part}s.
|
||||
*/
|
||||
public copyToClipboard(coll: go.Iterable<go.Part>): void {
|
||||
try {
|
||||
if (coll === null) {
|
||||
window.localStorage.setItem(this.StorageKey, '');
|
||||
window.localStorage.setItem(this.FormatKey, '');
|
||||
} else {
|
||||
const 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
|
||||
super.copyToClipboard(coll);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If LocalStorage holds JSON for a collection of {@link Part}s,
|
||||
* and if the {@link Model#dataFormat} matches that stored in the clipboard,
|
||||
* this makes a copy of the clipboard's parts and adds the copies to this {@link Diagram}.
|
||||
* @return {Set.<Part>} a collection of newly pasted {@link Part}s
|
||||
*/
|
||||
public pasteFromClipboard(): go.Set<go.Part> {
|
||||
const coll = new go.Set<go.Part>();
|
||||
try {
|
||||
const clipstr = window.localStorage.getItem(this.StorageKey);
|
||||
const clipfrmt = window.localStorage.getItem(this.FormatKey);
|
||||
if (clipstr === null || clipstr === '' || clipfrmt !== this.diagram.model.dataFormat) {
|
||||
return coll;
|
||||
} else {
|
||||
const 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
|
||||
const allparts = new go.Set<go.Part>();
|
||||
allparts.addAll(clipdiag.parts).addAll(clipdiag.nodes).addAll(clipdiag.links);
|
||||
const copymap = this.diagram.copyParts(allparts, this.diagram, false);
|
||||
// return a Set of the copied Parts
|
||||
return new go.Set<go.Part>().addAll(copymap.iteratorValues);
|
||||
}
|
||||
} catch (ex) {
|
||||
// fallback implementation
|
||||
return super.pasteFromClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This predicate controls whether or not the user can invoke the {@link #pasteSelection} command.
|
||||
*
|
||||
* This works just like {@link CommandHandler#canPasteSelection}, but looks at LocalStorage instead of a static variable.
|
||||
*/
|
||||
public canPasteSelection(pos?: go.Point): boolean {
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
|
||||
if (!diagram.allowInsert || !diagram.allowClipboard) return false;
|
||||
try {
|
||||
const clipstr = window.localStorage.getItem(this.StorageKey);
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>State Chart with Draggable Node Labels</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: Allow the user to shift the label of a node." />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">NodeLabelDraggingTool.ts</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">Save</button>
|
||||
<button id="LoadButton">Load</button> Diagram Model saved in JSON format:
|
||||
<br />
|
||||
<textarea id="mySavedModel" style="width:100%;height:300px">
|
||||
{ "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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { NodeLabelDraggingTool } from './NodeLabelDraggingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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', (e) => {
|
||||
const button = document.getElementById('SaveButton');
|
||||
if (button)
|
||||
button.disabled = !myDiagram.isModified;
|
||||
const idx = document.title.indexOf('*');
|
||||
if (myDiagram.isModified) {
|
||||
if (idx < 0)
|
||||
document.title += '*';
|
||||
}
|
||||
else {
|
||||
if (idx >= 0)
|
||||
document.title = document.title.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,
|
||||
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,
|
||||
_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) {
|
||||
const adorn = obj.part;
|
||||
const fromNode = adorn.adornedPart;
|
||||
if (fromNode === null)
|
||||
return;
|
||||
e.handled = true;
|
||||
const diagram = e.diagram;
|
||||
diagram.startTransaction('Add State');
|
||||
// get the node data for which the user clicked the button
|
||||
const fromData = fromNode.data;
|
||||
// create a new "State" data object, positioned off to the right of the adorned Node
|
||||
const toData = { text: 'new' };
|
||||
const 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
|
||||
const model = diagram.model;
|
||||
model.addNodeData(toData);
|
||||
// create a link data from the old node data to the new node data
|
||||
const linkdata = {
|
||||
from: model.getKeyForNodeData(fromData),
|
||||
to: model.getKeyForNodeData(toData),
|
||||
text: 'transition'
|
||||
};
|
||||
// and add the link data to the model
|
||||
model.addLinkData(linkdata);
|
||||
// select the new Node
|
||||
const 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
|
||||
if (newnode !== null)
|
||||
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);
|
||||
}
|
||||
document.getElementById("SaveButton").onclick = app.save;
|
||||
document.getElementById("LoadButton").onclick = app.load;
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The NodeLabelDraggingTool class lets the user move a label on a Node.
|
||||
*
|
||||
* 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 {@link GraphObject#alignment} property to have an
|
||||
* offset from the center of the panel.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/NodeLabelDragging.html">Node Label Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class NodeLabelDraggingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a NodeLabelDraggingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* The label being dragged.
|
||||
*/
|
||||
this.label = null;
|
||||
this._offset = new go.Point(); // of the mouse relative to the center of the label object
|
||||
this._originalAlignment = go.Spot.Default;
|
||||
this._originalCenter = new go.Point();
|
||||
this.name = 'NodeLabelDragging';
|
||||
}
|
||||
/**
|
||||
* 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).
|
||||
* @return {GraphObject} This returns null if no such label is at the mouse down point.
|
||||
*/
|
||||
findLabel() {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.firstInput;
|
||||
let 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;
|
||||
}
|
||||
/**
|
||||
* 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().
|
||||
*/
|
||||
canStart() {
|
||||
if (!super.canStart())
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left)
|
||||
return false;
|
||||
if (!this.isBeyondDragSize())
|
||||
return false;
|
||||
return this.findLabel() !== null;
|
||||
}
|
||||
/**
|
||||
* Start a transaction, call {@link findLabel} and remember it as the "label" property,
|
||||
* and remember the original value for the label's {@link GraphObject#alignment} property.
|
||||
*/
|
||||
doActivate() {
|
||||
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();
|
||||
const panel = this.label.panel;
|
||||
if (panel !== null) {
|
||||
const main = panel.findMainElement();
|
||||
if (main !== null)
|
||||
this._originalCenter = main.getDocumentPoint(go.Spot.Center);
|
||||
}
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
doDeactivate() {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
/**
|
||||
* Clear any reference to a label element.
|
||||
*/
|
||||
doStop() {
|
||||
this.label = null;
|
||||
super.doStop();
|
||||
}
|
||||
/**
|
||||
* Restore the label's original value for GraphObject.alignment.
|
||||
*/
|
||||
doCancel() {
|
||||
if (this.label !== null) {
|
||||
this.label.alignment = this._originalAlignment;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
/**
|
||||
* During the drag, call updateAlignment in order to set the {@link GraphObject#alignment} of the label.
|
||||
*/
|
||||
doMouseMove() {
|
||||
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.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (!this.isActive)
|
||||
return;
|
||||
this.updateAlignment();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Save the label's {@link GraphObject#alignment} as an absolute offset from the center of the Spot Panel
|
||||
* that the label is in.
|
||||
*/
|
||||
updateAlignment() {
|
||||
if (this.label === null)
|
||||
return;
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The NodeLabelDraggingTool class lets the user move a label on a Node.
|
||||
*
|
||||
* 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 {@link GraphObject#alignment} property to have an
|
||||
* offset from the center of the panel.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/NodeLabelDragging.html">Node Label Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class NodeLabelDraggingTool extends go.Tool {
|
||||
/**
|
||||
* The label being dragged.
|
||||
*/
|
||||
public label: go.GraphObject | null = null;
|
||||
private _offset: go.Point = new go.Point(); // of the mouse relative to the center of the label object
|
||||
private _originalAlignment: go.Spot = go.Spot.Default;
|
||||
private _originalCenter: go.Point = new go.Point();
|
||||
|
||||
/**
|
||||
* Constructs a NodeLabelDraggingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'NodeLabelDragging';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* @return {GraphObject} This returns null if no such label is at the mouse down point.
|
||||
*/
|
||||
public findLabel(): go.GraphObject | null {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.firstInput;
|
||||
let elt = diagram.findObjectAt(e.documentPoint, null, null);
|
||||
|
||||
if (elt === null || !(elt.part instanceof go.Node)) return null;
|
||||
while (elt.panel !== null) {
|
||||
if ((elt as any)['_isNodeLabel'] && elt.panel.type === go.Panel.Spot && elt.panel.findMainElement() !== elt) return elt;
|
||||
elt = elt.panel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!super.canStart()) return false;
|
||||
const diagram = this.diagram;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left) return false;
|
||||
if (!this.isBeyondDragSize()) return false;
|
||||
|
||||
return this.findLabel() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a transaction, call {@link findLabel} and remember it as the "label" property,
|
||||
* and remember the original value for the label's {@link GraphObject#alignment} property.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
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();
|
||||
const panel = this.label.panel;
|
||||
if (panel !== null) {
|
||||
const main = panel.findMainElement();
|
||||
if (main !== null) this._originalCenter = main.getDocumentPoint(go.Spot.Center);
|
||||
}
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any reference to a label element.
|
||||
*/
|
||||
public doStop(): void {
|
||||
this.label = null;
|
||||
super.doStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the label's original value for GraphObject.alignment.
|
||||
*/
|
||||
public doCancel(): void {
|
||||
if (this.label !== null) {
|
||||
this.label.alignment = this._originalAlignment;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* During the drag, call updateAlignment in order to set the {@link GraphObject#alignment} of the label.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
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.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (!this.isActive) return;
|
||||
this.updateAlignment();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the label's {@link GraphObject#alignment} as an absolute offset from the center of the Spot Panel
|
||||
* that the label is in.
|
||||
*/
|
||||
public updateAlignment(): void {
|
||||
if (this.label === null) return;
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Non-Realtime Dragging</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">NonRealtimeDraggingTool.ts</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { NonRealtimeDraggingTool } from './NonRealtimeDraggingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const myDiagram = $(go.Diagram, 'myDiagramDiv', {
|
||||
draggingTool: $(NonRealtimeDraggingTool, { duration: 600 }),
|
||||
'undoManager.isEnabled': true
|
||||
});
|
||||
myDiagram.nodeTemplate =
|
||||
$(go.Node, 'Auto', { locationSpot: go.Spot.Center }, $(go.Shape, 'Circle', {
|
||||
fill: 'white',
|
||||
portId: '', cursor: 'pointer',
|
||||
// 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,
|
||||
isMultiline: false,
|
||||
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' }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The NonRealtimeDraggingTool class lets the user drag an image instead of actually moving any selected nodes,
|
||||
* until the mouse-up event.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/NonRealtimeDragging.html">Non Realtime Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class NonRealtimeDraggingTool extends go.DraggingTool {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._duration = 0; // duration of movement animation; <= 0 to disable
|
||||
this._imagePart = null; // a Part holding a translucent image of what would be dragged
|
||||
this._ghostDraggedParts = null; // a Map of the _imagePart and its dragging information
|
||||
this._originalDraggedParts = null; // the saved normal value of DraggingTool.draggedParts
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get duration() { return this._duration; }
|
||||
set duration(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.
|
||||
* @param {Iterable.<Part>} parts A {@link Set} or {@link List} of {@link Part}s.
|
||||
* @return {Map.<Part,DraggingInfo>}
|
||||
*/
|
||||
computeEffectiveCollection(coll) {
|
||||
const map = super.computeEffectiveCollection(coll, this.dragOptions);
|
||||
if (this.isActive && this._imagePart === null) {
|
||||
const bounds = this.diagram.computePartsBounds(map.toKeySet());
|
||||
const offset = this.diagram.lastInput.documentPoint.copy().subtract(bounds.position);
|
||||
const $ = 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 {@link #draggedParts} with the ghost dragged parts, which
|
||||
* consists of just one Part, the image, added to the Diagram at the current mouse point.
|
||||
*/
|
||||
doActivate() {
|
||||
super.doActivate();
|
||||
if (this._imagePart !== null) {
|
||||
this._imagePart.location = this.diagram.lastInput.documentPoint;
|
||||
this.diagram.add(this._imagePart);
|
||||
this._originalDraggedParts = this.draggedParts;
|
||||
this._ghostDraggedParts = super.computeEffectiveCollection(new go.List().add(this._imagePart), this.dragOptions);
|
||||
this.draggedParts = this._ghostDraggedParts;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When deactivated, make sure any image is removed from the Diagram and all references are cleared out.
|
||||
*/
|
||||
doDeactivate() {
|
||||
if (this._imagePart !== null) {
|
||||
this.diagram.remove(this._imagePart);
|
||||
}
|
||||
this._imagePart = null;
|
||||
this._ghostDraggedParts = null;
|
||||
this._originalDraggedParts = null;
|
||||
super.doDeactivate();
|
||||
}
|
||||
/**
|
||||
* Do the normal mouse-up behavior, but only after restoring {@link #draggedParts}.
|
||||
*/
|
||||
doMouseUp() {
|
||||
const partsmap = this._originalDraggedParts;
|
||||
if (partsmap !== null) {
|
||||
this.draggedParts = partsmap;
|
||||
}
|
||||
super.doMouseUp();
|
||||
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 image.
|
||||
*/
|
||||
doKeyDown() {
|
||||
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);
|
||||
}
|
||||
super.doKeyDown();
|
||||
}
|
||||
/**
|
||||
* If the user changes back to "moving" mode,
|
||||
* show the image again and go back to dragging the ghost dragged parts.
|
||||
*/
|
||||
doKeyUp() {
|
||||
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;
|
||||
}
|
||||
super.doKeyUp();
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The NonRealtimeDraggingTool class lets the user drag an image instead of actually moving any selected nodes,
|
||||
* until the mouse-up event.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/NonRealtimeDragging.html">Non Realtime Dragging</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class NonRealtimeDraggingTool extends go.DraggingTool {
|
||||
private _duration: number = 0; // duration of movement animation; <= 0 to disable
|
||||
private _imagePart: go.Part | null = null; // a Part holding a translucent image of what would be dragged
|
||||
private _ghostDraggedParts: go.Map<go.Part, go.DraggingInfo> | null = null; // a Map of the _imagePart and its dragging information
|
||||
private _originalDraggedParts: go.Map<go.Part, go.DraggingInfo> | null = null; // the saved normal value of DraggingTool.draggedParts
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get duration(): number { return this._duration; }
|
||||
set duration(val: number) { 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.
|
||||
* @param {Iterable.<Part>} parts A {@link Set} or {@link List} of {@link Part}s.
|
||||
* @return {Map.<Part,DraggingInfo>}
|
||||
*/
|
||||
public computeEffectiveCollection(coll: go.Iterable<go.Part>): go.Map<go.Part, go.DraggingInfo> {
|
||||
const map = super.computeEffectiveCollection(coll, this.dragOptions);
|
||||
if (this.isActive && this._imagePart === null) {
|
||||
const bounds = this.diagram.computePartsBounds(map.toKeySet());
|
||||
const offset = this.diagram.lastInput.documentPoint.copy().subtract(bounds.position);
|
||||
const $ = 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 {@link #draggedParts} with the ghost dragged parts, which
|
||||
* consists of just one Part, the image, added to the Diagram at the current mouse point.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
super.doActivate();
|
||||
if (this._imagePart !== null) {
|
||||
this._imagePart.location = this.diagram.lastInput.documentPoint;
|
||||
this.diagram.add(this._imagePart);
|
||||
this._originalDraggedParts = this.draggedParts;
|
||||
this._ghostDraggedParts = super.computeEffectiveCollection(new go.List<go.Part>().add(this._imagePart), this.dragOptions);
|
||||
this.draggedParts = this._ghostDraggedParts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When deactivated, make sure any image is removed from the Diagram and all references are cleared out.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
if (this._imagePart !== null) {
|
||||
this.diagram.remove(this._imagePart);
|
||||
}
|
||||
this._imagePart = null;
|
||||
this._ghostDraggedParts = null;
|
||||
this._originalDraggedParts = null;
|
||||
super.doDeactivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the normal mouse-up behavior, but only after restoring {@link #draggedParts}.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
const partsmap = this._originalDraggedParts;
|
||||
if (partsmap !== null) {
|
||||
this.draggedParts = partsmap;
|
||||
}
|
||||
super.doMouseUp();
|
||||
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 image.
|
||||
*/
|
||||
public doKeyDown(): void {
|
||||
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);
|
||||
}
|
||||
super.doKeyDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user changes back to "moving" mode,
|
||||
* show the image again and go back to dragging the ghost dragged parts.
|
||||
*/
|
||||
public doKeyUp(): void {
|
||||
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;
|
||||
}
|
||||
super.doKeyUp();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Orthogonal Link Reshaping Tool</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="sample">
|
||||
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
|
||||
Routing:
|
||||
<input id="orthogonal" type="radio" name="routing" value="orthogonal" />Orthogonal
|
||||
<input id="avoidsnodes" type="radio" name="routing" value="avoidsnodes" checked="checked" />AvoidsNodes
|
||||
<p>
|
||||
This sample demonstrates the OrthogonalLinkReshapingTool that is defined in its own file,
|
||||
as <a href="OrthogonalLinkReshapingTool.ts">OrthogonalLinkReshapingTool.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { OrthogonalLinkReshapingTool } from './OrthogonalLinkReshapingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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', (e) => {
|
||||
// select the Link in order to show its two additional Adornments, for shifting the ends
|
||||
const link = myDiagram.links.first();
|
||||
if (link !== null)
|
||||
link.isSelected = true;
|
||||
});
|
||||
|
||||
function updateRouting() {
|
||||
const routing = getRadioValue('routing');
|
||||
const newRouting = (routing === 'orthogonal') ? go.Link.Orthogonal : go.Link.AvoidsNodes;
|
||||
myDiagram.startTransaction('update routing');
|
||||
myDiagram.linkTemplate.routing = newRouting;
|
||||
myDiagram.links.each((l) => {
|
||||
l.routing = newRouting;
|
||||
});
|
||||
myDiagram.commitTransaction('update routing');
|
||||
}
|
||||
function getRadioValue(name) {
|
||||
const radio = document.getElementsByName(name);
|
||||
for (let i = 0; i < radio.length; i++) {
|
||||
if (radio[i].checked)
|
||||
return radio[i].value;
|
||||
}
|
||||
}
|
||||
document.getElementById("orthogonal").onclick = updateRouting;
|
||||
document.getElementById("avoidsnodes").onclick = updateRouting;
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The OrthogonalLinkReshapingTool class lets a user drag a tool handle along the link segment, which will move the whole segment.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/OrthogonalLinkReshaping.html">Orthogonal Link Reshaping</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class OrthogonalLinkReshapingTool extends go.LinkReshapingTool {
|
||||
/**
|
||||
* Constructs an OrthogonalLinkReshapingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._alreadyAddedPoint = false;
|
||||
this.name = 'OrthogonalLinkReshaping';
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* For orthogonal, straight links, create the handles and set reshaping behavior.
|
||||
*/
|
||||
makeAdornment(pathshape) {
|
||||
const link = pathshape.part;
|
||||
// add all normal handles first
|
||||
const adornment = super.makeAdornment(pathshape);
|
||||
// add long reshaping handles for orthogonal, straight links
|
||||
if (link !== null && link.isOrthogonal && link.curve !== go.Link.Bezier) {
|
||||
const firstindex = link.firstPickIndex + (link.resegmentable ? 0 : 1);
|
||||
const lastindex = link.lastPickIndex - (link.resegmentable ? 0 : 1);
|
||||
for (let i = firstindex; i < lastindex; i++) {
|
||||
this.makeSegmentDragHandle(link, adornment, i);
|
||||
}
|
||||
}
|
||||
return adornment;
|
||||
}
|
||||
/**
|
||||
* This stops the current reshaping operation and updates any link handles.
|
||||
*/
|
||||
doDeactivate() {
|
||||
this._alreadyAddedPoint = false;
|
||||
// when we finish, recreate adornment to ensure proper reshaping behavior/cursor
|
||||
const link = this.adornedLink;
|
||||
if (link !== null && link.isOrthogonal && link.curve !== go.Link.Bezier) {
|
||||
const pathshape = link.path;
|
||||
if (pathshape !== null) {
|
||||
const adornment = this.makeAdornment(pathshape);
|
||||
if (adornment !== null) {
|
||||
link.addAdornment(this.name, adornment);
|
||||
adornment.location = link.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.doDeactivate();
|
||||
}
|
||||
/**
|
||||
* Change the route of the {@link #adornedLink} by moving the segment corresponding to the current
|
||||
* {@link #handle} to be at the given {@link Point}.
|
||||
*/
|
||||
reshape(newpt) {
|
||||
const 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 !== null && this.handle.toMaxLinks === 999) {
|
||||
link.startRoute();
|
||||
let 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());
|
||||
}
|
||||
}
|
||||
const 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 {
|
||||
super.reshape(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.
|
||||
*/
|
||||
makeSegmentDragHandle(link, adornment, index) {
|
||||
const a = link.getPoint(index);
|
||||
let b = link.getPoint(index + 1);
|
||||
const seglength = Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
|
||||
// determine segment orientation
|
||||
let orient = '';
|
||||
if (this.isApprox(a.x, b.x) && this.isApprox(a.y, b.y)) {
|
||||
b = link.getPoint(index - 1);
|
||||
if (this.isApprox(a.x, b.x)) {
|
||||
orient = 'vertical';
|
||||
}
|
||||
else if (this.isApprox(a.y, b.y)) {
|
||||
orient = 'horizontal';
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.isApprox(a.x, b.x)) {
|
||||
orient = 'vertical';
|
||||
}
|
||||
else if (this.isApprox(a.y, b.y)) {
|
||||
orient = 'horizontal';
|
||||
}
|
||||
}
|
||||
// make an invisible handle along the whole segment
|
||||
const h = new go.Shape();
|
||||
h.strokeWidth = 6;
|
||||
h.opacity = 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.
|
||||
*/
|
||||
isApprox(x, y) {
|
||||
const d = x - y;
|
||||
return d < 0.5 && d > -0.5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The OrthogonalLinkReshapingTool class lets a user drag a tool handle along the link segment, which will move the whole segment.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/OrthogonalLinkReshaping.html">Orthogonal Link Reshaping</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class OrthogonalLinkReshapingTool extends go.LinkReshapingTool {
|
||||
/**
|
||||
* Constructs an OrthogonalLinkReshapingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'OrthogonalLinkReshaping';
|
||||
}
|
||||
|
||||
private _alreadyAddedPoint: boolean = false;
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* For orthogonal, straight links, create the handles and set reshaping behavior.
|
||||
*/
|
||||
public makeAdornment(pathshape: go.Shape): go.Adornment {
|
||||
const link = pathshape.part as go.Link;
|
||||
|
||||
// add all normal handles first
|
||||
const adornment = super.makeAdornment(pathshape);
|
||||
|
||||
// add long reshaping handles for orthogonal, straight links
|
||||
if (link !== null && link.isOrthogonal && link.curve !== go.Link.Bezier) {
|
||||
const firstindex = link.firstPickIndex + (link.resegmentable ? 0 : 1);
|
||||
const lastindex = link.lastPickIndex - (link.resegmentable ? 0 : 1);
|
||||
for (let i = firstindex; i < lastindex; i++) {
|
||||
this.makeSegmentDragHandle(link, adornment, i);
|
||||
}
|
||||
}
|
||||
return adornment;
|
||||
}
|
||||
|
||||
/**
|
||||
* This stops the current reshaping operation and updates any link handles.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
this._alreadyAddedPoint = false;
|
||||
// when we finish, recreate adornment to ensure proper reshaping behavior/cursor
|
||||
const link = this.adornedLink;
|
||||
if (link !== null && link.isOrthogonal && link.curve !== go.Link.Bezier) {
|
||||
const pathshape = link.path;
|
||||
if (pathshape !== null) {
|
||||
const adornment = this.makeAdornment(pathshape);
|
||||
if (adornment !== null) {
|
||||
link.addAdornment(this.name, adornment);
|
||||
adornment.location = link.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.doDeactivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the route of the {@link #adornedLink} by moving the segment corresponding to the current
|
||||
* {@link #handle} to be at the given {@link Point}.
|
||||
*/
|
||||
public reshape(newpt: go.Point): void {
|
||||
const 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 !== null && this.handle.toMaxLinks === 999) {
|
||||
link.startRoute();
|
||||
let 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());
|
||||
}
|
||||
}
|
||||
const 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 {
|
||||
super.reshape(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.
|
||||
*/
|
||||
public makeSegmentDragHandle(link: go.Link, adornment: go.Adornment, index: number): void {
|
||||
const a = link.getPoint(index);
|
||||
let b = link.getPoint(index + 1);
|
||||
const seglength = Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
|
||||
// determine segment orientation
|
||||
let orient = '';
|
||||
if (this.isApprox(a.x, b.x) && this.isApprox(a.y, b.y)) {
|
||||
b = link.getPoint(index - 1);
|
||||
if (this.isApprox(a.x, b.x)) {
|
||||
orient = 'vertical';
|
||||
} else if (this.isApprox(a.y, b.y)) {
|
||||
orient = 'horizontal';
|
||||
}
|
||||
} else {
|
||||
if (this.isApprox(a.x, b.x)) {
|
||||
orient = 'vertical';
|
||||
} else if (this.isApprox(a.y, b.y)) {
|
||||
orient = 'horizontal';
|
||||
}
|
||||
}
|
||||
|
||||
// make an invisible handle along the whole segment
|
||||
const h = new go.Shape();
|
||||
h.strokeWidth = 6;
|
||||
h.opacity = 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.
|
||||
*/
|
||||
public isApprox(x: number, y: number): boolean {
|
||||
const d = x - y;
|
||||
return d < 0.5 && d > -0.5;
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Overview Resizing</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { OverviewResizingTool } from './OverviewResizingTool.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const 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) {
|
||||
const 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
|
||||
const 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) {
|
||||
const diagram = node.diagram;
|
||||
if (diagram === null)
|
||||
return;
|
||||
diagram.startTransaction('CollapseExpandTree');
|
||||
// this behavior is specific to this incrementalTree sample:
|
||||
const data = node.data;
|
||||
if (!data.everExpanded) {
|
||||
// only create children once per node
|
||||
diagram.model.setDataProperty(data, 'everExpanded', true);
|
||||
const numchildren = createSubTree(data);
|
||||
if (numchildren === 0) { // now known no children: don't need Button!
|
||||
const btn = node.findObject('TREEBUTTON');
|
||||
if (btn !== null)
|
||||
btn.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) {
|
||||
let 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
|
||||
const model = myDiagram.model;
|
||||
const parent = myDiagram.findNodeForData(parentdata);
|
||||
if (parent === null)
|
||||
return;
|
||||
for (let i = 0; i < numchildren; i++) {
|
||||
const childdata = {
|
||||
key: model.nodeDataArray.length,
|
||||
parent: parentdata.key
|
||||
};
|
||||
// add to model.nodeDataArray and create a Node
|
||||
model.addNodeData(childdata);
|
||||
// position the new child node close to the parent
|
||||
const child = myDiagram.findNodeForData(childdata);
|
||||
if (child !== null)
|
||||
child.location = parent.location;
|
||||
}
|
||||
return numchildren;
|
||||
}
|
||||
function expandAtRandom() {
|
||||
const eligibleNodes = [];
|
||||
myDiagram.nodes.each(function (n) {
|
||||
if (!n.isTreeExpanded)
|
||||
eligibleNodes.push(n);
|
||||
});
|
||||
const node = eligibleNodes[Math.floor(Math.random() * (eligibleNodes.length))];
|
||||
expandNode(node);
|
||||
}
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* 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.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class OverviewResizingTool extends go.ResizingTool {
|
||||
/**
|
||||
* Constructs an OverviewResizingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'OverviewResizing';
|
||||
this._handleSize = new go.Size(6, 6);
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @param {Shape} resizeBox
|
||||
* @return {Adornment}
|
||||
*/
|
||||
makeAdornment(resizeBox) {
|
||||
this._handleSize.setTo(resizeBox.strokeWidth * 3, resizeBox.strokeWidth * 3);
|
||||
// Set up the resize adornment
|
||||
const ad = new go.Adornment();
|
||||
ad.type = go.Panel.Spot;
|
||||
ad.locationSpot = go.Spot.Center;
|
||||
const ph = new go.Placeholder();
|
||||
ph.isPanelMain = true;
|
||||
ad.add(ph);
|
||||
const 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.
|
||||
* @param {Adornment} elt
|
||||
* @param {number} angle
|
||||
*/
|
||||
updateResizeHandles(elt, angle) {
|
||||
if (elt === null)
|
||||
return;
|
||||
const handle = elt.findObject('RSZHND');
|
||||
const 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.
|
||||
* @param {Rect} newr the intended new rectangular bounds the overview box.
|
||||
*/
|
||||
resize(newr) {
|
||||
const overview = this.diagram;
|
||||
const observed = overview.observed;
|
||||
if (observed === null)
|
||||
return;
|
||||
const oldr = observed.viewportBounds.copy();
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class OverviewResizingTool extends go.ResizingTool {
|
||||
// Internal property used to keep track of changing handle size
|
||||
private _handleSize: go.Size;
|
||||
|
||||
/**
|
||||
* Constructs an OverviewResizingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'OverviewResizing';
|
||||
this._handleSize = new go.Size(6, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* @param {Shape} resizeBox
|
||||
* @return {Adornment}
|
||||
*/
|
||||
public makeAdornment(resizeBox: go.Shape): go.Adornment {
|
||||
this._handleSize.setTo(resizeBox.strokeWidth * 3, resizeBox.strokeWidth * 3);
|
||||
// Set up the resize adornment
|
||||
const ad = new go.Adornment();
|
||||
ad.type = go.Panel.Spot;
|
||||
ad.locationSpot = go.Spot.Center;
|
||||
const ph = new go.Placeholder();
|
||||
ph.isPanelMain = true;
|
||||
ad.add(ph);
|
||||
const 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.
|
||||
* @param {Adornment} elt
|
||||
* @param {number} angle
|
||||
*/
|
||||
public updateResizeHandles(elt: go.Adornment, angle: number) {
|
||||
if (elt === null) return;
|
||||
const handle = elt.findObject('RSZHND') as go.Shape;
|
||||
const box = elt.adornedObject as go.Shape;
|
||||
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.
|
||||
* @param {Rect} newr the intended new rectangular bounds the overview box.
|
||||
*/
|
||||
public resize(newr: go.Rect): void {
|
||||
const overview = this.diagram as go.Overview;
|
||||
const observed = overview.observed;
|
||||
if (observed === null) return;
|
||||
const oldr = observed.viewportBounds.copy();
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
<!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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { PackedLayout } from "./PackedLayout.js";
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
// subclass PackedLayout to change default properties and override commitLayout function
|
||||
class HierarchyLayout extends PackedLayout {
|
||||
constructor() {
|
||||
super();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
commitLayout() {
|
||||
if (this.group !== null) {
|
||||
const groupData = keyToNodeDataMap.get(this.group.key);
|
||||
|
||||
const enclosingCircle = this.enclosingCircle;
|
||||
const actualBounds = this.actualBounds;
|
||||
|
||||
const size = new go.Size(enclosingCircle.width, enclosingCircle.width);
|
||||
this.diagram.model.setDataProperty(groupData, "size", size);
|
||||
|
||||
const dx = enclosingCircle.centerX - actualBounds.centerX;
|
||||
const dy = enclosingCircle.centerY - actualBounds.centerY;
|
||||
const position = new go.Point((actualBounds.width - enclosingCircle.width) / 2 + dx, (actualBounds.height - enclosingCircle.height) / 2 + dy);
|
||||
this.diagram.model.setDataProperty(groupData, "position", position);
|
||||
}
|
||||
}
|
||||
} // end HierarchyLayout
|
||||
|
||||
|
||||
const 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
|
||||
const toolTipTemplate =
|
||||
$(go.Adornment, "Auto",
|
||||
$(go.Shape, { fill: "white" }),
|
||||
$(go.TextBlock, { margin: 4 },
|
||||
new go.Binding("text", "tooltip"))
|
||||
);
|
||||
|
||||
const 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) {
|
||||
const 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
|
||||
const 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: []
|
||||
}
|
||||
];
|
||||
|
||||
const keyToNodeDataMap = new go.Map();
|
||||
keyToNodeDataMap.add("GoJS", nodeDataArray[0]);
|
||||
|
||||
// Iterate over all of the classes in "go"
|
||||
for (let k in go) {
|
||||
const cls = go[k];
|
||||
if (!cls) continue;
|
||||
const proto = cls.prototype;
|
||||
if (!proto) continue;
|
||||
proto.constructor.className = k; // remember name
|
||||
|
||||
let propCount = 0; // count number of properties on the class
|
||||
for (let prop in proto) {
|
||||
if (proto.hasOwnProperty(prop)) {
|
||||
propCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const 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
|
||||
const 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 (let i = nodeDataArray.length - 1; i >= 0; i--) {
|
||||
const 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
|
||||
let totalCount = n.propCount;
|
||||
let parentKey = n.group;
|
||||
let 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>
|
||||
</body>
|
||||
</html>
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Packed Layout</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: Arrange nodes into rectangular or elliptical areas, ignoring any links." />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<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 />
|
||||
<input type="radio" name="packShape" onclick="layout()" value="Spiral"> Spiral<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:<br /> <input type="radio" name="sortOrder" onclick="layout()" value="Descending" checked> Descending<br />
|
||||
<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()">
|
||||
</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>
|
||||
The layout is defined in its own file, as <a href="PackedLayout.ts">PackedLayout.ts</a>, with an additional dependency on <a href="Quadtree.ts">Quadtree.ts</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { PackedLayout } from './PackedLayout.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
// nodes need to be randomized again if any of these change
|
||||
let minSidePrevious = 0.0;
|
||||
let maxSidePrevious = 0.0;
|
||||
let sameSidesPrevious = false;
|
||||
|
||||
const myDiagram =
|
||||
$(go.Diagram, 'myDiagramDiv', // must be the ID or reference to div
|
||||
{
|
||||
'animationManager.isEnabled': true,
|
||||
layout: $(PackedLayout),
|
||||
scale: 0.75, isReadOnly: true
|
||||
});
|
||||
// Nodes have a template with bindings for size, shape, and fill color
|
||||
myDiagram.nodeTemplate =
|
||||
$(go.Node, 'Auto', new go.Binding('visible', 'visible'), $(go.Shape, { strokeWidth: 0 }, new go.Binding('figure', 'figure'), new go.Binding('width', 'width'), new go.Binding('height', 'height'), new go.Binding('fill', 'fill')));
|
||||
myDiagram.model = new go.GraphLinksModel([]);
|
||||
// find the elements in the DOM which control configuration
|
||||
const aspectRatio = document.getElementById('aspectRatio');
|
||||
aspectRatio.onkeydown = aspectRatioHandler;
|
||||
const layoutWidth = document.getElementById('width');
|
||||
const layoutHeight = document.getElementById('height');
|
||||
const nodeSpacing = document.getElementById('nodeSpacing');
|
||||
const hasCircularNodes = document.getElementById('hasCircularNodes');
|
||||
const numNodes = document.getElementById('numNodes');
|
||||
const nodeMinSide = document.getElementById('nodeMinSide');
|
||||
const nodeMaxSide = document.getElementById('nodeMaxSide');
|
||||
const sameSides = document.getElementById('sameSides');
|
||||
// create a layout with the default values
|
||||
rebuildGraph();
|
||||
|
||||
// when arrow keys are pressed and the aspect ratio is below 1, increment using the harmonic sequence
|
||||
// this makes the aspect ratio change as follows: 3:1, 2:1, 1:1, 1:2, 1:3, etc.
|
||||
function aspectRatioHandler(e) {
|
||||
if (e.key === 'ArrowUp' && parseFloat(aspectRatio.value) < 1) {
|
||||
e.preventDefault();
|
||||
const denom = Math.round(1 / parseFloat(aspectRatio.value));
|
||||
aspectRatio.value = (+(1 / (denom - 1)).toFixed(2)) + '';
|
||||
rebuildGraph();
|
||||
}
|
||||
else if (e.key === 'ArrowDown' && parseFloat(aspectRatio.value) <= 1) {
|
||||
e.preventDefault();
|
||||
const denom = Math.round(1 / parseFloat(aspectRatio.value));
|
||||
if (denom < 10) {
|
||||
aspectRatio.value = (+(1 / (denom + 1)).toFixed(2)) + '';
|
||||
rebuildGraph();
|
||||
}
|
||||
}
|
||||
}
|
||||
function validateInput() {
|
||||
if (!aspectRatio.value || parseFloat(aspectRatio.value) <= 0) {
|
||||
aspectRatio.value = '0.1';
|
||||
}
|
||||
if (!layoutWidth.value || parseFloat(layoutWidth.value) <= 0) {
|
||||
layoutWidth.value = '1';
|
||||
}
|
||||
if (!layoutHeight.value || parseFloat(layoutHeight.value) <= 0) {
|
||||
layoutHeight.value = '1';
|
||||
}
|
||||
if (!nodeSpacing.value) {
|
||||
nodeSpacing.value = '0';
|
||||
}
|
||||
if (!numNodes.value || parseInt(numNodes.value) < 1) {
|
||||
numNodes.value = '1';
|
||||
}
|
||||
if (!nodeMinSide.value || parseFloat(nodeMinSide.value) < 1) {
|
||||
nodeMinSide.value = '1';
|
||||
}
|
||||
if (!nodeMaxSide.value || parseFloat(nodeMaxSide.value) < 1) {
|
||||
nodeMaxSide.value = '1';
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildGraph() {
|
||||
validateInput();
|
||||
let packShape = PackedLayout.Elliptical;
|
||||
switch (getRadioValue('packShape')) {
|
||||
case 'Elliptical':
|
||||
packShape = PackedLayout.Elliptical;
|
||||
break;
|
||||
case 'Rectangular':
|
||||
packShape = PackedLayout.Rectangular;
|
||||
break;
|
||||
case 'Spiral':
|
||||
packShape = PackedLayout.Spiral;
|
||||
break;
|
||||
}
|
||||
let packMode = PackedLayout.AspectOnly;
|
||||
switch (getRadioValue('packMode')) {
|
||||
case 'AspectOnly':
|
||||
packMode = PackedLayout.AspectOnly;
|
||||
break;
|
||||
case 'Fit':
|
||||
packMode = PackedLayout.Fit;
|
||||
break;
|
||||
case 'ExpandToFit':
|
||||
packMode = PackedLayout.ExpandToFit;
|
||||
break;
|
||||
}
|
||||
let sortMode = PackedLayout.None;
|
||||
switch (getRadioValue('sortMode')) {
|
||||
case 'None':
|
||||
sortMode = PackedLayout.None;
|
||||
break;
|
||||
case 'MaxSide':
|
||||
sortMode = PackedLayout.MaxSide;
|
||||
break;
|
||||
case 'Area':
|
||||
sortMode = PackedLayout.Area;
|
||||
break;
|
||||
}
|
||||
let sortOrder = PackedLayout.Descending;
|
||||
switch (getRadioValue('sortOrder')) {
|
||||
case 'Descending':
|
||||
sortOrder = PackedLayout.Descending;
|
||||
break;
|
||||
case 'Ascending':
|
||||
sortOrder = PackedLayout.Ascending;
|
||||
break;
|
||||
}
|
||||
const params = {
|
||||
packMode: packMode,
|
||||
packShape: packShape,
|
||||
sortMode: sortMode,
|
||||
sortOrder: sortOrder,
|
||||
aspectRatio: parseFloat(aspectRatio.value),
|
||||
size: new go.Size(parseFloat(layoutWidth.value), parseFloat(layoutHeight.value)),
|
||||
spacing: parseFloat(nodeSpacing.value),
|
||||
hasCircularNodes: hasCircularNodes.checked,
|
||||
arrangesToOrigin: false
|
||||
};
|
||||
disableInputs(params);
|
||||
if (sameSides.checked !== sameSidesPrevious
|
||||
|| parseFloat(nodeMinSide.value) !== minSidePrevious
|
||||
|| parseFloat(nodeMaxSide.value) !== maxSidePrevious) {
|
||||
sameSidesPrevious = sameSides.checked;
|
||||
minSidePrevious = parseFloat(nodeMinSide.value);
|
||||
maxSidePrevious = parseFloat(nodeMaxSide.value);
|
||||
randomize();
|
||||
return;
|
||||
}
|
||||
myDiagram.startTransaction('packed layout');
|
||||
generateNodeData();
|
||||
myDiagram.layout = go.GraphObject.make(PackedLayout, params /* defined above */);
|
||||
myDiagram.commitTransaction('packed layout');
|
||||
}
|
||||
|
||||
function randomize() {
|
||||
myDiagram.model = new go.GraphLinksModel([]);
|
||||
rebuildGraph();
|
||||
}
|
||||
window.randomize = randomize;
|
||||
|
||||
function generateNodeData() {
|
||||
const nodeDataArray = myDiagram.model.nodeDataArray;
|
||||
const count = parseInt(numNodes.value);
|
||||
const min = parseFloat(nodeMinSide.value);
|
||||
const max = parseFloat(nodeMaxSide.value);
|
||||
const shapeToPack = getRadioValue('shapeToPack');
|
||||
if (count > nodeDataArray.length) {
|
||||
const arr = new Array();
|
||||
for (let i = nodeDataArray.length; i < count; i++) {
|
||||
const width = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const height = sameSides.checked ? width : Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const color = go.Brush.randomColor(128, 235);
|
||||
arr.push({ width: width, height: height, fill: color, figure: shapeToPack });
|
||||
}
|
||||
myDiagram.model.addNodeDataCollection(arr);
|
||||
}
|
||||
else if (count < nodeDataArray.length) {
|
||||
while (count < nodeDataArray.length) {
|
||||
myDiagram.model.removeNodeData(nodeDataArray[nodeDataArray.length - 1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const data of nodeDataArray) {
|
||||
myDiagram.model.set(data, 'figure', shapeToPack);
|
||||
}
|
||||
}
|
||||
sameSidesPrevious = sameSides.checked;
|
||||
minSidePrevious = min;
|
||||
maxSidePrevious = max;
|
||||
}
|
||||
var hasCircularNodesSavedState = null;
|
||||
var sameSidesSavedState = null;
|
||||
function disableInputs(params) {
|
||||
setRadioButtonsDisabled('packMode', params.packShape === PackedLayout.Spiral);
|
||||
aspectRatio.disabled = params.packMode !== PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral;
|
||||
layoutWidth.disabled = params.packMode === PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral;
|
||||
layoutHeight.disabled = params.packMode === PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral;
|
||||
nodeSpacing.disabled = params.packMode === PackedLayout.ExpandToFit;
|
||||
hasCircularNodes.disabled = params.packShape === PackedLayout.Spiral;
|
||||
if (params.packShape === PackedLayout.Spiral) {
|
||||
if (hasCircularNodesSavedState === null) {
|
||||
hasCircularNodesSavedState = hasCircularNodes.checked;
|
||||
}
|
||||
hasCircularNodes.checked = true;
|
||||
params.hasCircularNodes = true;
|
||||
}
|
||||
else if (hasCircularNodesSavedState !== null) {
|
||||
hasCircularNodes.checked = hasCircularNodesSavedState;
|
||||
params.hasCircularNodes = false;
|
||||
hasCircularNodesSavedState = null;
|
||||
}
|
||||
sameSides.disabled = params.hasCircularNodes;
|
||||
if (params.hasCircularNodes) {
|
||||
if (sameSidesSavedState === null) {
|
||||
sameSidesSavedState = sameSides.checked;
|
||||
}
|
||||
sameSides.checked = true;
|
||||
}
|
||||
else if (sameSidesSavedState !== null) {
|
||||
sameSides.checked = sameSidesSavedState;
|
||||
sameSidesSavedState = null;
|
||||
}
|
||||
}
|
||||
function getRadioValue(name) {
|
||||
const radio = document.getElementsByName(name);
|
||||
for (let i = 0; i < radio.length; i++) {
|
||||
if (radio[i].checked)
|
||||
return radio[i].value;
|
||||
}
|
||||
}
|
||||
function setRadioButtonsDisabled(name, disabled) {
|
||||
const radio = document.getElementsByName(name);
|
||||
for (let i = 0; i < radio.length; i++) {
|
||||
radio[i].disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("randomizeGraph").onclick = randomize;
|
||||
|
||||
function layout() {
|
||||
rebuildGraph();
|
||||
}
|
||||
window.layout = layout; // make accessible to button onclick event handlers
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1926
File diff suppressed because it is too large
Load Diff
+2071
File diff suppressed because it is too large
Load Diff
+156
@@ -0,0 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Parallel Layout</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">ParallelLayout.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { ParallelLayout } from './ParallelLayout.js';
|
||||
|
||||
if (window.goSamples) window.goSamples(); // init for these samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const myDiagram = $(go.Diagram, 'myDiagramDiv', // must be the ID or reference to div
|
||||
{
|
||||
allowCopy: false,
|
||||
allowDelete: false,
|
||||
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" })
|
||||
);
|
||||
|
||||
myDiagram.model = new go.GraphLinksModel([
|
||||
{ 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' }
|
||||
], [
|
||||
{ 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 }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Parallel.html">Parallel Layout</a> sample.
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class ParallelLayout extends go.TreeLayout {
|
||||
/**
|
||||
* Constructs a ParallelLayout and sets the following properties:
|
||||
* - {@link #isRealtime} = false
|
||||
* - {@link #alignment} = {@link TreeLayout.AlignmentCenterChildren}
|
||||
* - {@link #compaction} = {@link TreeLayout.CompactionNone}
|
||||
* - {@link #alternateAlignment} = {@link TreeLayout.AlignmentCenterChildren}
|
||||
* - {@link #alternateCompaction} = {@link TreeLayout.CompactionNone}
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._splitNode = null;
|
||||
this._mergeNode = null;
|
||||
this.isRealtime = false;
|
||||
this.alignment = go.TreeLayout.AlignmentCenterChildren;
|
||||
this.compaction = go.TreeLayout.CompactionNone;
|
||||
this.alternateAlignment = go.TreeLayout.AlignmentCenterChildren;
|
||||
this.alternateCompaction = go.TreeLayout.CompactionNone;
|
||||
}
|
||||
/**
|
||||
* This read-only property returns the node that the tree will extend from.
|
||||
*/
|
||||
get splitNode() { return this._splitNode; }
|
||||
set splitNode(val) { this._splitNode = val; }
|
||||
/**
|
||||
* This read-only property returns the node that the tree will converge at.
|
||||
*/
|
||||
get mergeNode() { return this._mergeNode; }
|
||||
set mergeNode(val) { this._mergeNode = val; }
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
isSplit(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}
|
||||
*/
|
||||
isMerge(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}
|
||||
*/
|
||||
isConditional(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}
|
||||
*/
|
||||
isSwitch(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 {Iterable<TreeVertex>} vertexes
|
||||
*/
|
||||
findSplitMerge(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
|
||||
*/
|
||||
makeNetwork(coll) {
|
||||
const net = super.makeNetwork(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.iterator);
|
||||
// don't have TreeLayout lay out the Merge node; commitNodes will do it
|
||||
if (this.mergeNode)
|
||||
net.deleteNode(this.mergeNode);
|
||||
return net;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
commitNodes() {
|
||||
super.commitNodes();
|
||||
// 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
|
||||
*/
|
||||
commitLinks() {
|
||||
const splitNode = this.splitNode;
|
||||
const mergeNode = this.mergeNode;
|
||||
if (splitNode === null || mergeNode === null || this.network === null)
|
||||
return;
|
||||
// set default link spots based on this.angle
|
||||
const it = this.network.edges.iterator;
|
||||
while (it.next()) {
|
||||
const e = it.value;
|
||||
const 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
|
||||
const cond = this.isConditional(splitNode);
|
||||
const swtch = this.isSwitch(splitNode);
|
||||
// Handle links going out of the Split node
|
||||
let first = true; // handle "If" nodes specially
|
||||
const lit = splitNode.findLinksOutOf();
|
||||
while (lit.next()) {
|
||||
const 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
|
||||
const iit = mergeNode.findLinksInto();
|
||||
while (iit.next()) {
|
||||
const link = iit.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) {
|
||||
const pts = link.points.copy();
|
||||
const p2 = pts.elt(pts.length - 4);
|
||||
const p3 = pts.elt(pts.length - 3);
|
||||
if (this.angle === 0 && p2.x === p3.x) {
|
||||
const 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) {
|
||||
const 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
|
||||
const oit = mergeNode.findLinksOutOf();
|
||||
while (oit.next()) {
|
||||
const link = oit.value;
|
||||
// if connects internal with external node, it isn't a loop-back link
|
||||
if (link.toNode && 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/Parallel.html">Parallel Layout</a> sample.
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class ParallelLayout extends go.TreeLayout {
|
||||
private _splitNode: go.Node | null = null;
|
||||
private _mergeNode: go.Node | null = null;
|
||||
|
||||
/**
|
||||
* Constructs a ParallelLayout and sets the following properties:
|
||||
* - {@link #isRealtime} = false
|
||||
* - {@link #alignment} = {@link TreeLayout.AlignmentCenterChildren}
|
||||
* - {@link #compaction} = {@link TreeLayout.CompactionNone}
|
||||
* - {@link #alternateAlignment} = {@link TreeLayout.AlignmentCenterChildren}
|
||||
* - {@link #alternateCompaction} = {@link TreeLayout.CompactionNone}
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.isRealtime = false;
|
||||
this.alignment = go.TreeLayout.AlignmentCenterChildren;
|
||||
this.compaction = go.TreeLayout.CompactionNone;
|
||||
this.alternateAlignment = go.TreeLayout.AlignmentCenterChildren;
|
||||
this.alternateCompaction = go.TreeLayout.CompactionNone;
|
||||
}
|
||||
|
||||
/**
|
||||
* This read-only property returns the node that the tree will extend from.
|
||||
*/
|
||||
get splitNode(): go.Node | null { return this._splitNode; }
|
||||
set splitNode(val: go.Node | null) { this._splitNode = val; }
|
||||
|
||||
/**
|
||||
* This read-only property returns the node that the tree will converge at.
|
||||
*/
|
||||
get mergeNode(): go.Node | null { return this._mergeNode; }
|
||||
set mergeNode(val: go.Node | null) { this._mergeNode = val; }
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
isSplit(node: go.Node | null): boolean {
|
||||
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}
|
||||
*/
|
||||
isMerge(node: go.Node | null): boolean {
|
||||
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}
|
||||
*/
|
||||
isConditional(node: go.Node | null): boolean {
|
||||
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}
|
||||
*/
|
||||
isSwitch(node: go.Node | null): boolean {
|
||||
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 {Iterable<TreeVertex>} vertexes
|
||||
*/
|
||||
findSplitMerge(vertexes: go.Iterable<go.TreeVertex>): void {
|
||||
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
|
||||
*/
|
||||
public makeNetwork(coll: go.Iterable<go.Part>): go.TreeNetwork {
|
||||
const net = super.makeNetwork(coll) as go.TreeNetwork;
|
||||
// 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.iterator as go.Iterator<go.TreeVertex>);
|
||||
// don't have TreeLayout lay out the Merge node; commitNodes will do it
|
||||
if (this.mergeNode) net.deleteNode(this.mergeNode);
|
||||
return net;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
*/
|
||||
public commitNodes(): void {
|
||||
super.commitNodes();
|
||||
// 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) as go.TreeVertex;
|
||||
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
|
||||
*/
|
||||
public commitLinks(): void {
|
||||
const splitNode = this.splitNode;
|
||||
const mergeNode = this.mergeNode;
|
||||
if (splitNode === null || mergeNode === null || this.network === null) return;
|
||||
// set default link spots based on this.angle
|
||||
const it = this.network.edges.iterator;
|
||||
while (it.next()) {
|
||||
const e = it.value;
|
||||
const 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
|
||||
const cond = this.isConditional(splitNode);
|
||||
const swtch = this.isSwitch(splitNode);
|
||||
// Handle links going out of the Split node
|
||||
let first = true; // handle "If" nodes specially
|
||||
const lit = splitNode.findLinksOutOf();
|
||||
while (lit.next()) {
|
||||
const 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
|
||||
const iit = mergeNode.findLinksInto();
|
||||
while (iit.next()) {
|
||||
const link = iit.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) {
|
||||
const pts = link.points.copy();
|
||||
const p2 = pts.elt(pts.length - 4);
|
||||
const p3 = pts.elt(pts.length - 3);
|
||||
if (this.angle === 0 && p2.x === p3.x) {
|
||||
const 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) {
|
||||
const 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
|
||||
const oit = mergeNode.findLinksOutOf();
|
||||
while (oit.next()) {
|
||||
const link = oit.value;
|
||||
// if connects internal with external node, it isn't a loop-back link
|
||||
if (link.toNode && 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Parallel Route Links</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { ParallelRouteLink } from './ParallelRouteLink.js';
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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', (node) => node.port.fill).ofObject()), $(go.Shape, { toArrow: 'OpenTriangle', strokeWidth: 1.5 }, new go.Binding('stroke', 'fromNode', (node) => 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 }
|
||||
]);
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* This custom {@link 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/ParallelRoute.html">Parallel Route Links</a> sample.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class ParallelRouteLink extends go.Link {
|
||||
/**
|
||||
* Constructs the link's route by modifying {@link #points}.
|
||||
* @return {boolean} true if it computed a route of points
|
||||
*/
|
||||
computePoints() {
|
||||
const result = super.computePoints();
|
||||
if (!this.isOrthogonal && this.curve !== go.Link.Bezier && this.hasCurviness()) {
|
||||
const curv = this.computeCurviness();
|
||||
if (curv !== 0) {
|
||||
const num = this.pointsCount;
|
||||
let pidx = 0;
|
||||
let qidx = num - 1;
|
||||
if (num >= 4) {
|
||||
pidx++;
|
||||
qidx--;
|
||||
}
|
||||
const frompt = this.getPoint(pidx);
|
||||
const topt = this.getPoint(qidx);
|
||||
const dx = topt.x - frompt.x;
|
||||
const dy = topt.y - frompt.y;
|
||||
let mx = frompt.x + dx * 1 / 8;
|
||||
let my = frompt.y + dy * 1 / 8;
|
||||
let px = mx;
|
||||
let py = my;
|
||||
if (-0.01 < dy && dy < 0.01) {
|
||||
if (dx > 0)
|
||||
py -= curv;
|
||||
else
|
||||
py += curv;
|
||||
}
|
||||
else {
|
||||
const slope = -dx / dy;
|
||||
let 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;
|
||||
let qx = mx;
|
||||
let qy = my;
|
||||
if (-0.01 < dy && dy < 0.01) {
|
||||
if (dx > 0)
|
||||
qy -= curv;
|
||||
else
|
||||
qy += curv;
|
||||
}
|
||||
else {
|
||||
const slope = -dx / dy;
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* This custom {@link 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/ParallelRoute.html">Parallel Route Links</a> sample.
|
||||
* @category Part Extension
|
||||
*/
|
||||
export class ParallelRouteLink extends go.Link {
|
||||
/**
|
||||
* Constructs the link's route by modifying {@link #points}.
|
||||
* @return {boolean} true if it computed a route of points
|
||||
*/
|
||||
public computePoints(): boolean {
|
||||
const result = super.computePoints();
|
||||
if (!this.isOrthogonal && this.curve !== go.Link.Bezier && this.hasCurviness()) {
|
||||
const curv = this.computeCurviness();
|
||||
if (curv !== 0) {
|
||||
const num = this.pointsCount;
|
||||
let pidx = 0;
|
||||
let qidx = num - 1;
|
||||
if (num >= 4) {
|
||||
pidx++;
|
||||
qidx--;
|
||||
}
|
||||
|
||||
const frompt = this.getPoint(pidx);
|
||||
const topt = this.getPoint(qidx);
|
||||
const dx = topt.x - frompt.x;
|
||||
const dy = topt.y - frompt.y;
|
||||
|
||||
let mx = frompt.x + dx * 1 / 8;
|
||||
let my = frompt.y + dy * 1 / 8;
|
||||
let px = mx;
|
||||
let py = my;
|
||||
if (-0.01 < dy && dy < 0.01) {
|
||||
if (dx > 0) py -= curv; else py += curv;
|
||||
} else {
|
||||
const slope = -dx / dy;
|
||||
let 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;
|
||||
let qx = mx;
|
||||
let qy = my;
|
||||
if (-0.01 < dy && dy < 0.01) {
|
||||
if (dx > 0) qy -= curv; else qy += curv;
|
||||
} else {
|
||||
const slope = -dx / dy;
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Polygon Drawing Tool</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="sample">
|
||||
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 350px"></div>
|
||||
<div id="buttons">
|
||||
<button type="button" id="select">Select</button>
|
||||
<button type="button" id="drawPolygon">Draw Polygon</button>
|
||||
<button type="button" id="drawPolyline">Draw Polyline</button>
|
||||
<button type="button" id="finishDrawing">Finish Drawing</button>
|
||||
<button type="button" id="cancelDrawing">Cancel Drawing</button>
|
||||
<button type="button" id="undo">Undo Last Point</button>
|
||||
<br/>
|
||||
<label><input type="checkbox" id = "allowResizing" checked="checked" />Allow Resizing</label>
|
||||
<label><input type="checkbox" id = "allowReshaping" checked="checked" />Allow Reshaping</label>
|
||||
<label><input type="checkbox" id = "allowRotating" checked="checked" />Allow Rotating</label>
|
||||
<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.ts">PolygonDrawingTool.ts</a>. It also demonstrates the GeometryReshapingTool,
|
||||
another custom tool, defined in <a href="GeometryReshapingTool.ts">GeometryReshapingTool.ts</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>
|
||||
<div>
|
||||
<button id="save">Save</button>
|
||||
<button id="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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { GeometryReshapingTool } from './GeometryReshapingTool.js';
|
||||
import { PolygonDrawingTool } from './PolygonDrawingTool.js';
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const 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
|
||||
const 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
|
||||
const 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) {
|
||||
const tool = myDiagram.currentTool;
|
||||
if (commit && tool instanceof PolygonDrawingTool) {
|
||||
const lastInput = myDiagram.lastInput;
|
||||
if (lastInput.event instanceof 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() {
|
||||
const tool = myDiagram.currentTool;
|
||||
if (tool instanceof PolygonDrawingTool) {
|
||||
const lastInput = myDiagram.lastInput;
|
||||
if (lastInput.event instanceof MouseEvent)
|
||||
tool.removeLastPoint(); // remove point from last mouse-down
|
||||
tool.undo();
|
||||
}
|
||||
}
|
||||
function updateAllAdornments() {
|
||||
myDiagram.selection.each((p) => { p.updateAdornments(); });
|
||||
}
|
||||
// save a model to and load a model from Json text, displayed below the Diagram
|
||||
function save() {
|
||||
const str = '{ "position": "' + go.Point.stringify(myDiagram.position) + '",\n "model": ' + myDiagram.model.toJson() + ' }';
|
||||
document.getElementById('mySavedDiagram').value = str;
|
||||
}
|
||||
function load() {
|
||||
const str = document.getElementById('mySavedDiagram').value;
|
||||
try {
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
function select() {
|
||||
mode(false, false);
|
||||
}
|
||||
function drawPolygon() {
|
||||
mode(true, true);
|
||||
}
|
||||
function drawPolyline() {
|
||||
mode(true, false);
|
||||
}
|
||||
function finishDrawing() {
|
||||
finish(true);
|
||||
}
|
||||
function cancelDrawing() {
|
||||
finish(false);
|
||||
}
|
||||
function allowResizing() {
|
||||
myDiagram.allowResize = !myDiagram.allowResize;
|
||||
updateAllAdornments();
|
||||
}
|
||||
function allowReshaping() {
|
||||
myDiagram.allowReshape = !myDiagram.allowReshape;
|
||||
updateAllAdornments();
|
||||
}
|
||||
function allowRotating() {
|
||||
myDiagram.allowRotate = !myDiagram.allowRotate;
|
||||
updateAllAdornments();
|
||||
}
|
||||
|
||||
document.getElementById("select").onclick = select;
|
||||
document.getElementById("drawPolygon").onclick = drawPolygon;
|
||||
document.getElementById("drawPolyline").onclick = drawPolyline;
|
||||
document.getElementById("finishDrawing").onclick = finishDrawing;
|
||||
document.getElementById("cancelDrawing").onclick = cancelDrawing;
|
||||
document.getElementById("undo").onclick = undo;
|
||||
document.getElementById("allowResizing").onclick = allowResizing;
|
||||
document.getElementById("allowReshaping").onclick = allowReshaping;
|
||||
document.getElementById("allowRotating").onclick = allowRotating;
|
||||
document.getElementById("save").onclick = save;
|
||||
document.getElementById("load").onclick = load;
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The PolygonDrawingTool class lets the user draw a new polygon or polyline shape by clicking where the corners should go.
|
||||
* Right click or type ENTER to finish the operation.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/PolygonDrawing.html">Polygon Drawing</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class PolygonDrawingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs an PolygonDrawingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
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
|
||||
this.temp = go.GraphObject.make(go.Part, { layerName: 'Tool' }, this._temporaryShape);
|
||||
this.name = 'PolygonDrawing';
|
||||
}
|
||||
/**
|
||||
* Gets or sets whether this tools draws a filled polygon or an unfilled open polyline.
|
||||
*
|
||||
* The default value is true.
|
||||
*/
|
||||
get isPolygon() { return this._isPolygon; }
|
||||
set isPolygon(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.
|
||||
*/
|
||||
get hasArcs() { return this._hasArcs; }
|
||||
set hasArcs(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.
|
||||
*/
|
||||
get isOrthoOnly() { return this._isOrthoOnly; }
|
||||
set isOrthoOnly(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
|
||||
*/
|
||||
get isGridSnapEnabled() { return this._isGridSnapEnabled; }
|
||||
set isGridSnapEnabled(val) { this._isGridSnapEnabled = val; }
|
||||
/**
|
||||
* Gets or sets the node data object that is copied and added to the model
|
||||
* when the drawing operation completes.
|
||||
*/
|
||||
get archetypePartData() { return this._archetypePartData; }
|
||||
set archetypePartData(val) { this._archetypePartData = 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.
|
||||
*/
|
||||
get temporaryShape() { return this._temporaryShape; }
|
||||
set temporaryShape(val) {
|
||||
if (this._temporaryShape !== val && val !== null) {
|
||||
val.name = 'SHAPE';
|
||||
const panel = this._temporaryShape.panel;
|
||||
if (panel !== null) {
|
||||
panel.remove(this._temporaryShape);
|
||||
this._temporaryShape = val;
|
||||
panel.add(this._temporaryShape);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
canStart() {
|
||||
if (!this.isEnabled)
|
||||
return false;
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly)
|
||||
return false;
|
||||
const 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
|
||||
const 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}.
|
||||
*/
|
||||
doActivate() {
|
||||
super.doActivate();
|
||||
const 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.
|
||||
*/
|
||||
doDeactivate() {
|
||||
super.doDeactivate();
|
||||
const diagram = this.diagram;
|
||||
if (this.temporaryShape !== null && this.temporaryShape.part !== null) {
|
||||
diagram.remove(this.temporaryShape.part);
|
||||
}
|
||||
diagram.currentCursor = '';
|
||||
if (diagram.isMouseCaptured)
|
||||
diagram.isMouseCaptured = false;
|
||||
this.stopTransaction();
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Given a potential Point for the next segment, return a Point it to snap to the grid, and remain orthogonal, if either is applicable.
|
||||
*/
|
||||
modifyPointForGrid(p) {
|
||||
const pregrid = p.copy();
|
||||
const grid = this.diagram.grid;
|
||||
if (grid !== null && grid.visible && this.isGridSnapEnabled) {
|
||||
const cell = grid.gridCellSize;
|
||||
const 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;
|
||||
const geometry = this.temporaryShape.geometry;
|
||||
if (geometry === null)
|
||||
return p;
|
||||
const fig = geometry.figures.first();
|
||||
if (fig === null)
|
||||
return p;
|
||||
const segments = fig.segments;
|
||||
if (this.isOrthoOnly && segments.count > 0) {
|
||||
let lastPt = new go.Point(fig.startX, fig.startY); // assuming segments.count === 1
|
||||
if (segments.count > 1) {
|
||||
// the last segment is the current temporary segment, which we might be altering. We want the segment before
|
||||
const 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;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* This internal method adds a segment to the geometry of the {@link #temporaryShape}.
|
||||
*/
|
||||
addPoint(p) {
|
||||
const diagram = this.diagram;
|
||||
const shape = this.temporaryShape;
|
||||
if (shape === null)
|
||||
return;
|
||||
// for the temporary Shape, normalize the geometry to be in the viewport
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
const q = this.modifyPointForGrid(new go.Point(p.x - viewpt.x, p.y - viewpt.y));
|
||||
const part = shape.part;
|
||||
let geo = null;
|
||||
// if it's not in the Diagram, re-initialize the Shape's geometry and add the Part to the Diagram
|
||||
if (part !== null && part.diagram === null) {
|
||||
const fig = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill
|
||||
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);
|
||||
diagram.add(part);
|
||||
}
|
||||
else if (shape.geometry !== null) {
|
||||
// must copy whole Geometry in order to add a PathSegment
|
||||
geo = shape.geometry.copy();
|
||||
const fig = geo.figures.first();
|
||||
if (fig !== null) {
|
||||
if (this.hasArcs) {
|
||||
const 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;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* This internal method changes the last segment of the geometry of the {@link #temporaryShape} to end at the given point.
|
||||
*/
|
||||
moveLastPoint(p) {
|
||||
p = this.modifyPointForGrid(p);
|
||||
const diagram = this.diagram;
|
||||
// must copy whole Geometry in order to change a PathSegment
|
||||
const shape = this.temporaryShape;
|
||||
if (shape.geometry === null)
|
||||
return;
|
||||
const geo = shape.geometry.copy();
|
||||
const fig = geo.figures.first();
|
||||
if (fig === null)
|
||||
return;
|
||||
const segs = fig.segments;
|
||||
if (segs.count > 0) {
|
||||
// for the temporary Shape, normalize the geometry to be in the viewport
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
const 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) {
|
||||
let prevx = 0.0;
|
||||
let prevy = 0.0;
|
||||
if (segs.count > 1) {
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* This internal method removes the last segment of the geometry of the {@link #temporaryShape}.
|
||||
*/
|
||||
removeLastPoint() {
|
||||
// must copy whole Geometry in order to remove a PathSegment
|
||||
const shape = this.temporaryShape;
|
||||
if (shape.geometry === null)
|
||||
return;
|
||||
const geo = shape.geometry.copy();
|
||||
const fig = geo.figures.first();
|
||||
if (fig === null)
|
||||
return;
|
||||
const segs = fig.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}.
|
||||
*/
|
||||
finishShape() {
|
||||
const diagram = this.diagram;
|
||||
const 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();
|
||||
const tempgeo = shape.geometry;
|
||||
// require 3 points (2 segments) if polygon; 2 points (1 segment) if polyline
|
||||
if (tempgeo !== null) {
|
||||
const tempfig = tempgeo.figures.first();
|
||||
if (tempfig !== null && tempfig.segments.count >= (this.isPolygon ? 2 : 1)) {
|
||||
// normalize geometry and node position
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
const copygeo = tempgeo.copy();
|
||||
const copyfig = copygeo.figures.first();
|
||||
if (this.isPolygon && copyfig !== null) {
|
||||
// if polygon, close the last segment
|
||||
const segs = copyfig.segments;
|
||||
const seg = segs.elt(segs.count - 1);
|
||||
seg.isClosed = true;
|
||||
}
|
||||
// create the node data for the model
|
||||
const d = diagram.model.copyNodeData(this.archetypePartData);
|
||||
if (d !== null) {
|
||||
// adding data to model creates the actual Part
|
||||
diagram.model.addNodeData(d);
|
||||
const part = diagram.findPartForData(d);
|
||||
if (part !== null) {
|
||||
// assign the position for the whole Part
|
||||
const pos = copygeo.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
|
||||
const pShape = part.findObject('SHAPE');
|
||||
if (pShape !== null)
|
||||
pShape.geometry = copygeo;
|
||||
this.transactionResult = this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Add another point to the geometry of the {@link #temporaryShape}.
|
||||
*/
|
||||
doMouseDown() {
|
||||
const diagram = this.diagram;
|
||||
if (!this.isActive) {
|
||||
this.doActivate();
|
||||
}
|
||||
// a new temporary end point, the previous one is now "accepted"
|
||||
this.addPoint(diagram.lastInput.documentPoint);
|
||||
if (!diagram.lastInput.left) { // e.g. right mouse down
|
||||
this.finishShape();
|
||||
}
|
||||
else if (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.
|
||||
*/
|
||||
doMouseMove() {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
this.moveLastPoint(diagram.lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Do not stop this tool, but continue to accumulate Points via mouse-down events.
|
||||
*/
|
||||
doMouseUp() {
|
||||
// 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}.
|
||||
*
|
||||
* Typing the "Z" key causes the previous point to be discarded.
|
||||
*
|
||||
* Typing the "ESCAPE" key causes the temporary Shape and its geometry to be discarded and this tool to be stopped.
|
||||
*/
|
||||
doKeyDown() {
|
||||
const diagram = this.diagram;
|
||||
if (!this.isActive)
|
||||
return;
|
||||
const e = diagram.lastInput;
|
||||
if (e.key === '\r') { // accept
|
||||
this.finishShape(); // all done!
|
||||
}
|
||||
else if (e.key === 'Z') { // undo
|
||||
this.undo();
|
||||
}
|
||||
else {
|
||||
super.doKeyDown();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Undo: remove the last point and continue the drawing of new points.
|
||||
*/
|
||||
undo() {
|
||||
const diagram = this.diagram;
|
||||
// remove a point, and then treat the last one as a temporary one
|
||||
this.removeLastPoint();
|
||||
const lastInput = diagram.lastInput;
|
||||
if (lastInput.event instanceof MouseEvent)
|
||||
this.moveLastPoint(lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The PolygonDrawingTool class lets the user draw a new polygon or polyline shape by clicking where the corners should go.
|
||||
* Right click or type ENTER to finish the operation.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/PolygonDrawing.html">Polygon Drawing</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class PolygonDrawingTool extends go.Tool {
|
||||
private _isPolygon: boolean = true;
|
||||
private _hasArcs: boolean = false;
|
||||
private _isOrthoOnly: boolean = false;
|
||||
private _isGridSnapEnabled: boolean = false;
|
||||
private _archetypePartData: go.ObjectData= {}; // the data to copy for a new polygon Part
|
||||
|
||||
// this is the Shape that is shown during a drawing operation
|
||||
private _temporaryShape: go.Shape = 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
|
||||
private temp: go.Part = go.GraphObject.make(go.Part, { layerName: 'Tool' }, this._temporaryShape);
|
||||
|
||||
/**
|
||||
* Constructs an PolygonDrawingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'PolygonDrawing';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets whether this tools draws a filled polygon or an unfilled open polyline.
|
||||
*
|
||||
* The default value is true.
|
||||
*/
|
||||
get isPolygon(): boolean { return this._isPolygon; }
|
||||
set isPolygon(val: boolean) { 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.
|
||||
*/
|
||||
get hasArcs(): boolean { return this._hasArcs; }
|
||||
set hasArcs(val: boolean) { 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.
|
||||
*/
|
||||
get isOrthoOnly(): boolean { return this._isOrthoOnly; }
|
||||
set isOrthoOnly(val: boolean) { 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
|
||||
*/
|
||||
get isGridSnapEnabled(): boolean { return this._isGridSnapEnabled; }
|
||||
set isGridSnapEnabled(val: boolean) { this._isGridSnapEnabled = val; }
|
||||
|
||||
/**
|
||||
* Gets or sets the node data object that is copied and added to the model
|
||||
* when the drawing operation completes.
|
||||
*/
|
||||
get archetypePartData(): go.ObjectData { return this._archetypePartData; }
|
||||
set archetypePartData(val: go.ObjectData) { this._archetypePartData = 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.
|
||||
*/
|
||||
get temporaryShape(): go.Shape { return this._temporaryShape; }
|
||||
set temporaryShape(val: go.Shape) {
|
||||
if (this._temporaryShape !== val && val !== null) {
|
||||
val.name = 'SHAPE';
|
||||
const panel = this._temporaryShape.panel;
|
||||
if (panel !== null) {
|
||||
panel.remove(this._temporaryShape);
|
||||
this._temporaryShape = val;
|
||||
panel.add(this._temporaryShape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
if (!this.isEnabled) return false;
|
||||
const diagram = this.diagram;
|
||||
if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
|
||||
const 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
|
||||
const 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}.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
super.doActivate();
|
||||
const 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.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
super.doDeactivate();
|
||||
const diagram = this.diagram;
|
||||
if (this.temporaryShape !== null && this.temporaryShape.part !== null) {
|
||||
diagram.remove(this.temporaryShape.part);
|
||||
}
|
||||
diagram.currentCursor = '';
|
||||
if (diagram.isMouseCaptured) diagram.isMouseCaptured = false;
|
||||
this.stopTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Given a potential Point for the next segment, return a Point it to snap to the grid, and remain orthogonal, if either is applicable.
|
||||
*/
|
||||
public modifyPointForGrid(p: go.Point): go.Point {
|
||||
const pregrid = p.copy();
|
||||
const grid = this.diagram.grid;
|
||||
if (grid !== null && grid.visible && this.isGridSnapEnabled) {
|
||||
const cell = grid.gridCellSize;
|
||||
const 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;
|
||||
const geometry = this.temporaryShape.geometry;
|
||||
if (geometry === null) return p;
|
||||
const fig = geometry.figures.first();
|
||||
if (fig === null) return p;
|
||||
const segments = fig.segments;
|
||||
if (this.isOrthoOnly && segments.count > 0) {
|
||||
let lastPt = new go.Point(fig.startX, fig.startY); // assuming segments.count === 1
|
||||
if (segments.count > 1) {
|
||||
// the last segment is the current temporary segment, which we might be altering. We want the segment before
|
||||
const 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* This internal method adds a segment to the geometry of the {@link #temporaryShape}.
|
||||
*/
|
||||
public addPoint(p: go.Point): void {
|
||||
const diagram = this.diagram;
|
||||
const shape = this.temporaryShape;
|
||||
if (shape === null) return;
|
||||
|
||||
// for the temporary Shape, normalize the geometry to be in the viewport
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
const q = this.modifyPointForGrid(new go.Point(p.x - viewpt.x, p.y - viewpt.y));
|
||||
|
||||
const part = shape.part;
|
||||
let geo: go.Geometry | null = null;
|
||||
// if it's not in the Diagram, re-initialize the Shape's geometry and add the Part to the Diagram
|
||||
if (part !== null && part.diagram === null) {
|
||||
const fig = new go.PathFigure(q.x, q.y, true); // possibly filled, depending on Shape.fill
|
||||
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);
|
||||
diagram.add(part);
|
||||
} else if (shape.geometry !== null) {
|
||||
// must copy whole Geometry in order to add a PathSegment
|
||||
geo = shape.geometry.copy();
|
||||
const fig = geo.figures.first();
|
||||
if (fig !== null) {
|
||||
if (this.hasArcs) {
|
||||
const 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* This internal method changes the last segment of the geometry of the {@link #temporaryShape} to end at the given point.
|
||||
*/
|
||||
public moveLastPoint(p: go.Point): void {
|
||||
p = this.modifyPointForGrid(p);
|
||||
const diagram = this.diagram;
|
||||
// must copy whole Geometry in order to change a PathSegment
|
||||
const shape = this.temporaryShape;
|
||||
if (shape.geometry === null) return;
|
||||
const geo = shape.geometry.copy();
|
||||
const fig = geo.figures.first();
|
||||
if (fig === null) return;
|
||||
const segs = fig.segments;
|
||||
if (segs.count > 0) {
|
||||
// for the temporary Shape, normalize the geometry to be in the viewport
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
const 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) {
|
||||
let prevx = 0.0;
|
||||
let prevy = 0.0;
|
||||
if (segs.count > 1) {
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* This internal method removes the last segment of the geometry of the {@link #temporaryShape}.
|
||||
*/
|
||||
public removeLastPoint(): void {
|
||||
// must copy whole Geometry in order to remove a PathSegment
|
||||
const shape = this.temporaryShape;
|
||||
if (shape.geometry === null) return;
|
||||
const geo = shape.geometry.copy();
|
||||
const fig = geo.figures.first();
|
||||
if (fig === null) return;
|
||||
const segs = fig.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}.
|
||||
*/
|
||||
public finishShape(): void {
|
||||
const diagram = this.diagram;
|
||||
const shape: go.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();
|
||||
const tempgeo = shape.geometry;
|
||||
// require 3 points (2 segments) if polygon; 2 points (1 segment) if polyline
|
||||
if (tempgeo !== null) {
|
||||
const tempfig = tempgeo.figures.first();
|
||||
if (tempfig !== null && tempfig.segments.count >= (this.isPolygon ? 2 : 1)) {
|
||||
// normalize geometry and node position
|
||||
const viewpt = diagram.viewportBounds.position;
|
||||
const copygeo = tempgeo.copy();
|
||||
const copyfig = copygeo.figures.first();
|
||||
if (this.isPolygon && copyfig !== null) {
|
||||
// if polygon, close the last segment
|
||||
const segs = copyfig.segments;
|
||||
const seg = segs.elt(segs.count - 1);
|
||||
seg.isClosed = true;
|
||||
}
|
||||
// create the node data for the model
|
||||
const d = diagram.model.copyNodeData(this.archetypePartData);
|
||||
if (d !== null) {
|
||||
// adding data to model creates the actual Part
|
||||
diagram.model.addNodeData(d);
|
||||
const part = diagram.findPartForData(d);
|
||||
if (part !== null) {
|
||||
// assign the position for the whole Part
|
||||
const pos = copygeo.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
|
||||
const pShape: go.Shape = part.findObject('SHAPE') as go.Shape;
|
||||
if (pShape !== null) pShape.geometry = copygeo;
|
||||
this.transactionResult = this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add another point to the geometry of the {@link #temporaryShape}.
|
||||
*/
|
||||
public doMouseDown(): void {
|
||||
const diagram = this.diagram;
|
||||
if (!this.isActive) {
|
||||
this.doActivate();
|
||||
}
|
||||
// a new temporary end point, the previous one is now "accepted"
|
||||
this.addPoint(diagram.lastInput.documentPoint);
|
||||
if (!diagram.lastInput.left) { // e.g. right mouse down
|
||||
this.finishShape();
|
||||
} else if (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.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
const diagram = this.diagram;
|
||||
if (this.isActive) {
|
||||
this.moveLastPoint(diagram.lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not stop this tool, but continue to accumulate Points via mouse-down events.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
// 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}.
|
||||
*
|
||||
* Typing the "Z" key causes the previous point to be discarded.
|
||||
*
|
||||
* Typing the "ESCAPE" key causes the temporary Shape and its geometry to be discarded and this tool to be stopped.
|
||||
*/
|
||||
public doKeyDown(): void {
|
||||
const diagram = this.diagram;
|
||||
if (!this.isActive) return;
|
||||
const e = diagram.lastInput;
|
||||
if (e.key === '\r') { // accept
|
||||
this.finishShape(); // all done!
|
||||
} else if (e.key === 'Z') { // undo
|
||||
this.undo();
|
||||
} else {
|
||||
super.doKeyDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo: remove the last point and continue the drawing of new points.
|
||||
*/
|
||||
public undo(): void {
|
||||
const diagram = this.diagram;
|
||||
// remove a point, and then treat the last one as a temporary one
|
||||
this.removeLastPoint();
|
||||
const lastInput = diagram.lastInput;
|
||||
if (lastInput.event instanceof MouseEvent) this.moveLastPoint(lastInput.documentPoint);
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Polyline Linking Tool</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: 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="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="sample">
|
||||
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 600px"></div>
|
||||
<div id="buttons">
|
||||
<button id="SaveButton">Save</button>
|
||||
<button id="LoadButton">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.ts">PolylineLinkingTool.ts</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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import { PolylineLinkingTool } from './PolylineLinkingTool.js';
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make;
|
||||
|
||||
const myDiagram =
|
||||
$(go.Diagram, 'myDiagramDiv');
|
||||
|
||||
// install custom linking tool, defined in PolylineLinkingTool.js
|
||||
const 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,
|
||||
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
|
||||
export function save() {
|
||||
const str = myDiagram.model.toJson();
|
||||
document.getElementById('mySavedModel').value = str;
|
||||
}
|
||||
export function load() {
|
||||
const str = document.getElementById('mySavedModel').value;
|
||||
myDiagram.model = go.Model.fromJson(str);
|
||||
myDiagram.model.undoManager.isEnabled = true;
|
||||
}
|
||||
|
||||
document.getElementById("SaveButton").onclick = save;
|
||||
document.getElementById("LoadButton").onclick = load;
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The PolylineLinkingTool class the user to draw a new {@link Link} by clicking where the route should go,
|
||||
* until clicking on a valid target port.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/PolylineLinking.html">Polyline Linking</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class PolylineLinkingTool extends go.LinkingTool {
|
||||
/**
|
||||
* Constructs an PolylineLinkingTool, sets {@link #portGravity} to 0, and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._firstMouseDown = false;
|
||||
this._horizontal = false;
|
||||
this.portGravity = 0; // must click on a target port in order to complete the link
|
||||
this.name = 'PolylineLinking';
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* 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.
|
||||
* @param {Point} p
|
||||
*/
|
||||
addPoint(p) {
|
||||
if (this._firstMouseDown)
|
||||
return;
|
||||
const pts = this.temporaryLink.points.copy();
|
||||
this._horizontal = !this._horizontal;
|
||||
pts.add(p.copy());
|
||||
this.temporaryLink.points = pts;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* 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.
|
||||
* @param {Point} p
|
||||
*/
|
||||
moveLastPoint(p) {
|
||||
if (this._firstMouseDown)
|
||||
return;
|
||||
const pts = this.temporaryLink.points.copy();
|
||||
if (this.temporaryLink.isOrthogonal) {
|
||||
const 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;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* 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.
|
||||
*/
|
||||
removeLastPoint() {
|
||||
if (this._firstMouseDown)
|
||||
return;
|
||||
const pts = this.temporaryLink.points.copy();
|
||||
if (pts.length === 0)
|
||||
return;
|
||||
pts.removeAt(pts.length - 1);
|
||||
this.temporaryLink.points = pts;
|
||||
this._horizontal = !this._horizontal;
|
||||
}
|
||||
/**
|
||||
* Use a "crosshair" cursor.
|
||||
*/
|
||||
doActivate() {
|
||||
super.doActivate();
|
||||
this.diagram.currentCursor = 'crosshair';
|
||||
// until a mouse down occurs, allow the temporary link to be routed to the temporary node/port
|
||||
this._firstMouseDown = true;
|
||||
}
|
||||
/**
|
||||
* Add a point to the route that the temporary Link is accumulating.
|
||||
*/
|
||||
doMouseDown() {
|
||||
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;
|
||||
}
|
||||
const pts = this.temporaryLink.points;
|
||||
const ult = pts.elt(pts.length - 1);
|
||||
const 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.
|
||||
*/
|
||||
doMouseMove() {
|
||||
if (this.isActive) {
|
||||
this.moveLastPoint(this.diagram.lastInput.documentPoint);
|
||||
super.doMouseMove();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* If this event happens elsewhere in the diagram, this tool is not stopped: the drawing of the route continues.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (!this.isActive)
|
||||
return;
|
||||
const target = this.findTargetPort(this.isForwards);
|
||||
if (target !== null) {
|
||||
if (this._firstMouseDown) {
|
||||
super.doMouseUp();
|
||||
}
|
||||
else {
|
||||
let pts;
|
||||
this.removeLastPoint(); // remove temporary point
|
||||
const spot = this.isForwards ? target.toSpot : target.fromSpot;
|
||||
if (spot.equals(go.Spot.None)) {
|
||||
const 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.
|
||||
const natpts = this.temporaryLink.points;
|
||||
const numnatpts = natpts.length;
|
||||
if (numnatpts >= 2) {
|
||||
if (numnatpts >= 3) {
|
||||
const penult = natpts.elt(numnatpts - 2);
|
||||
pts.insertAt(pts.length - 1, penult);
|
||||
if (this.temporaryLink.isOrthogonal) {
|
||||
pts.insertAt(pts.length - 1, penult);
|
||||
}
|
||||
}
|
||||
const 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;
|
||||
super.doMouseUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This method overrides the standard link creation method by additionally
|
||||
* replacing the default link route with the custom one laid out by the user.
|
||||
*/
|
||||
insertLink(fromnode, fromport, tonode, toport) {
|
||||
const link = super.insertLink(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.
|
||||
*/
|
||||
doKeyDown() {
|
||||
if (!this.isActive)
|
||||
return;
|
||||
const 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 {
|
||||
super.doKeyDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The PolylineLinkingTool class the user to draw a new {@link Link} by clicking where the route should go,
|
||||
* until clicking on a valid target port.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/PolylineLinking.html">Polyline Linking</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class PolylineLinkingTool extends go.LinkingTool {
|
||||
private _firstMouseDown: boolean = false;
|
||||
private _horizontal: boolean = false;
|
||||
|
||||
/**
|
||||
* Constructs an PolylineLinkingTool, sets {@link #portGravity} to 0, and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.portGravity = 0; // must click on a target port in order to complete the link
|
||||
this.name = 'PolylineLinking';
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* 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.
|
||||
* @param {Point} p
|
||||
*/
|
||||
private addPoint(p: go.Point): void {
|
||||
if (this._firstMouseDown) return;
|
||||
const pts = this.temporaryLink.points.copy();
|
||||
this._horizontal = !this._horizontal;
|
||||
pts.add(p.copy());
|
||||
this.temporaryLink.points = pts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* 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.
|
||||
* @param {Point} p
|
||||
*/
|
||||
private moveLastPoint(p: go.Point): void {
|
||||
if (this._firstMouseDown) return;
|
||||
const pts = this.temporaryLink.points.copy();
|
||||
if (this.temporaryLink.isOrthogonal) {
|
||||
const 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden @internal
|
||||
* 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.
|
||||
*/
|
||||
private removeLastPoint(): void {
|
||||
if (this._firstMouseDown) return;
|
||||
const pts = this.temporaryLink.points.copy();
|
||||
if (pts.length === 0) return;
|
||||
pts.removeAt(pts.length - 1);
|
||||
this.temporaryLink.points = pts;
|
||||
this._horizontal = !this._horizontal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a "crosshair" cursor.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
super.doActivate();
|
||||
this.diagram.currentCursor = 'crosshair';
|
||||
// until a mouse down occurs, allow the temporary link to be routed to the temporary node/port
|
||||
this._firstMouseDown = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a point to the route that the temporary Link is accumulating.
|
||||
*/
|
||||
public doMouseDown(): void {
|
||||
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;
|
||||
}
|
||||
const pts = this.temporaryLink.points;
|
||||
const ult = pts.elt(pts.length - 1);
|
||||
const 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.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
if (this.isActive) {
|
||||
this.moveLastPoint(this.diagram.lastInput.documentPoint);
|
||||
super.doMouseMove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* If this event happens elsewhere in the diagram, this tool is not stopped: the drawing of the route continues.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (!this.isActive) return;
|
||||
const target = this.findTargetPort(this.isForwards);
|
||||
if (target !== null) {
|
||||
if (this._firstMouseDown) {
|
||||
super.doMouseUp();
|
||||
} else {
|
||||
let pts;
|
||||
this.removeLastPoint(); // remove temporary point
|
||||
const spot = this.isForwards ? target.toSpot : target.fromSpot;
|
||||
if (spot.equals(go.Spot.None)) {
|
||||
const pt = this.temporaryLink.getLinkPointFromPoint(target.part as go.Node, 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 as go.Node, target, this.temporaryToNode, this.temporaryToPort, true);
|
||||
this.temporaryLink.toNode = target.part as go.Node;
|
||||
} else {
|
||||
this.copyPortProperties(target.part as go.Node, target, this.temporaryFromNode, this.temporaryFromPort, false);
|
||||
this.temporaryLink.fromNode = target.part as go.Node;
|
||||
}
|
||||
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.
|
||||
const natpts = this.temporaryLink.points;
|
||||
const numnatpts = natpts.length;
|
||||
if (numnatpts >= 2) {
|
||||
if (numnatpts >= 3) {
|
||||
const penult = natpts.elt(numnatpts - 2);
|
||||
pts.insertAt(pts.length - 1, penult);
|
||||
if (this.temporaryLink.isOrthogonal) {
|
||||
pts.insertAt(pts.length - 1, penult);
|
||||
}
|
||||
}
|
||||
const 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;
|
||||
super.doMouseUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method overrides the standard link creation method by additionally
|
||||
* replacing the default link route with the custom one laid out by the user.
|
||||
*/
|
||||
public insertLink(fromnode: go.Node, fromport: go.GraphObject, tonode: go.Node, toport: go.GraphObject): go.Link | null {
|
||||
const link = super.insertLink(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.
|
||||
*/
|
||||
public doKeyDown(): void {
|
||||
if (!this.isActive) return;
|
||||
const 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 {
|
||||
super.doKeyDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Logic Circuit with shiftable ports</title>
|
||||
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
|
||||
<meta name="description" content="TypeScript: Allow the user to shift ports that are in a node." />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
|
||||
</head>
|
||||
<body>
|
||||
<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.ts">PortShiftingTool.ts</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">Save</button>
|
||||
<button id="loadModel">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>
|
||||
|
||||
<script type="module" id="code">
|
||||
import * as go from "../release/go-module.js";
|
||||
import './Figures.js';
|
||||
import { PortShiftingTool } from './PortShiftingTool.js';
|
||||
|
||||
if (window.goSamples) goSamples(); // init for the samples -- you don't need to call this
|
||||
const $ = go.GraphObject.make; // for conciseness in defining templates
|
||||
|
||||
const red = 'orangered'; // 0 or false
|
||||
const green = 'forestgreen'; // 1 or true
|
||||
|
||||
const myDiagram =
|
||||
$(go.Diagram, 'myDiagramDiv', // create a new Diagram in the HTML DIV element "myDiagramDiv"
|
||||
{
|
||||
'draggingTool.isGridSnapEnabled': true,
|
||||
'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) {
|
||||
const button = document.getElementById('saveModel');
|
||||
if (button)
|
||||
button.disabled = !myDiagram.isModified;
|
||||
const idx = document.title.indexOf('*');
|
||||
if (myDiagram.isModified) {
|
||||
if (idx < 0)
|
||||
document.title += '*';
|
||||
}
|
||||
else {
|
||||
if (idx >= 0)
|
||||
document.title = document.title.substr(0, idx);
|
||||
}
|
||||
});
|
||||
const 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,
|
||||
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
|
||||
const 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
|
||||
const 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) }), {
|
||||
doubleClick: function(e, obj) {
|
||||
if (!(obj instanceof go.Node))
|
||||
return;
|
||||
e.diagram.startTransaction('Toggle Input');
|
||||
const shp = obj.findObject('NODESHAPE');
|
||||
shp.fill = (shp.fill === green) ? red : green;
|
||||
updateStates();
|
||||
e.diagram.commitTransaction('Toggle Input');
|
||||
}
|
||||
});
|
||||
const 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) }));
|
||||
const 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) }));
|
||||
const 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) }));
|
||||
const 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) }));
|
||||
const 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) }));
|
||||
const 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) }));
|
||||
const 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) }));
|
||||
const 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() {
|
||||
const 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) {
|
||||
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) {
|
||||
const color = node.findLinksInto().all(linkIsTrue) ? green : red;
|
||||
setOutputLinks(node, color);
|
||||
}
|
||||
function doNand(node) {
|
||||
const color = !node.findLinksInto().all(linkIsTrue) ? green : red;
|
||||
setOutputLinks(node, color);
|
||||
}
|
||||
function doNot(node) {
|
||||
const color = !node.findLinksInto().all(linkIsTrue) ? green : red;
|
||||
setOutputLinks(node, color);
|
||||
}
|
||||
function doOr(node) {
|
||||
const color = node.findLinksInto().any(linkIsTrue) ? green : red;
|
||||
setOutputLinks(node, color);
|
||||
}
|
||||
function doNor(node) {
|
||||
const color = !node.findLinksInto().any(linkIsTrue) ? green : red;
|
||||
setOutputLinks(node, color);
|
||||
}
|
||||
function doXor(node) {
|
||||
let truecount = 0;
|
||||
node.findLinksInto().each(function(link) {
|
||||
if (linkIsTrue(link))
|
||||
truecount++;
|
||||
});
|
||||
const color = truecount % 2 !== 0 ? green : red;
|
||||
setOutputLinks(node, color);
|
||||
}
|
||||
function doXnor(node) {
|
||||
let truecount = 0;
|
||||
node.findLinksInto().each(function(link) {
|
||||
if (linkIsTrue(link))
|
||||
truecount++;
|
||||
});
|
||||
const 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);
|
||||
}
|
||||
document.getElementById("saveModel").onclick = save;
|
||||
document.getElementById("loadModel").onclick = load;
|
||||
|
||||
window.myDiagram = myDiagram; // Attach to the window for console debugging
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* The PortShiftingTool class lets a user move a port on a {@link Node}.
|
||||
*
|
||||
* 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 {@link GraphObject#alignment} property.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/PortShifting.html">Port Shifting</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class PortShiftingTool extends go.Tool {
|
||||
/**
|
||||
* Constructs a PortShiftingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* The port being shifted.
|
||||
*/
|
||||
this.port = null;
|
||||
this._originalAlignment = go.Spot.Default;
|
||||
this.name = 'PortShifting';
|
||||
}
|
||||
/**
|
||||
* 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 {@link #findPort}.
|
||||
*/
|
||||
canStart() {
|
||||
const diagram = this.diagram;
|
||||
if (!super.canStart())
|
||||
return false;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left || !e.shift)
|
||||
return false;
|
||||
if (!this.isBeyondDragSize())
|
||||
return false;
|
||||
return this.findPort() !== null;
|
||||
}
|
||||
/**
|
||||
* From the {@link GraphObject} at the mouse point, search up the visual tree until we get to
|
||||
* an object that has the {@link GraphObject#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).
|
||||
* @return {GraphObject} This returns null if no such port is at the mouse down point.
|
||||
*/
|
||||
findPort() {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.firstInput;
|
||||
let 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 {@link #findPort} and remember it as the "port" property,
|
||||
* and remember the original value for the port's {@link GraphObject#alignment} property.
|
||||
*/
|
||||
doActivate() {
|
||||
this.startTransaction('Shifted Label');
|
||||
this.port = this.findPort();
|
||||
if (this.port !== null) {
|
||||
this._originalAlignment = this.port.alignment.copy();
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
doDeactivate() {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
/**
|
||||
* Clear any reference to a port element.
|
||||
*/
|
||||
doStop() {
|
||||
this.port = null;
|
||||
super.doStop();
|
||||
}
|
||||
/**
|
||||
* Restore the port's original value for GraphObject.alignment.
|
||||
*/
|
||||
doCancel() {
|
||||
if (this.port !== null) {
|
||||
this.port.alignment = this._originalAlignment;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
/**
|
||||
* During the drag, call {@link #updateAlignment} in order to set the {@link GraphObject#alignment} of the port.
|
||||
*/
|
||||
doMouseMove() {
|
||||
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.
|
||||
*/
|
||||
doMouseUp() {
|
||||
if (!this.isActive)
|
||||
return;
|
||||
this.updateAlignment();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
/**
|
||||
* Save the port's {@link 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 {@link GraphObject#alignmentFocus} property.
|
||||
*/
|
||||
updateAlignment() {
|
||||
if (this.port === null || this.port.panel === null)
|
||||
return;
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
const main = this.port.panel.findMainElement();
|
||||
if (main === null)
|
||||
return;
|
||||
const tl = main.getDocumentPoint(go.Spot.TopLeft);
|
||||
const br = main.getDocumentPoint(go.Spot.BottomRight);
|
||||
const x = Math.max(0, Math.min((last.x - tl.x) / (br.x - tl.x), 1));
|
||||
const y = Math.max(0, Math.min((last.y - tl.y) / (br.y - tl.y), 1));
|
||||
this.port.alignment = new go.Spot(x, y);
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as go from '../release/go-module.js';
|
||||
|
||||
/**
|
||||
* The PortShiftingTool class lets a user move a port on a {@link Node}.
|
||||
*
|
||||
* 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 {@link GraphObject#alignment} property.
|
||||
*
|
||||
* If you want to experiment with this extension, try the <a href="../../extensionsTS/PortShifting.html">Port Shifting</a> sample.
|
||||
* @category Tool Extension
|
||||
*/
|
||||
export class PortShiftingTool extends go.Tool {
|
||||
/**
|
||||
* The port being shifted.
|
||||
*/
|
||||
public port: go.GraphObject | null = null;
|
||||
private _originalAlignment: go.Spot = go.Spot.Default;
|
||||
|
||||
/**
|
||||
* Constructs a PortShiftingTool and sets the name for the tool.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'PortShifting';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {@link #findPort}.
|
||||
*/
|
||||
public canStart(): boolean {
|
||||
const diagram = this.diagram;
|
||||
if (!super.canStart()) return false;
|
||||
// require left button & that it has moved far enough away from the mouse down point, so it isn't a click
|
||||
const e = diagram.lastInput;
|
||||
if (!e.left || !e.shift) return false;
|
||||
if (!this.isBeyondDragSize()) return false;
|
||||
|
||||
return this.findPort() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* From the {@link GraphObject} at the mouse point, search up the visual tree until we get to
|
||||
* an object that has the {@link GraphObject#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).
|
||||
* @return {GraphObject} This returns null if no such port is at the mouse down point.
|
||||
*/
|
||||
public findPort(): go.GraphObject | null {
|
||||
const diagram = this.diagram;
|
||||
const e = diagram.firstInput;
|
||||
let 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 {@link #findPort} and remember it as the "port" property,
|
||||
* and remember the original value for the port's {@link GraphObject#alignment} property.
|
||||
*/
|
||||
public doActivate(): void {
|
||||
this.startTransaction('Shifted Label');
|
||||
this.port = this.findPort();
|
||||
if (this.port !== null) {
|
||||
this._originalAlignment = this.port.alignment.copy();
|
||||
}
|
||||
super.doActivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any ongoing transaction.
|
||||
*/
|
||||
public doDeactivate(): void {
|
||||
super.doDeactivate();
|
||||
this.stopTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any reference to a port element.
|
||||
*/
|
||||
public doStop(): void {
|
||||
this.port = null;
|
||||
super.doStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the port's original value for GraphObject.alignment.
|
||||
*/
|
||||
public doCancel(): void {
|
||||
if (this.port !== null) {
|
||||
this.port.alignment = this._originalAlignment;
|
||||
}
|
||||
super.doCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* During the drag, call {@link #updateAlignment} in order to set the {@link GraphObject#alignment} of the port.
|
||||
*/
|
||||
public doMouseMove(): void {
|
||||
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.
|
||||
*/
|
||||
public doMouseUp(): void {
|
||||
if (!this.isActive) return;
|
||||
this.updateAlignment();
|
||||
this.transactionResult = 'Shifted Label';
|
||||
this.stopTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the port's {@link 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 {@link GraphObject#alignmentFocus} property.
|
||||
*/
|
||||
public updateAlignment(): void {
|
||||
if (this.port === null || this.port.panel === null) return;
|
||||
const last = this.diagram.lastInput.documentPoint;
|
||||
const main = this.port.panel.findMainElement();
|
||||
if (main === null) return;
|
||||
const tl = main.getDocumentPoint(go.Spot.TopLeft);
|
||||
const br = main.getDocumentPoint(go.Spot.BottomRight);
|
||||
const x = Math.max(0, Math.min((last.x - tl.x) / (br.x - tl.x), 1));
|
||||
const y = Math.max(0, Math.min((last.y - tl.y) / (br.y - tl.y), 1));
|
||||
this.port.alignment = new go.Spot(x, y);
|
||||
}
|
||||
}
|
||||
Executable
+954
@@ -0,0 +1,954 @@
|
||||
/*
|
||||
* Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
|
||||
*/
|
||||
import * as go from '../release/go-module.js';
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
class QuadNode {
|
||||
constructor(bounds, parent, level) {
|
||||
this.objects = [];
|
||||
this.treeObjects = [];
|
||||
this.totalObjects = 0; // total in this node + in all children (recursively)
|
||||
this.nodes = [null, null, null, null];
|
||||
this.bounds = bounds;
|
||||
this.parent = parent;
|
||||
this.level = level;
|
||||
}
|
||||
split() {
|
||||
const w2 = this.bounds.width / 2;
|
||||
const h2 = this.bounds.height / 2;
|
||||
const x = this.bounds.x;
|
||||
const y = this.bounds.y;
|
||||
this.nodes[0] = new QuadNode(new go.Rect(x + w2, y, w2, h2), this, this.level + 1);
|
||||
this.nodes[1] = new QuadNode(new go.Rect(x, y, w2, h2), this, this.level + 1);
|
||||
this.nodes[2] = new QuadNode(new go.Rect(x, y + h2, w2, h2), this, this.level + 1);
|
||||
this.nodes[3] = new QuadNode(new go.Rect(x + w2, y + h2, w2, h2), this, this.level + 1);
|
||||
}
|
||||
clear() {
|
||||
this.treeObjects = [];
|
||||
this.objects = [];
|
||||
this.totalObjects = 0;
|
||||
for (let i = 0; i < this.nodes.length; i++) {
|
||||
const n = this.nodes[i];
|
||||
if (n !== null) {
|
||||
n.clear();
|
||||
this.nodes[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Object to be contained by the {@link Quadtree} class. This object needs
|
||||
* to have rectangular bounds (described by an {@link Rect} object), as well
|
||||
* as something (of any type) associated with it.
|
||||
*/
|
||||
class TreeObject {
|
||||
constructor(bounds, obj) {
|
||||
this.bounds = bounds;
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Implementation of the quadtree data structure using the {@link Rect} class.
|
||||
* Each Quadtree has defined bounds found at {@link #bounds}, an array
|
||||
* of member rectangles, and an array of child nodes
|
||||
* (Quadtrees themselves). If the Quadtree has no
|
||||
* children, the nodes array will have four nulls. To construct a Quadtree, you
|
||||
* can call its constructor with no arguments. Then, to insert a rectangle, call
|
||||
* {@link #add}. This tree supports adding points (rectangles with 0
|
||||
* width and height), segments (rectangles with either 0 width or 0 height), and
|
||||
* rectangles with nonzero widths and heights.
|
||||
*
|
||||
* Quadtrees can be used to calculate intersections extremely quickly between a
|
||||
* given rectangle and all of the rectangles in the quadtree. Use of this data
|
||||
* structure prevents having to do precise intersection calculations for every
|
||||
* rectangle in the tree. To calculate all of the rectangular intersections for
|
||||
* a given rectangle, use {@link #intersecting}.
|
||||
*
|
||||
* Other common operations are detailed below.
|
||||
* @category Layout Extension
|
||||
*/
|
||||
export class Quadtree {
|
||||
/**
|
||||
* In most cases, simply calling this constructor with no arguments will produce the desired behaviour.
|
||||
* @constructor
|
||||
* @param {number=} nodeCapacity The node capacity of this quadtree. This is the number of objects a node can contain before it splits. Defaults to 1.
|
||||
* @param {number=} maxLevel The maximum depth the Quadtree will allow before it will no longer split. Defaults to Infinity (no maximum depth).
|
||||
* @param {Rect=} bounds The bounding box surrounding the entire Quadtree. If the bounds are unset or a node is inserted outside of the bounds, the tree will automatically grow.
|
||||
*/
|
||||
constructor(nodeCapacity, maxLevel, bounds) {
|
||||
/** @hidden @internal */ this._nodeCapacity = 1;
|
||||
/** @hidden @internal */ this._maxLevels = Infinity;
|
||||
/** @hidden @internal */ this._treeObjectMap = new go.Map();
|
||||
// we can avoid unnecessary work when adding objects if there are no objects with 0 width or height.
|
||||
// Note that after being set to true, these flags are not ever set again to false, even if all objects
|
||||
// with zero width/height are removed (assumption was made that this should almost never matter)
|
||||
/** @hidden @internal */ this._hasZeroWidthObject = false;
|
||||
/** @hidden @internal */ this._hasZeroHeightObject = false;
|
||||
if (nodeCapacity) {
|
||||
this._nodeCapacity = nodeCapacity;
|
||||
}
|
||||
if (maxLevel) {
|
||||
this._maxLevels = maxLevel;
|
||||
}
|
||||
if (bounds === undefined) {
|
||||
bounds = new go.Rect();
|
||||
}
|
||||
this._root = new QuadNode(bounds, null, 0);
|
||||
}
|
||||
/**
|
||||
* Gets the node capacity of this quadtree. This is the number of objects a node can contain before it splits.
|
||||
*/
|
||||
get nodeCapacity() { return this._nodeCapacity; }
|
||||
/**
|
||||
* Gets the maximum depth the Quadtree will allow before it will no longer split..
|
||||
*/
|
||||
get maxLevels() { return this._maxLevels; }
|
||||
/**
|
||||
* Gets the boundaries of the node. All nodes should be square.
|
||||
*/
|
||||
get bounds() { return this._root.bounds; }
|
||||
/**
|
||||
* Gets the root node of the tree
|
||||
*/
|
||||
get root() { return this._root; }
|
||||
/**
|
||||
* Clears the Quadtree, removing all objects and children nodes. Keeps the current bounds of the root node.
|
||||
* @this {Quadtree}
|
||||
* @return {void}
|
||||
*/
|
||||
clear() {
|
||||
this._root.clear();
|
||||
this._treeObjectMap.clear();
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Returns a list of possible quadrants that the given rect could be in
|
||||
* @this {Quadtree}
|
||||
* @param {Rect} rect the rectangle to test
|
||||
* @return {Array<number>}
|
||||
*/
|
||||
_getQuadrants(rect, node) {
|
||||
const quadrants = [];
|
||||
const horizontalMidpoint = node.bounds.x + (node.bounds.width / 2);
|
||||
const verticalMidpoint = node.bounds.y + (node.bounds.height / 2);
|
||||
const topQuadrant = rect.y <= verticalMidpoint;
|
||||
const bottomQuadrant = rect.y + rect.height >= verticalMidpoint;
|
||||
if (rect.x <= horizontalMidpoint) {
|
||||
if (topQuadrant) {
|
||||
quadrants.push(1);
|
||||
}
|
||||
if (bottomQuadrant) {
|
||||
quadrants.push(2);
|
||||
}
|
||||
}
|
||||
if (rect.x + rect.width >= horizontalMidpoint) {
|
||||
if (topQuadrant) {
|
||||
quadrants.push(0);
|
||||
}
|
||||
if (bottomQuadrant) {
|
||||
quadrants.push(3);
|
||||
}
|
||||
}
|
||||
return quadrants;
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Determine which node the rect belongs to. -1 means rect
|
||||
* cannot completely fit within a child node and is part of
|
||||
* the parent node. This function avoids some additional
|
||||
* calculations by assuming that the rect is contained entirely
|
||||
* within the parent node's bounds.
|
||||
* @this {Quadtree}
|
||||
* @param {Rect} rect the rect to test
|
||||
* @return {number}
|
||||
*/
|
||||
_getIndex(rect, node) {
|
||||
let index = -1;
|
||||
if (node.bounds === undefined) { // the quadtree is empty
|
||||
return index;
|
||||
}
|
||||
const horizontalMidpoint = node.bounds.x + (node.bounds.width / 2);
|
||||
const verticalMidpoint = node.bounds.y + (node.bounds.height / 2);
|
||||
const topQuadrant = rect.y <= verticalMidpoint && rect.y + rect.height <= verticalMidpoint;
|
||||
const bottomQuadrant = rect.y >= verticalMidpoint;
|
||||
if (rect.x + rect.width <= horizontalMidpoint) {
|
||||
if (topQuadrant) {
|
||||
index = 1;
|
||||
}
|
||||
else if (bottomQuadrant) {
|
||||
index = 2;
|
||||
}
|
||||
}
|
||||
else if (rect.x >= horizontalMidpoint) {
|
||||
if (topQuadrant) {
|
||||
index = 0;
|
||||
}
|
||||
else if (bottomQuadrant) {
|
||||
index = 3;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
/**
|
||||
* Insert the object into the quadtree. If the node
|
||||
* exceeds the capacity, it will split and add all
|
||||
* objects to their corresponding nodes. If the object is
|
||||
* outside the bounds of the tree's root node, the tree
|
||||
* will grow to accomodate it. Possibly restructures the
|
||||
* tree if a more efficient configuration can be found with
|
||||
* the new dimensions. Bounds can be given either as a
|
||||
* single {@link Rect} or as any combination of arguments
|
||||
* which is valid for the {@link Rect} constructor.
|
||||
* @this {Quadtree}
|
||||
* @param {T} obj the object to insert
|
||||
* @param {Rect|Point|number} x The Rect bounds of the object, or top-left Point, or x value.
|
||||
* @param {Point|Size|number} y Bottom-right Point or Size or y value.
|
||||
* @param {number} w Width to be used if x,y are specified;
|
||||
* must be non-negative.
|
||||
* @param {number} h Height to be used if x,y are specified;
|
||||
* @return {void}
|
||||
*/
|
||||
add(obj, x, y, w, h) {
|
||||
let bounds;
|
||||
if (!(obj instanceof TreeObject) && (x === undefined || x === null)) {
|
||||
throw new Error('Invalid bounds for added object');
|
||||
}
|
||||
if (x instanceof go.Rect) {
|
||||
bounds = x.copy();
|
||||
}
|
||||
else {
|
||||
bounds = new go.Rect(x, y, w, h);
|
||||
}
|
||||
let treeObj;
|
||||
if (obj instanceof TreeObject) {
|
||||
treeObj = obj;
|
||||
obj = treeObj.obj;
|
||||
bounds = treeObj.bounds;
|
||||
}
|
||||
else {
|
||||
treeObj = new TreeObject(bounds, obj);
|
||||
}
|
||||
if (isNaN(bounds.x) || bounds.x === Infinity ||
|
||||
isNaN(bounds.y) || bounds.y === Infinity ||
|
||||
isNaN(bounds.width) || bounds.width === Infinity ||
|
||||
isNaN(bounds.height) || bounds.height === Infinity) {
|
||||
throw new Error('Invalid rectangle, contains NaN or Infinity properties');
|
||||
}
|
||||
this._hasZeroWidthObject = this._hasZeroWidthObject || bounds.width === 0;
|
||||
this._hasZeroHeightObject = this._hasZeroHeightObject || bounds.height === 0;
|
||||
// initialize bounds of tree as the max width or height of the first object added
|
||||
if (this._root.bounds.width === 0 || this._root.bounds.height === 0) {
|
||||
const len = Math.max(bounds.width, bounds.height);
|
||||
this._root.bounds = new go.Rect(bounds.x, bounds.y, len, len);
|
||||
}
|
||||
// fixes quadtree having a width and height of 0 if the first object added is a point
|
||||
// this will only be called after a second object is added, the new width/height is the maximum distance between them
|
||||
if (this._root.bounds !== undefined && (this._root.bounds.width === 0 || this._root.bounds.height === 0)) {
|
||||
const len = Math.max(Math.abs(bounds.x - this._root.bounds.x), Math.abs(bounds.y - this._root.bounds.y));
|
||||
this._root.bounds = new go.Rect(Math.min(this._root.bounds.x, bounds.x), Math.min(this._root.bounds.y, bounds.y), len, len);
|
||||
}
|
||||
// map the object to its corresponding TreeObject (so that the bounds of this object can be retrieved later)
|
||||
this._treeObjectMap.add(obj, treeObj);
|
||||
// grow as many times as necessary to fit the new object
|
||||
while (!this._root.bounds.containsRect(bounds)) {
|
||||
const old = this._root;
|
||||
this.walk(n => n.level++, old);
|
||||
const intersectsTopBound = bounds.y < this._root.bounds.y;
|
||||
const intersectsBottomBound = bounds.y + bounds.height > this._root.bounds.y + this._root.bounds.height;
|
||||
const intersectsRightBound = bounds.x + bounds.width > this._root.bounds.x + this._root.bounds.width;
|
||||
const intersectsLeftBound = bounds.x < this._root.bounds.x;
|
||||
if ((intersectsTopBound && intersectsRightBound) || (intersectsTopBound && !intersectsLeftBound)) {
|
||||
/* _______
|
||||
* | 1 | 0 |
|
||||
* |___|___|
|
||||
* |old| 3 |
|
||||
* |___|___|
|
||||
*/
|
||||
const newBounds = new go.Rect(this._root.bounds.x, this._root.bounds.y - this._root.bounds.height, this._root.bounds.width * 2, this._root.bounds.height * 2);
|
||||
this._root = new QuadNode(newBounds, null, 0);
|
||||
this._root.split();
|
||||
this._root.nodes[2] = old;
|
||||
this._root.totalObjects = old.totalObjects;
|
||||
old.parent = this._root;
|
||||
this._restructure(old);
|
||||
this._restructureLevels(old);
|
||||
if (this._hasZeroHeightObject) {
|
||||
this._fixTopObjectPlacement(old);
|
||||
}
|
||||
}
|
||||
else if (intersectsTopBound && intersectsLeftBound) {
|
||||
/* _______
|
||||
* | 1 | 0 |
|
||||
* |___|___|
|
||||
* | 2 |old|
|
||||
* |___|___|
|
||||
*/
|
||||
const newBounds = new go.Rect(this._root.bounds.x - this._root.bounds.width, this._root.bounds.y - this._root.bounds.height, this._root.bounds.width * 2, this._root.bounds.height * 2);
|
||||
this._root = new QuadNode(newBounds, null, 0);
|
||||
this._root.split();
|
||||
this._root.nodes[3] = old;
|
||||
this._root.totalObjects = old.totalObjects;
|
||||
old.parent = this._root;
|
||||
this._restructure(old);
|
||||
this._restructureLevels(old);
|
||||
if (this._hasZeroWidthObject) {
|
||||
this._fixLeftObjectPlacement(old);
|
||||
}
|
||||
if (this._hasZeroHeightObject) {
|
||||
this._fixTopObjectPlacement(old);
|
||||
}
|
||||
}
|
||||
else if ((intersectsBottomBound && intersectsRightBound) || ((intersectsRightBound || intersectsBottomBound) && !intersectsLeftBound)) {
|
||||
/* _______
|
||||
* |old| 0 |
|
||||
* |___|___|
|
||||
* | 2 | 3 |
|
||||
* |___|___|
|
||||
*/
|
||||
const newBounds = new go.Rect(this._root.bounds.x, this._root.bounds.y, this._root.bounds.width * 2, this._root.bounds.height * 2);
|
||||
this._root = new QuadNode(newBounds, null, 0);
|
||||
this._root.split();
|
||||
this._root.nodes[1] = old;
|
||||
this._root.totalObjects = old.totalObjects;
|
||||
old.parent = this._root;
|
||||
this._restructure(old);
|
||||
this._restructureLevels(old);
|
||||
}
|
||||
else if ((intersectsBottomBound && intersectsLeftBound) || intersectsLeftBound) {
|
||||
/* _______
|
||||
* | 1 |old|
|
||||
* |___|___|
|
||||
* | 2 | 3 |
|
||||
* |___|___|
|
||||
*/
|
||||
const newBounds = new go.Rect(this._root.bounds.x - this._root.bounds.width, this._root.bounds.y, this._root.bounds.width * 2, this._root.bounds.height * 2);
|
||||
this._root = new QuadNode(newBounds, null, 0);
|
||||
this._root.split();
|
||||
this._root.nodes[0] = old;
|
||||
this._root.totalObjects = old.totalObjects;
|
||||
old.parent = this._root;
|
||||
this._restructure(old);
|
||||
this._restructureLevels(old);
|
||||
if (this._hasZeroWidthObject) {
|
||||
this._fixLeftObjectPlacement(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
// add the object to the tree
|
||||
this._addHelper(this._root, treeObj);
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Helper function to recursively perform the add operation
|
||||
* on the tree.
|
||||
* @this {Quadtree}
|
||||
* @param {QuadNode<T>} root the current node being operated on
|
||||
* @param {TreeObject<T>} treeObj the object being added
|
||||
* @return {void}
|
||||
*/
|
||||
_addHelper(root, treeObj) {
|
||||
root.totalObjects++;
|
||||
if (root.nodes[0]) {
|
||||
const index = this._getIndex(treeObj.bounds, root);
|
||||
if (index !== -1) {
|
||||
const selected = root.nodes[index];
|
||||
if (selected !== null) {
|
||||
this._addHelper(selected, treeObj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
root.treeObjects.push(treeObj);
|
||||
root.objects.push(treeObj.obj);
|
||||
if (root.treeObjects.length > this._nodeCapacity && root.level < this._maxLevels) {
|
||||
if (!root.nodes[0]) {
|
||||
root.split();
|
||||
}
|
||||
let i = 0;
|
||||
while (i < root.treeObjects.length) {
|
||||
const index = this._getIndex(root.treeObjects[i].bounds, root);
|
||||
if (index !== -1 && !(root.treeObjects[i].bounds.width === 0 || root.treeObjects[i].bounds.height === 0)) {
|
||||
root.objects.splice(i, 1);
|
||||
const selected = root.nodes[index];
|
||||
if (selected !== null) {
|
||||
this._addHelper(selected, root.treeObjects.splice(i, 1)[0]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Recursively moves objects placed on the right side of a vertical border
|
||||
* between two nodes to the left side of the vertical border. This allows
|
||||
* them to be located by {@link #_getIndex}. This function is called
|
||||
* after an {@link #add} call grows the Quadtree, but only if there
|
||||
* are 0 width objects in the tree.
|
||||
* @this {Quadtree}
|
||||
* @param {QuadNode<T>} root the current root node being operated on
|
||||
* @return {void}
|
||||
*/
|
||||
_fixLeftObjectPlacement(root) {
|
||||
const nw = root.nodes[1];
|
||||
if (nw !== null) { // if root is split
|
||||
this._fixLeftObjectPlacement(nw); // NW
|
||||
const sw = root.nodes[2];
|
||||
if (sw !== null) {
|
||||
this._fixLeftObjectPlacement(sw); // SW
|
||||
}
|
||||
}
|
||||
const toRemove = [];
|
||||
const toAdd = [];
|
||||
for (let i = 0; i < root.objects.length; i++) {
|
||||
const obj = root.treeObjects[i];
|
||||
if (obj.bounds.width === 0 && obj.bounds.x === root.bounds.x) {
|
||||
toRemove.push(i);
|
||||
toAdd.push(obj);
|
||||
}
|
||||
}
|
||||
this._removeFromOwner(root, toRemove);
|
||||
for (const obj of toAdd) {
|
||||
this.add(obj.obj, obj.bounds);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Recursively moves objects placed on the bottom side of a horizontal border
|
||||
* between two nodes to the top side of the vertical border. This allows
|
||||
* them to be located by {@link #_getIndex}. This function is called
|
||||
* after an {@link #add} call grows the Quadtree, but only if there
|
||||
* are 0 height objects in the tree.
|
||||
* @this {Quadtree}
|
||||
* @param {QuadNode<T>} root the current root node being operated on
|
||||
* @return {void}
|
||||
*/
|
||||
_fixTopObjectPlacement(root) {
|
||||
const ne = root.nodes[0];
|
||||
if (ne !== null) { // if root is split
|
||||
this._fixTopObjectPlacement(ne); // NE
|
||||
const nw = root.nodes[1];
|
||||
if (nw !== null) {
|
||||
this._fixTopObjectPlacement(nw); // NW
|
||||
}
|
||||
}
|
||||
const toRemove = [];
|
||||
const toAdd = [];
|
||||
for (let i = 0; i < root.objects.length; i++) {
|
||||
const obj = root.treeObjects[i];
|
||||
if (obj.bounds.height === 0 && obj.bounds.y === root.bounds.y) {
|
||||
toRemove.push(i);
|
||||
toAdd.push(obj);
|
||||
}
|
||||
}
|
||||
this._removeFromOwner(root, toRemove);
|
||||
for (const obj of toAdd) {
|
||||
this.add(obj);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Moves all objects from a leaf node to its parent and unsplits.
|
||||
* Used after growing the tree when level>max level.
|
||||
* @this {Quadtree}
|
||||
* @param {QuadNode<T>} node the leaf node to restructure
|
||||
* @return {void}
|
||||
*/
|
||||
_restructureLevels(node) {
|
||||
if (node && this._maxLevels < Infinity && node.nodes[0] !== null) {
|
||||
if (node.level >= this._maxLevels) {
|
||||
for (let i = 0; i < node.nodes.length; i++) {
|
||||
const selected = node.nodes[i];
|
||||
if (selected !== null) {
|
||||
node.objects.push.apply(node.objects, selected.objects);
|
||||
node.treeObjects.push.apply(node.treeObjects, selected.treeObjects);
|
||||
selected.clear();
|
||||
node.nodes[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < node.nodes.length; i++) {
|
||||
const selected = node.nodes[i];
|
||||
if (selected !== null) {
|
||||
this._restructureLevels(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return the node that contains the given object.
|
||||
* @this {Quadtree}
|
||||
* @param {T} obj the object to find
|
||||
* @return {QuadNode<T>} the node containing the given object, null if the object is not found
|
||||
*/
|
||||
find(obj) {
|
||||
const treeObj = this._treeObjectMap.get(obj);
|
||||
if (treeObj) {
|
||||
return this._findHelper(this._root, treeObj);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
_findHelper(root, treeObj) {
|
||||
for (const object of root.treeObjects) {
|
||||
if (object === treeObj) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
const index = this._getIndex(treeObj.bounds, root);
|
||||
const selected = index === -1 ? null : root.nodes[index];
|
||||
if (selected !== null) {
|
||||
const result = this._findHelper(selected, treeObj);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Convenience method, calls {@link #find} and returns a boolean
|
||||
* indicating whether or not the tree contains the given object
|
||||
* @this {Quadtree}
|
||||
* @param {T} obj the object to check for
|
||||
* @return {boolean} whether or not the given object is present in the tree
|
||||
*/
|
||||
has(obj) {
|
||||
return !!this.find(obj);
|
||||
}
|
||||
/**
|
||||
* Checks if any of the objects in the tree have the given boundaries
|
||||
* @this {Quadtree}
|
||||
* @param {Rect} bounds the rectangle to check for
|
||||
* @return {Rect} the actual bounds object stored in the tree
|
||||
*/
|
||||
findBounds(bounds) {
|
||||
if (bounds) {
|
||||
return this._findBoundsHelper(this._root, bounds);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
_findBoundsHelper(root, bounds) {
|
||||
for (const object of root.treeObjects) {
|
||||
if (bounds.equalsApprox(object.bounds)) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
const index = this._getIndex(bounds, root);
|
||||
const selected = index === -1 ? null : root.nodes[index];
|
||||
if (selected !== null) {
|
||||
return this._findBoundsHelper(selected, bounds);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Remove the given object from the tree, restructuring to
|
||||
* get rid of empty nodes that are unneeded.
|
||||
* @this {Quadtree}
|
||||
* @param {T} obj the object to remove
|
||||
* @return {boolean} whether or not the deletion was successful. False when the object is not in the tree.
|
||||
*/
|
||||
remove(obj) {
|
||||
const treeObj = this._treeObjectMap.get(obj);
|
||||
if (treeObj) {
|
||||
const owner = this._findHelper(this._root, treeObj);
|
||||
if (owner) {
|
||||
owner.treeObjects.splice(owner.treeObjects.indexOf(treeObj), 1);
|
||||
owner.objects.splice(owner.objects.indexOf(obj), 1);
|
||||
owner.totalObjects--;
|
||||
this._treeObjectMap.remove(obj);
|
||||
let parent = owner.parent;
|
||||
while (parent) {
|
||||
parent.totalObjects--;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (owner.nodes[0] && owner.totalObjects <= this._nodeCapacity) {
|
||||
this._addChildObjectsToNode(owner, owner);
|
||||
for (let i = 0; i < owner.nodes.length; i++) {
|
||||
const selected = owner.nodes[i];
|
||||
if (selected !== null) {
|
||||
selected.clear();
|
||||
}
|
||||
owner.nodes[i] = null;
|
||||
}
|
||||
}
|
||||
this._restructure(owner);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Removes multiple objects at the given indices from the given owner. Similar
|
||||
* to the normal remove function, but much faster when the owner and indices are
|
||||
* already known.
|
||||
* @this {Quadtree}
|
||||
* @param {QuadNode<T>} owner the node to remove objects from
|
||||
* @param {Array<number>} indexes the indices to remove. Should be given in ascending order.
|
||||
*/
|
||||
_removeFromOwner(owner, indexes) {
|
||||
if (indexes.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (let i = indexes.length - 1; i >= 0; i--) {
|
||||
this._treeObjectMap.remove(owner.objects[indexes[i]]);
|
||||
owner.treeObjects.splice(indexes[i], 1);
|
||||
owner.objects.splice(indexes[i], 1);
|
||||
}
|
||||
owner.totalObjects -= indexes.length;
|
||||
let parent = owner.parent;
|
||||
while (parent) {
|
||||
parent.totalObjects -= indexes.length;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (owner.nodes[0] && owner.totalObjects <= this._nodeCapacity) {
|
||||
this._addChildObjectsToNode(owner, owner);
|
||||
for (let i = 0; i < owner.nodes.length; i++) {
|
||||
const selected = owner.nodes[i];
|
||||
if (selected !== null) {
|
||||
selected.clear();
|
||||
}
|
||||
owner.nodes[i] = null;
|
||||
}
|
||||
}
|
||||
this._restructure(owner);
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Recursively adds all objects from children of the given
|
||||
* root tree to the given owner tree
|
||||
* Used internally by {@link #remove}
|
||||
* @this {Quadtree}
|
||||
* @param {Quadtree} owner the tree to add objects to
|
||||
* @return {void}
|
||||
*/
|
||||
_addChildObjectsToNode(owner, root) {
|
||||
for (const node of root.nodes) {
|
||||
if (node) {
|
||||
owner.treeObjects.push.apply(owner.treeObjects, node.treeObjects);
|
||||
owner.objects.push.apply(owner.objects, node.objects);
|
||||
this._addChildObjectsToNode(owner, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Recursively combines parent nodes that should be split, all the way
|
||||
* up the tree. Starts from the given node.
|
||||
* @this {Quadtree}
|
||||
* @return {void}
|
||||
*/
|
||||
_restructure(root) {
|
||||
const parent = root.parent;
|
||||
if (parent) {
|
||||
// if none of the child nodes have any objects, the parent should not be split
|
||||
let childrenHaveNoObjects = true;
|
||||
for (const node of parent.nodes) {
|
||||
if (node !== null && node.totalObjects > 0) {
|
||||
childrenHaveNoObjects = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// unsplit children and move nodes to parent
|
||||
if (parent.totalObjects <= this._nodeCapacity || childrenHaveNoObjects) {
|
||||
for (let i = 0; i < parent.nodes.length; i++) {
|
||||
const selected = parent.nodes[i];
|
||||
if (selected !== null) {
|
||||
parent.objects.push.apply(parent.objects, selected.objects);
|
||||
parent.treeObjects.push.apply(parent.treeObjects, selected.treeObjects);
|
||||
selected.clear();
|
||||
parent.nodes[i] = null;
|
||||
}
|
||||
}
|
||||
this._restructure(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Can be called as either (obj, x, y) or (obj, point). Translate
|
||||
* the given object to given x and y coordinates or to a given {@link Point}.
|
||||
* @this {Quadtree}
|
||||
* @param {T} obj the object to move
|
||||
* @param {number|Point} x the x coordinate or Point to move the object to
|
||||
* @param {number} y the y coordinate to move the object to
|
||||
* @return {boolean} whether or not the move was successful. False if the object was not in the tree.
|
||||
*/
|
||||
move(obj, x, y) {
|
||||
const treeObj = this._treeObjectMap.get(obj);
|
||||
if (treeObj && this.remove(obj)) {
|
||||
if (x instanceof go.Point) {
|
||||
treeObj.bounds.x = x.x;
|
||||
treeObj.bounds.y = x.y;
|
||||
}
|
||||
else if (y !== undefined) {
|
||||
treeObj.bounds.x = x;
|
||||
treeObj.bounds.y = y;
|
||||
}
|
||||
else {
|
||||
throw new Error('Please provide the position as either a Point or two numbers');
|
||||
}
|
||||
this.add(treeObj);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Can be called as either (obj, width, height) or (obj, size). Resize
|
||||
* the given object to given width and height or to a given {@link Size}.
|
||||
* @this {Quadtree}
|
||||
* @param {T} obj the object to resize
|
||||
* @param {number|Size} width the width or Size to resize the object to
|
||||
* @param {number} height the height to resize the object to
|
||||
* @return {boolean} whether or not the resize was successful. False if the object was not in the tree.
|
||||
*/
|
||||
resize(obj, width, height) {
|
||||
const treeObj = this._treeObjectMap.get(obj);
|
||||
if (treeObj && this.remove(obj)) {
|
||||
if (width instanceof go.Size) {
|
||||
treeObj.bounds.width = width.width;
|
||||
treeObj.bounds.height = width.height;
|
||||
}
|
||||
else if (height !== undefined) {
|
||||
treeObj.bounds.width = width;
|
||||
treeObj.bounds.height = height;
|
||||
}
|
||||
else {
|
||||
throw new Error('Please provide the size as either a Size or two numbers');
|
||||
}
|
||||
this.add(treeObj);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Updates the given object to have the bounds given, provided as either a
|
||||
* {@link Rect} or x, y, width, and height.
|
||||
* @this {Quadtree}
|
||||
* @param obj the object to change the bounds of
|
||||
* @param x the x-coordinate or Rect to set the object to
|
||||
* @param y the y-coordinate to set the object to, unnecessary if a Rect was given
|
||||
* @param width the width to set the object to, unnecessary if a Rect was given
|
||||
* @param height the height to set the object to, unnecessary if a Rect was given
|
||||
*/
|
||||
setTo(obj, x, y, width, height) {
|
||||
const treeObj = this._treeObjectMap.get(obj);
|
||||
if (treeObj && this.remove(obj)) {
|
||||
if (x instanceof go.Rect) {
|
||||
treeObj.bounds.set(x);
|
||||
}
|
||||
else if (y !== undefined && width !== undefined && height !== undefined) {
|
||||
treeObj.bounds.setTo(x, y, width, height);
|
||||
}
|
||||
else {
|
||||
throw new Error('Please provide new bounds as either a Rect or combination of four numbers (x, y, width, height)');
|
||||
}
|
||||
this.add(treeObj);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Return all objects that intersect (wholly or partially) with
|
||||
* the given {@link Rect} or {@link Point}. Touching edges and
|
||||
* objects overlapping by 1e-7 or less (to account for floating
|
||||
* point error) are both not considered intersections.
|
||||
* @this {Quadtree}
|
||||
* @param {Rect|Point} rect the Rect or Point to check intersections for. If a point is given, a Rect with size (0, 0) is created for intersection calculations.
|
||||
* @return {Array<T>} array containing all intersecting objects
|
||||
*/
|
||||
intersecting(rect) {
|
||||
if (rect instanceof go.Point) {
|
||||
rect = new go.Rect(rect.x, rect.y, 0, 0);
|
||||
}
|
||||
const returnObjects = [];
|
||||
this._intersectingHelper(rect, this._root, returnObjects);
|
||||
return returnObjects;
|
||||
}
|
||||
_intersectingHelper(rect, root, returnObjects) {
|
||||
const index = this._getIndex(rect, root);
|
||||
const selected = index === -1 ? null : root.nodes[index];
|
||||
if (selected !== null) {
|
||||
this._intersectingHelper(rect, selected, returnObjects);
|
||||
}
|
||||
else if (root.nodes[0] !== null) {
|
||||
const quadrants = this._getQuadrants(rect, root);
|
||||
for (const quadrant of quadrants) {
|
||||
const node = root.nodes[quadrant];
|
||||
if (node !== null) {
|
||||
this._intersectingHelper(rect, node, returnObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const obj of root.treeObjects) {
|
||||
if (Quadtree._rectsIntersect(obj.bounds, rect)) {
|
||||
returnObjects.push(obj.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Similar as {@link Rect.intersectsRect}, but doesn't count edges as intersections.
|
||||
* Also accounts for floating error (by returning false more often) up to an error of 1e-7.
|
||||
* Used by {@link #intersecting}.
|
||||
* @this {Quadtree}
|
||||
* @param {Rect} r1 first rectangle
|
||||
* @param {Rect} r2 second rectangle
|
||||
* @return {boolean} whether or not the two rectangles intersect
|
||||
*/
|
||||
static _rectsIntersect(r1, r2) {
|
||||
return !(r2.left + 1e-7 >= r1.right || r2.right - 1e-7 <= r1.left || r2.top + 1e-7 >= r1.bottom || r2.bottom - 1e-7 <= r1.top);
|
||||
}
|
||||
/**
|
||||
* Return all objects that fully contain the given {@link Rect} or {@link Point}.
|
||||
* @this {Quadtree}
|
||||
* @param {Rect|Point} rect the Rect or Point to check containing for. If a point is given, a Rect with size (0, 0) is created for containment calculations.
|
||||
* @return {Array<T>} array containing all containing objects
|
||||
*/
|
||||
containing(rect) {
|
||||
if (rect instanceof go.Point) {
|
||||
rect = new go.Rect(rect.x, rect.y, 0, 0);
|
||||
}
|
||||
const returnObjects = [];
|
||||
this._containingHelper(rect, this._root, returnObjects);
|
||||
return returnObjects;
|
||||
}
|
||||
_containingHelper(rect, root, returnObjects) {
|
||||
const index = this._getIndex(rect, root);
|
||||
const selected = index === -1 ? null : root.nodes[index];
|
||||
if (selected !== null) {
|
||||
this._containingHelper(rect, selected, returnObjects);
|
||||
}
|
||||
else if (root.nodes[0]) {
|
||||
const quadrants = this._getQuadrants(rect, root);
|
||||
for (const quadrant of quadrants) {
|
||||
const node = root.nodes[quadrant];
|
||||
if (node !== null) {
|
||||
this._containingHelper(rect, node, returnObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const obj of root.treeObjects) {
|
||||
if (obj.bounds.containsRect(rect)) {
|
||||
returnObjects.push(obj.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the square of the distance from the centers of the given objects
|
||||
* @this {Quadtree}
|
||||
* @param {T} obj1
|
||||
* @param {T} obj2
|
||||
* @return {number} square of the distance between the centers of obj1 and obj2
|
||||
*/
|
||||
distanceSquared(obj1, obj2) {
|
||||
const owner1 = this.find(obj1);
|
||||
const owner2 = this.find(obj2);
|
||||
if (owner1 !== null && owner2 !== null) {
|
||||
const treeObj1 = this._treeObjectMap.get(obj1);
|
||||
const treeObj2 = this._treeObjectMap.get(obj2);
|
||||
if (treeObj1 !== null && treeObj2 !== null) {
|
||||
return treeObj1.bounds.center.distanceSquaredPoint(treeObj2.bounds.center);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
/**
|
||||
* Recursively traverses the tree (depth first) and executes the
|
||||
* given callback on each node.
|
||||
* @this {Quadtree}
|
||||
* @param {function} callback the callback to execute on each node. Takes the form of (n: Quadtree) => void
|
||||
* @param {boolean} root whether or not to execute the callback on the root node as well. Defaults to true
|
||||
* @return {void}
|
||||
*/
|
||||
walk(callback, node = this._root, root = true) {
|
||||
if (root) {
|
||||
root = false;
|
||||
callback(node);
|
||||
}
|
||||
for (const n of node.nodes) {
|
||||
if (n) {
|
||||
callback(n);
|
||||
this.walk(callback, n, root);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Visits every object stored in the tree (depth first)
|
||||
* @this {Quadtree}
|
||||
* @param {function} callback the callback to execute on each object.
|
||||
* @return {void}
|
||||
*/
|
||||
forEach(callback) {
|
||||
this.walk((n) => {
|
||||
for (const obj of n.objects) {
|
||||
callback(obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Finds the most furthest object in each direction stored in the tree.
|
||||
* Bounds are tested using the center x and y coordinate.
|
||||
* @this {Quadtree}
|
||||
* @return {Array<T>} maximum and minimum objects in the tree, in the format [min x, max x, min y, max y].
|
||||
*/
|
||||
findExtremeObjects() {
|
||||
const [extremes0, extremes1, extremes2, extremes3] = this._findExtremeObjectsHelper();
|
||||
return [
|
||||
extremes0 !== null ? extremes0.obj : null,
|
||||
extremes1 !== null ? extremes1.obj : null,
|
||||
extremes2 !== null ? extremes2.obj : null,
|
||||
extremes3 !== null ? extremes3.obj : null
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @hidden @internal
|
||||
* Recursive helper function for {@link #findExtremeObjects}
|
||||
* @this {Quadtree}
|
||||
* @param {QuadNode<T>} root the current root node being searched
|
||||
* @return {Array<TreeObject<T>>} maximum and minimum objects in the tree, in the format [min x, max x, min y, max y].
|
||||
*/
|
||||
_findExtremeObjectsHelper(root = this._root) {
|
||||
let minX = null;
|
||||
let maxX = null;
|
||||
let minY = null;
|
||||
let maxY = null;
|
||||
if (root.nodes[0]) { // if root is split
|
||||
for (const node of root.nodes) {
|
||||
if (node !== null) {
|
||||
const [extremes0, extremes1, extremes2, extremes3] = this._findExtremeObjectsHelper(node);
|
||||
if (minX == null || (extremes0 !== null && extremes0.bounds.centerX < minX.bounds.centerX)) {
|
||||
minX = extremes0;
|
||||
}
|
||||
if (maxX === null || (extremes1 !== null && extremes1.bounds.centerX > maxX.bounds.centerX)) {
|
||||
maxX = extremes1;
|
||||
}
|
||||
if (minY === null || (extremes2 !== null && extremes2.bounds.centerY < minY.bounds.centerY)) {
|
||||
minY = extremes2;
|
||||
}
|
||||
if (maxY === null || (extremes3 !== null && extremes3.bounds.centerY > maxY.bounds.centerY)) {
|
||||
maxY = extremes3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const obj of root.treeObjects) {
|
||||
if (!minX || obj.bounds.centerX < minX.bounds.centerX) {
|
||||
minX = obj;
|
||||
}
|
||||
if (!maxX || obj.bounds.centerX > maxX.bounds.centerX) {
|
||||
maxX = obj;
|
||||
}
|
||||
if (!minY || obj.bounds.centerY < minY.bounds.centerY) {
|
||||
minY = obj;
|
||||
}
|
||||
if (!maxY || obj.bounds.centerY > maxY.bounds.centerY) {
|
||||
maxY = obj;
|
||||
}
|
||||
}
|
||||
return [minX, maxX, minY, maxY];
|
||||
}
|
||||
}
|
||||
Executable
+1037
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user