Back to jointjs.com.
CHANGELOG
- add Element Tools tutorial
- add Rappid integration with Salesforce Lightning tutorial
-
Prevent prototype pollution & DoS - fix
util.setByPath()
andutil.unsetByPath()
, upgrade lodash v4.17.20const obj = {}; // assert: obj.polluted === undefined joint.util.setByPath({}, 'constructor/prototype/polluted', true, '/'); joint.util.setByPath({}, '__proto__/polluted', true, '/'); // assert: obj.polluted === undefined
const obj = {}; // assert: typeof obj.toString === 'function' joint.util.unsetByPath({}, 'constructor/prototype/toString', '/'); joint.util.unsetByPath({}, '__proto__/toString', '/'); // assert: typeof obj.toString === 'function'
-
apps.Visio.BpmnImport - convert Microsoft Visio BPMN VSDX file to a Rappid diagram
ES2018 & TypeScript version
-
apps.Visio.BpmnExport - export Rappid BPMN diagram to VSDX file
ES2018 & TypeScript version
-
apps.Visio.FlowChartImport - convert Microsoft Visio Cross-Functional FlowChart VSDX file to a Rappid diagram
ES2018 & TypeScript version
-
apps.Visio.OrgChartImport - convert Organizational Chart VSDX file to a Rappid diagram
ES2018 & TypeScript version
-
apps.Visio.DefaultImport - show Microsoft Visio VSDX file inside a Rappid diagram as is
ES5 version
-
apps.Planogram - new application showcasing the Stencil drag & drop API
ES6 & TypeScript version
-
apps.ChatBot - slight change of the structure to create a
<chatbot/>
component, enable virtual rendering (VR*)VR - only render what the user sees, keeping the number of rendered cells to the minimum
-
apps.BPMNEditor - add new BPMN2 shapes
-
apps.OrgChart - redesign
ES6 & TypeScript version
-
apps.Layout - redesign
ES6 & TypeScript version
-
apps.Collapsible - improve viewport matching; indicate that export to png being processed
-
demo.ELK - add Eclipse Layout Kernel demo
An example of an integration with Eclipse Layout Kernel (ELK)’s automatic layout algorithms.
-
demo.Container - add collapse / expand container demo
An example of container elements with collapse / expand buttons. The demo uses viewport paper option to toggle the visibility of collapsed and expanded cells.
-
demo.Typescript - show class style shape definition
import { shapes, dia } from './vendor/joint'; export class MyShape extends dia.Element { defaults() { return { ...super.defaults, type: 'myNamespace.MyShape', size: { width: 100, height: 80 }, attrs: { body: { refCx: '50%', refCy: '50%', refRx: '50%', refRy: '50%', strokeWidth: 2, stroke: '#333333', fill: '#FFFFFF' }, label: { textVerticalAnchor: 'middle', textAnchor: 'middle', refX: '50%', refY: '50%', fontSize: 14, fill: '#333333' } } } } markup = [{ tagName: 'ellipse', selector: 'body' }, { tagName: 'text', selector: 'label' }] test(): void { console.log(`A prototype method test for ${this.get('type')}`); } static staticTest(i: number): void { console.log(`A static method test with an argument: ${i}`); } } Object.assign(shapes, { myNamespace: { MyShape } });
-
demo.HTML - implement z-index sorting
HTMLElementViews
are sorted based on the modelz
attribute.
-
format.Visio - new plugin to export and import Microsoft Visio VSDX file format.
With focus on mapping Rappid Shapes onto Visio Stencil Shapes, and vice versa.
package.json"dependencies": { "@clientio/rappid": "file:rappid.tgz", /* visio plugin is an independent package */ "@clientio/rappid-visio": "file:rappid-visio.tgz" }
app.js
import { util } from '@clientio/rappid'; import { VisioArchive } from '@clientio/rappid-visio'; const archive = await VisioArchive.fromURL('/visio/bpmn.vsdx'); const [page1, page2] = archive.document.getPages(); // Default Import const pageContent = await page1.getContent(); const cells = pageContent.toGraphCells(); graph.resetCells(cells); paper1.setDimensions(page1.width, page1.height); // Default Export await page2.fromPaper(paper2); const blob = await archive.toVSDX({ type: 'blob' }); util.downloadBlob(blob, 'bpmn-export.vsdx');
-
ui.FreeTransform - add
resizeGrid
optionnew joint.ui.FreeTransform({ /* */ resizeGrid: { width: 2, height: 3 } // While resizing // the `width` changes by step of `2` // the `height` changes by step of `3` });
-
ui.Inspector - Fix
when
conditions for generic nested objects and objects within lists// Allows reading values from generic objects within lists for the // purposes of the `when` condition. // // ports: [ // { label: { position: { name: 'manual', args: { y: 5 }}}}, // { label: { position: { name: 'top' }}} // ] const inspectorInputs = { ports: { type: 'list', item: { type: 'object', properties: { label: { // <-- Generic Object position: { // <-- Generic Object name: { type: 'select', options: ['top', 'bottom', 'manual'], label: 'Label Position', index: 1, }, args: { y: { type: 'number', label: 'Vertical Label Position', when: { eq: { 'ports/${index}/label/position/name': 'manual' // List / Number / Generic Object / Generic Object / Select } } } } } } } } } };
-
ui.PaperScroller - set max size to prevent an infinite loop
By default, the PaperScroller element fill the space of its containing element. Make sure that at least one of the ancestor elements have a fixed size when using
autoPaperResize
option. Otherwise the PaperScroller will grow infinitely. This patch only prevents browser from freezing.
-
ui.PaperScroller - triggers
scroll
eventlet visibleArea = scroller.getVisibleArea(); scroller.on('scroll', util.debounce(() => { // Keep the cached visible area up-to-date visibleArea = scroller.getVisibleArea(); }));
- ui.Selection - fix elements resizing (when multiple elements are rotated)
-
ui.Snaplines - improve integration with
ui.PaperScroller
Snaplines inside a PaperScroller does not affect the scrollbars anymore.
-
ui.Stencil - add drag & drop events and API
Events:
stencil.on('element:dragstart', (cloneView, evt, cloneArea, validDropTarget) => { // Triggered when the user starts dragging an element. }); stencil.on('element:drag', (cloneView, evt, cloneArea, validDropTarget) => { // Triggered while an element is being dragged. }); stencil.on('element:dragend', (cloneView, evt, cloneArea, validDropTarget) => { // Triggered when a drag operation ends (such as by releasing a mouse button // or by calling the `cancelDrag()` method). // This event fires regardless of whether the drag completed or was canceled. // The `dragend` event handler // can check `stencil.isDragCanceled()` and // the `validDropTarget` parameter to determine whether the drag // operation had succeeded or not. }); stencil.on('element:drop', (elementView, evt, x, y) => { // Triggered when an element is dropped onto a valid target paper. });
Methods:
stencil.cancelDrag({ dropAnimation: true }); stencil.isDragCanceled();
-
ui.Stencil - add
usePaperGrid
optionSnap elements to the target paper grid while dragging.
new joint.ui.Stencil({ /* */ usePaperGrid: true });
-
ui.Stencil - add
container
optionBy default, this plugin appends the temporary paper for showing the preview of an element to
document.body
. It can be changed if not desired (e.g. when you write a diagram component and it should not touch anywhere outside the component’s DOM).new joint.ui.Stencil({ /* */ // A CSS selector or a DOM element is the container element, // which the element being dragged is appended to. container: document.getElementById('my-component-container-id') });
-
ui.Stencil - add
filter()
methodconst stencil = new joint.ui.Stencil({ /* */ search: (element, keyword) => { const type = element.get('type').toLowerCase(); return type.include(keyword.toLowerCase()); } }); // Programmatically filter the stencil elements based on a specific keyword stencil.filter('standard');
-
ui.Stencil - make i18n easier
stencil.el.dataset.textNoMatchesFound = 'No matches found!'; stencil.el.querySelector('.search').placeholder = 'My Placeholder';
-
ui.TreeLayoutView - add drag & drop API
Live Stencil Integration Demo & Documentation
Methods:
const elements = [element1, element2]; const treeView = new joint.ui.TreeLayoutView({ /* */ }); // Start a new drag operation with <code>elements</code> set as its source. treeView.dragstart(elements, x, y); // Drag the source elements and display preview of the drop location if any. treeView.drag(elements, x, y); // End the drag operation and reconnect/translate the source elements // if there was a valid drop target. treeView.dragend(elements, x, y); // Stop the current drag operation. treeView.cancelDrag(); // Has the current drag operation valid drop target? // i.e. an element can be reconnected or translated. treeView.canDrop();
-
ui.TreeLayoutView - add
validatePosition()
optionAdd validation for dropping/moving elements to unoccupied area (to disallow new roots).
const treeView = new joint.ui.TreeLayoutView({ /* */ validateConnection(element, parent) { // validate if 2 elements can be connected together }, validatePosition(element, x, y) { // validate if an element can be disconnected/translated and dropped at a position // return `false` if loose elements (without a parent) are not allowed } });
-
shapes.BPMN2 - new set of BPMN shapes
- complete set of BPMN 2.0 shapes & markers & icons
- implements
Activity
,Annotation
,Conversation
,DataObject
,DataStore
,Event
,Gateway
,Group
andPool
elements - implements
AnnotationLink
,ConversationLink
,DataAssociation
andFlow
links - better rendering performance
- do not suffer from
non-scaling-stroke
issues in IE11 & MS Edge -
designed to work well with highlighters and boundary connection points
-
pools with lanes and milestones
-
shapes.standard.Record - preserve groups after
removeItem()
Fixes a bug when removing the last item of a group caused the entire group to be removed.
const items = [ [{ id: '1' }], // group 1 [{ id: '2' }] // group 2 ]; record.set('items', items); record.removeItem('1'); // assert: record.get('items') === [ [], [{ item: '2' }] ]
-
shapes.standard.Record - set
dry
run for building the markupThe internal markup’s change is not put on the undo / redo stack when used with
dia.CommandManager
(and not causing an exception when undone).
-
dia.Paper - add
snapLabels
optionconst paper = new dia.Paper({ /* ... */ snapLabels: true });
-
dia.Paper -
restrictTranslate
option defined as function returning a functionconst paper = new dia.Paper({ /* ... */ // Restrict an element movement along a path. restrictTranslate: function(elementView, x0, y0) { // x0 and y0 are pointer coordinates at the start of the translation const path = new g.Path('M 20 20 L 200 200 L 380 20'); const { x: x1, y: y1, width, height } = elementView.model.getBBox(); // The initial difference between the pointer coordinates // and the element position const dx = x1 - x0; const dy = y1 - y0; return function(x, y) { // Find the point on the path. const point = path.closestPoint({ x: x - dx, y: y - dy }); // Put the center of the element on this point point.offset(-width / 2, -height / 2); return point; }; } });
-
dia.Graph - fix
hasActiveBatch()
The method returns a correct result when called with no arguments.
graph.startBatch('my-batch'); // assert: graph.hasActiveBatch() === true // assert: graph.hasActiveBatch('my-batch') === true // Translate all elements in the graph by 10 pixels graph.getElements().forEach(element => element.translate(10, 0)); graph.stopBatch('my-batch'); // assert: graph.hasActiveBatch() === false // assert: graph.hasActiveBatch('my-batch') === false // Restore the original position of all elements in the graph commandManager.undo();
-
dia.Element - prevent unnecessary rounding errors in
resize()
Make a special case for unrotated elements and simplified the calculations of
x
,y
,width
andheight
to prevent binary floating-point rounding errors.
-
dia.Element - add
insertPort()
to support inserting port at a given position// An element with at least 2 `out` ports const group = 'out'; const [port1, port3] = element.getGroupPorts(group); const port2 = { id: 'p2', group }; // Insert `port2` between `port1` and `port3` element.insertPort(port3, port2);
-
dia.Link - fix order of points in
getPolyline()
const polyline = link.getPolyline(); // `polyline.points` ordered as below: // [sourcePoint, vertex1, vertex2, ..., vertexN, targetPoint]
-
dia.LinkView - prevent connection validation on magnets previously validated
If a magnet is evaluated as valid for connection and the user moves the cursor while staying over the same magnet - no validation is performed. This is important when
snapLinks
option is enabled and the validation logic looks if the magnet has any connected links (e.g. a rule that ports can have only a single link).new joint.dia.Paper({ /* */ snapLinks: true, validateConnection: (sourceView, sourceMagnet, targetView, targetMagnet) => { if (isMagnetAlreadyConnected(targetView, targetMagnet)) { return false; } return true; } })
-
dia.CellView - fix update order of nodes when
ref
in useNodes with
ref
attribute are updated before the other nodes to prevent undesired results.// The element view must update the nodes in a specific order element.attr({ first: { fill: 'black' }, second: { fill: 'green' }, third: { fill: 'blue', ref: 'second' }, fourth: { fill: 'red', ref: 'third' } });
-
dia.ToolsView - enable adding tool to a view, which has not been rendered
// assert: paper.isAsync() === true const element = new joint.shapes.standard.Rectangle(); const boundaryToolsView = new joint.dia.ToolsView([ new joint.elementTools.Boundary() ]); const elementView = element.findView(paper); // assert: elementView.el.childNodes.length === 0 (view has not been rendered) elementView.addTools(boundaryToolsView);
-
highlighters - revamped, add a new API
- Highlight elements, links, ports and labels
// Highlight an element or link const cellSelector = { selector: 'body' }; joint.highlighters.stroke.add(cellView, cellSelector, 'my-cell-highlight'); // Highlight an element's port const portSelector = { port: 'port1', selector: 'portBody' }; joint.highlighters.stroke.add(elementView, portSelector, 'my-port-highlight'); // Highlight a link's label const labelSelector = { label: 1, selector: 'labelBody' }; joint.highlighters.stroke.add(linkView, labelSelector, 'my-label-highlight');
- Supports asynchronous updates (no need to wait for views to be rendered, improved performance)
// assert: paper.isAsync() === true const element = new joint.shapes.standard.Rectangle(); const elementView = element.findView(paper); // assert: elementView.el.childNodes.length === 0 (view has not been rendered) joint.highlighters.stroke.add(elementView, 'body', 'my-element-highlight', { attrs: { 'stroke': 'red', 'stroke-width': 3 } });
- Automatic updates when related CellView changes
joint.highlighters.mask.add(linkView, 'line', 'my-link-highlight'); linkView.model.target(element1); // the `mask` is automatically updated
- Easier management of existing highlighters in the paper
// Highlighters are now referenced by an arbitrary name (id) which can describe // the purpose of the highlighter const highlighterName = 'the-name-of-the-highlighter'; // e.g `hgl-active-element`, `error-highlighter` joint.dia.HighlighterView.add(cellView, selector, highlighterName); // Remove all highlighters from the CellView joint.dia.HighlighterView.remove(cellView); // Remove the highlighter with ID `my-highlighter` from the CellView joint.dia.HighlighterView.remove(cellView, 'my-highlighter'); // Remove all Mask highlighters from the cellView joint.highlighters.mask.remove(cellView); // Remove Stroke Highlighter with ID `my-highlighter` from the cellView. joint.highlighters.stroke.remove(cellView, 'my-highlighter'); // If you have a reference to a highlighter, calling its prototype `remove()` method is also valid. const highlighter = joint.dia.HighlighterView.get(cellView, 'my-highlighter'); highlighter.remove();
- Stacking order for highlighters
joint.highlighters.mask.add(linkView, { selector: 'line' }, 'my-highlight', { // Draw the highlighter under the LinkView layer: 'back' // 'back' | 'front' | null });
-
Every highlighter is an instance of
dia.HighlighterView
. It’s easy to extend the class and define your own highlighter. -
Add ability to control default highlighters
// Disable Default Connecting Highlighter paper.options.highlighting.connecting = false; const MyHighlighters = { CONNECTING: 'connecting-highlighter', }; paper.on('cell:highlight', (cellView, node, { type }) => { if (type === joint.dia.CellView.Highlighting.CONNECTING) { const isLink = cellView.model.isLink(); // The links are highlighted in `red` and elements in `blue` joint.highlighters.mask.add(cellView, node, MyHighlighters.CONNECTING, { attrs: { 'stroke': isLink ? 'red' : 'blue', 'stroke-width': 2 } }); } }); paper.on('cell:unhighlight', (cellView, node, { type }) => { if (type === joint.dia.CellView.Highlighting.CONNECTING) { joint.highlighters.mask.remove(cellView, MyHighlighters.CONNECTING); } });
-
highlighters.mask - new highlighter
An alternative to the
stroke
highlighter which brings an equidistant padding along the node and ability to draw a stroke around the descendant nodes or draw a stroke around a path with no fill.
-
highlighters.stroke - add
useFirstSubpath
optionRender the outline only for the first subpath of the target node.
const dataObjectView = dataObject.findView(paper); joint.highlighters.stroke.add(dataObjectView, selector, highlighterId, { padding: 10, useFirstSubpath: true });
-
util.breakText - fix height for a text with empty lines
Empty lines at the end of the text are preserved.
const text = 'Lorem ipsum dolor sit amet.\n\n\n\n\n'; const wrappedText = joint.util.breakText(text, size, styles, { ellipsis: true }); // e.g. 'Lorem ipsum\n dolor sit\n amet.\n\n\n…'
-
routers.manhattan - fix scan directions order
The router yields similar results for mirrored connections. e.g. Left-Right behaves similar to Right-Left connection.
-
connectors.jumpover - request updates after
batch:stop
The jumps are now updated after a change in the graph (not after an arbitrary interaction with a cell). It also supports asynchronous updates.
-
dia.attributes - add
magnetSelector
,highlighterSelector
andcontainerSelector
Suppose we have an element
label
on the outside and under thebody
of the rectangle.// `standard.Rectangle` has `root`, `body` and `label` sub-elements const element = new joint.shapes.standard.Rectangle({ attrs: { label: { refY: '100%', y: 5, textVerticalAnchor: 'top' } } });
Normally, links would connect to the
root
sub-element (the wrapper ofbody
andlabel
) however, we want links to connect only to thebody
sub-element.Before:
// Let the user connects links to the `body` of the element. element.attr({ root: { magnet: false }, body: { magnet: 'passive' } }); // Make sure that programmatically added links also connects the `body`. link.target({ id: rect.id, selector: 'body' });
Now:
// A link pointing to the root of the element will always connect the body sub-element element.attr({ root: { magnetSelector: 'body' } }); // No need to specify `selector` on the link itself. link.target({ id: rect.id });
-
dia.attributes -
textWrap
takes letter-spacing into accountelement.attr(['label'], { text: 'Lorem ipsum dolor sit amet.', textWrap: true, fontFamily: 'sans-serif', fontSize: 15, letterSpacing: 2 // is now taken into account when calculating the line breaks });
-
Vectorizer - add
createSVGStyle()
andcreateCDATASection()
static methodsconst style = V.createSVGStyle(` rect, circle { fill: white; stroke: black; } `); // <style type="text/css"> // <![CDATA[rect, circle { // fill: white; // stroke: black; // }]]> // </style> // Note: CSS Stylesheet added this way persists in the SVG/PNG export paper.svg.appendChild(style);
-
Geometry - add
getSubpaths()
to get all subpaths of a compoundPath
const path = new g.Path('M 0 0 0 10 M 10 0 10 10'); const [subpath1, subpath2] = path.getSubpaths(); subpath1.toString(); // 'M 0 0 L 0 10' subpath2.toString(); // 'M 10 0 L 10 10'
-
Geometry - add
round([precision])
method toLine
,Curve
,Rect
,Ellipse
,Path
andPolyline
const rect = new g.Rect(77.983, 77.983, 99.99998, 203.2); rect.round(2); rect.toString(); // `77.98@77.98 100 203.2`