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.
Attaching a decorator
Decorators are specified in the render options for a Surface. They can be referenced just by their name, or by their name with some constructor options. You can attach an arbitrary number of decorators.
import { newInstance } from "@jsplumbtoolkit/browser-ui"
const toolkit = newInstance()
const surface = toolkit.render(someElement, {
"decorators": [
"MyFirstDecorator",
{
"type": "MySecondDecorator",
"options": {
"anOption": true,
"anotherOption": 42
}
}
]
});
JsPlumb does not ship with any pre-built decorators.
Creating a Decorator
The first step is to write the skeleton for your decorator and register it. 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 {
incremental = false
constructor(params:Record<string, any>, adapter:Surface, container:Element) {
super(params, adapter, container)
}
reset(params:any):void {
...
}
decorate(params:{
surface: Surface,
adapter: AbstractLayoutAdapter<BrowserElement>,
layout: AbstractLayout<any>,
setAbsolutePosition: (el:BrowserElement, xy:PointXY) => void,
toolkit: JsPlumbToolkit,
bounds: Extents,
positions: Map<string, PointXY>,
fixElement:(el: BrowserElement, pos: PointXY, constraints?: FixedElementConstraints, id?:string) => FixedElement,
floatElement:(el: BrowserElement, pos: PointXY) => void,
appendElement:(el:BrowserElement, pos:PointXY, alignment?:AppendedElementAlignments) => void
}):void {
...
}
}
registerDecorator(MyDecorator, function() {
this.reset
})
Registering a decorator with ES5
var MyDecorator = function (params, surface, container) {
this.incremental = true;
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 JsPlumb 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.
- setAbsolutePosition: (el:Element, xy:PointXY)
Sets the absolute position of some element. Note that this method does not add the element to the dom - use appendElement
if you want the surface to also append the element.
- appendElement:(el:BrowserElement, pos:PointXY, alignment?:AppendedElementAlignments)
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 {x:..., y:...}
- floatElement(el: BrowserElement, pos: PointXY)
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 specify the left and top.
- fixElement(el: BrowserElement, pos: PointXY, constraints?: FixedElementConstraints, id?:string):FixedElement
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, {x:50, y:50 }, { left:true });
fixElement
returns a FixedElement
object, which is a handle that can be used to find out the current positioning of some fixed element. We use it internally for a few niche tasks but it may be useful for users of the API also.
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 Extents
object, containing xmin
, ymin
, xmax
and ymax
values, giving the extents of the content. Remember that 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 JsPlumb, 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, params.bounds, { left:true });
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.
- 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.
- toolkit
The underlying JsPlumb Toolkit instance.
Incremental decoration
By default, the surface will clear all of the elements added by a decorator prior to instructing the decorator to decorate the canvas. You can change this behaviour by declaring that your decorator is "incremental", ie. that your decorator takes on the responsibility of maintaining its state with regards the elements it adds. incremental
is a member of the IDecorator
interface and is an abstract member of Decorator
. You can, as shown in the example below, provide a value for this in your decorator class. If you do not provide a value it is assumed that your decorator is not incremental.
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
JsPlumb will not take care of z-index for you. Webapps come in a million different shapes and sizes; it would be too invasive for JsPlumb to infer anything. Keep this in mind when you write your decorators...