Skip to main content

Surface

The surface component provides a pannable/zoomable canvas in which your UI can be rendered. It has a full suite of methods to assist in configuring the display and in navigating your way around, including the ability to zoom in to specific vertices or to the entire dataset, center the display on some vertex or group of vertices, and lots more. The canvas also offers the ability to clamp its movement so that whenever a user is panning or zooming the display there is always some portion of the content visible to the user, and there are methods available to assist in mapping coordinates from the page space to the surface's viewport.

Creating a surface#

How you go about creating a surface depends on whether you are using vanilla Toolkit, or one of the library integrations. On this page the code snippets assume you're using vanilla Toolkit, but if you're using an integration, just keep in mind that anywhere on this page that you see parameters passed to the render function, they're things that would go in the renderParams object in the library integration.

Panning#

The surface widget is an 'infinite pan' canvas that does not use scrollbars. To pan the canvas, the user drags using the left mouse button, or, on touch devices, by dragging a single touch.

Disabling Panning#

Panning is normally enabled, but a surface can be initialized with panning disabled by setting enablePan to false:

const surface = toolkit.render(someElement, {  enablePan:false});

You can also disable a surface entirely via the setMode method:

surface.setMode(Surface.DISABLED);

Filtering Panning#

It's a fairly common use case that there be some set of elements in your canvas on which a drag should not cause a pan to occur. To handle this, the surface has the panFilter parameter. This is either a String that is a CSS selector representing elements that should allow a pan to begin, or a Function from which you should return true if you would like a pan to begin.

CSS Selector Filter#

let surface = toolkit.render(someElement, {  panFilter:".someClassName"});

Function filter#

const surface = toolkit.render(someElement, {  panFilter:(eventTarget) => {    return someLogic(eventTarget);  }});

Zooming#

The default behaviour of the surface is to support zooming via the mouse wheel, or on touch devices, via pinch to zoom.

Specifying zoom range#

The surface can be zoomed within a given range, whose default value is:

[0.05, 3]

meaning the surface can zoom between 5% and 300%. You can set this when you create a surface:

const surface = toolkit.render(someElement, {  zoomRange:[1, 2]});

Wheel zoom#

const surface = toolkit.render(someElement, {  enableWheelZoom:false});

enableWheelZoom defaults to true. Whether or not zooming with the mouse wheel is enabled.

Filtering wheel events#

const surface = toolkit.render(someElement, {  wheelFilter:".someElementClass"});

wheelFilter is an optional CSS selector representing elements that should not respond to wheel zoom. Defaults to empty.

Wheel zoom meta key#

const surface = toolkit.render(someElement, {  wheelZoomMetaKey:true});

wheelZoomMetaKey defaults to false. If true, the wheel zoom only fires when Ctrl (or CMD on Mac) is pressed and the wheel is rotated.

Wheel direction#

const surface = toolkit.render(someElement, {  wheelReverse:true});

wheelReverse Defaults to false. If true, the zoom direction is reversed: wheel up zooms out, and wheel down zooms in.

Zoom Methods#

There are a few helper methods for zooming exposed on the surface.

zoomToFit(options)#

Zooms the display so that all tracked elements fit inside the viewport (including invisible nodes). This method will also increase the zoom if necessary in order that the content fills the 90% of the shortest axis of the viewport.

The most basic call you can make is this:

surface.zoomToFit();

zoomToFit supports a number of parameters:

  • fill A decimal indicating how much of the viewport to fill with the zoomed content. Defaults to 0.9.
  • padding Padding to leave around all elements. Default is 20 pixels.
  • onComplete Optional function to call on operation complete (centering may be animated).
  • onStep Optional function to call on operation step (centering may be animated).
  • doNotAnimate By default, the centering content step does not use animation (this parameter is set to true). This is due to this method being used most often to initially setup a UI.
  • doNotZoomIfVisible Defaults to false. If true, no action is taken if the content is currently all visible.

An example showing the surface animating the content until it fits 80% of the viewport and then popping up an alert:

