Skip to main content

JointJS+ changelog v3.7.0

Improve support for foreign objects in SVG

This is the main change of this release. While previously HTML control elements had to be treated as a separate functional layer (HTML elements independent from the SVG diagram which had to be kept in sync behind the scenes), JointJS now supports embedding HTML directly in SVG via <foreignObject> elements.

To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

Details...

In order to enable this functionality, adjustments were made to every step along the way of working with JointJS diagrams - from parsing of JSON through diagram interaction to diagram export.

Most changes were done at the first step - parsing of SVG with <foreignObject> elements inside. Those include a fix to use lowercase for tagName of parsed HTML elements, fixes for textContent to contain HTML text nodes from all descendants and to keep those in correct order without creating empty textContent, and changes to JSON processing logic so that it can interpret string array items as HTML text nodes.

Changes done at the second step - interaction with <foreignObject> elements - include improved event handling of form control elements (which enables seamless user interaction), a new Paper option to enable default view actions, and the addition of a new special attribute props to support setting most common HTML form control properties programmatically.

Finally, changes to export logic enable <img> elements inside foreign objects, while a new option enables exporting HTML form control elements (<input>, <select>, <textarea>) with their current values.

Working with HTML form control foreign objects is illustrated in the new ROI demo.


Upgrade jQuery dependency (v3.6.4)

Adds several minor fixes.


Rewrite AST Application using TypeScript

Live Abstract Syntax Tree Demo


Improve zooming in Chatbot Application using pinch and pan Paper events

Live Chatbot Demo


In Chatbot Application, fix error occurring when using CommandManager in React

Fixes an issue in the React versions of the app where triggering undo() immediately after using an Input component would throw an error.

Details...

This is because the onBlur() method of the Input component does not get triggered automatically in React when selecting a new element as it does in other environments. This causes CommandManager not to be notified that the undo/redo batch initiated by the Input component has ended, which leads to the undo() method throwing an error.

The issue was solved by defining a React useEffect on the Input component in order to call onBlur() when the component rerenders.


In KitchenSink Application, initiate Selection lasso also when user clicks on a cell while holding SHIFT key

This change was enabled by the new preventDefaultInteraction() method of dia.CellView. By default, Selection lasso is only initiated when user clicks on a blank area of the Paper while holding SHIFT key.

KitchenSink Selection lasso started when user clicks on a cell while holding SHIFT key

Live KitchenSink Demo


Add DWDM (Dense Wavelength-Division Multiplexing) demo as an example of a network graphical view

DWDM

View code on GitHub

Live DWDM Demo


Add Flowchart demo

Uses the new rightAngle router and the new straight connector. Also illustrates the new alignment options of the Paper transformToFitContent() method.

See CodePen.

View code on GitHub


Revamp FTA demo

Uses a custom highlighter and the new straight connector. Also illustrates the new alignment options of the Paper transformToFitContent() method.

See CodePen.

View code on GitHub


Add ROI demo

Illustrates working with foreign objects.

To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

Note: If you are viewing this page in Safari, you might notice some rendering bugs in this demo. Our Foreign Object tutorial has an overview of all caveats.

See CodePen.

View code on GitHub


dia.CommandManager - fix to prevent modifying provided undo/redo stack object in fromJSON()

Addresses an issue where an undo/redo stack stack provided to CommandManager via the fromJSON() function was affected by subsequent changes in the undo/redo stack (caused by calling undo(), redo() or making a change in the diagram).

The input object is now independent. If you need the current state of the CommandManager undo/redo stack after some changes, you should export the diagram again via the toJSON() method.

const commandManager = new joint.dia.CommandManager({ graph: graph });
// ...
const backupUndoRedoStack = commandManager.toJSON(); // save current undo/redo stack
// ...
commandManager.fromJSON(backupUndoRedoStack); // apply saved undo/redo stack
commandManager.undo();
// Assert: `backupUndoRedoStack` did not change

format.Raster, format.SVG - add fillFormControls option to export HTML form control elements (<input>, <select>, <textarea>) with their current values

This feature is related to the improved support for foreign objects - form control elements can be part of the export if they are inserted via an SVG <foreignObject> element. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

A new option fillFormControls is added to allow exporting HTML form control elements within SVG (<input>, <select>, <textarea>) with their current values.

Details...

The function works as follows: For each form control element, the current value (i.e. the value of the property) is taken and set as the default value (i.e. the value of the attribute), and this SVG is then exported as a raster format (toPNG() / toJPEG() / toDataURL() / toCanvas() on Paper). The option is true by default. You can export to SVG analogously by using the toSVG() method on Paper.

Note: When exporting to SVG (toSVG() on Paper), keep in mind that the exported data is not entirely standalone. In order for the contained HTML foreign objects to be rendered as expected, the file must be opened in an environment which can understand the HTML namespace (for example, a web browser).

The option is illustrated in the following demo:

See CodePen.


format.Raster, format.SVG - fix to handle image URL conversion errors

Previously, any image URL conversion errors encountered during export (toSVG() / toPNG() / toJPEG() / toDataURL() / toCanvas() on Paper) caused the export to fail completely without calling the provided callback function. For instance, an error would be thrown when an image's source attribute (xlink:href / href / src) links to an external resource which cannot be accessed.

This issue has now been fixed. If an error is encountered during export, the export proceeds without the affected image(s), and the encountered error is saved. The error object is passed to the export function callback as the second parameter, which allows you to introduce additional logic to resolve the encountered error.

Here is some example code taken from the handler of the "Export SVG" button in our KitchenSink application:

paper.hideTools().toSVG(
(svg, error) => {
if (error) {
console.error(error.message);
}
new joint.ui.Lightbox({
image: 'data:image/svg+xml,' + encodeURIComponent(svg),
downloadable: true,
fileName: 'JointJS',
}).open();
paper.showTools();
},
{
preserveDimensions: true,
convertImagesToDataUris: true,
useComputedStyles: false,
// ...
}
);

Live KitchenSink Demo


format.SVG - fix to support HTML <img> elements inside SVG <foreignObject> element

This fix is related to the improved support for foreign objects - <img> elements can be part of the export if they are inserted via an SVG <foreignObject> element. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

The Paper option convertImagesToDataUris allows image sources to be encoded as data URIs before export (toSVG() on Paper), to make the exported file standalone. Previously, JointJS was only able to handle SVG <image> elements (xlink:href / href attribute), but the functionality is now expanded to work analogously for HTML <img> elements (src attribute).

Note: When exporting to SVG, keep in mind that the exported data is not entirely standalone. In order for the contained HTML foreign objects to be rendered as expected, the file must be opened in an environment which can understand the HTML namespace (for example, a web browser).

const ForeignObjectImg = joint.dia.Element.define(
'example.ForeignObjectImg',
{
attrs: {
foreignObject: {
width: 'calc(w)',
height: 'calc(h)',
},
},
},
{
markup: joint.util.svg/* xml */ `
<foreignObject @selector="foreignObject">
<div
xmlns="http://www.w3.org/1999/xhtml"
style="border:none;background:cornflowerblue;width:100%;height:100%;"
>
<img src="assets/image.jpg" style="margin-left:10px;margin-top:10px;" />
</div>
</foreignObject>
`,
}
);

const SVGImage = joint.dia.Element.define(
'example.SVGImage',
{
attrs: {
body: {
width: 'calc(w)',
height: 'calc(h)',
fill: 'lightcoral',
},
img: {
xlinkHref: 'assets/image.jpg',
x: 10,
y: 10,
},
},
},
{
markup: joint.util.svg/* xml */ `
<rect @selector="body" />
<image @selector="img" />
`,
}
);

const foreignObjectImg = new ForeignObjectImg({
position: { x: 10, y: 10 },
size: { width: 120, height: 120 },
});

const svgImage = new SVGImage({
position: { x: 160, y: 10 },
size: { width: 120, height: 120 },
});

graph.addCells([foreignObjectImg, svgImage]);

const button = document.querySelector('.btn');

button.addEventListener('click', () => {
paper.toSVG(
function (svgString) {
new joint.ui.Lightbox({
image: 'data:image/svg+xml,' + encodeURIComponent(svgString),
downloadable: true,
fileName: 'JointJS',
}).open();
},
{
// Converts HTML <img> element src attribute inside foreignObject into data URI
convertImagesToDataUris: true,
}
);
});
Before fixAfter fix
Before fixAfter fix

graphUtils - add toAdjacencyList() method

For a provided graph object, returns an object with Element IDs as properties and arrays of linked Element IDs as values.

Details...

The adjacency list is always directed, where the source of the connection is the parent and the target is the child - i.e. if an Element is only ever a target of connections but never a source of one, it would have an empty array in the returned adjacency list object.

This method is able to resolve Link-Link connections by recursively resolving the target until an Element is reached. That Element is the target of all involved sources, as if each of them had a separate single-Link connection to it.

This functionality is illustrated in the following demo:

See CodePen.


ui.Clipboard - add deep option to copyElements() to also copy all embedded descendants of provided cells

This option acts as a shortcut to add all provided cells and all their embedded descendants to the Clipboard. Previously, it was necessary to collect the cell descendants into a flat collection first, and then call copyElements with that collection.

// `cells` is a collection of cells which have embedded cells
// We can get such a collection from Selection, for example:
const cells = selection.collection;

// Now, you can do this:
clipboard.copyElements(cells, graph, { deep: true });

// ----------
// Previously, you had to do this:
const cellsDeep = cells.toArray().reduce((acc, cell) => {
// get all embedded descendants of each `cell` and add them to `acc` alongside `cell`
return acc.concat(cell, ...cell.getEmbeddedCells({ deep: true }));
}, []);
clipboard.copyElements(cellsDeep, graph);

