From 100ef6c714a6f8a085de1c77aa3326d7d195be09 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Thu, 2 Jul 2026 01:09:43 +0200 Subject: [PATCH] feat(dia.Graph): add `port` and `magnet` options to getConnectedLinks `graph.getConnectedLinks(element, { inbound: true, port: 'xy', magnet: '.body' })` restricts the result to links attached to the element at the given port and/or magnet (selector). Both are matched on the direction-appropriate end (source for outbound, target for inbound), so a self-loop isn't matched via its other end when only one direction is requested. `port` and `magnet` combine with AND, and compose with the existing inbound/outbound flags. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SYaFvnrAk29uu4UdjwoAdK --- packages/joint-core/src/dia/Graph.mjs | 18 ++++ packages/joint-core/test/jointjs/graph.js | 109 ++++++++++++++++++++++ packages/joint-core/types/dia.d.ts | 4 + 3 files changed, 131 insertions(+) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 6e4252a0ae..950ac0d7f8 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -967,6 +967,24 @@ export const Graph = Model.extend({ }, this); } + // Restrict to links attached to `model` at a specific port and/or magnet. Matched on + // the direction-appropriate end (source for outbound, target for inbound), so a + // self-loop is not matched via its other end when only one direction is requested. + if (opt.port !== undefined || opt.magnet !== undefined) { + const { port, magnet } = opt; + const modelId = model.id; + const endMatches = (end) => { + if (end.id !== modelId) return false; + if (port !== undefined && end.port !== port) return false; + if (magnet !== undefined && end.magnet !== magnet) return false; + return true; + }; + links = links.filter((link) => + (outbound && endMatches(link.source())) || + (inbound && endMatches(link.target())) + ); + } + return links; }, diff --git a/packages/joint-core/test/jointjs/graph.js b/packages/joint-core/test/jointjs/graph.js index 39bfc79460..c19b970652 100644 --- a/packages/joint-core/test/jointjs/graph.js +++ b/packages/joint-core/test/jointjs/graph.js @@ -1018,6 +1018,115 @@ QUnit.module('graph', function(hooks) { ); }); + QUnit.test('graph.getConnectedLinks() with port/magnet options', function(assert) { + var graph = this.graph; + + new joint.shapes.standard.Rectangle({ + id: 'a', + ports: { items: [{ id: 'in' }, { id: 'out' }] }, + }).addTo(graph); + new joint.shapes.standard.Rectangle({ id: 'b' }).addTo(graph); + + // a.out -> b + new joint.shapes.standard.Link({ + id: 'l1', + source: { id: 'a', port: 'out' }, + target: { id: 'b' }, + }).addTo(graph); + // a.out -> b (second link on the same port) + new joint.shapes.standard.Link({ + id: 'l2', + source: { id: 'a', port: 'out' }, + target: { id: 'b' }, + }).addTo(graph); + // b -> a.in + new joint.shapes.standard.Link({ + id: 'l3', + source: { id: 'b' }, + target: { id: 'a', port: 'in' }, + }).addTo(graph); + // b -> a (no port) + new joint.shapes.standard.Link({ + id: 'l4', + source: { id: 'b' }, + target: { id: 'a' }, + }).addTo(graph); + // a.out -> a.in (self-loop across two ports) + new joint.shapes.standard.Link({ + id: 'l5', + source: { id: 'a', port: 'out' }, + target: { id: 'a', port: 'in' }, + }).addTo(graph); + // a(.body magnet) -> b + new joint.shapes.standard.Link({ + id: 'l6', + source: { id: 'a', magnet: '.body' }, + target: { id: 'b' }, + }).addTo(graph); + // a(out port + .body magnet) -> b + new joint.shapes.standard.Link({ + id: 'l7', + source: { id: 'a', port: 'out', magnet: '.body' }, + target: { id: 'b' }, + }).addTo(graph); + + var a = graph.getCell('a'); + var ids = function(links) { return _.sortBy(_.map(links, 'id')); }; + + // Port. + assert.deepEqual( + ids(graph.getConnectedLinks(a, { port: 'out' })), + ['l1', 'l2', 'l5', 'l7'], + 'port matches links attached to the element at that port, either end.', + ); + assert.deepEqual( + ids(graph.getConnectedLinks(a, { outbound: true, port: 'out' })), + ['l1', 'l2', 'l5', 'l7'], + 'outbound + port narrows to links leaving the given port.', + ); + assert.deepEqual( + ids(graph.getConnectedLinks(a, { inbound: true, port: 'in' })), + ['l3', 'l5'], + 'inbound + port narrows to links arriving at the given port.', + ); + assert.deepEqual( + ids(graph.getConnectedLinks(a, { outbound: true, port: 'in' })), + [], + 'outbound + input port yields no links (outbound attaches at the source end).', + ); + assert.deepEqual( + ids(graph.getConnectedLinks(a, { port: 'missing' })), + [], + 'unknown port yields no links.', + ); + + // Magnet. + assert.deepEqual( + ids(graph.getConnectedLinks(a, { magnet: '.body' })), + ['l6', 'l7'], + 'magnet matches links attached to the element at that magnet.', + ); + + // Port + magnet (AND). + assert.deepEqual( + ids(graph.getConnectedLinks(a, { port: 'out', magnet: '.body' })), + ['l7'], + 'port + magnet requires the end to match both.', + ); + assert.deepEqual( + ids(graph.getConnectedLinks(a, { port: 'in', magnet: '.body' })), + [], + 'port + magnet with no end matching both yields no links.', + ); + + // Unchanged when neither option is given. + assert.deepEqual( + ids(graph.getConnectedLinks(a)), + ['l1', 'l2', 'l3', 'l4', 'l5', 'l6', 'l7'], + 'no port/magnet option leaves behavior unchanged.', + ); + }); + QUnit.test('graph.getConnectedLinks()', function(assert) { var graph = this.graph; this.setupTestMixtureGraph(graph); diff --git a/packages/joint-core/types/dia.d.ts b/packages/joint-core/types/dia.d.ts index 70c502b49d..a53bc8e25a 100644 --- a/packages/joint-core/types/dia.d.ts +++ b/packages/joint-core/types/dia.d.ts @@ -258,6 +258,10 @@ export namespace Graph { interface ConnectionOptions extends Cell.EmbeddableOptions { inbound?: boolean; outbound?: boolean; + /** Restrict to links attached to the element at this port. */ + port?: string; + /** Restrict to links attached to the element at this magnet (selector). */ + magnet?: string; } interface ExploreOptions extends ConnectionOptions {