surface.zoomToFit({  fill:0.8,  doNotAnimate:false,  onComplete:function() { alert("done!"); }});

To zoom to show only visible nodes, see zoomToVisible.

zoomToFitIfNecessary(options)#

Works like zoomToFit but if all tracked elements are currently visible does not adjust the zoom.

surface.zoomToFitIfNecessary();

All of the parameters supported by zoomToFit are supported by zoomToFitIfNecessary.

zoomToBackground(options)#

If a background was set, zooms the widget such that the entire background is visible.

surface.zoomToBackground({  onComplete:() => { alert("done!"); }});

This method also supports onStep and doNotAnimate.

zoomToSelection(options)#

Zoom to either the current selected set of nodes in the Toolkit (defaults to the current selection filling 90% of the shortest axis in the viewport):

surface.zoomToSelection();

or provide a selection of your own to which to zoom:

surface.zoomToSelection({  fill:0.8,  selection:some Toolkit Selection});

To find our more about Selections, see here.

This method also supports an optional filter function, which is used to create a Selection by running it through the Toolkit's filter method. For instance, this is how you could create what the zoomToVisible method (described below) does:

surface.zoomToSelection({  filter:(obj) => { return obj.objectType === "Node" && surface.isVisible(obj); }});

zoomToVisible(options)#

Zooms to display all the currently visible nodes. All animation options are supported by this method - it is a wrapper around zoomToSelection in which we first create a selection representing all the visible nodes.

zoomToVisibleIfNecessary(options)#

This method is to zoomToVisible as zoomToFitIfNecessary is to zoomToFit - the content will be centered, but the zoom level will be changed only if all of the currently visible nodes are not visible in the viewport.

setZoom(value)#

Sets the current zoom level. This must be a positive decimal number. If it is outside of the current zoom range, it will be clamped to the zoom range.

surface.setZoom(0.5);

Here we have set the zoom to 50%.

nudgeZoom(amount)#

Nudges the current zoom level by some value (negative or positive).

// nudge the zoom by 5% surface.nudgeZoom(0.05);
// nudge the zoom by -15%surface.nudgeZoom(-0.15);

setZoomRange(range, doNotClamp)#

Sets the current zoom range. By default, this method checks if the current zoom is within the new range, and if it is not then setZoom is called, which will cause the zoom to be clamped to an allowed value in the new range. You can disable this by passing true for doNotClamp.

surface.setZoomRange(0.5, 2);

here we have set the zoom range to be 50% minimum, 200% maximum. If the current zoom was outside of this range, it was clamped to be within.

Let's set the zoom to 2, the top of our current range, and then adjust the zoom range without affecting the widget's zoom:

surface.setZoom(2);surface.setZoomRange([0.5, 1], true);

Clamping#

Pan Movement#

By default, the surface will clamp movement when panning so that some content is always visible. This can be overridden:

const surface = toolkit.render(someElement, {  ...  clamp:false,  ...});

Zoom Movement#

It is also default behaviour to clamp the movement of the canvas when the user zooms such that some content is always visible. Without this, it's quite easy for a user to accidentally zoom in such a way that all of the content disappears (consider the case that the canvas is zoomed out a long way and the user then zooms in on some whitespace that is a long way from any content).

As with pan clamping, you can also switch off zoom clamping if you wish:

const surface = toolkit.render(someElement, {  ...  clampZoom:false,  ...});

Background#

This is discussed below, but a brief mention should be made of the fact that you can also tell the surface to clamp movement such that part of the background is always visible:

const surface = toolkit.render(someElement, {  ...  clampToBackground:false,  ...});

Positioning#

Several methods are available to assist you with positioning the surface canvas. These are discussed in greater detail in the API documentation:

  • centerOn(node)

Takes a node as argument and positions the surface canvas such that the given node is at the center in both axes.

  • centerOnHorizontally(node)

Takes a node as argument and positions the surface canvas such that the given node is at the center in the X axis.

  • centerOnVertically(node)

