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