UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

1,539 lines (1,423 loc) 116 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="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