# Edit, copy, and remove nodes

Use the canvas for the user's selection and the instance's engine for command-based edits. This lets you edit a node label in place, copy or duplicate selected nodes, paste clipboard contents, and remove selected nodes without rebuilding the diagram.

## Edit a label in place

Enable in-place editing on the mounted instance. A double-click opens an editor on the node label; Enter or blur commits the new label as one undoable step, while Escape abandons it. The JavaScript, Angular, React, and Vue examples also add toolbar actions for copy, paste, duplicate, and delete; the Qwik example follows the mounted-canvas pattern and uses its keyboard shortcuts for clipboard actions.

For the typed node data and mounting patterns across JavaScript, Angular, React, and Vue, see [Build runnable workflows](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/build-runnable-workflows). This page adds the engine actions for editing, copying, pasting, duplicating, and deleting selected nodes; click a node before using Copy, Duplicate, or Delete. Paste uses the engine clipboard rather than the operating system clipboard.

:::code-group
```ts title="JavaScript"
import { render } from '@grafloria/element';
import type { NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'draft', position: { x: 80, y: 110 }, size: { width: 150, height: 60 }, label: 'Draft' },
  { id: 'review', position: { x: 360, y: 110 }, size: { width: 150, height: 60 }, label: 'Review' },
];

const root = document.createElement('main');
root.style.height = '620px';
document.body.append(root);
const toolbar = document.createElement('div');
const canvas = document.createElement('div');
canvas.style.height = '570px';
root.append(toolbar, canvas);

const instance = render({ nodes }, canvas);
instance.getEngine().setInteractionConfig({ enableInPlaceTextEdit: true });

async function act(action: 'copy' | 'paste' | 'duplicate' | 'delete'): Promise<void> {
  const engine = instance.getEngine();
  const hasSelection = instance.getModel().getSelectedNodes().length > 0;
  if (action === 'copy' && hasSelection) await engine.copy();
  if (action === 'paste' && engine.hasClipboardData()) await engine.paste();
  if (action === 'duplicate' && hasSelection) await engine.duplicate();
  if (action === 'delete' && hasSelection) await engine.deleteSelection();
  instance.renderNow();
}

for (const action of ['copy', 'paste', 'duplicate', 'delete'] as const) {
  const button = document.createElement('button');
  button.textContent = action[0].toUpperCase() + action.slice(1);
  button.addEventListener('click', () => { void act(action); });
  toolbar.append(button);
}
```
```ts title="Angular"
import { AfterViewInit, Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec } from '@grafloria/renderer';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <div style="height:620px">
      <button type="button" (click)="copy()">Copy</button>
      <button type="button" (click)="paste()">Paste</button>
      <button type="button" (click)="duplicate()">Duplicate</button>
      <button type="button" (click)="deleteSelection()">Delete</button>
      <grafloria-diagram-canvas [(nodes)]="nodes"
        style="display:block;height:570px" />
    </div>
  `,
})
export class NodeEditorComponent implements AfterViewInit {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  nodes: NodeSpec[] = [
    { id: 'draft', position: { x: 80, y: 110 }, size: { width: 150, height: 60 }, label: 'Draft' },
    { id: 'review', position: { x: 360, y: 110 }, size: { width: 150, height: 60 }, label: 'Review' },
  ];
  ngAfterViewInit(): void {
    this.canvas().activeEngine()?.setInteractionConfig({ enableInPlaceTextEdit: true });
  }

  async copy(): Promise<void> {
    const engine = this.canvas().activeEngine();
    if (engine && engine.getDiagram()?.getSelectedNodes().length) await engine.copy();
  }

  async paste(): Promise<void> {
    const engine = this.canvas().activeEngine();
    if (engine?.hasClipboardData()) await engine.paste();
  }

  async duplicate(): Promise<void> {
    const engine = this.canvas().activeEngine();
    if (engine && engine.getDiagram()?.getSelectedNodes().length) await engine.duplicate();
  }

