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