Skip to content
Merged
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
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Change Log

## 1.2.0 - 2025-29-25
## 1.2.1

- **Fixed:** v0.9 Hidden Tabset Fix, courtesy of @Lukas Götz. See PR Conversation [here](https://github.com/caplin/FlexLayout/pull/485).

## 1.2.0

- **Enhanced:** README enhancements - new Kibana, Discord, LOGO!
- **Enhanced:** Watch script to support Symbolic Linking with HRM for local development
- **Fixed:** BorderTabSet 'not all code path returns something' fix.

## 1.1.0 - NPM Publish Error

## 1.0.0 - 2025-09-22
## 1.0.0

- **Added:** New global boolean variable `tabSetEnableHideWhenEmpty` to control hiding of empty tabsets.
- **Added:** New optional boolean attribute `enableHideWhenEmpty` on `ITabSetAttribute` that inherits from the global `tabSetEnableHideWhenEmpty` setting.
Expand All @@ -18,7 +22,7 @@
- **Feature:** Empty tabsets can now serve as placeholders for future tab insertion, appearing only when populated with content.
- **Use Case:** Enables programmatic opening of tabs in placeholder tabsets for complex layouts like PDF editors with side panels.

## 0.9.0 = 2025-09-22
## 0.9.0

- **Added:** Pin/unpin feature for side (left/right) and bottom panels. Users can now keep panels permanently visible ("pinned") or allow them to collapse automatically ("unpinned"), similar to behavior in modern IDEs and dashboard layouts.
- **Enhanced:** Improved layout flexibility by allowing users to control panel visibility behavior, helping to streamline workflows and reduce visual clutter in complex layouts.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flexycakes",
"version": "1.2.0",
"version": "1.2.1",
"description": "Community fork of caplin/FlexLayout (ISC). Not affiliated.",
"repository": {
"type": "git",
Expand Down
69 changes: 38 additions & 31 deletions src/model/RowNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,34 @@ export class RowNode extends Node implements IDropTarget {
this.attributes.weight = weight;
}

/** @internal */
isHiddenNode(node: TabSetNode | RowNode): boolean {
return node instanceof TabSetNode && node.getChildren().length === 0 && node.isEnableHideWhenEmpty();
}

/** @internal */
getSplitterBounds(index: number) {
const h = this.getOrientation() === Orientation.HORZ;
const c = this.getChildren();
const ss = this.model.getSplitterSize();
const fr = c[0].getRect();
const lr = c[c.length - 1].getRect();
let lr = c[c.length - 1].getRect();

// Special-case: Last node is hidden, in this case
// we take the most right node which is visible
for (let i = c.length - 1; i >= 0; i--) {
const n = c[i] as TabSetNode | RowNode;
if (!this.isHiddenNode(n)) {
lr = n.getRect();
break;
}
}
let p = h ? [fr.x, lr.getRight()] : [fr.y, lr.getBottom()];
const q = h ? [fr.x, lr.getRight()] : [fr.y, lr.getBottom()];

for (let i = 0; i < index; i++) {
const n = c[i] as TabSetNode | RowNode;
if (this.isHiddenNode(n)) continue;
p[0] += h ? n.getMinWidth() : n.getMinHeight();
q[0] += h ? n.getMaxWidth() : n.getMaxHeight();
if (i > 0) {
Expand All @@ -117,6 +133,7 @@ export class RowNode extends Node implements IDropTarget {

for (let i = c.length - 1; i >= index; i--) {
const n = c[i] as TabSetNode | RowNode;
if (this.isHiddenNode(n)) continue;
p[1] -= (h ? n.getMinWidth() : n.getMinHeight()) + ss;
q[1] -= (h ? n.getMaxWidth() : n.getMaxHeight()) + ss;
}
Expand All @@ -143,7 +160,7 @@ export class RowNode extends Node implements IDropTarget {
sum += s;
}

const startRect = c[index].getRect()
const startRect = c[index].getRect();
const startPosition = (h ? startRect.x : startRect.y) - ss;

return { initialSizes, sum, startPosition };
Expand All @@ -158,7 +175,8 @@ export class RowNode extends Node implements IDropTarget {

const sizes = [...initialSizes];

if (splitterPos < startPosition) { // moved left
if (splitterPos < startPosition) {
// moved left
let shift = startPosition - splitterPos;
let altShift = 0;
if (sizes[index] + shift > smax) {
Expand All @@ -180,7 +198,7 @@ export class RowNode extends Node implements IDropTarget {
}
}

for (let i = index+1; i < c.length; i++) {
for (let i = index + 1; i < c.length; i++) {
const n = c[i] as TabSetNode | RowNode;
const m = h ? n.getMaxWidth() : n.getMaxHeight();
if (sizes[i] + altShift < m) {
Expand All @@ -191,16 +209,14 @@ export class RowNode extends Node implements IDropTarget {
sizes[i] = m;
}
}


} else {
let shift = splitterPos - startPosition;
let altShift = 0;
if (sizes[index-1] + shift > smax) {
altShift = sizes[index-1] + shift - smax;
sizes[index-1] = smax;
if (sizes[index - 1] + shift > smax) {
altShift = sizes[index - 1] + shift - smax;
sizes[index - 1] = smax;
} else {
sizes[index-1] += shift;
sizes[index - 1] += shift;
}

for (let i = index; i < c.length; i++) {
Expand Down Expand Up @@ -229,7 +245,7 @@ export class RowNode extends Node implements IDropTarget {
}

// 0.1 is to prevent weight ever going to zero
const weights = sizes.map(s => Math.max(0.1, s) * 100 / sum);
const weights = sizes.map((s) => (Math.max(0.1, s) * 100) / sum);

// console.log(splitterPos, startPosition, "sizes", sizes);
// console.log("weights",weights);
Expand Down Expand Up @@ -364,7 +380,6 @@ export class RowNode extends Node implements IDropTarget {
this.model.setActiveTabset(child, this.windowId);
this.addChild(child);
}

}

/** @internal */
Expand Down Expand Up @@ -438,8 +453,7 @@ export class RowNode extends Node implements IDropTarget {
if (dragNode instanceof TabSetNode || dragNode instanceof RowNode) {
node = dragNode;
// need to turn round if same orientation unless docking oposite direction
if (node instanceof RowNode && node.getOrientation() === this.getOrientation() &&
(location.getOrientation() === this.getOrientation() || location === DockLocation.CENTER)) {
if (node instanceof RowNode && node.getOrientation() === this.getOrientation() && (location.getOrientation() === this.getOrientation() || location === DockLocation.CENTER)) {
node = new RowNode(this.model, this.windowId, {});
node.addChild(dragNode);
}
Expand All @@ -465,11 +479,11 @@ export class RowNode extends Node implements IDropTarget {
} else {
this.addChild(node, index);
}
} else if (horz && dockLocation === DockLocation.LEFT || !horz && dockLocation === DockLocation.TOP) {
} else if ((horz && dockLocation === DockLocation.LEFT) || (!horz && dockLocation === DockLocation.TOP)) {
this.addChild(node, 0);
} else if (horz && dockLocation === DockLocation.RIGHT || !horz && dockLocation === DockLocation.BOTTOM) {
} else if ((horz && dockLocation === DockLocation.RIGHT) || (!horz && dockLocation === DockLocation.BOTTOM)) {
this.addChild(node);
} else if (horz && dockLocation === DockLocation.TOP || !horz && dockLocation === DockLocation.LEFT) {
} else if ((horz && dockLocation === DockLocation.TOP) || (!horz && dockLocation === DockLocation.LEFT)) {
const vrow = new RowNode(this.model, this.windowId, {});
const hrow = new RowNode(this.model, this.windowId, {});
hrow.setWeight(75);
Expand All @@ -481,7 +495,7 @@ export class RowNode extends Node implements IDropTarget {
vrow.addChild(node);
vrow.addChild(hrow);
this.addChild(vrow);
} else if (horz && dockLocation === DockLocation.BOTTOM || !horz && dockLocation === DockLocation.RIGHT) {
} else if ((horz && dockLocation === DockLocation.BOTTOM) || (!horz && dockLocation === DockLocation.RIGHT)) {
const vrow = new RowNode(this.model, this.windowId, {});
const hrow = new RowNode(this.model, this.windowId, {});
hrow.setWeight(75);
Expand All @@ -502,8 +516,6 @@ export class RowNode extends Node implements IDropTarget {
this.model.tidy();
}



/** @internal */
isEnableDrop() {
return true;
Expand All @@ -524,12 +536,11 @@ export class RowNode extends Node implements IDropTarget {
return RowNode.attributeDefinitions;
}


// NOTE: flex-grow cannot have values < 1 otherwise will not fill parent, need to normalize
// NOTE: flex-grow cannot have values < 1 otherwise will not fill parent, need to normalize
normalizeWeights() {
let sum = 0;
for (const n of this.children) {
const node = (n as TabSetNode | RowNode);
const node = n as TabSetNode | RowNode;
sum += node.getWeight();
}

Expand All @@ -538,21 +549,17 @@ export class RowNode extends Node implements IDropTarget {
}

for (const n of this.children) {
const node = (n as TabSetNode | RowNode);
node.setWeight(Math.max(0.001, 100 * node.getWeight() / sum));
const node = n as TabSetNode | RowNode;
node.setWeight(Math.max(0.001, (100 * node.getWeight()) / sum));
}
}

/** @internal */
private static createAttributeDefinitions(): AttributeDefinitions {
const attributeDefinitions = new AttributeDefinitions();
attributeDefinitions.add("type", RowNode.TYPE, true).setType(Attribute.STRING).setFixed();
attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription(
`the unique id of the row, if left undefined a uuid will be assigned`
);
attributeDefinitions.add("weight", 100).setType(Attribute.NUMBER).setDescription(
`relative weight for sizing of this row in parent row`
);
attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription(`the unique id of the row, if left undefined a uuid will be assigned`);
attributeDefinitions.add("weight", 100).setType(Attribute.NUMBER).setDescription(`relative weight for sizing of this row in parent row`);

return attributeDefinitions;
}
Expand Down
50 changes: 37 additions & 13 deletions src/view/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,46 @@ export const Row = (props: IRowProps) => {

const items: React.ReactNode[] = [];

let i = 0;
const isHiddenNode = (node: any): boolean =>
(
node instanceof TabSetNode &&
node.getChildren().length === 0 &&
node.isEnableHideWhenEmpty()
) ||
(
node instanceof RowNode &&
node.getChildren().every((cr) => isHiddenNode(cr))
);

for (const child of node.getChildren()) {
if (i > 0) {
const children = node.getChildren();

for (let i = 0; i < children.length; i++) {

const c = children[i];
const lc = i > 0 ? children[i - 1] : undefined;
const hidden = isHiddenNode(c);
const lcHidden = lc ? isHiddenNode(lc) : undefined;

if (lc && !lcHidden && !hidden) {
items.push(<Splitter key={"splitter" + i} layout={layout} node={node} index={i} horizontal={horizontal} />)
}
if (child instanceof RowNode) {
items.push(<Row key={child.getId()} layout={layout} node={child} />);
} else if (child instanceof TabSetNode) {
if (child.getChildren().length == 0 && child.isEnableHideWhenEmpty())
continue;
items.push(<TabSet key={child.getId()} layout={layout} node={child} />);
if (c instanceof RowNode) {
if (hidden) {
items.push(<div style={{ "display": "none" }}>
<Row key={c.getId()} layout={layout} node={c} />
</div>);
} else {
items.push(<Row key={c.getId()} layout={layout} node={c} />);
}
} else if (c instanceof TabSetNode) {
if (!hidden) {
items.push(<TabSet key={c.getId()} layout={layout} node={c} />);
} else {
items.push(<div style={{ "display": "none" }}>
<TabSet key={c.getId()} layout={layout} node={c} />
</div>);
}
}
i++;
}

const style: Record<string, any> = {
Expand All @@ -65,6 +91,4 @@ export const Row = (props: IRowProps) => {
{items}
</div>
);
};


};