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