Search code examples
javascriptjqueryjointjs

How to restrict linking between ports of elements if one link already exist between them in JointJS?


I have 3 elements with ports. My goal is to not allow linking ports between source and destination elements if one of their ports are already linked to each other. Please see my visualization below:

enter image description here enter image description here

As you can see, I need only 1 to 1 connection between cells. How do I acheive this with JointJS? Please see my JSFiddle.

HTML

<html>
  <body>
    <button id="btnAdd">Add Table</button>
    <div id="dbLookupCanvas"></div>
  </body>
</html>

JS

$(document).ready(function() {
  $('#btnAdd').on('click', function() {
    AddTable();
  });

  InitializeCanvas();

  // Adding of two sample tables on first load
  AddTable(50, 50);
  AddTable(250, 50);
  AddTable(150, 180);
});

var graph;
var paper
var selectedElement;
var namespace;

function InitializeCanvas() {
  let canvasContainer = $('#dbLookupCanvas').parent();

  namespace = joint.shapes;

  graph = new joint.dia.Graph({}, {
    cellNamespace: namespace
  });

  paper = new joint.dia.Paper({
    el: document.getElementById('dbLookupCanvas'),
    model: graph,
    width: canvasContainer.width(),
    height: 500,
    gridSize: 10,
    drawGrid: true,
    cellViewNamespace: namespace,
    validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
      // Prevent link to self
      if (cellViewS === cellViewT)
        return false;

      // Prevent linking from input ports
      if ((magnetS !== magnetT))
        return true;
    },
    snapLinks: {
      radius: 20
    },
    defaultLink: () => new joint.shapes.standard.Link({
      router: {
        name: 'manhattan'
      },
      connector: {
        name: 'normal'
      },
      attrs: {
        line: {
          stroke: 'black',
          strokeWidth: 1,
          sourceMarker: {
            'type': 'path',
            'stroke': 'black',
            'fill': 'black',
            'd': 'M 10 -5 0 0 10 5 Z'
          },
          targetMarker: {
            'type': 'path',
            'stroke': 'black',
            'fill': 'black',
            'd': 'M 10 -5 0 0 10 5 Z'
          }
        }
      }
    })
  });

  //Dragging navigation on canvas
  var dragStartPosition;
  paper.on('blank:pointerdown',
    function(event, x, y) {
      dragStartPosition = {
        x: x,
        y: y
      };
    }
  );

  paper.on('cell:pointerup blank:pointerup', function(cellView, x, y) {
    dragStartPosition = null;
  });

  $("#dbLookupCanvas")
    .mousemove(function(event) {
      if (dragStartPosition)
        paper.translate(
          event.offsetX - dragStartPosition.x,
          event.offsetY - dragStartPosition.y);
    });

  // Remove links not connected to anything
  paper.model.on('batch:stop', function() {
    var links = paper.model.getLinks();
    _.each(links, function(link) {
      var source = link.get('source');
      var target = link.get('target');
      if (source.id === undefined || target.id === undefined) {
        link.remove();
      }
    });
  });

  paper.on('cell:pointerdown', function(elementView) {
    resetAll(this);
    let isElement = elementView.model.isElement();

    if (isElement) {
      var currentElement = elementView.model;
      currentElement.attr('body/stroke', 'orange');
      selectedElement = elementView.model;
    } else
      selectedElement = null;
  });

  paper.on('blank:pointerdown', function(elementView) {
    resetAll(this);
  });

  $('#dbLookupCanvas')
    .attr('tabindex', 0)
    .on('mouseover', function() {
      this.focus();
    })
    .on('keydown', function(e) {
      if (e.keyCode == 46)
        if (selectedElement) selectedElement.remove();
    });
}

function AddTable(xCoord = undefined, yCoord = undefined, portID = undefined) {
  // This is a sample database data here
  let data = [{
      columnName: "radomData1"
    },
    {
      columnName: "radomData2"
    }
  ];

  if (xCoord == undefined && yCoord == undefined) {
    xCoord = 50;
    yCoord = 50;
  }

  const rect = new joint.shapes.standard.Rectangle({
    position: {
      x: xCoord,
      y: yCoord
    },
    size: {
      width: 150,
      height: 200
    },
    ports: {
      groups: {
        'a': {},
        'b': {}
      }
    }
  });

  $.each(data, (i, v) => {
    const port = {
      group: 'a',
      args: {}, // Extra arguments for the port layout function, see `layout.Port` section
      label: {
        position: {
          name: 'right',
          args: {
            y: 6
          } // Extra arguments for the label layout function, see `layout.PortLabel` section
        },
        markup: [{
          tagName: 'text',
          selector: 'label'
        }]
      },
      attrs: {
        body: {
          magnet: true,
          width: 16,
          height: 16,
          x: -8,
          y: -4,
          stroke: 'red',
          fill: 'gray'
        },
        label: {
          text: v.columnName,
          fill: 'black'
        }
      },
      markup: [{
        tagName: 'rect',
        selector: 'body'
      }]
    };

    rect.addPort(port);
  });

  rect.resize(150, data.length * 40);

  graph.addCell(rect);
}

function resetAll(paper) {
  paper.drawBackground({
    color: 'white'
  });

  var elements = paper.model.getElements();
  for (var i = 0, ii = elements.length; i < ii; i++) {
    var currentElement = elements[i];
    currentElement.attr('body/stroke', 'black');
  }

  var links = paper.model.getLinks();
  for (var j = 0, jj = links.length; j < jj; j++) {
    var currentLink = links[j];
    currentLink.attr('line/stroke', 'black');
    currentLink.label(0, {
      attrs: {
        body: {
          stroke: 'black'
        }
      }
    });
  }
}

Any help would be appreciated. Thanks!


Solution

  • If you add another condition to validateConnection, it should do the trick.

    If you compare the links of the source and target, then return false if they already contain a link with the same ID.

    The following seems to work well in your JSFiddle.

     validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
          // Prevent link on body
          if (magnetT == null)
            return false;
    
          // Prevent link to self
          if (cellViewS === cellViewT)
            return false;
            
          const sourceCell = cellViewS.model;
          const sourceLinks = graph.getConnectedLinks(sourceCell);
    
          const targetCell = cellViewT.model;
          const targetLinks = graph.getConnectedLinks(targetCell);
    
          let isConnection;
    
          // Compare link IDs of source and target elements
          targetLinks.forEach((linkT) => {
            sourceLinks.forEach((linkS) => {
              if (linkS.id === linkT.id) isConnection = true;
            });
          });
    
          // If source and target already contain a link with the same id , return false
          if (isConnection) return false;
    
          // Prevent linking from input ports
          if ((magnetS !== magnetT))
            return true;
        },