ui.Dialog - fix wrong positioning while dragging

This change fixes an issue which occurred when dragging a Dialog whose .body element had a margin property applied.

Before fixAfter fix
Before fixAfter fix

ui.FreeTransform, ui.Halo, ui.Selection - fix to remove unprefixed user-drag CSS property

Details...

Replaces user-drag property with -webkit-user-drag since the unprefixed version is not supported by any browsers.

.joint-free-transform {
/* ... */
-webkit-user-drag: none;
}

.joint-halo .handle {
/* ... */
-webkit-user-drag: none;
}

.joint-halo-pie .pie-toggle {
/* ... */
-webkit-user-drag: none;
}

.joint-selection .handle {
/* ... */
-webkit-user-drag: none;
}

ui.Halo - fix to always pass Halo cid when changing a model

This fix makes it possible to identify that a change on the Graph (e.g. centering an element on user cursor, making a loop link) has been made by a Halo object.

// listen to graph for changes on cell model
graph.on('change', (cell, opt) => {
const haloId = opt.halo;

if (haloId) {
console.log(`Change to cell ${cell.id} was made by ui.Halo.`);
}
});

ui.Inspector - allow specifying options parameter via a source callback

Adds new functionality to Inspector - it is now possible to specify the options parameter of a field as an object with a source callback (or a Promise) and dependencies. This has two use cases:

  • The source callback function has access to the resolved dependencies object via the first argument. This allows you to dynamically generate an array of options for a field based on the value of another field in the Inspector (as in the attached code example).
  • The source callback can also be used with no dependencies - for example, if you want to generate the array of options on-the-fly via an API call to an external service. You may also call the new refreshSource(path) and refreshSources() functions at any point to programmatically update options array(s) in your Inspector.

This parameter is applicable to 'radio-group', 'select-box', 'select', 'select-button-group' and 'color-palette' fields. (Where 'radio-group' is a field of a new type added in this release.)

Previously, it was only possible to specify the options parameter as (1) a static array of strings or (2) a static array of value/content objects. In order to make the list of options interactive, you had to use a workaround, as in one of our examples.

In the following example, the value chosen in the first 'select-box' (furnitureType) field is used to determine the options available in the second 'select-box' field:

See CodePen.


ui.PaperScroller - fix to stop panning after mouse was released outside the window

Stop the PaperScroller panning also when the mouseup or touchend event occurs outside the document.body (outside the browser window).

We now listen to document events instead of document.body events.


ui.PathDrawer - add enableCurves option

This option (true by default) allows disabling the drawing of curve path segments in PathDrawer.

With enableCurves: true (default)With enableCurves: false
PathDrawer with enableCurves: true (default)PathDrawer with enableCurves: false

ui.RadioGroup - add new component

This component provides functionality for a radio button component within your diagram application. It can be used as an Inspector field via the 'radio-group' type, or as a standalone component:

// INSPECTOR FIELD
inputs: {
myRadioGroup: {
type: 'radio-group',
options: [{
content: 'Value 1',
value: 1
}, {
content: 'Value 2',
value: 2
}, {
content: 'Value 3',
value: 3
}]
}
}

// ----------
// STANDALONE COMPONENT
const radioGroup = new joint.ui.RadioGroup({
name: 'myRadioGroup',
options: [{
content: 'Value 1',
value: 1
}, {
content: 'Value 2',
value: 2
}, {
content: 'Value 3',
value: 3
}]
});

document.body.appendChild(radioGroup.render().el);

Example of a standalone RadioGroup component:

See CodePen.


ui.Selection - allow integration with PaperScroller for purposes of selecting, translating, and resizing

The Selection plugin now accepts a PaperScroller object as a paper option, which enhances the user's interaction with Selection (selecting, translating and resizing) according to the scrollWhileDragging option on the PaperScroller. For a simple example, if the scrollWhileDragging option on PaperScroller is true, then approaching an edge of the visible PaperScroller area with a cursor while interacting with a Selection will scroll the visible area.

Similar integration between Stencil and PaperScroller is already part of JointJS+. Analogously, it scrolls the visible area of a PaperScroller when the user drags an element from a Stencil. Both this integration and the new one are illustrated in our KitchenSink application. (To activate a Selection lasso in the KitchenSink application, hold SHIFT key and then point-and-drag an area of the diagram. If you select multiple elements, the Selection shows up as a rectangle encompassing all selected elements.)

ui.Selection allow integration with PaperScroller

Live KitchenSink Demo


ui.Selection - add allowCellInteraction option

This option is false by default. Setting it to true prevents Selection box events from being triggered, which allows interaction with features of Elements (e.g. buttons, ports) even if the Element is inside a Selection.

With allowCellInteraction: false (default)With allowCellInteraction: true
allowCellInteraction: falseallowCellInteraction: true

ui.Snaplines - add canSnap callback option

The canSnap callback option allows the user to control whether the current view should use snaplines while moving. All Elements use snaplines by default.


ui.Snaplines - add additionalSnapPoints callback option

By default, Elements may snap to center points or bounding box corner points of other Elements. This function allows the user to specify additional snap points to which dragged Elements may snap, which take precedence over the default points.

See CodePen.


ui.Stencil, ui.TreeLayoutView - use the same cellNamespace as the target Paper

JointJS will now use the cellViewNamespace of the main Paper and the cellNamespace of the associated Graph as the default cellViewNamespace and cellNamespace for the additional Paper and Graph objects of the Stencil and TreeLayoutView components. That is the most common use case.

const graph = new joint.dia.Graph(
{},
{
cellNamespace: 'myShapesNamespace',
}
);
const paper = new joint.dia.Paper({
// Other Paper options...
model: graph,
cellViewNamespace: 'myShapesNamespace',
});
const stencil = new ui.Stencil({
// Other Stencil options...
paperOptions: {
//cellViewNamespace: 'myShapesNamespace' // NOT NEEDED ANYMORE
},
});
const treeLayoutView = new ui.TreeLayoutView({
// Other TreeLayoutView options...
paperOptions: {
//cellViewNamespace: 'myShapesNamespace' // NOT NEEDED ANYMORE
},
});

See CodePen.


The Stencil may now contain Links, which can be dragged and dropped into the Paper in the same way as Elements. Please note that Links are not subject to automatic Stencil layout - but they can be laid out manually, as illustrated in the following demo:

See CodePen.


ui.Stencil - add API to initiate stencil dragging

Adds the startDragging() method to initiate stencil drag & drop interaction. This change means that Stencil can now be used programmatically.

For example, if you have your own HTML list of items and you want to be able to drag and drop the items into the diagram as elements.

Compared to an approach using the native Drag API (as illustrated in one of our demos), the new Stencil approach has several advantages:

  • Dragged Elements are snapped to Paper grid.
  • Dragged Elements can be embedded into containers under the preview.
  • If Snaplines are used in your app, the Stencil integrates with them while dragging Elements.

See CodePen.

The Stencil dragging API may be used even without having a rendered Stencil component anywhere in your diagram. In the following example, the Stencil dragging API is used alongside the new preventDefaultInteraction() method of dia.CellView to enable dragging-and-dropping Element images to other Elements:

See CodePen.

This feature also allows the user to do an arbitrary action on Stencil Element click and start the dragging interaction only if the user moves the pointer:

stencil.options.canDrag = () => false;
const stencilPaper = stencil.getPaper('stencil_group_1');
stencilPaper.on('cell:pointermove', (cellView, evt) => {
if (evt.data.draggingStarted) return;
stencil.startDragging(cellView.model.clone(), evt);
evt.data.draggingStarted = true;
});

ui.TextEditor - fix caret position to prevent missing first letter when typing fast in Chrome on Windows

Details...

The way we previously updated caret position in TextEditor involved waiting for an asynchronous handler function to run immediately after a keydown event. However, in some rare cases in Google Chrome on Windows, fast typing caused a race condition - if two keydown events were registered before the handler function had a chance to fire once, they would be treated as one, resulting in the first one being dropped (and the first letter disappearing). To prevent the race condition, the caret position is now updated in the onInput event handler instead.


ui.Toolbar - trigger input and change events for widgets (checkbox, toggle, inputText, inputNumber, textarea)

According to the Web API specification, input and change events should be triggered for all <input> and <textarea> elements. In order to bring JointJS toolbar widgets into alignment with the specification, we add these event triggers for our input-element-based widgets (checkbox, toggle, inputText, inputNumber) and our textarea-element-based widget (textarea).

See CodePen.


ui.Toolbar - fix to prevent propagation of widget events if widget name is undefined

In order to listen to a widget, it must have a name option set.

Previously, it was possible to receive events such as undefined:input; this was not helpful because the event did not unambiguously define its widget of origin - an input event on any unnamed widget would trigger an event with the same signature.

const myToolbar = new joint.ui.Toolbar({
tools: [{ type: 'inputText', label: 'text', name: 'myInputText' }],
});

// If an `input` event is triggered on this inputText-type widget of `myToolbar`...
// Assert: `myInputText:input` event is triggered

ui.Tooltip - add extra evaluation logic for constructor content parameter of function type

The content option allows you to specify the content of a Tooltip object. It accepts several different value types, one of which is as a function callback - two changes have been done to how this callback is processed, which provide fine-grained control over what kind of dynamic content can be displayed in the Tooltip.

First, the function callback was previously provided with an Element object as the only parameter (i.e. the hovered/focused object), but now it also receives the triggered Tooltip object itself as a second parameter. This allows you to, for example, use Tooltip options as part of the dynamic logic.

// Example HTML structure:
// <html>
// <body>
// <div>Hello World!</div>
// </body>
// </html>

// ----------
const tooltip = new joint.ui.Tooltip({
target: 'div',
content: (element, tooltip) =>
tooltip.options.target + ': ' + element.textContent,
});
// Trigger `mouseover` on a <div> HTMLElement in example HTML...
// Assert: `tooltip` is rendered with "div: Hello World!" as content

Second, the result of the function callback is now interpreted in three different ways to allow dynamic control over whether a Tooltip is even displayed:

  • If the returned value is false, the Tooltip is not shown at all for the Element.
  • If the returned value is null or undefined, the Tooltip is shown with default value - i.e. the value of the data-tooltip attribute on the target HTMLElement (or the value of a different data- attribute, if specified by the dataAttributePrefix Tooltip option).
  • If the returned value is anything else, the Tooltip is shown with the returned value (original behavior).

The following demo illustrates all these options:

See CodePen.


ui.TreeLayoutView - add layoutFunction callback option

By default, immediately after reconnecting or translating elements, the layout() function of TreeLayout is called. This option gives you the freedom to modify or disable that behavior by providing an alternative callback function.

Previously, if you wanted to modify this behavior, you had to use a workaround which involved providing a custom reconnectElement callback option, let it run as usual (involving running the default layout() function), and then provide your own custom logic (possibly calling layout() again). This workaround is no longer required.

function runLayout(treeLayoutView) {
// automatically center new tree in Paper
const tree = treeLayoutView.model;
tree.layout(); // call default function
paper.fitToContent({
allowNewOrigin: 'any',
contentArea: tree.getLayoutBBox(),
padding: 200,
});
}

// NEW APPROACH
const treeLayoutView = new ui.TreeLayoutView({
// ...
layoutFunction: runLayout,
});

// OLD APPROACH:
const treeLayoutView = new ui.TreeLayoutView({
// ...
reconnectElements: (
[element],
target,
siblingRank,
direction,
treeLayoutView
) => {
treeLayoutView.reconnectElement(element, {
id: target.id,
siblingRank,
direction,
});
runLayout(treeLayoutView);
},
});

// Assert: Disconnecting and reconnecting children automatically centers the tree in Paper.

shapes.bpmn2.Pool - add getLanesIds() and getParentLaneId() methods

The getLanesIds() method returns an array of all lane identifiers (in a non-specific order), while the getParentLaneId() method returns the id of the parent lane of a specified lane (or null if the provided lane is a top-level lane).

These methods can be used to extract the structure of a Pool when it is being converted into a different representation, such as BPMN XML or another format. Previously, getting these pieces of information would require touching internal objects of the Pool.

const pool = new joint.shapes.bpmn2.Pool({
position: { x: 0, y: 0 },
size: { width: 600, height: 300 },
lanes: [
{
id: 'customId1',
sublanes: [
{}, // auto-assigned id = "lanes_0_0"
{ id: 'customId2' },
],
},
{}, // auto-assigned id = "lanes_1"
],
});

const sortedLanesIds = pool.getLanesIds().sort();
// Assert: `sortedLanesIds` has value `['customId1', 'customId2', 'lanes_0_0', 'lanes_1']`

const parentLaneId = pool.getParentLaneId('lanes_0_0');
// Assert: `parentLaneId` has value `customId1`
const topLaneParentLaneId = pool.getParentLaneId('customId1');
// Assert: `topLaneParentLaneId` has value `null`

shapes.bpmn2.Pool - use null instead of empty string for missing values in customId and parentId lane metrics

Providing an empty string ('') as id of a lane now has the same result as providing undefined (or not setting lane id at all) - the lane is auto-assigned an id based on its logical position in the structure of the Pool.

const pool = new joint.shapes.bpmn2.Pool({
position: { x: 0, y: 0 },
size: { width: 600, height: 300 },
lanes: [
{
id: '',
sublanes: [
// auto-assigned id = "lanes_0"
{
sublanes: [
// auto-assigned id = "lanes_0_0"
{ id: 'customId' },
],
},
],
},
],
});

const bottomLaneParentLaneId = pool.getParentLaneId('customId');
// Assert: `parentLaneId` has value `lanes_0_0`
const middleLaneParentLaneId = pool.getParentLaneId('lanes_0_0');
// Assert: `parentLaneId` has value `lanes_0`

shapes.bpmn2.Pool - fix some HeaderedPool headers not being rendered with default markup in Chrome

When there is not enough vertical space left for text, the expected behavior of joint.util.breakText() is to hide the text. The issue was that with the default markup, the HeaderedPool header text height (as calculated by Google Chrome) sometimes did not fit into the space available to the text within the header cell (when internal margins of the header cell are taken into account).

The issue was fixed by reducing the default vertical margin of header cells (defined in joint.shapes.bpmn2.Pool.attrs).

Before fixAfter fix
Before fixAfter fix

Live Visio Flowchart Import Demo


shapes.chart.Matrix - fix column width measurement for column labels and row dividers

Details...

The width of column labels and row dividers in the Matrix shape was calculated as (shape_width / number_of_rows). This produced incorrect results in matrices in which the number of columns was different from the number of rows:

Before fixAfter fix
Before fixAfter fix

shapes.chart.Matrix - fix to apply label attributes

This fix allows targeting Matrix labels via model attrs. This fixes two issues - the default label attributes are now applied again (e.g. labels have a color by default), and the .attr() method can now again be used for Matrix labels:

matrix.attr('.label/fill', 'red');

shapes.standard.Record - fix to add item-id to icons

According to our documentation of the items option of joint.shapes.standard.Record, it should be possible to find item id from a Record DOM element, but this did not work for item icons. Fixing the issue provides a logical link between item icons and their corresponding items in the Record via the item-id. This makes it possible to react to user interaction with item icon and, for example, change Record data accordingly.

In the example below, the element:checkbox:pointerdown event handler uses this logical link. When the user clicks an item icon (e.g. one that looks like an unchecked checkbox), the corresponding item in Record data is identified and its value is flipped to the opposite state, which triggers an update of the icon (e.g. to one that looks like a checked checkbox).

See CodePen.


shapes.standard.Record - add selectors for targeting icons

It was previously not possible to target a single icon (or a group of icons within a column) for CSS/JointJS attributes.

// For a group of `record` icons defined this way...
[
[
{
id: 'item1',
group: 'alerts',
icon: 'bell.svg',
},
{
id: 'item2',
},
],
];

// ...it is now possible to add a tooltip in this way:
record.attr(
'itemIcons_alerts/dataTooltip',
'There is an alert message for this item.'
);

// To target all icons: `record.attr('itemIcons/...', ...)`
// To target only icon with ID `item2`: `record.attr('itemIcons_item2/...', ...)`

See CodePen.


layout.PortLabel - fix to center position of labels in outside/inside oriented layouts

The PortLabel layout must always align the center of the label with its associated port.

Before fixAfter fix
Before fixAfter fix

layout.PortLabel - fix to correctly set vertical position of text labels

This fix is part of a series of fixes to make sure that ports using the ref attribute update correctly on size change.

Addresses an issue where the PortLabel layout was sometimes trying to set the vertical position of text labels by setting the text-anchor and y attributes on the port label wrapping <g> group element (added implicitly when the port label consists of two or more tags) - which has no effect - instead of always doing it on the text label's <text> element, specifically. This fix required a low-level change to dia.ports.

Before fixAfter fix
Before fixAfter fix

layout.TreeLayout - add symmetrical option to ensure that distance between children of the same element is the same

