//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Add a stack to give users unlimited undo/redo options.
import KeyLines from "keylines";
import dataGenerator from "./undoredo-data.js";

let chart;
const undoStack = [];
const redoStack = [];

const addButton = document.getElementById("add-node");
const removeButton = document.getElementById("remove-node");
const undoButton = document.getElementById("undo");
const redoButton = document.getElementById("redo");
const layoutButton = document.getElementById("layout");

function disableButtons(isDisabled) {
  addButton.disabled = isDisabled;
  removeButton.disabled = isDisabled;
  undoButton.disabled = isDisabled;
  redoButton.disabled = isDisabled;
  layoutButton.disabled = isDisabled;
}

function refreshUI() {
  disableButtons(false);

  undoButton.value = `Undo (${undoStack.length})`;
  undoButton.disabled = undoStack.length === 0;

  redoButton.value = `Redo (${redoStack.length})`;
  redoButton.disabled = redoStack.length === 0;

  // disables "remove button" if no more items to remove
  removeButton.disabled = chart.serialize().items.length === 0;
}

async function undoStackAdd(callbackFunction) {
  // save the current chart state into the undo stack
  undoStack.push(chart.serialize());
  // clear the redo stack
  redoStack.length = 0;

  if (callbackFunction) {
    await callbackFunction();
  }
  refreshUI();
}

async function expandNewItems() {
  disableButtons(true);
  // If a node is selected then adds new nodes to that node, otherwise adds to a random node
  const selectedIds = chart.selection();
  const selectedId =
    selectedIds.length > 0 && chart.getItem(selectedIds[0]).type === "node"
      ? selectedIds[0]
      : undefined;

  await chart.expand(dataGenerator.newNodeAndLink(selectedId), {
    time: 500,
    layout: { name: "organic", fix: "all" },
  });
  addButton.disabled = false;
  disableButtons(false);
}

function removeNode() {
  // If a node is selected then removed that node, otherwise remove random node
  const selectedIds = chart.selection();
  const selectedId = selectedIds[0] || dataGenerator.getRemoveNodeId(chart);
  chart.removeItem(selectedId);
}

function initialiseInteractions() {
  chart.load(dataGenerator.makeChart());
  chart.layout("organic");

  undoButton.addEventListener("click", () => {
    redoStack.push(chart.serialize());
    chart.load(undoStack.pop());
    refreshUI();
  });

  redoButton.addEventListener("click", () => {
    undoStack.push(chart.serialize());
    chart.load(redoStack.pop());
    refreshUI();
  });

  chart.on("prechange", ({ type }) => {
    if (/(move|offset)/.test(type)) {
      // Before moving a link / node, we save the chart state in the undo stack
      undoStackAdd();
    }
  });

  layoutButton.addEventListener("click", async () => {
    // Disable buttons to stop button used while layout happening
    disableButtons(true);
    await undoStackAdd(() => chart.layout("organic"));
    refreshUI();
  });

  addButton.addEventListener("click", () => {
    undoStackAdd(expandNewItems);
  });

  removeButton.addEventListener("click", () => {
    undoStackAdd(removeNode);
  });
}

async function loadKeyLines() {
  const options = {
    logo: { u: "/images/Logo.png" },
    handMode: true,
  };
  chart = await KeyLines.create({ container: "klchart", options });

  initialiseInteractions();
}

window.addEventListener("DOMContentLoaded", loadKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Undo/Redo</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/undoredo.js" crossorigin="use-credentials" type="module"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols">
      <div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
        <div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
          <div class="klchart" id="klchart">
          </div>
        </div>
      </div>
      <div class="rhs citext closed" id="demorhscontent">
        <div class="title-bar">
          <svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
            <text x="0" y="38" style="width: 100%;">Undo/Redo</text>
          </svg>
        </div>
        <div class="tab-content-panel" data-tab-group="rhs">
          <div class="toggle-content is-visible tabcontent" id="controlsTab">
            <p>This demo illustrates how to build an Undo/Redo stack with a KeyLines chart. Drag nodes to new positions or add new ones with the buttons below before trying out the Undo and Redo actions.</p>
            <form autocomplete="off" onsubmit="return false" id="rhsForm">
              <div>
                <div class="cicontent">
                  <legend>Controls</legend>
                  <div>
                    <input class="btn span2" type="button" value="Undo (0)" disabled id="undo">
                    <input class="btn span2" type="button" value="Redo (0)" disabled id="redo">
                  </div>
                  <div style="margin-top: 14px;">
                    <input class="btn" type="button" value="Layout" id="layout">
                  </div>
                  <div style="margin: 8px 0;">Actions on nodes:
                    <div>
                      <input class="btn span2" type="button" value="Add" id="add-node">
                      <input class="btn span2" type="button" value="Remove" id="remove-node">
                    </div>
                  </div>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
Loading source