518 lines
21 KiB
HTML
518 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Seating Chart</title>
|
|
<meta name="description" content="A seating chart editor for assigning places at tables." />
|
|
<!-- Copyright 1998-2017 by Northwoods Software Corporation. -->
|
|
<meta charset="UTF-8">
|
|
<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
|
|
allowDrop: true, // from myGuests
|
|
allowClipboard: false,
|
|
initialContentAlignment: go.Spot.Center,
|
|
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) { highlightSeats(node, node.diagram.selection, true); },
|
|
mouseDragLeave: function(e, node, next) { highlightSeats(node, 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) { highlightSeats(node, node.diagram.selection, true); },
|
|
mouseDragLeave: function(e, node, next) { highlightSeats(node, 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
|
|
allowDrop: true // from myDiagram
|
|
});
|
|
|
|
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;
|
|
}
|
|
if (coll.any(isTable)) {
|
|
// if dragging a Table, don't do any highlighting
|
|
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);
|
|
|
|
/** @override */
|
|
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.contains(n)) map.add(n, { point: 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);
|
|
|
|
/** @override */
|
|
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>
|