By default, the TreeLayout algorithm tries to minimize the space required to present the tree on screen, which may put children of one element at unequal distances from each other (based on the amount of space needed to present each element's descendants).

Setting the symmetrical option to true instead forces the layout algorithm to place all sibling elements at equal distances from each other (i.e. each branch gets half of the screen estate of the root node, regardless of the amount of space actually needed to present all nodes of the branch). This leads to a symmetrical look within each layer of the tree, at the cost of requiring more presentation space.

An example of symmetrical TreeLayout:

Symmetrical TreeLayout


layout.TreeLayout - add removeElement() method

Removes an element from the graph and reconnects children to the parent of the removed element. The reconnected elements are spliced into the list of the parent's children at the position of the removed element, while the ordering of the children is maintained.

This functionality is used in our Organizational Chart application. Clicking on a person's red (-) button removes them from the hierarchy, and reconnects all of their subordinates to the removed person's boss while maintaining their relative ordering (if the removed person has no boss, all subordinates become independent).

State before calling removeElement()Result
State before calling removeElementResult

Live Organizational Chart Demo


dia.Paper - add overflow option

By default, the content of Paper is clipped (if necessary) to fit the Paper area. When this option is set to true though, the content of the Paper is not clipped and content thus may be rendered outside the Paper area.

const paper = new joint.dia.Paper({
// Other paper options...
overflow: true,
});

Note: In the screenshots below, the Paper has a visible border applied for illustration purposes.

With overflow: false (default)With overflow: true
Paper with overflow: false (default)Paper with overflow: true

dia.Paper - add verticalAlign and horizontalAlign to transformToFitContent() method

This is a new method replacing scaleContentToFit(). (The old method name can still be used as an alias to the new name.) The method transforms (repositions and resizes) the paper in such a way that all of its content ends up fitting into the visible area available to the Paper as tightly as possible.

Two new options are added, verticalAlign and horizontalAlign, to adjust the fit in case the content does not have the same aspect ratio as the Paper area (i.e. most of the time).

Details...

To explain the two options a little bit more, consider the following two situations:

  • If the available area is taller than it is wide (like a phone screen) and the Paper content is wider than it is tall (like a horizontal timeline diagram), then there would be empty vertical space left over after the fit; the verticalAlign option would then specify whether the content should end up being positioned at the top / middle / bottom of the Paper area (where 'top' is the default value).
  • In the previous situation, the horizontalAlign option was not relevant, because the Paper content occupied all available space in the horizontal dimension. However, if the aspect ratios were reversed (wide available area and tall Paper content), then there would be empty horizontal space left over after the fit, instead, and the horizontalAlign option would specify whether the content should end up being positioned at the left / middle / right of the Paper area (where 'left' is the default value).

As you can see, if you want to make sure that Paper content is always presented in the middle of the Paper area, you should specify verticalAlign: 'middle' and horizontalAlign: 'middle' in transformToFitContent() options.

The following code does an initial Paper fit and then sets up a listener on window resize events to re-fit the Paper when necessary:

const fit = () =>
paper.transformToFitContent({
useModelGeometry: true,
padding: 10,
maxScale: 1,
verticalAlign: 'middle',
horizontalAlign: 'middle',
});

fit(); // initial fit
window.addEventListener('resize', () => fit());

The above code can be seen in action in our new Flowchart demo (as well as in our new ROI demo). Note that the diagram is initially horizontally-aligned to the middle of the available area. However, if the available area is narrowed (e.g. by clicking the "JS" button), the diagram switches to being vertically-aligned to the middle of the available area.

See CodePen.


dia.Paper - add element:magnet:pointerdown, element:magnet:pointermove and element:magnet:pointerup events

A new set of events is now triggered when the user interacts with a magnet (e.g. a port). When used together with the new preventDefaultInteraction() function, this allows custom magnet interactions to be implemented. For example on element:magnet:pointerdown - instead of creating and dragging a new link (the default interaction) - a Link could be created and immediately connected to a pre-determined Element.

See CodePen.


dia.Paper - improve event handling of form control elements inside <foreignObject> elements

This feature is related to the improved support for foreign objects - form control elements can be part of element markup if they are part of an SVG <foreignObject> element. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

This change prevents default JointJS interactions when the user is interacting with form control elements. For example, if the focus is at a <textarea> element, and the user is dragging across the text with their pointer, the expected interaction is that the text should be selected - this means that the default JointJS interaction of moving the element needs to be prevented.


dia.Paper - add preventDefaultViewAction option

This feature is related to the improved support for foreign objects - form control elements can be part of element markup if they are part of an SVG <foreignObject> element. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

By default, this option is true, which instructs JointJS to prevent default actions (evt.preventDefault) when a pointerdown event is registered on a cell view (i.e. a mousedown or touchstart). Among other interactions, this means that text content of elements cannot be selected for copying.

However, if you use form controls elements (<input>, <select>, <textarea>) in your diagram, it might be desirable to disable this functionality (by setting preventDefaultViewAction to false) and run the default action - for example, to make sure that clicking on a view triggers a blur event on the current activeElement on document (e.g. to get rid of focus on an input field that the user was interacting with previously). This is why the option is used in the new ROI demo.

Please note that browsers perform a lot of various actions by default, and you should test your application's UX thoroughly if you disable this option - especially if you expect both mouse events and touch events (since their default actions are different). There are various targeted ways to disable only some default actions - e.g. via user-select or touch-action CSS properties - which you should investigate.


dia.Paper - add drawGridSize option

This option overrides Paper gridSize option exclusively for drawing of the grid. In case a drawGridSize is provided, the gridSize value is used exclusively for the purposes of internal logic - e.g. for snap-to-grid calculations.

For an example use case, this enables dynamically switching gridSize in some cases (e.g. setting a low gridSize to enable high precision when dragging link endpoints, and switching it back for dragging elements within a grid) while keeping drawGridSize fixed at the element-dragging gridSize value.


dia.Paper - add autoFreeze option

By default, this advanced option is false, which instructs the Paper to periodically check the Paper viewport to find out whether the visible area has been changed. Setting the autoFreeze option to true suppresses this behavior for a significant reduction in background processing when viewing a JointJS diagram - when this option is enabled, it freezes the paper as soon as there are no more updates and unfreezes it when there is an update scheduled.

However, when this option is active, it is up to the developer to call the checkViewport() Paper method manually whenever the Paper viewport needs to be changed - e.g. during scrolling and zooming. After the processing of all updates is finished, the freeze state is activated on the Paper again on the next frame cycle.


dia.Paper - fix to trigger render callbacks and event when requireView() is called

If a view is updated manually (by calling requireView()), then beforeRender and afterRender callbacks - as well as the render:done event - are now all triggered. This prevents the situation where some updates could be done on a CellView without notifying the rest of the diagram.

Details...

Two example scenarios where the old behavior could cause a problem:

  • A scheduled update on the CellView which called requireView().
  • A newly-created Link dropped in an empty area of the diagram when the linkPinning Paper option was set to false, which would cause the link to be rolled back (i.e. removed) as it was not connected to any Cell. In this case, all rollback actions (link is deleted, checkMouseLeave() function is called, updates to view) were executed synchronously without any notification.

dia.Paper - fix to send mousewheel events when CTRL key is pressed while there are no paper:pinch subscribers

Touchpad devices send a mousewheel event with a fake CTRL keypress to signify a pinch gesture. When we implemented support for the paper:pinch event, our handler logic caused an issue in a customer application which depends on handling genuine mousewheel + CTRL events (i.e. not the pinch gesture).

The fixed logic now correctly checks whether there are any paper:pinch subscribers - and if not, does not prevent mousewheel + CTRL events from being propagated.


dia.Paper - fix to auto-rotate target markers as expected when using marker.markup

JointJS automatically rotates Link target markers by 180 degrees, but this functionality was previously not working as expected when markers were defined via marker.markup.

Link markers defined via marker.markup are illustrated in our new Connector Arrows demo, which also shows the auto-rotation feature working as expected now. You can click on each Link to get a closer look and also to get the marker ID# to cross-reference the JS code:

See CodePen.


dia.Paper - fix event handlers to correctly receive dia.Event on touch screens

Previously, JointJS was sending a Touch object in these cases, which was not useful for event handling. (For example, the Touch object API does not have a method to identify the native event that created it, but the reverse can be achieved with the TouchEvent API.) Event handlers now correctly receive objects of type dia.Event on touch screens.

This fix required a low-level change to the util.normalizeEvent() function.


dia.Paper - fix event handlers so that originalEvent always points to a native event

The originalEvent property of objects passed by JointJS to event handlers now always points to an instance of a native event. Previously, JointJS was inconsistent in which objects it was passing along. (For example, in some cases it was sending jQuery Event objects instead).


dia.Paper - fix to trigger element:magnet:pointerclick when user clicks an invalid magnet

The element:magnet:pointerclick event is now fired even if the user clicks on a magnet that is not a valid interaction target (e.g. because the interactive Paper option has value { addLinkFromMagnet: false }).

There was a workaround for this issue - using the special attribute magnet: 'passive' when defining the port - which prevented the default interaction while still triggering port events. It is not necessary to use this workaround anymore.

const paper = new joint.dia.Paper({
// Other paper options...
interactive: { addLinkFromMagnet: false },
});
// ...
const port = {
label: {
// Port label position and markup...
},
attrs: {
portBody: {
// Other portBody attributes...
// magnet: 'passive' // WORKAROUND NOT NEEDED ANYMORE
},
label: { text: 'port' },
},
markup: [
{
tagName: 'rect',
selector: 'portBody',
},
],
};
element.addPort(port);

dia.Paper - fix to hide tools and highlighters when associated cell view is detached

Addresses an issue where the Tools and Highlighters attached to a CellView would remain in the DOM even after their CellView was detached from the Paper.

This fix can be seen in action in our new FTA demo. When collapsing an Element, it makes sure that any "Collapse" Tools of child Elements are hidden.


dia.Paper - fix to throw an error when unfreeze() is called on a paper which has been removed

Details...

If an asynchronous Paper has been removed, it does not make sense to try to unfreeze it.

const paper = new Paper({
// ...
async: true,
});
paper.model.addCell({ type: 'standard.Rectangle' });
paper.remove();
paper.unfreeze();
// Assert: An error is thrown

dia.Paper - fix to allow immediate propagation on pointerup

Prevent the Paper from stopping other listeners of the pointerup event attached on document from being called (i.e. there is no need to call stopImmediatePropagation() on pointerup).


dia.ElementView - fix to correctly update port nodes with ref on size change

Scaling a port node with styling modified via the ref attribute (e.g. putting a background rectangle to port label text and using calc() in the process) exposed several issues, all of which were fixed in this release:

Together, these changes have the following effect:

Before all fixesAfter all fixes
Before all fixesAfter all fixes

dia.ElementView - fix to prevent double rendering of ports when CSS selectors are enabled

This fix is part of a series of fixes to make sure that ports using the ref attribute update correctly on size change.

When using CSS selectors (joint.config.useCSSSelectors = true, i.e. the default), Element ports were rendered twice on resize, where the second run yielded an incorrect result due to transformations applied to port nodes after the first run.

Before fixAfter fix
Before fixAfter fix

dia.LinkView - enable link label dragging on touch screens in async mode

Details...

In Paper async mode, it was previously not possible to drag a Link label on touch devices. This was because the label was getting re-rendered at the original position after each label touchmove event - since the original target of the event was removed from the DOM, the touchmove event stopped triggering. Link labels are now updated synchronously while dragging via touchmove events in progress.


dia.LinkView - fix to take defaultLabel.size into account for link label size calculations

This fix makes it possible to specify defaultLabel.size when initializing a Link and have its properties merged with the properties of label.size of individual Link labels. This brings the size argument into alignment with other label arguments (markup, attrs, position) which also inherit from their defaultLabel counterparts:

const link = new joint.shapes.standard.Link({
source: { x: 50, y: 400 },
target: { x: 500, y: 400 },
defaultLabel: {
// applied to all labels on this link:
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'text',
selector: 'label',
},
],
size: {
// used by `calc()` expressions in `attrs`
width: 150,
height: 30,
},
attrs: {
body: {
width: 'calc(w)',
height: 'calc(h)',
// center around label position:
x: 'calc(w/-2)',
y: 'calc(h/-2)',
stroke: 'black',
fill: 'white',
},
label: {
textWrap: {
width: 'calc(w-5)',
height: 'calc(h-5)',
},
// center text around label position:
// (no `x` and `y` provided = no offset)
textAnchor: 'middle',
textVerticalAnchor: 'middle',
fontSize: 16,
fontFamily: 'sans-serif',
},
},
},
labels: [
{
// specification of an individual label:
size: { width: 200 }, // partially overwrites `defaultLabel.size`
attrs: {
label: {
text: 'Hello World',
},
},
position: { distance: 0.25 }, // overwrites built-in default
},
],
});

