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