UNPKG

gojs

Version:

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

588 lines (540 loc) 26.5 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="A seating chart editor for assigning places at tables."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title>Seating Chart</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"> <style> /* Use a Flexbox to make the Palette/Diagram responsive */ #myFlexDiv { display: flex; width: 100%; justify-content: center; } @media (min-width: 768px) { #myGuests { width: 100px; height: 500px; margin-right: 10px; } #myDiagramDiv { height: 500px; flex: 1; } } @media (max-width: 767px) { #myGuests { width: 20%; max-width: 100px; height: 500px; } #myDiagramDiv { width: 80%; height: 500px; flex: 1; } } </style> <script id="code"> 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; // Initialize the main Diagram myDiagram = new go.Diagram("myDiagramDiv", { allowDragOut: true, // to myGuests allowClipboard: false, draggingTool: $(SpecialDraggingTool), rotatingTool: $(HorizontalTextRotatingTool), // For this sample, automatically show the state of the diagram's model on the page "ModelChanged": e => { if (e.isTransactionFinished) { document.getElementById("savedModel").textContent = myDiagram.model.toJson(); } }, "undoManager.isEnabled": true }); myDiagram.nodeTemplateMap.add("", // default template, for people $(go.Node, "Auto", { background: "transparent" }, // in front of all Tables // when selected is in foreground layer new go.Binding("layerName", "isSelected", s => s ? "Foreground" : "").ofObject(), { locationSpot: go.Spot.Center }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), new go.Binding("text", "key"), { // what to do when a drag-over or a drag-drop occurs on a Node representing a table mouseDragEnter: (e, node, prev) => { const dragCopy = node.diagram.toolManager.draggingTool.copiedParts; // could be copied from palette highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true); }, mouseDragLeave: (e, node, next) => { const dragCopy = node.diagram.toolManager.draggingTool.copiedParts; highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false); }, mouseDrop: (e, node) => { assignPeopleToSeats(node, node.diagram.selection, e.documentPoint); } }, $(go.Shape, "Rectangle", { fill: "blanchedalmond", stroke: null }), $(go.Panel, "Viewbox", { desiredSize: new go.Size(50, 38) }, $(go.TextBlock, { margin: 2, desiredSize: new go.Size(55, NaN), font: "8pt Verdana, sans-serif", textAlign: "center", stroke: "darkblue" }, new go.Binding("text", "", data => { let s = data.key; if (data.plus) s += " +" + data.plus.toString(); return s; })) ) )); // Create a seat element at a particular alignment relative to the table. function Seat(number, align, focus) { if (typeof align === 'string') align = go.Spot.parse(align); if (!align || !align.isSpot()) align = go.Spot.Right; if (typeof focus === 'string') focus = go.Spot.parse(focus); if (!focus || !focus.isSpot()) focus = align.opposite(); return $(go.Panel, "Spot", { name: number.toString(), alignment: align, alignmentFocus: focus }, $(go.Shape, "Circle", { name: "SEATSHAPE", desiredSize: new go.Size(40, 40), fill: "burlywood", stroke: "white", strokeWidth: 2 }, new go.Binding("fill")), $(go.TextBlock, number.toString(), { font: "10pt Verdana, sans-serif" }, new go.Binding("angle", "angle", n => -n)) ); } function tableStyle() { return [ { background: "transparent" }, { layerName: "Background" }, // behind all Persons { locationSpot: go.Spot.Center, locationObjectName: "TABLESHAPE" }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), { rotatable: true }, new go.Binding("angle").makeTwoWay(), { // what to do when a drag-over or a drag-drop occurs on a Node representing a table mouseDragEnter: (e, node, prev) => { const dragCopy = node.diagram.toolManager.draggingTool.copiedParts; // could be copied from palette highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true); }, mouseDragLeave: (e, node, next) => { const dragCopy = node.diagram.toolManager.draggingTool.copiedParts; highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false); }, mouseDrop: (e, node) => assignPeopleToSeats(node, node.diagram.selection, e.documentPoint) } ]; } // various kinds of tables: myDiagram.nodeTemplateMap.add("TableR8", // rectangular with 8 seats $(go.Node, "Spot", tableStyle(), $(go.Panel, "Spot", $(go.Shape, "Rectangle", { name: "TABLESHAPE", desiredSize: new go.Size(160, 80), fill: "burlywood", stroke: null }, new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify), new go.Binding("fill")), $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" }, new go.Binding("text", "name").makeTwoWay(), new go.Binding("angle", "angle", n => -n)) ), Seat(1, "0.2 0", "0.5 1"), Seat(2, "0.5 0", "0.5 1"), Seat(3, "0.8 0", "0.5 1"), Seat(4, "1 0.5", "0 0.5"), Seat(5, "0.8 1", "0.5 0"), Seat(6, "0.5 1", "0.5 0"), Seat(7, "0.2 1", "0.5 0"), Seat(8, "0 0.5", "1 0.5") )); myDiagram.nodeTemplateMap.add("TableR3", // rectangular with 3 seats in a line $(go.Node, "Spot", tableStyle(), $(go.Panel, "Spot", $(go.Shape, "Rectangle", { name: "TABLESHAPE", desiredSize: new go.Size(160, 60), fill: "burlywood", stroke: null }, new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify), new go.Binding("fill")), $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" }, new go.Binding("text", "name").makeTwoWay(), new go.Binding("angle", "angle", n => -n)) ), Seat(1, "0.2 0", "0.5 1"), Seat(2, "0.5 0", "0.5 1"), Seat(3, "0.8 0", "0.5 1") )); myDiagram.nodeTemplateMap.add("TableC8", // circular with 8 seats $(go.Node, "Spot", tableStyle(), $(go.Panel, "Spot", $(go.Shape, "Circle", { name: "TABLESHAPE", desiredSize: new go.Size(120, 120), fill: "burlywood", stroke: null }, new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify), new go.Binding("fill")), $(go.TextBlock, { editable: true, font: "bold 11pt Verdana, sans-serif" }, new go.Binding("text", "name").makeTwoWay(), new go.Binding("angle", "angle", n => -n)) ), Seat(1, "0.50 0", "0.5 1"), Seat(2, "0.85 0.15", "0.15 0.85"), Seat(3, "1 0.5", "0 0.5"), Seat(4, "0.85 0.85", "0.15 0.15"), Seat(5, "0.50 1", "0.5 0"), Seat(6, "0.15 0.85", "0.85 0.15"), Seat(7, "0 0.5", "1 0.5"), Seat(8, "0.15 0.15", "0.85 0.85") )); // what to do when a drag-drop occurs in the Diagram's background myDiagram.mouseDrop = e => { // when the selection is dropped in the diagram's background, // make sure the selected people no longer belong to any table e.diagram.selection.each(n => { if (isPerson(n)) unassignSeat(n.data); }); }; // to simulate a "move" from the Palette, the source Node must be deleted. myDiagram.addDiagramListener("ExternalObjectsDropped", e => { // if any Tables were dropped, don't delete from myGuests if (!e.subject.any(isTable)) { myGuests.commandHandler.deleteSelection(); } }); // put deleted people back in the myGuests diagram myDiagram.addDiagramListener("SelectionDeleted", e => { // no-op if deleted by myGuests' ExternalObjectsDropped listener if (myDiagram.disableSelectionDeleted) return; // e.subject is the myDiagram.selection collection e.subject.each(n => { if (isPerson(n)) { myGuests.model.addNodeData(myGuests.model.copyNodeData(n.data)); } }); }); // create some initial tables myDiagram.model = new go.GraphLinksModel([ { "key": 1, "category": "TableR3", "name": "Head 1", "guests": {}, "loc": "143.5 58" }, { "key": 2, "category": "TableR3", "name": "Head 2", "guests": {}, "loc": "324.5 58" }, { "key": 3, "category": "TableR8", "name": "3", "guests": {}, "loc": "121.5 203.5" }, { "key": 4, "category": "TableC8", "name": "4", "guests": {}, "loc": "364.5 223.5" } ]); // this sample does not make use of any links // initialize the Palette myGuests = new go.Diagram("myGuests", { layout: $(go.GridLayout, { sorting: go.GridLayout.Ascending // sort by Node.text value }), allowDragOut: true, // to myDiagram allowMove: false }); myGuests.nodeTemplateMap = myDiagram.nodeTemplateMap; // specify the contents of the Palette myGuests.model = new go.GraphLinksModel([ { key: "Tyrion Lannister" }, { key: "Daenerys Targaryen", plus: 3 }, // dragons, of course { key: "Jon Snow" }, { key: "Stannis Baratheon" }, { key: "Arya Stark" }, { key: "Jorah Mormont" }, { key: "Sandor Clegane" }, { key: "Joffrey Baratheon" }, { key: "Brienne of Tarth" }, { key: "Hodor" } ]); myGuests.model.undoManager = myDiagram.model.undoManager // shared UndoManager! // To simulate a "move" from the Diagram back to the Palette, the source Node must be deleted. myGuests.addDiagramListener("ExternalObjectsDropped", e => { // e.subject is the myGuests.selection collection // if the user dragged a Table to the myGuests diagram, cancel the drag if (e.subject.any(isTable)) { myDiagram.currentTool.doCancel(); myGuests.currentTool.doCancel(); return; } myDiagram.selection.each(n => { if (isPerson(n)) unassignSeat(n.data); }); myDiagram.disableSelectionDeleted = true; myDiagram.commandHandler.deleteSelection(); myDiagram.disableSelectionDeleted = false; myGuests.selection.each(n => { if (isPerson(n)) unassignSeat(n.data); }); }); go.AnimationManager.defineAnimationEffect("location", (obj, startValue, endValue, easing, currentTime, duration, animationState) => { obj.location = new go.Point(easing(currentTime, startValue.x, endValue.x - startValue.x, duration), easing(currentTime, startValue.y, endValue.y - startValue.y, duration)); } ); } // end init function isPerson(n) { return n !== null && n.category === ""; } function isTable(n) { return n !== null && n.category !== ""; } // Highlight the empty and occupied seats at a "Table" Node function highlightSeats(node, coll, show) { if (isPerson(node)) { // refer to the person's table instead node = node.diagram.findNodeForKey(node.data.table); if (node === null) return; } const it = coll.iterator; while (it.next()) { const n = it.key; // if dragging a Table, don't do any highlighting if (isTable(n)) return; } const guests = node.data.guests; for (const sit = node.elements; sit.next();) { const seat = sit.value; if (seat.name) { const num = parseFloat(seat.name); if (isNaN(num)) continue; const seatshape = seat.findObject("SEATSHAPE"); if (!seatshape) continue; if (show) { if (guests[seat.name]) { seatshape.stroke = "red"; } else { seatshape.stroke = "green"; } } else { seatshape.stroke = "white"; } } } } // Given a "Table" Node, assign seats for all of the people in the given collection of Nodes; // the optional Point argument indicates where the collection of people may have been dropped. function assignPeopleToSeats(node, coll, pt) { if (isPerson(node)) { // refer to the person's table instead node = node.diagram.findNodeForKey(node.data.table); if (node === null) return; } if (coll.any(isTable)) { // if dragging a Table, don't allow it to be dropped onto another table myDiagram.currentTool.doCancel(); return; } // OK -- all Nodes are people, call assignSeat on each person data coll.each(n => assignSeat(node, n.data, pt)); positionPeopleAtSeats(node); } // Given a "Table" Node, assign one guest data to a seat at that table. // Also handles cases where the guest represents multiple people, because guest.plus > 0. // This tries to assign the unoccupied seat that is closest to the given point in document coordinates. function assignSeat(node, guest, pt) { if (isPerson(node)) { // refer to the person's table instead node = node.diagram.findNodeForKey(node.data.table); if (node === null) return; } if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString()); if (!(pt instanceof go.Point)) pt = node.location; // in case the guest used to be assigned to a different seat, perhaps at a different table unassignSeat(guest); const model = node.diagram.model; const guests = node.data.guests; // iterate over all seats in the Node to find one that is not occupied const closestseatname = findClosestUnoccupiedSeat(node, pt); if (closestseatname) { model.setDataProperty(guests, closestseatname, guest.key); model.setDataProperty(guest, "table", node.data.key); model.setDataProperty(guest, "seat", parseFloat(closestseatname)); } const plus = guest.plus; if (plus) { // represents several people // forget the "plus" info, since next we create N copies of the node/data guest.plus = undefined; model.updateTargetBindings(guest); for (let i = 0; i < plus; i++) { const copy = model.copyNodeData(guest); // don't copy the seat assignment of the first person copy.table = undefined; copy.seat = undefined; model.addNodeData(copy); assignSeat(node, copy, pt); } } } // Declare that the guest represented by the data is no longer assigned to a seat at a table. // If the guest had been at a table, the guest is removed from the table's list of guests. function unassignSeat(guest) { if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString()); const model = myDiagram.model; // remove from any table that the guest is assigned to if (guest.table) { const table = model.findNodeDataForKey(guest.table); if (table) { const guests = table.guests; if (guests) model.setDataProperty(guests, guest.seat.toString(), undefined); } } model.setDataProperty(guest, "table", undefined); model.setDataProperty(guest, "seat", undefined); } // Find the name of the unoccupied seat that is closest to the given Point. // This returns null if no seat is available at this table. function findClosestUnoccupiedSeat(node, pt) { if (isPerson(node)) { // refer to the person's table instead node = node.diagram.findNodeForKey(node.data.table); if (node === null) return; } const guests = node.data.guests; let closestseatname = null; let closestseatdist = Infinity; // iterate over all seats in the Node to find one that is not occupied for (const sit = node.elements; sit.next();) { const seat = sit.value; if (seat.name) { const num = parseFloat(seat.name); if (isNaN(num)) continue; // not really a "seat" if (guests[seat.name]) continue; // already assigned const seatloc = seat.getDocumentPoint(go.Spot.Center); const seatdist = seatloc.distanceSquaredPoint(pt); if (seatdist < closestseatdist) { closestseatdist = seatdist; closestseatname = seat.name; } } } return closestseatname; } // Position the nodes of all of the guests that are seated at this table // to be at their corresponding seat elements of the given "Table" Node. function positionPeopleAtSeats(node) { if (isPerson(node)) { // refer to the person's table instead node = node.diagram.findNodeForKey(node.data.table); if (node === null) return; } const guests = node.data.guests; const model = node.diagram.model; for (let seatname in guests) { const guestkey = guests[seatname]; const guestdata = model.findNodeDataForKey(guestkey); positionPersonAtSeat(guestdata); } } // Position a single guest Node to be at the location of the seat to which they are assigned. function positionPersonAtSeat(guest) { if (guest instanceof go.GraphObject) throw Error("A guest object must not be a GraphObject: " + guest.toString()); if (!guest || !guest.table || !guest.seat) return; const diagram = myDiagram; const table = diagram.findPartForKey(guest.table); const person = diagram.findPartForData(guest); if (table && person) { const seat = table.findObject(guest.seat.toString()); const loc = seat.getDocumentPoint(go.Spot.Center); // animate movement, instead of: person.location = loc; const animation = new go.Animation(); animation.add(person, "location", person.location, loc); animation.start(); } } // Automatically drag people Nodes along with the table Node at which they are seated. class SpecialDraggingTool extends go.DraggingTool { computeEffectiveCollection(parts) { const map = super.computeEffectiveCollection(parts); // for each Node representing a table, also drag all of the people seated at that table parts.each(table => { if (isPerson(table)) return; // ignore persons // this is a table Node, find all people Nodes using the same table key for (const nit = table.diagram.nodes; nit.next();) { const n = nit.value; if (isPerson(n) && n.data.table === table.data.key) { if (!map.has(n)) map.add(n, new go.DraggingInfo(n.location.copy())); } } }); return map; } } // end SpecialDraggingTool // Automatically move seated people as a table is rotated, to keep them in their seats. // Note that because people are separate Nodes, rotating a table Node means the people Nodes // are not rotated, so their names (TextBlocks) remain horizontal. class HorizontalTextRotatingTool extends go.RotatingTool { rotate(newangle) { super.rotate(newangle); const node = this.adornedObject.part; positionPeopleAtSeats(node); } } // end HorizontalTextRotatingTool window.addEventListener('DOMContentLoaded', init); </script> <div id="sample"> <div id="myFlexDiv"> <div id="myGuests" style="border: solid 1px black"></div> <div id="myDiagramDiv" style="border: solid 1px black"></div> </div> This sample demonstrates how a "Person" node can be dropped onto a "Table" node, causing the person to be assigned a position at the closest empty seat at that table. When a person is dropped into the background of the diagram, that person is no longer assigned a seat. People dragged between diagrams are automatically removed from the diagram they came from. <p> "Table" nodes are defined by separate templates, to permit maximum customization of the shapes and sizes and positions of the tables and chairs. <p> "Person" nodes in the <code>myGuests</code> diagram can also represent a group of people, for example a named person plus one whose name might not be known. When such a person is dropped onto a table, additional nodes are created in <code>myDiagram</code>. Those people are seated at the table if there is room. <p> Tables can be moved or rotated. Moving or rotating a table automatically repositions the people seated at that table. <p> The <a>UndoManager</a> is shared between the two Diagrams, so that one can undo/redo in either diagram and have it automatically handle drags between diagrams, as well as the usual changes within the diagram. </p> <div> Diagram Model saved in JSON format, automatically updated after each transaction: <pre id="savedModel" style="height:250px"></pre> </div> </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>