UNPKG

gojs

Version:

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

1,071 lines (983 loc) 87.2 kB
/* * Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved. */ import { DrawCommandHandler } from '../../extensionsTS/DrawCommandHandler.js'; import '../../extensionsTS/Figures.js'; import * as go from '../../release/go.js'; import { BPMNLinkingTool, BPMNRelinkingTool, PoolLink } from './BPMNClasses.js'; declare var jQuery: any; // This file holds all of the JavaScript code specific to the BPMN.html page. let myDiagram: go.Diagram; // Setup all of the Diagrams and what they need. // This is called after the page is loaded. export function init() { const $ = go.GraphObject.make; // for more concise visual tree definitions // function checkLocalStorage() { // try { // window.localStorage.setItem('item', 'item'); // window.localStorage.removeItem('item'); // return true; // } catch (e) { // return false; // } // } if (!checkLocalStorage()) { const currentFile = document.getElementById('currentFile') as HTMLDivElement; currentFile.textContent = "Sorry! No web storage support. If you're using Internet Explorer / Microsoft Edge, you must load the page from a server for local storage to work."; } // setup the menubar (jQuery('#menuui') as any).menu(); jQuery(function () { (jQuery('#menuui') as any).menu({ position: { my: 'left top', at: 'left top+30' } }); }); (jQuery('#menuui') as any).menu({ icons: { submenu: 'ui-icon-triangle-1-s' } }); // hides open HTML Element const openDocumentDiv = document.getElementById('openDocument') as HTMLDivElement; openDocumentDiv.style.visibility = 'hidden'; // hides remove HTML Element const removeDocumentDiv = document.getElementById('removeDocument') as HTMLDivElement; removeDocumentDiv.style.visibility = 'hidden'; // constants for design choices const GradientYellow = $(go.Brush, 'Linear', { 0: 'LightGoldenRodYellow', 1: '#FFFF66' }); const GradientLightGreen = $(go.Brush, 'Linear', { 0: '#E0FEE0', 1: 'PaleGreen' }); const GradientLightGray = $(go.Brush, 'Linear', { 0: 'White', 1: '#DADADA' }); const ActivityNodeFill = $(go.Brush, 'Linear', { 0: 'OldLace', 1: 'PapayaWhip' }); const ActivityNodeStroke = '#CDAA7D'; const ActivityMarkerStrokeWidth = 1.5; const ActivityNodeWidth = 120; const ActivityNodeHeight = 80; const ActivityNodeStrokeWidth = 1; const ActivityNodeStrokeWidthIsCall = 4; const SubprocessNodeFill = ActivityNodeFill; const SubprocessNodeStroke = ActivityNodeStroke; const EventNodeSize = 42; const EventNodeInnerSize = EventNodeSize - 6; const EventNodeSymbolSize = EventNodeInnerSize - 14; const EventEndOuterFillColor = 'pink'; const EventBackgroundColor = GradientLightGreen; const EventSymbolLightFill = 'white'; const EventSymbolDarkFill = 'dimgray'; const EventDimensionStrokeColor = 'green'; const EventDimensionStrokeEndColor = 'red'; const EventNodeStrokeWidthIsEnd = 4; const GatewayNodeSize = 80; const GatewayNodeSymbolSize = 45; const GatewayNodeFill = GradientYellow; const GatewayNodeStroke = 'darkgoldenrod'; const GatewayNodeSymbolStroke = 'darkgoldenrod'; const GatewayNodeSymbolFill = GradientYellow; const GatewayNodeSymbolStrokeWidth = 3; const DataFill = GradientLightGray; // custom figures for Shapes go.Shape.defineFigureGenerator('Empty', function (shape, w, h) { return new go.Geometry(); }); const annotationStr = 'M 150,0L 0,0L 0,600L 150,600 M 800,0'; const annotationGeo = go.Geometry.parse(annotationStr); annotationGeo.normalize(); go.Shape.defineFigureGenerator('Annotation', function (shape, w, h) { const geo = annotationGeo.copy(); // calculate how much to scale the Geometry so that it fits in w x h const bounds = geo.bounds; const scale = Math.min(w / bounds.width, h / bounds.height); geo.scale(scale, scale); return geo; }); const gearStr = 'F M 391,5L 419,14L 444.5,30.5L 451,120.5L 485.5,126L 522,141L 595,83L 618.5,92L 644,106.5' + 'L 660.5,132L 670,158L 616,220L 640.5,265.5L 658.122,317.809L 753.122,322.809L 770.122,348.309L 774.622,374.309' + 'L 769.5,402L 756.622,420.309L 659.122,428.809L 640.5,475L 616.5,519.5L 670,573.5L 663,600L 646,626.5' + 'L 622,639L 595,645.5L 531.5,597.5L 493.192,613.462L 450,627.5L 444.5,718.5L 421.5,733L 393,740.5L 361.5,733.5' + 'L 336.5,719L 330,627.5L 277.5,611.5L 227.5,584.167L 156.5,646L 124.5,641L 102,626.5L 82,602.5L 78.5,572.5' + 'L 148.167,500.833L 133.5,466.833L 122,432.5L 26.5,421L 11,400.5L 5,373.5L 12,347.5L 26.5,324L 123.5,317.5' + 'L 136.833,274.167L 154,241L 75.5,152.5L 85.5,128.5L 103,105.5L 128.5,88.5001L 154.872,82.4758L 237,155' + 'L 280.5,132L 330,121L 336,30L 361,15L 391,5 Z M 398.201,232L 510.201,275L 556.201,385L 505.201,491L 399.201,537' + 'L 284.201,489L 242.201,385L 282.201,273L 398.201,232 Z'; const gearGeo = go.Geometry.parse(gearStr); gearGeo.normalize(); go.Shape.defineFigureGenerator('BpmnTaskService', function (shape, w, h) { const geo = gearGeo.copy(); // calculate how much to scale the Geometry so that it fits in w x h const bounds = geo.bounds; const scale = Math.min(w / bounds.width, h / bounds.height); geo.scale(scale, scale); // text should go in the hand geo.spot1 = new go.Spot(0, 0.6, 10, 0); geo.spot2 = new go.Spot(1, 1); return geo; }); const handGeo = go.Geometry.parse('F1M18.13,10.06 C18.18,10.07 18.22,10.07 18.26,10.08 18.91,' + '10.20 21.20,10.12 21.28,12.93 21.36,15.75 21.42,32.40 21.42,32.40 21.42,' + '32.40 21.12,34.10 23.08,33.06 23.08,33.06 22.89,24.76 23.80,24.17 24.72,' + '23.59 26.69,23.81 27.19,24.40 27.69,24.98 28.03,24.97 28.03,33.34 28.03,' + '33.34 29.32,34.54 29.93,33.12 30.47,31.84 29.71,27.11 30.86,26.56 31.80,' + '26.12 34.53,26.12 34.72,28.29 34.94,30.82 34.22,36.12 35.64,35.79 35.64,' + '35.79 36.64,36.08 36.72,34.54 36.80,33.00 37.17,30.15 38.42,29.90 39.67,' + '29.65 41.22,30.20 41.30,32.29 41.39,34.37 42.30,46.69 38.86,55.40 35.75,' + '63.29 36.42,62.62 33.47,63.12 30.76,63.58 26.69,63.12 26.69,63.12 26.69,' + '63.12 17.72,64.45 15.64,57.62 13.55,50.79 10.80,40.95 7.30,38.95 3.80,' + '36.95 4.24,36.37 4.28,35.35 4.32,34.33 7.60,31.25 12.97,35.75 12.97,' + '35.75 16.10,39.79 16.10,42.00 16.10,42.00 15.69,14.30 15.80,12.79 15.96,' + '10.75 17.42,10.04 18.13,10.06z '); handGeo.rotate(90, 0, 0); handGeo.normalize(); go.Shape.defineFigureGenerator('BpmnTaskManual', function (shape, w, h) { const geo = handGeo.copy(); // calculate how much to scale the Geometry so that it fits in w x h const bounds = geo.bounds; const scale = Math.min(w / bounds.width, h / bounds.height); geo.scale(scale, scale); // guess where text should go (in the hand) geo.spot1 = new go.Spot(0, 0.6, 10, 0); geo.spot2 = new go.Spot(1, 1); return geo; }); // define the appearance of tooltips, shared by various templates const tooltiptemplate = $<go.Adornment>('ToolTip', $(go.TextBlock, { margin: 3, editable: true }, new go.Binding('text', '', function (data) { if (data.item !== undefined) return data.item; return '(unnamed item)'; })) ); // conversion functions used by data Bindings function nodeActivityTaskTypeConverter(s: number) { const tasks = ['Empty', 'BpmnTaskMessage', 'BpmnTaskUser', 'BpmnTaskManual', // Custom hand symbol 'BpmnTaskScript', 'BpmnTaskMessage', // should be black on white 'BpmnTaskService', // Custom gear symbol 'InternalStorage']; if (s < tasks.length) return tasks[s]; return 'NotAllowed'; // error } // location of event on boundary of Activity is based on the index of the event in the boundaryEventArray function nodeActivityBESpotConverter(s: number) { const x = 10 + (EventNodeSize / 2); if (s === 0) return new go.Spot(0, 1, x, 0); // bottom left if (s === 1) return new go.Spot(1, 1, -x, 0); // bottom right if (s === 2) return new go.Spot(1, 0, -x, 0); // top right return new go.Spot(1, 0, -x - (s - 2) * EventNodeSize, 0); // top ... right-to-left-ish spread } function nodeActivityTaskTypeColorConverter(s: number) { return (s === 5) ? 'dimgray' : 'white'; } function nodeEventTypeConverter(s: number) { // order here from BPMN 2.0 poster const tasks = ['NotAllowed', 'Empty', 'BpmnTaskMessage', 'BpmnEventTimer', 'BpmnEventEscalation', 'BpmnEventConditional', 'Arrow', 'BpmnEventError', 'ThinX', 'BpmnActivityCompensation', 'Triangle', 'Pentagon', 'ThickCross', 'Circle']; if (s < tasks.length) return tasks[s]; return 'NotAllowed'; // error } function nodeEventDimensionStrokeColorConverter(s: number) { if (s === 8) return EventDimensionStrokeEndColor; return EventDimensionStrokeColor; } function nodeEventDimensionSymbolFillConverter(s: number) { if (s <= 6) return EventSymbolLightFill; return EventSymbolDarkFill; } // ------------------------------------------ Activity Node Boundary Events ---------------------------------------------- const boundaryEventMenu = // context menu for each boundaryEvent on Activity node $<go.Adornment>('ContextMenu', $('ContextMenuButton', $(go.TextBlock, 'Remove event'), // in the click event handler, the obj.part is the Adornment; its adornedObject is the port { click: function (e: go.InputEvent, obj: go.GraphObject) { removeActivityNodeBoundaryEvent((obj.part as go.Adornment).adornedObject); } }) ); // removing a boundary event doesn't not reposition other BE circles on the node // just reassigning alignmentIndex in remaining BE would do that. function removeActivityNodeBoundaryEvent(obj: go.GraphObject | null) { if (obj === null || obj.panel === null || obj.panel.itemArray === null) return; myDiagram.startTransaction('removeBoundaryEvent'); const pid = obj.portId; const arr = obj.panel.itemArray; for (let i = 0; i < arr.length; i++) { if (arr[i].portId === pid) { myDiagram.model.removeArrayItem(arr, i); break; } } myDiagram.commitTransaction('removeBoundaryEvent'); } const boundaryEventItemTemplate = $(go.Panel, 'Spot', { contextMenu: boundaryEventMenu, alignmentFocus: go.Spot.Center, fromLinkable: true, toLinkable: false, cursor: 'pointer', fromSpot: go.Spot.Bottom, fromMaxLinks: 1, toMaxLinks: 0 }, new go.Binding('portId', 'portId'), new go.Binding('alignment', 'alignmentIndex', nodeActivityBESpotConverter), $(go.Shape, 'Circle', { desiredSize: new go.Size(EventNodeSize, EventNodeSize) }, new go.Binding('strokeDashArray', 'eventDimension', function (s) { return (s === 6) ? [4, 2] : null; }), new go.Binding('fromSpot', 'alignmentIndex', function (s) { // nodeActivityBEFromSpotConverter, 0 & 1 go on bottom, all others on top of activity if (s < 2) return go.Spot.Bottom; return go.Spot.Top; }), new go.Binding('fill', 'color')), $(go.Shape, 'Circle', { alignment: go.Spot.Center, desiredSize: new go.Size(EventNodeInnerSize, EventNodeInnerSize), fill: null }, new go.Binding('strokeDashArray', 'eventDimension', function (s) { return (s === 6) ? [4, 2] : null; }) ), $(go.Shape, 'NotAllowed', { alignment: go.Spot.Center, desiredSize: new go.Size(EventNodeSymbolSize, EventNodeSymbolSize), fill: 'white' }, new go.Binding('figure', 'eventType', nodeEventTypeConverter) ) ); // ------------------------------------------ Activity Node contextMenu ---------------------------------------------- const activityNodeMenu = $<go.Adornment>('ContextMenu', $('ContextMenuButton', $(go.TextBlock, 'Add Email Event', { margin: 3 }), { click: function (e: go.InputEvent, obj: go.GraphObject) { addActivityNodeBoundaryEvent(2, 5); } }), $('ContextMenuButton', $(go.TextBlock, 'Add Timer Event', { margin: 3 }), { click: function (e: go.InputEvent, obj: go.GraphObject) { addActivityNodeBoundaryEvent(3, 5); } }), $('ContextMenuButton', $(go.TextBlock, 'Add Escalation Event', { margin: 3 }), { click: function (e: go.InputEvent, obj: go.GraphObject) { addActivityNodeBoundaryEvent(4, 5); } }), $('ContextMenuButton', $(go.TextBlock, 'Add Error Event', { margin: 3 }), { click: function (e: go.InputEvent, obj: go.GraphObject) { addActivityNodeBoundaryEvent(7, 5); } }), $('ContextMenuButton', $(go.TextBlock, 'Add Signal Event', { margin: 3 }), { click: function (e: go.InputEvent, obj: go.GraphObject) { addActivityNodeBoundaryEvent(10, 5); } }), $('ContextMenuButton', $(go.TextBlock, 'Add N-I Escalation Event', { margin: 3 }), { click: function (e: go.InputEvent, obj: go.GraphObject) { addActivityNodeBoundaryEvent(4, 6); } }), $('ContextMenuButton', $(go.TextBlock, 'Rename', { margin: 3 }), { click: function (e: go.InputEvent, obj: go.GraphObject) { rename(obj); } })); // sub-process, loop, parallel, sequential, ad doc and compensation markers in horizontal array function makeSubButton(sub: boolean) { if (sub) { return [$('SubGraphExpanderButton'), { margin: 2, visible: false }, new go.Binding('visible', 'isSubProcess')]; } return []; } // sub-process, loop, parallel, sequential, ad doc and compensation markers in horizontal array function makeMarkerPanel(sub: boolean, scale: number) { return $(go.Panel, 'Horizontal', { alignment: go.Spot.MiddleBottom, alignmentFocus: go.Spot.MiddleBottom }, $(go.Shape, 'BpmnActivityLoop', { width: 12 / scale, height: 12 / scale, margin: 2, visible: false, strokeWidth: ActivityMarkerStrokeWidth }, new go.Binding('visible', 'isLoop')), $(go.Shape, 'BpmnActivityParallel', { width: 12 / scale, height: 12 / scale, margin: 2, visible: false, strokeWidth: ActivityMarkerStrokeWidth }, new go.Binding('visible', 'isParallel')), $(go.Shape, 'BpmnActivitySequential', { width: 12 / scale, height: 12 / scale, margin: 2, visible: false, strokeWidth: ActivityMarkerStrokeWidth }, new go.Binding('visible', 'isSequential')), $(go.Shape, 'BpmnActivityAdHoc', { width: 12 / scale, height: 12 / scale, margin: 2, visible: false, strokeWidth: ActivityMarkerStrokeWidth }, new go.Binding('visible', 'isAdHoc')), $(go.Shape, 'BpmnActivityCompensation', { width: 12 / scale, height: 12 / scale, margin: 2, visible: false, strokeWidth: ActivityMarkerStrokeWidth, fill: null }, new go.Binding('visible', 'isCompensation')), makeSubButton(sub) ); // end activity markers horizontal panel } const activityNodeTemplate = $(go.Node, 'Spot', { locationObjectName: 'SHAPE', locationSpot: go.Spot.Center, resizable: true, resizeObjectName: 'PANEL', toolTip: tooltiptemplate, selectionAdorned: false, // use a Binding on the Shape.stroke to show selection contextMenu: activityNodeMenu, itemTemplate: boundaryEventItemTemplate }, new go.Binding('itemArray', 'boundaryEventArray'), new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), // move a selected part into the Foreground layer, so it isn"t obscured by any non-selected parts new go.Binding('layerName', 'isSelected', function (s) { return s ? 'Foreground' : ''; }).ofObject(), $(go.Panel, 'Auto', { name: 'PANEL', minSize: new go.Size(ActivityNodeWidth, ActivityNodeHeight), desiredSize: new go.Size(ActivityNodeWidth, ActivityNodeHeight) }, new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(go.Size.stringify), $(go.Panel, 'Spot', $(go.Shape, 'RoundedRectangle', // the outside rounded rectangle { name: 'SHAPE', fill: ActivityNodeFill, stroke: ActivityNodeStroke, parameter1: 10, // corner size portId: '', fromLinkable: true, toLinkable: true, cursor: 'pointer', fromSpot: go.Spot.RightSide, toSpot: go.Spot.LeftSide }, new go.Binding('fill', 'color'), new go.Binding('strokeWidth', 'isCall', function (s) { return s ? ActivityNodeStrokeWidthIsCall : ActivityNodeStrokeWidth; }) ), // $(go.Shape, "RoundedRectangle", // the inner "Transaction" rounded rectangle // { margin: 3, // stretch: go.GraphObject.Fill, // stroke: ActivityNodeStroke, // parameter1: 8, fill: null, visible: false // }, // new go.Binding("visible", "isTransaction") // ), // task icon $(go.Shape, 'BpmnTaskScript', // will be None, Script, Manual, Service, etc via converter { alignment: new go.Spot(0, 0, 5, 5), alignmentFocus: go.Spot.TopLeft, width: 22, height: 22 }, new go.Binding('fill', 'taskType', nodeActivityTaskTypeColorConverter), new go.Binding('figure', 'taskType', nodeActivityTaskTypeConverter) ), // end Task Icon makeMarkerPanel(false, 1) // sub-process, loop, parallel, sequential, ad doc and compensation markers ), // end main body rectangles spot panel $(go.TextBlock, // the center text { alignment: go.Spot.Center, textAlign: 'center', margin: 12, editable: true }, new go.Binding('text').makeTwoWay()) ) // end Auto Panel ); // end go.Node, which is a Spot Panel with bound itemArray // ------------------------------- template for Activity / Task node in Palette ------------------------------- const palscale = 2; const activityNodeTemplateForPalette = $(go.Node, 'Vertical', { locationObjectName: 'SHAPE', locationSpot: go.Spot.Center, selectionAdorned: false }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Panel, 'Spot', { name: 'PANEL', desiredSize: new go.Size(ActivityNodeWidth / palscale, ActivityNodeHeight / palscale) }, $(go.Shape, 'RoundedRectangle', // the outside rounded rectangle { name: 'SHAPE', fill: ActivityNodeFill, stroke: ActivityNodeStroke, parameter1: 10 / palscale // corner size (default 10) }, new go.Binding('strokeWidth', 'isCall', function (s) { return s ? ActivityNodeStrokeWidthIsCall : ActivityNodeStrokeWidth; })), $(go.Shape, 'RoundedRectangle', // the inner "Transaction" rounded rectangle { margin: 3, stretch: go.GraphObject.Fill, stroke: ActivityNodeStroke, parameter1: 8 / palscale, fill: null, visible: false }, new go.Binding('visible', 'isTransaction')), // task icon $(go.Shape, 'BpmnTaskScript', // will be None, Script, Manual, Service, etc via converter { alignment: new go.Spot(0, 0, 5, 5), alignmentFocus: go.Spot.TopLeft, width: 22 / palscale, height: 22 / palscale }, new go.Binding('fill', 'taskType', nodeActivityTaskTypeColorConverter), new go.Binding('figure', 'taskType', nodeActivityTaskTypeConverter)), makeMarkerPanel(false, palscale) // sub-process, loop, parallel, sequential, ad doc and compensation markers ), // End Spot panel $(go.TextBlock, // the center text { alignment: go.Spot.Center, textAlign: 'center', margin: 2 }, new go.Binding('text')) ); // End Node const subProcessGroupTemplateForPalette = $(go.Group, 'Vertical', { locationObjectName: 'SHAPE', locationSpot: go.Spot.Center, isSubGraphExpanded: false, selectionAdorned: false }, $(go.Panel, 'Spot', { name: 'PANEL', desiredSize: new go.Size(ActivityNodeWidth / palscale, ActivityNodeHeight / palscale) }, $(go.Shape, 'RoundedRectangle', // the outside rounded rectangle { name: 'SHAPE', fill: ActivityNodeFill, stroke: ActivityNodeStroke, parameter1: 10 / palscale // corner size (default 10) }, new go.Binding('strokeWidth', 'isCall', function (s) { return s ? ActivityNodeStrokeWidthIsCall : ActivityNodeStrokeWidth; }) ), $(go.Shape, 'RoundedRectangle', // the inner "Transaction" rounded rectangle { margin: 3, stretch: go.GraphObject.Fill, stroke: ActivityNodeStroke, parameter1: 8 / palscale, fill: null, visible: false }, new go.Binding('visible', 'isTransaction')), makeMarkerPanel(true, palscale) // sub-process, loop, parallel, sequential, ad doc and compensation markers ), // end main body rectangles spot panel $(go.TextBlock, // the center text { alignment: go.Spot.Center, textAlign: 'center', margin: 2 }, new go.Binding('text')) ); // end go.Group // ------------------------------------------ Event Node Template ---------------------------------------------- const eventNodeTemplate = $(go.Node, 'Vertical', { locationObjectName: 'SHAPE', locationSpot: go.Spot.Center, toolTip: tooltiptemplate }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), // move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts new go.Binding('layerName', 'isSelected', function (s) { return s ? 'Foreground' : ''; }).ofObject(), // can be resided according to the user's desires { resizable: false, resizeObjectName: 'SHAPE' }, $(go.Panel, 'Spot', $(go.Shape, 'Circle', // Outer circle { strokeWidth: 1, name: 'SHAPE', desiredSize: new go.Size(EventNodeSize, EventNodeSize), portId: '', fromLinkable: true, toLinkable: true, cursor: 'pointer', fromSpot: go.Spot.RightSide, toSpot: go.Spot.LeftSide }, // allows the color to be determined by the node data new go.Binding('fill', 'eventDimension', function (s) { return (s === 8) ? EventEndOuterFillColor : EventBackgroundColor; }), new go.Binding('strokeWidth', 'eventDimension', function (s) { return s === 8 ? EventNodeStrokeWidthIsEnd : 1; }), new go.Binding('stroke', 'eventDimension', nodeEventDimensionStrokeColorConverter), new go.Binding('strokeDashArray', 'eventDimension', function (s) { return (s === 3 || s === 6) ? [4, 2] : null; }), new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(go.Size.stringify) ), // end main shape $(go.Shape, 'Circle', // Inner circle { alignment: go.Spot.Center, desiredSize: new go.Size(EventNodeInnerSize, EventNodeInnerSize), fill: null }, new go.Binding('stroke', 'eventDimension', nodeEventDimensionStrokeColorConverter), new go.Binding('strokeDashArray', 'eventDimension', function (s) { return (s === 3 || s === 6) ? [4, 2] : null; }), // dashes for non-interrupting new go.Binding('visible', 'eventDimension', function (s) { return s > 3 && s <= 7; }) // inner only visible for 4 thru 7 ), $(go.Shape, 'NotAllowed', { alignment: go.Spot.Center, desiredSize: new go.Size(EventNodeSymbolSize, EventNodeSymbolSize), stroke: 'black' }, new go.Binding('figure', 'eventType', nodeEventTypeConverter), new go.Binding('fill', 'eventDimension', nodeEventDimensionSymbolFillConverter) ) ), // end Auto Panel $(go.TextBlock, { alignment: go.Spot.Center, textAlign: 'center', margin: 5, editable: true }, new go.Binding('text').makeTwoWay()) ); // end go.Node Vertical // ------------------------------------------ Gateway Node Template ---------------------------------------------- function nodeGatewaySymbolTypeConverter(s: number) { const tasks = ['NotAllowed', 'ThinCross', // 1 - Parallel 'Circle', // 2 - Inclusive 'AsteriskLine', // 3 - Complex 'ThinX', // 4 - Exclusive (exclusive can also be no symbol, just bind to visible=false for no symbol) 'Pentagon', // 5 - double cicle event based gateway 'Pentagon', // 6 - exclusive event gateway to start a process (single circle) 'ThickCross'] ; // 7 - parallel event gateway to start a process (single circle) if (s < tasks.length) return tasks[s]; return 'NotAllowed'; // error } // tweak the size of some of the gateway icons function nodeGatewaySymbolSizeConverter(s: number) { const size = new go.Size(GatewayNodeSymbolSize, GatewayNodeSymbolSize); if (s === 4) { size.width = size.width / 4 * 3; size.height = size.height / 4 * 3; } else if (s > 4) { size.width = size.width / 1.6; size.height = size.height / 1.6; } return size; } function nodePalGatewaySymbolSizeConverter(s: number) { const size = nodeGatewaySymbolSizeConverter(s); size.width = size.width / 2; size.height = size.height / 2; return size; } const gatewayNodeTemplate = $(go.Node, 'Vertical', { locationObjectName: 'SHAPE', locationSpot: go.Spot.Center, toolTip: tooltiptemplate }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), // move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts new go.Binding('layerName', 'isSelected', function (s) { return s ? 'Foreground' : ''; }).ofObject(), // can be resided according to the user's desires { resizable: false, resizeObjectName: 'SHAPE' }, $(go.Panel, 'Spot', $(go.Shape, 'Diamond', { strokeWidth: 1, fill: GatewayNodeFill, stroke: GatewayNodeStroke, name: 'SHAPE', desiredSize: new go.Size(GatewayNodeSize, GatewayNodeSize), portId: '', fromLinkable: true, toLinkable: true, cursor: 'pointer', fromSpot: go.Spot.NotLeftSide, toSpot: go.Spot.NotRightSide }, new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(go.Size.stringify)), // end main shape $(go.Shape, 'NotAllowed', { alignment: go.Spot.Center, stroke: GatewayNodeSymbolStroke, fill: GatewayNodeSymbolFill }, new go.Binding('figure', 'gatewayType', nodeGatewaySymbolTypeConverter), // new go.Binding("visible", "gatewayType", function(s) { return s !== 4; }), // comment out if you want exclusive gateway to be X instead of blank. new go.Binding('strokeWidth', 'gatewayType', function (s) { return (s <= 4) ? GatewayNodeSymbolStrokeWidth : 1; }), new go.Binding('desiredSize', 'gatewayType', nodeGatewaySymbolSizeConverter)), // the next 2 circles only show up for event gateway $(go.Shape, 'Circle', // Outer circle { strokeWidth: 1, stroke: GatewayNodeSymbolStroke, fill: null, desiredSize: new go.Size(EventNodeSize, EventNodeSize) }, new go.Binding('visible', 'gatewayType', function (s) { return s >= 5; }) // only visible for > 5 ), // end main shape $(go.Shape, 'Circle', // Inner circle { alignment: go.Spot.Center, stroke: GatewayNodeSymbolStroke, desiredSize: new go.Size(EventNodeInnerSize, EventNodeInnerSize), fill: null }, new go.Binding('visible', 'gatewayType', function (s) { return s === 5; }) // inner only visible for == 5 ) ), $(go.TextBlock, { alignment: go.Spot.Center, textAlign: 'center', margin: 5, editable: true }, new go.Binding('text').makeTwoWay()) ); // end go.Node Vertical // -------------------------------------------------------------------------------------------------------------- const gatewayNodeTemplateForPalette = $(go.Node, 'Vertical', { toolTip: tooltiptemplate, resizable: false, locationObjectName: 'SHAPE', locationSpot: go.Spot.Center, resizeObjectName: 'SHAPE' }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Panel, 'Spot', $(go.Shape, 'Diamond', { strokeWidth: 1, fill: GatewayNodeFill, stroke: GatewayNodeStroke, name: 'SHAPE', desiredSize: new go.Size(GatewayNodeSize / 2, GatewayNodeSize / 2) }), $(go.Shape, 'NotAllowed', { alignment: go.Spot.Center, stroke: GatewayNodeSymbolStroke, strokeWidth: GatewayNodeSymbolStrokeWidth, fill: GatewayNodeSymbolFill }, new go.Binding('figure', 'gatewayType', nodeGatewaySymbolTypeConverter), // new go.Binding("visible", "gatewayType", function(s) { return s !== 4; }), // comment out if you want exclusive gateway to be X instead of blank. new go.Binding('strokeWidth', 'gatewayType', function (s) { return (s <= 4) ? GatewayNodeSymbolStrokeWidth : 1; }), new go.Binding('desiredSize', 'gatewayType', nodePalGatewaySymbolSizeConverter)), // the next 2 circles only show up for event gateway $(go.Shape, 'Circle', // Outer circle { strokeWidth: 1, stroke: GatewayNodeSymbolStroke, fill: null, desiredSize: new go.Size(EventNodeSize / 2, EventNodeSize / 2) }, // new go.Binding("desiredSize", "gatewayType", new go.Size(EventNodeSize/2, EventNodeSize/2)), new go.Binding('visible', 'gatewayType', function (s) { return s >= 5; }) // only visible for > 5 ), // end main shape $(go.Shape, 'Circle', // Inner circle { alignment: go.Spot.Center, stroke: GatewayNodeSymbolStroke, desiredSize: new go.Size(EventNodeInnerSize / 2, EventNodeInnerSize / 2), fill: null }, new go.Binding('visible', 'gatewayType', function (s) { return s === 5; }) // inner only visible for == 5 )), $(go.TextBlock, { alignment: go.Spot.Center, textAlign: 'center', margin: 5, editable: false }, new go.Binding('text')) ); // -------------------------------------------------------------------------------------------------------------- const annotationNodeTemplate = $(go.Node, 'Auto', { background: GradientLightGray, locationSpot: go.Spot.Center }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Shape, 'Annotation', // A left bracket shape { portId: '', fromLinkable: true, cursor: 'pointer', fromSpot: go.Spot.Left, strokeWidth: 2, stroke: 'gray' }), $(go.TextBlock, { margin: 5, editable: true }, new go.Binding('text').makeTwoWay()) ); const dataObjectNodeTemplate = $(go.Node, 'Vertical', { locationObjectName: 'SHAPE', locationSpot: go.Spot.Center }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Shape, 'File', { name: 'SHAPE', portId: '', fromLinkable: true, toLinkable: true, cursor: 'pointer', fill: DataFill, desiredSize: new go.Size(EventNodeSize * 0.8, EventNodeSize) }), $(go.TextBlock, { margin: 5, editable: true }, new go.Binding('text').makeTwoWay()) ); const dataStoreNodeTemplate = $(go.Node, 'Vertical', { locationObjectName: 'SHAPE', locationSpot: go.Spot.Center }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Shape, 'Database', { name: 'SHAPE', portId: '', fromLinkable: true, toLinkable: true, cursor: 'pointer', fill: DataFill, desiredSize: new go.Size(EventNodeSize, EventNodeSize) }), $(go.TextBlock, { margin: 5, editable: true }, new go.Binding('text').makeTwoWay()) ); // ------------------------------------------ private process Node Template Map ---------------------------------------------- const privateProcessNodeTemplate = $(go.Node, 'Auto', { layerName: 'Background', resizable: true, resizeObjectName: 'LANE' }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Shape, 'Rectangle', { fill: null }), $(go.Panel, 'Table', // table with 2 cells to hold header and lane { desiredSize: new go.Size(ActivityNodeWidth * 6, ActivityNodeHeight), background: DataFill, name: 'LANE', minSize: new go.Size(ActivityNodeWidth, ActivityNodeHeight * 0.667) }, new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(go.Size.stringify), $(go.TextBlock, { row: 0, column: 0, angle: 270, margin: 5, editable: true, textAlign: 'center' }, new go.Binding('text').makeTwoWay()), $(go.RowColumnDefinition, { column: 1, separatorStrokeWidth: 1, separatorStroke: 'black' }), $(go.Shape, 'Rectangle', { row: 0, column: 1, stroke: null, fill: 'transparent', portId: '', fromLinkable: true, toLinkable: true, fromSpot: go.Spot.TopBottomSides, toSpot: go.Spot.TopBottomSides, cursor: 'pointer', stretch: go.GraphObject.Fill }) ) ); const privateProcessNodeTemplateForPalette = $(go.Node, 'Vertical', { locationSpot: go.Spot.Center }, $(go.Shape, 'Process', { fill: DataFill, desiredSize: new go.Size(GatewayNodeSize / 2, GatewayNodeSize / 4) }), $(go.TextBlock, { margin: 5, editable: true }, new go.Binding('text')) ); const poolTemplateForPalette = $(go.Group, 'Vertical', { locationSpot: go.Spot.Center, computesBoundsIncludingLinks: false, isSubGraphExpanded: false }, $(go.Shape, 'Process', { fill: 'white', desiredSize: new go.Size(GatewayNodeSize / 2, GatewayNodeSize / 4) }), $(go.Shape, 'Process', { fill: 'white', desiredSize: new go.Size(GatewayNodeSize / 2, GatewayNodeSize / 4) }), $(go.TextBlock, { margin: 5, editable: true }, new go.Binding('text')) ); const swimLanesGroupTemplateForPalette = $(go.Group, 'Vertical'); // empty in the palette const subProcessGroupTemplate = $(go.Group, 'Spot', { locationSpot: go.Spot.Center, locationObjectName: 'PH', // locationSpot: go.Spot.Center, isSubGraphExpanded: false, memberValidation: function (group: go.Group, part: go.Part) { return !(part instanceof go.Group) || (part.category !== 'Pool' && part.category !== 'Lane'); }, mouseDrop: function (e: go.InputEvent, grp: go.GraphObject) { if (!(grp instanceof go.Group) || grp.diagram === null) return; const ok = grp.addMembers(grp.diagram.selection, true); if (!ok) grp.diagram.currentTool.doCancel(); }, contextMenu: activityNodeMenu, itemTemplate: boundaryEventItemTemplate }, new go.Binding('itemArray', 'boundaryEventArray'), new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), // move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts // new go.Binding("layerName", "isSelected", function (s) { return s ? "Foreground" : ""; }).ofObject(), $(go.Panel, 'Auto', $(go.Shape, 'RoundedRectangle', { name: 'PH', fill: SubprocessNodeFill, stroke: SubprocessNodeStroke, minSize: new go.Size(ActivityNodeWidth, ActivityNodeHeight), portId: '', fromLinkable: true, toLinkable: true, cursor: 'pointer', fromSpot: go.Spot.RightSide, toSpot: go.Spot.LeftSide }, new go.Binding('strokeWidth', 'isCall', function (s) { return s ? ActivityNodeStrokeWidthIsCall : ActivityNodeStrokeWidth; }) ), $(go.Panel, 'Vertical', { defaultAlignment: go.Spot.Left }, $(go.TextBlock, // label { margin: 3, editable: true }, new go.Binding('text', 'text').makeTwoWay(), new go.Binding('alignment', 'isSubGraphExpanded', function (s) { return s ? go.Spot.TopLeft : go.Spot.Center; })), // create a placeholder to represent the area where the contents of the group are $(go.Placeholder, { padding: new go.Margin(5, 5) }), makeMarkerPanel(true, 1) // sub-process, loop, parallel, sequential, ad doc and compensation markers ) // end Vertical Panel ) ); // end Group // ** need this in the subprocess group template above. // $(go.Shape, "RoundedRectangle", // the inner "Transaction" rounded rectangle // { margin: 3, // stretch: go.GraphObject.Fill, // stroke: ActivityNodeStroke, // parameter1: 8, fill: null, visible: false // }, // new go.Binding("visible", "isTransaction") // ), function groupStyle() { // common settings for both Lane and Pool Groups return [ { layerName: 'Background', // all pools and lanes are always behind all nodes and links background: 'transparent', // can grab anywhere in bounds movable: true, // allows users to re-order by dragging copyable: false, // can't copy lanes or pools avoidable: false // don't impede AvoidsNodes routed Links }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify) ]; } // hide links between lanes when either lane is collapsed function updateCrossLaneLinks(group: go.Group) { group.findExternalLinksConnected().each((l) => { l.visible = (l.fromNode !== null && l.fromNode.isVisible() && l.toNode !== null && l.toNode.isVisible()); }); } const laneEventMenu = // context menu for each lane $<go.Adornment>('ContextMenu', $('ContextMenuButton', $(go.TextBlock, 'Add Lane'), // in the click event handler, the obj.part is the Adornment; its adornedObject is the port { click: function (e: go.InputEvent, obj: go.GraphObject) { addLaneEvent((obj.part as go.Adornment).adornedObject as go.Node); } }) ); // Add a lane to pool (lane parameter is lane above new lane) function addLaneEvent(lane: go.Node) { myDiagram.startTransaction('addLane'); if (lane != null && lane.data.category === 'Lane') { // create a new lane data object const shape = lane.findObject('SHAPE'); const size = new go.Size(shape ? shape.width : MINLENGTH, MINBREADTH); const newlanedata = { category: 'Lane', text: 'New Lane', color: 'white', isGroup: true, loc: go.Point.stringify(new go.Point(lane.location.x, lane.location.y + 1)), // place below selection size: go.Size.stringify(size), group: lane.data.group }; // and add it to the model myDiagram.model.addNodeData(newlanedata); } myDiagram.commitTransaction('addLane'); } const swimLanesGroupTemplate = $(go.Group, 'Spot', groupStyle(), { name: 'Lane', contextMenu: laneEventMenu, minLocation: new go.Point(NaN, -Infinity), // only allow vertical movement maxLocation: new go.Point(NaN, Infinity), 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: $(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.LayeredDigraphLayout.LayerLongestPathSource }), 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: function (e: go.InputEvent, grp: go.GraphObject) { // dropping a copy of some Nodes and Links onto this Group adds them to this Group // don't allow drag-and-dropping a mix of regular Nodes and Groups if (!e.diagram.selection.any((n) => (n instanceof go.Group && n.category !== 'subprocess') || n.category === 'privateProcess')) { if (!(grp instanceof go.Group) || grp.diagram === null) return; const ok = grp.addMembers(grp.diagram.selection, true); if (ok) { updateCrossLaneLinks(grp); relayoutDiagram(); } else { grp.diagram.currentTool.doCancel(); } } }, subGraphExpandedChanged: function (grp: go.Group) { if (grp.diagram === null) return; if (grp.diagram.undoManager.isUndoingRedoing) return; const shp = grp.resizeObject; if (grp.isSubGraphExpanded) { shp.height = (grp as any)['_savedBreadth']; } else { (grp as any)['_savedBreadth'] = shp.height; shp.height = NaN; } updateCrossLaneLinks(grp); } }, // new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(), $(go.Shape, 'Rectangle', // this is the resized object { name: 'SHAPE', fill: 'white', stroke: null }, // need stroke null here or you gray out some of pool border. new go.Binding('fill', 'color'), new go.Binding('desiredSize', 'size', go.Size.parse).makeTwoWay(go.Size.stringify)), // the lane header consisting of a Shape and a TextBlock $(go.Panel, 'Horizontal', { name: 'HEADER', angle: 270, // maybe rotate the header to read sideways going up alignment: go.Spot.LeftCenter, alignmentFocus: go.Spot.LeftCenter }, $(go.TextBlock, // the lane label { editable: true, margin: new go.Margin(2, 0, 0, 8) }, new go.Binding('visible', 'isSubGraphExpanded').ofObject(), new go.Binding('text', 'text').makeTwoWay()), $('SubGraphExpanderButton', { margin: 4, angle: -270 }) // but this remains always visible! ), // end Horizontal Panel $(go.Placeholder, { padding: 12, alignment: go.Spot.TopLeft, alignmentFocus: go.Spot.TopLeft }), $(go.Panel, 'Horizontal', { alignment: go.Spot.TopLeft, alignmentFocus: go.Spot.TopLeft }, $(go.TextBlock, // this TextBlock is only seen when the swimlane is collapsed { name: 'LABEL', editable: true, visible: false, angle: 0, margin: new go.Margin(6, 0, 0, 20) }, new go.Binding('visible', 'isSubGraphExpanded', function (e) { return !e; }).ofObject(), new go.Binding('text', 'text').makeTwoWay()) ) ); // end swimLanesGroupTemplate // define a custom resize adornment that has two resize handles if the group is expanded // myDiagram.groupTemplate.resizeAdornmentTemplate = swimLanesGroupTemplate.resizeAdornmentTemplate = $(go.Adornment, 'Spot', $(go.Placeholder), $(go.Shape, // for changing the length of a lane { alignment: go.Spot.Right, desiredSize: new go.Size(7, 50), fill: 'lightblue', stroke: 'dodgerblue', cursor: 'col-resize' }, new go.Binding('visible', '', function (ad) { if (ad.adornedPart === null) return false; return ad.adornedPart.isSubGraphExpanded; }).ofObject()), $(go.Shape, // for changing the breadth of a lane { alignment: go.Spot.Bottom, desiredSize: new go.Size(50, 7), fill: 'lightblue', stroke: 'dodgerblue', cursor: 'row-resize' }, new go.Binding('visible', '', function (ad) { if (ad.adornedPart === null) return false; return ad.adornedPart.isSubGraphExpanded; }).ofObject()) ); const poolGroupTemplate = $(go.Group, 'Auto', groupStyle(), { computesBoundsIncludingLinks: false, // use a simple layout that ignores links to stack the "lane" Groups on top of each other layout: $(PoolLayout, { spacing: new go.Size(0, 0) }) // no space between lanes }, new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Shape, { fill: 'white' }, new go.Binding('fill', 'color')), $(go.Panel, 'Table', { defaultColumnSeparatorStroke: 'black' }, $(go.Panel, 'Horizontal', { column: 0, angle: 270 }, $(go.TextBlock, { editable: true, margin: new go.Margin(5, 0, 5, 0) }, // margin matches private process (black box pool) new go.Binding('text').makeTwoWay()) ), $(go.Placeholder, { background: 'darkgray', column: 1 }) ) ); // end poolGroupTemplate // ------------------------------------------ Template Maps ---------------------------------------------- // create the nodeTemplateMap, holding main view node templates: const nodeTemplateMap = new go.Map<string, go.Node>(); // for each of the node categories, specify which template to use nodeTemplateMap.add('activity', activityNodeTemplate); nodeTemplateMap.add('event', eventNodeTemplate); nodeTemplateMap.add('gateway', gatewayNodeTemplate); nodeTemplateMap.add('annotation', annotationNodeTemplate); nodeTemplateMap.add('dataobject', dataObjectNodeTemplate); nodeTemplateMap.add('datastore', dataStoreNodeTemplate); nodeTemplateMap.add('privateProcess', privateProcessNodeTemplate); // for the default category, "", use the same template that Diagrams use by default // this just shows the key value as a simple TextBlock const groupTemplateMap = new go.Map<string, go.Group>(); groupTemplateMap.add('subprocess', subProcessGroupTemplate); groupTemplateMap.add('Lane', swimLanesGroupTemplate); groupTemplateMap.add('Pool', poolGroupTemplate); // create the nodeTemplateMap, holding special palette "mini" node templates: const palNodeTemplateMap = new go.Map<string, go.Node>(); palNodeTemplateMap.add('activity', activityNodeTemplateForPalette); palNodeTemplateMap.add('event', eventNodeTemplate); palNodeTemplateMap.add('gateway', gatewayNodeTemplateForPalette); palNodeTemplateMap.add('annotation', annotationNodeTemplate); palNodeTemplateMap.add('dataobject', dataObjectNodeTemplate); palNodeTemplateMap.add('datastore', dataStoreNodeTemplate); palNodeTemplateMap.add('privateProcess', privateProcessNodeTemplateForPalette); const palGroupTemplateMap = new go.Map<string, go.Group>(); palGroupTemplateMap.add('subprocess', subProcessGroupTemplateForPalette); palGroupTemplateMap.add('Pool', poolTemplateForPalette); palGroupTemplateMap.add('Lane', swimLanesGroupTemplateForPalette); // ------------------------------------------ Link Templates ---------------------------------------------- const sequenceLinkTemplate = $(go.Link, { contextMenu: $<go.Adornment>('ContextMenu', $('ContextMenuButton', $(go.TextBlock, 'Default Flow'), // in the click event handler, the obj.part is the Adornment; its adornedObject is the port { click: function (e: go.InputEvent, obj: go.GraphObject) { setSequenceLinkDefaultFlow((obj.part as go.Adornment).ado