Restricting drops
What can be dragged where, today - attributes, setOnAllowDrop and onAction.
Three mechanisms decide what can be dragged and where it can land. All three exist today and work together.
Intermediate exampleLocked regionsStop drops into part of the layout: model.setOnAllowDrop refuses tabs that do not belong to a region, enableDrop, enableDrag and enableDivide lock a tabset, and onAction vetoes what gets through. Custom drop zones and feedback for refused drops are the Drop control Epic (#19).Open the live example1. Attributes
Set them per node in the JSON, or for every node through the global attributes
(tabEnableDrag, tabSetEnableDrop, …):
| attribute | on | effect |
|---|---|---|
enableDrag | tab | whether the user can drag the tab (the Tab is not draggable when false) |
enableDrag | tabset | whether the user can drag tabs out of this tabset |
enableDrop | tabset | whether tabs can be dropped into this tabset |
enableDivide | tabset | whether dropping on an edge of this tabset splits it to create a new tabset |
pinned | tab | a pinned tab cannot be dragged out of its tabset |
enableEdgeDock | global | enable docking to the edges of the layout |
{
"type": "tabset",
"id": "console",
"enableDrop": false,
"enableDivide": false,
"children": [{ "type": "tab", "name": "Console", "enableDrag": false }]
}Change them at runtime with Actions.updateNodeAttributes(nodeId, { enableDrop: false }) (a
"lock layout" toggle), or Actions.updateModelAttributes({ tabSetEnableDrop: false }) for all.
2. model.setOnAllowDrop
For rules that depend on what is dragged and where, give the model a predicate. It runs during the drag, for every candidate target, so a refused target never shows the indicator:
import { type DropInfo, type Node, TabNode, TabSetNode } from "@fragiola/dockable";
model.setOnAllowDrop((dragNode: Node, dropInfo: DropInfo) => {
const target = dropInfo.node; // the tabset (or row) the drop would go into
// tabs from the "tools" group only dock into tool tabsets
if (dragNode instanceof TabNode && dragNode.getConfig()?.group === "tools") {
return target instanceof TabSetNode && target.getConfig()?.kind === "tools";
}
// nothing docks to the layout's outer edge
return dropInfo.kind !== "edge";
});dropInfo has the target node, the location (a DockLocation: dropInfo.location.getName()
is "center", "top", …), the tab index, the outline rect and the kind ("rect" or
"edge").
Set it once, right after creating the model. It belongs to the model instance: a new model
(after undo/redo or restoring) needs it set
again. Or pass the same function as onAllowDrop to Dockable.Root: the root sets it on whatever
model it renders, and restores the model's own rule when the prop goes away.
<Dockable.Root model={model} onAllowDrop={allowDrop}>const [model] = useState(() => {
const model = Model.fromJson(json);
model.setOnAllowDrop(allowDrop);
return model;
});3. onAction: the last word
A drop dispatches Actions.moveNode through onAction, so you can still veto (or rewrite) a
drop at the moment it happens. Use it for rules that need async work or app state, knowing that
the indicator already showed the target as valid:
const onAction = (action: Action) => {
if (action.type === Actions.MOVE_NODE && isLocked(action.data.toNode)) {
return undefined;
}
return action;
};Feedback for a refused target
Over a target that a rule refuses, the outline hides, the browser shows its "not allowed" cursor,
and Dockable.Root, DropIndicator and the refusing TabSet get data-drop-refused. Style them
to say why: see Drop zones and drop feedback and the
locked-regions example above.