Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c91f2a0
Cancelling live interactions
Jacek-Synergy Jul 14, 2026
9ceb827
Merge with await logic
Jacek-Synergy Aug 3, 2026
bdbd60f
Merge with await logic
Jacek-Synergy Aug 3, 2026
43f7aa5
Update changelog
Jacek-Synergy Aug 3, 2026
cee1291
Merge branch 'main' into cancel-interaction
lukasz-jazwa Aug 5, 2026
b14797d
Canceling hardening
lukasz-jazwa Aug 5, 2026
f5d4a4c
Clear the shared touch marker only from its owning gesture
lukasz-jazwa Aug 5, 2026
ac8bcb8
Add e2e coverage for cancelActiveInteraction and the Escape binding
lukasz-jazwa Aug 5, 2026
6329c2d
Simplify the cancel-interaction additions after review
lukasz-jazwa Aug 5, 2026
e82018e
Fix adversarial-review findings in gesture cancellation
lukasz-jazwa Aug 5, 2026
08a7236
Simplify the adversarial-fix commit after review
lukasz-jazwa Aug 5, 2026
594bbab
Make the cancel machinery self-guiding
lukasz-jazwa Aug 5, 2026
4ea0206
Drop gesture input while a cancel rollback is still committing
lukasz-jazwa Aug 5, 2026
4ac6c5b
Grab diagram focus in the capture phase of pointerdown
lukasz-jazwa Aug 5, 2026
c3fa227
Keep KeyboardInputsDirective's public shape after the focus-grab change
lukasz-jazwa Aug 5, 2026
de389a7
Sort the shortcut actions table alphabetically
lukasz-jazwa Aug 5, 2026
6099b29
Tighten the cancelActiveInteraction TSDoc
lukasz-jazwa Aug 5, 2026
e5eddc7
Extract the cancel machinery into InteractionCoordinator
lukasz-jazwa Aug 5, 2026
81d9ff7
Make the cancel-path comments say what, not how it went
lukasz-jazwa Aug 5, 2026
326447e
Pin InteractionCoordinator behavior with dedicated unit tests
lukasz-jazwa Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `waitForMeasurements` option on service methods — `addNodes`, `addEdges`, `updateNode`, `updateNodes`, `updateNodeData`, `updateEdge`, `updateEdges`, `updateEdgeData` on `NgDiagramModelService`, `resizeNode` on `NgDiagramNodeService` and `paste` on `NgDiagramClipboardService` accept `options?: { waitForMeasurements?: boolean }`; when set, the returned promise resolves only after the elements affected by the change have been measured — useful whenever the next step depends on real dimensions (for example `zoomToFit()` or `centerOnNode()`). The option exists only on methods whose changes can trigger measurements; deletions and other model-only operations have nothing to measure, so awaiting the method itself is already enough there. Inside an already active transaction the option is ignored with a console warning — pass `{ waitForMeasurements: true }` to the transaction itself instead ([#769](https://github.com/synergycodes/ng-diagram/pull/769))
- **Customizable runtime-property stripping** – `initializeModel` and `initializeModelAdapter` accept an optional `InitializeModelOptions` parameter to control which properties are stripped on initialization and `toJSON()`. Overriding the defaults can break the diagram — use at your own risk ([#760](https://github.com/synergycodes/ng-diagram/pull/760))
- **Resize snap offset** — new `computeSnapOffsetForNodeSize` and `defaultResizeSnapOffset` options on `SnappingConfig`. Snapped node sizes now follow the sequence `offset + n * snap` per axis, so a node with a 60px header and a 50px vertical resize snap can snap to 60, 110, 160, … instead of 50, 100, 150, …. Defaults to `{ width: 0, height: 0 }` ([#765](https://github.com/synergycodes/ng-diagram/issues/765), [#770](https://github.com/synergycodes/ng-diagram/pull/770)) — thanks [@logan-brd](https://github.com/logan-brd) for the suggestion! 🙏

- **`NgDiagramService.transaction` always returns the commit promise** — the synchronous-callback overload used to return `void`, and for some async callbacks the commit promise was silently discarded; all overloads now return `Promise<TransactionResult>`, so awaiting a transaction reliably waits for its commit ([#769](https://github.com/synergycodes/ng-diagram/pull/769))
- **Cancel in-progress gestures** – new `NgDiagramService.cancelActiveInteraction()` aborts the active linking, drag, resize, rotate or pan gesture immediately and restores the pre-gesture state: dragged nodes snap back to their initial positions, resized/rotated nodes regain their original geometry, and the temporary edge is discarded (state cleared, document listeners removed, no need to wait for pointer release). Bound to Escape by default via the new `cancelInteraction` shortcut action. The `edgeDrawEnded` event gains a `cancelled` reason, and `nodeDragEnded`/`nodeResizeEnded`/`nodeRotateEnded` gain an optional `cancelReason` field ([#747](https://github.com/synergycodes/ng-diagram/issues/747), [#766](https://github.com/synergycodes/ng-diagram/pull/766))

### Fixed

Expand All @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Dangling edges survive persistence** — `initializeModel` and `initializeModelAdapter` no longer strip the authored `sourcePosition`/`targetPosition` of an edge's free endpoint (empty `source`/`target`), and `toJSON()` now includes them in the serialized output, so dangling edges load from a persisted model the same way they work when added at runtime; no more "Invalid edge coordinates detected" for valid dangling edges on init ([#751](https://github.com/synergycodes/ng-diagram/issues/751), [#760](https://github.com/synergycodes/ng-diagram/pull/760))
- **Port `side`/`type` no longer stay stale after a port moves** — recreating a port with the same id in a different place (e.g. toggling a port between a `side: 'left'` and a `side: 'right'` block) now updates `measuredPorts` with the new `side`/`type`, so edges anchor to the correct side; measured `size`/`position` keep coming from the DOM as before. The same applies to edge labels re-registered with a changed `positionOnEdge` ([#750](https://github.com/synergycodes/ng-diagram/issues/750), [#763](https://github.com/synergycodes/ng-diagram/pull/763))
- **Group with children jumping on resize snap** — resizing a group that contains child nodes from the bottom/right edge no longer moves the group when a resize snap is configured ([#765](https://github.com/synergycodes/ng-diagram/issues/765), [#770](https://github.com/synergycodes/ng-diagram/pull/770)) — thanks [@logan-brd](https://github.com/logan-brd) for the issue submission! 🙏
- **Keyboard shortcuts work when a gesture starts with focus outside the diagram** — the resize/rotate handles stop the pointerdown propagation, which used to skip the diagram's focus grab; starting a resize right after clicking an external control (e.g. a toolbar button) left every shortcut dead — in particular Escape could not cancel the gesture. The diagram now takes focus on any pointerdown inside it ([#766](https://github.com/synergycodes/ng-diagram/pull/766))
- **Touch gestures stay exclusive under virtualization** — on touch devices with virtualization enabled, nodes and ports leaving the rendered area during a pan or pinch-zoom no longer reset the internal gesture-exclusivity state, so a stray touch can no longer start a second gesture (drag, resize, linking) in the middle of an active one ([#766](https://github.com/synergycodes/ng-diagram/pull/766))
- **Resize snapping no longer cuts group children** — with `allowResizeBelowChildrenBounds: false`, a snapped group size that would land inside the children bounds now rounds up to the next snap value that still contains the children ([#770](https://github.com/synergycodes/ng-diagram/pull/770))

## [1.2.4] - 2026-06-02
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ Key is node ID, value is the accumulated delta that hasn't been applied due to s

***

### cancelReason?

> `optional` **cancelReason**: `"cancelled"`

Set when the drag is aborted; carried into `nodeDragEnded`.

***

### modifiers

> **modifiers**: [`InputModifiers`](/docs/api/types/configuration/shortcuts/inputmodifiers/)
Expand Down
8 changes: 8 additions & 0 deletions apps/docs/src/content/docs/api/Internals/ResizeActionState.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ State tracking a node resize operation in progress.

## Properties

### cancelReason?

> `optional` **cancelReason**: `"cancelled"`

Set when the resize is aborted; carried into `nodeResizeEnded`.

***

### resizingNode

> **resizingNode**: [`Node`](/docs/api/types/model/node/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ State tracking a node rotation operation in progress.

## Properties

### cancelReason?

> `optional` **cancelReason**: `"cancelled"`

Set when the rotation is aborted; carried into `nodeRotateEnded`.

***

### initialNodeAngle

> **initialNodeAngle**: `number`
Expand Down
35 changes: 35 additions & 0 deletions apps/docs/src/content/docs/api/Services/NgDiagramService.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,41 @@ True if events are enabled.

***

### cancelActiveInteraction()

> **cancelActiveInteraction**(): `Promise`\<`boolean`\>

Aborts the in-progress gesture (linking, drag, resize, rotate or pan):
removes its listeners immediately, restores the state it modified
(positions, size, angle, temporary edge — the viewport is not rolled
back) and fires the corresponding "ended" event with the `cancelled`
reason.

No-op when nothing is active, when the gesture is already completing, or
while a transaction is active (refused with a console warning — cancel
after it settles).

Bound to Escape by default via the `cancelInteraction` shortcut action —
see [configureShortcuts](/docs/api/utilities/configureshortcuts/).

#### Returns

`Promise`\<`boolean`\>

Promise resolving to whether anything was torn down

#### Example

```typescript
ngDiagramService.cancelActiveInteraction();
```

#### Since

1.3.0

***

### getDefaultRouting()

> **getDefaultRouting**(): `string`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ prev: false
title: "KeyboardActionName"
---

> **KeyboardActionName** = [`KeyboardMoveSelectionAction`](/docs/api/types/configuration/shortcuts/keyboardmoveselectionaction/) \| [`KeyboardPanAction`](/docs/api/types/configuration/shortcuts/keyboardpanaction/) \| [`KeyboardZoomAction`](/docs/api/types/configuration/shortcuts/keyboardzoomaction/) \| `Extract`\<`InputEventName`, `"cut"` \| `"paste"` \| `"copy"` \| `"deleteSelection"` \| `"undo"` \| `"redo"` \| `"selectAll"`\>
> **KeyboardActionName** = [`KeyboardMoveSelectionAction`](/docs/api/types/configuration/shortcuts/keyboardmoveselectionaction/) \| [`KeyboardPanAction`](/docs/api/types/configuration/shortcuts/keyboardpanaction/) \| [`KeyboardZoomAction`](/docs/api/types/configuration/shortcuts/keyboardzoomaction/) \| `Extract`\<`InputEventName`, `"cut"` \| `"paste"` \| `"copy"` \| `"deleteSelection"` \| `"undo"` \| `"redo"` \| `"selectAll"` \| `"cancelInteraction"`\>

Keyboard action names that can be triggered by keyboard events
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ prev: false
title: "EdgeDrawCancelReason"
---

> **EdgeDrawCancelReason** = `"noTarget"` \| `"invalidConnection"` \| `"invalidTarget"`
> **EdgeDrawCancelReason** = `"noTarget"` \| `"invalidConnection"` \| `"invalidTarget"` \| `"cancelled"`

Reason an edge draw gesture was cancelled.

- `noTarget` — the user released on empty space (no target node/port snapped)
- `invalidConnection` — `validateConnection()` returned false
- `invalidTarget` — the target node doesn't exist or the target port has wrong type
- `cancelled` — the gesture was aborted programmatically (e.g. Esc key,
[NgDiagramService.cancelActiveInteraction](/docs/api/services/ngdiagramservice/#cancelactiveinteraction))
15 changes: 15 additions & 0 deletions apps/docs/src/content/docs/api/Types/Events/GestureCancelReason.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
version: "since v1.3.0"
editUrl: false
next: false
prev: false
title: "GestureCancelReason"
---

> **GestureCancelReason** = `"cancelled"`

Reason an interactive gesture (drag, resize, rotation) ended without a normal
pointer release.

- `cancelled` — the gesture was aborted programmatically (e.g. Esc key,
[NgDiagramService.cancelActiveInteraction](/docs/api/services/ngdiagramservice/#cancelactiveinteraction))
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Nodes will have their final positions when this event is received.

## Properties

### cancelReason?

> `optional` **cancelReason**: `"cancelled"`

Present when the drag ended without a normal pointer release

***

### nodes

> **nodes**: [`Node`](/docs/api/types/model/node/)[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ The node will have its final size when this event is received.

## Properties

### cancelReason?

> `optional` **cancelReason**: `"cancelled"`

Present when the resize ended without a normal pointer release

***

### node

> **node**: [`Node`](/docs/api/types/model/node/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ The node will have its final angle when this event is received.

## Properties

### cancelReason?

> `optional` **cancelReason**: `"cancelled"`

Present when the rotation ended without a normal pointer release

***

### node

> **node**: [`Node`](/docs/api/types/model/node/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ prev: false
title: "ModelActionType"
---

> **ModelActionType** = `"init"` \| `"changeSelection"` \| `"moveNodesBy"` \| `"deleteSelection"` \| `"addNodes"` \| `"updateNode"` \| `"updateNodes"` \| `"deleteNodes"` \| `"clearModel"` \| `"paletteDropNode"` \| `"addEdges"` \| `"updateEdge"` \| `"deleteEdges"` \| `"deleteElements"` \| `"addEdgeLabelsBulk"` \| `"updateEdgeLabelsBulk"` \| `"deleteEdgeLabelsBulk"` \| `"addPortsBulk"` \| `"updatePortsBulk"` \| `"deletePortsBulk"` \| `"paste"` \| `"moveViewport"` \| `"resizeNode"` \| `"resizeNodeStart"` \| `"resizeNodeStop"` \| `"startLinking"` \| `"moveTemporaryEdge"` \| `"finishLinking"` \| `"zoom"` \| `"changeZOrder"` \| `"rotateNodeTo"` \| `"rotateNodeStart"` \| `"rotateNodeStop"` \| `"highlightGroup"` \| `"highlightGroupClear"` \| `"moveNodes"` \| `"moveNodesStart"` \| `"moveNodesStop"` \| `"selectEnd"`
> **ModelActionType** = `"init"` \| `"changeSelection"` \| `"moveNodesBy"` \| `"deleteSelection"` \| `"addNodes"` \| `"updateNode"` \| `"updateNodes"` \| `"deleteNodes"` \| `"clearModel"` \| `"paletteDropNode"` \| `"addEdges"` \| `"updateEdge"` \| `"deleteEdges"` \| `"deleteElements"` \| `"addEdgeLabelsBulk"` \| `"updateEdgeLabelsBulk"` \| `"deleteEdgeLabelsBulk"` \| `"addPortsBulk"` \| `"updatePortsBulk"` \| `"deletePortsBulk"` \| `"paste"` \| `"moveViewport"` \| `"resizeNode"` \| `"resizeNodeStart"` \| `"resizeNodeStop"` \| `"cancelResize"` \| `"startLinking"` \| `"moveTemporaryEdge"` \| `"finishLinking"` \| `"zoom"` \| `"changeZOrder"` \| `"rotateNodeTo"` \| `"rotateNodeStart"` \| `"rotateNodeStop"` \| `"cancelRotate"` \| `"highlightGroup"` \| `"highlightGroupClear"` \| `"moveNodes"` \| `"moveNodesStart"` \| `"moveNodesStop"` \| `"cancelDrag"` \| `"selectEnd"`

Individual model action type that can trigger middleware execution.
These represent all possible operations that modify the diagram state.
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/api/_readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ title: "ng-diagram"
- [SelectionRotatedEvent](/docs/api/types/events/selectionrotatedevent/)
- [ViewportChangedEvent](/docs/api/types/events/viewportchangedevent/)
- [EdgeDrawCancelReason](/docs/api/types/events/edgedrawcancelreason/)
- [GestureCancelReason](/docs/api/types/events/gesturecancelreason/)

## Types/Geometry

Expand Down
17 changes: 9 additions & 8 deletions apps/docs/src/content/docs/guides/shortcut-manager.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,27 @@ All available shortcut actions with their default key bindings. See [`ShortcutAc

| Action | Default Shortcut | Description |
| ---------------------------- | ----------------------------------------- | --------------------------------------------------------------------------- |
| `boxSelection` | <kbd>Shift</kbd> held | Enable box selection mode (pointer only) |
| `cancelInteraction` | <kbd>Escape</kbd> | Cancel the in-progress interaction (linking, drag, resize, rotate, pan) |
| `copy` | <kbd>Ctrl/Cmd + C</kbd> | Copy selected elements to clipboard |
| `cut` | <kbd>Ctrl/Cmd + X</kbd> | Cut selected elements to clipboard |
| `paste` | <kbd>Ctrl/Cmd + V</kbd> | Paste elements from clipboard |
| `deleteSelection` | <kbd>Delete</kbd> or <kbd>Backspace</kbd> | Delete currently selected elements |
| `selectAll` | <kbd>Ctrl/Cmd + A</kbd> | Select all elements in the diagram |
| `boxSelection` | <kbd>Shift</kbd> held | Enable box selection mode (pointer only) |
| `multiSelection` | <kbd>Ctrl/Cmd</kbd> held | Multi-selection mode (pointer only) |
| `keyboardMoveSelectionUp` | <kbd>↑</kbd> | Move selected elements up |
| `keyboardMoveSelectionDown` | <kbd>↓</kbd> | Move selected elements down |
| `keyboardMoveSelectionLeft` | <kbd>←</kbd> | Move selected elements left |
| `keyboardMoveSelectionRight` | <kbd>→</kbd> | Move selected elements right |
| `keyboardPanUp` | <kbd>↑</kbd> | Pan viewport up |
| `keyboardMoveSelectionUp` | <kbd>↑</kbd> | Move selected elements up |
| `keyboardPanDown` | <kbd>↓</kbd> | Pan viewport down |
| `keyboardPanLeft` | <kbd>←</kbd> | Pan viewport left |
| `keyboardPanRight` | <kbd>→</kbd> | Pan viewport right |
| `zoom` | <kbd>Ctrl/Cmd + Wheel</kbd> | Increase or decrease diagram viewport scale |
| `keyboardPanUp` | <kbd>↑</kbd> | Pan viewport up |
| `keyboardZoomIn` | <kbd>=</kbd> | Increase diagram viewport scale |
| `keyboardZoomOut` | <kbd>-</kbd> | Decrease diagram viewport scale |
| `undo` | <kbd>Ctrl/Cmd + Z</kbd> | Undo last action (not implemented by default; requires custom model) |
| `multiSelection` | <kbd>Ctrl/Cmd</kbd> held | Multi-selection mode (pointer only) |
| `paste` | <kbd>Ctrl/Cmd + V</kbd> | Paste elements from clipboard |
| `redo` | <kbd>Ctrl/Cmd + Y</kbd> | Redo last undone action (not implemented by default; requires custom model) |
| `selectAll` | <kbd>Ctrl/Cmd + A</kbd> | Select all elements in the diagram |
| `undo` | <kbd>Ctrl/Cmd + Z</kbd> | Undo last action (not implemented by default; requires custom model) |
| `zoom` | <kbd>Ctrl/Cmd + Wheel</kbd> | Increase or decrease diagram viewport scale |

## Basic Usage

Expand Down
Loading
Loading