//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Include a context menu that provides element-specific options.
import KeyLines from "keylines";
import { data } from "./contextmenu-data.js";

let chart;
let selectedItemId;
const contextMenuId = "contextMenu";

// Background context menu options
const contextMenuBackground = [
  {
    id: "selectAll",
    text: "Select All",
  },
];

// Item context menu options
const contextMenuItems = [
  {
    id: "selectNeighbours",
    text: "Select Neighbours",
  },
  {
    id: "deselectAll",
    text: "Deselect All",
  },
];

// Creates html for the context menu that we want to show
function createMenuMarkup(entries, x, y) {
  const listEntries = entries
    .map(
      (entry) =>
        `<li><a id='${entry.id}' tabindex='-1' oncontextmenu='return false;'> ${entry.text}</a></li>`
    )
    .join("");

  return `
    <ul class='kl-dropdown-menu' role='menu' style='top: ${y}px; left: ${x}px; display: block;'>
        ${listEntries}
    </ul>
  `;
}

// Remove any context menu from chart if it is displayed
function removeExistingMenu() {
  const menuToRemove = document.getElementById(contextMenuId);
  if (menuToRemove) {
    menuToRemove.remove();
  }
}

function displayContextMenu(item, x, y) {
  // Clicked on an item or on the background
  const menuToDisplay = item ? contextMenuItems : contextMenuBackground;
  // Set id of selected item if exists
  selectedItemId = item ? item.id : null;
  // Render context menu markup and display
  const newMenu = document.createElement("div");
  newMenu.id = contextMenuId;
  newMenu.innerHTML = createMenuMarkup(menuToDisplay, x + 8, y + 8);
  document.getElementsByClassName("chart-wrapper")[0].append(newMenu);
}

function createContextMenu({ id, x, y }) {
  const item = chart.getItem(id);
  removeExistingMenu();
  displayContextMenu(item, x, y);
}

// Select items that are linked to the current item
function selectNeighbours(selectedItem) {
  const neighbours = chart.graph().neighbours(selectedItem);
  // Make sure selection includes all links, nodes and the current item
  chart.selection([selectedItem, ...neighbours.nodes, ...neighbours.links]);
}

// Select all items
function selectAll() {
  const select = [];
  chart.each({ type: "all" }, (chartItem) => select.push(chartItem.id));
  chart.selection(select);
}

// Deselect all items
function deselectAll() {
  chart.selection([]);
}

function createEventHandlers() {
  // Listener for all click events
  document.addEventListener("click", (event) => {
    // Handle click events on menu list items
    if (event.target.id === "selectAll") {
      selectAll();
    } else if (event.target.id === "selectNeighbours") {
      selectNeighbours(selectedItemId);
    } else if (event.target.id === "deselectAll") {
      deselectAll();
    }
    // Hide the context menu when clicking outside of the chart
    if (event.which === 1) {
      removeExistingMenu();
    }
  });

  // When user pans, zooms or clicks the chart we will hide the context menu
  chart.on("view-change", removeExistingMenu);
  chart.on("pointer-down", removeExistingMenu);
  // Listening for the context menu event allows us to create the menu
  chart.on("context-menu", createContextMenu);
  // Prevent dragging while the context menu is is visible
  chart.on("drag-start", (e) => {
    if (document.getElementById(contextMenuId)) {
      e.preventDefault();
    }
  });
}

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

window.addEventListener("DOMContentLoaded", loadKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Context Menus</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="/contextmenu.js" crossorigin="use-credentials" defer 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%;">Context Menus</text>
          </svg>
        </div>
        <div class="tab-content-panel" data-tab-group="rhs">
          <div class="toggle-content is-visible tabcontent" id="controlsTab">
            <form autocomplete="off" onsubmit="return false" id="rhsForm">
              <div class="cicontent" style="height:100%; max-height: 565px; overflow-y:auto;box-sizing: border-box;">
                <p>Right-click on the chart or chart background (or long press on touch devices) to display different context menus.</p>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
Loading source