Skip to main content

Inspectors

One of the Toolkit's most popular demonstration applications has always been the Flowchart Builder. In version 6.2.0 of the Toolkit we upgraded this demonstration application to support "inspectors", which make use of the new browser-ui-inspector module.

note

This functionality is only available from version 6.2.0 of the Toolkit. Prior versions of the Toolkit do not offer this functionality.

Overview

An inspector is, basically, a form that can be used to edit the properties of some object, or set of objects, in the Toolkit's dataset. The approach that the Toolkit takes to supporting inspectors is to provide a top-level component which is aware of the DOM, but which does not generate any UI components itself. This allows you to use whatever you like to render your inspectors.

Creating an inspector

Since an inspector is agnostic of the HTML content it displays and of the means by which that content was generated, you need to supply a few helper functions. The constructor's signature is:

constructor(opts:InspectorOptions)

And the options are:

export interface InspectorOptions {
/**
* The element in which the inspector is drawn.
* @public
*/
container:HTMLElement
/**
* The surface to attach to.
*/
surface:Surface

/**
* Optional css class(es) to set on the inspector - a space separated list.
*/
cssClass?:string

/**
* Whether or not to support multiple selections. Defaults to true.
*/
multipleSelections?:boolean

/**
* Optional callback to invoke after an update has occurred.
*/
afterUpdate?:() => any

/**
* Callback invoked when the inspector is cleared.
* @private
*/
renderEmptyContainer:()=> void

/**
* Callback invoked when a new object has started to be edited.
* @param obj
* @param cb
*/
refresh:(obj:Base, cb:()=> any)=>void
}

The two callbacks you need to supply are renderEmptyContainer and refresh, and how you implement these methods is up to you.

The inspector binds to selection, undo and redo events from the instance of the toolkit that the surface is attached to. Whenever a new object is selected, the inspector either adds the object to its list of managed objects, or sets the object as its managed object (see multiple selections below), and the redraws the display.

When an undo or redo event is received from the toolkit, the inspector recalculates what can be edited, and then redraws the display.

VanillaInspector

The Toolkit ships a VanillaInspector class, which is a subclass of Inspector that renders content via the Toolkit's default template renderer. If you're not using a library integration and you do not have a complicated setup then VanillaInspector is a good choice.

To instantiate a VanillaInspector you need to supply, at a minimum, container, surface, and atemplateResolver:

const myInspector = new VanillaInspector({
container:someHTMLElement,
surface:someSurface,
templateResolver:(obj:Base) => {
// find some HTML that you want to use as an inspector for this object, and return it.
}
})

The vanilla inspector will generate its own refresh method that invokes your templateResolver, compiles it, injects it into the DOM and then invokes the cb() callback from the underlying inspector.

The vanilla inspector will also generate its own renderEmptyContainer method to pass to the underlying inspector, which, by default, will just render an empty div element. You can provide an emptyTemplate if you wish to override this behaviour:

const myInspector = new VanillaInspector({
container:someHTMLElement,
surface:someSurface,
templateResolver:(obj:Base) => {
// find some HTML that you want to use as an inspector for this object, and return it.
},
emptyTemplate:`<h1>There is nothing selected right now</h1>`
})

Caching templates

The Vanilla Inspector will, by default, cache templates, keyed both by their object type (node/group/port/edge) and the type field of each object. In some situations you might want to supply a template whose content was generated dynamically based upon the contents of some object that is being inspected, and in that case you can switch off the template cache:

const myInspector = new VanillaInspector({
cacheTemplates:false,
container:someHTMLElement,
surface:someSurface,
templateResolver:(obj:Base) => {
// find some HTML that you want to use as an inspector for this object, and return it.
},
emptyTemplate:`<h1>There is nothing selected right now</h1>`
})

Example - Flowchart Builder

This is the inspector from the Flowchart Builder application (minus a few constants that are declared in other files):

const inspectorTemplates = {
[TMPL_NODE_INSPECTOR] : `
<div class="jtk-inspector jtk-node-inspector">
<div class="jtk-inspector-section">
<div>Text</div>
<input type="text" jtk-att="text" jtk-focus/>
</div>

<div class="jtk-inspector-section">
<div>Fill</div>
<input type="color" jtk-att="${PROPERTY_FILL}"/>
</div>

<div class="jtk-inspector-section">
<div>Color</div>
<input type="color" jtk-att="${PROPERTY_TEXT_COLOR}"/>
</div>

<div class="jtk-inspector-section">
<div>Outline</div>
<input type="color" jtk-att="${PROPERTY_OUTLINE}"/>
</div>

</div>`,
[TMPL_EDGE_INSPECTOR] : `
<div class="jtk-inspector jtk-edge-inspector">
<div>Label</div>
<input type="text" jtk-att="${PROPERTY_LABEL}"/>
<div>Line style</div>
<jtk-line-style value="{{lineStyle}}" jtk-att="${PROPERTY_LINE_STYLE}"></jtk-line-style>
<div>Color</div>
<input type="color" jtk-att="${PROPERTY_COLOR}"/>
</div>`
}

