Skip to content
  1. Examples

Editable notes and in-place forms

This example shows how to use layers to add rich texts to nodes. Click node to open a rich text form. Change the text to add tooltips to the nodes. Move and drag nodes around to see how tooltips follow them. Have a look at our annotations plugin.

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

const ogma = new Ogma({
  graph: {
    nodes: [
      { id: 0, data: { tooltip: `Your <b> text</b> here` } },
      {
        id: 1,
        data: { tooltip: `Your <i>text</i> here` },
        attributes: { x: 150, y: 0 }
      }
    ],
    edges: [{ source: 0, target: 1 }]
  },
  container: 'graph-container'
});

await ogma.view.locateGraph({ padding: { top: 200 }, ignoreZoomLimits: true });

ogma.styles.addNodeRule({
  radius: 20
});

interface Tooltip {
  layer: Overlay;
  id: NodeId;
}

const tooltips: Tooltip[] = [];
const tooltipByNode: Record<NodeId, Tooltip> = {};
const addTooltipToNode = (node: Node, html: string) => {
  const position = node.getPosition();
  const nodeId = node.getId();
  let tooltip = tooltipByNode[nodeId];

  position.y -= 75;
  position.x += 15;

  const size = { width: 100, height: 50 };

  if (tooltip) {
    tooltip.layer.setPosition(position).setSize(size);
    tooltip.layer.element.innerHTML = `<div class="tooltip">${html}</div>`;
  } else {
    const layer = ogma.layers.addOverlay({
      element: `<div><div class="tooltip">${html}</div></div>`,
      size: { width: 100, height: 50 },
      position
    });
    tooltip = { layer, id: nodeId };
  }
  node.setData('tooltip', html);
  tooltipByNode[nodeId] = tooltip;
  tooltips.push(tooltip);
};

const formWidth = 300;
const formHeight = 300;

let layer: Overlay | null, currentNode: Node;
const showFormForNode = (node: Node) => {
  const text = node.getData('tooltip');
  currentNode = node;
  // create new form
  layer = ogma.layers.addLayer(`<form class="note-form">
    <div id="editor">${text}</div>
    <button type="submit" class="submit">Submit</button>
  </form>`);
  const position = node.getPositionOnScreen();
  position.y -= formHeight + 15;
  position.x += 15;

  layer.setPosition(position).setSize({ width: formWidth, height: formHeight });

  const editor = new Quill('#editor', {
    theme: 'snow'
  });
  layer.element.addEventListener('submit', evt => {
    evt.preventDefault();
    //get quill editor contents
    const contents = editor.getContents().ops;
    // render to HTML
    const html = new QuillDeltaToHtmlConverter(contents, {}).convert();
    layer.destroy();
    layer = null;
    // create scalable tooltip layer
    addTooltipToNode(node, html);
  });

  // disable the camera
  ogma.setOptions({
    interactions: {
      drag: { enabled: false },
      //pan: { enabled: false },
      zoom: { enabled: false }
    }
  });
};

// move form when the camera has been moved
const onCameraMove = () => {
  if (layer) {
    const position = currentNode.getPositionOnScreen();
    position.y -= formHeight + 15;
    position.x += 15;
    layer.setPosition(position);
  }
};

ogma.events.on(['dragProgress', 'zoom'], onCameraMove);

// click handler
ogma.events.on('click', ({ target, domEvent }) => {
  // hide form if present and the click does not originate from it
  if (layer && !layer.element.contains(domEvent.target as HTMLElement)) {
    layer.destroy();
    layer = null;
  }

  // disable the camera
  ogma.setOptions({
    interactions: {
      drag: { enabled: true },
      //pan: { enabled: true },
      zoom: { enabled: true }
    }
  });

  // not a node
  if (!target || !target.isNode) return;

  showFormForNode(target);
});

// node has been dragged, move tooltips
ogma.events.on('nodesDragProgress', ({ dx, dy }) => {
  // show and update tooltips
  requestAnimationFrame(() => {
    const nodes = ogma.getNodes(tooltips.map(({ id }) => id));
    const positions = nodes.getPosition();
    tooltips.forEach(({ layer }, i) => {
      const position = positions[i];

      position.x += 15;
      position.y -= 75;

      layer.setPosition(position);
      layer.show();
    });
  });
});

setTimeout(() => {
  const node = ogma.getNodes().get(0);
  node.setSelected(true);
  showFormForNode(node);
}, 500);
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link type="text/css" rel="stylesheet" href="styles.css" />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css"
    />
    <script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quill-delta-to-html@0.12.1/dist/browser/QuillDeltaToHtmlConverter.bundle.js"></script>
  </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;
}

.note, .note-form {
  box-shadow: 0 0 5px rgba(0,0,0,0.25);
  border-radius: 10px 10px 10px 0;
  overflow: hidden;
}

.note-form {
  position: absolute;
  background: rgba(255,255,255,0.86);
}

.note-form #editor {
  height: 200px;
}

.note-form .submit {
  margin: 8px;
  float: right;
}


.tooltip {
  border-radius: 15px;
  background-color: lightyellow;
  border: 1px solid #666;
  padding: 0 8px;
  border-bottom-left-radius: 0;
}