Dockable
Guides

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 example

1. Attributes

Set them per node in the JSON, or for every node through the global attributes (tabEnableDrag, tabSetEnableDrop, …):

attributeoneffect
enableDragtabwhether the user can drag the tab (the Tab is not draggable when false)
enableDragtabsetwhether the user can drag tabs out of this tabset
enableDroptabsetwhether tabs can be dropped into this tabset
enableDividetabsetwhether dropping on an edge of this tabset splits it to create a new tabset
pinnedtaba pinned tab cannot be dragged out of its tabset
enableEdgeDockglobalenable 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.

On this page