JointJS+ Stencil

ui.Stencil

Stencil plugin implements a palette of JointJS elements. These elements can be dragged onto a paper.

Installation

Include joint.ui.stencil.js and joint.ui.stencil.css to your HTML:

<link href="joint.ui.stencil.css" rel="stylesheet" type="text/css">
<script src="joint.ui.stencil.js"></script>

Creating a stencil

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
    el: $('#paper'),
    width: 500,
    height: 300,
    model: graph
});

var stencil = new joint.ui.Stencil({
    paper: paper,
    width: 200,
    height: 300
});

$('#stencil-holder').append(stencil.render().el);

The joint.ui.Stencil constructor takes a paper object and the dimensions of the stencil area. The next thing to do is rendering the stencil widget (stencil.render()) and appending the stencil element to a holder which can be any element in your HTML. Please see the demo for reference on how to do that if you're in doubts.

Stencil Configuration

The joint.ui.Stencil constructor function takes an options object as an argument. The options object can have the following parameters:

paperthe joint.dia.Paper or joint.ui.PaperScroller object [mandatory]
widththe width of the stencil [mandatory]
heightthe height of the stencil [mandatory]
searchobject - An object defining what properties of the elements in the stencil will be used for searching. The object has element types as keys (or * wildcard denoting any type) and arrays with element property paths as values. The current element values under those property paths will be used by stencil to find a match between the search term and those values.
search: {
    '*': ['attrs/text/text'],
    'standard.Image': ['description'],
    'standard.Path': ['description']
}
function - A function executed on every cell in the stencil returning true (matched) or false (unmatched).
search: function(cell, keyword, group, stencil) {
    var description = cell.get('description');
    return typeof description === 'string' && description.indexOf(keyword) > -1;
}
See the Searchable Stencil section for more information.
groupsAn object that defines the groups in the stencil (if any). The keys of this object are strings that uniquely identify the groups and values are objects that contain label, index and closed properties. label property specifies the name of the group that will be displayed in the UI, index is a number specifying the position of the group among other groups within the stencil and the optional closed property tells stencil whether this group should be closed or opened by default. An example of this object can look like:
groups: {
    basic: { label: 'Basic Elements', index: 1 },
    text: { label: 'Text', index: 2, closed: true },
    advanced: { label: 'Advanced Elements', index: 3, closed: true }
}
groupsToggleButtonsIf set to true buttons for expanding/collapsing groups (expand all/collapse all) are rendered into the stencil. It defaults to false.
dropAnimationAn object defining an animation that will be performed on an element that is dropped outside the target paper area. It defaults to undefined meaning that there will be no animation. The dropAnimation object can have duration and easing properties where duration is the duration of the animation in milliseconds (defaults to 150) and easing is either "swing" (the default) or "linear". Example:
dropAnimation: {
    duration: 300,
    easing: 'linear'
}
labelA string or HTML Element rendered at the top of the stencil. It defaults to `Stencil`.
layout If set to true, the stencil elements are automatically laid out using the Grid Layout plugin. To adjust the layout behavior, pass an object defining the layout configuration. For all available options see the aforementioned plugin.
layout: {
    columnWidth: 100,
    columns: 3,
    rowHeight: 100,
}

The layout can be defined as a function in which case it will be called for each group on initialization and after the stencil is filtered. The function receives the group graph as the first parameter and the group definition as the second parameter. Note that the graph does not contain elements that don't match the search criteria (if any). Each graph has the `group` attribute set to the group id.

layout: function(graph, group) {
    const groupName = graph.get('group');
    if (groupName === 'my-group-1') {
        // layout the elements in this group only
        graph.getElements().forEach(function(el, index) {
            el.position(0, index * 100);
        });
    }
});

