Layouts / Decorators

A Decorator is a class that can add arbitrary extra content to the UI after a layout has been run. Content can be added in three different ways:

  • Appended to the work area. In this case, the decoration will pan and zoom with the nodes and edges in your UI.
  • Floated over the work area. Use this to add layout-specific control elements, for example.
  • Appended to the work area but fixed in one or both axes so that it never leaves the viewport.

The first step is to write the skeleton for your decorator and register it on the Toolkit:

jsPlumbToolkit.Layouts.Decorators["Example"] = function(decoratorParams, surface) {

    this.reset = function(params) {

        // Contents of params:
        
        // remove(el, doNotRepaint)
    };

    this.decorate = function(params) {
    
        // Contents of params:
    
        // adapter (a Surface)
        // append(el, id, pos)
        // fixElement(el, axes, pos)
        // bounds 
        // jsPlumb
        // layout 
        // setAbsolutePosition(el, xy)
        // toolkit
    };
};
Lifecycle
  • reset is called before every relayout or refresh that occurs on the layout.
  • decorate is called before every relayout or refresh, immediately after reset.
Lifecycle Method Parameters

Each method is passed a params object, the contents of which are as follows:

reset
  • remove(el, doNotRefresh)

This is a function you can call to have the Toolkit remove some element from the canvas (or a floated element). You should use this method in preference to some other way of removing content since it ensures everything is cleaned up appropriately.

decorate
  • adapter

This is the underlying Surface that is rendering your content. In future releases of the Toolkit that support server side rendering, this may be some other object, the interface it implements will be the same.

  • append(el, id, pos)

A function you can call to append some DOM element to the canvas. Elements appended in this way are panned and zoomed with the rest of the content. id is optional, and pos is location data in the form {left:..., top:...}

  • floatElement(el, pos)

A function you can call to append some element to the viewport at a given position. Elements added to the viewport float over the content and remain fixed at the given location. Note that we do not currently support specifying right or bottom properties in this method - the values in pos are expected to be [left, top].

  • fixElement(el, axes, pos)

This behaves like append in that it adds the given element to the canvas so that it is panned and zoomed with the rest of the content, but the axes argument allows you to specify that the element should not be allowed to exit the viewport in one or more axes. An example will be best to explain this:

let el = document.createElement("div");
el.innerHTML = "I'm a label";

params.fixElement(el, { left:true }, [ 50, 50 ]);

In this example we have requested that our label div be placed on the content at position [50, 50]. But when the content is panned to the left to the extent that the label would be less than 50 pixels from the edge of the viewport, its location is adjusted so that it remains fixed in place in the x axis. In case it is not obvious, you could also specify top:true in the axes argument.

  • bounds

This is an array of [ xmin, ymin, xmax, ymax ] locations, giving the extents of the content. Remember that in the Toolkit, the origin of the canvas does not necessarily correlate with the top left corner of your layout's extents. It depends on what the layout chooses to do. All of the layouts that ship with the Toolkit, for instance, routinely draw into the negative in both axes. So the point here is that it may be the case that if you want something to appear at the origin of your layout's extents, that point will not be [0,0]. It will, though, be the first two values in bounds, and you are safe to pass bounds to either append or fixElement:

let el = document.createElement("div");
el.innerHTML = "I'm a label";
     
params.fixElement(el, { left:true }, params.bounds);

Here we have requested that our label appear at the origin of the layout's extents, and for it to stay there in the X axis as the user pans to the left.

  • jsPlumb

The underlying jsPlumb renderer. The majority of use cases will not need to access this.

  • layout

The layout used to render this data. Typically, a decorator and a layout work in tandem: the layout knows the key bits of information a decorator needs. Take a process flow diagram as an example: the layout has created the lanes and placed nodes into these lanes; it is the layout that can tell the decorator where the lanes start and end.

  • setAbsolutePosition(el, pos)

A function you can use to set the position of some element on the canvas. el is a DOM element, and pos is an array of [left,top] pixel values.

  • toolkit

The underlying jsPlumb Toolkit instance.

Invoking a decorator

You invoke decorators by specifying them in the layout section of a render method call:

let renderer = myToolkitInstance.render({
    container:someElement,
    ...,
    layout:{
        type:"Hierarchy",
        decorators:[ "Example" ]
    }
});

You can attach an arbitrary number of decorators.

You may have noticed in the constructor for the Example decorator above there were two arguments: decoratorParams and surface. The second of these is the underlying Surface widget. The first argument would be an empty object in the arrangement shown above, but you can pass constructor parameters into a decorator like this:

let renderer = myToolkitInstance.render({
    container:someElement,
    ...,
    layout:{
        type:"Hierarchy",
        decorators:[ 
           [ "Example", { foo:1, bar:25 } ]
        ]
    }
});

Simple Example

In our Example decorator we will float an element near the top left corner, and draw a blue background around the entire contents:

jsPlumbToolkit.Layouts.Decorators["Example"] = function(options) {

    var label, background;

    this.reset = function(params) {
        label && params.removeElement(label);
        background && params.removeElement(background);
    };

    this.decorate = function(params) {
    
        label = document.createElement("div");
        label.className = "aLabel";
        label.innerHTML = "My Decorator";
        params.floatElement(label, [ 50, 50 ]);
        
        background = document.createElement("div");
        background.className = "background";
        var w = params.bounds[2] - params.bounds[0], 
            h = params.bounds[3] - params.bounds[1];
        background.style.width = (w + 40) + "px";
        background.style.height = (h + 40) + "px";
        var xy = [ params.bounds[0] - 20, params.bounds[1] - 20 ];
        
        params.append(background, null, xy);
    };
};

Here we've floated the label, and then created a background element that will overlap the content by 20 pixels on each side. 20 pixels, though...what if we wanted to set that?

Decorator Options

You can pass options to a decorator using the syntax with which you may be familiar from anchors etc in theCommunity edition:

var renderer = myToolkitInstance.render({
    container:someElement,
    ...,
    layout:{
        type:"Hierarchy",
        decorators:[ 
            [ "Example", { padding:20 } ]
        ]
    }
});

The options are passed in to the constructor of the decorator. So we can rewrite our decorator now:

jsPlumbToolkit.Layouts.Decorators["Example"] = function(options) {

    var label, background;
    
    var padding = options.padding || 20;

    this.reset = function(params) {
        label && params.removeElement(label);
        background && params.removeElement(background);
    };

    this.decorate = function(params) {
    
        label = document.createElement("div");
        label.className = "aLabel";
        label.innerHTML = "My Decorator";
        params.floatElement(label, [ 50, 50 ]);
        
        background = document.createElement("div");
        background.className = "background";
        var w = params.bounds[2] - params.bounds[0], 
            h = params.bounds[3] - params.bounds[1];
        background.style.width = (w + (padding * 2)) + "px";
        background.style.height = (h + (padding * 2)) + "px";
        var xy = [ params.bounds[0] - padding, params.bounds[1] - padding ];
        
        params.append(background, null, xy);
    };
};

Z Index

The Toolkit will not take care of z-index for you. Webapps come in a million different shapes and sizes; it would be too invasive for the Toolkit to infer anything. Keep this in mind when you write your decorators...

Hierarchy Decorator

From version 1.1.0, the Toolkit ships with a decorator you can use to draw a background behind a Hierarchical layout. You use it like this:

toolkit.render({
  container:"someElement",
  layout:{
    type:"Hierarchical",
    decorators:"Hierarchy"
  }
});

This decorator is used in the Toolkit's Hierarchical Layout demonstration.

TOP