UNPKG

gojs

Version:

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

534 lines (491 loc) 23 kB
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Seating Chart</title> <meta name="description" content="A seating chart editor for assigning places at tables." /> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Copyright 1998-2020 by Northwoods Software Corporation. --> <style> /* Use a Flexbox to make the Palette/Diagram responsive */ #myFlexDiv { display: -webkit-flex; display: flex; flex-flow: row wrap; 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: 90%; height: 100px; margin-bottom: 10px; } #myDiagramDiv { width: 90%; height: 500px; } } </style> <script src="../release/go.js"></script> <script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework --> <script id="code"> function init() { if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this var $ = go.GraphObject.make; // Initialize the main Diagram myDiagram = $(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": function(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", function(s) { return 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: function(e, node, prev) { var dragCopy = node.diagram.toolManager.draggingTool.copiedParts; // could be copied from palette highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true); }, mouseDragLeave: function(e, node, next) { var dragCopy = node.diagram.toolManager.draggingTool.copiedParts; highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false); }, mouseDrop: function(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", "", function(data) { var 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", function(n) { return -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: function(e, node, prev) { var dragCopy = node.diagram.toolManager.draggingTool.copiedParts; // could be copied from palette highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, true); }, mouseDragLeave: function(e, node, next) { var dragCopy = node.diagram.toolManager.draggingTool.copiedParts; highlightSeats(node, dragCopy ? dragCopy : node.diagram.selection, false); }, mouseDrop: function(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", function(n) { return -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", function(n) { return -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", function(n) { return -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 = function(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(function(n) { if (isPerson(n)) unassignSeat(n.data); }); }; // to simulate a "move" from the Palette, the source Node must be deleted. myDiagram.addDiagramListener("ExternalObjectsDropped", function(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", function(e) { // no-op if deleted by myGuests' ExternalObjectsDropped listener if (myDiagram.disableSelectionDeleted) return; // e.subject is the myDiagram.selection collection e.subject.each(function(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 = $(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", function(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(function(n) { if (isPerson(n)) unassignSeat(n.data); }); myDiagram.disableSelectionDeleted = true; myDiagram.commandHandler.deleteSelection(); myDiagram.disableSelectionDeleted = false; myGuests.selection.each(function(n) { if (isPerson(n)) unassignSeat(n.data); }); }); } // 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; } var it = coll.iterator; while (it.next()) { var n = it.key; // if dragging a Table, don't do any highlighting if (isTable(n)) return; } var guests = node.data.guests; for (var sit = node.elements; sit.next();) { var seat = sit.value; if (seat.name) { var num = parseFloat(seat.name); if (isNaN(num)) continue; var 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(function(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); var model = node.diagram.model; var guests = node.data.guests; // iterate over all seats in the Node to find one that is not occupied var 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)); } var 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 (var i = 0; i < plus; i++) { var 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()); var model = myDiagram.model; // remove from any table that the guest is assigned to if (guest.table) { var table = model.findNodeDataForKey(guest.table); if (table) { var 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; } var guests = node.data.guests; var closestseatname = null; var closestseatdist = Infinity; // iterate over all seats in the Node to find one that is not occupied for (var sit = node.elements; sit.next();) { var seat = sit.value; if (seat.name) { var num = parseFloat(seat.name); if (isNaN(num)) continue; // not really a "seat" if (guests[seat.name]) continue; // already assigned var seatloc = seat.getDocumentPoint(go.Spot.Center); var 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; } var guests = node.data.guests; var model = node.diagram.model; for (var seatname in guests) { var guestkey = guests[seatname]; var 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; var diagram = myDiagram; var table = diagram.findPartForKey(guest.table); var person = diagram.findPartForData(guest); if (table && person) { var seat = table.findObject(guest.seat.toString()); var loc = seat.getDocumentPoint(go.Spot.Center); person.location = loc; } } // Automatically drag people Nodes along with the table Node at which they are seated. function SpecialDraggingTool() { go.DraggingTool.call(this); this.isCopyEnabled = false; // don't want to copy people except between Diagrams } go.Diagram.inherit(SpecialDraggingTool, go.DraggingTool); SpecialDraggingTool.prototype.computeEffectiveCollection = function(parts) { var map = go.DraggingTool.prototype.computeEffectiveCollection.call(this, parts); // for each Node representing a table, also drag all of the people seated at that table parts.each(function(table) { if (isPerson(table)) return; // ignore persons // this is a table Node, find all people Nodes using the same table key for (var nit = table.diagram.nodes; nit.next();) { var 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. function HorizontalTextRotatingTool() { go.RotatingTool.call(this); } go.Diagram.inherit(HorizontalTextRotatingTool, go.RotatingTool); HorizontalTextRotatingTool.prototype.rotate = function(newangle) { go.RotatingTool.prototype.rotate.call(this, newangle); var node = this.adornedObject.part; positionPeopleAtSeats(node); }; // end HorizontalTextRotatingTool </script> </head> <body onload="init()"> <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> </body> </html>