Skip to content

feat(dia.Paper): add startLinkDrag() to drag a link end programmatically - #3505

Draft
kumilingus wants to merge 7 commits into
clientIO:devfrom
kumilingus:feat/paper-start-link-drag
Draft

feat(dia.Paper): add startLinkDrag() to drag a link end programmatically#3505
kumilingus wants to merge 7 commits into
clientIO:devfrom
kumilingus:feat/paper-start-link-drag

Conversation

@kumilingus

@kumilingus kumilingus commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an official API to start dragging an end of any link, replacing the private-state workarounds apps currently need (e.g. ai-agent-builder/ts/src/system/actions/connect-nodes.ts in joint-demos, which injects targetMagnet into eventData, reads the private linkView from it and "cancels" by removing the link).

// existing link, pointer already pressed (e.g. in an `element:pointerdown` handler)
await paper.startLinkDrag(link, { end: 'source' }).followPointer();

// new link from a magnet of an element OR a link, click-move-click, Escape / right click cancels
const { cancelled } = await paper
    .startLinkDrag(view.createLinkFromMagnet(magnet, x, y))
    .followPointer({ finishOn: 'pointerdown' });

// full control (tests, keyboard, custom gestures)
const drag = paper.startLinkDrag(link);
drag.move(300, 200);
drag.finish(300, 200); // or drag.cancel()

dia.Paper.startLinkDrag(link, { end = 'target', whenNotAllowed, ...batchData })dia.LinkDrag

Sugar for new dia.LinkDrag(paper, link, opt). Takes a link model only (a LinkView could belong to another paper).

  • The link may not be in the graph yet: it is added in an add-link batch and whenNotAllowed defaults to 'remove'. A link already in the graph is wrapped in an arrowhead-move batch (default 'revert'), like linkTools.Arrowhead.
  • Extra options flow into the batch data (JointJS convention), e.g. { ui: true, tool: cid } from the tools.
  • The view is resolved with requireView(), so it works on an async paper before the view is rendered. This is why the entry point lives on the paper and not on a view.

dia.LinkDrag

  • move(evt) / move(evt, x, y) / move(x, y), same for finish(); cancel(), isActive(), link, linkView, end.

  • followPointer({ finishOn })Promise<{ cancelled, linkView }>. Binds document pointermove, pointerdown / pointerup, keydown Escape and contextmenu (prevented + cancel). finishOn:

    • 'pointerup' (default) – primary release, press-drag-release
    • 'pointerdown' – primary click, click-move-click
    • 'connection' – primary click over a valid magnet (validateConnection), other clicks are ignored
    • (evt, linkDrag) => boolean – called on every pointerdown / pointerup, full control (e.g. add a vertex on a blank click, finish on a magnet)

    The finishing event first moves the end to its position, so the connection is evaluated where the user clicked even without a preceding pointermove.

  • getConnectionCandidate(){ cellView, magnet } | null – the validated magnet the end would connect to right now (under the pointer, or the closest one with snapLinks). Suspends paper events while active and restores them afterwards, exactly as the tools do. Calling it twice returns the same promise; on a finished handle it resolves immediately.

  • External link.remove() mid-drag cleans up (highlighters, pointer-events, z, available-magnet marks) and resolves with cancelled: true.

Internal consumers ported onto LinkDrag

  • CellView.dragLinkStart / dragLink / dragLinkEnd (paper magnet dragging), elementTools.Connect / linkTools.Connect (+ HoverConnect) and linkTools.Arrowhead. Paper event sequences (link:pointerdown/move/up, link:connect/disconnect, mouseleave) are unchanged and covered by the existing tests.
  • One observable change: the add-link / arrowhead-move batch now stops right after the end is connected, i.e. before link:pointerup is triggered (it used to stop after it). Flagged in the changeset.

Other changes

  • dia.LinkView.cancelArrowheadMove(data) – counterpart of startArrowheadMove(); reverts/removes only if the link is still in the graph.
  • dia.CellView.createLinkFromMagnet(magnet, x, y) – builds the default link with its source set through connectionStrategy without adding it to the graph; addLinkFromMagnet() uses it. Both, and getLinkEnd(), are public in the type definitions.

Not in this PR (follow-ups)

  • Per-drag overrides of snapLinks / snapLinksSelf / markAvailable.
  • Docs.

Test plan

  • test/jointjs/dia/linkDrag.js – 35 QUnit tests (manual control incl. the (evt, x, y) form, batch data, getConnectionCandidate() plain and with snapLinks, link resolution incl. async paper, magnet on element and on link, external removal, followPointer with real PointerEvent / KeyboardEvent dispatch incl. the 'connection' mode and the predicate form, listener cleanup). karma:joint: 2148 pass.
  • Existing linkTools.js / elementTools.js / paper.js tests for Arrowhead, Connect and magnet dragging pass unchanged.
  • yarn test-ts with a new usage snippet in test/ts/index.test.ts, yarn test-server, yarn lint.
  • Manual check in a browser demo (press-drag-release and click-move-click, Escape, right click).

🤖 Generated with Claude Code

kumilingus and others added 6 commits September 12, 2026 16:13
Adds `paper.startLinkDrag(link, { end, whenNotAllowed })` returning a
`dia.LinkDrag` handle. The handle is driven either manually (`move()`,
`finish()`, `cancel()`) or by the pointer (`followPointer({ finishOn })`),
which covers press-drag-release as well as click-move-click flows and
cancels on Escape / context menu.

The link may not be in the graph yet (it is added in an `add-link` batch)
and its view may not be rendered yet (async paper), so the entry point
lives on the paper rather than on a view.

`LinkView.cancelArrowheadMove()` is added as the counterpart of
`startArrowheadMove()`. `CellView.addLinkFromMagnet()` and `getLinkEnd()`
become public in the type definitions so a link can be created from a
magnet of an element or a link before starting the drag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…through LinkDrag

`LinkDrag` takes `(paper, link, opt)` and owns the whole lifecycle (batch,
adding the link, `requireView`, `startArrowheadMove`). `move()` and
`finish()` accept `(evt, x, y)` like every other JointJS handler, and extra
`startLinkDrag()` options flow into the batch data.

`CellView.dragLinkStart/dragLink/dragLinkEnd`, `cellTools.Connect` (both
`elementTools.Connect` and `linkTools.Connect`) and `linkTools.Arrowhead`
now use it. `CellView.createLinkFromMagnet()` builds the link without
adding it so the add still lands in the `add-link` batch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds `updateArrowheadMove(data, evt, x, y)` and `finishArrowheadMove(data,
evt, x, y)` next to `startArrowheadMove()` / `cancelArrowheadMove()`, so the
move state is passed explicitly instead of being looked up in `evt.data`.
`dragArrowhead()` / `dragArrowheadEnd()` are thin wrappers over them, and
`LinkDrag` no longer needs to know the `View.eventData()` key format.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kumilingus
kumilingus force-pushed the feat/paper-start-link-drag branch from 89c140a to 493cca6 Compare September 12, 2026 15:12
…ate and getConnectionCandidate()

`followPointer({ finishOn })` now accepts `'connection'` (a primary click
over a valid magnet finishes, other clicks are ignored) and a function
`(evt, linkDrag) => boolean` called on every pointerdown / pointerup. The
finishing event first moves the end to its position so the candidate is
evaluated where the user clicked.

`getConnectionCandidate()` exposes the validated magnet the end would
connect to (`magnetUnderPointer`, or the closest one with `snapLinks`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant