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