UI / Rendering

Rendering

To view the data managed by an instance of the jsPlumb Toolkit, you must arrange for it to be rendered. The Toolkit can render its data to an arbitrary number of elements, and each renderer is backed with its own instance of jsPlumb. The component used by the Toolkit to render is known as a Surface. It supports pan/zoom/select, and has the ability to save and load its state locally in the user's browser.

Before reading this section it is important that you are thoroughly acquainted with the [data model](Data Model) used by the Toolkit. It would also be useful to be have at least a basic knowledge of jsPlumb, since the appearance of Endpoints and Connections is jsPlumb's responsibility and you'll want to learn the syntax.

To configure a Surface, call the render method on an existing Toolkit instance. This can be done at any time - before or after data is loaded:

var myToolkit = jsPlumbToolkit.newInstance();
var surface = myToolkit.render({
  container:"someElementId",
  ... other options ...
});

The render method returns a Surface object. It is important to remember that for view related functionality you use a Surface object, whereas for operations on the data model you use a jsPlumb Toolkit instance. Operations on the data model are reflected in all registered Surfaces for the specific instance of the jsPlumb Toolkit as they occur. If you wish to add a Node to the data model, for instance, you need to call addNode on a Toolkit instance. If you want to center the view in some element, you would call centerContent on the Surface registered for that element.

There is one required parameter to a render call - container.

Container