dia.LinkView - fix to remember initial cursor offset from label anchor coordinates when dragging

This fix ensures that when dragging a Link label, the label remembers the coordinates of the initial cursor position. Previously, the label would jump so that its anchor would be under the cursor - as in this example, where the anchor is in the center of the label:

Before fixAfter fix
Before fixAfter fix

dia.LinkView - fix incorrect rotation of labels using keepGradient and absoluteOffset options

Fixes an issue where Link labels using the keepGradient and absoluteOffset options had unexpected rotation applied to them. This was caused by a low-level issue in the g.Point.offset() function.

Before fixAfter fix
Before fixAfter fix

dia.LinkView - fix to prevent label jumping while being dragged along straight-line Curve links

Straight-line Links represented by a Curve object behind the scenes (e.g. Links with the smooth connector applied in which both endpoints have the same y coordinate) experienced an issue where relatively-positioned labels were jumping back and forth under the cursor when dragged by the user.

const link = new joint.shapes.standard.Link({
source: { x: 400, y: 200 },
target: { x: 740, y: 200 },
connector: { name: 'smooth' },
labels: [
{
attrs: {
text: {
text: 'Hello World!',
},
},
position: {
distance: 0.3, // relative
},
},
],
});

Note that this issue was affecting only a very small subset of Links. It was not affecting any Links represented by a Line object (e.g. Links with any other connector applied), nor any Links represented by a Curve object which were actually curved (e.g. Links with the smooth connector applied in which the two endpoints have different x and y coordinates).

The issue was caused by an unhandled edge case in the low-level g.Curve.getSubdivisions() function which caused the LinkView's label positioning function (getLabelPosition()) to have insufficient precision.

Note: You may notice that although the label jumping has been significantly reduced, it has not been eliminated completely. That is not a specific issue; it affects all curved Links due to the speed/precision tradeoff in our Curve algorithms:

Before fixAfter fix
Before fixAfter fix

dia.CellView - add preventDefaultInteraction() and isDefaultInteractionPrevented() methods

Adds an API to dynamically prevent default JointJS interactions (e.g. Element and Link movement on drag, adding a Link on port click, Link label dragging). The default interactions can now also be prevented for a particular event based on its characteristics - for example, based on whether the SHIFT key was pressed, or based on the target DOM element of the event. (Previously, the default JointJS interactions could only be prevented by the interactive Paper option, which has access only to the CellView in question but not the event.)

In the following demo, the default interaction (Element dragging) is prevented for Element images - instead, dragging an image allows the user to drag the image to another Element via the new Stencil dragging API:

See CodePen.

There was a situation where you wanted to select the children inside the container, but you couldn't draw the selection lasso without moving the container.

See CodePen.

This feature is also illustrated in our KitchenSink application - holding SHIFT and dragging always triggers a Selection lasso - even if the interaction starts with a pointerdown event on an Element (which would normally initiate Element dragging):

paper.on('element:pointerdown', (elementView, evt) => {
if (evt.shiftKey) {
// prevent element movement
elementView.preventDefaultInteraction(evt);
// start drawing selection lasso
}
});

Live KitchenSink Demo


dia.CellView - fix link update if connected element changes at the same time the connection is made

In Paper async mode, if changes are made to the connected Element in the link:connect event handler, the LinkView must only update after taking these new changes (e.g. new size, new port position) into account. Example:

const paper = new joint.dia.Paper({
// ...
async: true,
});

function makeElementTaller(element, increment) {
const size = element.size();
element.resize(size.width, size.height + increment);
}

paper.on('link:connect', (_, _, elementViewConnected, _, _) => {
// Make a change to the connected Element's size
makeElementTaller(elementViewConnected.model, 100);
// Assert: LinkView connects to Element according to new size
});

dia.CellView - fix to return correct target under the pointer for pointer events

The getEventTarget() method is intended to normalize behavior among mouse events, touch events, and pointer events - it returns the target Cell under pointer even for events which do not have it natively (i.e. touchmove and touchend events). This fix expands that functionality also to pointermove and pointerup events when the pointer capture was set.

This allows the Link arrowhead move interaction to behave consistently for mouseevents, touchevents and pointerevents (i.e. dragging the arrowhead to connect the Link to an Element or to pin it to the Paper).


dia.CellView - fix to get correct ref node bounding box

This fix is part of a series of fixes to make sure that ports using the ref attribute update correctly on size change.

The size and position of the ref node used in SVG custom attributes (e.g. calc() expressions) must only be transformed using transformations that are defined between (1) the ref node and (2) the common parent of the ref node and the current node (to which we want to set the custom attribute). Previously, the ref node was transformed by all transformations between the ref node and the root of the CellView.

Details...

Assume the following JointJS code:

elementModel.attr({
// set the size of rectangle `a` to the same size as `b` (= the `ref` node)
a: {
x: 'calc(x)',
y: 'calc(y)',
width: 'calc(w)',
height: 'calc(h)',
},
});

Depending on the Element markup, the bounding box has to be calculated differently. This depends on whether there are transformations which apply only to b (the ref node) but not to a (the current node).

  • Markup 1:

The b and a nodes are affected by the same transformation:

<g transform="translate(11, 13)">
<rect @selector="b" x="1" y="2" width="3" height="4" />
<rect @selector="a" />
</g>

In this case, the reference bounding box does not need to be adjusted for the transform attribute on the <g> SVG element, because the same transformation is applied to the a node as to the b node. Therefore, the bounding box of b from the perspective of a is { x: 1, y: 2, width: 3, height: 4 }.

  • Markup 2:

While the b node is affected by a transformation, the a node is not:

<g transform="translate(11, 13)">
<rect @selector="b" x="1" y="2" width="3" height="4" />
</g>
<rect @selector="a" />

In this case, the reference bounding box needs to be adjusted for the transform attribute on the <g> SVG element - since the a node is not a descendant of the <g> SVG element, it is not affected by the transformation. Therefore, the bounding box of b from the perspective of a is { x: 12, y: 15, width: 3, height: 4 }.


dia.Element - add expandOnly and shrinkOnly options to fitToChildren() and fitParent() methods

Enhances the API for working with embedded cells:

  • Adds fitToChildren() and fitParent() methods
  • Keeps deep and padding options for both methods
  • Adds expandOnly and shrinkOnly options for both methods
  • Adds terminator option for fitParent() method

This change adds two new methods.

  • The fitToChildren method:

This is a new method replacing fitEmbeds(). (The old method name can still be used as an alias to the new name.) This method adjusts the bounding box of this Element so that it envelops all of the Element's embedded children.

  • The fitParent() method:

This is a completely new method which adjusts the bounding box of the embedding parent of this Element so that it envelops this Element.

Additionally, this change adds three new options and adapts the behavior of two existing options.

Details...
  • The padding option:

By default for both methods, the embedding elements are fitted tightly around their embedded children. However, this behavior can be modified - the padding option instructs the fitting algorithm to leave some amount of empty space around elements when fitting the parent around them. The option can be applied in the following way:

element.fitToChildren({ padding: 10 });
element.fitParent({ padding: 10 });

Both methods are illustrated below, with padding option applied:

In all of the following examples, the structure of the Graph is as follows: the orange element (r1) has two embedded children (r11 at top-left, and r12 at top-right), and r11 has an embedded child (r111 at bottom-left).

Function callBefore callResult
r1.fitToChildren({ padding: 10 })Before callResult of r1.fitToChildren({ padding: 10 })
r111.fitParent({ padding: 10 })Before callResult of r111.fitParent({ padding: 10 })
  • The deep option:

Both methods can also be modified by the deep option, which instructs the fitting algorithm to be applied recursively - to all embedded descendants of this Element (in the case of fitToChildren()), or to all embedding ancestors of this Element (in the case of fitParent()). The option can be applied in the following way:

element.fitToChildren({ deep: true }); // fit to descendants
element.fitParent({ deep: true }); // fit ancestors

You can think of the fitToChildren() and fitParent() methods as two sides of the same coin. While fitToChildren() starts with the outermost element you need to adjust and progresses inward (without affecting the parent or siblings of the start element), fitParent() starts with the innermost element you need to adjust and progresses outward (without affecting children or siblings of the start element):

Function callBefore callResult
r1.fitToChildren({ padding: 10, deep: true })Before callResult of r1.fitToChildren({ padding: 10, deep: true })
r111.fitParent({ padding: 10, deep: true })Before callResult of r111.fitParent({ padding: 10, deep: true })

