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