Drag and Drop
Drag-and-drop Capabilities for tree items and external drag objects
Source | View Source |
Types | View Types |
Import | import { drag-and-dropFeature } from "@headless-tree/core |
Type Documentation | ConfigurationStateTree InstanceItem Instance |
Overview
The Drag-And-Drop Feature provides drag-and-drop capabilities. It allows to drag a single tree item (or many of the selection feature is included in the tree config) and drop it somewhere else in the tree. The feature also allows you to create interactions between the tree and external drag objects, allowing you to drag tree items out of the tree, or foreign data objects from outside the tree inside. As extension of that, this can also be used to implement drag behavior between several trees.
This page explains the general idea of how Dnd works in Headless Tree, and how to get started with dragging items within a tree. The guides following after will go into more depth about
- TODO
Configuring Drag and Drop
The gist of configuring Drag and Drop in Headless Tree, is to
- add the
dragAndDropFeature
to the tree config, - define the indent of your tree items in the config variable
indent
, and - handle the
onDrop
event in the tree config.
Note that the indent
property is not required in the core tree configuration, since you can just use a custom
indentation when rendering your items. For Drag and Drop it is required however to determine the drop position
when the user is trying to reparent an item. (TODO link)
The Dnd Feature also exposes a Dnd State that you can handle yourself if you want to.
import {
syncDataLoaderFeature,
dragAndDropFeature,
selectionFeature,
} from "@headless-tree/core";
const tree = useTree<Item>({
indent: 20,
canReorder: true,
onDrop: (items, target) => {
// handle drop
},
features: [
syncDataLoaderFeature,
selectionFeature,
dragAndDropFeature,
],
});
The Drop Event
When the user drops tree items onto a target within the tree, the onDrop
event is called
with the list of items that were dragged, and a drop target. This drop target is an object that contains
either
- the
item
that is dropped to, if the user dragged on the title of an item with the intent to make it a child of that item
or, if the user dragged between items while the canReorder
config option is set to true:
- the
item
that should become the new parent - the
childIndex
, describing the index within theitem
where the user dropped - the
insertionIndex
, describing the index within theitem
where the items should be placed (see details below) dragLineIndex
anddragLineLevel
describing the position of the drag line that is rendered while dragging
childIndex
and insertionIndex
childIndex
describes the visual location inside the item that is being dropped into, where the user is dragging
the items to. insertionIndex
on the other hand also respects the special case that the user might drag items within
the same folder from further up in a folder, to further down in the same folder.
If items are dragged from a different location into a new folder, childIndex
and insertionIndex
will be the same.
However, if the two top-most items in a folder are dragged two items down, childIndex
will be 4, but insertionIndex
will be 2, since removing those two items will shift the items up by two.
Rendering a Tree with Drag and Drop
The only difference between rendering a normal HT Tree, and rendering one with Drag and Drop capabilities, is that you
need to render a drag line that shows the user where the items will be dropped. You just need to render an additional
div with the style tree.getDragLineStyle()
, and style it however you want. The computed stylings will automatically
add display: none
if the user is not currently dragging items, and will compute relative positioning based on the
item rendered with tree.getContainerProps()
.
Below are examples of how to render a tree with Drag and Drop capabilities, as well as the exemplary CSS stylings used in the demo.
<div {...tree.getContainerProps()} className="tree">
{tree.getItems().map((item) => (
<MyTreeItem key={item.id} item={item} />
))}
<div style={tree.getDragLineStyle()} className="dragline" />
</div>
.dragline {
position: absolute;
height: 2px;
width: unset;
margin-top: -1px;
background-color: #00aff4;
}
/* Render the circle at the left of the dragline */
.dragline::before {
content: "";
position: absolute;
left: 0;
top: -3px;
height: 4px;
width: 4px;
background: #fff;
border: 2px solid #00aff4;
border-radius: 99px;
}
Mutating your data after the Drop
When the drop event is registered with onDrop
, you will likely want to mutate your data source to reflect
the new order of items after the drag. Headless Tree provides three utility methods that make this very easy:
removeItemsFromParents(movedItems, onChangeChildren)
: Calls the providedonChangeChildren
handler to remove the items defined in the first argument from their parents.insertItemsAtTarget(itemIds, target, onChangeChildren)
: Calls the providedonChangeChildren
handler to insert the items defined in the first argument at the target defined in the second argument.createOnDropHandler(onChangeChildren)
: Combines the two methods above to create a drop handler that removes the items from their parents and inserts them at the target. This can directly be passed to theonDrop
config option.
itemIds
and movedItems
are arrays of item instances, and thus the same type as what the onDrop
config option provides.
Similarly, target
is the same type as the drop target that is provided in the onDrop
event.
In most situations, it is sufficient to call createOnDropHandler
with a function that mutates your data source, and
use that to handle drop events of items within a single tree. The other methods are useful for interactions between
several trees (imagine items being removed from one tree and then inserted in another), handling cases where foreign
data is dragged into a tree to create a new tree item or tree items being dragged out of the tree to external drop targets,
or handling other more complicated use cases.
import { createOnDropHandler } from "@headless-tree/core";
const tree = useTree<Item>({
indent: 20,
canReorder: true,
onDrop: createOnDropHandler((item, newChildren) => {
myData[item.getId()].children = newChildren;
}),
features: [
syncDataLoaderFeature,
selectionFeature,
dragAndDropFeature,
],
});
In the Demo above, switch to the sample "Multiple Trees" in the selection box to see a sample
of how to use the removeItemsFromParents
and insertItemsAtTarget
methods instead of the
createOnDropHandler
method.
Updated caches in async data loaders
You can ignore this when using removeItemsFromParents
, removeItemsFromParents
or createOnDropHandler
to handle
the drop event, as those handlers do this for you.
When you handle the drop event yourself while using the async data loader, note that the async data loader caches tree data and does not automatically update the cache when you mutate the data source.
When you update the childrens assigned to an item as response to a drop event, you can update the cache with
item.updateCachedChildrenIds(children: string[])
:
// Update your data source
myData[parent.getId()].children = newChildren;
// Update cache of async data loader
parent.updateCachedChildrenIds(newChildren);
// Trigger the recomputation of the internal tree structure
tree.rebuildTree();