Skip to content
  1. Examples

Context menu

This example shows how to use the layers API to add a DOM context menu to the graph. The context menu will appear when you right-click on a node and will allow you to remove the node from the graph.

ts
import Ogma, { Overlay, Node } from '@linkurious/ogma';

const ogma = new Ogma({
  graph: {
    nodes: [{ id: 0 }, { id: 1 }],
    edges: [{ source: 0, target: 1 }]
  },
  container: 'graph-container'
});

ogma.layouts.force({ locate: true });
let contextmenu: Overlay | null = null;

const expand = (node: Node) => {
  const maxNId = ogma.getNodes().size;
  const maxEId = ogma.getEdges().size;

  return ogma.generate
    .balancedTree({ children: 3, height: 1 })
    .then(subGraph => {
      subGraph.nodes.forEach(n => {
        n.id = +n.id! + maxNId;
      });
      subGraph.edges.forEach(e => {
        e.id = +e.id! + maxEId;
        e.source = +e.source! + maxNId;
        e.target = +e.target! + maxNId;
      });
      subGraph.edges.push({
        source: node.getId(),
        target: subGraph.nodes[0].id!
      });
      return ogma.addGraph(subGraph);
    })
    .then(() => ogma.layouts.force({ locate: true }));
};

function createMenuItem(text: string, action: (evt: MouseEvent) => void) {
  const item = document.createElement('span');
  item.classList.add('item');
  item.innerText = text;
  item.addEventListener('click', action);
  item.addEventListener('click', evt => {
    evt.preventDefault();
    evt.stopPropagation();
    destroyContextMenu();
  });
  return item;
}

function showContextMenu(node: Node) {
  const expandButton = createMenuItem('Expand', evt => {
    evt.preventDefault();
    evt.stopPropagation();
    expand(node);
  });
  const destroyButton = createMenuItem('Remove', evt => {
    evt.preventDefault();
    evt.stopPropagation();
    ogma.removeNode(node);
  });

  const div = document.createElement('div');
  div.classList.add('context-menu');
  div.appendChild(expandButton);
  div.appendChild(destroyButton);

  // to be able to measure the element
  document.body.appendChild(div);

  const screenPos = node.getPositionOnScreen();
  const radius = node.getAttribute('radius');
  const scaledRadius = +radius * ogma.view.getZoom();

  const offsetWidth = Math.min(100, div.offsetWidth);
  const offsetHeight = div.offsetHeight;
  const { width, height } = ogma.view.getSize();

  let x = screenPos.x + scaledRadius;
  let y = screenPos.y + scaledRadius;

  if (screenPos.x + scaledRadius + offsetWidth > width) {
    x = Math.max(0, screenPos.x - scaledRadius - offsetWidth);
  }
  if (screenPos.y + scaledRadius + offsetHeight > height) {
    y = Math.max(0, screenPos.y - scaledRadius - offsetHeight);
  }

  const position = ogma.view.screenToGraphCoordinates({ x, y });

  contextmenu = ogma.layers.addOverlay({
    element: div,
    position,
    scaled: false,
    size: { width: 100, height: 'auto' }
  });
}

function destroyContextMenu() {
  contextmenu?.destroy();
  contextmenu = null;
}

ogma.events.on('click', ({ target, domEvent }) => {
  const isContextMenuTarget =
    contextmenu && contextmenu.element.contains(domEvent.target as HTMLElement);
  if (isContextMenuTarget) return; // if the target is the context menu just let its item handle the propagated click
  if (contextmenu) return destroyContextMenu(); // otherwise, if there is a context menu, destroy it
  if (target && target.isNode) showContextMenu(target); // otherwise, if the target is a node, create it
});

setTimeout(() => {
  const node = ogma.getNodes().get(0);
  node.setSelected(true);
  showContextMenu(node);
}, 500);
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div id="graph-container"></div>
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
html,
body {
  margin: 0;
  padding: 0;
}

#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

.context-menu {
  font-family: Georgia, 'Times New Roman', Times, serif;
  border: 1px solid #ccc;
  border-radius: 3px;
  background-color: #eee;
  padding: 1px;
}
.context-menu > .item {
  border-bottom: 1px solid #ccc;
  background: white;
  color: #444;
  display: block;
  padding: 5px 10px;
  cursor: pointer;
}

.context-menu > .item:hover {
  background: #ddd;
  color: #222;
}

.context-menu > .item:last-child {
  border-bottom: none;
}