@aptre/flex-layout 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/Layout.e2e.test.d.ts +1 -0
- package/dist/Layout.test.d.ts +1 -0
- package/dist/OptimizedLayout.e2e.test.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +199 -52
- package/dist/model/IJsonModel.d.ts +4 -4
- package/dist/test/setup.d.ts +8 -0
- package/dist/test/unit-setup.d.ts +1 -0
- package/dist/view/Layout.d.ts +2 -0
- package/dist/view/OptimizedLayout.d.ts +29 -0
- package/package.json +119 -102
- package/tsconfig.json +18 -18
- package/typedoc/assets/hierarchy.js +1 -1
- package/typedoc/assets/main.js +5 -5
- package/typedoc/assets/navigation.js +1 -1
- package/typedoc/assets/search.js +1 -1
- package/typedoc/assets/style.css +251 -228
- package/typedoc/classes/Action.html +4 -4
- package/typedoc/classes/Actions.html +74 -74
- package/typedoc/classes/BorderNode.html +25 -25
- package/typedoc/classes/BorderSet.html +2 -2
- package/typedoc/classes/DockLocation.html +10 -10
- package/typedoc/classes/DropInfo.html +7 -7
- package/typedoc/classes/Layout.html +96 -96
- package/typedoc/classes/LayoutWindow.html +12 -12
- package/typedoc/classes/Model.html +42 -42
- package/typedoc/classes/Node.html +12 -12
- package/typedoc/classes/Orientation.html +6 -6
- package/typedoc/classes/Rect.html +26 -26
- package/typedoc/classes/RowNode.html +16 -16
- package/typedoc/classes/TabNode.html +39 -39
- package/typedoc/classes/TabSetNode.html +39 -39
- package/typedoc/enums/CLASSES.html +94 -94
- package/typedoc/enums/I18nLabel.html +11 -11
- package/typedoc/enums/ICloseType.html +4 -4
- package/typedoc/functions/OptimizedLayout.html +18 -0
- package/typedoc/functions/findJsonNodeById.html +4 -4
- package/typedoc/functions/walkJsonModel.html +3 -3
- package/typedoc/hierarchy.html +1 -1
- package/typedoc/index.html +1 -1
- package/typedoc/interfaces/IBorderAttributes.html +25 -25
- package/typedoc/interfaces/IDraggable.html +1 -1
- package/typedoc/interfaces/IDropTarget.html +1 -1
- package/typedoc/interfaces/IGlobalAttributes.html +99 -99
- package/typedoc/interfaces/IIcons.html +9 -9
- package/typedoc/interfaces/IJsonBorderNode.html +27 -27
- package/typedoc/interfaces/IJsonModel.html +5 -5
- package/typedoc/interfaces/IJsonPopout.html +3 -3
- package/typedoc/interfaces/IJsonRect.html +5 -5
- package/typedoc/interfaces/IJsonRowNode.html +8 -8
- package/typedoc/interfaces/IJsonTabNode.html +53 -53
- package/typedoc/interfaces/IJsonTabSetNode.html +52 -52
- package/typedoc/interfaces/ILayoutProps.html +41 -39
- package/typedoc/interfaces/IOptimizedLayoutProps.html +42 -0
- package/typedoc/interfaces/IRowAttributes.html +7 -7
- package/typedoc/interfaces/ITabAttributes.html +53 -53
- package/typedoc/interfaces/ITabRenderValues.html +7 -7
- package/typedoc/interfaces/ITabSetAttributes.html +47 -47
- package/typedoc/interfaces/ITabSetRenderValues.html +7 -7
- package/typedoc/interfaces/VisitorResult.html +5 -5
- package/typedoc/types/DragRectRenderCallback.html +1 -1
- package/typedoc/types/IBorderLocation.html +1 -1
- package/typedoc/types/ITabLocation.html +1 -1
- package/typedoc/types/JsonNode.html +2 -2
- package/typedoc/types/ModelVisitor.html +5 -5
- package/typedoc/types/NodeMouseEvent.html +1 -1
- package/typedoc/types/ShowOverflowMenuCallback.html +1 -1
- package/typedoc/types/TabSetPlaceHolderCallback.html +1 -1
- package/typedoc/variables/FlexLayoutVersion.html +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,19 @@ Features:
|
|
|
38
38
|
* component state is preserved when tabs are moved
|
|
39
39
|
* typescript type declarations
|
|
40
40
|
|
|
41
|
+
## Demo
|
|
42
|
+
|
|
43
|
+
To demo and test this library, clone this repo, then:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
npm i -g yarn
|
|
47
|
+
yarn
|
|
48
|
+
yarn test:browser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Your browser will open to show + all the tests with vitest Browser Mode.
|
|
52
|
+
|
|
53
|
+
|
|
41
54
|
## Installation
|
|
42
55
|
|
|
43
56
|
FlexLayout is in the npm repository. install using:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
package/dist/index.d.ts
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1316,6 +1316,14 @@ var TabNode = class _TabNode extends Node {
|
|
|
1316
1316
|
}
|
|
1317
1317
|
};
|
|
1318
1318
|
|
|
1319
|
+
// src/model/ICloseType.ts
|
|
1320
|
+
var ICloseType = /* @__PURE__ */ ((ICloseType2) => {
|
|
1321
|
+
ICloseType2[ICloseType2["Visible"] = 1] = "Visible";
|
|
1322
|
+
ICloseType2[ICloseType2["Always"] = 2] = "Always";
|
|
1323
|
+
ICloseType2[ICloseType2["Selected"] = 3] = "Selected";
|
|
1324
|
+
return ICloseType2;
|
|
1325
|
+
})(ICloseType || {});
|
|
1326
|
+
|
|
1319
1327
|
// src/view/Utils.tsx
|
|
1320
1328
|
function isDesktop() {
|
|
1321
1329
|
const desktop = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
|
@@ -1422,6 +1430,18 @@ function isSafari() {
|
|
|
1422
1430
|
const userAgent = navigator.userAgent;
|
|
1423
1431
|
return userAgent.includes("Safari") && !userAgent.includes("Chrome") && !userAgent.includes("Chromium");
|
|
1424
1432
|
}
|
|
1433
|
+
function isTabClosable(node, selected) {
|
|
1434
|
+
const closeType = node.getCloseType();
|
|
1435
|
+
if (selected || closeType === 2 /* Always */) {
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
if (closeType === 1 /* Visible */) {
|
|
1439
|
+
if (window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1425
1445
|
|
|
1426
1446
|
// src/model/Utils.ts
|
|
1427
1447
|
function adjustSelectedIndex(parent, removedIndex) {
|
|
@@ -3537,6 +3557,7 @@ var Splitter = (props) => {
|
|
|
3537
3557
|
function BorderTab(props) {
|
|
3538
3558
|
const { layout, border, show } = props;
|
|
3539
3559
|
const selfRef = React3.useRef(null);
|
|
3560
|
+
const isFirstRender = React3.useRef(true);
|
|
3540
3561
|
React3.useLayoutEffect(() => {
|
|
3541
3562
|
const outerRect = layout.getBoundingClientRect(selfRef.current);
|
|
3542
3563
|
const contentRect = Rect.getContentRect(selfRef.current).relativeTo(layout.getDomRect());
|
|
@@ -3544,9 +3565,12 @@ function BorderTab(props) {
|
|
|
3544
3565
|
border.setOuterRect(outerRect);
|
|
3545
3566
|
if (!border.getContentRect().equals(contentRect)) {
|
|
3546
3567
|
border.setContentRect(contentRect);
|
|
3547
|
-
|
|
3568
|
+
if (!isFirstRender.current) {
|
|
3569
|
+
layout.redrawInternal("border content rect");
|
|
3570
|
+
}
|
|
3548
3571
|
}
|
|
3549
3572
|
}
|
|
3573
|
+
isFirstRender.current = false;
|
|
3550
3574
|
});
|
|
3551
3575
|
let horizontal = true;
|
|
3552
3576
|
const style2 = {};
|
|
@@ -3574,16 +3598,6 @@ import * as React8 from "react";
|
|
|
3574
3598
|
|
|
3575
3599
|
// src/view/BorderButton.tsx
|
|
3576
3600
|
import * as React4 from "react";
|
|
3577
|
-
|
|
3578
|
-
// src/model/ICloseType.ts
|
|
3579
|
-
var ICloseType = /* @__PURE__ */ ((ICloseType2) => {
|
|
3580
|
-
ICloseType2[ICloseType2["Visible"] = 1] = "Visible";
|
|
3581
|
-
ICloseType2[ICloseType2["Always"] = 2] = "Always";
|
|
3582
|
-
ICloseType2[ICloseType2["Selected"] = 3] = "Selected";
|
|
3583
|
-
return ICloseType2;
|
|
3584
|
-
})(ICloseType || {});
|
|
3585
|
-
|
|
3586
|
-
// src/view/BorderButton.tsx
|
|
3587
3601
|
var BorderButton = (props) => {
|
|
3588
3602
|
const { layout, node, selected, border, icons, path } = props;
|
|
3589
3603
|
const selfRef = React4.useRef(null);
|
|
@@ -3617,20 +3631,8 @@ var BorderButton = (props) => {
|
|
|
3617
3631
|
layout.setEditingTab(void 0);
|
|
3618
3632
|
}
|
|
3619
3633
|
};
|
|
3620
|
-
const isClosable = () => {
|
|
3621
|
-
const closeType = node.getCloseType();
|
|
3622
|
-
if (selected || closeType === 2 /* Always */) {
|
|
3623
|
-
return true;
|
|
3624
|
-
}
|
|
3625
|
-
if (closeType === 1 /* Visible */) {
|
|
3626
|
-
if (window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
|
|
3627
|
-
return true;
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
|
-
return false;
|
|
3631
|
-
};
|
|
3632
3634
|
const onClose = (event) => {
|
|
3633
|
-
if (
|
|
3635
|
+
if (isTabClosable(node, selected)) {
|
|
3634
3636
|
layout.doAction(Actions.deleteTab(node.getId()));
|
|
3635
3637
|
} else {
|
|
3636
3638
|
onClick();
|
|
@@ -3834,12 +3836,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3834
3836
|
React7.useLayoutEffect(() => {
|
|
3835
3837
|
userControlledLeft.current = false;
|
|
3836
3838
|
}, [node.getSelectedNode(), node.getRect().width, node.getRect().height]);
|
|
3839
|
+
const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
|
|
3837
3840
|
React7.useLayoutEffect(() => {
|
|
3838
|
-
const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
|
|
3839
3841
|
if (nodeRect.width > 0 && nodeRect.height > 0) {
|
|
3840
3842
|
updateVisibleTabs();
|
|
3841
3843
|
}
|
|
3842
|
-
});
|
|
3844
|
+
}, [nodeRect.width, nodeRect.height]);
|
|
3843
3845
|
const instance = toolbarRef.current;
|
|
3844
3846
|
React7.useEffect(() => {
|
|
3845
3847
|
if (!instance) {
|
|
@@ -3879,15 +3881,15 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3879
3881
|
if (firstRender.current === true) {
|
|
3880
3882
|
tabsTruncated.current = false;
|
|
3881
3883
|
}
|
|
3882
|
-
const
|
|
3884
|
+
const nodeRect2 = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
|
|
3883
3885
|
const lastChild = node.getChildren()[node.getChildren().length - 1];
|
|
3884
3886
|
const stickyButtonsSize = stickyButtonsRef.current === null ? 0 : getSize(stickyButtonsRef.current.getBoundingClientRect());
|
|
3885
|
-
if (firstRender.current === true || lastHiddenCount.current === 0 && hiddenTabs.length !== 0 ||
|
|
3886
|
-
|
|
3887
|
+
if (firstRender.current === true || lastHiddenCount.current === 0 && hiddenTabs.length !== 0 || nodeRect2.width !== lastRect.current.width || // incase rect changed between first render and second
|
|
3888
|
+
nodeRect2.height !== lastRect.current.height) {
|
|
3887
3889
|
lastHiddenCount.current = hiddenTabs.length;
|
|
3888
|
-
lastRect.current =
|
|
3890
|
+
lastRect.current = nodeRect2;
|
|
3889
3891
|
const enabled = node instanceof TabSetNode ? node.isEnableTabStrip() === true : true;
|
|
3890
|
-
let endPos = getFar(
|
|
3892
|
+
let endPos = getFar(nodeRect2) - stickyButtonsSize;
|
|
3891
3893
|
if (toolbarRef.current !== null) {
|
|
3892
3894
|
endPos -= getSize(toolbarRef.current.getBoundingClientRect());
|
|
3893
3895
|
}
|
|
@@ -3901,12 +3903,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3901
3903
|
const selectedRect = selectedTab.getTabRect();
|
|
3902
3904
|
const selectedStart = getNear(selectedRect) - tabMargin;
|
|
3903
3905
|
const selectedEnd = getFar(selectedRect) + tabMargin;
|
|
3904
|
-
if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(
|
|
3905
|
-
shiftPos = getNear(
|
|
3906
|
+
if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(nodeRect2)) {
|
|
3907
|
+
shiftPos = getNear(nodeRect2) - selectedStart;
|
|
3906
3908
|
} else {
|
|
3907
|
-
if (selectedEnd > endPos || selectedStart < getNear(
|
|
3908
|
-
if (selectedStart < getNear(
|
|
3909
|
-
shiftPos = getNear(
|
|
3909
|
+
if (selectedEnd > endPos || selectedStart < getNear(nodeRect2)) {
|
|
3910
|
+
if (selectedStart < getNear(nodeRect2)) {
|
|
3911
|
+
shiftPos = getNear(nodeRect2) - selectedStart;
|
|
3910
3912
|
}
|
|
3911
3913
|
if (selectedEnd + shiftPos > endPos) {
|
|
3912
3914
|
shiftPos = endPos - selectedEnd;
|
|
@@ -3920,7 +3922,7 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3920
3922
|
const hidden = [];
|
|
3921
3923
|
for (let i = 0; i < node.getChildren().length; i++) {
|
|
3922
3924
|
const child = node.getChildren()[i];
|
|
3923
|
-
if (getNear(child.getTabRect()) + diff < getNear(
|
|
3925
|
+
if (getNear(child.getTabRect()) + diff < getNear(nodeRect2) || getFar(child.getTabRect()) + diff > endPos) {
|
|
3924
3926
|
hidden.push({ node: child, index: i });
|
|
3925
3927
|
}
|
|
3926
3928
|
}
|
|
@@ -4371,21 +4373,9 @@ var TabButton = (props) => {
|
|
|
4371
4373
|
layout.setEditingTab(void 0);
|
|
4372
4374
|
}
|
|
4373
4375
|
};
|
|
4374
|
-
const isClosable = () => {
|
|
4375
|
-
const closeType = node.getCloseType();
|
|
4376
|
-
if (selected || closeType === 2 /* Always */) {
|
|
4377
|
-
return true;
|
|
4378
|
-
}
|
|
4379
|
-
if (closeType === 1 /* Visible */) {
|
|
4380
|
-
if (window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
|
|
4381
|
-
return true;
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
|
-
return false;
|
|
4385
|
-
};
|
|
4386
4376
|
const onClose = (event) => {
|
|
4387
4377
|
event.preventDefault();
|
|
4388
|
-
if (
|
|
4378
|
+
if (isTabClosable(node, selected)) {
|
|
4389
4379
|
layout.doAction(Actions.deleteTab(node.getId()));
|
|
4390
4380
|
} else {
|
|
4391
4381
|
onClick();
|
|
@@ -4502,6 +4492,7 @@ var TabSet = (props) => {
|
|
|
4502
4492
|
const overflowbuttonRef = React15.useRef(null);
|
|
4503
4493
|
const stickyButtonsRef = React15.useRef(null);
|
|
4504
4494
|
const icons = layout.getIcons();
|
|
4495
|
+
const isFirstRender = React15.useRef(true);
|
|
4505
4496
|
React15.useEffect(() => {
|
|
4506
4497
|
node.setRect(layout.getBoundingClientRect(selfRef.current));
|
|
4507
4498
|
if (tabStripRef.current) {
|
|
@@ -4510,8 +4501,11 @@ var TabSet = (props) => {
|
|
|
4510
4501
|
const newContentRect = Rect.getContentRect(contentRef.current).relativeTo(layout.getDomRect());
|
|
4511
4502
|
if (!node.getContentRect().equals(newContentRect)) {
|
|
4512
4503
|
node.setContentRect(newContentRect);
|
|
4513
|
-
|
|
4504
|
+
if (!isFirstRender.current) {
|
|
4505
|
+
layout.redrawInternal("tabset content rect " + newContentRect);
|
|
4506
|
+
}
|
|
4514
4507
|
}
|
|
4508
|
+
isFirstRender.current = false;
|
|
4515
4509
|
});
|
|
4516
4510
|
const { selfRef, position, userControlledLeft, hiddenTabs, onMouseWheel, tabsTruncated } = useTabOverflow(node, Orientation.HORZ, buttonBarRef, stickyButtonsRef);
|
|
4517
4511
|
const onOverflowClick = (event) => {
|
|
@@ -4924,7 +4918,7 @@ var Tab = (props) => {
|
|
|
4924
4918
|
firstSelect.current = false;
|
|
4925
4919
|
}
|
|
4926
4920
|
}
|
|
4927
|
-
});
|
|
4921
|
+
}, [selected]);
|
|
4928
4922
|
const onPointerDown = () => {
|
|
4929
4923
|
const parent = node.getParent();
|
|
4930
4924
|
if (parent instanceof TabSetNode) {
|
|
@@ -5764,6 +5758,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
|
|
|
5764
5758
|
this.showOverlay(false);
|
|
5765
5759
|
this.dragEnterCount = 0;
|
|
5766
5760
|
this.dragging = false;
|
|
5761
|
+
this.props.onDragStateChange?.(false);
|
|
5767
5762
|
if (this.outlineDiv) {
|
|
5768
5763
|
this.selfRef.current.removeChild(this.outlineDiv);
|
|
5769
5764
|
this.outlineDiv = void 0;
|
|
@@ -5795,6 +5790,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
|
|
|
5795
5790
|
rootdiv.appendChild(this.outlineDiv);
|
|
5796
5791
|
this.dragging = true;
|
|
5797
5792
|
this.showOverlay(true);
|
|
5793
|
+
this.props.onDragStateChange?.(true);
|
|
5798
5794
|
if (!this.isDraggingOverWindow && this.props.model.getMaximizedTabset(this.windowId) === void 0) {
|
|
5799
5795
|
this.setState({ showEdges: this.props.model.isEnableEdgeDock() });
|
|
5800
5796
|
}
|
|
@@ -5880,6 +5876,156 @@ var DragState = class {
|
|
|
5880
5876
|
}
|
|
5881
5877
|
};
|
|
5882
5878
|
|
|
5879
|
+
// src/view/OptimizedLayout.tsx
|
|
5880
|
+
import * as React20 from "react";
|
|
5881
|
+
import { useCallback, useEffect as useEffect6, useMemo, useRef as useRef12, useState as useState4 } from "react";
|
|
5882
|
+
function TabRef({ node, onRectChange, onVisibilityChange }) {
|
|
5883
|
+
useEffect6(() => {
|
|
5884
|
+
const handleResize = (params) => {
|
|
5885
|
+
onRectChange(node.getId(), params.rect);
|
|
5886
|
+
};
|
|
5887
|
+
const handleVisibility = (params) => {
|
|
5888
|
+
onVisibilityChange(node.getId(), params.visible);
|
|
5889
|
+
};
|
|
5890
|
+
node.setEventListener("resize", handleResize);
|
|
5891
|
+
node.setEventListener("visibility", handleVisibility);
|
|
5892
|
+
const parent = node.getParent();
|
|
5893
|
+
if (parent) {
|
|
5894
|
+
const contentRect = parent.getContentRect();
|
|
5895
|
+
if (contentRect && contentRect.width > 0 && contentRect.height > 0) {
|
|
5896
|
+
onRectChange(node.getId(), contentRect);
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
onVisibilityChange(node.getId(), node.isSelected());
|
|
5900
|
+
return () => {
|
|
5901
|
+
node.removeEventListener("resize");
|
|
5902
|
+
node.removeEventListener("visibility");
|
|
5903
|
+
};
|
|
5904
|
+
}, [node, onRectChange, onVisibilityChange]);
|
|
5905
|
+
return null;
|
|
5906
|
+
}
|
|
5907
|
+
function TabContainer({
|
|
5908
|
+
tabs,
|
|
5909
|
+
renderTab,
|
|
5910
|
+
isDragging,
|
|
5911
|
+
classNameMapper
|
|
5912
|
+
}) {
|
|
5913
|
+
const getClassName = useCallback(
|
|
5914
|
+
(defaultClassName) => {
|
|
5915
|
+
return classNameMapper ? classNameMapper(defaultClassName) : defaultClassName;
|
|
5916
|
+
},
|
|
5917
|
+
[classNameMapper]
|
|
5918
|
+
);
|
|
5919
|
+
return /* @__PURE__ */ React20.createElement(
|
|
5920
|
+
"div",
|
|
5921
|
+
{
|
|
5922
|
+
style: {
|
|
5923
|
+
position: "absolute",
|
|
5924
|
+
inset: 0,
|
|
5925
|
+
// CRITICAL: Disable pointer events during drag to prevent drag overlay from disappearing
|
|
5926
|
+
// When tabs are rendered outside FlexLayout's DOM, dragging over them triggers dragleave
|
|
5927
|
+
// on the Layout element, causing dragEnterCount to drop to 0 and clearing the drag UI.
|
|
5928
|
+
pointerEvents: isDragging ? "none" : "auto",
|
|
5929
|
+
// Ensure tab container doesn't block FlexLayout's tab bar interactions when not dragging
|
|
5930
|
+
zIndex: 0
|
|
5931
|
+
},
|
|
5932
|
+
"data-layout-path": "/tab-container"
|
|
5933
|
+
},
|
|
5934
|
+
Array.from(tabs.entries()).map(([nodeId, tabInfo]) => {
|
|
5935
|
+
const { node, rect, visible } = tabInfo;
|
|
5936
|
+
const contentClassName = node.getContentClassName();
|
|
5937
|
+
return /* @__PURE__ */ React20.createElement(
|
|
5938
|
+
"div",
|
|
5939
|
+
{
|
|
5940
|
+
key: nodeId,
|
|
5941
|
+
role: "tabpanel",
|
|
5942
|
+
"data-tab-id": nodeId,
|
|
5943
|
+
className: getClassName("flexlayout__tab") + (contentClassName ? " " + contentClassName : ""),
|
|
5944
|
+
style: {
|
|
5945
|
+
position: "absolute",
|
|
5946
|
+
display: visible ? "flex" : "none",
|
|
5947
|
+
left: rect.x,
|
|
5948
|
+
top: rect.y,
|
|
5949
|
+
width: rect.width,
|
|
5950
|
+
height: rect.height,
|
|
5951
|
+
overflow: "auto"
|
|
5952
|
+
}
|
|
5953
|
+
},
|
|
5954
|
+
renderTab(node)
|
|
5955
|
+
);
|
|
5956
|
+
})
|
|
5957
|
+
);
|
|
5958
|
+
}
|
|
5959
|
+
function OptimizedLayout({ model, renderTab, classNameMapper, onDragStateChange, ...layoutProps }) {
|
|
5960
|
+
const [isDragging, setIsDragging] = useState4(false);
|
|
5961
|
+
const [tabs, setTabs] = useState4(() => /* @__PURE__ */ new Map());
|
|
5962
|
+
const tabNodesRef = useRef12(/* @__PURE__ */ new Map());
|
|
5963
|
+
useEffect6(() => {
|
|
5964
|
+
const newTabNodes = /* @__PURE__ */ new Map();
|
|
5965
|
+
model.visitNodes((node) => {
|
|
5966
|
+
if (node instanceof TabNode) {
|
|
5967
|
+
newTabNodes.set(node.getId(), node);
|
|
5968
|
+
}
|
|
5969
|
+
});
|
|
5970
|
+
tabNodesRef.current = newTabNodes;
|
|
5971
|
+
setTabs((prevTabs) => {
|
|
5972
|
+
const nextTabs = /* @__PURE__ */ new Map();
|
|
5973
|
+
for (const [nodeId, node] of newTabNodes) {
|
|
5974
|
+
const existing = prevTabs.get(nodeId);
|
|
5975
|
+
if (existing) {
|
|
5976
|
+
nextTabs.set(nodeId, { ...existing, node });
|
|
5977
|
+
} else {
|
|
5978
|
+
const parent = node.getParent();
|
|
5979
|
+
const contentRect = parent?.getContentRect() ?? Rect.empty();
|
|
5980
|
+
nextTabs.set(nodeId, {
|
|
5981
|
+
node,
|
|
5982
|
+
rect: contentRect,
|
|
5983
|
+
visible: node.isSelected()
|
|
5984
|
+
});
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
return nextTabs;
|
|
5988
|
+
});
|
|
5989
|
+
}, [model]);
|
|
5990
|
+
const handleRectChange = useCallback((nodeId, rect) => {
|
|
5991
|
+
setTabs((prevTabs) => {
|
|
5992
|
+
const existing = prevTabs.get(nodeId);
|
|
5993
|
+
if (!existing || existing.rect.equals(rect)) {
|
|
5994
|
+
return prevTabs;
|
|
5995
|
+
}
|
|
5996
|
+
const nextTabs = new Map(prevTabs);
|
|
5997
|
+
nextTabs.set(nodeId, { ...existing, rect });
|
|
5998
|
+
return nextTabs;
|
|
5999
|
+
});
|
|
6000
|
+
}, []);
|
|
6001
|
+
const handleVisibilityChange = useCallback((nodeId, visible) => {
|
|
6002
|
+
setTabs((prevTabs) => {
|
|
6003
|
+
const existing = prevTabs.get(nodeId);
|
|
6004
|
+
if (!existing || existing.visible === visible) {
|
|
6005
|
+
return prevTabs;
|
|
6006
|
+
}
|
|
6007
|
+
const nextTabs = new Map(prevTabs);
|
|
6008
|
+
nextTabs.set(nodeId, { ...existing, visible });
|
|
6009
|
+
return nextTabs;
|
|
6010
|
+
});
|
|
6011
|
+
}, []);
|
|
6012
|
+
const handleDragStateChange = useCallback(
|
|
6013
|
+
(dragging) => {
|
|
6014
|
+
setIsDragging(dragging);
|
|
6015
|
+
onDragStateChange?.(dragging);
|
|
6016
|
+
},
|
|
6017
|
+
[onDragStateChange]
|
|
6018
|
+
);
|
|
6019
|
+
const factory = useCallback(
|
|
6020
|
+
(node) => {
|
|
6021
|
+
return /* @__PURE__ */ React20.createElement(TabRef, { key: node.getId(), node, onRectChange: handleRectChange, onVisibilityChange: handleVisibilityChange });
|
|
6022
|
+
},
|
|
6023
|
+
[handleRectChange, handleVisibilityChange]
|
|
6024
|
+
);
|
|
6025
|
+
const tabsForContainer = useMemo(() => tabs, [tabs]);
|
|
6026
|
+
return /* @__PURE__ */ React20.createElement("div", { style: { position: "relative", width: "100%", height: "100%" } }, /* @__PURE__ */ React20.createElement(Layout, { model, factory, classNameMapper, onDragStateChange: handleDragStateChange, ...layoutProps }), /* @__PURE__ */ React20.createElement(TabContainer, { tabs: tabsForContainer, renderTab, isDragging, classNameMapper }));
|
|
6027
|
+
}
|
|
6028
|
+
|
|
5883
6029
|
// src/model/walk.ts
|
|
5884
6030
|
function findJsonNodeById(model, id) {
|
|
5885
6031
|
let result;
|
|
@@ -5939,6 +6085,7 @@ export {
|
|
|
5939
6085
|
LayoutWindow,
|
|
5940
6086
|
Model,
|
|
5941
6087
|
Node,
|
|
6088
|
+
OptimizedLayout,
|
|
5942
6089
|
Orientation,
|
|
5943
6090
|
Rect,
|
|
5944
6091
|
RowNode,
|
|
@@ -424,7 +424,7 @@ export interface IRowAttributes {
|
|
|
424
424
|
*/
|
|
425
425
|
id?: string;
|
|
426
426
|
/** The type of this node (always "row") */
|
|
427
|
-
type
|
|
427
|
+
type: "row";
|
|
428
428
|
/**
|
|
429
429
|
relative weight for sizing of this row in parent row
|
|
430
430
|
|
|
@@ -560,7 +560,7 @@ export interface ITabSetAttributes {
|
|
|
560
560
|
*/
|
|
561
561
|
tabLocation?: ITabLocation;
|
|
562
562
|
/** The type of this node (always "tabset") */
|
|
563
|
-
type
|
|
563
|
+
type: "tabset";
|
|
564
564
|
/**
|
|
565
565
|
relative weight for sizing of this tabset in parent row
|
|
566
566
|
|
|
@@ -721,7 +721,7 @@ export interface ITabAttributes {
|
|
|
721
721
|
*/
|
|
722
722
|
tabsetClassName?: string;
|
|
723
723
|
/** The type of this node (always "tab") */
|
|
724
|
-
type
|
|
724
|
+
type: "tab";
|
|
725
725
|
}
|
|
726
726
|
export interface IBorderAttributes {
|
|
727
727
|
/**
|
|
@@ -791,5 +791,5 @@ export interface IBorderAttributes {
|
|
|
791
791
|
*/
|
|
792
792
|
size?: number;
|
|
793
793
|
/** The type of this node (always "border") */
|
|
794
|
-
type
|
|
794
|
+
type: "border";
|
|
795
795
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
package/dist/view/Layout.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface ILayoutProps {
|
|
|
49
49
|
onTabSetPlaceHolder?: TabSetPlaceHolderCallback;
|
|
50
50
|
/** Name given to popout windows, defaults to 'Popout Window' */
|
|
51
51
|
popoutWindowName?: string;
|
|
52
|
+
/** callback for when drag state changes, useful for OptimizedLayout to set pointer-events: none on external tab container during drag */
|
|
53
|
+
onDragStateChange?: (isDragging: boolean) => void;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
56
|
* A React component that hosts a multi-tabbed layout
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { TabNode } from "../model/TabNode";
|
|
3
|
+
import { ILayoutProps } from "./Layout";
|
|
4
|
+
/**
|
|
5
|
+
* Props for OptimizedLayout - similar to Layout but with `renderTab` instead of `factory`
|
|
6
|
+
*/
|
|
7
|
+
export interface IOptimizedLayoutProps extends Omit<ILayoutProps, "factory"> {
|
|
8
|
+
/** Function to render tab content - receives TabNode, returns React element */
|
|
9
|
+
renderTab: (node: TabNode) => React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* OptimizedLayout - A wrapper around FlexLayout that renders tab content outside of
|
|
13
|
+
* FlexLayout's DOM structure for better performance.
|
|
14
|
+
*
|
|
15
|
+
* Key benefits:
|
|
16
|
+
* 1. Tab components are NOT re-rendered when Model changes
|
|
17
|
+
* 2. Tab state (scroll position, form inputs, etc.) is preserved across layout mutations
|
|
18
|
+
* 3. Only CSS properties change when layout changes - no React re-renders
|
|
19
|
+
*
|
|
20
|
+
* The component works by:
|
|
21
|
+
* 1. Rendering FlexLayout with TabRef placeholders instead of actual tab content
|
|
22
|
+
* 2. TabRef components listen to resize/visibility events from TabNodes
|
|
23
|
+
* 3. A sibling TabContainer renders the actual tab content with absolute positioning
|
|
24
|
+
* 4. During drag operations, TabContainer uses pointer-events: none to prevent
|
|
25
|
+
* interfering with FlexLayout's drag overlay
|
|
26
|
+
*
|
|
27
|
+
* @see https://github.com/caplin/FlexLayout/issues/456
|
|
28
|
+
*/
|
|
29
|
+
export declare function OptimizedLayout({ model, renderTab, classNameMapper, onDragStateChange, ...layoutProps }: IOptimizedLayoutProps): React.JSX.Element;
|