Skip to main content

Drop Manager

A common use case in the sorts of applications for which the Toolkit is useful is the requirement to be able to drag and drop new nodes/groups onto the workspace. The Toolkit ships with the @jsplumbtoolkit/drop package, which contains two classes - DropManager, a low level drag/drop manager, and SurfaceDropManager, an extension of the drop manager that makes integrating it with a surface a breeze. The difference between the two classes is that DropManager provides a means for you to respond to items being dropped onto a surface canvas, but it does not do anything when such an event occurs than inform you, via a set of callbacks, whereas the SurfaceDropManager provides implementations of these callbacks that manipulate the canvas and the data model. For the vast majority of applications the SurfaceDropManager will be the class you'll want to use. On this page we discuss the DropManager class; SurfaceDropManager is discussed on a separate page.

The main functions that the drop manager packages offers are:

  • the ability to drag objects onto the whitespace of the canvas
  • the ability to drag objects onto groups or nodes
  • the ability to drag objects onto edges
  • the ability to disable the drag/drop functionality programmatically

Installation#

"dependencies":{   ...,   "@jsplumbtoolkit/drop":"^5.0.0"   ...

Basic Example#

import { createManager } from "@jsplumbtoolkit/drop"const dropManager = createManager({        surface:SomeSurfaceWidget,    source:someHTMLElementContainingDraggableChildren,    selector:".draggable-child",    onDrop:function(data, target, draggedElement, event, position) {        console.log("drop on node or group", arguments);    },    onCanvasDrop:function(data, canvasPosition, draggedElement, event, position) {        console.log("drop on canvas", arguments);        },    onEdgeDrop:function(data, target, draggedElement, event, position) {        console.log("drop on edge", arguments);    }   })

The arguments here are:

  • surface Required. Identifies the Surface widget with which to interact
  • source Required. Identifies the DOM element inside which the Drop Manager will find draggable elements
  • selector Required. Identifies the child elements inside source that are draggable.
  • onDrop Optional callback to hit when the user drops an element on a node or group
  • onCanvasDrop Optional callback to hit when the user drops an element on whitespace in the canvas
  • onEdgeDrop Optional callback to hit when the user drops an element on an edge

In this setup, objects can be dropped on nodes, groups, edges, and the whitespace of the canvas.

Associating data with the dragged object#

When drag starts, you can generate some data for the drop manager to associate with the object being dragged. You do this with a dataGenerator function:

createManager({        ...    dataGenerator:function(el) {        return {type: el.getAttribute("data-type") }    },    ...})

Our dataGenerator here extracts the value of the data-type attribute on the element being dragged, and returns it in an object. The return value of the dataGenerator is what is passed in as the data argument to the various drop methods.

The signature of the dataGenerator function is:

export type DataGeneratorFunction<T> = (el:HTMLElement) => T;

Controlling drop targets#

You can control which parts of the Surface act as drop targets by only supplying specific callbacks. Here we'll disable everything except dropping on edges, because we've only supplied an onEdgeDrop callback:

createManager({        surface:SomeSurfaceWidget,    source:someHTMLElementContainingDraggableChildren,    selector:".draggable-child",        dataGenerator:function(el) {        return {type: el.getAttribute("data-type") }    },    onEdgeDrop:function(data, target, draggedElement, event, position) {       console.log("drop on edge", arguments);    }   })

Filtering drop targets#

You can use filters to exclude elements at drag time. In this example we'll filter out any node/group that has foo:true in its data:

createManager({        surface:SomeSurfaceWidget,    source:someHTMLElementContainingDraggableChildren,    selector:".draggable-child",    dropFilter:function(data, nodeOrGroup) {        return nodeOrGroup.data.foo !== true;        },    onDrop:function(data, target, draggedElement, event, position) {        console.log("drop on node or group", arguments);    }  })

The method signature for dropFilter is:

export type DropFilter<T> = (data:T, target:Node|Group) => boolean;

It is also possible, when drag starts, to decide whether or not you want the dragged element to be droppable on the canvas:

createDropManager({        surface:SomeSurfaceWidget,    source:someHTMLElementContainingDraggableChildren,    selector:".draggable-child",    canvasDropFilter:function(data) {        return data.type === "someDroppableOnCanvasType";        },    onDrop:function(data, canvasPosition, draggedElement, event, position) {        console.log("drop on canvas", arguments);    }  })

The method signature for the canvasDropFilter is:

export type CanvasDropFilter<T> = (data:T) => boolean;

And of course you can also filter by edge:

createManager({        surface:SomeSurfaceWidget,    source:someHTMLElementContainingDraggableChildren,    selector:".draggable-child",    edgeDropFilter:function(data, edge) {        return edge.data.foo !== true;        },      ...})

The method signature for edgeDropFilter is:

export type EdgeDropFilter<T> = (data:T, target:Edge) => boolean;

Enabling/Disabling the Drop Manager#

You can disable/enable the entire drop manager at any time:

dropManager.setEnabled(false)

CSS#

There are two CSS classes that are assigned to parts of the UI during the lifecycle of a drag (this applies to both DropManager and SurfaceDropManager)

namedefaultpurpose
dragActiveClassjtk-drag-drop-activeAssigned to any part of the UI that is a target for a drop of the current element
dragHoverClassjtk-drag-drop-hoverAssigned to any drop target over which the current element is hovering. When the mouse is released the element having this class will be the recipient of an on drop event

You can provide your own values for these in the drop manager constructor:

createManager({    dragActiveClass:"drag-active",    dragHoverClass:"you-can-drop-here"});

In order to use these classes for visual cues in the UI, you'll probably want to define slightly different selectors for each target type. Let's suppose when a drag starts we want to outline our canvas and any nodes/groups with a purple line, and we want to draw any possible target edges with a purple line too:

.jtk-surface.jtk-drag-drop-active, .jtk-node.jtk-drag-drop-active, .jtk-group.jtk-drag-drop-active {    outline:4px solid purple;}
svg.jtk-drag-drop-active path {    stroke:purple;}

Now when something is the current drop target, we'll either outline it green or make its path green:

.jtk-surface.jtk-drag-drop-hover, .jtk-node.jtk-drag-drop-hover, .jtk-group.jtk-drag-drop-hover {    outline:4px solid green;}
svg.jtk-drag-drop-hover path {    stroke:green;}

This is just an example of course. You can do anything with the CSS that you like.

Usage from within Typescript#

The drop manager takes a type parameter T that identifies the type of data that your dataGenerator function is going to return. You'll see the type T listed in various function definitions above; if you are not using Typescript then this type parameter disappears and you don't need to think about it.


Inserting a node between two nodes on edge drop#

This is a use case we are asked about fairly regularly. We provide here an example onEdgeDrop function you can use as a basis. Note though that the SurfaceDropManager class offers this functionality.

note

In this example, we are assuming that an Absolute layout is in use, and so the new node's position will be the left and top values we provide. But other layouts will place the new node where they think it ought to go.

onEdgeDrop:function(data:any, edge:Edge,                      draggedElement:HTMLElement, evt:Event,                      pageLocation:{left:number, top:number}) {        let positionOnSurface = surface.mapLocation(pageLocation);    toolkit.addFactoryNode(data.type, data,         function(newNode) {            let currentSource = edge.source; // the current source node/port            let currentTarget = edge.target; // the target node/port            toolkit.removeEdge(edge);            toolkit.addEdge({source:currentSource, target:newNode});            toolkit.addEdge({source:newNode, target:currentTarget});            surface.setPosition(newNode, positionOnSurface.left, positionOnSurface.top);        }    ); }

In this example we use the addFactoryNode method to add a new node. This mechanism allows us to generate the data for the new node via our node factory. You could also use addNode(someData) here. We also assume you have a reference to surface - the Surface object you're dropping nodes on - and toolkit, the underlying Toolkit instance.


Working with decorators#

If you have any Decorators in your UI, you may wish to inform the drop manager about the elements they have created, because without doing this the drop manager will not be able to recognise them as background. To do this, you use the canvasSelector option:

new jsPlumbToolkitDropManager({        surface:SomeSurfaceWidget,    source:someHTMLElementContainingDraggableChildren,    selector:".draggable-child",    canvasSelector:".someElementMyDecoratorCreated",      ...})

canvasSelector takes any valid CSS3 selector. This identifies the elements that your decorator has created that the drop manager should treat as background.