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