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
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.
Add DWDM (Dense Wavelength-Division Multiplexing) demo as an example of a network graphical view
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.
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.
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.
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,
// ...
}
);
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 fix | After 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 fix | After 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 resolveddependencies
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 nodependencies
- 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 newrefreshSource(path)
andrefreshSources()
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 |
---|---|
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 - 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 |
---|---|
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.
ui.Stencil - add support for Links
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
orundefined
, the Tooltip is shown with default value - i.e. the value of thedata-tooltip
attribute on the target HTMLElement (or the value of a differentdata-
attribute, if specified by thedataAttributePrefix
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 fix | After 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 fix | After 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 fix | After 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 fix | After 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:
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 |
---|---|
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 |
---|---|
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 thehorizontalAlign
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 tofalse
, 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:
- Vertical position of PortLabel text labels was set incorrectly.
- Ports were rendered twice.
- Port
ref
node's bounding box was calculated incorrectly. - Port layout transformations were applied after
ref
node's bounding box was measured.
Together, these changes have the following effect:
Before all fixes | After 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 fix | After 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 fix | After 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 fix | After 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 fix | After 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
}
});
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()
andfitParent()
methods - Keeps
deep
andpadding
options for both methods - Adds
expandOnly
andshrinkOnly
options for both methods - Adds
terminator
option forfitParent()
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 call | Before call | Result |
---|---|---|
r1.fitToChildren({ padding: 10 }) | ||
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 call | Before call | Result |
---|---|---|
r1.fitToChildren({ padding: 10, deep: true }) | ||
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 call | Before call | Result |
---|---|---|
r1.fitToChildren({ expandOnly: true, padding: 10 }) | ||
r1.fitToChildren({ expandOnly: true, padding: 10, deep: true }) | ||
r111.fitParent({ expandOnly: true, padding: 10 }) | ||
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 call | Before call | Result |
---|---|---|
r1.fitToChildren({ shrinkOnly: true, padding: 10 }) | ||
r1.fitToChildren({ shrinkOnly: true, padding: 10, deep: true }) | ||
r111.fitParent({ shrinkOnly: true, padding: 10 }) | ||
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 call | Before call | Result |
---|---|---|
r1.fitToChildren({ shrinkOnly: true, padding: 10, deep: true }) | ||
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 callingfitParent()
without thedeep
option - If
terminator
is not an embedding ancestor of the Element, the result is the same as callingfitParent()
with thedeep
option without aterminator
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 theoptions
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, theoptions
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 |
---|
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.
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.
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.