javascriptjqueryjointjs

How to add a header/title in a rectangle using JointJS without overlapping with the ports?


I have a rectangle which has multiple ports. I wanted to add a title on top to name the rectangle just like this.

enter image description here

Essentially, I am making and ERD-like design with each ports representing a table column. I utilized the Headered Rectangle from the docs but the problem is that the ports overlaps with the header.

enter image description here

Is there any way to dynamically adjust the position of the port just to be right under the header and don't overlap? Here's my code in 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);
});

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) {
      return (magnetS !== magnetT);
    },
    snapLinks: {
      radius: 20
    }
  });

  //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) {
  // This is a sample database data here
  let data = [{
      columnName: "radomData1"
    },
    {
      columnName: "radomData2"
    },
    {
      columnName: "radomData3"
    }
  ];

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

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

  rect.attr('root/title', 'joint.shapes.standard.HeaderedRectangle');
  rect.attr('headerText/text', 'Sample Header');

  $.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

  • You can take advantage of ports layout. I think the line layout would solve the issue. layout.Port documentation reference

    In your JSFiddle, something like the following seems to work well:

     const rect = new joint.shapes.standard.HeaderedRectangle({
        position: {
          x: xCoord,
          y: yCoord
        },
        size: {
          width: 150,
          height: 200
        },
        ports: {
          groups: {
            'a': {
                position: {
                 name: 'line',
                 args: {
                   start: { x: 0, y: 30 },
                   end: { x:0, y: 110 }
                 }
              }
            },
            'b': {}
          }
        }
      });