Furthermore, both methods can be modified by two new options, expandOnly and shrinkOnly. These limit which types of adjustments the fitting algorithm is allowed to do to Elements - either only expansion or only shrinking:

  • The expandOnly option:

This option can be applied in the following way:

element.fitToChildren({ expandOnly: true });
element.fitParent({ expandOnly: true });

Note that r1 never shrinks on the left side, for example:

Function callBefore callResult
r1.fitToChildren({ expandOnly: true, padding: 10 })Before callResult of r1.fitToChildren({ expandOnly: true, padding: 10 })
r1.fitToChildren({ expandOnly: true, padding: 10, deep: true })Before callResult of r1.fitToChildren({ expandOnly: true, padding: 10, deep: true })
r111.fitParent({ expandOnly: true, padding: 10 })Before callResult of r111.fitParent({ expandOnly: true, padding: 10 })
r111.fitParent({ expandOnly: true, padding: 10, deep: true })Before callResult of r111.fitParent({ expandOnly: true, padding: 10, deep: true })
  • The shrinkOnly option:

This option can be applied in the following way:

element.fitToChildren({ shrinkOnly: true });
element.fitParent({ shrinkOnly: true });

Note that there is no overlap between r11 and r111 in our example - when such a situation is encountered in an iteration of the shrinkOnly algorithm, that iteration is skipped. In our examples, that means that the only Element adjusted is r1, where relevant (it shrinks on the left and on the bottom, and never expands):

Function callBefore callResult
r1.fitToChildren({ shrinkOnly: true, padding: 10 })Before callResult of r1.fitToChildren({ shrinkOnly: true, padding: 10 })
r1.fitToChildren({ shrinkOnly: true, padding: 10, deep: true })Before callResult of r1.fitToChildren({ shrinkOnly: true, padding: 10, deep: true })
r111.fitParent({ shrinkOnly: true, padding: 10 })Before callResult of r111.fitParent({ shrinkOnly: true, padding: 10 })
r111.fitParent({ shrinkOnly: true, padding: 10, deep: true })Before callResult of r111.fitParent({ shrinkOnly: true, padding: 10, deep: true })

Additionally, note that when shrinkOnly is used together with the deep option, the results are evaluated iteratively (i.e. one step at a time). In the example below, this means that the orange element (r1) adjusts according to its two children (r11 at top-left and r12 at top-right), but not according to its grandchild r111 (at bottom-left). The behavior is the same for both functions, fitToChildren() and fitParent(), as illustrated in the following examples:

Function callBefore callResult
r1.fitToChildren({ shrinkOnly: true, padding: 10, deep: true })Before callResult of r1.fitToChildren({ shrinkOnly: true, padding: 10, deep: true })
r111.fitParent({ shrinkOnly: true, padding: 10, deep: true })Before callResult of r111.fitParent({ shrinkOnly: true, padding: 10, deep: true })
  • The terminator option:

Finally, the new terminator allows you to modify the behavior of the deep option on the fitParent() method - it specifies the last embedding ancestor of this Element for which the fitting algorithm should be applied. This option can be applied in the following way:

element.fitParent({ deep: true, terminator: ancestorElement });

Edge cases are handled in the following ways:

  • If terminator is the Element itself, the function call has no effect
  • If terminator is the Element's embedding parent, the result is the same as calling fitParent() without the deep option
  • If terminator is not an embedding ancestor of the Element, the result is the same as calling fitParent() with the deep option without a terminator option

dia.Cell - fix to always send propertyPath and propertyValue options when calling prop()

Calling the prop() function on a Cell (also used internally when calling the attr() function) triggers a change event on the Cell (unless the function is called with the option silent: true). That change event can be handled via a callback function which is provided with two parameters - the Cell in question and an additional options object. The options object parameter in this callback is the subject of this fix.

The prop() function can be used with a variety of function signatures. The options object always repeats the options provided to prop() (e.g. silent or rewrite). However, for some of the function signatures, the options object was previously enhanced with additional information (propertyPath, propertyPathArray, propertyValue), but this was not the case for other function signatures.

The options object is now enhanced in all cases.

Details...

In the four examples below, the function signatures are described in a TypeScript-like fashion:

  • Function signature prop(path: array, value: any, options?: object):
cell.prop(['name', 'first'], 'John', { rewrite: true });
// Triggers the following:
cell.on('change', (cell, options) => {
/* Assert: `options` has the following value:
{
propertyPath: 'name/first', // = convert `path` to string
propertyPathArray: ['name', 'first'], // = `path`
propertyValue: 'John', // = `value`
rewrite: true
}*/
});
  • Function signature prop(path: string, value: any, options?: object):
cell.prop('name/first', 'John', { rewrite: true });
// Triggers the following:
cell.on('change', (cell, options) => {
/* Assert: `options` has the following value:
{
propertyPath: 'name/first', // = `path`
propertyPathArray: ['name', 'first'], // = convert `path` to array
propertyValue: 'John', // = `value`
rewrite: true
}*/
});
  • Function signature prop(path: string, value: any, options?: object) (same as previous case, but notice how the values in the options object seem to be converging towards the values in the last case):
cell.prop('name', { first: 'John' }, { rewrite: true });
// Triggers the following:
cell.on('change', (cell, options) => {
/* Assert: `options` has the following value:
{
propertyPath: 'name', // = `path`
propertyPathArray: ['name'], // = convert `path` to array
propertyValue: { first: 'John' }, // = `value`
rewrite: true
}*/
});
  • Function signature prop(value: object, options?: object) (previously, the options object was not enhanced in this case):
cell.prop({ name: { first: 'John' } }, { rewrite: true });
// Triggers the following:
cell.on('change', (cell, options) => {
/* Assert: `options` has the following value:
{
propertyPath: null, // = there is no `path` in this case
propertyPathArray: [], // = there is no `path` in this case
propertyValue: { name: { first: 'John' }}, // = `value`
rewrite: true
}*/
});

dia.Cell - fix inconsistent merging behavior in prop()

This change unifies the merging behavior of the prop() function. The current values are never rewritten unless the { rewrite: true } option is passed.

Previously, the prop() function was not merging existing properties with new ones when the path argument resolved to a top-level attribute and the value argument was an object (but it was doing so in all other cases):

cell.prop({ a: { b: 1 } });
cell.prop({ a: { c: 2 } });
// Assert: `cell.prop('a')` has the value `{ b: 1, c: 2 }`

// ----------
cell.prop('a/b', 1);
cell.prop('a/c', 2);
// Assert: `cell.prop('a')` has the value `{ b: 1, c: 2 }`

// ----------
cell.prop('a', { b: 1 });
cell.prop('a', { c: 2 });
// Assert: `cell.prop('a')` has the value `{ b: 1, c: 2 }`
// PREVIOUSLY INCORRECTLY EVALUATED SAME AS THE FOLLOWING:

cell.prop('a', { b: 1 });
cell.prop('a', { c: 2 }, { rewrite: true });
// Assert: `cell.prop('a')` has the value `{ c: 2 }`

dia.Cell - fix to preserve stacking of nested cells when toFront()/toBack() is called

This change fixes an issue where the stacking order (equivalent to HTML z-index) among Cells was sometimes not preserved when the functions toFront() and toBack() were called.

Additionally, this change adds the z() method to return the current stacking order of a Cell.

Details...

To illustrate the issue, assume the following Graph structure, where r1 has two embedded children which partially overlap:

const r1 = new joint.shapes.basic.Rect({});
const r2 = new joint.shapes.basic.Rect({ z: 10 });
const r3 = new joint.shapes.basic.Rect({ z: 5 });

r1.embed(r2);
r1.embed(r3);

Previously, calling r1.toFront({ deep: true }) in this situation led to the stacking order between the two embedded children to be reversed. The fix gives the expected result.

Before fix
Before fix
After fix
After fix

The fix is illustrated in the following demo:

See CodePen.


dia.Cell - fix to prevent Cell id from being undefined

All Cells are expected by JointJS to have an id - either a custom one or an automatically-generated one. This fix resolves an issue where it was possible to circumvent id auto-generation when creating a new Cell by explicitly providing an id with the value of undefined.

const json = {
/* no `id` property */
};
const { id } = json; // `id = undefined`
const rect = new joint.shapes.standard.Rectangle({ id });
// Assert: `rect.id !== undefined`

linkTools.Anchor - fix to trigger mouseleave event after drag-and-drop interaction

Make sure that the cell:mouseleave event is triggered after drag-and-drop interaction even if the pointer is outside the LinkView on mouseup.

Details...

This fix is necessary since we undelegate all paper events on mousedown behind the scenes (to prevent other events like mouseover and mouseout events from triggering during the drag-and-drop interaction) with the expectation that they would be delegated back on mouseup. The issue with that approach is that if the mouseup event is not fired as expected, the paper events stay undelegated.

With this fix, JointJS now makes sure that paper events are delegated back even in the edge case that a mouseup event is called while the pointer is outside of the LinkView to which this Anchor link tool is attached.


highlighters.mask - fix to prevent copying of class attribute to <mask> elements

This fix makes sure that the <mask> nodes added to Paper's <defs> node by MaskHighlighter (highlighters.mask) no longer receive class attributes (copied over from source Cell nodes) when the Highlighter is added to a CellView via the add() method.

Previously, the presence of the class attribute on the <mask> nodes caused these nodes to become the target of CSS rules intended for the original Cell nodes. This behavior was removed because it makes no sense for these CSS rules to change the presentation attributes of the <mask> nodes.

