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.
Creating a Decorator
The first step is to write the skeleton for your decorator and register it. In 6.x you must register this via the registerDecorator
method of the @jsplumbtoolkit/browser-ui
package.
Registering a decorator with ES6 / Typescript
import { registerDecorator, Decorator } from "@jsplumbtoolkit/browser-ui"
export class MyDecorator extends Decorator {
constructor(params:Record<string, any>, adapter:Surface, container:Element) {
super(params, adapter, container)
}
reset(params:any):void {
...
}
decorate(params:{
surface:Surface,
adapter:AbstractLayoutAdapter<Element>,
layout: AbstractLayout<any>,
append: Function,
setAbsolutePosition: Function,
toolkit: JsPlumbToolkit,
bounds: Extents,
positions:Map<string, PointXY>
}):void {
...
}
}
registerDecorator(MyDecorator, function() {
this.reset
})
Registering a decorator with ES5
var MyDecorator = function (params, surface, container) {
this.reset = function(params) {
};
this.decorate = function(params) {
};
});
Lifecycle
- reset is called before every
relayout
orrefresh
that occurs on the layout. - decorate is called before every
relayout
orrefresh
, immediately afterreset
.
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, andpos
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
orbottom
properties in this method - the values inpos
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 theaxes
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 theaxes
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 passbounds
to eitherappend
orfixElement
: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, andpos
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(someElement, {
...,
layout:{
type:"Hierarchy"
},
decorators:[
{ type:"MyDecorator", options:{ foo:1, bar:25 } }
]
});
You can attach an arbitrary number of decorators.
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:
export class Example extends Decorator {
label:Element
background:Element
constructor(params:Record<string, any>, adapter:Surface, container:Element) {
super(params, adapter, container)
}
reset (params:any) {
this.label && params.removeElement(this.label)
this.background && params.removeElement(this.background)
};
decorate (params:any) {
this.label = document.createElement("div")
this.label.className = "aLabel"
this.label.innerHTML = "My Decorator"
params.floatElement(this.label, [ 50, 50 ])
this.background = document.createElement("div")
this.background.className = "background"
const 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"
const 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(someElement, {
...,
layout:{
type:"Hierarchy"
},
decorators:[
{ type:"Example", options:{ padding:20 }}
]
});
The options are passed in to the constructor of the decorator. So we can rewrite our decorator now:
export class Example extends Decorator {
label:Element
background:Element
padding:number
constructor(params:Record<string, any>, adapter:Surface, container:Element) {
super(params, adapter, container)
this.padding = options.padding || 20
}
reset (params:any) {
this.label && params.removeElement(this.label)
this.background && params.removeElement(this.background)
}
decorate (params:any) {
this.label = document.createElement("div")
this.label.className = "aLabel"
this.label.innerHTML = "My Decorator"
params.floatElement(this.label, [ 50, 50 ])
this.background = document.createElement("div");
this.background.className = "background";
const w = params.bounds[2] - params.bounds[0],
h = params.bounds[3] - params.bounds[1]
this.background.style.width = (w + (this.padding * 2)) + "px"
this.background.style.height = (h + (this.padding * 2)) + "px"
const xy = [ params.bounds[0] - this.padding, params.bounds[1] -this.padding ]
params.append(this.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...