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