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