/**
* Inspector for nodes/edges. We extend `VanillaInspector` here and provide a resolver to get an appropriate
* template based on whether the inspector is editing a node/nodes or an edge.
*/
export class FlowchartBuilderInspector extends VanillaInspector {

constructor(options:FlowchartInspectorOptions) {
super(Object.assign(options, {
templateResolver:(obj:Base) => {
if (isNode(obj)) {
return inspectorTemplates[TMPL_NODE_INSPECTOR]
} else if (isEdge(obj)) {
return inspectorTemplates[TMPL_EDGE_INSPECTOR]
}
}
}))

this.registerTag("jtk-line-style", createEdgeTypePickerTag(PROPERTY_LINE_STYLE, edgeMappings(), (v:string) => {
this.setValue(PROPERTY_LINE_STYLE, v)
}))

}
}

The key things to note here are:

  • We implement a templateResolver that looks up HTML from an object we have defined. In this case we discriminate between isNode(obj) and isEdge(obj), but you can implement any logic you like in this method. You may wish to return a different piece of HTML based on the object's type, for instance, or maybe you want to vary the response based upon some other condition extant in your application at the time.

  • The values of the jtk-att attribute map to keys in the underlying data:

{
text:"My Vertex",
fill:"#220044",
outline:"#000000",
textColor:"#FFFFFF"
}
  • We register a jtk-line-style tag on the inspector, which we use in the edge inspector HTML. This is discussed below.

Multiple selections

By default, an Inspector supports 'multiple selections'. When multiple objects are being edited, the inspector calculates a set of "common data", ie. the set of keys for which every object in the inspector has a value. For text inputs and text areas, if every value for some given key is the same across all the managed objects, that value is shown and can be edited, resulting in a change to all the managed objects. Otherwise, if not every value is the same, the text input or text area will display a blank value. If the user types in the input field then that change will be propagated to all the managed objects.

caution

If you have multipleSelections switched on (and remember it is switched on by default), then you might find yourself in a situation where you are editing different object types at the same time. This can, of course, be fine for some applications, but for others - such as the flowchart builder demonstration app - it is not.

To avoid editing multiple different types at once, you can set a selection mode of isolated on the underlying toolkit. See the documentation on selections


Supported input types

Currently, the list of supported input types is:

  • input type='text'
  • input type='radio'
  • input type='checkbox'
  • textarea
  • select
  • input type='color'
  • input type='hidden'

Edge type

The flowchart builder demonstration app makes use of the concept of edge property mappings to set the look and feel of different edge types. In our inspector for edges we could have just provided a drop down or set of radio buttons from which the user could choose a particular line style, but we thought it might be nicer to show the style instead, so we created a few UI components to support that. Although we created this for the purposes of demonstration it seems it might be useful for others so we have included it in the Toolkit.

EdgeTypePicker

At the core we have the EdgeTypePicker class, which, under the hood, creates a renderer and then draws a connection for each type listed in some set of edge mappings. This class can be used standalone (and is used that way by the various library integrations). To create one:

import { EdgeTypePicker } from "@jsplumbtoolkit/browser-ui"

const mappings:EdgePropertyMappings = ...
const currentValue = "targetArrow"

const myPicker = new EdgeTypePicker(someHTMLElement, mappings, currentValue, (v:string) => {
myInspector.setValue("lineStyle", v)
})

// now tell the picker to render and provide the name of the property to render mappings for
myPicker.render("lineStyle")

note

This class is not directly coupled to an inspector. In order to use the selected value from this class in an inspector you'll want to invoke setValue on the inspector, as shown above.

Here's an example that renders the edge property mappings for lineStyle from the flowchart builder demonstration application - you can click on these to change the label on the right:

targetArrow

EdgeTypePicker tag

If you want to embed an EdgeTypePicker into your own templates (and you are using the VanillaInspector) you can do so by registering a tag on the inspector after you create it. We showed this above in the code for the flowchart builder's inspector, in which we actually create a subclass of VanillaInspector and register the tag in its constructor, but you can also invoke this method on a vanilla inspector you have created yourself:


import { VanillaInspector, createEdgeTypePickerTag } from "@jsplumbtoolkit/browser-ui"

const PROPERTY_LINE_STYLE = "lineStyle"
const edgeMappings:EdgePropertyMappings = ...

const myInspector = new VanillaInspector(....)

myInspector.registerTag("jtk-line-style", createEdgeTypePickerTag(PROPERTY_LINE_STYLE, edgeMappings, (v:string) => {
this.setValue(PROPERTY_LINE_STYLE, v)
}))