<div id="stencil-container"></div>
<div id="paper-container"></div>
<div id="inspector-container">
  <div class="custom-fields"></div>
</div>
<div id="toolbar-container"></div>
<a target="_blank" href="https://www.jointjs.com">
  <img id="logo" src="https://assets.codepen.io/7589991/jointjs-logo.svg" width="200" height="50"></img>
</a>
#stencil-container {
  position: absolute;
  left: 0;
  top: 0;
  width: 100px;
  bottom: 0;
}

#paper-container {
  position: absolute;
  right: 250px;
  top: 0;
  left: 100px;
  bottom: 0;

  .selection-wrapper[data-selection-length="1"] {
    display: block;
    padding: 6px;
    margin: -8px;

    ~ .selection-box {
      display: none;
    }
  }
}

#inspector-container {
  position: absolute;
  right: 0;
  top: 0;
  width: 250px;
  bottom: 0;
  border-left: 1px solid #d3d3d3;

  .joint-inspector {
    background: #fcfcfc;
    position: relative;
  }
}

#toolbar-container {
  position: absolute;
  top: 20px;
  right: 270px;

  .joint-toolbar {
    margin: 0;
    padding: 4px;
  }
}

#logo {
  position: absolute;
  bottom: 20px;
  right: 0;
}
View Compiled
const { dia, ui, shapes: defaultShapes, linkTools } = joint;

class LinkElement extends dia.Element {
  defaults() {
    return {
      ...super.defaults,
      type: "LinkElement",
      attrs: {
        body: {
          width: "calc(w)",
          height: "calc(h)",
          fill: "white",
          stroke: "#333333",
          strokeWidth: 2
        },
        label: {
          text: "Links",
          fontSize: 13,
          fontFamily: "sans-serif",
          fill: "#333333",
          x: "calc(0.5*w)",
          y: "calc(0.5*h)",
          textAnchor: "middle",
          textVerticalAnchor: "middle",
          textWrap: {
            ellipsis: true,
            width: -10,
            height: -20
          }
        }
      },
      size: {
        width: 80,
        height: 80
      }
    };
  }

  preinitialize() {
    this.markup = [
      {
        tagName: "rect",
        selector: "body"
      },
      {
        tagName: "text",
        selector: "label"
      }
    ];
  }
}

const shapes = { ...defaultShapes, LinkElement };

// Paper

const graph = new dia.Graph({}, { cellNamespace: shapes });
const paper = new dia.Paper({
  model: graph,
  cellViewNamespace: shapes,
  width: "100%",
  height: "100%",
  gridSize: 20,
  drawGrid: { name: "mesh" },
  async: true,
  sorting: dia.Paper.sorting.APPROX,
  background: { color: "#F3F7F6" },
  restrictTranslate: true,
  clickThreshold: 10
});

document.getElementById("paper-container").appendChild(paper.el);

// Stencil

const stencil = new ui.Stencil({
  paper,
  usePaperGrid: true,
  width: 100,
  height: "100%",
  paperOptions: () => {
    return {
      model: new dia.Graph({}, { cellNamespace: shapes }),
      cellViewNamespace: shapes,
      background: {
        color: "#FCFCFC"
      }
    };
  },
  layout: {
    columns: 1,
    rowHeight: "compact",
    rowGap: 10,
    columnWidth: 100,
    marginY: 10,
    // reset defaults
    resizeToFit: false,
    dx: 0,
    dy: 0
  }
});

stencil.render();
document.getElementById("stencil-container").appendChild(stencil.el);

stencil.load([
  {
    type: "LinkElement"
  }
]);

// Selection

const selection = new ui.Selection({
  paper,
  useModelGeometry: true,
  theme: "material",
  boxContent: false
});

selection.removeHandle("resize");
selection.removeHandle("rotate");
selection.removeHandle("remove");

paper.on("element:pointerclick", function (elementView) {
  selection.collection.reset([elementView.model]);
});

paper.on("blank:pointerclick", function () {
  selection.collection.reset([]);
});

stencil.on("element:drop", function (elementView) {
  const element = elementView.model;
  selection.collection.reset([element]);
});

selection.collection.on("reset", function () {
  openInspector();
});

// Inspector

let _inspector = null;

