Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
13-02-2026 (v4.2.4)

@joint/core
* dia.Paper - fix to handle element removal during pointer events
* dia.Graph - fix to pass options to `batch:start` and `batch:stop` events consistently
* mvc.Model - fix to trigger the `changeId` event only if the previous ID is different from the current ID
* Vectorizer - fix `getTransformToElement()` when the target node is inside a nested SVG document

21-01-2026 (v4.2.3)

@joint/core
Expand Down
40 changes: 40 additions & 0 deletions examples/absolute-port-layout-dynamic-port-sizes-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# JointJS: Absolute Port Layout & Dynamic Port Sizes

Do you need the size of element ports to be driven by the text they contain? Do you want to position ports based on dynamic factors? This demo shows exactly that.

## Install

From the root of the monorepo, install all dependencies:

```bash
yarn install
yarn run build
````

## Development

Run the development server from this example directory:

```bash
yarn dev
```

Then open the URL printed in the terminal (usually `http://localhost:5173`).

## Build

Create a production build:

```bash
yarn build
```

The output will be generated in the `dist/` directory.

## Preview

Preview the production build locally:

```bash
yarn preview
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions examples/absolute-port-layout-dynamic-port-sizes-js/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="See how to make the size of element ports to be driven by the text they contain.">
<title>JointJS: Absolute Port Layout & Dynamic Port Sizes</title>
</head>

<body id="app">
<div id="paper-container"></div>
<button id="add-port">Add Port</button>
<button id="remove-port">Remove Port</button>
<a target="_blank" href="https://www.jointjs.com">
<img id="logo" src="./assets/jointjs-logo-black.svg" width="200" height="50"></img>
</a>
<!-- Application files: -->
<script type="module" src="/src/main.js"></script>
</body>

</html>
17 changes: 17 additions & 0 deletions examples/absolute-port-layout-dynamic-port-sizes-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@joint/demo-absolute-port-layout-dynamic-port-sizes-js",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^7.3.1"
},
"dependencies": {
"@joint/core": "workspace:^"
}
}
276 changes: 276 additions & 0 deletions examples/absolute-port-layout-dynamic-port-sizes-js/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { V, dia, shapes as defaultShapes, anchors, util } from '@joint/core';
import './styles.css';

class Shape extends dia.Element {
defaults() {
return {
...super.defaults,
type: 'Shape',
size: {
width: 120,
height: 60
},
attrs: {
root: {
cursor: 'move'
},
body: {
fill: '#f2f1ed',
stroke: '#4b557d',
strokeWidth: 2,
d:
'M 0 calc(h) H calc(w) V 4 a 4 4 1 0 0 -4 -4 H 4 a 4 4 1 0 0 -4 4 z M 0 calc(h-4) H calc(w)'
},
label: {
text: 'Custom shape with dynamic port size',
textWrap: { width: -20, height: -10, ellipsis: true },
fontSize: 15,
fontFamily: 'sans-serif',
fill: '#4b557d',
textVerticalAnchor: 'middle',
textAnchor: 'middle',
x: 'calc(0.5*w)',
y: 'calc(0.5*h-2)'
}
},
ports: {
groups: {
out: {
z: -1,
position: 'absolute',
markup: [
{
tagName: 'rect',
selector: 'portBody'
},
{
tagName: 'text',
selector: 'portLabel'
}
],
attrs: {
portBody: {
width: 'calc(w)',
height: 'calc(h + 4)',
fill: '#7088eb',
stroke: '#4666E5',
strokeWidth: 2,
rx: 4,
ry: 5,
y: -4,
magnet: true,
cursor: 'crosshair'
},
portLabel: {
x: 'calc(0.5 * w)',
y: 'calc(0.5 * h)',
textAnchor: 'middle',
textVerticalAnchor: 'middle',
textWrap: {
width: -this.portPadding / 2,
ellipsis: true
},
pointerEvents: 'none',
fill: '#ffffff',
...this.portFontAttributes
}
}
}
}
}
};
}

preinitialize() {
this.minWidth = 100;
this.portPadding = 10;
this.portGap = 10;
this.portHeight = 20;
this.portFontAttributes = {
'font-size': 14,
'font-family': 'sans-serif'
};
this.markup = [
{
tagName: 'path',
selector: 'body'
},
{
tagName: 'text',
selector: 'label'
}
];
}

initialize() {
super.initialize();
if (!this.constructor.svgDocument) {
throw new Error('SVG Document not provided.');
}
this.on('change', this.onAttributeChange);
this.setOutPorts();
}

onAttributeChange(change, opt) {
if (opt.shape === this.id) return;
if ('outPorts' in this.changed) {
this.setOutPorts();
}
}

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

setOutPorts(opt = {}) {
const {
attributes,
portPadding,
portGap,
portHeight,
portFontAttributes,
minWidth,
constructor
} = this;
const { outPorts = [], size, ports } = attributes;
let x = 0;
const items = outPorts.map((port) => {
const { id, label = 'Port' } = port;
let { width } = this.measureText(
constructor.svgDocument,
label,
portFontAttributes
);
width += 2 * portPadding;
const item = {
id,
group: 'out',
size: { width, height: portHeight },
args: { x, y: '100%' },
attrs: {
portLabel: {
text: label
}
}
};
x += width + portGap;
return item;
});
this.set(
{
ports: {
...ports,
items
},
size: {
...size,
width: Math.max(x - portGap, minWidth)
}
},
{ ...opt, shape: this.id }
);
}

addOutPort(port, opt = {}) {
const { outPorts = [] } = this.attributes;
this.set('outPorts', [...outPorts, port], opt);
}

removeLastOutPort(opt = {}) {
const { outPorts = [] } = this.attributes;
this.set('outPorts', outPorts.slice(0, outPorts.length - 1), opt);
}

static svgDocument = null;
}

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

// Paper

const paperContainer = document.getElementById('paper-container');

const graph = new dia.Graph({}, { cellNamespace: shapes });
const paper = new dia.Paper({
model: graph,
cellViewNamespace: shapes,
width: '100%',
height: '100%',
gridSize: 20,
async: true,
sorting: dia.Paper.sorting.APPROX,
background: { color: '#F3F7F6' },
linkPinning: false,
defaultLink: () =>
new shapes.standard.Link({
attrs: {
line: {
stroke: '#4666E5'
}
}
}),
validateConnection: (sv, _, tv) => {
if (sv.model.isLink() || tv.model.isLink()) return false;
return sv !== tv;
},
defaultConnectionPoint: { name: 'anchor' },
defaultAnchor: (view, magnet, ...rest) => {
const anchorFn = view.model instanceof Shape ? anchors.bottom : anchors.top;
return anchorFn(view, magnet, ...rest);
},
defaultConnector: {
name: 'curve'
}
});
paperContainer.appendChild(paper.el);

paper.setGrid('mesh');

Shape.svgDocument = paper.svg;

const words = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed convallis lacinia nibh. Sed posuere felis sit amet porttitor sollicitudin. Sed lorem felis, semper at volutpat eget, accumsan mollis quam. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nullam volutpat sodales sapien, et iaculis mauris pulvinar vel. Fusce in interdum nisi. Donec vel ultricies lectus. Suspendisse mi nisl, vulputate sed scelerisque quis, porttitor ut enim. Praesent augue ligula, interdum sit amet pulvinar ac, tincidunt ut dolor. Vivamus luctus eget ipsum ac eleifend. Suspendisse lorem enim, hendrerit in semper in, porttitor id nulla. Pellentesque iaculis risus ac purus efficitur, id elementum velit hendrerit. Ut nisl mi, ornare eu consectetur congue, placerat at nulla.'.split(
' '
);

function getRandomWord() {
return words[Math.floor(Math.random() * words.length)];
}

function getRandomPort() {
return {
id: util.uuid(),
label: getRandomWord()
};
}

const shape = new Shape({
outPorts: [getRandomPort(), getRandomPort(), getRandomPort()]
});

shape.position(100, 100).addTo(graph);

const target = new shapes.standard.Ellipse({
size: { width: 50, height: 50 },
attrs: {
root: {
highlighterSelector: 'body'
},
body: {
stroke: '#705d10',
fill: '#efdc8f'
}
}
});
target.position(150, 300).addTo(graph);

document.getElementById('add-port').addEventListener('click', () => {
shape.addOutPort(getRandomPort());
});

document.getElementById('remove-port').addEventListener('click', () => {
shape.removeLastOutPort();
});
Loading