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