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