gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
534 lines (491 loc) • 23 kB
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>