Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ Each node type has a defined set of required and optional attributes.

Weights on rows and tabsets specify their relative size within the parent row. The absolute values do not matter, only their proportions (e.g., two tabsets with weights 30 and 70 would render the same as if they had weights 3 and 7).

Rows and tabsets can instead retain a chosen pixel size with `preferredWidth` or `preferredHeight`. For example, set `preferredWidth: 240` on a left tabset and `preferredWidth: 280` on a right tabset, leaving the middle tabset without a preferred width. The side panels keep their widths as the window resizes or a sibling closes, floats, or moves into another tabset; the middle panel fills the remaining space. Multiple flexible siblings share that space by weight. See [the preferred sizes example](examples/preferred-sizes/PreferredSizes.tsx) for a nested workspace with a retained-height console.

Weight and preferred sizes can coexist. A preferred size takes precedence over weight on the parent row's split axis: `preferredWidth` for a horizontal row, `preferredHeight` for a vertical row. Weight remains stored and splitters continue to update it; weight controls sizing whenever no explicit or derived preference applies on that axis. For example, `{ weight: 30, preferredWidth: 240 }` requests 240 pixels in a horizontal parent row and uses weight in a vertical parent row without a preferred height. Every row and tabset has a default weight of 100 even if omitted from JSON, so setting a preference does not require removing weight.

Preferred sizes describe the outer panel dimensions, including its tabstrip/header, and apply on the parent row's split axis. An explicit preference on a row sizes the whole group; otherwise a row derives its preference when all its children retain a size on that axis. Root rows, sole children and maximized tabsets fill their container. Without a flexible sibling, preferred panels can leave unused space.

Dragging a splitter updates the affected preferred sizes. Minimum and maximum constraints still apply: panels may shrink when space is insufficient and recover their preferred sizes when it becomes available, without overwriting the saved preferences. While compressed, splitter movement adjusts the preference by the drag amount; constraints can limit the visible movement. Preferences are saved by `model.toJson()`, can be changed with `Actions.updateNodeAttributes`, and belong to the row or tabset rather than individual tabs. Moving tabs into an existing tabset uses the destination's sizing policy.

Moving a whole tabset carries its preferences. Moving an individual tab into a new tabset does not copy its source tabset's preferences; the new tabset is flexible unless attributes supplied during creation give it a preference. Docking beside a tabset on the parent row's split axis preserves the target's preferred size even though its weight is split. Docking on the other axis transfers the target's preference for the parent axis to the new containing row. Resizing that row updates the group's preference, leaving each child tabset's own preferences intact. The group keeps its preference even if only one child remains; moving that tabset out of the group carries its own saved preference, which can differ from the group's visible size.

If any child of a row has an explicit or derived preference on its split axis, splitter actions arrive at `onAction` and `onModelChange` as an `Actions.GROUP` (`GroupAction`) containing an `ADJUST_WEIGHTS` action and `UPDATE_NODE_ATTRIBUTES` actions for the preferences. Callbacks that detect splitter changes must inspect `GroupAction.actions` as well as bare `ADJUST_WEIGHTS` actions. Accept or cancel the whole group in `onAction` so weight and preference updates remain together. During live resizing, `isAdjusting()` is true on the group; the final action has it set to false. Weight-only row splitters still emit bare `ADJUST_WEIGHTS` actions.

Live preferred-size updates use the same direct DOM sizing and measurement path as weight updates, including refreshing derived ancestor preferences, with a full redraw on drag completion or when the optimization cannot apply.