dragStartClone(element)A function that produces an element clone when the user starts dragging. It defaults to element.clone().
dragEndClone(element)A function that produces an element clone when the user places the dragged element into the paper. It defaults to element.clone().
canDrag(elementView, evt, groupName)A function to determine whether an element from the stencil can be dragged by the user or not. All elements can be dragged by default.
snaplinesAn instance of the Snapline plugin which is responsible for drawing snaplines while the user drags an element from the stencil.
scaleClonesWhen set to true dragged clones are automatically scaled based on the current paper transformations. Note, that this option is ignored when snaplines option applied (it always scales the elements). It defaults to false.
paperOptions An object with joint.dia.Paper options to adjust the look and behavior of the stencil's papers. e.g.
paperOptions: {
  elementView: CustomElementView,
  background: { color: 'red' }
}
This options can be also used to adjust a single paper (and takes precedence) when defined inside of the group definition. e.g.
groups: {
  myGroup: {
    index: 1,
    label: 'My Group',
    paperOptions: { color: 'blue' }
  }
}
To adjust the graph in Stencil papers it's also possible to pass a model as well. Please note, the paperOptions has to be a function in such a case:
paperOptions: function() {
  return {
    elementView: CustomElementView,
    model: new joint.dia.Graph([], { cellNamespace: CustomElementNamespace }),
    background: { color: 'red' }
  }
}
contentOptions When the stencil papers are resized to fit their content, the joint.dia.Paper:fitToContent is used internally. Use this option to modify the behavior.
container A CSS selector or a DOM element is the container element, which the element being dragged is appended to.
paperPadding (deprecated) A number specifying the padding that will be used in the internal papers that hold the elements in the stencil. It defaults to 10.

Populating Stencil with elements

const r = new joint.shapes.standard.Rectangle({
    position: { x: 10, y: 10 }, size: { width: 50, height: 30 }
});
const c = new joint.shapes.standard.Circle({
    position: { x: 70, y: 10 }, size: { width: 50, height: 30 }
});

// Stencil is rendered in the DOM now.

// load the elements into the default group
stencil.load([r, c]);

// load the elements into multiple groups
stencil.load({ group1: [r], group2: [c] });

// load the elements into the `group2` group
stencil.loadGroup([r, c], 'group2');

As you can see, there is multiple way how to populate the stencil with elements. Use either stencil.load() or stencil.loadGroup() which make sure the elements are rendered into the stencil.

Note: Populating the stencil with elements needs to be done after the stencil is rendered in the DOM.

Stencil groups (a.k.a. Accordion)

Creating an accordion-like Stencil is as easy as passing the groups object to the stencil constructor. groups object is a hash table mapping group identifiers to the configuration of the particular group. Each group configuration object contains these properties:

labelgroup label. Can be either a string or a html.
indexposition of the group in the stencil.
closed[optional] when true, this group will be initially closed.
height[optional] the height of the paper containing the shapes of the group. If not defined, then the height of the stencil will be used. If non of these height are defined, the paper will be automatically scaled to fit its content (recommended).
layout[optional] same as the stencil layout option. It allows you to override the global settings for a particular group.
paperOptions[optional] same as the stencil paperOptions option. It allows you to override the global settings for a particular group.
const stencil = new joint.ui.Stencil({
     graph: graph,
     paper: paper,
     width: 200,
     groups: {
          one: { label: 'First group', index: 1 },
          two: { label: 'Second group', index: 2, closed: true }
     }
  });

Searchable Stencil

The Stencil allows users to filter the elements by an arbitrary keyword. That is useful especially when the palette contains a lot of elements. To enable this add search: { /* rules */ } option to the Stencil constructor.

The rules determine what attributes we match a given keyword with and are defined by an object with ELEMENT_TYPE: [ATTRIBUTE_PATH1, ATTRIBUTE_PATH2, ..] key-value pairs. Where ELEMENT_TYPE is either an element type (e.g standard.Rectangle) or '*' to match any type. ATTRIBUTE_PATH is a path to element's attribute or property (e.g 'attrs/text/text', 'description').

const stencil = new joint.ui.Stencil({
  search: {
    '*': ['attrs/text/text'],
    'standard.Image': ['description'],
    'standard.Path': ['description']
  }
});

The rules can be alternatively defined as a function, which, upon search, will be invoked for each element with the element model, string keyword, the group id the element belongs to, and the stencil instance as its arguments.

