- Configuring a Surface
- View
- Rendering to Multiple Elements
- The Surface Widget
- Zoom
- Pan/Drag
- Save/Restore state
- Node/Group Select
- Background Images
- Hiding/Showing Elements
- Dragging Nodes
- The Miniview Widget
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.
The Surface widget has a setVisible
method that takes a variety of arguments:
- a Node/Group/Port id
- a DOM Element
- a Node/Group/Port/Edge
- a Selection
- a Path
- a Toolkit instance
..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):
or perhaps you want to select the Path from some node to another and then hide it:
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);
});
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 ]
}
});
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:
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:
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:
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:
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.
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:
...and then ask that Surface to create a Miniview:
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.
Class | Description |
---|---|
jtk-miniview | Assigned to an element that is acting as a Miniview's container |
jtk-miniview-canvas | Assigned to the work area in the Miniview |
jtk-miniview-panner | Assigned to the element used to pan the Surface from the Miniview |
jtk-miniview-element | Assigned 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:
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".
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.
Repainting Objects
To force a Surface to repaint some object, you can use the repaint
method:
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.
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
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:
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:
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.
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);
});
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 the state of the Surface and return the serialized string. This is of course the method that saveState
uses internally.
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:
Stores a string using the provided handle as the key.
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.
Retrieves a string using the provided handle as the key. Will return null if nothing is stored against the given handle.
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.
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:
- Call this method on an existing Surface:
Disabling a Surface
To temporarily "switch off" a Surface (ie. have it stop responding to mouse events), you can use the setEnabled
method: