Drag and Drop
Out of the box, GraphEditorInputMode’s NodeDropInputMode can handle drop events where an INode instance is dropped. A copy of the node will then be created at the location of the drop. Similarly, ILabels and IPorts are handled by LabelDropInputMode and PortDropInputMode, respectively.
However, you can use the drag and drop mechanism in yFiles for HTML to drag and drop anything into the GraphComponent. As usual, an IInputMode is responsible for handling the appropriate events on the GraphComponent.
General Drag and Drop Support
The DropInputMode class provides an abstraction for drop event handling. Each instance of DropInputMode is specific for a ’format’ string that is passed to the constructor. This string serves as an identifier for the input mode and lets you install several input modes that handle different droppped data. The DropInputMode processes any drag events that occur on the GraphComponent and fires its events accordingly if the drag operation carries data of the expected format.
For example, if you wish to process files that are dropped onto the GraphComponent, you could use the following DropInputMode:
const dropInputMode = new DropInputMode('someFormat')
Its subclasses ItemDropInputMode<T> and NodeDropInputMode handle only drag and drop operations that carry IModelItems and INodes, respectively.
If you wish to process drag and drop operations for arbitrary types of objects however, you need to configure a DropInputMode appropriately. For this, you can react to the following drag related events:
Event | Occurs when … |
---|---|
For example, you could implement a DropInputMode that accepts strings. This would enable you to drag and drop a string into the GraphComponent and automatically create a node with the dropped string as label by reacting to the drag-dropped event, as illustrated in the following example:
const dropInputMode = new DropInputMode('someFormat')
dropInputMode.addEventListener('drag-dropped', () => {
// drop data is list of dropped files
const data = dropInputMode.dropData
if (data.length > 0) {
// this is a string... - create a node with a label using the given text
const node = graphComponent.graph.createNode()
graph.addLabel(node, data)
}
})
dropInputMode.priority = graphEditorInputMode.nodeDropInputMode.priority - 1
graphEditorInputMode.add(dropInputMode)
You can also control the area on which a drop may occur via the validDropHitTestable property. By default, the area is unrestricted. For example, you could set the property to NEVER, thus effectively preventing to drop anywhere, and then only react to the above mentioned drag events:
dropInputMode.addEventListener('drag-entered', (_, sender) => {
// first call the super class method that does some basic checks
// (is the input mode active, is the GraphComponent editable, does the type of the drag match)
const dropData = dropInputMode.dropData
// we only accept dropping one file
if (dropData === 'magicData') {
// set the valid drop HitTestable so that nowhere can be dropped (effectively rejecting the drag)
sender.validDropHitTestable = IHitTestable.NEVER
// important: remember to reset the HitTestable when the drag is exited/canceled
}
})
// reset the HitTestable when the drag is exited/canceled
// The DragLeft event is raised in those cases, so we can do this here
dropInputMode.addEventListener('drag-left', (_, sender) => {
sender.validDropHitTestable = IHitTestable.ALWAYS
})
Drag and Drop of Graph Items
The ItemDropInputMode<T> adds highlight and preview functionality to DropInputMode and is specific for IModelItem. It also supports snapping of the previewed item. You can disable these features as desired using the properties listed below.
- showPreview
- Determines whether to show a preview of the dragged item during the drag.
- snappingEnabled
- Determines whether snapping is enabled. Note that a valid snap context either has to be set as snapContext or it has to be retrieved from the IInputModeContext, which is the case if snapping is enabled on the parent GraphEditorInputMode.
- highlightDropTarget
- Determines whether to indicate a potential drop target item with a highlight visualization.
After a drop operation, the ItemDropInputMode<T> delegates the creation of an item to a callback that can be set using the itemCreator property. The callback gets all information needed to create a copy of the dragged item in the GraphComponent’s graph:
Parameter | Description |
---|---|
context | |
graph | |
draggedItem | |
dropTarget | null . |
dropLocation |
A newly created default ItemDropInputMode<T> without a set itemCreator doesn’t do anything on its own. You have to either set an itemCreator or, in the case of INode or IStripe, use the subclasses NodeDropInputMode or StripeDropInputMode. They have an itemCreator set by default to handle dropping the items that they are specified for.
Preview within the GraphComponent
The GraphComponent can display a preview of the currently dragged item with proper snapping support while dragging within the GraphComponent. There are several protected methods that you can override to customize the drop preview:
- initializePreview
- Is called when the drag enters the GraphComponent. The default implementation initializes a separate graph model that is meant to contain the preview items. It then calls the following two methods:
- populatePreviewGraph
- Subclasses are meant to create items that act as a drop preview in this method. The given IGraph instance is not the IGraph of the GraphComponent, but a separate instance that only contains the preview items. The base implementation does nothing.
- updatePreview
- Called for each drag event that occurs while dragging over the GraphComponent. Again, the given IGraph instance is not the IGraph of the GraphComponent but a separate instance that only contains the preview items. The derived classes should update the drop preview items in this method accordingly, e.g., adjusting the preview item’s layout to the drag location or similar.
- cleanUpPreview
- This method is called once the drag and drop operation is completed or canceled and gives client the chance to free resources, etc.
The following example shows how to customize the preview to make the preview item transparent:
class TransparentPreviewNodeDropInputMode extends NodeDropInputMode {
initializePreview() {
// call super method that generates a preview node in the preview graph
super.initializePreview()
const previewNode = super.previewGraph.nodes.first()
// assume ShapeNodeStyle for sake of brevity
const nodeStyle = previewNode.style.clone()
// important: clone the style to not alter the original style,
// because the reference to that style is used for the new node
const color = nodeStyle.fill // assume it's a color
// use the same color, but transparent
const transparentFill = Color.fromRGBA(color.r, color.g, color.b, 0.6)
nodeStyle.fill = transparentFill
super.previewGraph.setStyle(previewNode, nodeStyle)
}
}
const nodeDropInputMode = new TransparentPreviewNodeDropInputMode()
graphEditorInputMode.nodeDropInputMode = nodeDropInputMode
class TransparentPreviewNodeDropInputMode extends NodeDropInputMode {
initializePreview(): void {
// call super method that generates a preview node in the preview graph
super.initializePreview()
const previewNode = super.previewGraph!.nodes.first()!
// assume ShapeNodeStyle for sake of brevity
const nodeStyle = previewNode.style.clone() as ShapeNodeStyle
// important: clone the style to not alter the original style,
// because the reference to that style is used for the new node
const color = nodeStyle.fill as Color // assume it's a color
// use the same color, but transparent
const transparentFill = Color.fromRGBA(color.r, color.g, color.b, 0.6)
nodeStyle.fill = transparentFill
super.previewGraph!.setStyle(previewNode, nodeStyle)
}
}
const nodeDropInputMode = new TransparentPreviewNodeDropInputMode()
graphEditorInputMode.nodeDropInputMode = nodeDropInputMode
Preview outside the GraphComponent
The library provides direct support for two approaches to display a drop preview while dragging the element outside the GraphComponent.
Providing a Custom Preview Element
A custom preview element can be provided as last argument of the startDrag method. A similar method is also available for the LabelDropInputMode, PortDropInputMode and StripeDropInputMode.
The given element will automatically be included in the DOM and positioned below the pointer location during the drag operation. It will be removed when the gesture is canceled or the element is actually dropped into the GraphComponent.
We recommend to hide this custom preview element while the dragging location is within the GraphComponent and let the ItemDropInputMode<T> display its preview element instead because only then, item snapping can be considered and properly applied to the dragged item.
To hide the custom preview element, a suitable CSS class may be applied when the drag gesture enters the GraphComponent or removed again when the gesture leaves the GraphComponent. NodeDropInputMode.startDrag returns a DragSource whose query-continue-drag event receives a QueryContinueDragEventArgs. The QueryContinueDragEventArgs provides a reference to the current DropTarget which can be used to determine if there is a DropTarget that can handle the visualization of the preview element.
// create a drag preview element for a node that is shown during the drag
const dragPreview = document.getElementById('myCustomNodePreviewElement')
// start the drag
const dragSource = NodeDropInputMode.startDrag(
dragElement,
simpleNode,
DragDropEffects.ALL,
true,
dragPreview
)
// let the GraphComponent handle the preview rendering if possible
dragSource.addEventListener('query-continue-drag', (evt) => {
if (evt.dropTarget === null) {
removeClass(dragPreview, 'hidden')
} else {
addClass(dragPreview, 'hidden')
}
})
The Simple Drag And Drop demo illustrates this approach.
Using native HTML Drag and Drop API
The native HTML drag and drop API is also supported and may be used as alternative approach for showing a drag preview that is also visible outside of the GraphComponent.
In contrast to the approach of providing a custom preview element, there is no way of letting the ItemDropInputMode<T> take control while the dragged element is within the GraphComponent due to how the HTML drag and drop API is designed. Therefore, there is no previewGraph and no snapping support.
To use this approach, the draggable elements need to have the draggable
attribute
set to true
as well as a dragstart
listener that configures some drop properties
and also sets the transfer data to an item ID that represents the actual dragged graph item.
Also, a customized itemCreator needs to provide
different items to insert into the graph.
The transferred data in the drag event may only be strings, which is why we need to
use a map from the node id to the actual node item which holds the information
about the node’s layout, style, tag etc.
// this map is used to get the node instance from the string data
// of the data transfer during 'drop'
const nodes = new Map()
// fill the map with the draggable nodes
for (let i = 0; i < draggableNodeItems.length; i++) {
// the actual node item that is dragged
const node = draggableNodeItems[i]
// the node ID is used to get the node instance from the data transfer during 'drop'
const nodeID = `node ${i}`
nodes.set(`node ${i}`, node)
// creates a div with draggable="true" that contains a representation of the dragged node
const nodeElement = createNodeVisual(node, exportGraphComponent)
// set the node ID as data of the data transfer and configure some other drop properties
nodeElement.addEventListener('dragstart', (e) => {
e.dataTransfer.dropEffect = 'copy'
e.dataTransfer.effectAllowed = 'copy'
e.dataTransfer.setData('INode', nodeID)
})
// finally, add the visual to palette
dragDropPalette.appendChild(nodeElement)
}
// make the item creator return the node according to the given id
const oldItemCreator = nodeDropInputMode.itemCreator
nodeDropInputMode.itemCreator = (
context,
graph,
info,
dropTarget,
dropLocation
) => {
const node = typeof info === 'string' ? nodes.get(info) : info
// The old item creator handles the placement of the new node in the graph
return oldItemCreator(context, graph, node, dropTarget, dropLocation)
}
// this map is used to get the node instance from the string data
// of the data transfer during 'drop'
const nodes = new Map()
// fill the map with the draggable nodes
for (let i = 0; i < draggableNodeItems.length; i++) {
// the actual node item that is dragged
const node = draggableNodeItems[i]
// the node ID is used to get the node instance from the data transfer during 'drop'
const nodeID = `node ${i}`
nodes.set(`node ${i}`, node)
// creates a div with draggable="true" that contains a representation of the dragged node
const nodeElement = createNodeVisual(node, exportGraphComponent)
// set the node ID as data of the data transfer and configure some other drop properties
nodeElement.addEventListener('dragstart', (e) => {
e.dataTransfer!.dropEffect = 'copy'
e.dataTransfer!.effectAllowed = 'copy'
e.dataTransfer!.setData('INode', nodeID)
})
// finally, add the visual to palette
dragDropPalette.appendChild(nodeElement)
}
// make the item creator return the node according to the given id
const oldItemCreator = nodeDropInputMode.itemCreator!
nodeDropInputMode.itemCreator = (
context: IInputModeContext,
graph: IGraph,
info: any,
dropTarget: IModelItem | null,
dropLocation: Point
): any => {
const node = typeof info === 'string' ? nodes.get(info) : info
// The old item creator handles the placement of the new node in the graph
return oldItemCreator(context, graph, node, dropTarget, dropLocation)
}
Drag and Drop of INodes
The NodeDropInputMode is a specialized ItemDropInputMode<T> for nodes. It specifies the expected format to be ‘INode’.
To customize the creation process of the item that is being dropped, override the createNode method.
The default implementation of the createNode method is very sophisticated and minimizes the effort that is necessary to customize node dropping behavior. We’d recommend trying to configure the NodeDropInputMode properties and override the auxiliary methods like getNodeLayout first to achieve the desired behavior before resorting to override this method. Even then it is probably wise to let the derived class call the super method to let it perform the actual node creation first and then apply the desired customizations to the created node instead of implementing the whole process yourself.
An example for an application that uses the NodeDropInputMode to drag nodes from a palette and drop them into the GraphComponent is the Drag and Drop
Behavior of NodeDropInputMode
In the default configuration, the NodeDropInputMode manages dropping nodes directly into groups. The created node will then be part of that group. You can customize what groups are valid targets for the drop by setting an appropriate isValidParentPredicate.
Furthermore, you can enable additional grouping/folding features on the NodeDropInputMode. For example, you can instruct it to automatically convert regular nodes to group nodes when dragging and dropping a node on it by enabling the allowNonGroupNodeAsParent property.
Additionally, enabling allowFolderNodeAsParent lets you drop nodes into collapsed group nodes, adding them to the corresponding group nodes as well.
Customizing the Dropped Node
NodeDropInputMode provides hooks to alter the node that was created in the IGraph as a result of the drag and drop operation after the operation is completed.
You can:
- Change the location and size of the dropped node by overriding getNodeLayout(pointerLocation: Point, size: Size): Rect.
- Create the node directly as a group node by setting an appropriate isGroupNodePredicate.
- Make alterations to the created node by overriding createNode as mentioned earlier.
Drag and Drop of ILabels
The LabelDropInputMode is a specialized ItemDropInputMode<T> for labels. It specifies that the expected data format is ILabel.
To customize the creation process of the item that is being dropped, override the createLabel method.
The default implementation of the createLabel queries the lookup of the provided ILabelOwner for an IEditLabelHelper.
Customizing the Dropped Label
LabelDropInputMode provides hooks to alter the label that was created in the IGraph as a result of the drag-and-drop operation after the operation is completed.
You can:
- Change the ILabelModelParameter of the dropped label by overriding getNewLabelModelParameter.
- Change the owner of the newly created label by overriding getDropTarget.
Note that returning
null
will prevent a label from being created at the given location. - Make alterations to the created label by overriding createLabel as mentioned earlier.
Drag and Drop of IPorts
The PortDropInputMode is a specialized ItemDropInputMode<T> for ports. It specifies that the expected data format is IPort.
To customize the creation process of the item that is being dropped, override the createPort method.
The default implementation of the createPort method also copies the labels from the dragged port to the newly created port.
Customizing the Dropped Port
PortDropInputMode provides hooks to alter the port that was created in the IGraph as a result of the drag-and-drop operation after the operation is completed.
You can:
- Change the IPortLocationModelParameter of the dropped port by overriding getNewPortLocationModelParameter.
- Change the owner of the newly created port by overriding getDropTarget.
Note that returning
null
will prevent a port from being created at the given location. - Make alterations to the created port by overriding createPort as mentioned earlier.