create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
1,539 lines (1,423 loc) • 116 kB
HTML
<!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 editor for defining planograms: visual displays of merchandise." />
<meta itemprop="description" content="An editor for defining planograms: visual displays of merchandise." />
<meta property="og:description" content="An editor for defining planograms: visual displays of merchandise." />
<meta name="twitter:description" content="An editor for defining planograms: visual displays of merchandise." />
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
<meta itemprop="name" content="Planogram Editor: Drag and Drop sodas onto Vending Machine" />
<meta property="og:title" content="Planogram Editor: Drag and Drop sodas onto Vending Machine" />
<meta name="twitter:title" content="Planogram Editor: Drag and Drop sodas onto Vending Machine" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/vendingPlanogram.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/vendingPlanogram.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/vendingPlanogram.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/vendingPlanogram.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/vendingPlanogram.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Planogram Editor: Drag and Drop sodas onto Vending Machine | GoJS Diagramming Library
</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
<div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
<a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
href="../">
GoJS
</a>
<div class="relative">
<button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
<svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="topnavList" class="hidden md:block">
<div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
<a href="../learn/">Learn</a>
<a href="../samples/">Samples</a>
<a href="../intro/">Intro</a>
<a href="../api/">API</a>
<a href="../download.html">Download</a>
<a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
<a id="tc" href="https://nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
<a id="tb" href="https://nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
</div>
</div>
</div>
</div>
</nav>
<script>
window.addEventListener("DOMContentLoaded", function () {
// topnav
var topButton = document.getElementById("topnavButton");
var topnavList = document.getElementById("topnavList");
if (topButton && topnavList) {
topButton.addEventListener("click", function (e) {
topnavList
.classList
.toggle("hidden");
e.stopPropagation();
});
document.addEventListener("click", function (e) {
// if the clicked element isn't the list, close the list
if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
topButton.click();
}
});
// set active <a> element
var url = window
.location
.href
.toLowerCase();
var aTags = topnavList.getElementsByTagName('a');
for (var i = 0; i < aTags.length; i++) {
var lowerhref = aTags[i]
.href
.toLowerCase();
if (lowerhref.endsWith('.html'))
lowerhref = lowerhref.slice(0, -5);
if (url.startsWith(lowerhref)) {
aTags[i]
.classList
.add('active');
break;
}
}
}
});
</script>
<div class="flex flex-col prose">
<div class="w-full max-w-screen-xl mx-auto">
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>
<div id="allSampleContent" class="p-4 w-full">
<style>
:root {
--light: rgba(235, 243, 235, 1);
--dark: rgba(89, 99, 89, 1);
--medium: rgba(197, 206, 197, 1);
--stroke: #2f3c2f;
}
.html-info {
display: none; /* hide by default */
}
#height-input-div {
padding: 6px;
border-radius: 6px;
font: 600 15px sans-serif;
outline: none;
background: var(--light);
border: 2px solid var(--dark);
color: var(--dark);
}
#height-input-div input {
outline: none; /* no outline when focused */
border: none;
background: var(--light);
width: 40px;
}
#height-input-div .arrows {
display: flex;
flex-direction: column;
gap: 3px;
}
#height-input-div .arrow {
width: 16px;
}
.arrow {
right: 8px;
height: 16px;
cursor: pointer;
display: flex;
justify-content: center; /* center text content (the arrow) horizontally */
align-items: center; /* center text content (the arrow) vertically */
background-color: var(--medium);
border-radius: 3px;
font-size: 12px;
color: var(--dark);
user-select: none; /* no highlight or anything when selected */
}
#add-row-input-div {
flex-direction: column;
gap: 5px;
padding: 6px;
border-radius: 6px;
font: 600 15px sans-serif;
outline: none;
background: var(--light);
border: 2px solid var(--dark);
color: var(--dark);
user-select: none; /* no highlight or anything when selected */
}
#add-row-input-div div {
display: flex;
gap: 4px;
width: 130px;
height: 18px;
}
#add-row-input-div input {
width: 20px;
font-size: 10px;
border: none;
border-radius: 3px;
color: var(--dark);
padding: 4px;
background-color: var(--medium);
}
#add-row-input-div p {
width: 40px;
font-size: 10px;
color: var(--dark);
}
#fill-shelf-button {
border: 2px solid var(--stroke);
border-radius: 3px;
background-color: var(--light);
color: var(--stroke);
}
input::selection {
background-color: #6b7b6b;
color: white;
}
/* This CSS is used to create the accordion for the Palettes */
input[type="radio"] {
position: absolute;
opacity: 0;
z-index: -1;
}
input[type="radio"]:checked + .tab-label {
background: #1a252f;
font-size: 25px;
}
input[type="radio"]:checked + .tab-label::after {
transform: rotate(90deg);
}
input[type="radio"]:checked ~ .tab-content {
max-height: 100vh;
}
/* Accordion styles */
.tabs {
overflow: hidden;
}
.tab {
width: 100%;
color: white;
overflow: hidden;
}
.tab-label {
font-family: sans-serif;
display: flex;
justify-content: space-between;
padding: 0.5em;
background: #1f4963;
cursor: pointer;
}
.tab-label:hover {
background: #627f91;
}
.tab-label::after {
content: "❯";
width: 1em;
height: 1em;
text-align: center;
transition: all 0.35s;
}
.tab-content {
max-height: 0;
color: #2c3e50;
background: white;
}
.tab-close {
display: flex;
justify-content: flex-end;
padding: 1em;
font-size: 0.75em;
background: #2c3e50;
cursor: pointer;
}
.tab-close:hover {
background: #1a252f;
}
.checkbox-wrapper input[type="checkbox"] {
height: 0;
width: 0;
visibility: hidden;
}
.checkbox-wrapper label {
cursor: pointer;
width: 130px;
height: 25px;
background: grey;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100px;
position: relative;
color: white;
font-size: 12px;
}
.checkbox-wrapper label:after {
content: "";
position: absolute;
top: 2.5px;
left: 2.5px;
width: 25px;
height: 20px;
background: #fff;
border-radius: 90px;
transition: 0.3s;
}
.checkbox-wrapper input:checked + label {
background: #7e9c8f;
}
.checkbox-wrapper input:checked + label:after {
left: calc(100% - 2.5%);
transform: translateX(-100%);
}
.checkbox-wrapper .slider-text {
font-family: sans-serif;
text-decoration: line-through;
display: inline-block; /* so transform works */
transform: translateX(10px);
transition: transform 0.3s, text-decoration 0.3s;
}
/* when checked: remove strikethrough and shift left */
.checkbox-wrapper input:checked + label .slider-text {
text-decoration: none;
transform: translateX(-10px);
}
</style>
<script id="code">
/***********************************************************************
* GLOBAL VARIABLES *
***********************************************************************/
const settings = {
colors: {
exterior: "#6B7B6B",
dark: "#4A594A",
interior: "#E6F0E6",
stroke: "#2F3C2F",
keypad: "#B8BCA5",
highlight: "#FDFDFD",
shadow: "#4A594A",
palettes: {
green: {
fillColor: "#8FA99E",
labelColor: "#C3D4CA",
stroke: "#4B6257",
},
blue: {
fillColor: "#A3BDD9",
labelColor: "#DCE8F7",
stroke: "#607B9E",
},
beige: {
fillColor: "#E8DCC1",
labelColor: "#F5F0E3",
stroke: "#8C7E56",
},
},
},
paletteXSpacing: 45,
paletteYSpacing: 85,
defaultShelfHeight: 70,
// if allowTopLevel is false, that means you can't drag sodas onto the diagram background
allowTopLevel: false,
// settings the user can change:
editMode: true,
allowDuplicates: true,
// r is rounding on some things
r: 10,
};
const sodaCategories = ["can", "bottle"];
// will be defined in init()
let myDiagram;
let palette;
let vendingMachinePalette;
// when you right click a soda (fill shelf button pops up) it stores the data
// fillShelf() uses the data - to know:
// 1. what type of soda to fill the shelf with
// 2. what group & shelf to fill
let sodaRightClickedData;
/***********************************************************************
* CUSTOM INPUT EDITOR *
***********************************************************************/
// Create an HTMLInfo and dynamically create some HTML to show/hide
const customEditor = new go.HTMLInfo();
// the onclick functions on the arrows for the height input use this
function changeHeightInput(change) {
inputDiv = document.getElementById("height-input-div");
inputBox = inputDiv.querySelector("input");
inputBox.value = Math.max(
parseInt(inputBox.value) + change,
settings.defaultShelfHeight - 20
);
}
function placeInput(div, pos, divWidth, divHeight) {
div.style.left = `${pos.x - divWidth / 2}px`;
div.style.top = `${pos.y - divHeight / 2}px`;
}
function handleAddShelfInput(textBlock, diagram, tool, pos, inputDiv) {
const rowInput = document.getElementById("row-input");
const heightInput = document.getElementById("height-input");
function addRow(itemArray, shelfNum, groupKey) {
let numRows = parseInt(rowInput.value);
let height = parseInt(heightInput.value);
if (height < settings.defaultShelfHeight - 20) {
height = settings.defaultShelfHeight - 20;
}
if (isNaN(height)) height = settings.defaultShelfHeight;
if (isNaN(numRows)) numRows = 1;
myDiagram.startTransaction("Add row(s)");
for (let i = 0; i < numRows; i++) {
// insert a new shelf at the specified index
myDiagram.model.insertArrayItem(itemArray, shelfNum, {
height: height,
});
}
for (const soda of allSodas()) {
if (soda.group === groupKey && soda.shelf >= shelfNum) {
myDiagram.model.setDataProperty(
soda,
"shelf",
soda.shelf + numRows
);
}
}
// we need this to update locations before transaction is finished
myDiagram.findNodeForKey(groupKey).ensureBounds();
updateInvisibleCells();
updateHighlights();
myDiagram.updateAllTargetBindings();
myDiagram.commitTransaction("Add row(s)");
}
rowInput.value = "1";
heightInput.value = "70";
const shelfData = getDataFromGraphObject(textBlock);
const groupData = findGroupData(shelfData);
const shelfNum = groupData.itemArray.indexOf(shelfData);
const groupKey = groupData.key;
// add row above
inputDiv.querySelector(".arrow.up").onclick = () => {
addRow(groupData.itemArray, shelfNum, groupKey);
customEditor.hide(diagram, tool);
};
// add row below
inputDiv.querySelector(".arrow.down").onclick = () => {
addRow(groupData.itemArray, shelfNum + 1, groupKey);
customEditor.hide(diagram, tool);
};
placeInput(inputDiv, pos, 142, 72);
}
function handleChangeShelfHeightInput(
textBlock,
diagram,
tool,
pos,
inputDiv,
inputBox
) {
inputBox.value = textBlock.text;
// Do a few different things when a user presses a key
inputBox.addEventListener(
"keydown",
(e) => {
if (e.isComposing) return;
const key = e.key;
if (key === "Enter") {
// Accept on Enter
customEditor.hide(diagram, tool);
return;
} else if (key === "Tab") {
// Accept on Tab
customEditor.hide(diagram, tool);
e.preventDefault();
return false;
} else if (key === "Escape") {
// Cancel on Esc
tool.doCancel();
}
},
false
);
placeInput(inputDiv, pos, 40, 30);
}
function handleFillShelfButton(pos, button) {
const mousePoint = myDiagram.lastInput.viewPoint;
placeInput(button, mousePoint, 0, 0);
}
customEditor.show = (textBlock, diagram, tool) => {
customEditor._textBlockName = textBlock.name; // for use later in valueFunction
const loc = textBlock.getDocumentPoint(go.Spot.TopLeft);
const pos = diagram.transformDocToView(loc);
let div;
let inputBox;
if (textBlock.name === "ADD_SHELF_PLACEHOLDER_TEXT") {
div = document.getElementById("add-row-input-div");
handleAddShelfInput(textBlock, diagram, tool, pos, div);
} else if (textBlock.name === "FILL_SHELF_PLACEHOLDER_TEXT") {
div = document.getElementById("fill-shelf-button");
handleFillShelfButton(pos, div);
} else {
div = document.getElementById("height-input-div");
inputBox = div.querySelector("input");
handleChangeShelfHeightInput(
textBlock,
diagram,
tool,
pos,
div,
inputBox
);
}
div.style.position = "absolute";
div.style.zIndex = 100; // place it in front of the Diagram
diagram.div.appendChild(div);
div.style.display = "flex"; // show the input div
if (inputBox) {
inputBox.focus();
} else {
div.focus();
}
};
customEditor.hide = (diagram, tool) => {
let div;
if (tool.textBlock.name === "ADD_SHELF_PLACEHOLDER_TEXT") {
div = document.getElementById("add-row-input-div");
} else if (tool.textBlock.name === "FILL_SHELF_PLACEHOLDER_TEXT") {
div = document.getElementById("fill-shelf-button");
} else {
div = document.getElementById("height-input-div");
const inputBox = div.querySelector("input");
diagram.startTransaction("change shelf height");
tool.textBlock.text = inputBox.value;
diagram.layoutDiagram(true); // force layout to redo
updateInvisibleCells();
updateHighlights();
diagram.updateAllTargetBindings();
diagram.commitTransaction("change shelf height");
}
if (diagram.div.contains(div)) {
div.style.display = "none"; // hide the input div
}
};
// This is necessary for HTMLInfo instances that are used as text editors
customEditor.valueFunction = () => {
if (customEditor._textBlockName !== "ADD_SHELF_PLACEHOLDER_TEXT" &&
customEditor._textBlockName !== "FILL_SHELF_PLACEHOLDER_TEXT") {
return document
.getElementById("height-input-div")
.querySelector("input").value;
} else {
return "";
}
};
/***********************************************************************
* CUSTOM SHAPE (SODA) *
***********************************************************************/
go.Shape.defineFigureGenerator("Soda", (shape, w, h) => {
const geo = new go.Geometry();
const mid = w / 2;
const capWidth = w / 8;
const capX = [mid - capWidth, mid + capWidth];
const capY = h / 10;
const bottomStartY = h / 4;
// corner radius
const r =
shape && shape.parameter1 ? Math.min(shape.parameter1, 18) : 8;
// start x, start y, filled
// top left corner of cap
const fig = new go.PathFigure(capX[0], 0, true);
geo.add(fig);
// top right corner of cap
fig.add(new go.PathSegment(go.SegmentType.Line, capX[1], 0));
// point between neck and cap (right)
fig.add(new go.PathSegment(go.SegmentType.Line, capX[1], capY));
// right side, soda bottle neck
fig.add(
new go.PathSegment(
go.SegmentType.QuadraticBezier,
w,
bottomStartY + r,
w,
bottomStartY - r / 4
)
);
// bottom right corner
fig.add(
new go.PathSegment(
go.SegmentType.Arc,
0,
90, // start angle and sweep angle (from center point)
w - r,
h - r, // center x and center y
r,
r // radius x and radius y
)
);
// bottom left corner
fig.add(
new go.PathSegment(
go.SegmentType.Arc,
90,
90, // start angle and sweep angle (from center point)
r,
h - r, // center x and center y
r,
r // radius x and radius y
)
);
// left side, soda bottle neck
fig.add(new go.PathSegment(go.SegmentType.Line, 0, bottomStartY + r));
fig.add(
new go.PathSegment(
go.SegmentType.QuadraticBezier,
capX[0],
capY,
0,
bottomStartY - r / 4
).close()
);
const capLineFig = new go.PathFigure(capX[0], capY, false);
capLineFig.add(new go.PathSegment(go.SegmentType.Line, capX[1], capY));
geo.add(capLineFig);
return geo;
});
/***********************************************************************
* HANDLE EVENTS *
***********************************************************************/
// handles item being dropped onto the vending machine
function handleVendingMouseDrop(e, grp) {
let cancelled = false;
grp.diagram.selection.each((node) => {
if (cancelled) return;
if (node instanceof go.Group) {
animateDrop(new go.Set().add(node));
return;
}
const closestCell = getClosestCell(node);
if (closestCell === null) {
grp.diagram.currentTool.doCancel();
cancelled = true;
return;
}
myDiagram.startTransaction("drop object");
// reset invisible cell back to being invisible
closestCell.findObject("SHAPE").fill = "transparent";
// set shelf location
myDiagram.model.setDataProperty(
node.data,
"shelf",
closestCell.data.shelf
);
myDiagram.model.setDataProperty(
node.data,
"coil",
closestCell.data.coil
);
// update visual
myDiagram.updateAllTargetBindings();
var ok = grp.addMembers([node], true);
if (!ok) {
grp.diagram.currentTool.doCancel();
return;
}
if (node.data.isFromPalette) {
handleItemFromPalette(node.data);
}
myDiagram.commitTransaction("drop object");
});
}
/***********************************************************************
* FUNCTIONS FOR MAKING THE TEMPLATES *
***********************************************************************/
// =================== REUSABLE FUNCTIONS ===================
function buttonStyle(obj) {
return obj
.set({
width: 12,
height: 12,
})
.attach({ "ButtonBorder.strokeWidth": 0 });
}
// use to make a button
function button(symbol, click, margin) {
const btn = go.GraphObject.build("Button", {
click: click,
})
.apply(buttonStyle)
.add(
new go.Shape(symbol, {
strokeWidth: 2,
stroke: settings.colors.stroke,
})
);
if (margin) {
btn.margin = margin;
}
return btn;
}
function getStrokeColor(color) {
return settings.colors.palettes[color].stroke;
}
function sodaStyle(template) {
template
.findObject("MAIN_SHAPE")
.set({
width: 35,
fill: settings.colors.dark,
strokeWidth: 2,
stroke: settings.colors.stroke,
})
.bind(
"fill",
"color",
(color) => settings.colors.palettes[color].fillColor
)
.bind("stroke", "color", getStrokeColor);
template
.findObject("LABEL_SHAPE")
.set({
fill: settings.colors.exterior,
strokeWidth: 2,
stroke: settings.colors.stroke,
})
.bind(
"fill",
"color",
(color) => settings.colors.palettes[color].labelColor
)
.bind("stroke", "color", getStrokeColor);
template
.findObject("LABEL_TEXT")
.set({
stroke: settings.colors.stroke,
})
.bind("stroke", "color", getStrokeColor);
template.findObject("HIGHLIGHT").set({
name: "HIGHLIGHT",
width: 15,
strokeWidth: 0,
alignment: go.Spot.Left,
opacity: 0.4,
fill: "white",
});
template
.findObject("DUPLICATE_OUTLINE")
.set({
fill: null,
stroke: "red",
strokeWidth: 2,
width: 35,
visible: false,
})
.bind("visible", "", (data) => {
return (
(data.duplicate && !settings.allowDuplicates) ||
data.toDelete === true
);
})
.bind("stroke", "toDelete", (toDelete) => {
return toDelete ? "blue" : "red";
});
return template
.add(new go.TextBlock({ name: "FILL_SHELF_PLACEHOLDER_TEXT" }))
.set({
// handle right click
contextClick: (e, obj) => {
const tb = obj.findObject("FILL_SHELF_PLACEHOLDER_TEXT");
e.diagram.commandHandler.editTextBlock(tb);
sodaRightClickedData = obj.data;
},
locationSpot: go.Spot.Center,
zOrder: 3,
// it must be in the foreground or you can't click on sodas because of the invisible cells being in front
mouseDrop: (e, node) => {
handleVendingMouseDrop(e, node.containingGroup);
},
})
.bindObject("zOrder", "", (obj) => {
if (obj.isSelected) {
return highestZOrder() * 4 + 4;
}
const data = obj.data;
const groupData = myDiagram.model.findNodeDataForKey(data.group);
if (!groupData) return 0;
const zOrder = groupData.zOrder;
return zOrder * 4 + 2;
/* each group has a block of 4 z-orders for nodes related to the group
* +0: vending machine
* +1: invisible cells
* +2: soda
* +3: highlight
*/
})
.bind("location", "", (data) => {
let node = myDiagram.findNodeForKey(data.key);
if (!node) node = palette.findNodeForKey(data.key);
const height = node.actualBounds.height;
if (data.paletteLocation) {
const { x, y } = go.Point.parse(data.paletteLocation);
return new go.Point(x, y - height / 2);
}
const { group, shelf, coil } = data;
if (
group === undefined ||
shelf === undefined ||
coil === undefined
) {
// default
const soda = myDiagram.findNodeForKey(data.key);
// it will return 0,0 for items in palette i hope that's fine
if (soda) {
return soda.location;
} else {
return new go.Point(0, 0);
}
}
const ivc = myDiagram.findNodeForKey(
"IVC " + group + " " + shelf + " " + coil
);
if (ivc) {
const { x, y } = ivc.location;
return new go.Point(x, y - height / 2);
} else {
return new go.Point(0, 0);
}
});
}
function makeCornerDecoration(args) {
// set defaults (what they pass in will override what I'm writing here)
args = {
alignment: "TopLeft",
margin: [0],
strokeWidth: 5,
r: 5,
width: 10,
height: 10,
horizontalDotSpacing: 10,
horizontalDotWidth: 0,
verticalDotSpacing: 10,
verticalDotWidth: 0,
stroke: settings.colors.highlight,
opacity: 0.7,
...args,
};
const r = args.r;
const isRight = args.alignment.includes("Right") ? 1 : -1;
const isTop = args.alignment.includes("Top") ? 1 : -1;
let geom = "";
if (args.horizontalDotWidth > 0) {
geom = `M${args.horizontalDotSpacing + args.horizontalDotWidth} 0 l${
isRight * args.horizontalDotWidth
} 0 m${isRight * args.horizontalDotSpacing} 0`;
} else {
geom = "M0 0";
}
const sweepFlag = isRight === isTop ? 1 : 0;
const arcEndX = isRight * r;
const arcEndY = isTop * r;
geom += `l${
isRight * args.width
} 0 a${r} ${r} 0 0 ${sweepFlag} ${arcEndX} ${arcEndY} l0 ${
isTop * args.height
}`;
if (args.verticalDotWidth > 0) {
geom += `m0 ${isTop * args.verticalDotSpacing} l0 ${
isTop * args.verticalDotWidth
}`;
}
return new go.Shape({
alignment: go.Spot[args.alignment],
geometryString: geom,
margin: new go.Margin(...args.margin),
strokeWidth: args.strokeWidth,
strokeCap: "round",
stroke: args.stroke,
opacity: args.opacity,
});
}
// =================== ONE TIME USE ===================
const modelTemplate = new go.GraphLinksModel([
{
key: 1,
vendingMachineWidth: 4,
isGroup: true,
itemArray: [{}, { height: 100 }, {}, {}, {}],
zOrder: 1,
},
{
key: 3,
category: "bottle",
shelf: 1,
coil: 1,
group: 1,
color: "green",
duplicate: true,
},
{
key: 4,
category: "can",
shelf: 0,
coil: 3,
group: 1,
color: "green",
duplicate: true,
},
{
key: 5,
category: "bottle",
shelf: 1,
coil: 3,
group: 1,
color: "green",
duplicate: true,
},
{
key: 6,
category: "can",
color: "beige",
shelf: 3,
coil: 2,
group: 1,
duplicate: true,
},
{
key: 2,
isGroup: true,
vendingMachineWidth: 3,
itemArray: [{}, { height: 70 }, {}],
position: "320 0",
zOrder: 2,
},
{
key: 7,
category: "can",
color: "blue",
shelf: 0,
coil: 0,
group: 2,
duplicate: true,
},
{
key: 8,
category: "can",
color: "blue",
shelf: 0,
coil: 2,
group: 2,
duplicate: true,
},
{
key: 9,
category: "can",
color: "blue",
shelf: 0,
coil: 1,
group: 2,
duplicate: true,
},
{
key: 10,
category: "can",
color: "beige",
shelf: 1,
coil: 0,
group: 2,
duplicate: true,
},
{
key: 11,
category: "can",
color: "beige",
shelf: 1,
coil: 1,
group: 2,
duplicate: true,
},
{
key: 12,
category: "can",
color: "beige",
shelf: 1,
coil: 2,
group: 2,
duplicate: true,
},
{
key: 13,
category: "can",
color: "green",
shelf: 2,
coil: 0,
group: 2,
duplicate: true,
},
{
key: 14,
category: "can",
color: "green",
shelf: 2,
coil: 2,
group: 2,
duplicate: true,
},
{
key: 15,
category: "can",
color: "green",
shelf: 2,
coil: 1,
group: 2,
duplicate: true,
},
]);
// the button on that shelves that displays their heights
// when you click it the HTML pops up to change their heights
function numberInput() {
return go.GraphObject.build("Button", {
height: 12,
margin: new go.Margin(0, 0, 0, 30),
"ButtonBorder.strokeWidth": 0,
click: (e, obj) => {
const tb = obj.findObject("TEXT");
e.diagram.commandHandler.editTextBlock(tb);
},
}).add(
new go.TextBlock({
name: "TEXT",
text: settings.defaultShelfHeight,
font: "8px sans-serif",
strokeWidth: 2,
stroke: settings.colors.stroke,
}).bindTwoWay("text", "height", undefined, (t) =>
Math.max(parseInt(t), settings.defaultShelfHeight - 20)
)
);
}
function addShelfButton() {
return new go.Panel("Auto", {
click: (e, obj) => {
const tb = obj.findObject("ADD_SHELF_PLACEHOLDER_TEXT");
e.diagram.commandHandler.editTextBlock(tb);
},
}).add(
go.GraphObject.build("Button", {
margin: new go.Margin(0, 3, 0, 0),
})
.apply(buttonStyle)
.add(
new go.Shape("PlusLine", {
strokeWidth: 2,
stroke: settings.colors.stroke,
})
),
new go.TextBlock({
name: "ADD_SHELF_PLACEHOLDER_TEXT",
})
);
}
function removeShelfButton() {
return button("MinusLine", (e, btn) => {
const { itemArray, groupKey, shelfNum } = getButtonClickedInfo(btn);
const affectedSodas = [];
for (const soda of allSodas()) {
if (soda.group === groupKey && soda.shelf >= shelfNum) {
affectedSodas.push(soda);
}
}
myDiagram.startTransaction("show modal");
let sodaDeleteCount = 0;
for (const soda of affectedSodas) {
if (soda.shelf === shelfNum) {
sodaDeleteCount++;
myDiagram.model.setDataProperty(soda, "toDelete", true);
}
}
if (sodaDeleteCount > 0) {
showModal(
sodaDeleteCount,
() => {
removeShelf(affectedSodas, shelfNum, itemArray, groupKey);
},
btn
);
myDiagram.commitTransaction("show modal");
} else {
myDiagram.commitTransaction("show modal");
removeShelf(affectedSodas, shelfNum, itemArray, groupKey);
}
}).bind("visible", "", (data) => {
nodeData = findGroupData(data);
return nodeData.itemArray.length > 1;
});
}
// number buttons are just visual decoration on the controls btw
// they are not clickable
function makeNumberButtons() {
const r = 2;
const panel = new go.Panel("Table", {
width: 46,
height: 67,
});
for (let i = 0; i < 12; i++) {
panel.add(
new go.Panel("Auto", {
row: Math.floor(i / 3),
column: i % 3,
}).add(
// invisible rectangle to make it the right size
new go.Shape("RoundedRectangle", {
fill: null,
stroke: null,
width: 14,
height: 14,
}),
new go.TextBlock({
text: i < 9 ? i + 1 : ["*", "0", "#"][i - 9],
// the 900 makes text thicker
font: "900 8px sans-serif",
stroke: settings.colors.stroke,
}),
// shadow for aesthetics
makeCornerDecoration({
height: 7 - r,
width: 7 - r,
alignment: "BottomRight",
stroke: settings.colors.dark,
strokeWidth: 1.5,
opacity: 0.8,
r: r,
}),
makeCornerDecoration({
alignment: "TopLeft",
strokeWidth: 1.5,
height: 2,
width: 2,
r: r,
})
)
);
}
return panel;
}
function leftAddAndDeleteButtons() {
return new go.Panel("Vertical", {
// extra space between buttons and window
margin: new go.Margin(0, 3, 0, 0),
}).add(
button("PlusLine", (e, btn) => {
myDiagram.startTransaction("add column");
// add 1 to vendingMachineWidth
const data = getDataFromGraphObject(btn);
myDiagram.model.setDataProperty(
data,
"vendingMachineWidth",
data.vendingMachineWidth + 1
);
myDiagram.layoutDiagram(true); // force layout to redo
// update invisibleCells' positions
updateInvisibleCells();
myDiagram.commitTransaction("add column");
}).bind("visible", "", () => settings.editMode),
button(
"MinusLine",
(e, btn) => {
// subtract 1 from vendingMachineWidth
const data = getDataFromGraphObject(btn);
const newVendingMachineWidth = data.vendingMachineWidth - 1;
const affectedSodas = allSodas().filter(
(soda) =>
soda.group === data.key && soda.coil >= newVendingMachineWidth
);
myDiagram.startTransaction("show modal");
for (const soda of affectedSodas) {
myDiagram.model.setDataProperty(soda, "toDelete", true);
}
if (affectedSodas.length > 0) {
showModal(
affectedSodas.length,
() => {
removeColumn(affectedSodas, data, newVendingMachineWidth);
},
btn
);
myDiagram.commitTransaction("show modal");
} else {
myDiagram.commitTransaction("show modal");
removeColumn(affectedSodas, data, newVendingMachineWidth);
}
},
// space between add and delete button
new go.Margin(3, 0, 0, 0)
).bind(
"visible",
"vendingMachineWidth",
(count) => count > 3 && settings.editMode
)
);
}
function spacerForWhenButtonsArentThere() {
return new go.Shape({
width: 11,
opacity: 0,
}).bind("visible", "", () => !settings.editMode);
}
function shelfTop() {
return new go.Panel("Horizontal", {
height: settings.defaultShelfHeight - 20,
itemTemplate: new go.Panel("Horizontal", {
height: 37,
alignment: go.Spot.Bottom,
}).add(
new go.Shape({
fill: "transparent",
name: "COIL",
// somewhat vertically stretched half circle
geometryString: "M0 0 a20 20 0 0 1 40 0",
height: 12,
margin: new go.Margin(0, 5),
width: 25,
strokeWidth: 2,
stroke: settings.colors.stroke,
alignment: go.Spot.Bottom,
}),
new go.Shape("MinusLine", {
// height means width because the angle being 90 flips it
height: 2,
margin: new go.Margin(0, 1.5),
angle: 90,
strokeWidth: 2,
stroke: settings.colors.stroke,
}).bind("visible", "lineVisible")
),
itemArray: [{}, {}],
})
.bind("margin", "height", (h) => new go.Margin(h - 70, 0, 0, 0))
.bind("itemArray", "", (data) => {
// find group
const nodeData = findGroupData(data);
if (nodeData) {
array = Array(nodeData.vendingMachineWidth)
.fill(null)
.map(() => ({ lineVisible: true }));
array[nodeData.vendingMachineWidth - 1] = {
lineVisible: false,
};
return array;
} else {
return [];
}
});
}
// I made this way more complicated than it should have to be because it wasn't arranged right otherwise
function shelfLabels() {
return new go.Panel("Horizontal", {
alignment: go.Spot.Left,
itemTemplate: new go.Panel("Horizontal", {}).add(
new go.Panel("Auto", {}).add(
// invisible shape to force size
new go.Shape("Rectangle", {
width: 36,
opacity: 0,
}),
// label
new go.Panel("Auto").add(
new go.Shape("RoundedRectangle", {
height: 10,
width: 20,
fill: "white",
stroke: null,
}),
// labels (A1, A2, etc)
new go.TextBlock({
// the 900 makes text thicker
font: "900 8px sans-serif",
stroke: settings.colors.exterior,
}).bind("text", "", (data) => {
return data.letter + "" + data.i;
})
)
),
new go.Shape("Rectangle", {
// height means width because the angle being 90 flips it
width: 6,
opacity: 0,
}).bind("visible", "lineVisible")
),
itemArray: [{}, {}],
})
.bind("itemArray", "", (data) => {
// find group
const nodeData = findGroupData(data);
if (nodeData) {
const row = nodeData.itemArray.indexOf(data);
const letter = String.fromCharCode(65 + row);
array = Array(nodeData.vendingMachineWidth)
.fill(null)
.map((_, i) => ({
i: i + 1,
lineVisible: true,
letter: letter,
}));
array[nodeData.vendingMachineWidth - 1].lineVisible = false;
return array;
} else {
return [];
}
})
.bind("visible", "", () => !settings.editMode);
}
function shelfBottom() {
return new go.Panel("Auto", {
stretch: go.Stretch.Horizontal,
height: 20,
}).add(
new go.Shape("Rectangle", {
fill: settings.colors.exterior,
stroke: settings.colors.stroke,
strokeWidth: 2,
alignment: go.Spot.Center,
height: 18,
}),
// shelf + and - buttons
new go.Panel("Horizontal")
.add(addShelfButton(), removeShelfButton(), numberInput())
.bind("visible", "", () => settings.editMode),
shelfLabels()
);
}
function interior() {
return new go.Panel("Horizontal", {
stretch: go.Stretch.Horizontal,
column: 0,
margin: new go.Margin(20, 5),
}).add(
leftAddAndDeleteButtons(),
spacerForWhenButtonsArentThere(),
// WINDOW
new go.Panel("Auto", {
name: "HIGHLIGHT GOES HERE",
}).add(
// window background
new go.Shape("RoundedRectangle", {
name: "WINDOW",
fill: settings.colors.interior,
stroke: null,
strokeWidth: 2,
}),
// shelves
new go.Panel("Vertical", {
name: "SHELVESLIST",
itemTemplate: new go.Panel("Vertical", {
margin: new go.Margin(5, 0),
opacity: 0.8,
})
.bind("height", "height")
.add(shelfTop(), shelfBottom()),
}).bind("itemArray")
)
);
}
function controls() {
const r2 = settings.r - 5;
return new go.Panel("Auto", {
column: 1,
width: 70,
height: 180,
margin: new go.Margin(0, 10),
}).add(
// dark background
new go.Shape("RoundedRectangle", {
stroke: settings.colors.stroke,
strokeWidth: 2,
fill: settings.colors.dark,
}),
new go.Panel("Vertical", {}).add(
// screen
new go.Panel("Auto").add(
new go.Shape("RoundedRectangle", {
stroke: settings.colors.stroke,
strokeWidth: 2,
fill: settings.colors.keypad,
height: 45,
width: 50,
margin: new go.Margin(2),
}),
// highlight for aesthetics
makeCornerDecoration({
alignment: "TopLeft",
margin: [3],
r: r2,
})
),
// arrow
new go.Shape("TriangleDown", {
stroke: null,
strokeWidth: 2,
fill: "#e9cf86",
width: 15,
height: 5,
alignment: go.Spot.Center,
margin: new go.Margin(2),
}),
// card slot
new go.Panel("Auto", {
margin: new go.Margin(2),
height: 10,
width: 50,
}).add(
new go.Shape("RoundedRectangle", {
stroke: settings.colors.stroke,
strokeWidth: 2,
fill: settings.colors.keypad,
}),
new go.Shape("MinusLine", {
width: 30,
stroke: settings.colors.dark,
strokeWidth: 2,
fill: settings.colors.keypad,
})
),
// keypad
new go.Panel("Auto").add(
new go.Shape("RoundedRectangle", {
stroke: settings.colors.stroke,
strokeWidth: 2,
fill: settings.colors.keypad,
height: 70,
width: 50,
margin: new go.Margin(2),
}),
makeNumberButtons()
)
)
);
}
function vendingFeet() {
return new go.Panel("Table", {
stretch: go.Stretch.Horizontal,
height: 20,
}).add(
new go.Shape("RoundedBottomRectangle", {
column: 0,
width: 30,
height: 18,
stroke: settings.colors.stroke,
strokeWidth: 2,
fill: settings.colors.exterior,
alignment: go.Spot.Left,
}),
new go.Shape("RoundedBottomRectangl