import mxgraph from '@mxgraph/core'; import { globalTypes } from '../.storybook/preview'; export default { title: 'Layouts/SwimLanes', argTypes: { ...globalTypes } }; const Template = ({ label, ...args }) => { const { mxEditor, mxConnectionHandler, mxImage, mxPerimeter, mxPoint, mxConstants, mxCloneUtils, mxEdgeStyle, mxEvent, mxSwimlaneManager, mxStackLayout, mxLayoutManager } = mxgraph; const container = document.createElement('div'); container.style.position = 'relative'; container.style.overflow = 'hidden'; container.style.width = `${args.width}px`; container.style.height = `${args.height}px`; container.style.background = 'url(/images/grid.gif)'; container.style.cursor = 'default'; // Defines an icon for creating new connections in the connection handler. // This will automatically disable the highlighting of the source vertex. mxConnectionHandler.prototype.connectImage = new mxImage( 'images/connector.gif', 16, 16 ); // Creates a wrapper editor around a new graph inside // the given container using an XML config for the // keyboard bindings // const config = mxUtils // .load('editors/config/keyhandler-commons.xml') // .getDocumentElement(); // const editor = new mxEditor(config); const editor = new mxEditor(null); editor.setGraphContainer(container); const { graph } = editor; const model = graph.getModel(); // Auto-resizes the container graph.border = 80; graph.getView().translate = new mxPoint(graph.border / 2, graph.border / 2); graph.setResizeContainer(true); graph.graphHandler.setRemoveCellsFromParent(false); // Changes the default vertex style in-place let style = graph.getStylesheet().getDefaultVertexStyle(); style.shape = mxConstants.SHAPE_SWIMLANE; style.verticalAlign = 'middle'; style.labelBackgroundColor = 'white'; style.fontSize = 11; style.startSize = 22; style.horizontal = false; style.fontColor = 'black'; style.strokeColor = 'black'; delete style.fillColor; style = mxCloneUtils.clone(style); style.shape = mxConstants.SHAPE_RECTANGLE; style.fontSize = 10; style.rounded = true; style.horizontal = true; style.verticalAlign = 'middle'; delete style.startSize; style.labelBackgroundColor = 'none'; graph.getStylesheet().putCellStyle('process', style); style = mxCloneUtils.clone(style); style.shape = mxConstants.SHAPE_ELLIPSE; style.perimiter = mxPerimeter.EllipsePerimeter; delete style.rounded; graph.getStylesheet().putCellStyle('state', style); style = mxCloneUtils.clone(style); style.shape = mxConstants.SHAPE_RHOMBUS; style.perimiter = mxPerimeter.RhombusPerimeter; style.verticalAlign = 'top'; style.spacingTop = 40; style.spacingRight = 64; graph.getStylesheet().putCellStyle('condition', style); style = mxCloneUtils.clone(style); style.shape = mxConstants.SHAPE_DOUBLE_ELLIPSE; style.perimiter = mxPerimeter.EllipsePerimeter; style.spacingTop = 28; style.fontSize = 14; style.fontStyle = 1; delete style.spacingRight; graph.getStylesheet().putCellStyle('end', style); style = graph.getStylesheet().getDefaultEdgeStyle(); style.edge = mxEdgeStyle.ElbowConnector; style.endArrow = mxConstants.ARROW_BLOCK; style.rounded = true; style.fontColor = 'black'; style.strokeColor = 'black'; style = mxCloneUtils.clone(style); style.dashed = true; style.endArrow = mxConstants.ARROW_OPEN; style.startArrow = mxConstants.ARROW_OVAL; graph.getStylesheet().putCellStyle('crossover', style); // Installs double click on middle control point and // changes style of edges between empty and this value graph.alternateEdgeStyle = 'elbow=vertical'; // Adds automatic layout and various switches if the // graph is enabled if (graph.isEnabled()) { // Allows new connections but no dangling edges graph.setConnectable(true); graph.setAllowDanglingEdges(false); // End-states are no valid sources const previousIsValidSource = graph.isValidSource; graph.isValidSource = function(cell) { if (previousIsValidSource.apply(this, arguments)) { const style = cell.getStyle(); return ( style == null || !(style == 'end' || style.indexOf('end') == 0) ); } return false; }; // Start-states are no valid targets, we do not // perform a call to the superclass function because // this would call isValidSource // Note: All states are start states in // the example below, so we use the state // style below graph.isValidTarget = function(cell) { const style = cell.getStyle(); return ( !cell.isEdge() && !this.isSwimlane(cell) && (style == null || !(style == 'state' || style.indexOf('state') == 0)) ); }; // Allows dropping cells into new lanes and // lanes into new pools, but disallows dropping // cells on edges to split edges graph.setDropEnabled(true); graph.setSplitEnabled(false); // Returns true for valid drop operations graph.isValidDropTarget = function(target, cells, evt) { if (this.isSplitEnabled() && this.isSplitTarget(target, cells, evt)) { return true; } const model = this.getModel(); let lane = false; let pool = false; let cell = false; // Checks if any lanes or pools are selected for (let i = 0; i < cells.length; i++) { const tmp = cells[i].getParent(); lane = lane || this.isPool(tmp); pool = pool || this.isPool(cells[i]); cell = cell || !(lane || pool); } return ( !pool && cell != lane && ((lane && this.isPool(target)) || (cell && this.isPool(target.getParent()))) ); }; // Adds new method for identifying a pool graph.isPool = function(cell) { const model = this.getModel(); const parent = cell.getParent(); return parent != null && parent.getParent() == model.getRoot(); }; // Keeps widths on collapse/expand const foldingHandler = function(sender, evt) { const cells = evt.getProperty('cells'); for (let i = 0; i < cells.length; i++) { const geo = cells[i].getGeometry(); if (geo.alternateBounds != null) { geo.width = geo.alternateBounds.width; } } }; graph.addListener(mxEvent.FOLD_CELLS, foldingHandler); } // Changes swimlane orientation while collapsed const getStyle = function() { // TODO super cannot be used here // let style = super.getStyle(); let style; if (this.isCollapsed()) { if (style != null) { style += ';'; } else { style = ''; } style += 'horizontal=1;align=left;spacingLeft=14;'; } return style; }; // Applies size changes to siblings and parents new mxSwimlaneManager(graph); // Creates a stack depending on the orientation of the swimlane const layout = new mxStackLayout(graph, false); // Makes sure all children fit into the parent swimlane layout.resizeParent = true; // Applies the size to children if parent size changes layout.fill = true; // Only update the size of swimlanes layout.isVertexIgnored = function(vertex) { return !graph.isSwimlane(vertex); }; // Keeps the lanes and pools stacked const layoutMgr = new mxLayoutManager(graph); layoutMgr.getLayout = function(cell) { if ( !cell.isEdge() && cell.getChildCount() > 0 && (cell.getParent() == model.getRoot() || graph.isPool(cell)) ) { layout.fill = graph.isPool(cell); return layout; } return null; }; // Gets the default parent for inserting new cells. This // is normally the first child of the root (ie. layer 0). const parent = graph.getDefaultParent(); const insertVertex = options => { const v = graph.insertVertex(options); v.getStyle = getStyle; return v; }; const insertEdge = options => { const e = graph.insertEdge(options); e.getStyle = getStyle; return e; }; // Adds cells to the model in a single step graph.batchUpdate(() => { const pool1 = insertVertex({ parent, value: 'Pool 1', position: [0, 0], size: [640, 0], }); pool1.setConnectable(false); const lane1a = insertVertex({ parent: pool1, value: 'Lane A', position: [0, 0], size: [640, 110], }); lane1a.setConnectable(false); const lane1b = insertVertex({ parent: pool1, value: 'Lane B', position: [0, 0], size: [640, 110], }); lane1b.setConnectable(false); const pool2 = insertVertex({ parent, value: 'Pool 2', position: [0, 0], size: [640, 0], }); pool2.setConnectable(false); const lane2a = insertVertex({ parent: pool2, value: 'Lane A', position: [0, 0], size: [640, 140], }); lane2a.setConnectable(false); const lane2b = insertVertex({ parent: pool2, value: 'Lane B', position: [0, 0], size: [640, 110], }); lane2b.setConnectable(false); const start1 = insertVertex({ parent: lane1a, position: [40, 40], size: [30, 30], style: 'state', }); const end1 = insertVertex({ parent: lane1a, value: 'A', position: [560, 40], size: [30, 30], style: 'end', }); const step1 = insertVertex({ parent: lane1a, value: 'Contact\nProvider', position: [90, 30], size: [80, 50], style: 'process', }); const step11 = insertVertex({ parent: lane1a, value: 'Complete\nAppropriate\nRequest', position: [190, 30], size: [80, 50], style: 'process', }); const step111 = insertVertex({ parent: lane1a, value: 'Receive and\nAcknowledge', position: [385, 30], size: [80, 50], style: 'process', }); const start2 = insertVertex({ parent: lane2b, position: [40, 40], size: [30, 30], style: 'state', }); const step2 = insertVertex({ parent: lane2b, value: 'Receive\nRequest', position: [90, 30], size: [80, 50], style: 'process', }); const step22 = insertVertex({ parent: lane2b, value: 'Refer to Tap\nSystems\nCoordinator', position: [190, 30], size: [80, 50], style: 'process', }); const step3 = insertVertex({ parent: lane1b, value: 'Request 1st-\nGate\nInformation', position: [190, 30], size: [80, 50], style: 'process', }); const step33 = insertVertex({ parent: lane1b, value: 'Receive 1st-\nGate\nInformation', position: [290, 30], size: [80, 50], style: 'process', }); const step4 = insertVertex({ parent: lane2a, value: 'Receive and\nAcknowledge', position: [290, 20], size: [80, 50], style: 'process', }); const step44 = insertVertex({ parent: lane2a, value: 'Contract\nConstraints?', position: [400, 20], size: [50, 50], style: 'condition', }); const step444 = insertVertex({ parent: lane2a, value: 'Tap for gas\ndelivery?', position: [480, 20], size: [50, 50], style: 'condition', }); const end2 = insertVertex({ parent: lane2a, value: 'B', position: [560, 30], size: [30, 30], style: 'end', }); const end3 = insertVertex({ parent: lane2a, value: 'C', position: [560, 84], size: [30, 30], style: 'end', }); let e = null; insertEdge({ parent: lane1a, source: start1, target: step1, }); insertEdge({ parent: lane1a, source: step1, target: step11, }); insertEdge({ parent: lane1a, source: step11, target: step111, }); insertEdge({ parent: lane2b, source: start2, target: step2, }); insertEdge({ parent: lane2b, source: step2, target: step22, }); insertEdge({ parent, source: step22, target: step3, }); insertEdge({ parent: lane1b, source: step3, target: step33, }); insertEdge({ parent: lane2a, source: step4, target: step44, }); insertEdge({ parent: lane2a, value: 'No', source: step44, target: step444, style: 'verticalAlign=bottom', }); insertEdge({ parent, value: 'Yes', source: step44, target: step111, style: 'verticalAlign=bottom;horizontal=0;labelBackgroundColor=white;', }); insertEdge({ parent: lane2a, value: 'Yes', source: step444, target: end2, style: 'verticalAlign=bottom', }); e = insertEdge({ parent: lane2a, value: 'No', source: step444, target: end3, style: 'verticalAlign=top', }); e.geometry.points = [ new mxPoint( step444.geometry.x + step444.geometry.width / 2, end3.geometry.y + end3.geometry.height / 2 ), ]; insertEdge({ parent, source: step1, target: step2, style: 'crossover', }); insertEdge({ parent, source: step3, target: step11, style: 'crossover', }); e = insertEdge({ parent: lane1a, source: step11, target: step33, style: 'crossover', }); e.geometry.points = [ new mxPoint( step33.geometry.x + step33.geometry.width / 2 + 20, step11.geometry.y + (step11.geometry.height * 4) / 5 ), ]; insertEdge({ parent, source: step33, target: step4, }); insertEdge({ parent: lane1a, source: step111, target: end1, }); }); return container; } export const Default = Template.bind({});