  async deleteSelection(): Promise<void> {
    const engine = this.canvas().activeEngine();
    if (engine && engine.getDiagram()?.getSelectedNodes().length) await engine.deleteSelection();
  }
}
```
```tsx title="React"
import { useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import type { NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'draft', position: { x: 80, y: 110 }, size: { width: 150, height: 60 }, label: 'Draft' },
  { id: 'review', position: { x: 360, y: 110 }, size: { width: 150, height: 60 }, label: 'Review' },
];

export default function NodeEditor() {
  const instance = useRef<DiagramInstance | null>(null);

  async function act(action: 'copy' | 'paste' | 'duplicate' | 'delete'): Promise<void> {
    const api = instance.current;
    if (!api) return;
    const engine = api.getEngine();
    const hasSelection = api.getModel().getSelectedNodes().length > 0;
    if (action === 'copy' && hasSelection) await engine.copy();
    if (action === 'paste' && engine.hasClipboardData()) await engine.paste();
    if (action === 'duplicate' && hasSelection) await engine.duplicate();
    if (action === 'delete' && hasSelection) await engine.deleteSelection();
    api.renderNow();
  }

  return (
    <div style={{ height: '620px' }}>
      <button type="button" onClick={() => { void act('copy'); }}>Copy</button>
      <button type="button" onClick={() => { void act('paste'); }}>Paste</button>
      <button type="button" onClick={() => { void act('duplicate'); }}>Duplicate</button>
      <button type="button" onClick={() => { void act('delete'); }}>Delete</button>
      <div style={{ height: '570px' }}>
        <GrafloriaFlow defaultNodes={nodes} onInit={(api) => {
          instance.current = api;
          api.getEngine().setInteractionConfig({ enableInPlaceTextEdit: true });
        }} />
      </div>
    </div>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { ref } from 'vue';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue';
import type { NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'draft', position: { x: 80, y: 110 }, size: { width: 150, height: 60 }, label: 'Draft' },
  { id: 'review', position: { x: 360, y: 110 }, size: { width: 150, height: 60 }, label: 'Review' },
];
const instance = ref<DiagramInstance | null>(null);

function onInit(api: DiagramInstance): void {
  instance.value = api;
  api.getEngine().setInteractionConfig({ enableInPlaceTextEdit: true });
}

async function copy(): Promise<void> {
  const api = instance.value;
  if (api && api.getModel().getSelectedNodes().length > 0) { await api.getEngine().copy(); api.renderNow(); }
}
async function paste(): Promise<void> {
  const api = instance.value;
  if (api?.getEngine().hasClipboardData()) { await api.getEngine().paste(); api.renderNow(); }
}
async function duplicate(): Promise<void> {
  const api = instance.value;
  if (api && api.getModel().getSelectedNodes().length > 0) { await api.getEngine().duplicate(); api.renderNow(); }
}
async function deleteSelection(): Promise<void> {
  const api = instance.value;
  if (api && api.getModel().getSelectedNodes().length > 0) { await api.getEngine().deleteSelection(); api.renderNow(); }
}
</script>

<template>
  <div style="height:620px">
    <button type="button" @click="copy">Copy</button>
    <button type="button" @click="paste">Paste</button>
    <button type="button" @click="duplicate">Duplicate</button>
    <button type="button" @click="deleteSelection">Delete</button>
    <div style="height:570px">
      <GrafloriaFlow :default-nodes="nodes" @init="onInit" />
    </div>
  </div>
</template>
```
:::

On the canvas, double-click Draft or Review to edit its label. Enter and blur commit the edit; Escape cancels it. Click a node to select it, then use Copy followed by Paste to create a separate node; repeat Paste to place further copies at cascading offsets. Duplicate creates a copy directly from the selection, and Delete removes the selected node. The JavaScript, Angular, React, and Vue examples expose these actions as toolbar buttons that run only when their required selection or clipboard content exists. Command-based changes participate in the engine's undo history; ⌘Z or Ctrl+Z undoes the last edit.

## Copy and paste versus duplicate

`copy()` snapshots the current selection to the engine clipboard; `paste()` creates new entities from that clipboard. `duplicate()` creates copies directly from the current selection. Both operations create nodes with new IDs and positions, so editing one copy does not change its source. Paste can be repeated from the same clipboard contents. These are engine operations, not browser clipboard integration.

| Operation | Option | Type | Default | Effect |
| --- | --- | --- | --- | --- |
| `copy()` | `includeGroups` | `boolean` | `false` | Includes groups containing selected nodes. |
| `copy()` | `includeLinks` | `boolean` | `true` | Includes links between copied nodes. |
| `paste()` | `offset` | [`Point`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-interaction-interfaces-a-t#point) | First paste: `{ x: 20, y: 20 }`; later pastes cascade | Shifts pasted entities to a new position. |
| `paste()` | `selectPasted` | `boolean` | `true` | Selects the pasted entities. |
| `duplicate()` | `offset` | [`Point`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-interaction-interfaces-a-t#point) | `{ x: 20, y: 20 }` | Shifts duplicated entities to a new position. |
| `duplicate()` | `selectDuplicated` | `boolean` | `true` | Selects the duplicated entities. |
| `deleteSelection()` | `deleteChildren` | `boolean` | `true` | Recursively deletes child nodes. |
| `deleteSelection()` | `deleteLinks` | `boolean` | `true` | Removes links before deleting nodes; links attached to a removed node are removed either way. |

The default paste offset starts at 20 pixels down and right and advances for subsequent pastes of the same clipboard contents. The copy and paste demo shows the rendered result: each paste has a different ID and position, and moving one copy leaves the others in place.

Try [Copy / paste](https://grafloria.com/demos/interaction/copy-paste.html) while you click Original, copy it, and paste more than once. The demo source is [copy-paste.html](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/copy-paste.html).

## Remove selected nodes

`deleteSelection()` acts on the current selection. When you remove a node, its connected links are removed too, so the diagram does not retain dangling edges. The deletion is undoable with the engine's command history. This is ordinary deletion; reconnecting neighbors across a removed middle node is an extra behavior implemented by the separate delete-middle-node demo, not an effect of `deleteSelection()` itself.

See [Delete a middle node](https://grafloria.com/demos/nodes/delete-middle-node.html) for the custom variant that adds an undoable A-to-C bridge after removing B; its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/nodes/delete-middle-node.html) explicitly reads the neighbors before deleting.

## Add a context menu

A context menu is host UI: the [context-menu demo](https://grafloria.com/demos/interaction/context-menu.html) builds a Rename / Duplicate / Delete menu, listens for a real `contextmenu` event, anchors its menu at the pointer, and calls model or engine operations for the targeted node. Its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/context-menu.html) is a starting point when you need that interaction rather than the toolbar used above.

For plain JavaScript, the same task is also shown in the [edit-label demo](https://grafloria.com/demos/nodes/edit-label.html), where double-click opens the editor and Enter commits the label. See its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/nodes/edit-label.html).

## Options that affect the edit

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `enableInPlaceTextEdit` | `boolean` | `false` | Enables double-click editing of node labels. |
| `offset` on `paste()` | `Point` | First paste: `{ x: 20, y: 20 }`; later pastes cascade | Sets the position shift applied to pasted entities. Pass an explicit point to control placement. |
| `offset` on `duplicate()` | `Point` | `{ x: 20, y: 20 }` | Sets the position shift applied to duplicated entities. |
| `selectPasted` | `boolean` | `true` | Selects the pasted entities. |
| `selectDuplicated` | `boolean` | `true` | Selects the duplicated entities. |

## Pitfalls

- The engine clipboard is separate from `navigator.clipboard`; connect it to the operating-system clipboard yourself if cross-tab or system paste is required.
- In-place label editing is opt-in. Leave `enableInPlaceTextEdit` off if your app handles the node's double-click event with its own editor.
- Do not put user-supplied node data into `innerHTML` in a custom renderer. Use `textContent` for untrusted values to avoid interpreting them as markup. See [Create custom nodes](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/create-custom-nodes).
- For a custom delete that reconnects neighboring nodes, use the delete-middle-node demo's separate read-neighbors, remove, and bridge steps; node deletion alone does not create that bridge.

## Related

- [Command history and edits](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/command-history) explains how edits enter the undo stack.
- [Undo and redo edits](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/undo-and-redo) covers the user-facing history controls.
- [Resize and size nodes](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/resize-and-size-nodes) covers custom node rendering and registration timing.
