Multi-select + drag-and-drop¶
Source: examples/panels/wunderbaum/multi_select_dnd.py
Test: tests/panels/wunderbaum/examples/test_multi_select_dnd.py
selectMode: "multi" treats a checkbox as another view of the row selection rather than a state of its own: checking a folder checks everything below it, but unchecking a child never changes the folder. Combined with dnd: True, a drag that starts on a checked row moves the whole selection.
The code¶
import panel as pn
from panelini import Panelini
from panelini.panels.wunderbaum import Wunderbaum
source = [
{
"title": "Folder A",
"key": "a",
"expanded": True,
"children": [
{"title": "File 1", "key": "a/1"},
{"title": "File 2", "key": "a/2"},
{"title": "File 3", "key": "a/3"},
],
},
# ... Folder B with File 4 and File 5
]
tree = Wunderbaum(
source=source,
options={"checkbox": True, "selectMode": "multi", "dnd": True},
height=260,
)
selection_display = pn.pane.Markdown("**Selected:** (none)")
def selected_keys(src: list[dict]) -> list[str]:
"""Walk *src* and return the keys of every node marked ``selected``."""
keys: list[str] = []
def walk(nodes: list[dict]) -> None:
for node in nodes:
if node.get("selected"):
keys.append(node["key"])
walk(node.get("children", []))
walk(src)
return keys
def _on_source_change(*args: object) -> None:
keys = selected_keys(tree.source)
selection_display.object = f"**Selected:** {', '.join(keys)}" if keys else "**Selected:** (none)"
tree.param.watch(_on_source_change, ["source"])
Key points:
selectMode: "multi"propagates in one direction only. Checking a folder checks its whole subtree; unchecking a single child leaves the folder checked, and checking every child of a folder does not check the folder. There is no tri-state parent, unlikehier.Selection lands in
tree.sourceas aselected: Trueflag per node, so a singleparam.watchonsourceis enough to keep a live display in sync.With
dnd: True, a drag that starts on a checked row carries every selected node. Dragging an unchecked row still moves only that row.Nodes that cannot move are dropped from the set before the move: the drop target itself, any node already sitting where the drop would put it, and any node whose selected ancestor is travelling with it.
How the test exercises it¶
The test does five interactions and asserts after each one, both client-side (reading node.selected through a findInShadowRoots helper) and server-side (via selected_keys(tree.source)):
clicking
Folder A’s checkbox selectsa, a/1, a/2, a/3;unchecking
File 2leavesa, a/1, a/3- the folder stays checked;checking
File 4andFile 5givesa, a/1, a/3, b/4, b/5, withFolder Bitself unchecked;dragging
File 4onto the middle ofFolder Amoves onlyb/4andb/5, leavingFolder Awitha/1, a/2, a/3, b/4, b/5andFolder Bempty.
Step 4 is the interesting one: five nodes are selected, but Folder A is the drop target and a/1/a/3 are already its children, so only the two files from Folder B actually move.
Run it live¶
This example runs entirely in your browser via Pyodide. The first load downloads packages, so give it a few seconds.
See also¶
Checkbox tree - hierarchical selection - the
hiercounterpart, with tri-state parents and upward propagationVirtual filesystem - the full demo - the full demo, with columns, context menu, and file drop