For anyone who has used the jsPlumb Community edition, the container parameter is probably familiar (and it's perhaps useful to know that the Toolkit passes container into the jsPlumb instance backing the Surface as the Container for that instance of jsPlumb). This is the element to be used as the parent for every element added by the Surface. In addition, all artefacts added to the UI by the associated jsPlumb instance will have this element as their parent. For a deeper discussion of this, see the jsPlumb documentation.

The vast majority of setups will also need to specify a view (the only exception being when you are happy to use the Community edition's default connection styles and you're using inferred template ids.


A View contains configuration information for all of the Nodes, Ports, Groups and Edges in your UI; it is what you use to define the appearance and behaviour of the various parts of your UI, and also offers event binding to numerous lifecycle events. For a full discussion of Views, see here.


You can call render as many times as you like on a single Toolkit instance. If the target container already has a Surface, that Surface is discarded. Every Surface that an instance of the Toolkit is tracking will be notified of changes to the data model. An example of multiple views on one Toolkit instance is given in the Multiple Renderers demonstration.


For a full discussion of the capabilities and usage of the Surface widget, see this page. Here on this page we provide a short summary of its features.

Surface is a widget with several useful features:

Change the zoom using the mouse wheel, pinch (on touch devices), right click + mouse move, or programmatically.

Click/tap and hold then drag the contents around. The Surface also, by default, provides 'nudge' bars on the edges of the work area, which you can click to pan the contents by a given amount automatically, or click and hold to pan continuously. These nudge bars can be programmatically suppressed or simply hidden via CSS.

By default, the Surface writes the current position of elements contained to local storage. When the user comes back to a page that they have already viewed, these saved positions are used to restore the UI to the state it was in when they left it. The current zoom and pan position are also saved and restored. This behaviour can be set to automatic or manual.

In select mode, click/tap and drag to lasso a group of nodes/groups. Hold down the shift key to add to the current selection. When you drag from left to right you need only intersect some node or group to select it. When you drag from right to left you must fully enclose a node or group before it is selected.

You can supply a background image for a Surface widget to display. You can also use a tiled image.

TOP


The Surface widget has a setVisible method that takes a variety of arguments:

..or an array of any of these. When you hide a node or group, any edges connected to that node or group are also hidden. When you make a node or group visible, only edges that are not connected to some other hidden node or group are made visible.

Say you want to select and hide a few nodes, for instance (assume, in these example, the existence of a toolkit and a surface widget):

var sel = toolkit.select(["node1", "node2"]);
surface.setVisible(sel, false);

or perhaps you want to select the Path from some node to another and then hide it:

var path = toolkit.getPath({ source:"node1", target:"node23" });
surface.setVisible(sel, false);

Or maybe even you just want to hide a DOM element that you got through jQuery and you don't need to know what its underlying Node is:

$("aContainerElement").on("click", "aNodeSelector", function() {
    surface.setVisible(this, false);
});

TOP


The vast majority of applications using the Toolkit will want to support dragging of nodes and groups in the Surface. For this reason, dragging is automatically enabled for any nodes and groups in your UI.

The Toolkit uses Katavorio to support dragging.

Switching off Dragging

To switch off dragging:

var surface = myToolkitInstance.render({
  container:"someElement",
  view:{ ... },
  elementsDraggable:false
});

Lifecycle Events

You can hook into start, drag and stop events:

var surface = myToolkitInstance.render({
  container:"someElement",
  view:{ ... },
  dragOptions:{
    start:function() {
    
    },
    drag:function() {
    
    },
    stop:function() {
    
    }
  }
});

Avoiding overlapping elements

You can instruct the Toolkit to automatically adjust a recently dragged Node/Group so that it does not overlap any other elements:

var surface = myToolkitInstance.render({
  container:"someElement",
  view:{ ... },
  dragOptions:{
    magnetize:true
  }
});

This operation will move the Node/Group so that it is at least 5 pixels from any other Node/Group. Note: this does not work for Nodes inside Groups.

Dragging on a grid

Katavorio supports constraining dragged elements to a grid:

var surface = myToolkitInstance.render({
  container:"someElement",
  view:{ ... },
  dragOptions:{
    grid:[ 50, 50 ]
  }
});

TOP


Drag Filters

Nodes are automatically made draggable by the Toolkit, unless you set elementsDraggable:false in the render call. By default, a mousedown event on any part of a draggable Node will cause a drag to begin. This can mean that it is difficult to interact with Nodes that have complex content. In these cases, you will need to set a filter in your dragOptions:

var surface = myToolkitInstance.render({
  container:"someElement",
  view:{ ... },
  dragOptions:{
    filter:"button, .dontDragHere"
  }
});

So here we have said that we don't want a mousedown on a button or an element with class "dontDragHere" to start a drag. Valid values for filter are any valid CSS selector.

Drag Posses

A drag Posse is a set of Nodes that should always be dragged as a unit. Unlike in a group (in which a set of Nodes share a common element as parent, which is collapsible etc), these Nodes do not necessarily have any visual indication that they belong together.

Any given Node can belong to multiple Posses at a time. Additionally, each Node's participation in a Posse is either active, in which dragging the Node causes all Nodes in the Posse to be dragged, or passive, in which the Node drags independently but is dragged when one of the Posse's active Nodes is dragged.

Configuring a Posse

Nodes are assigned to a Posse via an optional assignPosse function that you provide to a render method call:

var surface = myToolkitInstance.render({
  container:"someElement",
  view: { ... },
  assignPosse:function(node) {
    return node.data.parentId || node.data.id;
  }
});

Note the object passed to assignPosse is a Toolkit Node object; its backing data is available via the data property.

This posse assign function returns either the parentId of the Node's data, or just the Node's id. So we're saying that the Posse consists of some "parent" node and any other nodes marked as "children" of the node. An example few nodes conforming to this data model might be:

[
  {
    "id":"001",
    "foo":"FOO"
  },
  {
    "id":"z59-b",
    "parentId":"001",
    "foo":"bar",
  },
  {
    "id":"qx-78",
    "parentId":"001",
    "foo":"ok"
  }
]

If assignPosse returns null then the relevant Node is not added to a Posse. Note that in this case if there were any "parent" nodes that had no "children" then they would just be in a Posse of one. That's ok; a bit boring, perhaps, around the campfire, but ok.

Active Nodes and Passive Nodes

In the previous example we returned a String to identify the Posse to which each Node belongs. This indicates that the Node should play an active role in the Posse, ie. when it is dragged, all Nodes in the Posse are dragged:

assignPosse:function(node) {
    return node.data.parentId || node.data.id;
}

You can, however, pass back an array in which the second argument indicates whether or not the Node should be active:

assignPosse:function(node) {
    return node.data.parentId  ? [ node.data.parentId, false ] : node.data.id;
}

In this second example we check if a Node has a parentId. If so, the Node is added to the Posse with that value, but as a passive Node: dragging it will not cause the other Nodes to be dragged. Otherwise if there is no parentId then the Node is a parent and it is added as a master to its own Posse.

Adding Nodes Manually

You can at any time manually add a Node to a Posse with this method:

surface.addToPosse(aNode, posseId);

aNode may be the ID of a Node, an actual Node, a DOM element representing a Node, or a list-like object of any of these. Note here that the method is on the Surface widget and not on the Toolkit; it is a view concern. The Toolkit is unaware of the concept of Posses - only the Surface widget knows about them.

Removing Nodes Manually

You can at any time manually remove a Node from its Posse with this method:

surface.removeFromPosse(aNode, posseId);

aNode may be the ID of a Node, an actual Node, a DOM element representing a Node, or a list-like object of any of these.

Updating via the Toolkit

If you call this method:

toolkit.updateNode(someNode, { ... });

The Toolkit updates its data model and then informs any Surface widgets that are attached. The Surface widgets then re-render the Node and run the Node's data through the assignPosse function, updating - or clearing - the Posse as required.

TOP


Every Surface widget can have a Miniview associated with it - a small window displaying the structure of the UI, which is independently pannable and zoomable, and which controls the Surface's view. It is zoomed out to such an extent that all of the nodes in the Surface are visible within its viewport, and contains a "panner" element that maps the current visible viewport of the Surface to which it is related.

Instantiating a Miniview

To get a Miniview you can either first get a Surface:

var toolkit = jsPlumbToolkit.newInstance();
var surface = toolkit.render({
  container:"someElId"
});

...and then ask that Surface to create a Miniview:

var miniview = surface.createMiniview({ 
  container:"someMiniviewContainerId"  
});

or, you can supply Miniview parameters to the render call and then subsequently get the Miniview from the Surface:

var toolkit = jsPlumbToolkit.newInstance();
var surface = toolkit.render({
  container:"someElId",
  miniview:{
    container:"someMiniContainerId"  
  }
});
var miniview = surface.getMiniview();

NB These code samples show how you can retrieve a Miniview from some Surface after it has been created. In reality you will not need to work directly with a Miniview very often, if at all.

Configuring a Miniview

The size of a Miniview is something you set yourself, either through CSS, or via inline styles on the Miniview's container element. The jsPlumb Toolkit uses the size of a Miniview's container combined with the extents of the visible content in the related Surface to compute the appropriate zoom level for the Miniview.

The zoom wheel can be used to zoom in and out on a Surface via its associated Miniview. When this occurs, the visible node set does not change - the Miniview always shows the entire dataset - but the panner element changes size to reflect the fact that the Nodes that are visible in the related Surface's viewport have changed.

CSS

These are the classes you can use to style the Miniview widget. Note that nodes in the Miniview are sized to be identical to their mapped nodes in the related Surface (but the Miniview is zoomed out, so they are not 1:1 in size with their related nodes). You could of course use CSS to force a size for nodes in the Miniview, but this is not recommended; if your Surface contains nodes of various sizes but the Miniview uses a uniform size, the user may experience a certain discontinuity between the two views.

ClassDescription
jtk-miniviewAssigned to an element that is acting as a Miniview's container
jtk-miniview-canvasAssigned to the work area in the Miniview
jtk-miniview-pannerAssigned to the element used to pan the Surface from the Miniview
jtk-miniview-elementAssigned to all elements on the Miniview's canvas

Node type in Miniview

To give you an extra degree of control over the rendering of elements in the Miniview, you can supply a typeFunction, which will be called whenever a Node/Group is rendered, and whose result value is written as the jtk-miniview-type attribute on the DOM element in the Miniview:

var toolkit = jsPlumbToolkit.newInstance();
var surface = toolkit.render({
  container:"someElId",
  miniview:{
    container:"someMiniContainerId",
      typeFunction:function(obj) {
        return "foo";
      }
  }
});

Here we'd end up with an element like:

<div jtk-miniview-type="foo">...</div>

Of course in a real world scenario you'd like inspect the contents of obj to figure out what value to write. We use an attribute here rather than class because it keeps things simple for the Miniview renderer. If you have not yet encountered this, it is possible to style an element using "attribute selectors".

TOP


Drop/Detach Interceptors

In jsPlumb you can attach interceptors for connection drop and connection detach; these are functions which, should they return false, cause the associated action to be aborted. This is supported in the Toolkit via interceptors in a view.

TOP


Repainting Objects

To force a Surface to repaint some object, you can use the repaint method:

surface.repaint(someObject);

Valid values for someObject are:

  • a Toolkit Node
  • a Toolkit Port
  • a DOM element representing a Toolkit Node or Port
  • the ID of a Toolkit Node or Port
  • an element that has a Toolkit Node or Port element as an ancestor.

The last item in this list might need a little explanation: the idea is that you might have made some changes to part of your UI and you want to repaint the Toolkit object to which it belongs. Rather than forcing you to go off and find the appropriate ancestor, or the id of the appropriate ancestor, you can just pass some element in here and have the Surface figure out what it would be best to repaint.

TOP


CSS Classes

For a discussion of how the Toolkit handles CSS, see this page.


Saving and Restoring UI State

The state of a given renderer can be written to, and subsequently read from, local storage (or a cookie, depending on the capabilities of the browser). This is achieved through a few methods and/or constructor parameters:

Constructor Parameters for automatically saving state

var surface = toolkit.render({
  container:"someElement",
  saveStateOnExit:true,              // serialize state on page unload automatically. defaults to false.
  saveStateOnDrag:true,              // serialize state after each drag. defaults to false.
  stateHandle:"someString"           // required for either of the auto state save options. the handle to store the state by.
});

Methods for saving/restoring state

saveState([String handle], [Function preprocessor]);

Saves the state with the given handle. If handle is not supplied but stateHandle was provided as a constructor parameter, that value is used. If neither is supplied then the state is not saved.

The optional preprocessor argument allows you to pre-process the data loaded from localStorage before it is applied to the Surface. This could be used for many things, but the main intended usage for this mechanism is to allow you to apply compression/decompression to the data that is placed into localStorage. Limits vary, but at the time of writing you could consider as a rough guide that the limit on data that can be placed in localStorage is about 5MB.

The preprocessor function must take two arguments:

function(data, callback);

and is required to call callback with the pre-processed data. As a spurious example, maybe you want to prepend a timestamp to the data before storing:

surface.State.save("myHandle", function(data, callback) {
  callback(new Date().getTime() + "-" + data);
});

Or perhaps you might decide to use the LZMA library to compress your data. The compress method of LZMA has this signature:

compress(data, [level], callback);
surface.State.save("myHandle", function(data, callback) {
  LZMA.compress(state, 1, callback);
});

Notice here that we passed callback directly to LZMA; it wasn't necessary to wrap it in another function.

Note that you can call this method with the preprocessor as the single argument, since the Surface can figure out that if you supply a Function it is not a handle with which to save the data.

restoreState(String handle, [Function preprocessor]);

Restores the state with the given handle. If handle is not supplied but stateHandle was provided as a constructor parameter, that value is used. If neither is supplied then the state is not restored.

See the note on saveState for a discussion of the optional preprocessor Function. Note also that you can call this method with the preprocessor as the single argument, since the Surface can figure out that if you supply a Function it is not a handle with which to save the data.

This might be the matching decompression preprocessor you'd use for the timestamp example above:

surface.State.restore("myHandle", function(data, callback) {
  var parts = data.split(/^([0-9]+)(?:-)/),
      timestamp = new Date(parseInt(parts[1], 10));
      
  callback(parts[2]);
});

For the LZMA decompression example, you could do this:

surface.State.save("myHandle", function(data, callback) {
  LZMA.decompress(layout.payload, callback);
});
clearState([String handle]);

Clears the state stored with the given handle. If handle is not supplied but stateHandle was provided as a constructor parameter, that value is used. If neither is supplied then the state is not cleared.

serialize();

Serialize the state of the Surface and return the serialized string. This is of course the method that saveState uses internally.

deserialize(String serializedData);

Deserialize the state of the Surface from the given string. This is of course the method that restoreState uses internally.

Saving and Restoring Custom Properties

The Surface widget exposes the functionality used to save/restore state as four methods that you can call to store arbitrary data of your own:

surface.State.store({String} handle, {String} data);

Stores a string using the provided handle as the key.

surface.State.storeJSON({String} handle, {Object} data);

Stores an object using the provided handle as the key. This method actually serializes the given object to a string, and therefore expects that there is a global JSON object available in the browser, which is true of many modern browsers, and is also provided by jQuery. If JSON is not available in the browser this method fails silently.

{String} surface.State.retrieve({String} handle);

Retrieves a string using the provided handle as the key. Will return null if nothing is stored against the given handle.

{Object} surface.State.retrieveJSON({String} handle, {Object} data);

Retrieves an object using the provided handle as the key. See the note above about the required JSON dependency. Will return null if nothing is stored against the given handle.

TOP


Dragging Nodes from a Palette

A common use case for applications using the jsPlumb Toolkit is the requirement for a palette of nodes from which items can be dragged into the work area. This is now handled by the Drop manager.

TOP

Suspending Event Consumption

By default, the Surface will consume any right click events it catches. You can turn this off, which is handy when developing, in one of two ways:

  • Supply a parameter to the render call:
var surface = toolkit.render({
  ...
  consumeRightClick:false,
  ...
});
  • Call this method on an existing Surface:
surface.setConsumeRightClick(false);

Disabling a Surface

To temporarily "switch off" a Surface (ie. have it stop responding to mouse events), you can use the setEnabled method:

renderer.setEnabled(false);
... time passes, things happen....
renderer.setEnabled(true);