A Content Driven Element

The size of this custom element is derived from its content (the size of the text and an image if present).

by Roman Bruckner

HTML

<html>
<head>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.4.4/joint.css" />
</head>
<body>
      <!-- content -->
      <div id="paper"></div>

      <!-- dependencies -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.4.4/joint.js"></script>
    
    </body>
</html>

JavaScript

const { dia, shapes } = joint;

const graph = new dia.Graph({}, {
  cellNamespace: shapes
});

const paper = new dia.Paper({
  el: document.getElementById('paper'),
  width: 600,
  height: 600,
  gridSize: 20,
  model: graph,
  async: true,
  sorting: joint.dia.Paper.sorting.APPROX,
  cellViewNamespace: joint.shapes
});

const svg = paper.svg;

function measureText(svgDocument, text, attrs) {
  const vText = V('text').attr(attrs).text(text);
  vText.appendTo(svgDocument);
  const bbox = vText.getBBox();
  vText.remove();
  return bbox;
}

class Shape extends joint.dia.Element {

  defaults() {
    return {
      ...super.defaults,
      type: 'custom.Shape',
      fillColor: 'red',
      outlineColor: 'blue',
      label: '',
      image: ''
    };
  }

  preinitialize() {
    this.spacing = 10;
    this.labelAttributes = {
      'font-size': 14,
      'font-family': 'sans-serif',
    };
    this.imageAttributes = {
      'width': 50,
      'height': 50,
      'preserveAspectRatio': 'none'
    };
    this.cache = {};
  }

  initialize() {
    super.initialize();
    this.on('change', this.onAttributeChange);
    this.setSizeFromContent();
  }

  /* Attributes that affects the size of the model. */
  onAttributeChange() {
    const {
      changed,
      cache
    } = this;
    if ('label' in changed) {
      // invalidate the cache only if the text of the `label` has changed
      delete cache.label;
    }
    if ('label' in changed || 'image' in changed) {
      this.setSizeFromContent();
    }
  }

  setSizeFromContent() {
    delete this.cache.layout;
    const {
      width,
      height
    } = this.layout();
    this.resize(width, height);
  }

  layout() {
    const {
      cache
    } = this;
    let {
      layout
    } = cache;
    if (layout) {
      return layout;
    } else {
      const layout = this.calcLayout();
      cache.layout = layout;
      return layout;
    }
  }

  calcLayout() {
    const {
      attributes,
      labelAttributes,
     ...