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