create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
852 lines (789 loc) • 37.2 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<meta name="description" content="Horizontal swim lanes and pools with collapsible lanes and limited dragging." />
<meta itemprop="description" content="Horizontal swim lanes and pools with collapsible lanes and limited dragging." />
<meta property="og:description" content="Horizontal swim lanes and pools with collapsible lanes and limited dragging." />
<meta name="twitter:description" content="Horizontal swim lanes and pools with collapsible lanes and limited dragging." />
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
<meta itemprop="name" content="Collapsible, Resizable, Re-orderable Swim Lanes for Flow Diagram" />
<meta property="og:title" content="Collapsible, Resizable, Re-orderable Swim Lanes for Flow Diagram" />
<meta name="twitter:title" content="Collapsible, Resizable, Re-orderable Swim Lanes for Flow Diagram" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/swimlanes.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/swimlanes.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/swimlanes.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/swimLanes.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/swimLanes.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Collapsible, Resizable, Re-orderable Swim Lanes for Flow Diagram | GoJS Diagramming Library
</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
<div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
<a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
href="../">
GoJS
</a>
<div class="relative">
<button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
<svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="topnavList" class="hidden md:block">
<div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
<a href="../learn/">Learn</a>
<a href="../samples/">Samples</a>
<a href="../intro/">Intro</a>
<a href="../api/">API</a>
<a href="../download.html">Download</a>
<a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
<a id="tc" href="https://nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
<a id="tb" href="https://nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
</div>
</div>
</div>
</div>
</nav>
<script>
window.addEventListener("DOMContentLoaded", function () {
// topnav
var topButton = document.getElementById("topnavButton");
var topnavList = document.getElementById("topnavList");
if (topButton && topnavList) {
topButton.addEventListener("click", function (e) {
topnavList
.classList
.toggle("hidden");
e.stopPropagation();
});
document.addEventListener("click", function (e) {
// if the clicked element isn't the list, close the list
if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
topButton.click();
}
});
// set active <a> element
var url = window
.location
.href
.toLowerCase();
var aTags = topnavList.getElementsByTagName('a');
for (var i = 0; i < aTags.length; i++) {
var lowerhref = aTags[i]
.href
.toLowerCase();
if (lowerhref.endsWith('.html'))
lowerhref = lowerhref.slice(0, -5);
if (url.startsWith(lowerhref)) {
aTags[i]
.classList
.add('active');
break;
}
}
}
});
</script>
<div class="flex flex-col prose">
<div class="w-full max-w-screen-xl mx-auto">
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>
<div id="allSampleContent" class="p-4 w-full">
<script id="code">
// These parameters need to be set before defining the templates.
const MINLENGTH = 200; // this controls the minimum length of any swimlane
const MINBREADTH = 20; // this controls the minimum breadth of any non-collapsed swimlane
// some shared functions
// this may be called to force the lanes to be laid out again
function relayoutLanes() {
myDiagram.nodes.each(lane => {
if (!(lane instanceof go.Group)) return;
if (lane.category === 'Pool') return;
lane.layout.isValidLayout = false; // force it to be invalid
});
myDiagram.layoutDiagram();
}
// this is called after nodes have been moved or lanes resized, to layout all of the Pool Groups again
function relayoutDiagram(diagram) {
diagram.layout.invalidateLayout();
diagram.findTopLevelGroups().each(g => {
if (g.category === 'Pool') g.layout.invalidateLayout();
});
diagram.layoutDiagram();
}
// compute the minimum size of a Pool Group needed to hold all of the Lane Groups
function computeMinPoolSize(pool) {
// assert(pool instanceof go.Group && pool.category === "Pool");
let len = MINLENGTH;
pool.memberParts.each(lane => {
// pools ought to only contain lanes, not plain Nodes
if (!(lane instanceof go.Group)) return;
const holder = lane.placeholder;
if (holder !== null) {
len = Math.max(len, holder.actualBounds.width);
}
});
return new go.Size(len, NaN);
}
// compute the minimum size for a particular Lane Group
function computeLaneSize(lane) {
// assert(lane instanceof go.Group && lane.category !== "Pool");
const sz = computeMinLaneSize(lane);
if (lane.isSubGraphExpanded) {
const holder = lane.placeholder;
if (holder !== null) {
sz.height = Math.ceil(Math.max(sz.height, holder.actualBounds.height));
}
}
// minimum breadth needs to be big enough to hold the header
const hdr = lane.findObject('HEADER');
if (hdr !== null) sz.height = Math.ceil(Math.max(sz.height, hdr.actualBounds.height));
return sz;
}
// determine the minimum size of a Lane Group, even if collapsed
function computeMinLaneSize(lane) {
if (!lane.isSubGraphExpanded) return new go.Size(MINLENGTH, 1);
return new go.Size(MINLENGTH, MINBREADTH);
}
// define a custom ResizingTool to limit how far one can shrink a lane Group
class LaneResizingTool extends go.ResizingTool {
constructor(init) {
super();
if (init) Object.assign(this, init);
}
isLengthening() {
return this.handle.alignment === go.Spot.Right;
}
computeMinSize() {
const lane = this.adornedObject.part;
// assert(lane instanceof go.Group && lane.category !== "Pool");
const msz = computeMinLaneSize(lane); // get the absolute minimum size
if (this.isLengthening()) {
// compute the minimum length of all lanes
const sz = computeMinPoolSize(lane.containingGroup);
msz.width = Math.max(msz.width, sz.width);
} else {
// find the minimum size of this single lane
const sz = computeLaneSize(lane);
msz.width = Math.max(msz.width, sz.width);
msz.height = Math.max(msz.height, sz.height);
}
return msz;
}
resize(newr) {
const lane = this.adornedObject.part;
if (this.isLengthening()) {
// changing the length of all of the lanes
lane.containingGroup.memberParts.each(lane => {
if (!(lane instanceof go.Group)) return;
const shape = lane.resizeObject;
if (shape !== null) {
// set its desiredSize length, but leave each breadth alone
shape.width = newr.width;
}
});
} else {
// changing the breadth of a single lane
super.resize(newr);
}
relayoutDiagram(this.diagram); // now that the lane has changed size, layout the pool again
}
}
// end LaneResizingTool class
// define a custom grid layout that makes sure the length of each lane is the same
// and that each lane is broad enough to hold its subgraph
class PoolLayout extends go.GridLayout {
constructor(init) {
super();
this.cellSize = new go.Size(1, 1);
this.wrappingColumn = 1;
this.wrappingWidth = Infinity;
this.isRealtime = false; // don't continuously layout while dragging
this.alignment = go.GridAlignment.Position;
// This sorts based on the location of each Group.
// This is useful when Groups can be moved up and down in order to change their order.
this.comparer = (a, b) => {
const ay = a.location.y;
const by = b.location.y;
if (isNaN(ay) || isNaN(by)) return 0;
if (ay < by) return -1;
if (ay > by) return 1;
return 0;
};
this.boundsComputation = (part, layout, rect) => {
part.getDocumentBounds(rect);
rect.inflate(-1, -1); // negative strokeWidth of the border Shape
return rect;
};
if (init) Object.assign(this, init);
}
doLayout(coll) {
const diagram = this.diagram;
if (diagram === null) return;
diagram.startTransaction('PoolLayout');
const pool = this.group;
if (pool !== null && pool.category === 'Pool') {
// make sure all of the Group Shapes are big enough
const minsize = computeMinPoolSize(pool);
pool.memberParts.each(lane => {
if (!(lane instanceof go.Group)) return;
if (lane.category !== 'Pool') {
const shape = lane.resizeObject;
if (shape !== null) {
// change the desiredSize to be big enough in both directions
const sz = computeLaneSize(lane);
shape.width = isNaN(shape.width)
? minsize.width
: Math.max(shape.width, minsize.width);
shape.height = !isNaN(shape.height) ? Math.max(shape.height, sz.height) : sz.height;
const cell = lane.resizeCellSize;
if (!isNaN(shape.width) && !isNaN(cell.width) && cell.width > 0) {
shape.width = Math.ceil(shape.width / cell.width) * cell.width;
}
if (!isNaN(shape.height) && !isNaN(cell.height) && cell.height > 0) {
shape.height = Math.ceil(shape.height / cell.height) * cell.height;
}
}
}
});
}
// now do all of the usual stuff, according to whatever properties have been set on this GridLayout
super.doLayout(coll);
diagram.commitTransaction('PoolLayout');
}
}
// end PoolLayout class
function init() {
myDiagram = new go.Diagram('myDiagramDiv', {
// use a custom ResizingTool (along with a custom ResizeAdornment on each Group)
resizingTool: new LaneResizingTool(),
// use a simple layout that ignores links to stack the top-level Pool Groups next to each other
layout: new PoolLayout(),
// don't allow dropping onto the diagram's background unless they are all Groups (lanes or pools)
mouseDragOver: e => {
if (!e.diagram.selection.all(n => n instanceof go.Group)) {
e.diagram.currentCursor = 'not-allowed';
}
},
mouseDrop: e => {
if (!e.diagram.selection.all(n => n instanceof go.Group)) {
e.diagram.currentTool.doCancel();
}
},
// a clipboard copied node is pasted into the original node's group (i.e. lane).
'commandHandler.copiesGroupKey': true,
// automatically re-layout the swim lanes after dragging the selection
SelectionMoved: e => relayoutDiagram(e.diagram),
SelectionCopied: e => relayoutDiagram(e.diagram),
'animationManager.isEnabled': false,
// enable undo & redo
'undoManager.isEnabled': true
});
// this is a Part.dragComputation function for limiting where a Node may be dragged
// use GRIDPT instead of PT if DraggingTool.isGridSnapEnabled and movement should snap to grid
function stayInGroup(part, pt, gridpt) {
// don't constrain top-level nodes
const grp = part.containingGroup;
if (grp === null) return pt;
// try to stay within the background Shape of the Group
const back = grp.resizeObject;
if (back === null) return pt;
// allow dragging a Node out of a Group if the Shift key is down
if (part.diagram.lastInput.shift) return pt;
const r = back.getDocumentBounds();
const b = part.actualBounds;
const loc = part.location;
// find the padding inside the group's placeholder that is around the member parts
const m = grp.placeholder.padding;
// now limit the location appropriately
const x =
Math.max(r.x + m.left, Math.min(pt.x, r.right - m.right - b.width - 1)) + (loc.x - b.x);
const y =
Math.max(r.y + m.top, Math.min(pt.y, r.bottom - m.bottom - b.height - 1)) + (loc.y - b.y);
return new go.Point(x, y);
}
myDiagram.nodeTemplate =
new go.Node('Auto', {
// limit dragging of Nodes to stay within the containing Group, defined above
dragComputation: stayInGroup
})
.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
.add(
new go.Shape('Rectangle', {
fill: 'white',
portId: '',
cursor: 'pointer',
fromLinkable: true,
toLinkable: true
}),
new go.TextBlock({ margin: 5 })
.bind('text', 'key')
);
function groupStyle(grp) {
// common settings for both Lane and Pool Groups
grp.layerName = 'Background'; // all pools and lanes are always behind all nodes and links
grp.background = 'transparent'; // can grab anywhere in bounds
grp.movable = true; // allows users to re-order by dragging
grp.copyable = false; // can't copy lanes or pools
grp.avoidable = false; // don't impede AvoidsNodes routed Links
grp.minLocation = new go.Point(NaN, -Infinity); // only allow horizontal movement
grp.maxLocation = new go.Point(NaN, Infinity);
grp.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify);
}
// hide links between lanes when either lane is collapsed
function updateCrossLaneLinks(group) {
group.findExternalLinksConnected().each(l => {
l.visible = l.fromNode.isVisible() && l.toNode.isVisible();
});
}
// each Group is a "swimlane" with a header on the left and a resizable lane on the right
myDiagram.groupTemplateMap.add('Lane',
new go.Group('Horizontal')
.apply(groupStyle)
.set({
selectionObjectName: 'SHAPE', // selecting a lane causes the body of the lane to be highlit, not the label
resizable: true,
resizeObjectName: 'SHAPE', // the custom resizeAdornmentTemplate only permits two kinds of resizing
layout: new go.LayeredDigraphLayout({
// automatically lay out the lane's subgraph
isInitial: false, // don't even do initial layout
isOngoing: false, // don't invalidate layout when nodes or links are added or removed
direction: 0,
columnSpacing: 10,
layeringOption: go.LayeredDigraphLayering.LongestPathSource
}),
computesBoundsAfterDrag: true, // needed to prevent recomputing Group.placeholder bounds too soon
computesBoundsIncludingLinks: false, // to reduce occurrences of links going briefly outside the lane
computesBoundsIncludingLocation: true, // to support empty space at top-left corner of lane
handlesDragDropForMembers: true, // don't need to define handlers on member Nodes and Links
mouseDrop: (e, grp) => {
// dropping a copy of some Nodes and Links onto this Group adds them to this Group
if (!e.shift) return; // cannot change groups with an unmodified drag-and-drop
// don't allow drag-and-dropping a mix of regular Nodes and Groups
if (!e.diagram.selection.any(n => n instanceof go.Group)) {
const ok = grp.addMembers(grp.diagram.selection, true);
if (ok) {
updateCrossLaneLinks(grp);
} else {
grp.diagram.currentTool.doCancel();
}
} else {
e.diagram.currentTool.doCancel();
}
},
subGraphExpandedChanged: grp => {
const shp = grp.resizeObject;
if (grp.diagram.undoManager.isUndoingRedoing) return;
if (grp.isSubGraphExpanded) {
shp.height = grp.data.savedBreadth;
} else {
if (!isNaN(shp.height)) grp.diagram.model.set(grp.data, 'savedBreadth', shp.height);
shp.height = NaN;
}
updateCrossLaneLinks(grp);
}
})
.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
.bindTwoWay('isSubGraphExpanded', 'expanded')
.add(
// the lane header consisting of a Shape and a TextBlock
new go.Panel('Horizontal', {
name: 'HEADER',
angle: 270, // maybe rotate the header to read sideways going up
alignment: go.Spot.Center
})
.add(
new go.Panel('Horizontal') // this is hidden when the swimlane is collapsed
.bindObject('visible', 'isSubGraphExpanded')
.add(
new go.Shape('Diamond', { width: 8, height: 8, fill: 'white' })
.bind('fill', 'color'),
new go.TextBlock({
font: 'bold 13pt sans-serif',
editable: true,
margin: new go.Margin(2, 0, 0, 0)
})
.bindTwoWay('text')
),
go.GraphObject.build('SubGraphExpanderButton', { margin: 5 }) // but this remains always visible!
), // end Horizontal Panel
new go.Panel('Auto') // the lane consisting of a background Shape and a Placeholder representing the subgraph
.add(
new go.Shape('Rectangle', { // this is the resized object
name: 'SHAPE',
fill: 'white'
})
.bind('fill', 'color')
.bindTwoWay('desiredSize', 'size', go.Size.parse, go.Size.stringify),
new go.Placeholder({ padding: 12, alignment: go.Spot.TopLeft }),
new go.TextBlock({
// this TextBlock is only seen when the swimlane is collapsed
name: 'LABEL',
font: 'bold 13pt sans-serif',
editable: true,
angle: 0,
alignment: go.Spot.TopLeft,
margin: new go.Margin(2, 0, 0, 4)
})
.bindObject('visible', 'isSubGraphExpanded', e => !e)
.bindTwoWay('text')
) // end Auto Panel
)
); // end Group
// define a custom resize adornment that has two resize handles if the group is expanded
myDiagram.groupTemplateMap.get('Lane').resizeAdornmentTemplate =
new go.Adornment('Spot')
.add(
new go.Placeholder(),
new go.Shape({
// for changing the length of a lane
alignment: go.Spot.Right,
desiredSize: new go.Size(7, 50),
fill: 'lightblue',
stroke: 'dodgerblue',
cursor: 'col-resize'
})
.bindObject('visible', '', ad => {
if (ad.adornedPart === null) return false;
return ad.adornedPart.isSubGraphExpanded;
}),
new go.Shape({
// for changing the breadth of a lane
alignment: go.Spot.Bottom,
desiredSize: new go.Size(50, 7),
fill: 'lightblue',
stroke: 'dodgerblue',
cursor: 'row-resize'
})
.bindObject('visible', '', ad => {
if (ad.adornedPart === null) return false;
return ad.adornedPart.isSubGraphExpanded;
})
);
myDiagram.groupTemplateMap.add('Pool',
new go.Group('Auto')
.apply(groupStyle)
.set({
// use a simple layout that ignores links to stack the "lane" Groups on top of each other
layout: new PoolLayout({ spacing: new go.Size(0, 0) }) // no space between lanes
})
.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
.add(
new go.Shape({ fill: 'white' })
.bind('fill', 'color'),
new go.Panel('Table', { defaultColumnSeparatorStroke: 'black' })
.add(
new go.Panel('Horizontal', { column: 0, angle: 270 })
.add(
new go.TextBlock({
font: 'bold 16pt sans-serif',
editable: true,
margin: new go.Margin(2, 0, 0, 0)
})
.bindTwoWay('text')
),
new go.Placeholder({ column: 1 })
)
)
);
myDiagram.linkTemplate =
new go.Link({
routing: go.Routing.AvoidsNodes,
corner: 5,
relinkableFrom: true,
relinkableTo: true
})
.add(
new go.Shape(),
new go.Shape({ toArrow: 'Standard' })
);
// define some sample graphs in some of the lanes
myDiagram.model = new go.GraphLinksModel(
[
// node data
{ key: 'Pool1', text: 'Pool', isGroup: true, category: 'Pool' },
{ key: 'Pool2', text: 'Pool2', isGroup: true, category: 'Pool' },
{
key: 'Lane1',
text: 'Lane1',
isGroup: true,
category: 'Lane',
group: 'Pool1',
color: 'lightblue'
},
{
key: 'Lane2',
text: 'Lane2',
isGroup: true,
category: 'Lane',
group: 'Pool1',
color: 'lightgreen'
},
{
key: 'Lane3',
text: 'Lane3',
isGroup: true,
category: 'Lane',
group: 'Pool1',
color: 'lightyellow'
},
{
key: 'Lane4',
text: 'Lane4',
isGroup: true,
category: 'Lane',
group: 'Pool1',
color: 'orange'
},
{ key: 'oneA', group: 'Lane1' },
{ key: 'oneB', group: 'Lane1' },
{ key: 'oneC', group: 'Lane1' },
{ key: 'oneD', group: 'Lane1' },
{ key: 'twoA', group: 'Lane2' },
{ key: 'twoB', group: 'Lane2' },
{ key: 'twoC', group: 'Lane2' },
{ key: 'twoD', group: 'Lane2' },
{ key: 'twoE', group: 'Lane2' },
{ key: 'twoF', group: 'Lane2' },
{ key: 'twoG', group: 'Lane2' },
{ key: 'fourA', group: 'Lane4' },
{ key: 'fourB', group: 'Lane4' },
{ key: 'fourC', group: 'Lane4' },
{ key: 'fourD', group: 'Lane4' },
{
key: 'Lane5',
text: 'Lane5',
isGroup: true,
category: 'Lane',
group: 'Pool2',
color: 'lightyellow'
},
{
key: 'Lane6',
text: 'Lane6',
isGroup: true,
category: 'Lane',
group: 'Pool2',
color: 'lightgreen'
},
{ key: 'fiveA', group: 'Lane5' },
{ key: 'sixA', group: 'Lane6' }
],
[
// link data
{ from: 'oneA', to: 'oneB' },
{ from: 'oneA', to: 'oneC' },
{ from: 'oneB', to: 'oneD' },
{ from: 'oneC', to: 'oneD' },
{ from: 'twoA', to: 'twoB' },
{ from: 'twoA', to: 'twoC' },
{ from: 'twoA', to: 'twoF' },
{ from: 'twoB', to: 'twoD' },
{ from: 'twoC', to: 'twoD' },
{ from: 'twoD', to: 'twoG' },
{ from: 'twoE', to: 'twoG' },
{ from: 'twoF', to: 'twoG' },
{ from: 'fourA', to: 'fourB' },
{ from: 'fourB', to: 'fourC' },
{ from: 'fourC', to: 'fourD' }
]
);
// force all lanes' layouts to be performed
relayoutLanes();
save();
} // end init
// 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);
myDiagram.delayInitialization(relayoutDiagram);
}
window.addEventListener('DOMContentLoaded', init);
</script>
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 700px"></div>
<button onclick="relayoutLanes()">Diagram Layout</button>
<p>
In this design each swimlane is implemented by a <a>Group</a>, and all lanes are inside a "Pool"
Group. Each lane Group has its own <a>Group.layout</a>, which in this case is a
<a>LayeredDigraphLayout</a>. Each pool Group has its own custom <a>GridLayout</a> that arranges
all of its lanes in a vertical stack. That custom layout makes sure all of the pool's lanes have
the same length. If you don't want each lane/group to have its own layout, you could use set the
lane group's <a>Group.layout</a> to null and set the pool group's <a>Group.layout</a> to an
instance of <a>SwimLaneLayout</a>, shown at
<a href="../samples/SwimLaneLayout.html">Swim Lane Layout</a>.
</p>
<p>
When dragging nodes note that the nodes are limited to stay within the lanes. This is
implemented by a custom <a>Part.dragComputation</a> function, here named <b>stayInGroup</b>.
Hold down the Shift key while dragging simple nodes to move the selection to another lane. Lane
groups cannot be moved between pool groups.
</p>
<p>
A Group (i.e. swimlane) is movable but not copyable. When the user moves a lane up or down the
lanes automatically re-order. You can prevent lanes from being moved and thus re-ordered by
setting Group.movable to false.
</p>
<p>
Each Group is collapsible. The previous breadth of that lane is saved in the savedBreadth
property, to be restored when expanded.
</p>
<p>
When a Group/lane is selected, its custom <a>Part.resizeAdornmentTemplate</a> gives it a broad
resize handle at the bottom of the Group and a broad resize handle at the right side of the
Group. This allows the user to resize the "breadth" of the selected lane as well as the "length"
of all of the lanes. However, the custom <a>ResizingTool</a> prevents the lane from being too
narrow to hold the <a>Group.placeholder</a> that represents the subgraph, and it prevents the
lane from being too short to hold any of the contents of the lanes. Each Group/lane is also has
a <a>GraphObject.minSize</a> to keep it from being too narrow even if there are no member
<a>Part</a>s at all.
</p>
<p>
A different sample has its swim lanes vertically oriented:
<a href="swimLanesVertical.html">Swim Lanes (vertical)</a>.
</p>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
Diagram Model saved in JSON format:
<br />
<textarea id="mySavedModel" style="width: 100%; height: 300px"></textarea>
</div>
</div>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
<div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto">
<hr/>
<h3 class="text-xl">GoJS Features in this sample</h3>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Table Panels</h4>
<p>
The "Table" Panel, <a href="../api/symbols/Panel.html#static-Table" target="api">Panel.Table</a>, arranges objects in rows and columns.
Each object in a Table Panel is put into the cell indexed by the value of <a href="../api/symbols/GraphObject.html#row" target="api">GraphObject.row</a> and <a href="../api/symbols/GraphObject.html#column" target="api">GraphObject.column</a>.
The panel will look at the rows and columns for all of the objects in the panel to determine how many rows and columns the table should have.
More information can be found in the <a href="../intro/tablePanels.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#tables">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Grid Layouts</h4>
<p>
This predefined layout is used for placing Nodes in a grid-like arrangement.
Nodes can be ordered, spaced apart, and wrapped as needed. This Layout ignores any Links connecting the nodes being laid out.
More information can be found in the <a href="../intro/layouts.html#GridLayout">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#gridlayout">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Layered Digraph Layout</h4>
<p>
This predefined layout is used for placing Nodes of a general directed graph in layers (rows or columns). This is more general than <a href="../api/symbols/TreeLayout.html">TreeLayout</a>,
as it does not require that the graph be tree-structured.
More information can be found in the <a href="../intro/layouts.html#LayeredDigraphLayout">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#layered-digraph">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Custom Layouts</h4>
<p>
GoJS allows for the creation of custom layouts to meet specific needs.
</p>
<p>
There are also many layouts that are extensions -- not predefined in the <code>go.js</code> or <code>go-debug.js</code> library,
but available as source code in one of the three extension directories, with some documentation and corresponding samples.
More information can be found in the <a href="../intro/layouts.html#CustomLayouts">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#customlayout">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Groups</h4>
<p>
The <a href="../api/symbols/Group.html" target="api">Group</a> class is used to treat a collection of <a href="../api/symbols/Node.html" target="api">Node</a>s and <a href="../api/symbols/Link.html" target="api">Link</a>s as if they were a single <a href="../api/symbols/Node.html" target="api">Node</a>.
Those nodes and links are members of the group; together they constitute a subgraph.
</p>
<p>
A subgraph is <em>not</em> another <a href="../api/symbols/Diagram.html" target="api">Diagram</a>, so there is no separate HTML Div element for the subgraph of a group.
All of the <a href="../api/symbols/Part.html" target="api">Part</a>s that are members of a <a href="../api/symbols/Group.html" target="api">Group</a> belong to the same Diagram as the Group.
There can be links between member nodes and nodes outside of the group as well as links between the group itself and other nodes.
There can even be links between member nodes and the containing group itself.
</p>
<p>
More information can be found in the <a href="../intro/groups.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#groups">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Tools</h4>
<p>
<a href="../api/symbols/Tool.html" target="api">Tool</a>s handle all input events, such as mouse and keyboard interactions, in a Diagram.
There are many kinds of predefined Tool classes that implement all of the common operations that users do.
</p>
<p>
For flexibility and simplicity, all input events are canonicalized as <a href="../api/symbols/InputEvent.html" target="api">InputEvent</a>s and
redirected by the diagram to go to the <a href="../api/symbols/Diagram.html#currentTool" target="api">Diagram.currentTool</a>.
By default the Diagram.currentTool is an instance of <a href="../api/symbols/ToolManager.html" target="api">ToolManager</a> held as the <a href="../api/symbols/Diagram.html#toolManager" target="api">Diagram.toolManager</a>.
The ToolManager implements support for all mode-less tools.
The ToolManager is responsible for finding another tool that is ready to run and then making it the new current tool.
This causes the new tool to process all of the input events (mouse, keyboard, and touch) until the tool decides that it is finished,
at which time the diagram's current tool reverts back to the <a href="../api/symbols/Diagram.html#defaultTool" target="api">Diagram.defaultTool</a>, which is normally the ToolManager, again.
</p>
<p>
More information can be found in the <a href="../intro/tools.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#tools">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Buttons</h4>
<p>
GoJS defines several <a href="../api/symbols/Panel.html" target="api">Panel</a>s for common uses.
These include "Button", "TreeExpanderButton", "SubGraphExpanderButton", "PanelExpanderButton", "ContextMenuButton", and "CheckBoxButton".
"ContextMenuButton"s are typically used inside of "ContextMenu" Panels;
"CheckBoxButton"s are used in the implementation of "CheckBox" Panels.
</p>
<p>
These predefined panels can be used as if they were <a href="../api/symbols/Panel.html" target="api">Panel</a>-derived classes in calls to <a href="../api/symbols/GraphObject.html#build" target="api">GraphObject.build</a>.
They are implemented as simple visual trees of <a href="../api/symbols/GraphObject.html" target="api">GraphObject</a>s in <a href="../api/symbols/Panel.html" target="api">Panel</a>s,
with pre-set properties and event handlers.
</p>
<p>
More information can be found in the <a href="../intro/buttons.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#buttons">Related samples</a>
</p>
<hr>
</div>
</div>
</body>
<!-- This script is part of the gojs.net website, and is not needed to run the sample -->
<script src="../assets/js/goSamples.js"></script>
</html>