The MaskHighlighter is shown in our new Flowchart demo on mouseover of any Element:

See CodePen.


connectionPoints.boundary - add option to disable automatic magnet lookup within <g> elements

Allows you to specify <g> elements as magnets and explicitly define magnets using magnetSelector when using the boundary connection point.

Details...

This change concerns the selector option of the boundary connection point logic, which identifies the subelement/magnet of the Link end Element at whose boundary we want the connection point to be found.

The default behavior (i.e. when selector is undefined) is to use the first non-group (<g>) descendant of the Link source/target Element's SVGElement. Alternatively, a selector identifier can be provided to explicitly identify the subelement/magnet to be used by the algorithm.

What was missing, however, was an option to disable the default lookup behavior in order to allow the use of an arbitrary Element magnet (i.e. one where we may or may not need to target a <g> subelement, depending on the type of Element we are connecting to) - e.g. when used in conjunction with a magnetSelector attribute on a specific Elements.

This can now be achieved by setting the selector option of the boundary connection point logic to false. Previously, this required writing a custom connection point function to distinguish between different cases (which overrode any magnetSelector attributes on individual Elements). That workaround is no longer necessary. For example:

const paper = new joint.dia.Paper({
// ...
defaultConnectionPoint: {
name: 'boundary',
args: {
selector: false,
},
},
// WORKAROUND NOT NEEDED ANYMORE:
/*defaultConnectionPoint: (endPathSegmentLine, endView, endMagnet) => {
// Note: This overrides any `magnetSelector` options on individual Elements
return joint.connectionPoints.boundary.call(this, endPathSegmentLine, endView, endMagnet, {
selector: (endView.model.get('type') === 'standard.Rectangle' ? 'root' : 'body')
});
}*/
});

// built-in default connection point = on bounding box of `body` <rect> SVGElement
// = (first non-group descendant of `root` <g> SVGElement)
// new `defaultConnectionPoint` = on boundary of `root` <g> SVGElement
// actual connection point = same as `defaultConnectionPoint`
const el1 = new joint.shapes.standard.Rectangle({
// ...
});

// built-in default connection point = on bounding box of `body` <ellipse> SVGElement
// = (first non-group descendant of `root` <g> SVGElement)
// new `defaultConnectionPoint` = on boundary of the `root` <g> SVGElement
// actual connection point = overridden via `attrs/root/magnetSelector`
const el2 = new joint.shapes.standard.Ellipse({
// ...
attrs: {
root: {
// explicitly redirect the magnet to the `body` <ellipse> SVGElement for `el2`
// = (otherwise a rectangular gap appears between the Link and the ellipse)
// = (because the default magnet is the rectangular `root` <g> SVGElement)
// connection point for `el2` = on boundary of the `body` <ellipse> SVGElement
magnetSelector: 'body',
},
},
});

const l1 = new joint.shapes.standard.Link({
source: {
id: el1.id,
},
target: {
id: el2.id,
},
});

// Assert: `l1` connects to:
// - the boundary of the `root` <g> SVGElement of `el1`
// - the boundary of the `body` <ellipse> SVGElement of `el2`

See CodePen.


connectors.straight - add new connector, and deprecate normal and rounded connectors

Adds a new connector which combines existing normal and rounded connectors with additional options to make path bevelled / with gaps at corners of the path. The normal and rounded connectors were deprecated.

Different functionality is enabled via options. The cornerType option is the most fundamental of those - it determines what should be done at the corners which appear around path points of the Link:

  • 'point' - (Default) Connect path points with straight lines with no corner modification (normal connector logic)
  • 'cubic' - Draw a cubic segment at path points (rounded connector logic)
  • 'line' - Draw a bevel segment at path points
  • 'gap' - Leave empty space at path points

Four additional options are available:

  • cornerRadius
  • cornerPreserveAspectRatio
  • precision
  • raw

These options change more specific aspects of the straight connector.

Details...
  • The cornerRadius option:

This option determines how far away from a path point should the start/end points of the corner modification (i.e. the rounding, bevel, or gap) be placed. The default value is 10 (i.e. the default for rounded connector). This option is ignored by the 'point' type.

  • The cornerPreserveAspectRatio option:

This option determines what should happen in the edge case where the start/end points of a corner modification have to be placed at different distances away from the path point (e.g. because of squishing caused by two subsequent path points being closer together than cornerRadius * 2) - should they both be coerced to the same (shorter) distance away from the path point? The default is false (i.e. the default for rounded connector), but note that setting it to true is often necessary in order to make the 'line' type achieve a bevelled look for the Link. This option is ignored by the 'point' type.

  • The precision option:

This determines the default rounding precision on corner modification start/end points. The default is 1. This option is ignored by the 'point' type.

  • The raw option:

This option is shared by all connectors; it also applies to this connector.

For example, to achieve a bevelled look for a Link:

// DEFAULT CONNECTOR FOR ALL LINKS IN PAPER:
const paper = new dia.Paper({
// ...
defaultConnector: {
name: 'straight',
args: {
cornerType: 'line',
cornerRadius: 20,
cornerPreserveAspectRatio: true,
},
},
});

// ----------
// FOR A SINGLE NEW LINK:
const link = new joint.shapes.standard.Link({
// ...
connector: {
name: 'straight',
args: {
cornerType: 'line',
cornerRadius: 20,
cornerPreserveAspectRatio: true,
},
},
});

// ----------
// FOR A SINGLE EXISTING LINK:
link.connector('straight', {
cornerType: 'line',
cornerRadius: 20,
cornerPreserveAspectRatio: true,
});

The three most important options of the straight connector are showcased in the following demo:

See CodePen.

The normal and rounded connectors are deprecated.

Migration guide

The following example shows how to migrate a usage of the normal connector to the new straight connector:

Previously:

link.connector('normal');

Version 3.7.0:

link.connector('straight');

The following example shows how to migrate a usage of the rounded connector to the new straight connector:

Previously:

link.connector('rounded', {
radius: 20,
});

Version 3.7.0:

link.connector('straight', {
cornerType: 'cubic',
cornerRadius: 20,
precision: 0,
});

routers.rightAngle - add new router, and deprecate oneSide router

The new rightAngle router returns a route with orthogonal line segments just like orthogonal router, except it chooses the direction of the link based on the position of the source and target anchors (or port position). The router avoids collisions with source and target elements, but does not avoid other obstacles.

The router currently completely ignores Link vertices (user-defined way points).

// DEFAULT ROUTER FOR ALL LINKS IN PAPER:
const paper = new dia.Paper({
// ...
defaultRouter: {
name: 'rightAngle',
args: {
margin: 30,
},
},
});

// ----------
// FOR A SINGLE NEW LINK:
const link = new joint.shapes.standard.Link({
// ...
router: {
name: 'rightAngle',
args: {
margin: 30,
},
},
});

// ----------
// FOR A SINGLE EXISTING LINK:
link.router('rightAngle', {
margin: 30,
});

The full list of available rightAngle.Directions values can be found in our documentation.

The rightAngle router is shown in action in our new Flowchart demo. Notice how Link connection directions change based on the relative position of connected Elements:

See CodePen.

The oneSide router was deprecated.

Migration guide

It is possible to override the default logic and specify the connection sides manually (separately for sourceDirection and targetDirection). This functionality completely replaces and extends the logic of the oneSide router (in which the source and target directions had to be the same).

The following example shows how to migrate a usage of the oneSide router to the new rightAngle router:

Previously:

link.router('oneSide', {
padding: 30,
side: 'top',
});

Version 3.7.0:

link.router('rightAngle', {
margin: 30,
sourceDirection: routers.rightAngle.Directions.TOP,
targetDirection: routers.rightAngle.Directions.TOP,
});

dia.attributes - add props special attribute for setting various HTML form control properties

This feature is related to the improved support for foreign objects - form control elements can be part of element markup if they are part of an SVG <foreignObject> element. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

Adds the props special attribute for setting various HTML form control properties programmatically. Supported properties are:

  • checked
  • selected
  • disabled
  • readOnly
  • contentEditable
  • value
  • indeterminate
  • multiple

This functionality is required because the value of an HTML form control element's attribute (e.g. value in <input value="test"/>) serves only as the initial value.

Details...

In order to be able to set the current value at any time through the model, the HTML form control element's DOM property must be accessed instead. To save you the trouble of accessing these properties through raw DOM elements, we are adding the props special attribute to do so via JointJS objects:

// Set new current value of `input` on the `shape` model:
shape.attr('input/props/value', 'test');
// ...which does the following internally:
//inputEl.value = 'test';

// ----------
// PROBABLY NOT WHAT YOU NEED
// Set new initial value of `input` on the `shape` model:
shape.attr('input/value', 'test');
// ...which does the following internally:
//inputEl.setAttribute('value', 'test');

You can set multiple properties at the same time; you can even do so while setting attributes:

shape.attr('input', {
type: 'text',
placeholder: 'Enter text',
props: {
value: 'text',
readonly: false,
},
});

To set the content of a <textarea> element:

element.attr('textarea/props/value', 'textarea content');

To set the value of a <select> element, pass the value of the <option> element:

element.attr('select/props/value', 'option1');

For a <select> with multiple options, pass an array of values:

element.attr('select/props/value', ['option1', 'option2'], { rewrite: true });

dia.attributes - fix to prevent error when title is used on element with a text node

Details...

For example, for an element with the following markup:

<title @selector="myTitle">existing title</title>

The following does not throw an exception anymore:

el.attr('myTitle/title', 'new title');

dia.ports - fix to apply port layout transformations before ref node's bbox measuring

This fix is part of a series of fixes to make sure that ports using the ref attribute update correctly on size change.

