External drag
Drag things from outside the layout into it - a sidebar of widgets, a palette, files from the OS.
A tab does not have to exist before it is dragged. Two paths create one on drop:
Dockable.DragSource: an element of your page (a sidebar item, a palette entry) that carries the tab to create.onExternalDragonDockable.Root: accepts drags that did not start in your UI at all, such as files from the OS, links, or another library's draggable elements.
Both drop through Actions.addTab, so everything else keeps working: onAction sees (and may veto
or replace) the add, setOnAllowDrop decides where it may land,
the drop indicator follows the pointer, and the new tab can land in a tabset's centre, on its
edges, in its tab strip or at the layout edge.
A sidebar of widgets
A Dockable.DragSource needs the layout's model and the tab to create. It does not have to be
inside Dockable.Root:
import { Model } from "@fragiola/dockable";
import { Dockable } from "@fragiola/dockable-react";
function Sidebar({ model }: { model: Model }) {
return (
<ul>
<Dockable.DragSource
model={model}
// a function builds the tab at each drag start
json={() => ({ type: "tab", name: "Revenue", component: "chart" })}
onDrop={(tab) => tab && console.log(`added ${tab.getName()}`)}
render={<li />}
className="cursor-grab data-dragging:opacity-50"
>
Revenue chart
</Dockable.DragSource>
</ul>
);
}
export function App() {
const [model] = useState(() => Model.fromJson(json));
return (
<div className="flex h-dvh">
<Sidebar model={model} />
<Dockable.Root model={model} className="flex-1">
{/* Row, Panels, DropIndicator… */}
</Dockable.Root>
</div>
);
}The panel renders the new tab like any other: pick the content from tab.getComponent() (and
tab.getConfig()).
While the drag is over the layout, the source has data-dragging and so does the root: style the
tabsets as drop targets with in-data-dragging: (Tailwind) or
[data-layout-path="/layout"][data-dragging] [role="tablist"] (CSS).
Keyboard users
Native drag and drop cannot be done from the keyboard. Make the source a button and let a press do the same thing, into the active tabset:
import { Actions, DockLocation, LayoutEngine } from "@fragiola/dockable";
const addToActiveTabset = () => {
const engine = LayoutEngine.of(model); // the engine of the mounted layout
const target = model.getActiveTabset() ?? model.getFirstTabSet();
if (engine && target) {
engine.doAction(Actions.addTab(json, target.getId(), DockLocation.CENTER, -1, true));
}
};
<Dockable.DragSource model={model} json={json} render={<button type="button" onClick={addToActiveTabset} />}>
Revenue chart
</Dockable.DragSource>LayoutEngine.of(model) returns the engine of the mounted layout, so UI outside the root can
dispatch through onAction instead of calling model.doAction directly.
Files and other foreign drags
onExternalDrag is called when a drag the layout did not start enters it (once per entry). Return the
tab to create, or undefined to ignore the drag:
<Dockable.Root
model={model}
onExternalDrag={(event) => {
// until the drop, browsers expose only the types, not the data
if (!event.dataTransfer?.types.includes("Files")) return undefined;
return {
json: { type: "tab", name: "File", component: "file" },
onDrop: (tab, drop) => {
const file = drop.dataTransfer?.files[0];
if (!tab || !file) return; // vetoed by onAction
LayoutEngine.of(model)?.doAction(Actions.renameTab(tab.getId(), file.name));
files.set(tab.getId(), file); // keep the File outside the model: it is not JSON
},
};
}}
>Read the payload (files, getData("text/uri-list"), …) in onDrop: during the drag the browser
hides it. An external drag ends when it leaves every layout; the next one asks again.
Where a new tab may land
setOnAllowDrop receives the dragged node, a temporary TabNode built from your json, so rules
can read its component:
model.setOnAllowDrop((dragNode, dropInfo) => {
const kpi = dragNode instanceof TabNode && dragNode.getComponent()?.startsWith("kpi-");
const intoKpis = dropInfo.node.getId() === "kpis";
return kpi ? intoKpis && dropInfo.location === DockLocation.CENTER : !intoKpis;
});The same rule applies when a tab already in the layout is moved.
Advanced exampleDashboard builderBuild a dashboard by dragging widgets from a palette: KPIs only go in the KPI strip, charts and tables anywhere else, and the layout is saved between visits.Open the live exampleVetoing or replacing the add
A drop dispatches Actions.addTab(json, targetId, location, index). onAction receives it like
any other action (action.type === Actions.ADD_TAB, the JSON in action.data.json): return it,
return a replacement (a different JSON, for example), or return undefined to cancel the drop.
onDrop then receives undefined instead of the tab.
Lower layers
useDragSource({ model, json, onDrop, disabled })isDockable.DragSourcewithout the element: spread itsdraggable,onDragStartandonDragEndon your own (hooks).- In the core,
engine.getDragDropManager().addTabWithDragAndDrop(event, json, onDrop?, dragImage?)starts a new-tab drag from a nativedragstart, andonDragEnded()ends it fromdragend. An adapter for another framework calls these.