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