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