Takes a node as argument and positions the surface canvas such that the given node is at the center in the Y axis.

  • centerContent()

Centers the content in the viewport (without altering the zoom, so the content may still extend past the viewport bounds).

  • pan(x, y)

Pans the canvas by a given amount in X and Y.

  • positionElementAt(el, x, y)

Positions a DOM element at a given X,Y on the canvas, in canvas coordinates (meaning it takes into account the current zoom and pan). This is not intended for use with elements the surface is managing: it is designed to be used with elements such as pop-ups that you may wish to position relative to the content in your canvas.

  • positionElementAtEventLocation(el, event)

Positions a DOM element at the apparent canvas location corresponding to the page location given by some event. This is not intended for use with elements the surface is managing: it is designed to be used with elements such as pop-ups that you may wish to position relative to the content in your canvas.

  • positionElementAtPageLocation(el, x, y)

Positions a DOM element at the apparent canvas location corresponding to the given page location. This is not intended for use with elements the surface is managing: it is designed to be used with elements such as pop-ups that you may wish to position relative to the content in your canvas.


Dragging#

By default, any nodes rendered by a surface will be initialized to be draggable. This can be overridden:

const surface = toolkit.render(someElement, {  ...  elementsDraggable:false,  ...});

Drag Options#

You can provide a set of options that will be applied to every node when it is made draggable:

var surface = toolkit.render({  ...  dragOptions:{    grid:{x:20,y:20}  },  ...});

Valid values for the contents of dragOptions are defined in the SurfaceDragOptions interface.


Grids#

You can impose a grid on the elements in a surface:

const surface = toolkit.render(someElement, {    grid:{        size:{x:50, y:50}    }})

The full list of options supported by grid are defined in the SurfaceGridOptions interface:

Home > @jsplumbtoolkit/browser-ui > SurfaceGridOptions

SurfaceGridOptions interface#

Options for the grid in a Surface.

Signature:

export interface SurfaceGridOptions 

Properties#

PropertyTypeDescription
fitGroupsToGrid?boolean(Optional) Whether or not to ensure calculated group sizes (from auto sized groups) are a multiple of the grid size in each axis. Defaults to false.
size?Grid(Optional) width/height of the grid
snap?boolean(Optional) Whether or not to snap elements to the grid when dragging. Defaults to false.

Selecting vertices#

Vertices managed by a surface widget may be "selected" at any point in time programmatically via the associated toolkit instance. When a node is selected, the surface is informed, and its DOM element is assigned the class jtk-surface-selected-element.

You can also select nodes using the mouse, with a "lasso". To switch into this mode, call setMode:

surface.setMode(Surface.SELECT);

In order to use the lasso, you need to install the lasso plugin. Checkout the linked page for information about how to configure the lasso.

Exiting Select Mode#

Ordinarily, the surface will jump back into pan mode from select mode after some nodes have been selected, but this behaviour can be overridden, using the autoExitSelectMode flag:

const surface = toolkit.render(someElement, {  ...  autoExitSelectMode:false  ...});

Mapping coordinates#

Sometimes you'll want to map an event that has page coordinates into an event mapped to the surface's viewport (or vice versa). As an example, imagine your user clicks on your canvas somewhere, and you want to add a new node at that location. If the surface has been panned and zoomed from the initial state, as is almost always the case, you need to map that location to the surface's viewport. There are a few methods to assist with this sort of thing:

isInViewport#

Tells you whether or not a given point (which is relative to the page origin) is within the surface's viewport.

Home > @jsplumbtoolkit/browser-ui > Surface > isInViewport

Surface.isInViewport() method#

Returns whether or not the given point (relative to page origin) is within the viewport for the widget.

Signature:

isInViewport(x: number, y: number): boolean;

Parameters#

ParameterTypeDescription
xnumberX location of point to test
ynumberY location of point to test

Returns:

boolean

True if the point is within the viewport, false if not.

fromPageLocation#

Converts the given page location into a location relative to the viewport's origin.