NOTE: The easiest way to create your initial layout JSON is to use the [demo](https://caplin.github.io/FlexLayout/demos/v0.11/demo/index.html) app. Modify an existing layout by dragging, dropping, and adding nodes, then press the 'print' button to print the JSON to the browser's developer console. Use the Model Explorer panel to view and modify the attributes of the layout and its nodes, and use the render dropdown in the demo to blank out the panels (other than the Model Explorer) so you can focus on the layout.

By changing global or node attributes, you can modify the layout's appearance and functionality. For example, setting `tabSetEnableTabStrip: false` in the global options would change the layout into a multi-splitter (without tabs or drag-and-drop):
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ <h1>FlexLayout Examples</h1>
<li><a href="tabset-rendering/">Tabset Rendering</a> — customizing the tabset header via onRenderTabSet</li>
<li><a href="placeholder/">Placeholder</a> — a custom placeholder when a tabset is empty</li>
<li><a href="min-sizes/">Min Sizes</a> — panels with a 200px minimum size</li>
<li><a href="preferred-sizes/">Preferred Sizes</a> — adjustable auxiliary panels retain pixel sizes while the workspace fills the remaining space</li>
<li><a href="tab-wrapping/">Tab Wrapping</a> — wrapping tabs onto multiple lines</li>
<li><a href="pinned-tabs/">Pinned Tabs</a> — tabs pinned to the start of the tabstrip</li>
<li><a href="tab-groups/">Tab Groups</a> — Chrome-style colored group pills in the tabstrip</li>
Expand Down
104 changes: 104 additions & 0 deletions examples/preferred-sizes/PreferredSizes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { createRoot } from "react-dom/client";
import { Actions, DockLocation, IJsonModel, Layout, Model, TabNode, useUndo } from "../../src/index";
import "../../style/combined.scss";
import { ExampleHeader } from "../Header";
import sourceCode from "./PreferredSizes.tsx?raw";

const json: IJsonModel = {
global: { tabEnableFloat: true },
layout: {
type: "row",
id: "root",
children: [
{
type: "tabset",
id: "explorer",
preferredWidth: 240,
minWidth: 140,
children: [{ type: "tab", id: "explorer-tab", name: "Explorer", component: "explorer" }],
},
{
type: "row",
children: [
{
type: "tabset",
id: "workspace",
minWidth: 120,
minHeight: 80,
children: [{ type: "tab", id: "workspace-tab", name: "Workspace", component: "workspace" }],
},
{
type: "tabset",
id: "console",
preferredHeight: 180,
minHeight: 80,
children: [{ type: "tab", id: "console-tab", name: "Console", component: "console" }],
},
],
},
{
type: "tabset",
id: "inspector",
preferredWidth: 280,
minWidth: 160,
children: [{ type: "tab", id: "inspector-tab", name: "Inspector", component: "inspector" }],
},
],
},
};

const factory = (node: TabNode) => {
const descriptions: Record<string, string> = {
explorer: "Starts at 240px wide. Its width stays the same when the inspector closes, floats, or joins the workspace.",
workspace: "Takes the remaining width and height. Resize the window or remove an auxiliary panel to see it grow.",
console: "Starts at 180px high. Drag its splitter to choose a new height, then resize the window.",
inspector: "Starts at 280px wide. Use the buttons above to close it, float it, or move its tab into the workspace.",
};
const description = descriptions[node.getComponent() ?? ""];
return description ? (
<div style={{ padding: 16 }}>
<h2>{node.getName()}</h2>
<p>{description}</p>
</div>
) : undefined;
};

function PreferredSizes() {
const { model, setModel, undo, redo, canUndo, canRedo } = useUndo(() => Model.fromJson(json));
if (!model) {
return null;
}
const inspectorIsDocked = model.getNodeById("inspector")?.getParent()?.getId() === "root";
return (
<div style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
<ExampleHeader title="Preferred Sizes" path="examples/preferred-sizes/PreferredSizes.tsx" source={sourceCode} />
<div style={{ padding: 8, display: "flex", flexWrap: "wrap", gap: 6 }}>
<button onClick={() => model.doAction(Actions.deleteTabset("inspector"))} disabled={!inspectorIsDocked}>
Close inspector
</button>
<button onClick={() => model.doAction(Actions.popoutTabset("inspector", "float"))} disabled={!inspectorIsDocked}>
Float inspector
</button>
<button onClick={() => model.doAction(Actions.moveNode("inspector", "workspace", DockLocation.CENTER, -1))} disabled={!inspectorIsDocked || !model.getNodeById("workspace")}>
Join workspace
</button>
<button onClick={undo} disabled={!canUndo}>
Undo
</button>
<button onClick={redo} disabled={!canRedo}>
Redo
</button>
<button onClick={() => setModel(Model.fromJson(json))}>Reset</button>
</div>
<div style={{ padding: "0 8px 8px" }}>Drag the splitters to choose panel sizes. Those choices are retained when the window or layout changes.</div>
<div style={{ position: "relative", flexGrow: 1, minHeight: 0, border: "1px solid #ddd" }}>
<Layout model={model} factory={factory} />
</div>
</div>
);
}

const container = document.getElementById("container");
if (container) {
createRoot(container).render(<PreferredSizes />);
}
22 changes: 22 additions & 0 deletions examples/preferred-sizes/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en" class="flexlayout__theme_alpha_light">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>FlexLayout Example: Preferred Sizes</title>
<style>
html,
body,
#container {
height: 100%;
margin: 0;
}
body {
font-family: ui-sans-serif, system-ui;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="module" src="./PreferredSizes.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions examples/vite.config.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default defineConfig({
"tabset-rendering": fileURLToPath(new URL("tabset-rendering/index.html", import.meta.url)),
"context-menu": fileURLToPath(new URL("context-menu/index.html", import.meta.url)),
"min-sizes": fileURLToPath(new URL("min-sizes/index.html", import.meta.url)),
"preferred-sizes": fileURLToPath(new URL("preferred-sizes/index.html", import.meta.url)),
"pinned-tabs": fileURLToPath(new URL("pinned-tabs/index.html", import.meta.url)),
"sticky-button": fileURLToPath(new URL("sticky-button/index.html", import.meta.url)),
"tab-groups": fileURLToPath(new URL("tab-groups/index.html", import.meta.url)),
Expand Down
32 changes: 30 additions & 2 deletions src/model/IJsonModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,20 @@ export interface IRowAttributes {
*/
id?: string;

/**
preferred outer height in pixels; takes precedence over weight in a vertical parent row without removing the weight; splitters update it, while passive resizing preserves it

Default: undefined
*/
preferredHeight?: number;

/**
preferred outer width in pixels; takes precedence over weight in a horizontal parent row without removing the weight; splitters update it, while passive resizing preserves it

Default: undefined
*/
preferredWidth?: number;

/**


Expand All @@ -570,7 +584,7 @@ export interface IRowAttributes {
type?: string;

/**
relative weight for sizing of this row in parent row
relative weight for sizing of this row in parent row when no explicit or derived preferred size applies on the parent's split axis; retained and updated by splitters even when a preferred size takes precedence

Default: 100
*/
Expand Down Expand Up @@ -731,6 +745,20 @@ export interface ITabSetAttributes {
*/
name?: string;

/**
preferred outer height in pixels; takes precedence over weight in a vertical parent row without removing the weight; splitters update it, while passive resizing preserves it

Default: undefined
*/
preferredHeight?: number;

/**
preferred outer width in pixels; takes precedence over weight in a horizontal parent row without removing the weight; splitters update it, while passive resizing preserves it

Default: undefined
*/
preferredWidth?: number;

/**
index of selected/visible tab in tabset; -1 means no tab selected

Expand All @@ -753,7 +781,7 @@ export interface ITabSetAttributes {
type?: string;

/**
relative weight for sizing of this tabset in parent row
relative weight for sizing of this tabset in parent row when no preferred size applies on the parent's split axis; retained and updated by splitters even when a preferred size takes precedence

Default: 100
*/
Expand Down
Loading