function openInspector() {
  const container = document.getElementById("inspector-container");

  if (_inspector) {
    _inspector.remove();
    _inspector = null;
  }

  const cells = selection.collection.models;
  if (cells.length !== 1) return;

  const [cell] = cells;
  const type = cell.get("type");

  const inputs = {
    LinkElement: {
      "attrs/label/text": {
        label: "Text",
        type: "content-editable"
      },
      links: {
        type: "list",
        label: "List of links",
        item: {
          type: "object",
          properties: {
            url: {
              type: "text",
              label: "URL",
              index: 1
            },
            name: {
              type: "text",
              label: "Link",
              index: 2
            }
          }
        }
      }
    }
  };

  const inspector = new ui.Inspector({
    cell,
    theme: "material",
    inputs: inputs[type]
  });

  container.append(inspector.el);
  inspector.render();

  _inspector = inspector;
}

// Example

const te1 = new LinkElement({
  position: { x: 100, y: 100 },
  attrs: {
    label: {
      text: "Products"
    }
  },
  links: [
    {
      name: "JointJS",
      url: "https://www.jointjs.com"
    },
    {
      name: "AppMixer",
      url: "https://www.appmixer.com"
    }
  ]
});

graph.addCells([te1]);

selection.collection.reset([te1]);

// Command Manager

const history = new dia.CommandManager({
  graph
});

history.on("stack:undo stack:redo", function () {
  selection.collection.reset(
    selection.collection.filter((cell) => graph.getCell(cell))
  );
});

// Toolbar

const toolbar = new ui.Toolbar({
  autoToggle: true,
  tools: ["undo", "redo"],
  references: {
    commandManager: history
  }
});

document.getElementById("toolbar-container").appendChild(toolbar.render().el);

new ui.Tooltip({
  theme: "material",
  rootTarget: paper.el,
  target: "[data-tooltip]",
  direction: "auto",
  padding: 20,
  animation: true
});

function setupLinks(paper) {
  const { model: graph } = paper;

  const LinkHighlighter = dia.HighlighterView.extend({
    MOUNTABLE: true,
    tagName: "g",
    attributes: {
      event: "element:links:click",
      "data-tooltip": "Click to open links"
    },
    children: [
      {
        tagName: "circle",
        attributes: {
          r: 10,
          fill: "#0057FF",
          stroke: "#333",
          "stroke-width": 2
        }
      },
      {
        tagName: "text",
        textContent: "→",
        attributes: {
          x: 0,
          y: ".3em",
          fill: "#ffffff",
          "font-size": 15,
          "font-family": "monospace",
          "text-anchor": "middle"
        }
      }
    ],
    highlight: function (cellView) {
      const { width, height } = cellView.model.size();
      this.renderChildren();
      this.el.setAttribute(
        "transform",
        `translate(${width - 20},${height - 5})`
      );
    }
  });

  function toggleLinkHighlighter(el) {
    if (el.isLink()) return;
    const { links = [] } = el.attributes;
    const highlighter = LinkHighlighter.get(el.findView(paper), "links");
    if (links.length === 0) {
      if (highlighter) highlighter.remove();
    } else {
      if (highlighter) return;
      LinkHighlighter.add(el.findView(paper), "root", "links");
    }
  }

  graph.on("add change:links", function (el) {
    toggleLinkHighlighter(el);
  });

  graph.on("reset", function (el) {
    graph.getElements().forEach((el) => toggleLinkHighlighter(el));
  });

  graph.getElements().forEach((el) => toggleLinkHighlighter(el));

  paper.on("element:links:click", function (elementView, evt) {
    evt.stopPropagation();
    const { links = [] } = elementView.model.attributes;
    const tools = links
      .filter((link) => link.url)
      .map((link) => {
        return {
          action: link.url,
          content: link.name || link.url
        };
      });
    if (tools.length === 0) return;
    const contextMenu = new ui.ContextToolbar({
      target: evt.target,
      vertical: true,
      tools
    });
    contextMenu.render();
    contextMenu.on("all", function (url, evt) {
      if (url === "close") return;
      evt.stopPropagation();
      contextMenu.remove();
      window.open(url.slice("action:".length), "_blank");
    });
  });
}

setupLinks(paper);
View Compiled

External CSS

  1. https://resources.jointjs.com/demos/rappid/build/package/rappid.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone-min.js
  4. https://resources.jointjs.com/demos/rappid/build/package/rappid.js