Appearance
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;
}