Home > @jsplumbtoolkit/browser-ui > Surface > fromPageLocation

Surface.fromPageLocation() method#

Maps the given page location to a value relative to the viewport origin, allowing for zoom and pan of the canvas. This takes into account the offset of the viewport in the page so that what you get back is the mapped position relative to the target element's [left,top] corner. If you wish, you can supply true for 'doNotAdjustForOffset', to suppress that behavior.

Signature:

fromPageLocation(left: number, top: number, doNotAdjustForOffset?: boolean): PointXY;

Parameters#

ParameterTypeDescription
leftnumberX location
topnumberY location
doNotAdjustForOffsetbooleanWhether or not to adjust for the offset of the viewport in the page. Defaults to false.

Returns:

PointXY

The mapped location, as a PointXY object.

toPageLocation#

Converts the given location relative to the viewport's origin into a location relative to the page origin.

Home > @jsplumbtoolkit/browser-ui > Surface > toPageLocation

Surface.toPageLocation() method#

Maps the given location relative to the viewport origin, to a page location, allowing for zoom and pan of the canvas. This takes into account the offset of the viewport in the page so that what you get back is the mapped position relative to the target element's [left,top] corner. If you wish, you can supply true for 'doNotAdjustForOffset', to suppress that behavior.

Signature:

toPageLocation(left: number, top: number, doNotAdjustForOffset?: boolean): PointXY;

Parameters#

ParameterTypeDescription
leftnumberX location
topnumberY location
doNotAdjustForOffsetbooleanWhether or not to adjust for the offset of the viewport in the page. Defaults to false.

Returns:

PointXY

The mapped location, as a PointXY object.


Events#

Event listeners can be bound to the surface widget in one of three ways:

  • via the bind method
  • in the events parameter to a render call on an instance of the Toolkit
  • in the individual Node, Group, Port and Edge definitions in the view parameter to a render call.

The full list of bindable events is listed on the events page.

Using bind#

const surface = toolkitInstance.render(someElement, {  ...});
surface.bind("canvasClick", (e) => {  console.log("click on the canvas");});
surface.bind("node:added", (params) => { });

Declarative binding#

const surface = toolkitInstance.render(someElement, {  ...  events:{      canvasClick:(e) => {        console.log("click on the canvas");      },      "node:added":(params) => {}  }});

Each of the entries in the events block is equivalent to first instantiating the surface and then calling bind on it.

View Events#

Each entry in the nodes, groups, ports or edges section of the view you pass to a render call can also specify a list of events. The advantage to doing this is that you can be very granular in your event registrations, limiting listeners to objects of some specific type as you need to. The disadvantage is of course more complexity in configuring everything.

Here's a simple example showing two node types, one of which declares a dblclick listener:

const surface = toolkitInstance.render(someElement, {  view:{    nodes:{      "type_a":{        templateId:"TypeA"      },      "type_b":{        templateId:"TypeB",        events:{          dblclick:(node) => {            console.log("double click on node " + node.id);          }        }      }    }  }});

View Event Inheritance#

If you have a type declared that extends some other type, it will inherit the parent type's listeners. But if your subtype declares some event listener, that listener will override the parent's listener for the given event, if one is present. For example:

const surface = toolkitInstance.render(someElement, {  view:{    nodes:{      "type_a":{        templateId:"TypeA",        events:{          dblclick:(node) => {            console.log("type A double click");          }        }      },      "type_b":{        parent:"type_a",        events:{          dblclick:(node) => {            console.log("type B double click");          }        }      }    }  }});

A click on a node of type type_b will produce a single line in the console: "type B double click".

Subscribable Events in Views#

This is the full list of events to which you can subscribe in a View definition:

  • click - Notification that a click event occurred
  • tap - A synthesized click event that normalises click behaviour between touch and mouse devices
  • dblclick - Notification that a double click event occurred
  • mouseover - Notification the mouse entered the object
  • mouseout - Notification the mouse left the object
  • mousedown - Notification a mouse button was pressed on the object
  • mouseup - Notification a mouse button was released
  • contextmenu - Notification that a context menu event occurred on a node. This typically means a right click.

