react-resizable-panels 0.0.54 → 0.0.56
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/.eslintrc.cjs +26 -0
- package/CHANGELOG.md +253 -80
- package/README.md +55 -49
- package/dist/declarations/src/Panel.d.ts +76 -20
- package/dist/declarations/src/PanelGroup.d.ts +29 -21
- package/dist/declarations/src/PanelResizeHandle.d.ts +1 -1
- package/dist/declarations/src/index.d.ts +5 -5
- package/dist/declarations/src/types.d.ts +3 -25
- package/dist/declarations/src/vendor/react.d.ts +4 -4
- package/dist/react-resizable-panels.browser.cjs.js +1279 -796
- package/dist/react-resizable-panels.browser.development.cjs.js +1404 -809
- package/dist/react-resizable-panels.browser.development.esm.js +1398 -803
- package/dist/react-resizable-panels.browser.esm.js +1279 -796
- package/dist/react-resizable-panels.cjs.js +1279 -796
- package/dist/react-resizable-panels.cjs.js.map +1 -0
- package/dist/react-resizable-panels.development.cjs.js +1399 -804
- package/dist/react-resizable-panels.development.esm.js +1400 -805
- package/dist/react-resizable-panels.development.node.cjs.js +1172 -755
- package/dist/react-resizable-panels.development.node.esm.js +1173 -756
- package/dist/react-resizable-panels.esm.js +1279 -796
- package/dist/react-resizable-panels.esm.js.map +1 -0
- package/dist/react-resizable-panels.node.cjs.js +1064 -749
- package/dist/react-resizable-panels.node.esm.js +1065 -750
- package/jest.config.js +10 -0
- package/package.json +3 -1
- package/src/Panel.test.tsx +308 -0
- package/src/Panel.ts +179 -127
- package/src/PanelGroup.test.tsx +210 -0
- package/src/PanelGroup.ts +751 -580
- package/src/PanelGroupContext.ts +33 -0
- package/src/PanelResizeHandle.ts +13 -8
- package/src/hooks/useUniqueId.ts +1 -1
- package/src/hooks/useWindowSplitterBehavior.ts +9 -161
- package/src/hooks/useWindowSplitterPanelGroupBehavior.ts +185 -0
- package/src/index.ts +24 -11
- package/src/types.ts +3 -29
- package/src/utils/adjustLayoutByDelta.test.ts +1808 -0
- package/src/utils/adjustLayoutByDelta.ts +211 -0
- package/src/utils/calculateAriaValues.test.ts +111 -0
- package/src/utils/calculateAriaValues.ts +67 -0
- package/src/utils/calculateDeltaPercentage.ts +68 -0
- package/src/utils/calculateDragOffsetPercentage.ts +30 -0
- package/src/utils/calculateUnsafeDefaultLayout.test.ts +92 -0
- package/src/utils/calculateUnsafeDefaultLayout.ts +55 -0
- package/src/utils/callPanelCallbacks.ts +81 -0
- package/src/utils/compareLayouts.test.ts +9 -0
- package/src/utils/compareLayouts.ts +12 -0
- package/src/utils/computePanelFlexBoxStyle.ts +44 -0
- package/src/utils/computePercentagePanelConstraints.test.ts +71 -0
- package/src/utils/computePercentagePanelConstraints.ts +56 -0
- package/src/utils/convertPercentageToPixels.test.ts +9 -0
- package/src/utils/convertPercentageToPixels.ts +6 -0
- package/src/utils/convertPixelConstraintsToPercentages.ts +55 -0
- package/src/utils/convertPixelsToPercentage.test.ts +9 -0
- package/src/utils/convertPixelsToPercentage.ts +6 -0
- package/src/utils/determinePivotIndices.ts +10 -0
- package/src/utils/dom/calculateAvailablePanelSizeInPixels.ts +29 -0
- package/src/utils/dom/getAvailableGroupSizePixels.ts +29 -0
- package/src/utils/dom/getPanelElement.ts +7 -0
- package/src/utils/dom/getPanelGroupElement.ts +7 -0
- package/src/utils/dom/getResizeHandleElement.ts +9 -0
- package/src/utils/dom/getResizeHandleElementIndex.ts +12 -0
- package/src/utils/dom/getResizeHandleElementsForGroup.ts +9 -0
- package/src/utils/dom/getResizeHandlePanelIds.ts +18 -0
- package/src/utils/events.ts +13 -0
- package/src/utils/getPercentageSizeFromMixedSizes.test.ts +47 -0
- package/src/utils/getPercentageSizeFromMixedSizes.ts +15 -0
- package/src/utils/getResizeEventCursorPosition.ts +19 -0
- package/src/utils/initializeDefaultStorage.ts +26 -0
- package/src/utils/numbers/fuzzyCompareNumbers.test.ts +16 -0
- package/src/utils/numbers/fuzzyCompareNumbers.ts +17 -0
- package/src/utils/numbers/fuzzyNumbersEqual.ts +9 -0
- package/src/utils/resizePanel.ts +41 -0
- package/src/utils/serialization.ts +9 -4
- package/src/utils/shouldMonitorPixelBasedConstraints.test.ts +23 -0
- package/src/utils/shouldMonitorPixelBasedConstraints.ts +13 -0
- package/src/utils/test-utils.ts +136 -0
- package/src/utils/validatePanelConstraints.test.ts +151 -0
- package/src/utils/validatePanelConstraints.ts +103 -0
- package/src/utils/validatePanelGroupLayout.test.ts +233 -0
- package/src/utils/validatePanelGroupLayout.ts +88 -0
- package/src/vendor/react.ts +4 -0
- package/.eslintrc.json +0 -22
- package/src/PanelContexts.ts +0 -20
- package/src/utils/coordinates.ts +0 -149
- package/src/utils/group.ts +0 -315
|
@@ -7,6 +7,7 @@ import * as React from 'react';
|
|
|
7
7
|
const {
|
|
8
8
|
createElement,
|
|
9
9
|
createContext,
|
|
10
|
+
createRef,
|
|
10
11
|
forwardRef,
|
|
11
12
|
useCallback,
|
|
12
13
|
useContext,
|
|
@@ -21,6 +22,9 @@ const {
|
|
|
21
22
|
// `toString()` prevents bundlers from trying to `import { useId } from 'react'`
|
|
22
23
|
const useId = React["useId".toString()];
|
|
23
24
|
|
|
25
|
+
const PanelGroupContext = createContext(null);
|
|
26
|
+
PanelGroupContext.displayName = "PanelGroupContext";
|
|
27
|
+
|
|
24
28
|
const useIsomorphicLayoutEffect = useLayoutEffect ;
|
|
25
29
|
|
|
26
30
|
const wrappedUseId = typeof useId === "function" ? useId : () => null;
|
|
@@ -31,123 +35,140 @@ function useUniqueId(idFromParams = null) {
|
|
|
31
35
|
if (idRef.current === null) {
|
|
32
36
|
idRef.current = "" + counter++;
|
|
33
37
|
}
|
|
34
|
-
return idRef.current;
|
|
38
|
+
return idFromParams ?? idRef.current;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
const PanelGroupContext = createContext(null);
|
|
38
|
-
PanelGroupContext.displayName = "PanelGroupContext";
|
|
39
|
-
|
|
40
41
|
function PanelWithForwardedRef({
|
|
41
|
-
children
|
|
42
|
+
children,
|
|
42
43
|
className: classNameFromProps = "",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
collapsedSizePercentage,
|
|
45
|
+
collapsedSizePixels,
|
|
46
|
+
collapsible,
|
|
47
|
+
defaultSizePercentage,
|
|
48
|
+
defaultSizePixels,
|
|
46
49
|
forwardedRef,
|
|
47
|
-
id: idFromProps
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
id: idFromProps,
|
|
51
|
+
maxSizePercentage,
|
|
52
|
+
maxSizePixels,
|
|
53
|
+
minSizePercentage,
|
|
54
|
+
minSizePixels,
|
|
55
|
+
onCollapse,
|
|
56
|
+
onExpand,
|
|
57
|
+
onResize,
|
|
58
|
+
order,
|
|
59
|
+
style: styleFromProps,
|
|
54
60
|
tagName: Type = "div"
|
|
55
61
|
}) {
|
|
56
62
|
const context = useContext(PanelGroupContext);
|
|
57
63
|
if (context === null) {
|
|
58
64
|
throw Error(`Panel components must be rendered within a PanelGroup container`);
|
|
59
65
|
}
|
|
60
|
-
const panelId = useUniqueId(idFromProps);
|
|
61
66
|
const {
|
|
62
67
|
collapsePanel,
|
|
63
68
|
expandPanel,
|
|
69
|
+
getPanelSize,
|
|
64
70
|
getPanelStyle,
|
|
71
|
+
isPanelCollapsed,
|
|
65
72
|
registerPanel,
|
|
66
73
|
resizePanel,
|
|
67
74
|
unregisterPanel
|
|
68
75
|
} = context;
|
|
69
|
-
|
|
70
|
-
// Use a ref to guard against users passing inline props
|
|
71
|
-
const callbacksRef = useRef({
|
|
72
|
-
onCollapse,
|
|
73
|
-
onResize
|
|
74
|
-
});
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
callbacksRef.current.onCollapse = onCollapse;
|
|
77
|
-
callbacksRef.current.onResize = onResize;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Basic props validation
|
|
81
|
-
if (minSize < 0 || minSize > 100) {
|
|
82
|
-
throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
|
|
83
|
-
} else if (maxSize < 0 || maxSize > 100) {
|
|
84
|
-
throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
|
|
85
|
-
} else {
|
|
86
|
-
if (defaultSize !== null) {
|
|
87
|
-
if (defaultSize < 0 || defaultSize > 100) {
|
|
88
|
-
throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
|
|
89
|
-
} else if (minSize > defaultSize && !collapsible) {
|
|
90
|
-
console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
|
|
91
|
-
defaultSize = minSize;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const style = getPanelStyle(panelId, defaultSize);
|
|
96
|
-
const committedValuesRef = useRef({
|
|
97
|
-
size: parseSizeFromStyle(style)
|
|
98
|
-
});
|
|
76
|
+
const panelId = useUniqueId(idFromProps);
|
|
99
77
|
const panelDataRef = useRef({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
78
|
+
callbacks: {
|
|
79
|
+
onCollapse,
|
|
80
|
+
onExpand,
|
|
81
|
+
onResize
|
|
82
|
+
},
|
|
83
|
+
constraints: {
|
|
84
|
+
collapsedSizePercentage,
|
|
85
|
+
collapsedSizePixels,
|
|
86
|
+
collapsible,
|
|
87
|
+
defaultSizePercentage,
|
|
88
|
+
defaultSizePixels,
|
|
89
|
+
maxSizePercentage,
|
|
90
|
+
maxSizePixels,
|
|
91
|
+
minSizePercentage,
|
|
92
|
+
minSizePixels
|
|
93
|
+
},
|
|
104
94
|
id: panelId,
|
|
105
|
-
|
|
106
|
-
maxSize,
|
|
107
|
-
minSize,
|
|
95
|
+
idIsFromProps: idFromProps !== undefined,
|
|
108
96
|
order
|
|
109
97
|
});
|
|
98
|
+
const devWarningsRef = useRef({
|
|
99
|
+
didLogMissingDefaultSizeWarning: false
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Normally we wouldn't log a warning during render,
|
|
103
|
+
// but effects don't run on the server, so we can't do it there
|
|
104
|
+
{
|
|
105
|
+
if (!devWarningsRef.current.didLogMissingDefaultSizeWarning) ;
|
|
106
|
+
}
|
|
110
107
|
useIsomorphicLayoutEffect(() => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
panelDataRef.current
|
|
115
|
-
panelDataRef.current.defaultSize = defaultSize;
|
|
108
|
+
const {
|
|
109
|
+
callbacks,
|
|
110
|
+
constraints
|
|
111
|
+
} = panelDataRef.current;
|
|
116
112
|
panelDataRef.current.id = panelId;
|
|
117
|
-
panelDataRef.current.
|
|
118
|
-
panelDataRef.current.maxSize = maxSize;
|
|
119
|
-
panelDataRef.current.minSize = minSize;
|
|
113
|
+
panelDataRef.current.idIsFromProps = idFromProps !== undefined;
|
|
120
114
|
panelDataRef.current.order = order;
|
|
115
|
+
callbacks.onCollapse = onCollapse;
|
|
116
|
+
callbacks.onExpand = onExpand;
|
|
117
|
+
callbacks.onResize = onResize;
|
|
118
|
+
constraints.collapsedSizePercentage = collapsedSizePercentage;
|
|
119
|
+
constraints.collapsedSizePixels = collapsedSizePixels;
|
|
120
|
+
constraints.collapsible = collapsible;
|
|
121
|
+
constraints.defaultSizePercentage = defaultSizePercentage;
|
|
122
|
+
constraints.defaultSizePixels = defaultSizePixels;
|
|
123
|
+
constraints.maxSizePercentage = maxSizePercentage;
|
|
124
|
+
constraints.maxSizePixels = maxSizePixels;
|
|
125
|
+
constraints.minSizePercentage = minSizePercentage;
|
|
126
|
+
constraints.minSizePixels = minSizePixels;
|
|
121
127
|
});
|
|
122
128
|
useIsomorphicLayoutEffect(() => {
|
|
123
|
-
|
|
129
|
+
const panelData = panelDataRef.current;
|
|
130
|
+
registerPanel(panelData);
|
|
124
131
|
return () => {
|
|
125
|
-
unregisterPanel(
|
|
132
|
+
unregisterPanel(panelData);
|
|
126
133
|
};
|
|
127
134
|
}, [order, panelId, registerPanel, unregisterPanel]);
|
|
128
135
|
useImperativeHandle(forwardedRef, () => ({
|
|
129
|
-
collapse: () =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
collapse: () => {
|
|
137
|
+
collapsePanel(panelDataRef.current);
|
|
138
|
+
},
|
|
139
|
+
expand: () => {
|
|
140
|
+
expandPanel(panelDataRef.current);
|
|
141
|
+
},
|
|
142
|
+
getId() {
|
|
143
|
+
return panelId;
|
|
133
144
|
},
|
|
134
145
|
getSize() {
|
|
135
|
-
return
|
|
146
|
+
return getPanelSize(panelDataRef.current);
|
|
147
|
+
},
|
|
148
|
+
isCollapsed() {
|
|
149
|
+
return isPanelCollapsed(panelDataRef.current);
|
|
136
150
|
},
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
isExpanded() {
|
|
152
|
+
return !isPanelCollapsed(panelDataRef.current);
|
|
153
|
+
},
|
|
154
|
+
resize: mixedSizes => {
|
|
155
|
+
resizePanel(panelDataRef.current, mixedSizes);
|
|
156
|
+
}
|
|
157
|
+
}), [collapsePanel, expandPanel, getPanelSize, isPanelCollapsed, panelId, resizePanel]);
|
|
158
|
+
const style = getPanelStyle(panelDataRef.current);
|
|
139
159
|
return createElement(Type, {
|
|
140
160
|
children,
|
|
141
161
|
className: classNameFromProps,
|
|
142
|
-
"data-panel": "",
|
|
143
|
-
"data-panel-collapsible": collapsible || undefined,
|
|
144
|
-
"data-panel-id": panelId,
|
|
145
|
-
"data-panel-size": parseFloat("" + style.flexGrow).toFixed(1),
|
|
146
|
-
id: `data-panel-id-${panelId}`,
|
|
147
162
|
style: {
|
|
148
163
|
...style,
|
|
149
164
|
...styleFromProps
|
|
150
|
-
}
|
|
165
|
+
},
|
|
166
|
+
// CSS selectors
|
|
167
|
+
"data-panel": "",
|
|
168
|
+
// e2e test attributes
|
|
169
|
+
"data-panel-collapsible": collapsible || undefined ,
|
|
170
|
+
"data-panel-id": panelId ,
|
|
171
|
+
"data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
|
|
151
172
|
});
|
|
152
173
|
}
|
|
153
174
|
const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
|
|
@@ -157,33 +178,133 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
|
|
|
157
178
|
PanelWithForwardedRef.displayName = "Panel";
|
|
158
179
|
Panel.displayName = "forwardRef(Panel)";
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
|
|
181
|
+
const PRECISION = 10;
|
|
182
|
+
|
|
183
|
+
function convertPixelsToPercentage(pixels, groupSizePixels) {
|
|
184
|
+
return pixels / groupSizePixels * 100;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function convertPixelConstraintsToPercentages(panelConstraints, groupSizePixels) {
|
|
188
|
+
let {
|
|
189
|
+
collapsedSizePercentage = 0,
|
|
190
|
+
collapsedSizePixels,
|
|
191
|
+
defaultSizePercentage,
|
|
192
|
+
defaultSizePixels,
|
|
193
|
+
maxSizePercentage = 100,
|
|
194
|
+
maxSizePixels,
|
|
195
|
+
minSizePercentage = 0,
|
|
196
|
+
minSizePixels
|
|
197
|
+
} = panelConstraints;
|
|
198
|
+
if (collapsedSizePixels != null) {
|
|
199
|
+
collapsedSizePercentage = convertPixelsToPercentage(collapsedSizePixels, groupSizePixels);
|
|
200
|
+
}
|
|
201
|
+
if (defaultSizePixels != null) {
|
|
202
|
+
defaultSizePercentage = convertPixelsToPercentage(defaultSizePixels, groupSizePixels);
|
|
203
|
+
}
|
|
204
|
+
if (minSizePixels != null) {
|
|
205
|
+
minSizePercentage = convertPixelsToPercentage(minSizePixels, groupSizePixels);
|
|
206
|
+
}
|
|
207
|
+
if (maxSizePixels != null) {
|
|
208
|
+
maxSizePercentage = convertPixelsToPercentage(maxSizePixels, groupSizePixels);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
collapsedSizePercentage,
|
|
212
|
+
defaultSizePercentage,
|
|
213
|
+
maxSizePercentage,
|
|
214
|
+
minSizePercentage
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, groupSizePixels) {
|
|
219
|
+
// All panel constraints, excluding the current one
|
|
220
|
+
let totalMinConstraints = 0;
|
|
221
|
+
let totalMaxConstraints = 0;
|
|
222
|
+
for (let index = 0; index < panelConstraintsArray.length; index++) {
|
|
223
|
+
if (index !== panelIndex) {
|
|
224
|
+
const {
|
|
225
|
+
collapsible
|
|
226
|
+
} = panelConstraintsArray[index];
|
|
227
|
+
const {
|
|
228
|
+
collapsedSizePercentage,
|
|
229
|
+
maxSizePercentage,
|
|
230
|
+
minSizePercentage
|
|
231
|
+
} = convertPixelConstraintsToPercentages(panelConstraintsArray[index], groupSizePixels);
|
|
232
|
+
totalMaxConstraints += maxSizePercentage;
|
|
233
|
+
totalMinConstraints += collapsible ? collapsedSizePercentage : minSizePercentage;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
162
236
|
const {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
237
|
+
collapsedSizePercentage,
|
|
238
|
+
defaultSizePercentage,
|
|
239
|
+
maxSizePercentage,
|
|
240
|
+
minSizePercentage
|
|
241
|
+
} = convertPixelConstraintsToPercentages(panelConstraintsArray[panelIndex], groupSizePixels);
|
|
242
|
+
return {
|
|
243
|
+
collapsedSizePercentage,
|
|
244
|
+
defaultSizePercentage,
|
|
245
|
+
maxSizePercentage: panelConstraintsArray.length > 1 ? Math.min(maxSizePercentage, 100 - totalMinConstraints) : maxSizePercentage,
|
|
246
|
+
minSizePercentage: panelConstraintsArray.length > 1 ? Math.max(minSizePercentage, 100 - totalMaxConstraints) : minSizePercentage
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
|
|
251
|
+
actual = parseFloat(actual.toFixed(fractionDigits));
|
|
252
|
+
expected = parseFloat(expected.toFixed(fractionDigits));
|
|
253
|
+
const delta = actual - expected;
|
|
254
|
+
if (delta === 0) {
|
|
255
|
+
return 0;
|
|
167
256
|
} else {
|
|
168
|
-
return
|
|
257
|
+
return delta > 0 ? 1 : -1;
|
|
169
258
|
}
|
|
170
259
|
}
|
|
171
260
|
|
|
172
|
-
|
|
261
|
+
function fuzzyNumbersEqual(actual, expected, fractionDigits) {
|
|
262
|
+
return fuzzyCompareNumbers(actual, expected, fractionDigits) === 0;
|
|
263
|
+
}
|
|
173
264
|
|
|
174
|
-
|
|
265
|
+
// Panel size must be in percentages; pixel values should be pre-converted
|
|
266
|
+
function resizePanel({
|
|
267
|
+
groupSizePixels,
|
|
268
|
+
panelConstraints,
|
|
269
|
+
panelIndex,
|
|
270
|
+
size
|
|
271
|
+
}) {
|
|
272
|
+
let {
|
|
273
|
+
collapsible
|
|
274
|
+
} = panelConstraints[panelIndex];
|
|
175
275
|
const {
|
|
176
|
-
|
|
177
|
-
|
|
276
|
+
collapsedSizePercentage,
|
|
277
|
+
maxSizePercentage,
|
|
278
|
+
minSizePercentage
|
|
279
|
+
} = computePercentagePanelConstraints(panelConstraints, panelIndex, groupSizePixels);
|
|
280
|
+
if (minSizePercentage != null) {
|
|
281
|
+
if (fuzzyCompareNumbers(size, minSizePercentage) < 0) {
|
|
282
|
+
if (collapsible) {
|
|
283
|
+
size = collapsedSizePercentage;
|
|
284
|
+
} else {
|
|
285
|
+
size = minSizePercentage;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (maxSizePercentage != null) {
|
|
290
|
+
size = Math.min(maxSizePercentage, size);
|
|
291
|
+
}
|
|
292
|
+
return size;
|
|
293
|
+
}
|
|
178
294
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
295
|
+
// All units must be in percentages; pixel values should be pre-converted
|
|
296
|
+
function adjustLayoutByDelta({
|
|
297
|
+
delta,
|
|
298
|
+
groupSizePixels,
|
|
299
|
+
layout: prevLayout,
|
|
300
|
+
panelConstraints,
|
|
301
|
+
pivotIndices,
|
|
302
|
+
trigger
|
|
303
|
+
}) {
|
|
304
|
+
if (fuzzyNumbersEqual(delta, 0)) {
|
|
305
|
+
return prevLayout;
|
|
184
306
|
}
|
|
185
|
-
const
|
|
186
|
-
const nextSizes = baseSizes.concat();
|
|
307
|
+
const nextLayout = [...prevLayout];
|
|
187
308
|
let deltaApplied = 0;
|
|
188
309
|
|
|
189
310
|
// A resizing panel affects the panels before or after it.
|
|
@@ -194,268 +315,360 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
|
|
|
194
315
|
// A positive delta means the panel immediately before the resizer should "expand".
|
|
195
316
|
// This is accomplished by shrinking/contracting (and shifting) one or more of the panels after the resizer.
|
|
196
317
|
|
|
197
|
-
//
|
|
318
|
+
// First, check the panel we're pivoting around;
|
|
319
|
+
// We should only expand or contract by as much as its constraints allow
|
|
198
320
|
{
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
321
|
+
const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
|
|
322
|
+
const initialSize = nextLayout[pivotIndex];
|
|
323
|
+
const {
|
|
324
|
+
collapsible
|
|
325
|
+
} = panelConstraints[pivotIndex];
|
|
326
|
+
const {
|
|
327
|
+
collapsedSizePercentage,
|
|
328
|
+
maxSizePercentage,
|
|
329
|
+
minSizePercentage
|
|
330
|
+
} = computePercentagePanelConstraints(panelConstraints, pivotIndex, groupSizePixels);
|
|
331
|
+
const isCollapsed = collapsible && fuzzyNumbersEqual(initialSize, collapsedSizePercentage);
|
|
332
|
+
let unsafeSize = initialSize + Math.abs(delta);
|
|
333
|
+
if (isCollapsed) {
|
|
334
|
+
switch (trigger) {
|
|
335
|
+
case "keyboard":
|
|
336
|
+
if (minSizePercentage > unsafeSize) {
|
|
337
|
+
unsafeSize = minSizePercentage;
|
|
338
|
+
}
|
|
210
339
|
}
|
|
211
|
-
delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
|
|
212
340
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
deltaApplied += baseSize - nextSize;
|
|
226
|
-
nextSizes[index] = nextSize;
|
|
227
|
-
if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
|
|
228
|
-
numeric: true
|
|
229
|
-
}) >= 0) {
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
341
|
+
const safeSize = resizePanel({
|
|
342
|
+
groupSizePixels,
|
|
343
|
+
panelConstraints,
|
|
344
|
+
panelIndex: pivotIndex,
|
|
345
|
+
size: unsafeSize
|
|
346
|
+
});
|
|
347
|
+
if (fuzzyNumbersEqual(initialSize, safeSize)) {
|
|
348
|
+
// If there's no room for the pivot panel to grow, we should ignore this change
|
|
349
|
+
return nextLayout;
|
|
350
|
+
} else {
|
|
351
|
+
delta = delta < 0 ? initialSize - safeSize : safeSize - initialSize;
|
|
232
352
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Delta added to a panel needs to be subtracted from other panels
|
|
356
|
+
// within the constraints that those panels allow
|
|
357
|
+
{
|
|
358
|
+
const pivotIndex = delta < 0 ? pivotIndices[0] : pivotIndices[1];
|
|
359
|
+
let index = pivotIndex;
|
|
360
|
+
while (index >= 0 && index < panelConstraints.length) {
|
|
361
|
+
const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
|
|
362
|
+
const prevSize = prevLayout[index];
|
|
363
|
+
const unsafeSize = prevSize - deltaRemaining;
|
|
364
|
+
let safeSize = resizePanel({
|
|
365
|
+
groupSizePixels,
|
|
366
|
+
panelConstraints,
|
|
367
|
+
panelIndex: index,
|
|
368
|
+
size: unsafeSize
|
|
369
|
+
});
|
|
370
|
+
if (!fuzzyNumbersEqual(prevSize, safeSize)) {
|
|
371
|
+
deltaApplied += prevSize - safeSize;
|
|
372
|
+
nextLayout[index] = safeSize;
|
|
373
|
+
if (deltaApplied.toPrecision(3).localeCompare(Math.abs(delta).toPrecision(3), undefined, {
|
|
374
|
+
numeric: true
|
|
375
|
+
}) >= 0) {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
236
378
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
379
|
+
if (delta < 0) {
|
|
380
|
+
index--;
|
|
381
|
+
} else {
|
|
382
|
+
index++;
|
|
240
383
|
}
|
|
241
384
|
}
|
|
242
385
|
}
|
|
243
386
|
|
|
244
387
|
// If we were unable to resize any of the panels panels, return the previous state.
|
|
245
|
-
// This will essentially bailout and ignore
|
|
246
|
-
if (deltaApplied
|
|
247
|
-
return
|
|
388
|
+
// This will essentially bailout and ignore e.g. drags past a panel's boundaries
|
|
389
|
+
if (fuzzyNumbersEqual(deltaApplied, 0)) {
|
|
390
|
+
return prevLayout;
|
|
248
391
|
}
|
|
392
|
+
{
|
|
393
|
+
const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
|
|
394
|
+
const unsafeSize = prevLayout[pivotIndex] + deltaApplied;
|
|
395
|
+
const safeSize = resizePanel({
|
|
396
|
+
groupSizePixels,
|
|
397
|
+
panelConstraints,
|
|
398
|
+
panelIndex: pivotIndex,
|
|
399
|
+
size: unsafeSize
|
|
400
|
+
});
|
|
249
401
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
402
|
+
// Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
|
|
403
|
+
nextLayout[pivotIndex] = safeSize;
|
|
404
|
+
|
|
405
|
+
// Edge case where expanding or contracting one panel caused another one to change collapsed state
|
|
406
|
+
if (!fuzzyNumbersEqual(safeSize, unsafeSize)) {
|
|
407
|
+
let deltaRemaining = unsafeSize - safeSize;
|
|
408
|
+
const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
|
|
409
|
+
let index = pivotIndex;
|
|
410
|
+
while (index >= 0 && index < panelConstraints.length) {
|
|
411
|
+
const prevSize = nextLayout[index];
|
|
412
|
+
const unsafeSize = prevSize + deltaRemaining;
|
|
413
|
+
const safeSize = resizePanel({
|
|
414
|
+
groupSizePixels,
|
|
415
|
+
panelConstraints,
|
|
416
|
+
panelIndex: index,
|
|
417
|
+
size: unsafeSize
|
|
418
|
+
});
|
|
419
|
+
if (!fuzzyNumbersEqual(prevSize, safeSize)) {
|
|
420
|
+
deltaRemaining -= safeSize - prevSize;
|
|
421
|
+
nextLayout[index] = safeSize;
|
|
422
|
+
}
|
|
423
|
+
if (fuzzyNumbersEqual(deltaRemaining, 0)) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
if (delta > 0) {
|
|
427
|
+
index--;
|
|
428
|
+
} else {
|
|
429
|
+
index++;
|
|
430
|
+
}
|
|
279
431
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
432
|
+
|
|
433
|
+
// If we can't redistribute, this layout is invalid;
|
|
434
|
+
// There may be an incremental layout that is valid though
|
|
435
|
+
if (!fuzzyNumbersEqual(deltaRemaining, 0)) {
|
|
436
|
+
try {
|
|
437
|
+
return adjustLayoutByDelta({
|
|
438
|
+
delta: delta < 0 ? delta + 1 : delta - 1,
|
|
439
|
+
groupSizePixels,
|
|
440
|
+
layout: prevLayout,
|
|
441
|
+
panelConstraints,
|
|
442
|
+
pivotIndices,
|
|
443
|
+
trigger
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (error instanceof RangeError) {
|
|
447
|
+
console.error(`Could not apply delta ${delta} to layout`);
|
|
448
|
+
return prevLayout;
|
|
449
|
+
}
|
|
450
|
+
} finally {
|
|
285
451
|
}
|
|
286
452
|
}
|
|
287
453
|
}
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
function getBeforeAndAfterIds(id, panelsArray) {
|
|
291
|
-
if (panelsArray.length < 2) {
|
|
292
|
-
return [null, null];
|
|
293
454
|
}
|
|
294
|
-
|
|
295
|
-
if (index < 0) {
|
|
296
|
-
return [null, null];
|
|
297
|
-
}
|
|
298
|
-
const isLastPanel = index === panelsArray.length - 1;
|
|
299
|
-
const idBefore = isLastPanel ? panelsArray[index - 1].current.id : id;
|
|
300
|
-
const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
|
|
301
|
-
return [idBefore, idAfter];
|
|
455
|
+
return nextLayout;
|
|
302
456
|
}
|
|
303
457
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return "100";
|
|
309
|
-
}
|
|
310
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
311
|
-
const index = panelsArray.findIndex(panel => panel.current.id === id);
|
|
312
|
-
const size = sizes[index];
|
|
313
|
-
if (size == null) {
|
|
314
|
-
return "0";
|
|
458
|
+
function assert(expectedCondition, message = "Assertion failed!") {
|
|
459
|
+
if (!expectedCondition) {
|
|
460
|
+
console.error(message);
|
|
461
|
+
throw Error(message);
|
|
315
462
|
}
|
|
316
|
-
return size.toPrecision(PRECISION);
|
|
317
463
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
464
|
+
|
|
465
|
+
function getPercentageSizeFromMixedSizes({
|
|
466
|
+
sizePercentage,
|
|
467
|
+
sizePixels
|
|
468
|
+
}, groupSizePixels) {
|
|
469
|
+
if (sizePercentage != null) {
|
|
470
|
+
return sizePercentage;
|
|
471
|
+
} else if (sizePixels != null) {
|
|
472
|
+
return convertPixelsToPercentage(sizePixels, groupSizePixels);
|
|
322
473
|
}
|
|
323
|
-
return
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function calculateAriaValues({
|
|
478
|
+
groupSizePixels,
|
|
479
|
+
layout,
|
|
480
|
+
panelsArray,
|
|
481
|
+
pivotIndices
|
|
482
|
+
}) {
|
|
483
|
+
let currentMinSize = 0;
|
|
484
|
+
let currentMaxSize = 100;
|
|
485
|
+
let totalMinSize = 0;
|
|
486
|
+
let totalMaxSize = 0;
|
|
487
|
+
|
|
488
|
+
// A panel's effective min/max sizes also need to account for other panel's sizes.
|
|
489
|
+
panelsArray.forEach((panelData, index) => {
|
|
490
|
+
const {
|
|
491
|
+
constraints
|
|
492
|
+
} = panelData;
|
|
493
|
+
const {
|
|
494
|
+
maxSizePercentage,
|
|
495
|
+
maxSizePixels,
|
|
496
|
+
minSizePercentage,
|
|
497
|
+
minSizePixels
|
|
498
|
+
} = constraints;
|
|
499
|
+
const minSize = getPercentageSizeFromMixedSizes({
|
|
500
|
+
sizePercentage: minSizePercentage,
|
|
501
|
+
sizePixels: minSizePixels
|
|
502
|
+
}, groupSizePixels) ?? 0;
|
|
503
|
+
const maxSize = getPercentageSizeFromMixedSizes({
|
|
504
|
+
sizePercentage: maxSizePercentage,
|
|
505
|
+
sizePixels: maxSizePixels
|
|
506
|
+
}, groupSizePixels) ?? 100;
|
|
507
|
+
if (index === pivotIndices[0]) {
|
|
508
|
+
currentMinSize = minSize;
|
|
509
|
+
currentMaxSize = maxSize;
|
|
510
|
+
} else {
|
|
511
|
+
totalMinSize += minSize;
|
|
512
|
+
totalMaxSize += maxSize;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
const valueMax = Math.min(currentMaxSize, 100 - totalMinSize);
|
|
516
|
+
const valueMin = Math.max(currentMinSize, 100 - totalMaxSize);
|
|
517
|
+
const valueNow = layout[pivotIndices[0]];
|
|
518
|
+
return {
|
|
519
|
+
valueMax,
|
|
520
|
+
valueMin,
|
|
521
|
+
valueNow
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function getResizeHandleElementsForGroup(groupId) {
|
|
526
|
+
return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function getResizeHandleElementIndex(groupId, id) {
|
|
530
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
531
|
+
const index = handles.findIndex(handle => handle.getAttribute("data-panel-resize-handle-id") === id);
|
|
532
|
+
return index ?? null;
|
|
324
533
|
}
|
|
325
|
-
|
|
534
|
+
|
|
535
|
+
function determinePivotIndices(groupId, dragHandleId) {
|
|
536
|
+
const index = getResizeHandleElementIndex(groupId, dragHandleId);
|
|
537
|
+
return index != null ? [index, index + 1] : [-1, -1];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getPanelGroupElement(id) {
|
|
326
541
|
const element = document.querySelector(`[data-panel-group-id="${id}"]`);
|
|
327
542
|
if (element) {
|
|
328
543
|
return element;
|
|
329
544
|
}
|
|
330
545
|
return null;
|
|
331
546
|
}
|
|
332
|
-
|
|
547
|
+
|
|
548
|
+
function calculateAvailablePanelSizeInPixels(groupId) {
|
|
549
|
+
const panelGroupElement = getPanelGroupElement(groupId);
|
|
550
|
+
if (panelGroupElement == null) {
|
|
551
|
+
return NaN;
|
|
552
|
+
}
|
|
553
|
+
const direction = panelGroupElement.getAttribute("data-panel-group-direction");
|
|
554
|
+
const resizeHandles = getResizeHandleElementsForGroup(groupId);
|
|
555
|
+
if (direction === "horizontal") {
|
|
556
|
+
return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
|
|
557
|
+
return accumulated + handle.offsetWidth;
|
|
558
|
+
}, 0);
|
|
559
|
+
} else {
|
|
560
|
+
return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
|
|
561
|
+
return accumulated + handle.offsetHeight;
|
|
562
|
+
}, 0);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function getAvailableGroupSizePixels(groupId) {
|
|
567
|
+
const panelGroupElement = getPanelGroupElement(groupId);
|
|
568
|
+
if (panelGroupElement == null) {
|
|
569
|
+
return NaN;
|
|
570
|
+
}
|
|
571
|
+
const direction = panelGroupElement.getAttribute("data-panel-group-direction");
|
|
572
|
+
const resizeHandles = getResizeHandleElementsForGroup(groupId);
|
|
573
|
+
if (direction === "horizontal") {
|
|
574
|
+
return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
|
|
575
|
+
return accumulated + handle.offsetWidth;
|
|
576
|
+
}, 0);
|
|
577
|
+
} else {
|
|
578
|
+
return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
|
|
579
|
+
return accumulated + handle.offsetHeight;
|
|
580
|
+
}, 0);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function getResizeHandleElement(id) {
|
|
333
585
|
const element = document.querySelector(`[data-panel-resize-handle-id="${id}"]`);
|
|
334
586
|
if (element) {
|
|
335
587
|
return element;
|
|
336
588
|
}
|
|
337
589
|
return null;
|
|
338
590
|
}
|
|
339
|
-
|
|
340
|
-
const handles = getResizeHandles();
|
|
341
|
-
const index = handles.findIndex(handle => handle.getAttribute("data-panel-resize-handle-id") === id);
|
|
342
|
-
return index ?? null;
|
|
343
|
-
}
|
|
344
|
-
function getResizeHandles() {
|
|
345
|
-
return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id]`));
|
|
346
|
-
}
|
|
347
|
-
function getResizeHandlesForGroup(groupId) {
|
|
348
|
-
return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`));
|
|
349
|
-
}
|
|
591
|
+
|
|
350
592
|
function getResizeHandlePanelIds(groupId, handleId, panelsArray) {
|
|
351
|
-
const handle =
|
|
352
|
-
const handles =
|
|
593
|
+
const handle = getResizeHandleElement(handleId);
|
|
594
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
353
595
|
const index = handle ? handles.indexOf(handle) : -1;
|
|
354
|
-
const idBefore = panelsArray[index]?.
|
|
355
|
-
const idAfter = panelsArray[index + 1]?.
|
|
596
|
+
const idBefore = panelsArray[index]?.id ?? null;
|
|
597
|
+
const idAfter = panelsArray[index + 1]?.id ?? null;
|
|
356
598
|
return [idBefore, idAfter];
|
|
357
599
|
}
|
|
358
|
-
function panelsMapToSortedArray(panels) {
|
|
359
|
-
return Array.from(panels.values()).sort((panelA, panelB) => {
|
|
360
|
-
const orderA = panelA.current.order;
|
|
361
|
-
const orderB = panelB.current.order;
|
|
362
|
-
if (orderA == null && orderB == null) {
|
|
363
|
-
return 0;
|
|
364
|
-
} else if (orderA == null) {
|
|
365
|
-
return -1;
|
|
366
|
-
} else if (orderB == null) {
|
|
367
|
-
return 1;
|
|
368
|
-
} else {
|
|
369
|
-
return orderA - orderB;
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
function safeResizePanel(panel, delta, prevSize, event) {
|
|
374
|
-
const nextSizeUnsafe = prevSize + delta;
|
|
375
|
-
const {
|
|
376
|
-
collapsedSize,
|
|
377
|
-
collapsible,
|
|
378
|
-
maxSize,
|
|
379
|
-
minSize
|
|
380
|
-
} = panel.current;
|
|
381
|
-
if (collapsible) {
|
|
382
|
-
if (prevSize > collapsedSize) {
|
|
383
|
-
// Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
|
|
384
|
-
if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
|
|
385
|
-
return collapsedSize;
|
|
386
|
-
}
|
|
387
|
-
} else {
|
|
388
|
-
const isKeyboardEvent = event?.type?.startsWith("key");
|
|
389
|
-
if (!isKeyboardEvent) {
|
|
390
|
-
// Keyboard events should expand a collapsed panel to the min size,
|
|
391
|
-
// but mouse events should wait until the panel has reached its min size
|
|
392
|
-
// to avoid a visual flickering when dragging between collapsed and min size.
|
|
393
|
-
if (nextSizeUnsafe < minSize) {
|
|
394
|
-
return collapsedSize;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
|
|
400
|
-
return nextSize;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function assert(expectedCondition, message = "Assertion failed!") {
|
|
404
|
-
if (!expectedCondition) {
|
|
405
|
-
console.error(message);
|
|
406
|
-
throw Error(message);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
600
|
|
|
410
601
|
// https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
411
602
|
|
|
412
603
|
function useWindowSplitterPanelGroupBehavior({
|
|
413
604
|
committedValuesRef,
|
|
414
605
|
groupId,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
panelSizeBeforeCollapse
|
|
606
|
+
layout,
|
|
607
|
+
panelDataArray,
|
|
608
|
+
setLayout
|
|
419
609
|
}) {
|
|
610
|
+
const devWarningsRef = useRef({
|
|
611
|
+
didWarnAboutMissingResizeHandle: false
|
|
612
|
+
});
|
|
613
|
+
useIsomorphicLayoutEffect(() => {
|
|
614
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
615
|
+
const resizeHandleElements = getResizeHandleElementsForGroup(groupId);
|
|
616
|
+
for (let index = 0; index < panelDataArray.length - 1; index++) {
|
|
617
|
+
const {
|
|
618
|
+
valueMax,
|
|
619
|
+
valueMin,
|
|
620
|
+
valueNow
|
|
621
|
+
} = calculateAriaValues({
|
|
622
|
+
groupSizePixels,
|
|
623
|
+
layout,
|
|
624
|
+
panelsArray: panelDataArray,
|
|
625
|
+
pivotIndices: [index, index + 1]
|
|
626
|
+
});
|
|
627
|
+
const resizeHandleElement = resizeHandleElements[index];
|
|
628
|
+
if (resizeHandleElement == null) {
|
|
629
|
+
{
|
|
630
|
+
const {
|
|
631
|
+
didWarnAboutMissingResizeHandle
|
|
632
|
+
} = devWarningsRef.current;
|
|
633
|
+
if (!didWarnAboutMissingResizeHandle) {
|
|
634
|
+
devWarningsRef.current.didWarnAboutMissingResizeHandle = true;
|
|
635
|
+
console.warn(`WARNING: Missing resize handle for PanelGroup "${groupId}"`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
resizeHandleElement.setAttribute("aria-controls", panelDataArray[index].id);
|
|
640
|
+
resizeHandleElement.setAttribute("aria-valuemax", "" + Math.round(valueMax));
|
|
641
|
+
resizeHandleElement.setAttribute("aria-valuemin", "" + Math.round(valueMin));
|
|
642
|
+
resizeHandleElement.setAttribute("aria-valuenow", "" + Math.round(valueNow));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return () => {
|
|
646
|
+
resizeHandleElements.forEach((resizeHandleElement, index) => {
|
|
647
|
+
resizeHandleElement.removeAttribute("aria-controls");
|
|
648
|
+
resizeHandleElement.removeAttribute("aria-valuemax");
|
|
649
|
+
resizeHandleElement.removeAttribute("aria-valuemin");
|
|
650
|
+
resizeHandleElement.removeAttribute("aria-valuenow");
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
}, [groupId, layout, panelDataArray]);
|
|
420
654
|
useEffect(() => {
|
|
421
655
|
const {
|
|
422
656
|
direction,
|
|
423
|
-
|
|
657
|
+
panelDataArray
|
|
424
658
|
} = committedValuesRef.current;
|
|
425
|
-
const groupElement =
|
|
659
|
+
const groupElement = getPanelGroupElement(groupId);
|
|
660
|
+
assert(groupElement != null, `No group found for id "${groupId}"`);
|
|
426
661
|
const {
|
|
427
662
|
height,
|
|
428
663
|
width
|
|
429
664
|
} = groupElement.getBoundingClientRect();
|
|
430
|
-
const handles =
|
|
665
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
431
666
|
const cleanupFunctions = handles.map(handle => {
|
|
432
667
|
const handleId = handle.getAttribute("data-panel-resize-handle-id");
|
|
433
|
-
const
|
|
434
|
-
const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelsArray);
|
|
668
|
+
const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelDataArray);
|
|
435
669
|
if (idBefore == null || idAfter == null) {
|
|
436
670
|
return () => {};
|
|
437
671
|
}
|
|
438
|
-
let minSize = 0;
|
|
439
|
-
let maxSize = 100;
|
|
440
|
-
let totalMinSize = 0;
|
|
441
|
-
let totalMaxSize = 0;
|
|
442
|
-
|
|
443
|
-
// A panel's effective min/max sizes also need to account for other panel's sizes.
|
|
444
|
-
panelsArray.forEach(panelData => {
|
|
445
|
-
if (panelData.current.id === idBefore) {
|
|
446
|
-
maxSize = panelData.current.maxSize;
|
|
447
|
-
minSize = panelData.current.minSize;
|
|
448
|
-
} else {
|
|
449
|
-
totalMinSize += panelData.current.minSize;
|
|
450
|
-
totalMaxSize += panelData.current.maxSize;
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
|
|
454
|
-
const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
|
|
455
|
-
const flexGrow = getFlexGrow(panels, idBefore, sizes);
|
|
456
|
-
handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
|
|
457
|
-
handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
|
|
458
|
-
handle.setAttribute("aria-valuenow", "" + Math.round(parseInt(flexGrow)));
|
|
459
672
|
const onKeyDown = event => {
|
|
460
673
|
if (event.defaultPrevented) {
|
|
461
674
|
return;
|
|
@@ -464,20 +677,32 @@ function useWindowSplitterPanelGroupBehavior({
|
|
|
464
677
|
case "Enter":
|
|
465
678
|
{
|
|
466
679
|
event.preventDefault();
|
|
467
|
-
const index =
|
|
680
|
+
const index = panelDataArray.findIndex(panelData => panelData.id === idBefore);
|
|
468
681
|
if (index >= 0) {
|
|
469
|
-
const panelData =
|
|
470
|
-
const size =
|
|
682
|
+
const panelData = panelDataArray[index];
|
|
683
|
+
const size = layout[index];
|
|
471
684
|
if (size != null) {
|
|
685
|
+
const groupSizePixels = getAvailableGroupSizePixels(groupId);
|
|
686
|
+
const minSize = getPercentageSizeFromMixedSizes({
|
|
687
|
+
sizePercentage: panelData.constraints.minSizePercentage,
|
|
688
|
+
sizePixels: panelData.constraints.minSizePixels
|
|
689
|
+
}, groupSizePixels) ?? 0;
|
|
472
690
|
let delta = 0;
|
|
473
|
-
if (size.toPrecision(PRECISION) <=
|
|
691
|
+
if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
|
|
474
692
|
delta = direction === "horizontal" ? width : height;
|
|
475
693
|
} else {
|
|
476
694
|
delta = -(direction === "horizontal" ? width : height);
|
|
477
695
|
}
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
696
|
+
const nextLayout = adjustLayoutByDelta({
|
|
697
|
+
delta,
|
|
698
|
+
groupSizePixels,
|
|
699
|
+
layout,
|
|
700
|
+
panelConstraints: panelDataArray.map(panelData => panelData.constraints),
|
|
701
|
+
pivotIndices: determinePivotIndices(groupId, handleId),
|
|
702
|
+
trigger: "keyboard"
|
|
703
|
+
});
|
|
704
|
+
if (layout !== nextLayout) {
|
|
705
|
+
setLayout(nextLayout);
|
|
481
706
|
}
|
|
482
707
|
}
|
|
483
708
|
}
|
|
@@ -486,72 +711,14 @@ function useWindowSplitterPanelGroupBehavior({
|
|
|
486
711
|
}
|
|
487
712
|
};
|
|
488
713
|
handle.addEventListener("keydown", onKeyDown);
|
|
489
|
-
const panelBefore = getPanel(idBefore);
|
|
490
|
-
if (panelBefore != null) {
|
|
491
|
-
handle.setAttribute("aria-controls", panelBefore.id);
|
|
492
|
-
}
|
|
493
714
|
return () => {
|
|
494
|
-
handle.removeAttribute("aria-valuemax");
|
|
495
|
-
handle.removeAttribute("aria-valuemin");
|
|
496
|
-
handle.removeAttribute("aria-valuenow");
|
|
497
715
|
handle.removeEventListener("keydown", onKeyDown);
|
|
498
|
-
if (panelBefore != null) {
|
|
499
|
-
handle.removeAttribute("aria-controls");
|
|
500
|
-
}
|
|
501
716
|
};
|
|
502
717
|
});
|
|
503
718
|
return () => {
|
|
504
719
|
cleanupFunctions.forEach(cleanupFunction => cleanupFunction());
|
|
505
720
|
};
|
|
506
|
-
}, [committedValuesRef, groupId,
|
|
507
|
-
}
|
|
508
|
-
function useWindowSplitterResizeHandlerBehavior({
|
|
509
|
-
disabled,
|
|
510
|
-
handleId,
|
|
511
|
-
resizeHandler
|
|
512
|
-
}) {
|
|
513
|
-
useEffect(() => {
|
|
514
|
-
if (disabled || resizeHandler == null) {
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const handleElement = getResizeHandle(handleId);
|
|
518
|
-
if (handleElement == null) {
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
const onKeyDown = event => {
|
|
522
|
-
if (event.defaultPrevented) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
switch (event.key) {
|
|
526
|
-
case "ArrowDown":
|
|
527
|
-
case "ArrowLeft":
|
|
528
|
-
case "ArrowRight":
|
|
529
|
-
case "ArrowUp":
|
|
530
|
-
case "End":
|
|
531
|
-
case "Home":
|
|
532
|
-
{
|
|
533
|
-
event.preventDefault();
|
|
534
|
-
resizeHandler(event);
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
case "F6":
|
|
538
|
-
{
|
|
539
|
-
event.preventDefault();
|
|
540
|
-
const handles = getResizeHandles();
|
|
541
|
-
const index = getResizeHandleIndex(handleId);
|
|
542
|
-
assert(index !== null);
|
|
543
|
-
const nextIndex = event.shiftKey ? index > 0 ? index - 1 : handles.length - 1 : index + 1 < handles.length ? index + 1 : 0;
|
|
544
|
-
const nextHandle = handles[nextIndex];
|
|
545
|
-
nextHandle.focus();
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
handleElement.addEventListener("keydown", onKeyDown);
|
|
551
|
-
return () => {
|
|
552
|
-
handleElement.removeEventListener("keydown", onKeyDown);
|
|
553
|
-
};
|
|
554
|
-
}, [disabled, handleId, resizeHandler]);
|
|
721
|
+
}, [committedValuesRef, groupId, layout, panelDataArray, setLayout]);
|
|
555
722
|
}
|
|
556
723
|
|
|
557
724
|
function areEqual(arrayA, arrayB) {
|
|
@@ -566,41 +733,61 @@ function areEqual(arrayA, arrayB) {
|
|
|
566
733
|
return true;
|
|
567
734
|
}
|
|
568
735
|
|
|
569
|
-
function
|
|
736
|
+
function isKeyDown(event) {
|
|
737
|
+
return event.type === "keydown";
|
|
738
|
+
}
|
|
739
|
+
function isMouseEvent(event) {
|
|
740
|
+
return event.type.startsWith("mouse");
|
|
741
|
+
}
|
|
742
|
+
function isTouchEvent(event) {
|
|
743
|
+
return event.type.startsWith("touch");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function getResizeEventCursorPosition(direction, event) {
|
|
570
747
|
const isHorizontal = direction === "horizontal";
|
|
571
|
-
let pointerOffset = 0;
|
|
572
748
|
if (isMouseEvent(event)) {
|
|
573
|
-
|
|
749
|
+
return isHorizontal ? event.clientX : event.clientY;
|
|
574
750
|
} else if (isTouchEvent(event)) {
|
|
575
751
|
const firstTouch = event.touches[0];
|
|
576
|
-
|
|
752
|
+
return isHorizontal ? firstTouch.screenX : firstTouch.screenY;
|
|
577
753
|
} else {
|
|
578
|
-
|
|
754
|
+
throw Error(`Unsupported event type "${event.type}"`);
|
|
579
755
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState) {
|
|
759
|
+
const isHorizontal = direction === "horizontal";
|
|
760
|
+
const handleElement = getResizeHandleElement(dragHandleId);
|
|
761
|
+
const groupId = handleElement.getAttribute("data-panel-group-id");
|
|
762
|
+
let {
|
|
763
|
+
initialCursorPosition
|
|
764
|
+
} = initialDragState;
|
|
765
|
+
const cursorPosition = getResizeEventCursorPosition(direction, event);
|
|
766
|
+
const groupElement = getPanelGroupElement(groupId);
|
|
767
|
+
const groupRect = groupElement.getBoundingClientRect();
|
|
768
|
+
const groupSizeInPixels = isHorizontal ? groupRect.width : groupRect.height;
|
|
769
|
+
const offsetPixels = cursorPosition - initialCursorPosition;
|
|
770
|
+
const offsetPercentage = offsetPixels / groupSizeInPixels * 100;
|
|
771
|
+
return offsetPercentage;
|
|
584
772
|
}
|
|
585
773
|
|
|
586
774
|
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX
|
|
587
|
-
function
|
|
588
|
-
const {
|
|
589
|
-
dragOffset = 0,
|
|
590
|
-
dragHandleRect,
|
|
591
|
-
sizes: initialSizes
|
|
592
|
-
} = initialDragState || {};
|
|
593
|
-
|
|
594
|
-
// If we're resizing by mouse or touch, use the initial sizes as a base.
|
|
595
|
-
// This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
|
|
596
|
-
const baseSizes = initialSizes || prevSizes;
|
|
775
|
+
function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initialDragState, keyboardResizeByOptions) {
|
|
597
776
|
if (isKeyDown(event)) {
|
|
598
777
|
const isHorizontal = direction === "horizontal";
|
|
599
|
-
const groupElement =
|
|
778
|
+
const groupElement = getPanelGroupElement(groupId);
|
|
600
779
|
const rect = groupElement.getBoundingClientRect();
|
|
601
780
|
const groupSizeInPixels = isHorizontal ? rect.width : rect.height;
|
|
602
|
-
|
|
603
|
-
|
|
781
|
+
let delta = 0;
|
|
782
|
+
if (event.shiftKey) {
|
|
783
|
+
delta = 100;
|
|
784
|
+
} else if (keyboardResizeByOptions.percentage != null) {
|
|
785
|
+
delta = keyboardResizeByOptions.percentage;
|
|
786
|
+
} else if (keyboardResizeByOptions.pixels != null) {
|
|
787
|
+
delta = keyboardResizeByOptions.pixels / groupSizeInPixels;
|
|
788
|
+
} else {
|
|
789
|
+
delta = 10;
|
|
790
|
+
}
|
|
604
791
|
let movement = 0;
|
|
605
792
|
switch (event.key) {
|
|
606
793
|
case "ArrowDown":
|
|
@@ -616,40 +803,152 @@ function getMovement(event, groupId, handleId, panelsArray, direction, prevSizes
|
|
|
616
803
|
movement = isHorizontal ? 0 : -delta;
|
|
617
804
|
break;
|
|
618
805
|
case "End":
|
|
619
|
-
movement =
|
|
806
|
+
movement = 100;
|
|
620
807
|
break;
|
|
621
808
|
case "Home":
|
|
622
|
-
movement = -
|
|
809
|
+
movement = -100;
|
|
623
810
|
break;
|
|
624
811
|
}
|
|
625
|
-
|
|
626
|
-
// If the Panel being resized is collapsible,
|
|
627
|
-
// we need to special case resizing around the minSize boundary.
|
|
628
|
-
// If contracting, Panels should shrink to their minSize and then snap to fully collapsed.
|
|
629
|
-
// If expanding from collapsed, they should snap back to their minSize.
|
|
630
|
-
const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelsArray);
|
|
631
|
-
const targetPanelId = movement < 0 ? idBefore : idAfter;
|
|
632
|
-
const targetPanelIndex = panelsArray.findIndex(panel => panel.current.id === targetPanelId);
|
|
633
|
-
const targetPanel = panelsArray[targetPanelIndex];
|
|
634
|
-
if (targetPanel.current.collapsible) {
|
|
635
|
-
const baseSize = baseSizes[targetPanelIndex];
|
|
636
|
-
if (baseSize === 0 || baseSize.toPrecision(PRECISION) === targetPanel.current.minSize.toPrecision(PRECISION)) {
|
|
637
|
-
movement = movement < 0 ? -targetPanel.current.minSize * groupSizeInPixels : targetPanel.current.minSize * groupSizeInPixels;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
812
|
return movement;
|
|
641
813
|
} else {
|
|
642
|
-
return
|
|
814
|
+
return calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState);
|
|
643
815
|
}
|
|
644
816
|
}
|
|
645
|
-
|
|
646
|
-
|
|
817
|
+
|
|
818
|
+
function calculateUnsafeDefaultLayout({
|
|
819
|
+
groupSizePixels,
|
|
820
|
+
panelDataArray
|
|
821
|
+
}) {
|
|
822
|
+
const layout = Array(panelDataArray.length);
|
|
823
|
+
const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
|
|
824
|
+
let numPanelsWithSizes = 0;
|
|
825
|
+
let remainingSize = 100;
|
|
826
|
+
|
|
827
|
+
// Distribute default sizes first
|
|
828
|
+
for (let index = 0; index < panelDataArray.length; index++) {
|
|
829
|
+
const {
|
|
830
|
+
defaultSizePercentage
|
|
831
|
+
} = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
|
|
832
|
+
if (defaultSizePercentage != null) {
|
|
833
|
+
numPanelsWithSizes++;
|
|
834
|
+
layout[index] = defaultSizePercentage;
|
|
835
|
+
remainingSize -= defaultSizePercentage;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Remaining size should be distributed evenly between panels without default sizes
|
|
840
|
+
for (let index = 0; index < panelDataArray.length; index++) {
|
|
841
|
+
const {
|
|
842
|
+
defaultSizePercentage
|
|
843
|
+
} = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
|
|
844
|
+
if (defaultSizePercentage != null) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
|
|
848
|
+
const size = remainingSize / numRemainingPanels;
|
|
849
|
+
numPanelsWithSizes++;
|
|
850
|
+
layout[index] = size;
|
|
851
|
+
remainingSize -= size;
|
|
852
|
+
}
|
|
853
|
+
return layout;
|
|
647
854
|
}
|
|
648
|
-
|
|
649
|
-
|
|
855
|
+
|
|
856
|
+
function convertPercentageToPixels(percentage, groupSizePixels) {
|
|
857
|
+
return percentage / 100 * groupSizePixels;
|
|
650
858
|
}
|
|
651
|
-
|
|
652
|
-
|
|
859
|
+
|
|
860
|
+
// Layout should be pre-converted into percentages
|
|
861
|
+
function callPanelCallbacks(groupId, panelsArray, layout, panelIdToLastNotifiedMixedSizesMap) {
|
|
862
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
863
|
+
layout.forEach((sizePercentage, index) => {
|
|
864
|
+
const panelData = panelsArray[index];
|
|
865
|
+
if (!panelData) {
|
|
866
|
+
// Handle initial mount (when panels are registered too late to be in the panels array)
|
|
867
|
+
// The subsequent render+effects will handle the resize notification
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const {
|
|
871
|
+
callbacks,
|
|
872
|
+
constraints,
|
|
873
|
+
id: panelId
|
|
874
|
+
} = panelData;
|
|
875
|
+
const {
|
|
876
|
+
collapsible
|
|
877
|
+
} = constraints;
|
|
878
|
+
const mixedSizes = {
|
|
879
|
+
sizePercentage,
|
|
880
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
881
|
+
};
|
|
882
|
+
const lastNotifiedMixedSizes = panelIdToLastNotifiedMixedSizesMap[panelId];
|
|
883
|
+
if (lastNotifiedMixedSizes == null || mixedSizes.sizePercentage !== lastNotifiedMixedSizes.sizePercentage || mixedSizes.sizePixels !== lastNotifiedMixedSizes.sizePixels) {
|
|
884
|
+
panelIdToLastNotifiedMixedSizesMap[panelId] = mixedSizes;
|
|
885
|
+
const {
|
|
886
|
+
onCollapse,
|
|
887
|
+
onExpand,
|
|
888
|
+
onResize
|
|
889
|
+
} = callbacks;
|
|
890
|
+
if (onResize) {
|
|
891
|
+
onResize(mixedSizes, lastNotifiedMixedSizes);
|
|
892
|
+
}
|
|
893
|
+
if (collapsible && (onCollapse || onExpand)) {
|
|
894
|
+
const collapsedSize = getPercentageSizeFromMixedSizes({
|
|
895
|
+
sizePercentage: constraints.collapsedSizePercentage,
|
|
896
|
+
sizePixels: constraints.collapsedSizePixels
|
|
897
|
+
}, groupSizePixels) ?? 0;
|
|
898
|
+
const size = getPercentageSizeFromMixedSizes(mixedSizes, groupSizePixels);
|
|
899
|
+
if (onExpand && (lastNotifiedMixedSizes == null || lastNotifiedMixedSizes.sizePercentage === collapsedSize) && size !== collapsedSize) {
|
|
900
|
+
onExpand();
|
|
901
|
+
}
|
|
902
|
+
if (onCollapse && (lastNotifiedMixedSizes == null || lastNotifiedMixedSizes.sizePercentage !== collapsedSize) && size === collapsedSize) {
|
|
903
|
+
onCollapse();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function compareLayouts(a, b) {
|
|
911
|
+
if (a.length !== b.length) {
|
|
912
|
+
return false;
|
|
913
|
+
} else {
|
|
914
|
+
for (let index = 0; index < a.length; index++) {
|
|
915
|
+
if (a[index] != b[index]) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// This method returns a number between 1 and 100 representing
|
|
924
|
+
|
|
925
|
+
// the % of the group's overall space this panel should occupy.
|
|
926
|
+
function computePanelFlexBoxStyle({
|
|
927
|
+
dragState,
|
|
928
|
+
layout,
|
|
929
|
+
panelData,
|
|
930
|
+
panelIndex,
|
|
931
|
+
precision = 3
|
|
932
|
+
}) {
|
|
933
|
+
const size = layout[panelIndex];
|
|
934
|
+
let flexGrow;
|
|
935
|
+
if (panelData.length === 1) {
|
|
936
|
+
flexGrow = "100";
|
|
937
|
+
} else if (size == null) {
|
|
938
|
+
flexGrow = "0";
|
|
939
|
+
} else {
|
|
940
|
+
flexGrow = size.toPrecision(precision);
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
flexBasis: 0,
|
|
944
|
+
flexGrow,
|
|
945
|
+
flexShrink: 1,
|
|
946
|
+
// Without this, Panel sizes may be unintentionally overridden by their content
|
|
947
|
+
overflow: "hidden",
|
|
948
|
+
// Disable pointer events inside of a panel during resize
|
|
949
|
+
// This avoid edge cases like nested iframes
|
|
950
|
+
pointerEvents: dragState !== null ? "none" : undefined
|
|
951
|
+
};
|
|
653
952
|
}
|
|
654
953
|
|
|
655
954
|
let currentState = null;
|
|
@@ -703,17 +1002,47 @@ function debounce(callback, durationMs = 10) {
|
|
|
703
1002
|
return callable;
|
|
704
1003
|
}
|
|
705
1004
|
|
|
1005
|
+
// PanelGroup might be rendering in a server-side environment where localStorage is not available
|
|
1006
|
+
// or on a browser with cookies/storage disabled.
|
|
1007
|
+
// In either case, this function avoids accessing localStorage until needed,
|
|
1008
|
+
// and avoids throwing user-visible errors.
|
|
1009
|
+
function initializeDefaultStorage(storageObject) {
|
|
1010
|
+
try {
|
|
1011
|
+
if (typeof localStorage !== "undefined") {
|
|
1012
|
+
// Bypass this check for future calls
|
|
1013
|
+
storageObject.getItem = name => {
|
|
1014
|
+
return localStorage.getItem(name);
|
|
1015
|
+
};
|
|
1016
|
+
storageObject.setItem = (name, value) => {
|
|
1017
|
+
localStorage.setItem(name, value);
|
|
1018
|
+
};
|
|
1019
|
+
} else {
|
|
1020
|
+
throw new Error("localStorage not supported in this environment");
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
console.error(error);
|
|
1024
|
+
storageObject.getItem = () => null;
|
|
1025
|
+
storageObject.setItem = () => {};
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
706
1029
|
// Note that Panel ids might be user-provided (stable) or useId generated (non-deterministic)
|
|
707
1030
|
// so they should not be used as part of the serialization key.
|
|
708
|
-
// Using
|
|
1031
|
+
// Using the min/max size attributes should work well enough as a backup.
|
|
709
1032
|
// Pre-sorting by minSize allows remembering layouts even if panels are re-ordered/dragged.
|
|
710
1033
|
function getSerializationKey(panels) {
|
|
711
1034
|
return panels.map(panel => {
|
|
712
1035
|
const {
|
|
713
|
-
|
|
1036
|
+
constraints,
|
|
1037
|
+
id,
|
|
1038
|
+
idIsFromProps,
|
|
714
1039
|
order
|
|
715
|
-
} = panel
|
|
716
|
-
|
|
1040
|
+
} = panel;
|
|
1041
|
+
if (idIsFromProps) {
|
|
1042
|
+
return id;
|
|
1043
|
+
} else {
|
|
1044
|
+
return `${order}:${JSON.stringify(constraints)}`;
|
|
1045
|
+
}
|
|
717
1046
|
}).sort((a, b) => a.localeCompare(b)).join(",");
|
|
718
1047
|
}
|
|
719
1048
|
function loadSerializedPanelGroupState(autoSaveId, storage) {
|
|
@@ -747,31 +1076,145 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
|
|
|
747
1076
|
}
|
|
748
1077
|
}
|
|
749
1078
|
|
|
750
|
-
|
|
1079
|
+
function shouldMonitorPixelBasedConstraints(constraints) {
|
|
1080
|
+
return constraints.some(constraints => {
|
|
1081
|
+
return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
751
1084
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1085
|
+
function validatePanelConstraints({
|
|
1086
|
+
groupSizePixels,
|
|
1087
|
+
panelConstraints,
|
|
1088
|
+
panelId,
|
|
1089
|
+
panelIndex
|
|
1090
|
+
}) {
|
|
1091
|
+
{
|
|
1092
|
+
const warnings = [];
|
|
1093
|
+
{
|
|
1094
|
+
const {
|
|
1095
|
+
collapsedSizePercentage,
|
|
1096
|
+
collapsedSizePixels,
|
|
1097
|
+
defaultSizePercentage,
|
|
1098
|
+
defaultSizePixels,
|
|
1099
|
+
maxSizePercentage,
|
|
1100
|
+
maxSizePixels,
|
|
1101
|
+
minSizePercentage,
|
|
1102
|
+
minSizePixels
|
|
1103
|
+
} = panelConstraints[panelIndex];
|
|
1104
|
+
const conflictingUnits = [];
|
|
1105
|
+
if (collapsedSizePercentage != null && collapsedSizePixels != null) {
|
|
1106
|
+
conflictingUnits.push("collapsed size");
|
|
1107
|
+
}
|
|
1108
|
+
if (defaultSizePercentage != null && defaultSizePixels != null) {
|
|
1109
|
+
conflictingUnits.push("default size");
|
|
1110
|
+
}
|
|
1111
|
+
if (maxSizePercentage != null && maxSizePixels != null) {
|
|
1112
|
+
conflictingUnits.push("max size");
|
|
1113
|
+
}
|
|
1114
|
+
if (minSizePercentage != null && minSizePixels != null) {
|
|
1115
|
+
conflictingUnits.push("min size");
|
|
1116
|
+
}
|
|
1117
|
+
if (conflictingUnits.length > 0) {
|
|
1118
|
+
warnings.push(`should not specify both percentage and pixel units for: ${conflictingUnits.join(", ")}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
{
|
|
1122
|
+
const {
|
|
1123
|
+
collapsedSizePercentage,
|
|
1124
|
+
defaultSizePercentage,
|
|
1125
|
+
maxSizePercentage,
|
|
1126
|
+
minSizePercentage
|
|
1127
|
+
} = computePercentagePanelConstraints(panelConstraints, panelIndex, groupSizePixels);
|
|
1128
|
+
if (minSizePercentage > maxSizePercentage) {
|
|
1129
|
+
warnings.push(`min size (${minSizePercentage}%) should not be greater than max size (${maxSizePercentage}%)`);
|
|
1130
|
+
}
|
|
1131
|
+
if (defaultSizePercentage != null) {
|
|
1132
|
+
if (defaultSizePercentage < 0) {
|
|
1133
|
+
warnings.push("default size should not be less than 0");
|
|
1134
|
+
} else if (defaultSizePercentage < minSizePercentage) {
|
|
1135
|
+
warnings.push("default size should not be less than min size");
|
|
1136
|
+
}
|
|
1137
|
+
if (defaultSizePercentage > 100) {
|
|
1138
|
+
warnings.push("default size should not be greater than 100");
|
|
1139
|
+
} else if (defaultSizePercentage > maxSizePercentage) {
|
|
1140
|
+
warnings.push("default size should not be greater than max size");
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (collapsedSizePercentage > minSizePercentage) {
|
|
1144
|
+
warnings.push("collapsed size should not be greater than min size");
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (warnings.length > 0) {
|
|
1148
|
+
const name = panelId != null ? `Panel "${panelId}"` : "Panel";
|
|
1149
|
+
console.warn(`${name} has an invalid configuration:\n\n${warnings.join("\n")}`);
|
|
1150
|
+
return false;
|
|
768
1151
|
}
|
|
769
|
-
} catch (error) {
|
|
770
|
-
console.error(error);
|
|
771
|
-
storageObject.getItem = () => null;
|
|
772
|
-
storageObject.setItem = () => {};
|
|
773
1152
|
}
|
|
1153
|
+
return true;
|
|
774
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
// All units must be in percentages; pixel values should be pre-converted
|
|
1157
|
+
function validatePanelGroupLayout({
|
|
1158
|
+
groupSizePixels,
|
|
1159
|
+
layout: prevLayout,
|
|
1160
|
+
panelConstraints
|
|
1161
|
+
}) {
|
|
1162
|
+
const nextLayout = [...prevLayout];
|
|
1163
|
+
|
|
1164
|
+
// Validate layout expectations
|
|
1165
|
+
if (nextLayout.length !== panelConstraints.length) {
|
|
1166
|
+
throw Error(`Invalid ${panelConstraints.length} panel layout: ${nextLayout.map(size => `${size}%`).join(", ")}`);
|
|
1167
|
+
} else if (!fuzzyNumbersEqual(nextLayout.reduce((accumulated, current) => accumulated + current, 0), 100)) {
|
|
1168
|
+
// This is not ideal so we should warn about it, but it may be recoverable in some cases
|
|
1169
|
+
// (especially if the amount is small)
|
|
1170
|
+
{
|
|
1171
|
+
console.warn(`WARNING: Invalid layout total size: ${nextLayout.map(size => `${size}%`).join(", ")}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
let remainingSize = 0;
|
|
1175
|
+
|
|
1176
|
+
// First pass: Validate the proposed layout given each panel's constraints
|
|
1177
|
+
for (let index = 0; index < panelConstraints.length; index++) {
|
|
1178
|
+
const unsafeSize = nextLayout[index];
|
|
1179
|
+
const safeSize = resizePanel({
|
|
1180
|
+
groupSizePixels,
|
|
1181
|
+
panelConstraints,
|
|
1182
|
+
panelIndex: index,
|
|
1183
|
+
size: unsafeSize
|
|
1184
|
+
});
|
|
1185
|
+
if (unsafeSize != safeSize) {
|
|
1186
|
+
remainingSize += unsafeSize - safeSize;
|
|
1187
|
+
nextLayout[index] = safeSize;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// If there is additional, left over space, assign it to any panel(s) that permits it
|
|
1192
|
+
// (It's not worth taking multiple additional passes to evenly distribute)
|
|
1193
|
+
if (!fuzzyNumbersEqual(remainingSize, 0)) {
|
|
1194
|
+
for (let index = 0; index < panelConstraints.length; index++) {
|
|
1195
|
+
const prevSize = nextLayout[index];
|
|
1196
|
+
const unsafeSize = prevSize + remainingSize;
|
|
1197
|
+
const safeSize = resizePanel({
|
|
1198
|
+
groupSizePixels,
|
|
1199
|
+
panelConstraints,
|
|
1200
|
+
panelIndex: index,
|
|
1201
|
+
size: unsafeSize
|
|
1202
|
+
});
|
|
1203
|
+
if (prevSize !== safeSize) {
|
|
1204
|
+
remainingSize -= safeSize - prevSize;
|
|
1205
|
+
nextLayout[index] = safeSize;
|
|
1206
|
+
|
|
1207
|
+
// Once we've used up the remainder, bail
|
|
1208
|
+
if (fuzzyNumbersEqual(remainingSize, 0)) {
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return nextLayout;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const LOCAL_STORAGE_DEBOUNCE_INTERVAL = 100;
|
|
775
1218
|
const defaultStorage = {
|
|
776
1219
|
getItem: name => {
|
|
777
1220
|
initializeDefaultStorage(defaultStorage);
|
|
@@ -782,301 +1225,464 @@ const defaultStorage = {
|
|
|
782
1225
|
defaultStorage.setItem(name, value);
|
|
783
1226
|
}
|
|
784
1227
|
};
|
|
785
|
-
|
|
786
|
-
// Initial drag state serves a few purposes:
|
|
787
|
-
// * dragOffset:
|
|
788
|
-
// Resize is calculated by the distance between the current pointer event and the resize handle being "dragged"
|
|
789
|
-
// This value accounts for the initial offset when the touch/click starts, so the handle doesn't appear to "jump"
|
|
790
|
-
// * dragHandleRect, sizes:
|
|
791
|
-
// When resizing is done via mouse/touch event– some initial state is stored
|
|
792
|
-
// so that any panels that contract will also expand if drag direction is reversed.
|
|
793
|
-
// TODO
|
|
794
|
-
// Within an active drag, remember original positions to refine more easily on expand.
|
|
795
|
-
// Look at what the Chrome devtools Sources does.
|
|
1228
|
+
const debounceMap = {};
|
|
796
1229
|
function PanelGroupWithForwardedRef({
|
|
797
1230
|
autoSaveId,
|
|
798
|
-
children
|
|
1231
|
+
children,
|
|
799
1232
|
className: classNameFromProps = "",
|
|
800
1233
|
direction,
|
|
801
|
-
disablePointerEventsDuringResize = false,
|
|
802
1234
|
forwardedRef,
|
|
803
|
-
id: idFromProps
|
|
804
|
-
onLayout,
|
|
1235
|
+
id: idFromProps,
|
|
1236
|
+
onLayout = null,
|
|
1237
|
+
keyboardResizeByPercentage = null,
|
|
1238
|
+
keyboardResizeByPixels = null,
|
|
805
1239
|
storage = defaultStorage,
|
|
806
|
-
style: styleFromProps
|
|
1240
|
+
style: styleFromProps,
|
|
807
1241
|
tagName: Type = "div"
|
|
808
1242
|
}) {
|
|
809
1243
|
const groupId = useUniqueId(idFromProps);
|
|
810
|
-
const [
|
|
811
|
-
const [
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
// This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
|
|
816
|
-
const initialDragStateRef = useRef(null);
|
|
817
|
-
const devWarningsRef = useRef({
|
|
818
|
-
didLogDefaultSizeWarning: false,
|
|
819
|
-
didLogIdAndOrderWarning: false,
|
|
820
|
-
prevPanelIds: []
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
// Use a ref to guard against users passing inline props
|
|
824
|
-
const callbacksRef = useRef({
|
|
825
|
-
onLayout
|
|
826
|
-
});
|
|
827
|
-
useEffect(() => {
|
|
828
|
-
callbacksRef.current.onLayout = onLayout;
|
|
829
|
-
});
|
|
830
|
-
const panelIdToLastNotifiedSizeMapRef = useRef({});
|
|
831
|
-
|
|
832
|
-
// 0-1 values representing the relative size of each panel.
|
|
833
|
-
const [sizes, setSizes] = useState([]);
|
|
834
|
-
|
|
835
|
-
// Used to support imperative collapse/expand API.
|
|
836
|
-
const panelSizeBeforeCollapse = useRef(new Map());
|
|
1244
|
+
const [dragState, setDragState] = useState(null);
|
|
1245
|
+
const [layout, setLayout] = useState([]);
|
|
1246
|
+
const [panelDataArray, setPanelDataArray] = useState([]);
|
|
1247
|
+
const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
|
|
1248
|
+
const panelSizeBeforeCollapseRef = useRef(new Map());
|
|
837
1249
|
const prevDeltaRef = useRef(0);
|
|
838
|
-
|
|
839
|
-
// Store committed values to avoid unnecessarily re-running memoization/effects functions.
|
|
840
1250
|
const committedValuesRef = useRef({
|
|
841
1251
|
direction,
|
|
842
|
-
|
|
843
|
-
|
|
1252
|
+
dragState,
|
|
1253
|
+
id: groupId,
|
|
1254
|
+
keyboardResizeByPercentage,
|
|
1255
|
+
keyboardResizeByPixels,
|
|
1256
|
+
layout,
|
|
1257
|
+
onLayout,
|
|
1258
|
+
panelDataArray
|
|
1259
|
+
});
|
|
1260
|
+
const devWarningsRef = useRef({
|
|
1261
|
+
didLogIdAndOrderWarning: false,
|
|
1262
|
+
didLogPanelConstraintsWarning: false,
|
|
1263
|
+
prevPanelIds: []
|
|
844
1264
|
});
|
|
845
1265
|
useImperativeHandle(forwardedRef, () => ({
|
|
1266
|
+
getId: () => committedValuesRef.current.id,
|
|
846
1267
|
getLayout: () => {
|
|
847
1268
|
const {
|
|
848
|
-
|
|
1269
|
+
id: groupId,
|
|
1270
|
+
layout
|
|
849
1271
|
} = committedValuesRef.current;
|
|
850
|
-
|
|
1272
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1273
|
+
return layout.map(sizePercentage => {
|
|
1274
|
+
return {
|
|
1275
|
+
sizePercentage,
|
|
1276
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1277
|
+
};
|
|
1278
|
+
});
|
|
851
1279
|
},
|
|
852
|
-
setLayout:
|
|
853
|
-
const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
|
|
854
|
-
assert(total === 100, "Panel sizes must add up to 100%");
|
|
1280
|
+
setLayout: mixedSizes => {
|
|
855
1281
|
const {
|
|
856
|
-
|
|
1282
|
+
id: groupId,
|
|
1283
|
+
layout: prevLayout,
|
|
1284
|
+
onLayout,
|
|
1285
|
+
panelDataArray
|
|
857
1286
|
} = committedValuesRef.current;
|
|
858
|
-
const
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
1287
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1288
|
+
const unsafeLayout = mixedSizes.map(mixedSize => getPercentageSizeFromMixedSizes(mixedSize, groupSizePixels));
|
|
1289
|
+
const safeLayout = validatePanelGroupLayout({
|
|
1290
|
+
groupSizePixels,
|
|
1291
|
+
layout: unsafeLayout,
|
|
1292
|
+
panelConstraints: panelDataArray.map(panelData => panelData.constraints)
|
|
1293
|
+
});
|
|
1294
|
+
if (!areEqual(prevLayout, safeLayout)) {
|
|
1295
|
+
setLayout(safeLayout);
|
|
1296
|
+
if (onLayout) {
|
|
1297
|
+
onLayout(safeLayout.map(sizePercentage => ({
|
|
1298
|
+
sizePercentage,
|
|
1299
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1300
|
+
})));
|
|
1301
|
+
}
|
|
1302
|
+
callPanelCallbacks(groupId, panelDataArray, safeLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1303
|
+
}
|
|
862
1304
|
}
|
|
863
1305
|
}), []);
|
|
864
1306
|
useIsomorphicLayoutEffect(() => {
|
|
865
1307
|
committedValuesRef.current.direction = direction;
|
|
866
|
-
committedValuesRef.current.
|
|
867
|
-
committedValuesRef.current.
|
|
1308
|
+
committedValuesRef.current.dragState = dragState;
|
|
1309
|
+
committedValuesRef.current.id = groupId;
|
|
1310
|
+
committedValuesRef.current.layout = layout;
|
|
1311
|
+
committedValuesRef.current.onLayout = onLayout;
|
|
1312
|
+
committedValuesRef.current.panelDataArray = panelDataArray;
|
|
868
1313
|
});
|
|
869
1314
|
useWindowSplitterPanelGroupBehavior({
|
|
870
1315
|
committedValuesRef,
|
|
871
1316
|
groupId,
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
panelSizeBeforeCollapse
|
|
1317
|
+
layout,
|
|
1318
|
+
panelDataArray,
|
|
1319
|
+
setLayout
|
|
876
1320
|
});
|
|
877
|
-
|
|
878
|
-
// Notify external code when sizes have changed.
|
|
879
1321
|
useEffect(() => {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
sizes
|
|
886
|
-
} = committedValuesRef.current;
|
|
1322
|
+
// If this panel has been configured to persist sizing information, save sizes to local storage.
|
|
1323
|
+
if (autoSaveId) {
|
|
1324
|
+
if (layout.length === 0 || layout.length !== panelDataArray.length) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
887
1327
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
onLayout(sizes);
|
|
1328
|
+
// Limit the frequency of localStorage updates.
|
|
1329
|
+
if (!debounceMap[autoSaveId]) {
|
|
1330
|
+
debounceMap[autoSaveId] = debounce(savePanelGroupLayout, LOCAL_STORAGE_DEBOUNCE_INTERVAL);
|
|
892
1331
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
// When possible, we notify before the next render so that rendering work can be batched together.
|
|
896
|
-
// Some cases are difficult to detect though,
|
|
897
|
-
// for example– panels that are conditionally rendered can affect the size of neighboring panels.
|
|
898
|
-
// In this case, the best we can do is notify on commit.
|
|
899
|
-
// The callPanelCallbacks() uses its own memoization to avoid notifying panels twice in these cases.
|
|
900
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
901
|
-
callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
|
|
1332
|
+
debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
|
|
902
1333
|
}
|
|
903
|
-
}, [
|
|
1334
|
+
}, [autoSaveId, layout, panelDataArray, storage]);
|
|
904
1335
|
|
|
905
1336
|
// Once all panels have registered themselves,
|
|
906
1337
|
// Compute the initial sizes based on default weights.
|
|
907
1338
|
// This assumes that panels register during initial mount (no conditional rendering)!
|
|
908
1339
|
useIsomorphicLayoutEffect(() => {
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
1340
|
+
const {
|
|
1341
|
+
id: groupId,
|
|
1342
|
+
layout,
|
|
1343
|
+
onLayout
|
|
1344
|
+
} = committedValuesRef.current;
|
|
1345
|
+
if (layout.length === panelDataArray.length) {
|
|
1346
|
+
// Only compute (or restore) default layout once per panel configuration.
|
|
912
1347
|
return;
|
|
913
1348
|
}
|
|
914
1349
|
|
|
915
1350
|
// If this panel has been configured to persist sizing information,
|
|
916
1351
|
// default size should be restored from local storage if possible.
|
|
917
|
-
let
|
|
1352
|
+
let unsafeLayout = null;
|
|
918
1353
|
if (autoSaveId) {
|
|
919
|
-
|
|
920
|
-
|
|
1354
|
+
unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
|
|
1355
|
+
}
|
|
1356
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1357
|
+
if (unsafeLayout == null) {
|
|
1358
|
+
unsafeLayout = calculateUnsafeDefaultLayout({
|
|
1359
|
+
groupSizePixels,
|
|
1360
|
+
panelDataArray
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Validate even saved layouts in case something has changed since last render
|
|
1365
|
+
// e.g. for pixel groups, this could be the size of the window
|
|
1366
|
+
const validatedLayout = validatePanelGroupLayout({
|
|
1367
|
+
groupSizePixels,
|
|
1368
|
+
layout: unsafeLayout,
|
|
1369
|
+
panelConstraints: panelDataArray.map(panelData => panelData.constraints)
|
|
1370
|
+
});
|
|
1371
|
+
if (!areEqual(layout, validatedLayout)) {
|
|
1372
|
+
setLayout(validatedLayout);
|
|
1373
|
+
}
|
|
1374
|
+
if (onLayout) {
|
|
1375
|
+
onLayout(validatedLayout.map(sizePercentage => ({
|
|
1376
|
+
sizePercentage,
|
|
1377
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1378
|
+
})));
|
|
921
1379
|
}
|
|
922
|
-
|
|
923
|
-
|
|
1380
|
+
callPanelCallbacks(groupId, panelDataArray, validatedLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1381
|
+
}, [autoSaveId, layout, panelDataArray, storage]);
|
|
1382
|
+
useIsomorphicLayoutEffect(() => {
|
|
1383
|
+
const constraints = panelDataArray.map(({
|
|
1384
|
+
constraints
|
|
1385
|
+
}) => constraints);
|
|
1386
|
+
if (!shouldMonitorPixelBasedConstraints(constraints)) {
|
|
1387
|
+
// Avoid the overhead of ResizeObserver if no pixel constraints require monitoring
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (typeof ResizeObserver === "undefined") {
|
|
1391
|
+
console.warn(`WARNING: Pixel based constraints require ResizeObserver but it is not supported by the current browser.`);
|
|
924
1392
|
} else {
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1393
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1394
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1395
|
+
const {
|
|
1396
|
+
layout: prevLayout,
|
|
1397
|
+
onLayout
|
|
1398
|
+
} = committedValuesRef.current;
|
|
1399
|
+
const nextLayout = validatePanelGroupLayout({
|
|
1400
|
+
groupSizePixels,
|
|
1401
|
+
layout: prevLayout,
|
|
1402
|
+
panelConstraints: panelDataArray.map(panelData => panelData.constraints)
|
|
1403
|
+
});
|
|
1404
|
+
if (!areEqual(prevLayout, nextLayout)) {
|
|
1405
|
+
setLayout(nextLayout);
|
|
1406
|
+
if (onLayout) {
|
|
1407
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1408
|
+
sizePercentage,
|
|
1409
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1410
|
+
})));
|
|
1411
|
+
}
|
|
1412
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
941
1413
|
}
|
|
942
1414
|
});
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
} else if (totalMinSize > 100) {
|
|
948
|
-
throw new Error(`Minimum panel sizes cannot exceed 100%`);
|
|
949
|
-
}
|
|
950
|
-
setSizes(panelsArray.map(panel => {
|
|
951
|
-
if (panel.current.defaultSize === null) {
|
|
952
|
-
return (100 - totalDefaultSize) / panelsWithNullDefaultSize;
|
|
953
|
-
}
|
|
954
|
-
return panel.current.defaultSize;
|
|
955
|
-
}));
|
|
1415
|
+
resizeObserver.observe(getPanelGroupElement(groupId));
|
|
1416
|
+
return () => {
|
|
1417
|
+
resizeObserver.disconnect();
|
|
1418
|
+
};
|
|
956
1419
|
}
|
|
957
|
-
}, [
|
|
958
|
-
useEffect(() => {
|
|
959
|
-
// If this panel has been configured to persist sizing information, save sizes to local storage.
|
|
960
|
-
if (autoSaveId) {
|
|
961
|
-
if (sizes.length === 0 || sizes.length !== panels.size) {
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
1420
|
+
}, [groupId, panelDataArray]);
|
|
965
1421
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
debounceMap[autoSaveId] = debounce(savePanelGroupLayout, 100);
|
|
969
|
-
}
|
|
970
|
-
debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
|
|
971
|
-
}
|
|
1422
|
+
// DEV warnings
|
|
1423
|
+
useEffect(() => {
|
|
972
1424
|
{
|
|
973
1425
|
const {
|
|
974
1426
|
didLogIdAndOrderWarning,
|
|
1427
|
+
didLogPanelConstraintsWarning,
|
|
975
1428
|
prevPanelIds
|
|
976
1429
|
} = devWarningsRef.current;
|
|
977
1430
|
if (!didLogIdAndOrderWarning) {
|
|
978
1431
|
const {
|
|
979
|
-
|
|
1432
|
+
panelDataArray
|
|
980
1433
|
} = committedValuesRef.current;
|
|
981
|
-
const panelIds =
|
|
1434
|
+
const panelIds = panelDataArray.map(({
|
|
1435
|
+
id
|
|
1436
|
+
}) => id);
|
|
982
1437
|
devWarningsRef.current.prevPanelIds = panelIds;
|
|
983
1438
|
const panelsHaveChanged = prevPanelIds.length > 0 && !areEqual(prevPanelIds, panelIds);
|
|
984
1439
|
if (panelsHaveChanged) {
|
|
985
|
-
if (
|
|
1440
|
+
if (panelDataArray.find(({
|
|
1441
|
+
idIsFromProps,
|
|
1442
|
+
order
|
|
1443
|
+
}) => !idIsFromProps || order == null)) {
|
|
986
1444
|
devWarningsRef.current.didLogIdAndOrderWarning = true;
|
|
987
1445
|
console.warn(`WARNING: Panel id and order props recommended when panels are dynamically rendered`);
|
|
988
1446
|
}
|
|
989
1447
|
}
|
|
990
1448
|
}
|
|
1449
|
+
if (!didLogPanelConstraintsWarning) {
|
|
1450
|
+
const panelConstraints = panelDataArray.map(panelData => panelData.constraints);
|
|
1451
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1452
|
+
for (let panelIndex = 0; panelIndex < panelConstraints.length; panelIndex++) {
|
|
1453
|
+
const isValid = validatePanelConstraints({
|
|
1454
|
+
groupSizePixels,
|
|
1455
|
+
panelConstraints,
|
|
1456
|
+
panelId: panelDataArray[panelIndex].id,
|
|
1457
|
+
panelIndex
|
|
1458
|
+
});
|
|
1459
|
+
if (!isValid) {
|
|
1460
|
+
devWarningsRef.current.didLogPanelConstraintsWarning = true;
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
991
1465
|
}
|
|
992
|
-
}
|
|
993
|
-
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
// External APIs are safe to memoize via committed values ref
|
|
1469
|
+
const collapsePanel = useCallback(panelData => {
|
|
994
1470
|
const {
|
|
995
|
-
|
|
1471
|
+
layout: prevLayout,
|
|
1472
|
+
onLayout,
|
|
1473
|
+
panelDataArray
|
|
996
1474
|
} = committedValuesRef.current;
|
|
1475
|
+
if (panelData.constraints.collapsible) {
|
|
1476
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1477
|
+
const {
|
|
1478
|
+
collapsedSizePercentage,
|
|
1479
|
+
panelSizePercentage,
|
|
1480
|
+
pivotIndices,
|
|
1481
|
+
groupSizePixels
|
|
1482
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
|
|
1483
|
+
if (panelSizePercentage !== collapsedSizePercentage) {
|
|
1484
|
+
// Store size before collapse;
|
|
1485
|
+
// This is the size that gets restored if the expand() API is used.
|
|
1486
|
+
panelSizeBeforeCollapseRef.current.set(panelData.id, panelSizePercentage);
|
|
1487
|
+
const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
|
|
1488
|
+
const delta = isLastPanel ? panelSizePercentage - collapsedSizePercentage : collapsedSizePercentage - panelSizePercentage;
|
|
1489
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1490
|
+
delta,
|
|
1491
|
+
groupSizePixels,
|
|
1492
|
+
layout: prevLayout,
|
|
1493
|
+
panelConstraints: panelConstraintsArray,
|
|
1494
|
+
pivotIndices,
|
|
1495
|
+
trigger: "imperative-api"
|
|
1496
|
+
});
|
|
1497
|
+
if (!compareLayouts(prevLayout, nextLayout)) {
|
|
1498
|
+
setLayout(nextLayout);
|
|
1499
|
+
if (onLayout) {
|
|
1500
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1501
|
+
sizePercentage,
|
|
1502
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1503
|
+
})));
|
|
1504
|
+
}
|
|
1505
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}, [groupId]);
|
|
997
1510
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1511
|
+
// External APIs are safe to memoize via committed values ref
|
|
1512
|
+
const expandPanel = useCallback(panelData => {
|
|
1513
|
+
const {
|
|
1514
|
+
layout: prevLayout,
|
|
1515
|
+
onLayout,
|
|
1516
|
+
panelDataArray
|
|
1517
|
+
} = committedValuesRef.current;
|
|
1518
|
+
if (panelData.constraints.collapsible) {
|
|
1519
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1520
|
+
const {
|
|
1521
|
+
collapsedSizePercentage,
|
|
1522
|
+
panelSizePercentage,
|
|
1523
|
+
minSizePercentage,
|
|
1524
|
+
pivotIndices,
|
|
1525
|
+
groupSizePixels
|
|
1526
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
|
|
1527
|
+
if (panelSizePercentage === collapsedSizePercentage) {
|
|
1528
|
+
// Restore this panel to the size it was before it was collapsed, if possible.
|
|
1529
|
+
const prevPanelSizePercentage = panelSizeBeforeCollapseRef.current.get(panelData.id);
|
|
1530
|
+
const baseSizePercentage = prevPanelSizePercentage != null ? prevPanelSizePercentage : minSizePercentage;
|
|
1531
|
+
const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
|
|
1532
|
+
const delta = isLastPanel ? panelSizePercentage - baseSizePercentage : baseSizePercentage - panelSizePercentage;
|
|
1533
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1534
|
+
delta,
|
|
1535
|
+
groupSizePixels,
|
|
1536
|
+
layout: prevLayout,
|
|
1537
|
+
panelConstraints: panelConstraintsArray,
|
|
1538
|
+
pivotIndices,
|
|
1539
|
+
trigger: "imperative-api"
|
|
1540
|
+
});
|
|
1541
|
+
if (!compareLayouts(prevLayout, nextLayout)) {
|
|
1542
|
+
setLayout(nextLayout);
|
|
1543
|
+
if (onLayout) {
|
|
1544
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1545
|
+
sizePercentage,
|
|
1546
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1547
|
+
})));
|
|
1548
|
+
}
|
|
1549
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1550
|
+
}
|
|
1004
1551
|
}
|
|
1005
|
-
return {
|
|
1006
|
-
flexBasis: 0,
|
|
1007
|
-
flexGrow: defaultSize != null ? defaultSize : undefined,
|
|
1008
|
-
flexShrink: 1,
|
|
1009
|
-
// Without this, Panel sizes may be unintentionally overridden by their content.
|
|
1010
|
-
overflow: "hidden"
|
|
1011
|
-
};
|
|
1012
1552
|
}
|
|
1013
|
-
|
|
1553
|
+
}, [groupId]);
|
|
1554
|
+
|
|
1555
|
+
// External APIs are safe to memoize via committed values ref
|
|
1556
|
+
const getPanelSize = useCallback(panelData => {
|
|
1557
|
+
const {
|
|
1558
|
+
layout,
|
|
1559
|
+
panelDataArray
|
|
1560
|
+
} = committedValuesRef.current;
|
|
1561
|
+
const {
|
|
1562
|
+
panelSizePercentage,
|
|
1563
|
+
panelSizePixels
|
|
1564
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, layout);
|
|
1014
1565
|
return {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
flexShrink: 1,
|
|
1018
|
-
// Without this, Panel sizes may be unintentionally overridden by their content.
|
|
1019
|
-
overflow: "hidden",
|
|
1020
|
-
// Disable pointer events inside of a panel during resize.
|
|
1021
|
-
// This avoid edge cases like nested iframes.
|
|
1022
|
-
pointerEvents: disablePointerEventsDuringResize && activeHandleId !== null ? "none" : undefined
|
|
1566
|
+
sizePercentage: panelSizePercentage,
|
|
1567
|
+
sizePixels: panelSizePixels
|
|
1023
1568
|
};
|
|
1024
|
-
}, [
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1569
|
+
}, [groupId]);
|
|
1570
|
+
|
|
1571
|
+
// This API should never read from committedValuesRef
|
|
1572
|
+
const getPanelStyle = useCallback(panelData => {
|
|
1573
|
+
const panelIndex = panelDataArray.indexOf(panelData);
|
|
1574
|
+
return computePanelFlexBoxStyle({
|
|
1575
|
+
dragState,
|
|
1576
|
+
layout,
|
|
1577
|
+
panelData: panelDataArray,
|
|
1578
|
+
panelIndex
|
|
1579
|
+
});
|
|
1580
|
+
}, [dragState, layout, panelDataArray]);
|
|
1581
|
+
|
|
1582
|
+
// External APIs are safe to memoize via committed values ref
|
|
1583
|
+
const isPanelCollapsed = useCallback(panelData => {
|
|
1584
|
+
const {
|
|
1585
|
+
layout,
|
|
1586
|
+
panelDataArray
|
|
1587
|
+
} = committedValuesRef.current;
|
|
1588
|
+
const {
|
|
1589
|
+
collapsedSizePercentage,
|
|
1590
|
+
collapsible,
|
|
1591
|
+
panelSizePercentage
|
|
1592
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, layout);
|
|
1593
|
+
return collapsible === true && panelSizePercentage === collapsedSizePercentage;
|
|
1594
|
+
}, [groupId]);
|
|
1595
|
+
|
|
1596
|
+
// External APIs are safe to memoize via committed values ref
|
|
1597
|
+
const isPanelExpanded = useCallback(panelData => {
|
|
1598
|
+
const {
|
|
1599
|
+
layout,
|
|
1600
|
+
panelDataArray
|
|
1601
|
+
} = committedValuesRef.current;
|
|
1602
|
+
const {
|
|
1603
|
+
collapsedSizePercentage,
|
|
1604
|
+
collapsible,
|
|
1605
|
+
panelSizePercentage
|
|
1606
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, layout);
|
|
1607
|
+
return !collapsible || panelSizePercentage > collapsedSizePercentage;
|
|
1608
|
+
}, [groupId]);
|
|
1609
|
+
const registerPanel = useCallback(panelData => {
|
|
1610
|
+
setPanelDataArray(prevPanelDataArray => {
|
|
1611
|
+
const nextPanelDataArray = [...prevPanelDataArray, panelData];
|
|
1612
|
+
return nextPanelDataArray.sort((panelA, panelB) => {
|
|
1613
|
+
const orderA = panelA.order;
|
|
1614
|
+
const orderB = panelB.order;
|
|
1615
|
+
if (orderA == null && orderB == null) {
|
|
1616
|
+
return 0;
|
|
1617
|
+
} else if (orderA == null) {
|
|
1618
|
+
return -1;
|
|
1619
|
+
} else if (orderB == null) {
|
|
1620
|
+
return 1;
|
|
1621
|
+
} else {
|
|
1622
|
+
return orderA - orderB;
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1033
1625
|
});
|
|
1034
1626
|
}, []);
|
|
1035
|
-
const registerResizeHandle = useCallback(
|
|
1036
|
-
|
|
1627
|
+
const registerResizeHandle = useCallback(dragHandleId => {
|
|
1628
|
+
return function resizeHandler(event) {
|
|
1037
1629
|
event.preventDefault();
|
|
1038
1630
|
const {
|
|
1039
1631
|
direction,
|
|
1040
|
-
|
|
1041
|
-
|
|
1632
|
+
dragState,
|
|
1633
|
+
id: groupId,
|
|
1634
|
+
keyboardResizeByPercentage,
|
|
1635
|
+
keyboardResizeByPixels,
|
|
1636
|
+
onLayout,
|
|
1637
|
+
panelDataArray,
|
|
1638
|
+
layout: prevLayout
|
|
1042
1639
|
} = committedValuesRef.current;
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1640
|
+
const {
|
|
1641
|
+
initialLayout
|
|
1642
|
+
} = dragState ?? {};
|
|
1643
|
+
const pivotIndices = determinePivotIndices(groupId, dragHandleId);
|
|
1644
|
+
let delta = calculateDeltaPercentage(event, groupId, dragHandleId, direction, dragState, {
|
|
1645
|
+
percentage: keyboardResizeByPercentage,
|
|
1646
|
+
pixels: keyboardResizeByPixels
|
|
1647
|
+
});
|
|
1648
|
+
if (delta === 0) {
|
|
1050
1649
|
return;
|
|
1051
1650
|
}
|
|
1052
|
-
const groupElement = getPanelGroup(groupId);
|
|
1053
|
-
const rect = groupElement.getBoundingClientRect();
|
|
1054
|
-
const isHorizontal = direction === "horizontal";
|
|
1055
1651
|
|
|
1056
1652
|
// Support RTL layouts
|
|
1653
|
+
const isHorizontal = direction === "horizontal";
|
|
1057
1654
|
if (document.dir === "rtl" && isHorizontal) {
|
|
1058
|
-
|
|
1655
|
+
delta = -delta;
|
|
1059
1656
|
}
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1657
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1658
|
+
const panelConstraints = panelDataArray.map(panelData => panelData.constraints);
|
|
1659
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1660
|
+
delta,
|
|
1661
|
+
groupSizePixels,
|
|
1662
|
+
layout: initialLayout ?? prevLayout,
|
|
1663
|
+
panelConstraints,
|
|
1664
|
+
pivotIndices,
|
|
1665
|
+
trigger: isKeyDown(event) ? "keyboard" : "mouse-or-touch"
|
|
1666
|
+
});
|
|
1667
|
+
const layoutChanged = !compareLayouts(prevLayout, nextLayout);
|
|
1064
1668
|
|
|
1065
|
-
//
|
|
1669
|
+
// Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
|
|
1670
|
+
// Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
|
|
1066
1671
|
if (isMouseEvent(event) || isTouchEvent(event)) {
|
|
1067
1672
|
// Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
|
|
1068
1673
|
// In this case, Panel sizes might not change–
|
|
1069
1674
|
// but updating cursor in this scenario would cause a flicker.
|
|
1070
1675
|
if (prevDeltaRef.current != delta) {
|
|
1071
|
-
|
|
1676
|
+
prevDeltaRef.current = delta;
|
|
1677
|
+
if (!layoutChanged) {
|
|
1072
1678
|
// If the pointer has moved too far to resize the panel any further,
|
|
1073
1679
|
// update the cursor style for a visual clue.
|
|
1074
1680
|
// This mimics VS Code behavior.
|
|
1075
1681
|
|
|
1076
1682
|
if (isHorizontal) {
|
|
1077
|
-
setGlobalCursorStyle(
|
|
1683
|
+
setGlobalCursorStyle(delta < 0 ? "horizontal-min" : "horizontal-max");
|
|
1078
1684
|
} else {
|
|
1079
|
-
setGlobalCursorStyle(
|
|
1685
|
+
setGlobalCursorStyle(delta < 0 ? "vertical-min" : "vertical-max");
|
|
1080
1686
|
}
|
|
1081
1687
|
} else {
|
|
1082
1688
|
// Reset the cursor style to the the normal resize cursor.
|
|
@@ -1084,185 +1690,100 @@ function PanelGroupWithForwardedRef({
|
|
|
1084
1690
|
}
|
|
1085
1691
|
}
|
|
1086
1692
|
}
|
|
1087
|
-
if (
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1693
|
+
if (layoutChanged) {
|
|
1694
|
+
setLayout(nextLayout);
|
|
1695
|
+
if (onLayout) {
|
|
1696
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1697
|
+
sizePercentage,
|
|
1698
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1699
|
+
})));
|
|
1700
|
+
}
|
|
1701
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1094
1702
|
}
|
|
1095
|
-
prevDeltaRef.current = delta;
|
|
1096
1703
|
};
|
|
1097
|
-
return resizeHandler;
|
|
1098
|
-
}, [groupId]);
|
|
1099
|
-
const unregisterPanel = useCallback(id => {
|
|
1100
|
-
setPanels(prevPanels => {
|
|
1101
|
-
if (!prevPanels.has(id)) {
|
|
1102
|
-
return prevPanels;
|
|
1103
|
-
}
|
|
1104
|
-
const nextPanels = new Map(prevPanels);
|
|
1105
|
-
nextPanels.delete(id);
|
|
1106
|
-
return nextPanels;
|
|
1107
|
-
});
|
|
1108
1704
|
}, []);
|
|
1109
|
-
|
|
1705
|
+
|
|
1706
|
+
// External APIs are safe to memoize via committed values ref
|
|
1707
|
+
const resizePanel = useCallback((panelData, mixedSizes) => {
|
|
1110
1708
|
const {
|
|
1111
|
-
|
|
1112
|
-
|
|
1709
|
+
layout: prevLayout,
|
|
1710
|
+
onLayout,
|
|
1711
|
+
panelDataArray
|
|
1113
1712
|
} = committedValuesRef.current;
|
|
1114
|
-
const
|
|
1115
|
-
if (panel == null) {
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1713
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1118
1714
|
const {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
if (prevSizes !== nextSizes) {
|
|
1144
|
-
const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
|
|
1145
|
-
setSizes(nextSizes);
|
|
1146
|
-
|
|
1147
|
-
// If resize change handlers have been declared, this is the time to call them.
|
|
1148
|
-
// Trigger user callbacks after updating state, so that user code can override the sizes.
|
|
1149
|
-
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
|
|
1715
|
+
groupSizePixels,
|
|
1716
|
+
panelSizePercentage,
|
|
1717
|
+
pivotIndices
|
|
1718
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
|
|
1719
|
+
const sizePercentage = getPercentageSizeFromMixedSizes(mixedSizes, groupSizePixels);
|
|
1720
|
+
const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
|
|
1721
|
+
const delta = isLastPanel ? panelSizePercentage - sizePercentage : sizePercentage - panelSizePercentage;
|
|
1722
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1723
|
+
delta,
|
|
1724
|
+
groupSizePixels,
|
|
1725
|
+
layout: prevLayout,
|
|
1726
|
+
panelConstraints: panelConstraintsArray,
|
|
1727
|
+
pivotIndices,
|
|
1728
|
+
trigger: "imperative-api"
|
|
1729
|
+
});
|
|
1730
|
+
if (!compareLayouts(prevLayout, nextLayout)) {
|
|
1731
|
+
setLayout(nextLayout);
|
|
1732
|
+
if (onLayout) {
|
|
1733
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1734
|
+
sizePercentage,
|
|
1735
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1736
|
+
})));
|
|
1737
|
+
}
|
|
1738
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1150
1739
|
}
|
|
1151
|
-
}, []);
|
|
1152
|
-
const
|
|
1740
|
+
}, [groupId]);
|
|
1741
|
+
const startDragging = useCallback((dragHandleId, event) => {
|
|
1153
1742
|
const {
|
|
1154
|
-
|
|
1155
|
-
|
|
1743
|
+
direction,
|
|
1744
|
+
layout
|
|
1156
1745
|
} = committedValuesRef.current;
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1165
|
-
const sizeBeforeCollapse = panelSizeBeforeCollapse.current.get(id) || minSize;
|
|
1166
|
-
if (!sizeBeforeCollapse) {
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
1170
|
-
const index = panelsArray.indexOf(panel);
|
|
1171
|
-
if (index < 0) {
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
const currentSize = prevSizes[index];
|
|
1175
|
-
if (currentSize !== collapsedSize) {
|
|
1176
|
-
// Panel is already expanded.
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
|
|
1180
|
-
if (idBefore == null || idAfter == null) {
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
const isLastPanel = index === panelsArray.length - 1;
|
|
1184
|
-
const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
|
|
1185
|
-
const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
|
|
1186
|
-
if (prevSizes !== nextSizes) {
|
|
1187
|
-
const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
|
|
1188
|
-
setSizes(nextSizes);
|
|
1189
|
-
|
|
1190
|
-
// If resize change handlers have been declared, this is the time to call them.
|
|
1191
|
-
// Trigger user callbacks after updating state, so that user code can override the sizes.
|
|
1192
|
-
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
|
|
1193
|
-
}
|
|
1746
|
+
const handleElement = getResizeHandleElement(dragHandleId);
|
|
1747
|
+
const initialCursorPosition = getResizeEventCursorPosition(direction, event);
|
|
1748
|
+
setDragState({
|
|
1749
|
+
dragHandleId,
|
|
1750
|
+
dragHandleRect: handleElement.getBoundingClientRect(),
|
|
1751
|
+
initialCursorPosition,
|
|
1752
|
+
initialLayout: layout
|
|
1753
|
+
});
|
|
1194
1754
|
}, []);
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
} = panel.current;
|
|
1210
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
1211
|
-
const index = panelsArray.indexOf(panel);
|
|
1212
|
-
if (index < 0) {
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
const currentSize = prevSizes[index];
|
|
1216
|
-
if (currentSize === nextSize) {
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
if (collapsible && nextSize === collapsedSize) ; else {
|
|
1220
|
-
nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
|
|
1221
|
-
}
|
|
1222
|
-
const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
|
|
1223
|
-
if (idBefore == null || idAfter == null) {
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
const isLastPanel = index === panelsArray.length - 1;
|
|
1227
|
-
const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
|
|
1228
|
-
const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
|
|
1229
|
-
if (prevSizes !== nextSizes) {
|
|
1230
|
-
const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
|
|
1231
|
-
setSizes(nextSizes);
|
|
1232
|
-
|
|
1233
|
-
// If resize change handlers have been declared, this is the time to call them.
|
|
1234
|
-
// Trigger user callbacks after updating state, so that user code can override the sizes.
|
|
1235
|
-
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
|
|
1236
|
-
}
|
|
1755
|
+
const stopDragging = useCallback(() => {
|
|
1756
|
+
resetGlobalCursorStyle();
|
|
1757
|
+
setDragState(null);
|
|
1758
|
+
}, []);
|
|
1759
|
+
const unregisterPanel = useCallback(panelData => {
|
|
1760
|
+
delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
|
|
1761
|
+
setPanelDataArray(panelDataArray => {
|
|
1762
|
+
const index = panelDataArray.indexOf(panelData);
|
|
1763
|
+
if (index >= 0) {
|
|
1764
|
+
panelDataArray = [...panelDataArray];
|
|
1765
|
+
panelDataArray.splice(index, 1);
|
|
1766
|
+
}
|
|
1767
|
+
return panelDataArray;
|
|
1768
|
+
});
|
|
1237
1769
|
}, []);
|
|
1238
1770
|
const context = useMemo(() => ({
|
|
1239
|
-
activeHandleId,
|
|
1240
1771
|
collapsePanel,
|
|
1241
1772
|
direction,
|
|
1773
|
+
dragState,
|
|
1242
1774
|
expandPanel,
|
|
1775
|
+
getPanelSize,
|
|
1243
1776
|
getPanelStyle,
|
|
1244
1777
|
groupId,
|
|
1778
|
+
isPanelCollapsed,
|
|
1779
|
+
isPanelExpanded,
|
|
1245
1780
|
registerPanel,
|
|
1246
1781
|
registerResizeHandle,
|
|
1247
1782
|
resizePanel,
|
|
1248
|
-
startDragging
|
|
1249
|
-
|
|
1250
|
-
if (isMouseEvent(event) || isTouchEvent(event)) {
|
|
1251
|
-
const handleElement = getResizeHandle(id);
|
|
1252
|
-
initialDragStateRef.current = {
|
|
1253
|
-
dragHandleRect: handleElement.getBoundingClientRect(),
|
|
1254
|
-
dragOffset: getDragOffset(event, id, direction),
|
|
1255
|
-
sizes: committedValuesRef.current.sizes
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
},
|
|
1259
|
-
stopDragging: () => {
|
|
1260
|
-
resetGlobalCursorStyle();
|
|
1261
|
-
setActiveHandleId(null);
|
|
1262
|
-
initialDragStateRef.current = null;
|
|
1263
|
-
},
|
|
1783
|
+
startDragging,
|
|
1784
|
+
stopDragging,
|
|
1264
1785
|
unregisterPanel
|
|
1265
|
-
}), [
|
|
1786
|
+
}), [collapsePanel, dragState, direction, expandPanel, getPanelSize, getPanelStyle, groupId, isPanelCollapsed, isPanelExpanded, registerPanel, registerResizeHandle, resizePanel, startDragging, stopDragging, unregisterPanel]);
|
|
1266
1787
|
const style = {
|
|
1267
1788
|
display: "flex",
|
|
1268
1789
|
flexDirection: direction === "horizontal" ? "row" : "column",
|
|
@@ -1271,19 +1792,20 @@ function PanelGroupWithForwardedRef({
|
|
|
1271
1792
|
width: "100%"
|
|
1272
1793
|
};
|
|
1273
1794
|
return createElement(PanelGroupContext.Provider, {
|
|
1274
|
-
children: createElement(Type, {
|
|
1275
|
-
children,
|
|
1276
|
-
className: classNameFromProps,
|
|
1277
|
-
"data-panel-group": "",
|
|
1278
|
-
"data-panel-group-direction": direction,
|
|
1279
|
-
"data-panel-group-id": groupId,
|
|
1280
|
-
style: {
|
|
1281
|
-
...style,
|
|
1282
|
-
...styleFromProps
|
|
1283
|
-
}
|
|
1284
|
-
}),
|
|
1285
1795
|
value: context
|
|
1286
|
-
}
|
|
1796
|
+
}, createElement(Type, {
|
|
1797
|
+
children,
|
|
1798
|
+
className: classNameFromProps,
|
|
1799
|
+
style: {
|
|
1800
|
+
...style,
|
|
1801
|
+
...styleFromProps
|
|
1802
|
+
},
|
|
1803
|
+
// CSS selectors
|
|
1804
|
+
"data-panel-group": "",
|
|
1805
|
+
// e2e test attributes
|
|
1806
|
+
"data-panel-group-direction": direction ,
|
|
1807
|
+
"data-panel-group-id": groupId
|
|
1808
|
+
}));
|
|
1287
1809
|
}
|
|
1288
1810
|
const PanelGroup = forwardRef((props, ref) => createElement(PanelGroupWithForwardedRef, {
|
|
1289
1811
|
...props,
|
|
@@ -1291,6 +1813,77 @@ const PanelGroup = forwardRef((props, ref) => createElement(PanelGroupWithForwar
|
|
|
1291
1813
|
}));
|
|
1292
1814
|
PanelGroupWithForwardedRef.displayName = "PanelGroup";
|
|
1293
1815
|
PanelGroup.displayName = "forwardRef(PanelGroup)";
|
|
1816
|
+
function panelDataHelper(groupId, panelDataArray, panelData, layout) {
|
|
1817
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1818
|
+
const panelIndex = panelDataArray.indexOf(panelData);
|
|
1819
|
+
const panelConstraints = panelConstraintsArray[panelIndex];
|
|
1820
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1821
|
+
const percentagePanelConstraints = computePercentagePanelConstraints(panelConstraintsArray, panelIndex, groupSizePixels);
|
|
1822
|
+
const isLastPanel = panelIndex === panelDataArray.length - 1;
|
|
1823
|
+
const pivotIndices = isLastPanel ? [panelIndex - 1, panelIndex] : [panelIndex, panelIndex + 1];
|
|
1824
|
+
const panelSizePercentage = layout[panelIndex];
|
|
1825
|
+
const panelSizePixels = convertPercentageToPixels(panelSizePercentage, groupSizePixels);
|
|
1826
|
+
return {
|
|
1827
|
+
...percentagePanelConstraints,
|
|
1828
|
+
collapsible: panelConstraints.collapsible,
|
|
1829
|
+
panelSizePercentage,
|
|
1830
|
+
panelSizePixels,
|
|
1831
|
+
groupSizePixels,
|
|
1832
|
+
pivotIndices
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
1837
|
+
|
|
1838
|
+
function useWindowSplitterResizeHandlerBehavior({
|
|
1839
|
+
disabled,
|
|
1840
|
+
handleId,
|
|
1841
|
+
resizeHandler
|
|
1842
|
+
}) {
|
|
1843
|
+
useEffect(() => {
|
|
1844
|
+
if (disabled || resizeHandler == null) {
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
const handleElement = getResizeHandleElement(handleId);
|
|
1848
|
+
if (handleElement == null) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
const onKeyDown = event => {
|
|
1852
|
+
if (event.defaultPrevented) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
switch (event.key) {
|
|
1856
|
+
case "ArrowDown":
|
|
1857
|
+
case "ArrowLeft":
|
|
1858
|
+
case "ArrowRight":
|
|
1859
|
+
case "ArrowUp":
|
|
1860
|
+
case "End":
|
|
1861
|
+
case "Home":
|
|
1862
|
+
{
|
|
1863
|
+
event.preventDefault();
|
|
1864
|
+
resizeHandler(event);
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
case "F6":
|
|
1868
|
+
{
|
|
1869
|
+
event.preventDefault();
|
|
1870
|
+
const groupId = handleElement.getAttribute("data-panel-group-id");
|
|
1871
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
1872
|
+
const index = getResizeHandleElementIndex(groupId, handleId);
|
|
1873
|
+
assert(index !== null);
|
|
1874
|
+
const nextIndex = event.shiftKey ? index > 0 ? index - 1 : handles.length - 1 : index + 1 < handles.length ? index + 1 : 0;
|
|
1875
|
+
const nextHandle = handles[nextIndex];
|
|
1876
|
+
nextHandle.focus();
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
handleElement.addEventListener("keydown", onKeyDown);
|
|
1882
|
+
return () => {
|
|
1883
|
+
handleElement.removeEventListener("keydown", onKeyDown);
|
|
1884
|
+
};
|
|
1885
|
+
}, [disabled, handleId, resizeHandler]);
|
|
1886
|
+
}
|
|
1294
1887
|
|
|
1295
1888
|
function PanelResizeHandle({
|
|
1296
1889
|
children = null,
|
|
@@ -1315,15 +1908,15 @@ function PanelResizeHandle({
|
|
|
1315
1908
|
throw Error(`PanelResizeHandle components must be rendered within a PanelGroup container`);
|
|
1316
1909
|
}
|
|
1317
1910
|
const {
|
|
1318
|
-
activeHandleId,
|
|
1319
1911
|
direction,
|
|
1912
|
+
dragState,
|
|
1320
1913
|
groupId,
|
|
1321
1914
|
registerResizeHandle,
|
|
1322
1915
|
startDragging,
|
|
1323
1916
|
stopDragging
|
|
1324
1917
|
} = panelGroupContext;
|
|
1325
1918
|
const resizeHandleId = useUniqueId(idFromProps);
|
|
1326
|
-
const isDragging =
|
|
1919
|
+
const isDragging = dragState?.dragHandleId === resizeHandleId;
|
|
1327
1920
|
const [isFocused, setIsFocused] = useState(false);
|
|
1328
1921
|
const [resizeHandler, setResizeHandler] = useState(null);
|
|
1329
1922
|
const stopDraggingAndBlur = useCallback(() => {
|
|
@@ -1387,6 +1980,8 @@ function PanelResizeHandle({
|
|
|
1387
1980
|
return createElement(Type, {
|
|
1388
1981
|
children,
|
|
1389
1982
|
className: classNameFromProps,
|
|
1983
|
+
// CSS selectors
|
|
1984
|
+
"data-resize-handle": "",
|
|
1390
1985
|
"data-resize-handle-active": isDragging ? "pointer" : isFocused ? "keyboard" : undefined,
|
|
1391
1986
|
"data-panel-group-direction": direction,
|
|
1392
1987
|
"data-panel-group-id": groupId,
|