UNPKG

gojs

Version:

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

432 lines (410 loc) 22.8 kB
<!DOCTYPE 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="An interactive Kanban board editor, a visual control used for organizing work items."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title>Kanban Board</title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary"> <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2"> <div class="md:pl-4"> <a class="text-white hover:text-white no-underline hover:no-underline font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../"> <h1 class="my-0 p-1 ">GoJS</h1> </a> </div> <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path> <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20"> <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0"> <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li> </ul> </div> </div> <hr class="border-b border-gray-600 opacity-50 my-0 py-0" /> </nav> <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="../release/go.js"></script> <div id="allSampleContent" class="p-4 w-full"> <link href='https://fonts.googleapis.com/css?family=Lato:300,400,700' rel='stylesheet' type='text/css'> <script id="code"> // 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() { super(); this.MINLENGTH = 200; // this controls the minimum length of any swimlane this.MINBREADTH = 100; // this controls the minimum breadth of any non-collapsed swimlane this.cellSize = new go.Size(1, 1); this.wrappingColumn = Infinity; this.wrappingWidth = Infinity; this.spacing = new go.Size(0, 0); this.alignment = go.GridLayout.Position; } doLayout(coll) { const diagram = this.diagram; if (diagram === null) return; diagram.startTransaction("PoolLayout"); // make sure all of the Group Shapes are big enough const minlen = this.computeMinPoolLength(); diagram.findTopLevelGroups().each(lane => { if (!(lane instanceof go.Group)) return; const shape = lane.selectionObject; if (shape !== null) { // change the desiredSize to be big enough in both directions const sz = this.computeLaneSize(lane); shape.width = (!isNaN(shape.width)) ? Math.max(shape.width, sz.width) : sz.width; // if you want the height of all of the lanes to shrink as the maximum needed height decreases: shape.height = minlen; // if you want the height of all of the lanes to remain at the maximum height ever needed: //shape.height = (isNaN(shape.height) ? minlen : Math.max(shape.height, minlen)); 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"); }; // compute the minimum length of the whole diagram needed to hold all of the Lane Groups computeMinPoolLength() { let len = this.MINLENGTH; myDiagram.findTopLevelGroups().each(lane => { const holder = lane.placeholder; if (holder !== null) { const sz = holder.actualBounds; len = Math.max(len, sz.height); } }); return len; } // compute the minimum size for a particular Lane Group computeLaneSize(lane) { // assert(lane instanceof go.Group); const sz = new go.Size(lane.isSubGraphExpanded ? this.MINBREADTH : 1, this.MINLENGTH); if (lane.isSubGraphExpanded) { const holder = lane.placeholder; if (holder !== null) { const hsz = holder.actualBounds; sz.width = Math.max(sz.width, hsz.width); } } // minimum breadth needs to be big enough to hold the header const hdr = lane.findObject("HEADER"); if (hdr !== null) sz.width = Math.max(sz.width, hdr.actualBounds.width); return sz; } } // end PoolLayout class function init() { // Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make // For details, see https://gojs.net/latest/intro/buildingObjects.html const $ = go.GraphObject.make; myDiagram = new go.Diagram("myDiagramDiv", { // make sure the top-left corner of the viewport is occupied contentAlignment: go.Spot.TopLeft, // use a simple layout to stack the top-level Groups next to each other layout: $(PoolLayout), // disallow nodes to be dragged to the diagram's background mouseDrop: e => { 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": relayoutDiagram, // this DiagramEvent listener is "SelectionCopied": relayoutDiagram, // defined above "undoManager.isEnabled": true, // allow TextEditingTool to start without selecting first "textEditingTool.starting": go.TextEditingTool.SingleClick }); // Customize the dragging tool: // When dragging a node set its opacity to 0.6 and move it to be in front of other nodes myDiagram.toolManager.draggingTool.doActivate = function() { // method override must be function, not => go.DraggingTool.prototype.doActivate.call(this); this.currentPart.opacity = 0.6; this.currentPart.layerName = "Foreground"; } myDiagram.toolManager.draggingTool.doDeactivate = function() { // method override must be function, not => this.currentPart.opacity = 1; this.currentPart.layerName = ""; go.DraggingTool.prototype.doDeactivate.call(this); } // this is called after nodes have been moved function relayoutDiagram() { myDiagram.selection.each(n => n.invalidateLayout()); myDiagram.layoutDiagram(); } // There are only three note colors by default, blue, red, and yellow but you could add more here: const noteColors = ['#009CCC', '#CC293D', '#FFD700']; function getNoteColor(num) { return noteColors[Math.min(num, noteColors.length - 1)]; } myDiagram.nodeTemplate = $(go.Node, "Horizontal", new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Shape, "Rectangle", { fill: '#009CCC', strokeWidth: 1, stroke: '#009CCC', width: 6, stretch: go.GraphObject.Vertical, alignment: go.Spot.Left, // if a user clicks the colored portion of a node, cycle through colors click: (e, obj) => { myDiagram.startTransaction("Update node color"); let newColor = parseInt(obj.part.data.color) + 1; if (newColor > noteColors.length - 1) newColor = 0; myDiagram.model.setDataProperty(obj.part.data, "color", newColor); myDiagram.commitTransaction("Update node color"); } }, new go.Binding("fill", "color", getNoteColor), new go.Binding("stroke", "color", getNoteColor) ), $(go.Panel, "Auto", $(go.Shape, "Rectangle", { fill: "white", stroke: '#CCCCCC' }), $(go.Panel, "Table", { width: 130, minSize: new go.Size(NaN, 50) }, $(go.TextBlock, { name: 'TEXT', margin: 6, font: '11px Lato, sans-serif', editable: true, stroke: "#000", maxSize: new go.Size(130, NaN), alignment: go.Spot.TopLeft }, new go.Binding("text", "text").makeTwoWay()) ) ) ); // While dragging, highlight the dragged-over group function highlightGroup(grp, show) { if (show) { const part = myDiagram.toolManager.draggingTool.currentPart; if (part.containingGroup !== grp) { grp.isHighlighted = true; return; } } grp.isHighlighted = false; } myDiagram.groupTemplate = $(go.Group, "Vertical", { selectable: false, selectionObjectName: "SHAPE", // even though its not selectable, this is used in the layout layerName: "Background", // all lanes are always behind all nodes and links layout: $(go.GridLayout, // automatically lay out the lane's subgraph { wrappingColumn: 1, cellSize: new go.Size(1, 1), spacing: new go.Size(5, 5), alignment: go.GridLayout.Position, comparer: (a, b) => { // can re-order tasks within a lane 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; } }), click: (e, grp) => { // allow simple click on group to clear selection if (!e.shift && !e.control && !e.meta) e.diagram.clearSelection(); }, computesBoundsIncludingLocation: true, computesBoundsAfterDrag: true, // needed to prevent recomputing Group.placeholder bounds too soon handlesDragDropForMembers: true, // don't need to define handlers on member Nodes and Links mouseDragEnter: (e, grp, prev) => highlightGroup(grp, true), mouseDragLeave: (e, grp, next) => highlightGroup(grp, false), mouseDrop: (e, grp) => { // 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.all(n => !(n instanceof go.Group))) { const ok = grp.addMembers(grp.diagram.selection, true); if (!ok) grp.diagram.currentTool.doCancel(); } }, subGraphExpandedChanged: grp => { const shp = grp.selectionObject; if (grp.diagram.undoManager.isUndoingRedoing) return; if (grp.isSubGraphExpanded) { shp.width = grp.data.savedBreadth; } else { // remember the original width if (!isNaN(shp.width)) grp.diagram.model.set(grp.data, "savedBreadth", shp.width); shp.width = NaN; } } }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(), // the lane header consisting of a TextBlock and an expander button $(go.Panel, "Horizontal", { name: "HEADER", alignment: go.Spot.Left }, $("SubGraphExpanderButton", { margin: 5 }), // this remains always visible $(go.TextBlock, // the lane label { font: "15px Lato, sans-serif", editable: true, margin: new go.Margin(2, 0, 0, 0) }, // this is hidden when the swimlane is collapsed new go.Binding("visible", "isSubGraphExpanded").ofObject(), new go.Binding("text").makeTwoWay()) ), // end Horizontal Panel $(go.Panel, "Auto", // the lane consisting of a background Shape and a Placeholder representing the subgraph $(go.Shape, "Rectangle", // this is the resized object { name: "SHAPE", fill: "#F1F1F1", stroke: null, strokeWidth: 4 }, // strokeWidth controls space between lanes new go.Binding("fill", "isHighlighted", h => h ? "#D6D6D6" : "#F1F1F1").ofObject(), new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify)), $(go.Placeholder, { padding: 12, alignment: go.Spot.TopLeft }), $(go.TextBlock, // this TextBlock is only seen when the swimlane is collapsed { name: "LABEL", font: "15px Lato, sans-serif", editable: true, angle: 90, alignment: go.Spot.TopLeft, margin: new go.Margin(4, 0, 0, 2) }, new go.Binding("visible", "isSubGraphExpanded", e => !e).ofObject(), new go.Binding("text").makeTwoWay()) ) // end Auto Panel ); // end Group // Set up an unmodeled Part as a legend, and place it directly on the diagram. myDiagram.add( $(go.Part, "Table", { position: new go.Point(10, 10), selectable: false }, $(go.TextBlock, "Key", { row: 0, font: "700 14px Droid Serif, sans-serif" }), // end row 0 $(go.Panel, "Horizontal", { row: 1, alignment: go.Spot.Left }, $(go.Shape, "Rectangle", { desiredSize: new go.Size(10, 10), fill: '#CC293D', margin: 5 }), $(go.TextBlock, "Halted", { font: "700 13px Droid Serif, sans-serif" }) ), // end row 1 $(go.Panel, "Horizontal", { row: 2, alignment: go.Spot.Left }, $(go.Shape, "Rectangle", { desiredSize: new go.Size(10, 10), fill: '#FFD700', margin: 5 }), $(go.TextBlock, "In Progress", { font: "700 13px Droid Serif, sans-serif" }) ), // end row 2 $(go.Panel, "Horizontal", { row: 3, alignment: go.Spot.Left }, $(go.Shape, "Rectangle", { desiredSize: new go.Size(10, 10), fill: '#009CCC', margin: 5 }), $(go.TextBlock, "Completed", { font: "700 13px Droid Serif, sans-serif" }) ), // end row 3 $(go.Panel, "Horizontal", { row: 4, click: (e, node) => { e.diagram.startTransaction('add node'); let sel = e.diagram.selection.first(); if (!sel) sel = e.diagram.findTopLevelGroups().first(); if (!(sel instanceof go.Group)) sel = sel.containingGroup; if (!sel) return; const newdata = { group: sel.key, loc: "0 9999", text: "New item " + sel.memberParts.count, color: 0 }; e.diagram.model.addNodeData(newdata); e.diagram.commitTransaction('add node'); const newnode = myDiagram.findNodeForData(newdata); e.diagram.select(newnode); e.diagram.commandHandler.editTextBlock(); e.diagram.commandHandler.scrollToPart(newnode); }, background: 'white', margin: new go.Margin(10, 4, 4, 4) }, $(go.Panel, "Auto", $(go.Shape, "Rectangle", { strokeWidth: 0, stroke: null, fill: '#6FB583' }), $(go.Shape, "PlusLine", { margin: 6, strokeWidth: 2, width: 12, height: 12, stroke: 'white', background: '#6FB583' }) ), $(go.TextBlock, "New item", { font: '10px Lato, sans-serif', margin: 6, }) ) ) ); load(); } // 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); } window.addEventListener('DOMContentLoaded', init); </script> <div id="sample"> <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:500px;"></div> <p>A Kanban board is a work and workflow visualization used to communicate the status and progress of work items. Click on the color of a note to cycle through colors.</p> <p> This design and implementation were adapted from the <a href="swimLanesVertical.html">Swim Lanes (vertical)</a> sample. Unlike that sample: <ul> <li>there are no Links</li> <li>lanes cannot be nested into "pools"</li> <li>lanes cannot be resized</li> <li>the user cannot drop tasks into the diagram's background</li> <li>all tasks are ordered within a single column; the user can rearrange the order</li> <li>tasks can freely be moved into other lanes</li> <li>lanes are not movable or copyable or deletable</li> </ul> </p> <button id="SaveButton" onclick="save()">Save</button> <button onclick="load()">Load</button> Diagram Model saved in JSON format: <br /> <textarea id="mySavedModel" style="width:100%;height:300px"> { "class": "go.GraphLinksModel", "nodeDataArray": [ {"key":"Problems", "text":"Problems", "isGroup":true, "loc":"0 23.52284749830794" }, {"key":"Reproduced", "text":"Reproduced", "isGroup":true, "color":"0", "loc":"109 23.52284749830794" }, {"key":"Identified", "text":"Identified", "isGroup":true, "color":"0", "loc":"235 23.52284749830794" }, {"key":"Fixing", "text":"Fixing", "isGroup":true, "color":"0", "loc":"343 23.52284749830794" }, {"key":"Reviewing", "text":"Reviewing", "isGroup":true, "color":"0", "loc":"451 23.52284749830794"}, {"key":"Testing", "text":"Testing", "isGroup":true, "color":"0", "loc":"562 23.52284749830794" }, {"key":"Customer", "text":"Customer", "isGroup":true, "color":"0", "loc":"671 23.52284749830794" }, {"key":1, "text":"text for oneA", "group":"Problems", "color":"0", "loc":"12 35.52284749830794"}, {"key":2, "text":"text for oneB", "group":"Problems", "color":"1", "loc":"12 65.52284749830794"}, {"key":3, "text":"text for oneC", "group":"Problems", "color":"0", "loc":"12 95.52284749830794"}, {"key":4, "text":"text for oneD", "group":"Problems", "color":"1", "loc":"12 125.52284749830794"}, {"key":5, "text":"text for twoA", "group":"Reproduced", "color":"1", "loc":"121 35.52284749830794"}, {"key":6, "text":"text for twoB", "group":"Reproduced", "color":"1", "loc":"121 65.52284749830794"}, {"key":7, "text":"text for twoC", "group":"Identified", "color":"0", "loc":"247 35.52284749830794"}, {"key":8, "text":"text for twoD", "group":"Fixing", "color":"0", "loc":"355 35.52284749830794"}, {"key":9, "text":"text for twoE", "group":"Reviewing", "color":"0", "loc":"463 35.52284749830794"}, {"key":10, "text":"text for twoF", "group":"Reviewing", "color":"1", "loc":"463 65.52284749830794"}, {"key":11, "text":"text for twoG", "group":"Testing", "color":"0", "loc":"574 35.52284749830794"}, {"key":12, "text":"text for fourA", "group":"Customer", "color":"1", "loc":"683 35.52284749830794"}, {"key":13, "text":"text for fourB", "group":"Customer", "color":"1", "loc":"683 65.52284749830794"}, {"key":14, "text":"text for fourC", "group":"Customer", "color":"1", "loc":"683 95.52284749830794"}, {"key":15, "text":"text for fourD", "group":"Customer", "color":"0", "loc":"683 125.52284749830794"}, {"key":16, "text":"text for fiveA", "group":"Customer", "color":"0", "loc":"683 155.52284749830795"} ], "linkDataArray": []} </textarea> </div> </div> <!-- * * * * * * * * * * * * * --> <!-- End of GoJS sample code --> </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>