Skip to content
  1. Examples

Nodes label editing

This example shows how to use Overlay layers to create a text editor for nodes.

Double-click on the node text to edit it.

ts
import Ogma, { NodeId, Node, Edge, BoundingBox } from '@linkurious/ogma';

const ogma = new Ogma({
  container: 'graph-container'
});

// this object will hold all the custom texts set by the user.
const nodeTexts: Record<NodeId, string> = {};
const textRule = ogma.styles.addNodeRule({
  text: {
    content: node => {
      if (node) {
        const id = node.getId();
        return nodeTexts[id] !== undefined ? nodeTexts[id] : `node-${id}`;
      }
    },
    minVisibleSize: 0,
    size: 20
  }
});
const graph = await ogma.generate.random({
  nodes: 10,
  edges: 20
});
await ogma.setGraph(graph);
await ogma.layouts.force({ locate: true });

// create the overlay layer
const input = document.querySelector<HTMLInputElement>('.node-text-input')!;
const inputOverlay = ogma.layers
  .addOverlay({
    element: input,
    position: { x: 0, y: 0 },
    scaled: false
  })
  .hide();

// hook it to the double click event
ogma.events.on('doubleclick', ({ target, x, y }) => {
  if (!target || !target.isNode) return;
  const bb = ogma.view.getTextBoundingBox(target);
  if (bb === null) return;
  const pt = ogma.view.screenToGraphCoordinates({ x, y });
  // check if we clicked on the text and not on the node
  if (pt.x >= bb.minX && pt.y >= bb.minY && pt.x < bb.maxX && pt.y < bb.maxY) {
    startTextEditing(target, bb);
  }
});

function startTextEditing(node: Node, bb: BoundingBox) {
  let isEditing = false;
  input.value = node.getAttribute('text.content') as string;
  input.style.fontSize = node.getAttribute('text.size') as string;

  function setInputPosition() {
    inputOverlay.setPosition({
      x: node.getAttribute('x') - input.clientWidth / 2 / ogma.view.getZoom(),
      y: bb.minY
    });
  }

  function onKeyPress(evt: KeyboardEvent) {
    if (evt.code !== 'Enter') return;
    stopEditing();
  }

  function stopEditing() {
    isEditing = false;
    nodeTexts[node.getId()] = input.value;
    textRule.refresh();
    inputOverlay.hide();

    node.setAttribute('text.content', undefined);
    input.removeEventListener('keypress', onKeyPress);
    input.removeEventListener('focusout', onFocusOut);
    ogma.events.off(setInputPosition).off(onClick);
  }

  function onFocusOut() {
    if (!isEditing) return;
    ogma.events.off(setInputPosition).off(onClick);
    isEditing = false;
    node.setAttribute('text.content', undefined);
    input.removeEventListener('keypress', onKeyPress);
    input.removeEventListener('focusout', onFocusOut);
  }

  function onClick({ target }: { target?: Node | Edge | null }) {
    if (!target) stopEditing();
  }

  setInputPosition();

  // temporary hide the node text, as we show the input
  node.setAttribute('text.content', '');
  ogma.events.on('zoom', setInputPosition).on('click', onClick);

  input.addEventListener('keypress', onKeyPress);
  input.addEventListener('focusout', onFocusOut);
  inputOverlay.show();
  input.focus();
}
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link
      href="https://fonts.googleapis.com/css2?family=Noto+Serif&family=Roboto&family=Ubuntu&family=Merriweather&display=swap"
      rel="stylesheet"
    />

    <link type="text/css" rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div id="graph-container"></div>
    <input type="text" class="node-text-input" />
    <script type="module" src="index.ts"></script>
  </body>
</html>
css
#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

.node-text-input {
  border: none;
  outline: none;
  background-color: #ddd;
  border-radius: 2px;
  padding: 4px 2px;
  text-align: center;
  max-width: 100px;
}