Drag and drop
Set dragAndDrop: true to let the user move nodes with the mouse or by touch:
new TreeElement({
data,
dragAndDrop: true,
htmlElement,
});Drag a node in this tree — onto a folder, or between two nodes:
Click a node, open a folder, or drag something.
While dragging, a ghost element shows where the node will land: a line between two nodes for a before or after drop, and a highlight on the folder for an inside drop. Dragging over a closed folder opens it after openFolderDelay milliseconds (500 by default; false disables it).
Dragging starts after startDndDelay milliseconds (300 by default), so a plain click still selects instead of starting a drag.
Restricting moves
onCanMove decides whether a node may be dragged at all:
new TreeElement({
dragAndDrop: true,
htmlElement,
onCanMove: (node) => node.name !== "locked",
});onCanMoveTo decides whether a specific drop is allowed. It gets the dragged node, the node it is dropped on and the position ("before", "after" or "inside"):
new TreeElement({
dragAndDrop: true,
htmlElement,
onCanMoveTo: (movedNode, targetNode, position) => {
// only allow dropping into folders, never between nodes
return position === "inside" && targetNode.isFolder();
},
});In this tree, that onCanMoveTo is in effect: a node can only be dropped into a folder, and the line between two nodes never appears.
A move that both hooks allow is applied by the tree itself.
Reacting to a move
The tree.move event fires when a node is dropped. Its moveInfo describes the move:
element.addEventListener("tree.move", (e) => {
const info = e.detail.moveInfo;
console.log("moved", info.movedNode.name);
console.log("relative to", info.targetNode.name, "at", info.position);
console.log("out of", info.previousParent.name);
});This is the place to tell the server:
element.addEventListener("tree.move", (e) => {
const info = e.detail.moveInfo;
void fetch("/move-node/", {
body: JSON.stringify({
node: info.movedNode.id,
position: info.position,
target: info.targetNode.id,
}),
headers: { "Content-Type": "application/json" },
method: "POST",
});
});Call preventDefault() to stop the tree from applying the move, and moveInfo.doMove() to apply it later — for instance after the server has confirmed it:
element.addEventListener("tree.move", (e) => {
const info = e.detail.moveInfo;
e.preventDefault();
void fetch("/move-node/", { method: "POST" /* ... */ }).then((response) => {
if (response.ok) {
info.doMove();
}
});
});Drag handles
By default the whole node title is a drag handle. onIsMoveHandle narrows that to specific elements:
new TreeElement({
dragAndDrop: true,
htmlElement,
onIsMoveHandle: (element) => element.classList.contains("drag-handle"),
});Combine it with onCreateLi to render the handle:
new TreeElement({
dragAndDrop: true,
htmlElement,
onCreateLi: (node, li) => {
const handle = document.createElement("span");
handle.className = "drag-handle";
handle.textContent = "☰";
li.querySelector(".tree-element-element")?.prepend(handle);
},
onIsMoveHandle: (element) => element.classList.contains("drag-handle"),
});Following the drag
onDragMove is called while a node is being dragged, onDragStop when the drag ends. Both get the node and the underlying event:
new TreeElement({
dragAndDrop: true,
htmlElement,
onDragMove: (node, event) => {
console.log("dragging", node.name, event);
},
onDragStop: (node) => {
console.log("stopped dragging", node.name);
},
});isDragging() tells you whether a drag is in progress:
if (tree.isDragging()) {
// ...
}Moving nodes from code
moveNode does the same as a drop, without any user interaction:
tree.moveNode(tree.getNodeById(2), tree.getNodeById(1), "inside");After changing the layout
Hit areas are computed when a drag starts. If you change the tree's size or position mid-drag, call refreshHitAreas():
tree.refreshHitAreas();