Back to jointjs.com.
Jump to other additions.
CHANGELOG
-
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 fortagName
of parsed HTML elements, fixes fortextContent
to contain HTML text nodes from all descendants and to keep those in correct order without creating emptytextContent
, 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 attributeprops
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
-
Improve zooming in Chatbot Application using
pinch
andpan
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 theundo()
method throwing an error.The issue was solved by defining a React
useEffect
on the Input component in order to callonBlur()
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 ofdia.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 newstraight
connector. Also illustrates the new alignment options of the PapertransformToFitContent()
method.See the Pen JointJS: Light and Dark Mode For Diagrams by JointJS (@jointjs) on CodePen.
-
Revamp FTA demo
Uses a custom highlighter and the new
straight
connector. Also illustrates the new alignment options of the PapertransformToFitContent()
method.See the Pen JointJS: Fault Tree Analysis by JointJS (@jointjs) on 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 the Pen JointJS: Risk of Investment by JointJS (@jointjs) on 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 callingundo()
,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 valuesThis 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 istrue
by default. You can export to SVG analogously by using thetoSVG()
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 the Pen JointJS+: raster.toPNG by JointJS (@jointjs) on 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>
elementThis 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()
methodFor 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 the Pen JointJS+: graphUtils.toAdjacencyList by JointJS (@jointjs) on CodePen.
-
ui.Clipboard - add
deep
option tocopyElements()
to also copy all embedded descendants of provided cellsThis 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 amargin
property applied.Before fix After fix
-
ui.FreeTransform, ui.Halo, ui.Selection - fix to remove unprefixed
user-drag
CSS propertyDetails...
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 modelThis 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 asource
callbackAdds new functionality to Inspector - it is now possible to specify the
options
parameter of a field as an object with asource
callback (or a Promise) anddependencies
. 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 the Pen JointJS+: Inspector source callback by JointJS (@jointjs) on CodePen.
- The
-
ui.PaperScroller - fix to stop panning after mouse was released outside the window
Stop the PaperScroller panning also when the
mouseup
ortouchend
event occurs outside thedocument.body
(outside the browser window).We now listen to
document
events instead ofdocument.body
events.
-
ui.PathDrawer - add
enableCurves
optionThis 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 the Pen JointJS+: ui.RadioGroup by JointJS (@jointjs) on 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 thescrollWhileDragging
option on the PaperScroller. For a simple example, if thescrollWhileDragging
option on PaperScroller istrue
, 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
optionThis option is
false
by default. Setting it totrue
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 optionThe
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 optionBy 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 the Pen JointJS+: additionalSnapPoints by JointJS (@jointjs) on CodePen.
-
ui.Stencil, ui.TreeLayoutView - use the same
cellNamespace
as the target PaperJointJS will now use the
cellViewNamespace
of the main Paper and thecellNamespace
of the associated Graph as the defaultcellViewNamespace
andcellNamespace
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 the Pen ui.Stencil, ui.TreeLayoutView - use the same cellNamespace by JointJS (@jointjs) on 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 the Pen JointJS+: Links in Stencil by JointJS (@jointjs) on 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 the Pen JointJS+: Stencil Drag API by JointJS (@jointjs) on 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 ofdia.CellView
to enable dragging-and-dropping Element images to other Elements:See the Pen JointJS+: Stencil Drag API by JointJS (@jointjs) on 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 twokeydown
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 theonInput
event handler instead.
-
ui.Toolbar - trigger
input
andchange
events for widgets (checkbox, toggle, inputText, inputNumber, textarea)According to the Web API specification,
input
andchange
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 the Pen JointJS+: joint.ui.Toolbar events by JointJS (@jointjs) on 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 - aninput
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 typeThe
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 the Pen ui.Tooltip: content option by JointJS (@jointjs) on CodePen.
- If the returned value is
-
ui.TreeLayoutView - add
layoutFunction
callback optionBy 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 defaultlayout()
function), and then provide your own custom logic (possibly callinglayout()
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()
andgetParentLaneId()
methodsThe
getLanesIds()
method returns an array of all lane identifiers (in a non-specific order), while thegetParentLaneId()
method returns theid
of the parent lane of a specified lane (ornull
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 incustomId
andparentId
lane metricsProviding an empty string (
''
) asid
of a lane now has the same result as providingundefined
(or not setting laneid
at all) - the lane is auto-assigned anid
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
-
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 iconsAccording to our documentation of the
items
option ofjoint.shapes.standard.Record
, it should be possible to find itemid
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 theitem-id
. This makes it possible to react to user interaction with item icon and, for example, change Recorddata
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 Recorddata
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 the Pen JointJS+: Searchable Record by JointJS (@jointjs) on 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 the Pen JointJS+: Stencil Drag API by JointJS (@jointjs) on 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
andy
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 todia.ports
.Before fix After fix
-
layout.TreeLayout - add
symmetrical
option to ensure that distance between children of the same element is the sameBy 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 totrue
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()
methodRemoves 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
-
dia.Paper - add
overflow
optionBy 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
andhorizontalAlign
totransformToFitContent()
methodThis 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
andhorizontalAlign
, 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'
andhorizontalAlign: 'middle'
intransformToFitContent()
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 the Pen JointJS: Light and Dark Mode For Diagrams by JointJS (@jointjs) on CodePen.
- 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
-
dia.Paper - add
element:magnet:pointerdown
,element:magnet:pointermove
andelement:magnet:pointerup
eventsA 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 onelement: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.
-
dia.Paper - improve event handling of form control elements inside
<foreignObject>
elementsThis 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
optionThis 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 apointerdown
event is registered on a cell view (i.e. amousedown
ortouchstart
). 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 settingpreventDefaultViewAction
tofalse
) and run the default action - for example, to make sure that clicking on a view triggers ablur
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
ortouch-action
CSS properties - which you should investigate.
-
dia.Paper - add
drawGridSize
optionThis option overrides Paper
gridSize
option exclusively for drawing of the grid. In case adrawGridSize
is provided, thegridSize
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 lowgridSize
to enable high precision when dragging link endpoints, and switching it back for dragging elements within a grid) while keepingdrawGridSize
fixed at the element-dragginggridSize
value.
-
dia.Paper - add
autoFreeze
optionBy default, this advanced option is
false
, which instructs the Paper to periodically check the Paperviewport
to find out whether the visible area has been changed. Setting theautoFreeze
option totrue
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 Paperviewport
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 calledIf a view is updated manually (by calling
requireView()
), thenbeforeRender
andafterRender
callbacks - as well as therender: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.
- A scheduled update on the CellView which called
-
dia.Paper - fix to send
mousewheel
events when CTRL key is pressed while there are nopaper:pinch
subscribersTouchpad devices send a
mousewheel
event with a fake CTRL keypress to signify a pinch gesture. When we implemented support for thepaper:pinch
event, our handler logic caused an issue in a customer application which depends on handling genuinemousewheel
+ 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 preventmousewheel
+ 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 the Pen JointJS: Connector Arrows by JointJS (@jointjs) on CodePen.
-
dia.Paper - fix event handlers to correctly receive
dia.Event
on touch screensPreviously, 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 eventThe
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 magnetThe
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 theinteractive
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 removedDetails...
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() onpointerup
).
-
dia.ElementView - fix to correctly update port nodes with
ref
on size changeScaling a port node with styling modified via the
ref
attribute (e.g. putting a background rectangle to port label text and usingcalc()
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 labeltouchmove
event - since the original target of the event was removed from the DOM, thetouchmove
event stopped triggering. Link labels are now updated synchronously while dragging viatouchmove
events in progress.
-
dia.LinkView - fix to take
defaultLabel.size
into account for link label size calculationsThis fix makes it possible to specify
defaultLabel.size
when initializing a Link and have its properties merged with the properties oflabel.size
of individual Link labels. This brings thesize
argument into alignment with other label arguments (markup
,attrs
,position
) which also inherit from theirdefaultLabel
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
andabsoluteOffset
optionsFixes an issue where Link labels using the
keepGradient
andabsoluteOffset
options had unexpected rotation applied to them. This was caused by a low-level issue in theg.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 samey
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 differentx
andy
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()
andisDefaultInteractionPrevented()
methodsAdds 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 the Pen JointJS+: Stencil Drag API by JointJS (@jointjs) on 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 the Pen JointJS+: Stencil Drag API by JointJS (@jointjs) on 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 thelink: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
andtouchend
events). This fix expands that functionality also topointermove
andpointerup
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 boxThis 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) theref
node and (2) the common parent of theref
node and the current node (to which we want to set the custom attribute). Previously, theref
node was transformed by all transformations between theref
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
(theref
node) but not toa
(the current node).- Markup 1:
The
b
anda
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 thea
node as to theb
node. Therefore, the bounding box ofb
from the perspective ofa
is{ x: 1, y: 2, width: 3, height: 4 }
.- Markup 2:
While the
b
node is affected by a transformation, thea
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 thea
node is not a descendant of the<g>
SVG element, it is not affected by the transformation. Therefore, the bounding box ofb
from the perspective ofa
is{ x: 12, y: 15, width: 3, height: 4 }
.
-
dia.Element - add
expandOnly
andshrinkOnly
options tofitToChildren()
andfitParent()
methodsEnhances 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, andr12
at top-right), andr11
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 offitToChildren()
), or to all embedding ancestors of this Element (in the case offitParent()
). 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()
andfitParent()
methods as two sides of the same coin. WhilefitToChildren()
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
andshrinkOnly
. 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
andr111
in our example - when such a situation is encountered in an iteration of theshrinkOnly
algorithm, that iteration is skipped. In our examples, that means that the only Element adjusted isr1
, 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 thedeep
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 andr12
at top-right), but not according to its grandchildr111
(at bottom-left). The behavior is the same for both functions,fitToChildren()
andfitParent()
, 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 thedeep
option on thefitParent()
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
- Adds
-
dia.Cell - fix to always send
propertyPath
andpropertyValue
options when callingprop()
Calling the
prop()
function on a Cell (also used internally when calling theattr()
function) triggers achange
event on the Cell (unless the function is called with the optionsilent: true
). Thatchange
event can be handled via a callback function which is provided with two parameters - the Cell in question and an additionaloptions
object. Theoptions
object parameter in this callback is the subject of this fix.The
prop()
function can be used with a variety of function signatures. Theoptions
object always repeats the options provided toprop()
(e.g.silent
orrewrite
). However, for some of the function signatures, theoptions
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 }*/ });
- Function signature
-
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 thepath
argument resolved to a top-level attribute and thevalue
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 calledThis change fixes an issue where the stacking order (equivalent to HTML z-index) among Cells was sometimes not preserved when the functions
toFront()
andtoBack()
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 the Pen JointJS: toFront, toBack bug by JointJS (@jointjs) on CodePen.
-
dia.Cell - fix to prevent Cell
id
from beingundefined
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 circumventid
auto-generation when creating a new Cell by explicitly providing anid
with the value ofundefined
.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 interactionMake sure that the
cell:mouseleave
event is triggered after drag-and-drop interaction even if the pointer is outside the LinkView onmouseup
.Details...
This fix is necessary since we undelegate all paper events on
mousedown
behind the scenes (to prevent other events likemouseover
andmouseout
events from triggering during the drag-and-drop interaction) with the expectation that they would be delegated back onmouseup
. The issue with that approach is that if themouseup
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>
elementsThis fix makes sure that the
<mask>
nodes added to Paper’s<defs>
node by MaskHighlighter (highlighters.mask
) no longer receiveclass
attributes (copied over from source Cell nodes) when the Highlighter is added to a CellView via theadd()
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 the Pen JointJS: Light and Dark Mode For Diagrams by JointJS (@jointjs) on CodePen.
-
connectionPoints.boundary - add option to disable automatic magnet lookup within
<g>
elementsAllows you to specify
<g>
elements as magnets and explicitly define magnets usingmagnetSelector
when using theboundary
connection point.Details...
This change concerns the
selector
option of theboundary
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
isundefined
) is to use the first non-group (<g>
) descendant of the Link source/target Element’s SVGElement. Alternatively, aselector
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 amagnetSelector
attribute on a specific Elements.This can now be achieved by setting the
selector
option of theboundary
connection point logic tofalse
. Previously, this required writing a custom connection point function to distinguish between different cases (which overrode anymagnetSelector
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`
-
connectors.straight - add new connector, and deprecate
normal
androunded
connectorsAdds a new connector which combines existing
normal
androunded
connectors with additional options to make path bevelled / with gaps at corners of the path. Thenormal
androunded
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 forrounded
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 isfalse
(i.e. the default forrounded
connector), but note that setting it totrue
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 the Pen JointJS: straight connector by JointJS (@jointjs) on CodePen.
The
normal
androunded
connectors are deprecated.See migration guide
The following example shows how to migrate a usage of the
normal
connector to the newstraight
connector:// NEW APPROACH: link.connector('straight'); // ---------- // DEPRECATED: link.connector('normal');
The following example shows how to migrate a usage of the
rounded
connector to the newstraight
connector:// NEW APPROACH: link.connector('straight', { cornerType: 'cubic', cornerRadius: 20, precision: 0 }); // ---------- // DEPRECATED: link.connector('rounded', { radius: 20 });
-
routers.rightAngle - add new router, and deprecate
oneSide
routerThe new
rightAngle
router returns a route with orthogonal line segments just likeorthogonal
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 the Pen JointJS: Light and Dark Mode For Diagrams by JointJS (@jointjs) on CodePen.
The
oneSide
router was deprecated.See migration guide
It is possible to override the default logic and specify the connection sides manually (separately for
sourceDirection
andtargetDirection
). This functionality completely replaces and extends the logic of theoneSide
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 newrightAngle
router:// NEW APPROACH: link.router('rightAngle', { margin: 30, sourceDirection: routers.rightAngle.Directions.TOP, targetDirection: routers.rightAngle.Directions.TOP }); // ---------- // DEPRECATED: link.router('oneSide', { padding: 30, side: 'top' });
-
dia.attributes - add
props
special attribute for setting various HTML form control propertiesThis 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 nodeDetails...
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 theref
calculations (calc()
). The value ofcalc(x)
now correctly refers to thex
coordinate of theref
node no matter whattext-anchor
is set.This means that the
ref
attributes used with ports positioned to the left of elements (i.e. those havingtext-anchor: 'end'
) are defined as expected:x: calc(x - 2)
. Thex: 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 the Pen JointJS: Working with Ports by JointJS (@jointjs) on 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
andy
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
functionsWe 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 thejoint/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 yourpackage.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 unitsThere 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 aspx
.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 aspx
.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-typechildren
of elements as HTML text nodes. This can be mixed-in with object-typechildren
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 JSONThis 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 theutil.svg()
function, any HTML text nodes within an HTML element are parsed as string-typechildren
. 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 (
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 JSONThis 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 JSONThis 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 theutil.svg()
function, all descendant HTML elements (i.e. those embedded within an HTML element) receive their owntextContent
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 thetextContent
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 JSONThis 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 notextContent
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 objectsThis 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.