This change fixes an issue where the PortLabel layout was setting the text-anchor attribute (which affects the bounding box of the port's <text> element) too late - i.e. after the bounding box is measured for purposes of the ref calculations (calc()). The value of calc(x) now correctly refers to the x coordinate of the ref node no matter what text-anchor is set.

This means that the ref attributes used with ports positioned to the left of elements (i.e. those having text-anchor: 'end') are defined as expected: x: calc(x - 2). The x: calc(x - calc(w)) syntax, which was previously used as a workaround, is no longer required:

element.addPort({
label: {
markup: [
{
tagName: 'rect',
selector: 'portLabelBackground',
},
{
tagName: 'text',
selector: 'portLabel',
attributes: {
fill: '#333333',
},
},
],
},
size: { width: 20, height: 20 },
attrs: {
portLabelBackground: {
ref: 'portLabel',
fill: '#FFFFFF',
fillOpacity: 0.7,
//x: "calc(x - calc(w + 2))", // WORKAROUND NOT NEEDED ANYMORE
x: 'calc(x - 2)',
y: 'calc(y - 2)',
width: 'calc(w + 4)',
height: 'calc(h + 4)',
pointerEvents: 'none',
},
portLabel: {
fontFamily: 'sans-serif',
pointerEvents: 'none',
},
},
});

The new approach is illustrated in the following demo:

See CodePen.


dia.ports - fix to apply port layout attributes to text element

This was the low-level cause of the issue where PortLabel layout was sometimes trying to set the vertical position of text labels by setting the text-anchor and y attributes on the port label wrapping <g> group element.


dia.HighlighterView - fix to prevent highlighter mounting to unmounted cell views

This fix prevents the mounting and updating of Highlighters attached to unmounted CellViews. This prevents standalone Highlighters (i.e. those whose CellView is unmounted) from being displayed. Additionally, the fix improves performance by not executing the Highlighter update and mounting logic when it is not necessary.

const elementView = graph.getCell('element1').findView(paper);

paper.dumpViews({ viewport: () => false }); // hide all cell views
const highlighter = joint.dia.HighlighterView.add(elementView, 'root', id, {
layer: dia.Paper.Layers.FRONT,
});
// Assert: `!highlighter.el.isConnected`

paper.dumpViews({ viewport: () => true }); // show the element view
// Assert: `highlighter.el.isConnected`

util - remove Lodash util functions

We are working on removing dependencies between internal JointJS code and external libraries. In this release, as a first step of this longer-term initiative, we replaced all util functions which were merely internal aliases to Lodash functions; these are now handled with our own code, which can be found in the joint/src/util/utilHelpers.mjs file. JointJS internal code no longer has a dependency on Lodash.

The full list of rewritten methods and their code can be found in our public JointJS GitHub repository.

Note that even though JointJS no longer uses any Lodash methods internally, we still kept an explicit dependency on Lodash inside the JointJS package.json file. This is because JointJS still depends on two external libraries which have a dependency on Lodash - Backbone and Dagre.

Details...

However, note that Backbone actually requires merely the Underscore library as a dependency - i.e. a lower-weight subset of Lodash. If you are an advanced user of NPM and your program does not rely on the parts of JointJS which need the Dagre library (e.g. layout.DirectedGraph) you may experiment with exchanging the Lodash dependency in your package.json for an appropriate version of Underscore in order to further reduce the size of the JointJS package. If you are importing libraries directly in your HTML (as illustrated in our tutorial, for example), you may achieve this by simply replacing the import script:

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

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.js"></script>
<!-- lodash no longer required when used without Dagre -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.js"></script> -->
<!-- instead import underscore -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.6/underscore.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.7.2/joint.js"></script>

<script type="text/javascript">
// Code must not use any components which require Dagre
const graph = new joint.dia.Graph();
const paper = new joint.dia.Paper({
el: document.getElementById('paper'),
model: graph,
// ...
});
// ...
</script>
</body>
</html>

util.breakText - support lineHeight in px units

There are now three units which can be used to specify lineHeight - em (e.g. 2em), px (e.g. 12px), and no unit (e.g. 12) with same behavior as px.

Details...

This change was mostly done for the sake of consistency - previously, the explicit px unit was not recognized, but providing no unit was nevertheless treated as px.

const styles = {
fontSize: 14,
fontFamily: 'Arial, Helvetica, sans-serif',
};
const t =
'This is very very very very very very very very very very very very very very very very very very very very very very long text';
const lineHeight = '21px';
// ...which is equivalent to...
//const lineHeight = 21;
// ...which is equivalent to...
//const lineHeight = '1.5em';
const cell = new joint.shapes.standard.Rectangle({
size: { width: 50, height: 250 },
attrs: {
text: {
text: t,
textWrap: {},
...styles,
lineHeight,
},
},
});
graph.addCell(cell);

util.normalizeEvent - fix to always return a dia.Event

This was the low-level cause of the issue where objects of incorrect type were passed to Event handlers on touch screens. Event handlers now receive objects of type dia.Event in all cases.


util.parseDOMJSON - add logic to process JSON with string array items as HTML text nodes

This feature is related to the improved support for foreign objects. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

New logic has been added to the parseDOMJSON() function to parse string-type children of elements as HTML text nodes. This can be mixed-in with object-type children as necessary.

For example, the following JSON:

{
tagName: 'p';
children: [
'a',
{
tagName: 'span',
children: ['b'],
},
'c',
];
}

...is parsed into the following HTML:

<p>a<span>b</span>c</p>

This matches the reverse algorithm of the util.svg() function.


util.svg - keep correct order of HTML text nodes when parsing <foreignObject> to JSON

This feature is related to the improved support for foreign objects. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

When HTML of the form <p>a<span>b</span>c</p> is parsed by the util.svg() function, any HTML text nodes within an HTML element are parsed as string-type children. This preserves the relative ordering between the HTML text nodes and any descendant HTML elements.

In addition, HTML-style whitespace removal is implemented for the parsed JSON. This means that any sequence of spaces and/or special symbols (e.g. newlines) is replaced with only one space ( ). An additional fix ensures that empty textContent is not generated.

For example, the JSON generated for the HTML <p>a<span>b</span>c</p> is the following:

{ "tagName": "p",  "children": [ "a", { "tagName": "span", "textContent": "b" }, "c" ] }

Previously, the content of all HTML text nodes within an HTML element was stored in the textContent property of the object corresponding to the HTML element in the parsed JSON. However, this led to problems when the text content was read in sequential order (i.e. in the order (1) textContent of the HTML element, followed by (2) textContent of first descendant HTML element, followed by (3) textContent of all other descendant HTML elements...) - the combined text content of the three elements was interpreted as "acb".


util.svg - fix to use lowercase for tagName of HTML elements when parsing <foreignObject> to JSON

This fix is related to the improved support for foreign objects. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

XHTML documents must use lowercase for all HTML element and attribute names, but the util.svg() function was parsing them as uppercase. We now detect the namespace of the tags and automatically switch to lowercase within XHTML context.

The JSON generated for <div namespace="http://www.w3.org/1999/xhtml"></div> is the following:

{ "tagName": "div", "namespaceURI": "http://www.w3.org/1999/xhtml" }

util.svg - fix textContent to contain HTML text nodes from all descendants when parsing <foreignObject> to JSON

This fix is related to the improved support for foreign objects. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

When HTML of the form <p>a<span>b</span></p> is parsed by the util.svg() function, all descendant HTML elements (i.e. those embedded within an HTML element) receive their own textContent based on their own HTML text nodes.

For example, the JSON generated for <p>a<span>b</span></p> is the following:

{ "tagName": "p",  "textContent": "a", "children": [{ "tagName": "span", "textContent": "b" }] }

Previously, the textContent of the parent <p> was set to "ab" instead, duplicating the textContent of the descendant - the combined text content of the two elements was interpreted as "abb".

Note that this fix resolves only the issue of duplicated textContent between the parent and its descendant HTML elements. A different feature within this release ensures that the relative ordering between the HTML text nodes and any descendant HTML elements is preserved.


util.svg - fix to prevent setting empty textContent when parsing <foreignObject> to JSON

This fix is related to the improved support for foreign objects. To get started with foreign objects in JointJS, refer to our new Foreign Objects tutorial.

The textContent property is no longer set if its value would consist only of whitespace (which is invalid in HTML).

The three <p> elements in the following examples get no textContent when parsed to JSON:

<p class="case-1"></p>
<p class="case-2"></p>
<p class="case-3"><span>span</span></p>

Geometry - fix getSubdivisions() method for straight-line Curve objects

This was the low-level cause of the issue where Link labels were jumping while being dragged alongside Links represented as straight-line Curve objects.

Details...

In the edge case of straight-line Curves (and only those), the subdivision algorithm exited early and returned subdivisions with much lower precision than expected by other Curve algorithms (instead of producing subdivisions with slightly higher precision than needed, as expected). This forced other Curve algorithms (including g.Curve.closestPointT() which is ultimately used by LinkView to find the closest point on the Curve given the user cursor coordinates), to find their own subdivisions at varying levels of precision. Not only were these constant calculations computationally inefficient, they also had the end result that for some pairs of input points with 1px offset the returned points were wildly different, while other sets of input points had the same returned point - the observed Link label jumping.

The Curve subdivision algorithm now has special handling logic for this edge case which produces the expected amount of Curve subdivisions (i.e. slightly higher than needed by all other Curve algorithms) to eliminate this issue.


Other additions

Add Foreign Object Tutorial - Learn how to use <foreignObject> SVG elements to embed HTML into your JointJS diagrams.

Go to tutorial