@aptre/flex-layout 0.3.5 → 0.4.1
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 +220 -52
- 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) {
|
|
@@ -1651,7 +1671,15 @@ var TabSetNode = class _TabSetNode extends Node {
|
|
|
1651
1671
|
}
|
|
1652
1672
|
/** @internal */
|
|
1653
1673
|
setContentRect(rect) {
|
|
1674
|
+
const changed = !this.contentRect.equals(rect);
|
|
1654
1675
|
this.contentRect = rect;
|
|
1676
|
+
if (changed && rect.width > 0 && rect.height > 0) {
|
|
1677
|
+
for (const child of this.children) {
|
|
1678
|
+
if (child instanceof TabNode) {
|
|
1679
|
+
child.setRect(rect);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1655
1683
|
}
|
|
1656
1684
|
/** @internal */
|
|
1657
1685
|
getContentRect() {
|
|
@@ -3157,7 +3185,15 @@ var BorderNode = class _BorderNode extends Node {
|
|
|
3157
3185
|
}
|
|
3158
3186
|
/** @internal */
|
|
3159
3187
|
setContentRect(r) {
|
|
3188
|
+
const changed = !this.contentRect.equals(r);
|
|
3160
3189
|
this.contentRect = r;
|
|
3190
|
+
if (changed && r.width > 0 && r.height > 0) {
|
|
3191
|
+
for (const child of this.children) {
|
|
3192
|
+
if (child instanceof TabNode) {
|
|
3193
|
+
child.setRect(r);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3161
3197
|
}
|
|
3162
3198
|
/** @internal */
|
|
3163
3199
|
isEnableDrop() {
|
|
@@ -3537,6 +3573,7 @@ var Splitter = (props) => {
|
|
|
3537
3573
|
function BorderTab(props) {
|
|
3538
3574
|
const { layout, border, show } = props;
|
|
3539
3575
|
const selfRef = React3.useRef(null);
|
|
3576
|
+
const isFirstRender = React3.useRef(true);
|
|
3540
3577
|
React3.useLayoutEffect(() => {
|
|
3541
3578
|
const outerRect = layout.getBoundingClientRect(selfRef.current);
|
|
3542
3579
|
const contentRect = Rect.getContentRect(selfRef.current).relativeTo(layout.getDomRect());
|
|
@@ -3544,9 +3581,12 @@ function BorderTab(props) {
|
|
|
3544
3581
|
border.setOuterRect(outerRect);
|
|
3545
3582
|
if (!border.getContentRect().equals(contentRect)) {
|
|
3546
3583
|
border.setContentRect(contentRect);
|
|
3547
|
-
|
|
3584
|
+
if (!isFirstRender.current) {
|
|
3585
|
+
layout.redrawInternal("border content rect");
|
|
3586
|
+
}
|
|
3548
3587
|
}
|
|
3549
3588
|
}
|
|
3589
|
+
isFirstRender.current = false;
|
|
3550
3590
|
});
|
|
3551
3591
|
let horizontal = true;
|
|
3552
3592
|
const style2 = {};
|
|
@@ -3574,16 +3614,6 @@ import * as React8 from "react";
|
|
|
3574
3614
|
|
|
3575
3615
|
// src/view/BorderButton.tsx
|
|
3576
3616
|
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
3617
|
var BorderButton = (props) => {
|
|
3588
3618
|
const { layout, node, selected, border, icons, path } = props;
|
|
3589
3619
|
const selfRef = React4.useRef(null);
|
|
@@ -3617,20 +3647,8 @@ var BorderButton = (props) => {
|
|
|
3617
3647
|
layout.setEditingTab(void 0);
|
|
3618
3648
|
}
|
|
3619
3649
|
};
|
|
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
3650
|
const onClose = (event) => {
|
|
3633
|
-
if (
|
|
3651
|
+
if (isTabClosable(node, selected)) {
|
|
3634
3652
|
layout.doAction(Actions.deleteTab(node.getId()));
|
|
3635
3653
|
} else {
|
|
3636
3654
|
onClick();
|
|
@@ -3834,12 +3852,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3834
3852
|
React7.useLayoutEffect(() => {
|
|
3835
3853
|
userControlledLeft.current = false;
|
|
3836
3854
|
}, [node.getSelectedNode(), node.getRect().width, node.getRect().height]);
|
|
3855
|
+
const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
|
|
3837
3856
|
React7.useLayoutEffect(() => {
|
|
3838
|
-
const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
|
|
3839
3857
|
if (nodeRect.width > 0 && nodeRect.height > 0) {
|
|
3840
3858
|
updateVisibleTabs();
|
|
3841
3859
|
}
|
|
3842
|
-
});
|
|
3860
|
+
}, [nodeRect.width, nodeRect.height]);
|
|
3843
3861
|
const instance = toolbarRef.current;
|
|
3844
3862
|
React7.useEffect(() => {
|
|
3845
3863
|
if (!instance) {
|
|
@@ -3879,15 +3897,15 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3879
3897
|
if (firstRender.current === true) {
|
|
3880
3898
|
tabsTruncated.current = false;
|
|
3881
3899
|
}
|
|
3882
|
-
const
|
|
3900
|
+
const nodeRect2 = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
|
|
3883
3901
|
const lastChild = node.getChildren()[node.getChildren().length - 1];
|
|
3884
3902
|
const stickyButtonsSize = stickyButtonsRef.current === null ? 0 : getSize(stickyButtonsRef.current.getBoundingClientRect());
|
|
3885
|
-
if (firstRender.current === true || lastHiddenCount.current === 0 && hiddenTabs.length !== 0 ||
|
|
3886
|
-
|
|
3903
|
+
if (firstRender.current === true || lastHiddenCount.current === 0 && hiddenTabs.length !== 0 || nodeRect2.width !== lastRect.current.width || // incase rect changed between first render and second
|
|
3904
|
+
nodeRect2.height !== lastRect.current.height) {
|
|
3887
3905
|
lastHiddenCount.current = hiddenTabs.length;
|
|
3888
|
-
lastRect.current =
|
|
3906
|
+
lastRect.current = nodeRect2;
|
|
3889
3907
|
const enabled = node instanceof TabSetNode ? node.isEnableTabStrip() === true : true;
|
|
3890
|
-
let endPos = getFar(
|
|
3908
|
+
let endPos = getFar(nodeRect2) - stickyButtonsSize;
|
|
3891
3909
|
if (toolbarRef.current !== null) {
|
|
3892
3910
|
endPos -= getSize(toolbarRef.current.getBoundingClientRect());
|
|
3893
3911
|
}
|
|
@@ -3901,12 +3919,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3901
3919
|
const selectedRect = selectedTab.getTabRect();
|
|
3902
3920
|
const selectedStart = getNear(selectedRect) - tabMargin;
|
|
3903
3921
|
const selectedEnd = getFar(selectedRect) + tabMargin;
|
|
3904
|
-
if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(
|
|
3905
|
-
shiftPos = getNear(
|
|
3922
|
+
if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(nodeRect2)) {
|
|
3923
|
+
shiftPos = getNear(nodeRect2) - selectedStart;
|
|
3906
3924
|
} else {
|
|
3907
|
-
if (selectedEnd > endPos || selectedStart < getNear(
|
|
3908
|
-
if (selectedStart < getNear(
|
|
3909
|
-
shiftPos = getNear(
|
|
3925
|
+
if (selectedEnd > endPos || selectedStart < getNear(nodeRect2)) {
|
|
3926
|
+
if (selectedStart < getNear(nodeRect2)) {
|
|
3927
|
+
shiftPos = getNear(nodeRect2) - selectedStart;
|
|
3910
3928
|
}
|
|
3911
3929
|
if (selectedEnd + shiftPos > endPos) {
|
|
3912
3930
|
shiftPos = endPos - selectedEnd;
|
|
@@ -3920,7 +3938,7 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
|
|
|
3920
3938
|
const hidden = [];
|
|
3921
3939
|
for (let i = 0; i < node.getChildren().length; i++) {
|
|
3922
3940
|
const child = node.getChildren()[i];
|
|
3923
|
-
if (getNear(child.getTabRect()) + diff < getNear(
|
|
3941
|
+
if (getNear(child.getTabRect()) + diff < getNear(nodeRect2) || getFar(child.getTabRect()) + diff > endPos) {
|
|
3924
3942
|
hidden.push({ node: child, index: i });
|
|
3925
3943
|
}
|
|
3926
3944
|
}
|
|
@@ -4371,21 +4389,9 @@ var TabButton = (props) => {
|
|
|
4371
4389
|
layout.setEditingTab(void 0);
|
|
4372
4390
|
}
|
|
4373
4391
|
};
|
|
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
4392
|
const onClose = (event) => {
|
|
4387
4393
|
event.preventDefault();
|
|
4388
|
-
if (
|
|
4394
|
+
if (isTabClosable(node, selected)) {
|
|
4389
4395
|
layout.doAction(Actions.deleteTab(node.getId()));
|
|
4390
4396
|
} else {
|
|
4391
4397
|
onClick();
|
|
@@ -4502,6 +4508,7 @@ var TabSet = (props) => {
|
|
|
4502
4508
|
const overflowbuttonRef = React15.useRef(null);
|
|
4503
4509
|
const stickyButtonsRef = React15.useRef(null);
|
|
4504
4510
|
const icons = layout.getIcons();
|
|
4511
|
+
const isFirstRender = React15.useRef(true);
|
|
4505
4512
|
React15.useEffect(() => {
|
|
4506
4513
|
node.setRect(layout.getBoundingClientRect(selfRef.current));
|
|
4507
4514
|
if (tabStripRef.current) {
|
|
@@ -4510,8 +4517,11 @@ var TabSet = (props) => {
|
|
|
4510
4517
|
const newContentRect = Rect.getContentRect(contentRef.current).relativeTo(layout.getDomRect());
|
|
4511
4518
|
if (!node.getContentRect().equals(newContentRect)) {
|
|
4512
4519
|
node.setContentRect(newContentRect);
|
|
4513
|
-
|
|
4520
|
+
if (!isFirstRender.current) {
|
|
4521
|
+
layout.redrawInternal("tabset content rect " + newContentRect);
|
|
4522
|
+
}
|
|
4514
4523
|
}
|
|
4524
|
+
isFirstRender.current = false;
|
|
4515
4525
|
});
|
|
4516
4526
|
const { selfRef, position, userControlledLeft, hiddenTabs, onMouseWheel, tabsTruncated } = useTabOverflow(node, Orientation.HORZ, buttonBarRef, stickyButtonsRef);
|
|
4517
4527
|
const onOverflowClick = (event) => {
|
|
@@ -4924,7 +4934,7 @@ var Tab = (props) => {
|
|
|
4924
4934
|
firstSelect.current = false;
|
|
4925
4935
|
}
|
|
4926
4936
|
}
|
|
4927
|
-
});
|
|
4937
|
+
}, [selected]);
|
|
4928
4938
|
const onPointerDown = () => {
|
|
4929
4939
|
const parent = node.getParent();
|
|
4930
4940
|
if (parent instanceof TabSetNode) {
|
|
@@ -5764,6 +5774,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
|
|
|
5764
5774
|
this.showOverlay(false);
|
|
5765
5775
|
this.dragEnterCount = 0;
|
|
5766
5776
|
this.dragging = false;
|
|
5777
|
+
this.props.onDragStateChange?.(false);
|
|
5767
5778
|
if (this.outlineDiv) {
|
|
5768
5779
|
this.selfRef.current.removeChild(this.outlineDiv);
|
|
5769
5780
|
this.outlineDiv = void 0;
|
|
@@ -5795,6 +5806,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
|
|
|
5795
5806
|
rootdiv.appendChild(this.outlineDiv);
|
|
5796
5807
|
this.dragging = true;
|
|
5797
5808
|
this.showOverlay(true);
|
|
5809
|
+
this.props.onDragStateChange?.(true);
|
|
5798
5810
|
if (!this.isDraggingOverWindow && this.props.model.getMaximizedTabset(this.windowId) === void 0) {
|
|
5799
5811
|
this.setState({ showEdges: this.props.model.isEnableEdgeDock() });
|
|
5800
5812
|
}
|
|
@@ -5880,6 +5892,161 @@ var DragState = class {
|
|
|
5880
5892
|
}
|
|
5881
5893
|
};
|
|
5882
5894
|
|
|
5895
|
+
// src/view/OptimizedLayout.tsx
|
|
5896
|
+
import * as React20 from "react";
|
|
5897
|
+
import { useCallback, useEffect as useEffect6, useMemo, useRef as useRef12, useState as useState4 } from "react";
|
|
5898
|
+
function TabRef({ node, onRectChange, onVisibilityChange }) {
|
|
5899
|
+
useEffect6(() => {
|
|
5900
|
+
const handleResize = (params) => {
|
|
5901
|
+
onRectChange(node.getId(), params.rect);
|
|
5902
|
+
};
|
|
5903
|
+
const handleVisibility = (params) => {
|
|
5904
|
+
onVisibilityChange(node.getId(), params.visible);
|
|
5905
|
+
};
|
|
5906
|
+
node.setEventListener("resize", handleResize);
|
|
5907
|
+
node.setEventListener("visibility", handleVisibility);
|
|
5908
|
+
const parent = node.getParent();
|
|
5909
|
+
if (parent) {
|
|
5910
|
+
const contentRect = parent.getContentRect();
|
|
5911
|
+
if (contentRect && contentRect.width > 0 && contentRect.height > 0) {
|
|
5912
|
+
onRectChange(node.getId(), contentRect);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
onVisibilityChange(node.getId(), node.isSelected());
|
|
5916
|
+
return () => {
|
|
5917
|
+
node.removeEventListener("resize");
|
|
5918
|
+
node.removeEventListener("visibility");
|
|
5919
|
+
};
|
|
5920
|
+
}, [node, onRectChange, onVisibilityChange]);
|
|
5921
|
+
return null;
|
|
5922
|
+
}
|
|
5923
|
+
function TabContainer({
|
|
5924
|
+
tabs,
|
|
5925
|
+
renderTab,
|
|
5926
|
+
isDragging,
|
|
5927
|
+
classNameMapper
|
|
5928
|
+
}) {
|
|
5929
|
+
const getClassName = useCallback(
|
|
5930
|
+
(defaultClassName) => {
|
|
5931
|
+
return classNameMapper ? classNameMapper(defaultClassName) : defaultClassName;
|
|
5932
|
+
},
|
|
5933
|
+
[classNameMapper]
|
|
5934
|
+
);
|
|
5935
|
+
return /* @__PURE__ */ React20.createElement(
|
|
5936
|
+
"div",
|
|
5937
|
+
{
|
|
5938
|
+
style: {
|
|
5939
|
+
position: "absolute",
|
|
5940
|
+
inset: 0,
|
|
5941
|
+
// CRITICAL: The container itself always has pointer-events: none
|
|
5942
|
+
// so it doesn't block clicks on elements beneath it (like FlexLayout's tab bar).
|
|
5943
|
+
// Individual tab panels have pointer-events: auto to receive clicks.
|
|
5944
|
+
// During drag, we also disable pointer events on the children to prevent
|
|
5945
|
+
// dragleave events on the Layout element.
|
|
5946
|
+
pointerEvents: "none",
|
|
5947
|
+
// Ensure tab container doesn't block FlexLayout's tab bar interactions
|
|
5948
|
+
zIndex: 0
|
|
5949
|
+
},
|
|
5950
|
+
"data-layout-path": "/tab-container"
|
|
5951
|
+
},
|
|
5952
|
+
Array.from(tabs.entries()).map(([nodeId, tabInfo]) => {
|
|
5953
|
+
const { node, rect, visible } = tabInfo;
|
|
5954
|
+
const contentClassName = node.getContentClassName();
|
|
5955
|
+
const hasValidDimensions = rect.width > 0 && rect.height > 0;
|
|
5956
|
+
return /* @__PURE__ */ React20.createElement(
|
|
5957
|
+
"div",
|
|
5958
|
+
{
|
|
5959
|
+
key: nodeId,
|
|
5960
|
+
role: "tabpanel",
|
|
5961
|
+
"data-tab-id": nodeId,
|
|
5962
|
+
className: getClassName("flexlayout__tab") + (contentClassName ? " " + contentClassName : ""),
|
|
5963
|
+
style: {
|
|
5964
|
+
position: "absolute",
|
|
5965
|
+
display: visible ? "flex" : "none",
|
|
5966
|
+
left: hasValidDimensions ? rect.x : 0,
|
|
5967
|
+
top: hasValidDimensions ? rect.y : 0,
|
|
5968
|
+
width: hasValidDimensions ? rect.width : "100%",
|
|
5969
|
+
height: hasValidDimensions ? rect.height : "100%",
|
|
5970
|
+
overflow: "auto",
|
|
5971
|
+
// Tab panels receive pointer events when visible and not dragging
|
|
5972
|
+
pointerEvents: visible && !isDragging ? "auto" : "none"
|
|
5973
|
+
}
|
|
5974
|
+
},
|
|
5975
|
+
renderTab(node)
|
|
5976
|
+
);
|
|
5977
|
+
})
|
|
5978
|
+
);
|
|
5979
|
+
}
|
|
5980
|
+
function OptimizedLayout({ model, renderTab, classNameMapper, onDragStateChange, ...layoutProps }) {
|
|
5981
|
+
const [isDragging, setIsDragging] = useState4(false);
|
|
5982
|
+
const [tabs, setTabs] = useState4(() => /* @__PURE__ */ new Map());
|
|
5983
|
+
const tabNodesRef = useRef12(/* @__PURE__ */ new Map());
|
|
5984
|
+
useEffect6(() => {
|
|
5985
|
+
const newTabNodes = /* @__PURE__ */ new Map();
|
|
5986
|
+
model.visitNodes((node) => {
|
|
5987
|
+
if (node instanceof TabNode) {
|
|
5988
|
+
newTabNodes.set(node.getId(), node);
|
|
5989
|
+
}
|
|
5990
|
+
});
|
|
5991
|
+
tabNodesRef.current = newTabNodes;
|
|
5992
|
+
setTabs((prevTabs) => {
|
|
5993
|
+
const nextTabs = /* @__PURE__ */ new Map();
|
|
5994
|
+
for (const [nodeId, node] of newTabNodes) {
|
|
5995
|
+
const existing = prevTabs.get(nodeId);
|
|
5996
|
+
if (existing) {
|
|
5997
|
+
nextTabs.set(nodeId, { ...existing, node });
|
|
5998
|
+
} else {
|
|
5999
|
+
const parent = node.getParent();
|
|
6000
|
+
const contentRect = parent?.getContentRect() ?? Rect.empty();
|
|
6001
|
+
nextTabs.set(nodeId, {
|
|
6002
|
+
node,
|
|
6003
|
+
rect: contentRect,
|
|
6004
|
+
visible: node.isSelected()
|
|
6005
|
+
});
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
return nextTabs;
|
|
6009
|
+
});
|
|
6010
|
+
}, [model]);
|
|
6011
|
+
const handleRectChange = useCallback((nodeId, rect) => {
|
|
6012
|
+
setTabs((prevTabs) => {
|
|
6013
|
+
const existing = prevTabs.get(nodeId);
|
|
6014
|
+
if (!existing || existing.rect.equals(rect)) {
|
|
6015
|
+
return prevTabs;
|
|
6016
|
+
}
|
|
6017
|
+
const nextTabs = new Map(prevTabs);
|
|
6018
|
+
nextTabs.set(nodeId, { ...existing, rect });
|
|
6019
|
+
return nextTabs;
|
|
6020
|
+
});
|
|
6021
|
+
}, []);
|
|
6022
|
+
const handleVisibilityChange = useCallback((nodeId, visible) => {
|
|
6023
|
+
setTabs((prevTabs) => {
|
|
6024
|
+
const existing = prevTabs.get(nodeId);
|
|
6025
|
+
if (!existing || existing.visible === visible) {
|
|
6026
|
+
return prevTabs;
|
|
6027
|
+
}
|
|
6028
|
+
const nextTabs = new Map(prevTabs);
|
|
6029
|
+
nextTabs.set(nodeId, { ...existing, visible });
|
|
6030
|
+
return nextTabs;
|
|
6031
|
+
});
|
|
6032
|
+
}, []);
|
|
6033
|
+
const handleDragStateChange = useCallback(
|
|
6034
|
+
(dragging) => {
|
|
6035
|
+
setIsDragging(dragging);
|
|
6036
|
+
onDragStateChange?.(dragging);
|
|
6037
|
+
},
|
|
6038
|
+
[onDragStateChange]
|
|
6039
|
+
);
|
|
6040
|
+
const factory = useCallback(
|
|
6041
|
+
(node) => {
|
|
6042
|
+
return /* @__PURE__ */ React20.createElement(TabRef, { key: node.getId(), node, onRectChange: handleRectChange, onVisibilityChange: handleVisibilityChange });
|
|
6043
|
+
},
|
|
6044
|
+
[handleRectChange, handleVisibilityChange]
|
|
6045
|
+
);
|
|
6046
|
+
const tabsForContainer = useMemo(() => tabs, [tabs]);
|
|
6047
|
+
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 }));
|
|
6048
|
+
}
|
|
6049
|
+
|
|
5883
6050
|
// src/model/walk.ts
|
|
5884
6051
|
function findJsonNodeById(model, id) {
|
|
5885
6052
|
let result;
|
|
@@ -5939,6 +6106,7 @@ export {
|
|
|
5939
6106
|
LayoutWindow,
|
|
5940
6107
|
Model,
|
|
5941
6108
|
Node,
|
|
6109
|
+
OptimizedLayout,
|
|
5942
6110
|
Orientation,
|
|
5943
6111
|
Rect,
|
|
5944
6112
|
RowNode,
|
|
@@ -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;
|