const stencil = new joint.ui.Stencil({
  search: function(element, keyword, groupId, stencil) {
    return element.get('type').includes(keyword) || groupId.includes(keyword);
  }
});

By default all the unmatched elements and groups are hidden (display: none; is set on them). Alternatively you can add .joint-stencil .joint-element.unmatched { display: block; } and .joint-stencil .group.unmatched { display: block } rules to your css stylesheet in order to have the unmatched elements translucent.

Here's a list of all CSS classes toggled by filtering.

ClassName CSS Selector Description
stencil-filtered .joint-stencil.stencil-filtered A keyword was provided. The stencil is filtered.
not-found .joint-stencil.not-found There are no results for the given keyword.
unmatched .joint-stencil .group.unmatched A stencil group of which none of the elements match the filter.
unmatched .joint-stencil .joint-element.unmatched An element, which does not match the filter.

The Stencil triggers the 'filter' event with a matched subset of elements wrapped in joint.dia.Graph as the first argument every time the elements get filtered.

The default text of the Stencil can be modified as shown below.

// No Matches Found
stencil.el.dataset.textNoMatchesFound = 'No matches found!';
// Search Placeholder
stencil.el.querySelector('.search').placeholder = 'My Placeholder';

API

load(groupedElements)

Accept an object with multiple key-value pairs, where key is the name of a group and value an array of elements to be rendered into that group. For instance:

stencil.load({
    group1: [{ type: 'standard.Rectangle' },{ type: 'standard.Circle' }],
    group2: [{ type: 'standard.Image' }]
});

Note that the elements could be also defined as plain javascript objects. That is useful for example when you store the stencil configuration in a JSON, which is received from a DB.

If the method is called with an array it behaves like the loadGroup below.

loadGroup(elements [, group]) Accept an array of elements and render them into a single stencil group. If no group provided, the default group is used.
getGraph(groupName) Get the graph associated with the group identified by groupName. If the stencil does not use groups, just omit the groupName parameter to get the only graph present.
getPaper(groupName) Get the paper associated with the group identified by groupName. If the stencil does not use groups, just omit the groupName parameter to get the only paper present.
setPaper(paper) Set the target paper for the stencil. It tells stencil to use a different paper than the one that was passed to it in the initialization through the options object. This is useful if you have a stencil instantiated and want to change the target paper dynamically. For example, if you have tabs each having its own paper with its own diagram but you want to use only one stencil, they you can call setPaper(myTab2) whenever the active tab changes. Note that the paper argument can be both a joint.dia.Paper object or the joint.ui.PaperScroller object, the stencil can handle both.
filter(keyword [, searchOptions]) Filter the stencil elements based on the given keyword and the current (or provided) search options.
openGroup(groupName) Open group groupName.
closeGroup(groupName) Close group groupName.
toggleGroup(groupName) Toggle group groupName.
isGroupOpen(groupName) Return true if the group with the groupName is open. Return false otherwise.
openGroups() Open all groups.
closeGroups() Close all groups.
stopListening() Disable dragging of elements from the stencil.
startListening() Enable dragging of elements from the stencil.
freeze(opt) Freeze all the papers of the stencil. In this state, the paper does not automatically re-render upon changes in the graph. This is useful when for instance the stencil is collapsed (not visible on the screen). For more information see paper.freeze().
unfreeze(opt) Unfreeze all the papers of the navigator. For more information see paper.unfreeze().
startDragging(cell, event) Start dragging an arbitrary cell (element or link). The cell will be added to the preview paper as is (i.e. the dragStartClone() callback will not be called). The dragEndClone() callback will be called as usual. The event argument is a DOM event object that defines, using clientX and clientY, where the preview is to be displayed.

Since the cell does not have to be from the stencil, this method can be used to initiate dragging an element outside the stencil (e.g. from an HTML list).

Note that you should not render the stencil if it is never to be attached to the DOM. Calling stencil.render() would create detached DOM elements and take up memory unnecessarily.


HTML:
<ul>
    <li data-type="element">Element</li>
    <li data-type="link">Link</li>
