Drag and drop
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 two classes to assist with drag and drop: 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 other 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.
Surface Drop Manager
Instantiation
At the bare minimum, you need to provide these parameters when you create an instance of the Surface Drop Manager:
import { SurfaceDropManager } from "@jsplumbtoolkit/browser-ui"
new SurfaceDropManager({
source:someElement,
selector:"[data-node-type]",
surface:renderer,
dataGenerator: (el) => {
return {
name:el.getAttribute("data-node-type"),
type:el.getAttribute("data-node-type")
};
}
});
- source The element containing other elements that are draggable
- selector a CSS3 selector identifying elements within
source
that are draggable - surface The Surface to attach to
- dataGenerator A function that can return an appropriate data object representing some element that is being dragged.
Options
The full set of options available are:
Interface SurfaceDropManagerOptions
Members
data-jtk-is-group
attribute on the element, with a value of true
.scale
transform to elements that are being dragged so that they appear
at the same size as the Surface they're being dragged to. Setting this flag to true will switch off that behaviour.source
that are draggableBy default, the Surface Drop Manager is configured to allow nodes/groups to be dropped onto the canvas or an existing edge, and for nodes to be dropped on groups. You can control this via the appropriate allowDropOn***
flags.
Providing data for a dragged element
The dataGenerator
function is used to get a suitable piece of backing data for some element that is being dragged. In the example above (which comes from the Database Visualiser demo), we provide a data object with name
and type
properties. The default mechanism used by the Toolkit for determining the type of some object is to test the type
member. Providing type
as we have here allows the Toolkit to determine which template to use to render the node, how it will behave, etc.
Specifying a dragged element's type
As mentioned, the Toolkit's default mechanism for determining the type of some object is to look at its type
member. If you wish, you can provide your own typeGenerator
function for the Surface Drop Manager to use.
Distinguishing between a node and a group
By default, the Toolkit will look for a jtk-is-group
attribute on a dragged element. If the value of this attribute is "true"
, the Toolkit will assume the element represents a group. You can provide your own groupIdentifier
if you wish; the signature is as shown above.
Specifying the size for a new element
By default, the DropManager (and SurfaceDropManager) clones the DOM element on which the user started a drag and then sets the size of the element that is being dragged to match the bounding client rectangle of the element that was cloned.
In some situations, particularly when you have a grid in your surface, you may wish to mandate the size for any new elements that are being dragged on to the surface, which you can do by providing a dragSize
object:
const dropManager = new SurfaceDropManager({
source:someElement,
selector:"[data-node-type]",
surface:renderer,
dragSize:{ w:250, h: 100 }
})
Magnetizing the UI when dropping on an edge
By default, the Toolkit will "magnetize" the UI when a node or group is inserted between two existing nodes/groups. You
can switch this off by setting magnetize:false
.
Filtering draggable elements
It is possible, when drag starts, to decide whether or not you want the dragged element to be droppable on the canvas:
new SurfaceDropManager({
surface:SomeSurfaceWidget,
source:someElement,
selector:".draggable-child",
canvasDropFilter:(data:ObjectData):boolean => {
return data.type === "someDroppableOnCanvasType";
}
})
Getting notification of a new vertex
You can create a SurfaceDropManager
with an onVertexAdded
callback, which will be invoked whenever a new vertex has been dropped onto the canvas:
new SurfaceDropManager({
surface:SomeSurfaceWidget,
source:someElement,
selector:".draggable-child",
onVertexAdded(vertex, dropTarget) => {
}
})
vertex
is the new vertex that was added. In the event that the new vertex was dropped on top of some existing node, dropTarget
will be provided, containing information about the node onto which the new vertex was dropped, as well as its position and size.
Enabling/disabling
Use the setEnabled(enabled:boolean)
method on a SurfaceDropManager to enable/disable it.
Library Integrations
Wrappers for the Surface Drop Manager are available for:
For Svelte no wrapper library is needed.
Drop Manager
The Drop Manager provides a superset of the functionality that the Surface Drop Manager offers: you can configure a drag source in the same way, but the Drop Manager does not manipulate the dataset in any way when an event occurs - it just hits an appropriate callback function with the event details.
Basic Example
import { DropManager } from "@jsplumbtoolkit/browser-ui"
const dropManager = new DropManager({
surface:SomeSurfaceWidget,
source:someElement,
selector:".draggable-child",
onDrop:(data, target, draggedElement, event, position) => {
console.log("drop on node or group", arguments);
},
onCanvasDrop:(data, canvasPosition, draggedElement, event, position) => {
console.log("drop on canvas", arguments);
},
onEdgeDrop:(data, target, draggedElement, event, position) => {
console.log("drop on edge", arguments);
},
dataGenerator:(el) => {
return {type: el.getAttribute("data-type") }
}
})
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
- dataGenerator A function that can return an appropriate data object representing some element that is being dragged.
In this setup, objects can be dropped on nodes, groups, edges, and the whitespace of the canvas.
Options
The full set of options available are:
Interface DropManagerOptions
Members
scale
transform to elements that are being dragged so that they appear
at the same size as the Surface they're being dragged to. Setting this flag to true will switch off that behaviour.source
.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:
new DropManager({
...
dataGenerator:(el:Element):ObjectData => {
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.
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:
new DropManager({
surface:SomeSurfaceWidget,
source:someElement,
selector:".draggable-child",
dataGenerator:(el) => {
return {type: el.getAttribute("data-type") }
},
onEdgeDrop:(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:
new DropManager({
surface:SomeSurfaceWidget,
source:someElement,
selector:".draggable-child",
dropFilter:(data:ObjectData, nodeOrGroup:Node|Group):boolean => {
return nodeOrGroup.data.foo !== true;
},
onDrop:(data, target, draggedElement, event, position) => {
console.log("drop on node or group", arguments);
}
})
And of course you can also filter by edge:
new DropManager({
surface:SomeSurfaceWidget,
source:someElement,
selector:".draggable-child",
edgeDropFilter:(data:ObjectData, edge:Edge):boolean => {
return edge.data.foo !== true;
},
...
})
Filtering draggable elements
It is possible, when drag starts, to decide whether or not you want the dragged element to be droppable on the canvas:
new DropManager({
surface:SomeSurfaceWidget,
source:someElement,
selector:".draggable-child",
canvasDropFilter:(data:ObjectData):boolean => {
return data.type === "someDroppableOnCanvasType";
},
onDrop:(data, canvasPosition, draggedElement, event, position) => {
console.log("drop on canvas", arguments);
}
})
Enabling/Disabling the Drop Manager
You can disable/enable the entire drop manager at any time:
dropManager.setEnabled(false)
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 were asked about fairly regularly, to the point that we ended up including this functionality in the Surface Drop Manager. For interests sake, we provide here an example onEdgeDrop
function that achieves this functionality using the Drop Manager
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:(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.
CSS
There are three CSS classes that are assigned to parts of the UI during the lifecycle of a drag (this applies to both DropManager
and SurfaceDropManager
)
name | default | purpose | |
---|---|---|---|
dragActiveClass | jtk-drag-drop-active | Assigned to any node, group or edge that is a target for a drop of the current element, and also to the canvas when the current element is not hovering over a candidate node. edge or group. | |
dragHoverClass | jtk-drag-drop-hover | Assigned to any drop target (edge, node or group) 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 | |
dragElementClass | jtk-drag-drop-current | Assigned to the element that is currently being dragged. Note that the element being dragged is a child of the root element, so make sure that if you use this selector you have scoped it correctly in your CSS |
You can provide your own values for these in the drop manager constructor:
new SurfaceDropManager({
...
dragActiveClass:"drag-active",
dragHoverClass:"you-can-drop-here",
dragElementClass:"this-is-being-dragged",
...
});
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.
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 SurfaceDropManager({
surface:SomeSurfaceWidget,
source:someElement,
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.