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