</ul>
JS:
const stencil = new ui.Stencil({ paper });

const listEl = document.querySelector('ul');
listEl.addEventListener('pointerdown', (evt) => {
    const { type } = evt.target.dataset;
    let cell;
    switch (type) {
        case 'element':
            cell = new shapes.standard.Rectangle({
                size: { width: 100, height: 100 }
            });
            break;
        case 'link':
            cell = new shapes.standard.Link({
                source: { x: 0, y: 0 },
                target: { x: 100, y: 100 }
            });
            break;
        default:
            throw new Error(`Unknown type: ${type}`);
    }
    stencil.startDragging(cell, evt);
});
cancelDrag(opt) Stop the current drag operation. If opt.dropAnimation is not specified, the default stencil.options.dropAnimation is used.
isDragCanceled() Was the current drag operation canceled? Returns a boolean value.

Reacting on elements added to the paper

Many times, you might want to perform some action as a reaction on a new element dragged from the stencil to the paper. This can be achieved by usual means, i.e. by reacting on new elements added to the graph:

graph.on('add', function(cell, collection, opt) {
      // The stencil adds the `stencil` property to the option object with value
      // set to a client id (`cid`) of the stencil view.
      if (opt.stencil) {
          console.log('A cell with id', cell.id, 'was just added to the paper from the stencil.');
      }
  });

Events

The Stencil object triggers events that you can react on in your applications. These events can be handled by using the Stencil on(eventName, handler) method.

Event Name Handler Signature Description
element:dragstart
cloneView: dia.ElementView,
evt: dia.Event,
dropArea: g.Rect,
validDropTarget: boolean
Triggered when the user starts dragging an element. See element:drag for more information about the parameters passed to the handler function.
element:drag
cloneView: dia.ElementView,
evt: dia.Event,
dropArea: g.Rect,
validDropTarget: boolean
Triggered while an element is being dragged.
The cloneView is an element view rendered for the model returned by the dragStartClone() method.
dropArea is the area (in local coordinates), which the element would occupy if it were a part of the target graph.
validDropTarget is a boolean value that answers whether the element is currently over a valid target paper.
stencil.on('element:drag', (cloneView, evt, dropArea, validDropTarget) => {
  cloneView.vel.attr('opacity', validDropTarget ? 1 : 0.3);
});
element:dragend
cloneView: dia.ElementView,
evt: dia.Event,
dropArea: g.Rect,
validDropTarget: boolean
Triggered when a drag operation ends (such as by releasing a mouse button or by calling the cancelDrag() method).
This event fires regardless of whether the drag completed or was canceled. The dragend event handler can check stencil.isDragCanceled() and the validDropTarget parameter to determine whether the drag operation had succeeded or not.
See element:drag for more information about the parameters passed to the handler function.
element:drop
elementView: dia.ElementView,
evt: dia.Event,
x: number,
y: number
Triggered when an element is dropped onto a valid target paper.
The elementView is an element view rendered for the model returned by the dragEndClone() method.
The x and y are coordinates (in local coordinate system) of the user pointer at the moment of the drop event.
stencil.on('element:drop', (elementView) => {
  const halo = new joint.ui.Halo({ cellView: elementView });
  halo.render();
});
drop:invalid
evt: dia.Event,
element: dia.Element
Triggered when the user drops an element from the stencil to an invalid area. Invalid area is defined as an area that is outside of the visible paper area so that the dropped element is not accepted by the target paper. Such an element is then either immediately disposed or returned to its original position in stencil in an animated fashion (see the dropAnimation option). This event gives you a chance to react when the invalid drop occurs.
The evt is a mouse event object and element is the element model that would have been otherwise dropped into the main paper (and graph for that matter).
filter
graph: dia.Graph,
groupName: string,
keyword: string
Triggered when the user uses the search input field to filter elements in the stencil.
The graph is a set of elements that matched the filter, groupName is the name of the group in which elements where filtered and keyword is the current search term.
group:open
groupName: string
Triggered when the user opens a closed group.
group:close
groupName: string
Triggered when the user closes an open group.