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