Each of these callback functions is given a JS object, the contents of which differs between edges and node/ports/groups.

Node Callback Parameters#

{  el:Element,   node:Node,   e:Event}

Port Callback Parameters#

{  el:Element,   node:Port,   e:Event}

Edge Callback Parameters#

{  connection:Connection,   edge:Edge,   e:Event,  toolkit:jsPlumbToolkitInstance,  renderer:AbstractRenderer}

Suspending Events#

You can suspend/enable events from the surface widget with this method:

surface.setSuspendEvents(true);

You can also wrap the underlying Toolkit's batch command (which runs a series of operations without making any rendering changes) with the surface widget's batch command:

surface.batch(() => {  toolkit.addNode({id:"foo"});  toolkit.addNode({id:"bar"});  toolkit.addNode({source:"foo", target:"bar"});});

This is equivalent to:

surface.setSuspendEvents(true);toolkit.batch(() => {  toolkit.addNode({id:"foo"});  toolkit.addNode({id:"bar"});  toolkit.addNode({source:"foo", target:"bar"});});surface.setSuspendEvents(false);

Background Images#

Single Background Image#

To display a single image as the background of the surface widget:

const surface = toolkit.render(someElement, {  background:{    url:"./myBackground.png"  }});

Tiled Background Image#

You can use a tiled image as your background - you need to provide a url pattern, the number of available zoom levels, the tile size, and the width and height of the full image:

To display a single image as the background of the surface widget:

const surface = toolkit.render(someElement, {  background:{    type:"tiled",    url:"tiles/{z}/tile_{x}_{y}.png",    tileSize:[750,750],    width:10000,    height:6000,    maxZoom:3  }});
Tiled Background Pattern URL#

The URL you supply for a tiled background should have a placeholder for each of zoom, x and y, as in the example above. Tiles are zero-indexed, so, for example, the top left tile at zoom level 0 would, in the previous example, generate this url:

tiles/0/tile_0_0.png

The surface does not support the concept of a "continuous world", in which tiles with negative indices may be requested.

Clamping to the background image#

Depending on your use case, you may wish to force the surface to clamp the pan/zoom such that some portion of the background image is always visible. You do this by setting the clampToBackground parameter on a render call:

const surface = toolkit.render(someElement, {  clampToBackground:true,  background:{    url:"myBackground.png"  }});

Zooming to the background image#

If you wish to zoom out to the point that the entire background image is visible:

const surface = toolkit.render({ ... });surface.zoomToBackground();

CSS#

This is a full list of CSS classes used by the surface widget.

ClassDescription
jtk-surfaceAssigned to an element that is acting as a surface widget
jtk-surface-canvasAssigned to the work area of a surface widget. This canvas element will have been created automatically by the surface and is not normally something you will need to style. If you do attach some styles, you should be careful to ensure that this element always has position:relative; set.
jtk-surface-lassoAssigned to the "lasso" element used when selecting elements in a surface using the mouse.
jtk-surface-panAssigned to all of the pan buttons in a surface, regardless of which direction they control
jtk-surface-pan-topAssigned to the pan button that appears on the top edge of the surface.
jtk-surface-pan-leftAssigned to the pan button that appears on the topleft edge of the surface.
jtk-surface-pan-rightAssigned to the pan button that appears on the right edge of the surface.
jtk-surface-pan-bottomAssigned to the pan button that appears on the bottom edge of the surface.
jtk-surface-selected-elementAssigned to any element that is part of some surface's current selection.
jtk-lassoAssigned to the selection lasso element

Miscellaneous#

Consuming Right Click#

The default behaviour is to consume any right-click events. This is good when your app is in production, and really annoying when you're in the middle of development. To suppress this behaviour, set consumeRightClick:

var surface = toolkit.render(someElement, {  ...  consumeRightClick:false  ...});