Add Adaptive Splitter Component and Update Styles
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a new AdaptiveSplitter component to enhance layout flexibility in the dashboard, allowing for dynamic resizing of panels. - Updated App.css to include styles for the new adaptive splitter, ensuring consistent appearance and behavior across different orientations. - Refactored DashboardSidebarSplitter to utilize the AdaptiveSplitter, improving sidebar management and responsiveness during resizing. - Adjusted transition properties and sizes for smoother user experience when interacting with the splitter components.
This commit is contained in:
parent
32168be911
commit
1b98f6a716
@ -37,6 +37,7 @@
|
|||||||
.ant-badge,
|
.ant-badge,
|
||||||
.ant-select-dropdown,
|
.ant-select-dropdown,
|
||||||
.ant-splitter,
|
.ant-splitter,
|
||||||
|
.adaptive-splitter,
|
||||||
.ant-steps-item-title,
|
.ant-steps-item-title,
|
||||||
.ant-steps-icon,
|
.ant-steps-icon,
|
||||||
.ant-color-picker-presets-label,
|
.ant-color-picker-presets-label,
|
||||||
@ -528,11 +529,13 @@ body {
|
|||||||
width: 35%;
|
width: 35%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmcontrol-splitter > .ant-splitter-bar {
|
.farmcontrol-splitter > .ant-splitter-bar,
|
||||||
|
.farmcontrol-splitter > .adaptive-splitter-bar {
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmcontrol-splitter.large > .ant-splitter-bar {
|
.farmcontrol-splitter.large > .ant-splitter-bar,
|
||||||
|
.farmcontrol-splitter.large > .adaptive-splitter-bar {
|
||||||
margin: 11px;
|
margin: 11px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
@ -938,41 +941,18 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
||||||
right: -1px;
|
top: 8px;
|
||||||
width: 0px !important;
|
right: calc(-1 * var(--scrollbox-vertical-right-padding) - 0.5px);
|
||||||
transition:
|
width: 8px !important;
|
||||||
width 0.15s ease-in-out,
|
|
||||||
right 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
|
||||||
border-radius: 0 0 6.5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll.dark .simplebar-track.simplebar-vertical {
|
|
||||||
background-color: #141414;
|
|
||||||
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
|
|
||||||
border-bottom: 1px solid rgba(253, 253, 253, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll.light .simplebar-track.simplebar-vertical {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-inline-end: 1px solid rgba(5, 5, 5, 0.06);
|
|
||||||
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-scroll.simplebar-scrolling .simplebar-track.simplebar-vertical {
|
|
||||||
width: 12px !important;
|
|
||||||
right: -13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-scroll
|
.sidebar-scroll
|
||||||
.simplebar-track.simplebar-vertical
|
.simplebar-track.simplebar-vertical
|
||||||
.simplebar-scrollbar:before {
|
.simplebar-scrollbar:before {
|
||||||
top: 4px;
|
top: 2px;
|
||||||
bottom: 4px;
|
bottom: 2px;
|
||||||
left: 4px;
|
left: 2px;
|
||||||
right: 4px;
|
right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-color-picker
|
.ant-color-picker
|
||||||
@ -1096,29 +1076,229 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmcontrol-splitter .ant-splitter-panel {
|
.farmcontrol-splitter .ant-splitter-panel,
|
||||||
|
.farmcontrol-splitter .adaptive-splitter-panel {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel {
|
.farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel,
|
||||||
|
.farmcontrol-splitter.adaptive-splitter-horizontal .adaptive-splitter-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmcontrol-splitter.ant-splitter-horizontal.is-size-transitioning
|
.adaptive-splitter {
|
||||||
.ant-splitter-panel {
|
display: flex;
|
||||||
transition:
|
width: 100%;
|
||||||
--dashboard-splitter-sidebar-size 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
height: 100%;
|
||||||
flex-basis 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
min-height: 0;
|
||||||
width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
align-items: stretch;
|
||||||
min-width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
position: relative;
|
||||||
max-width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property --dashboard-splitter-sidebar-size {
|
.adaptive-splitter-vertical {
|
||||||
syntax: '<length>';
|
flex-direction: column;
|
||||||
inherits: true;
|
}
|
||||||
initial-value: 80px;
|
|
||||||
|
.adaptive-splitter-panel {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-bar {
|
||||||
|
flex: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal > .adaptive-splitter-bar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-vertical > .adaptive-splitter-bar {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-bar-dragger {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 6px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-vertical
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
height: 6px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-bar-dragger::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger::before {
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--ant-color-split, rgba(5, 5, 5, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-vertical
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger::before {
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--ant-color-split, rgba(5, 5, 5, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-bar-dragger::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
background: var(--ant-color-text-quaternary, rgba(0, 0, 0, 0.25));
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger::after {
|
||||||
|
width: 2px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-vertical
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger::after {
|
||||||
|
height: 2px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-bar.is-active .adaptive-splitter-bar-dragger::before,
|
||||||
|
.adaptive-splitter-bar-dragger:hover::before {
|
||||||
|
background: var(--ant-color-primary-border-hover, #4096ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-bar-collapse {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ant-color-text-secondary, rgba(0, 0, 0, 0.45));
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-collapse {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-collapse-start {
|
||||||
|
left: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-collapse-end {
|
||||||
|
right: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-bar-preview {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
background: var(--ant-color-primary, #1677ff);
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-horizontal
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-preview {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-vertical
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-preview {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter.is-dragging {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter.is-dragging.adaptive-splitter-horizontal,
|
||||||
|
.adaptive-splitter.is-dragging.adaptive-splitter-horizontal * {
|
||||||
|
cursor: col-resize !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter.is-dragging.adaptive-splitter-vertical,
|
||||||
|
.adaptive-splitter.is-dragging.adaptive-splitter-vertical * {
|
||||||
|
cursor: row-resize !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter-mask {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptive-splitter.is-ready:not(.is-dragging) > .adaptive-splitter-panel,
|
||||||
|
.adaptive-splitter.is-size-transitioning > .adaptive-splitter-panel,
|
||||||
|
.farmcontrol-splitter.is-size-transitioning > .ant-splitter-panel,
|
||||||
|
.farmcontrol-splitter.is-size-transitioning > .adaptive-splitter-panel {
|
||||||
|
transition:
|
||||||
|
flex-basis var(--adaptive-splitter-size-transition, 0.3s)
|
||||||
|
cubic-bezier(0.2, 0, 0, 1),
|
||||||
|
width var(--adaptive-splitter-size-transition, 0.3s)
|
||||||
|
cubic-bezier(0.2, 0, 0, 1),
|
||||||
|
min-width var(--adaptive-splitter-size-transition, 0.3s)
|
||||||
|
cubic-bezier(0.2, 0, 0, 1),
|
||||||
|
max-width var(--adaptive-splitter-size-transition, 0.3s)
|
||||||
|
cubic-bezier(0.2, 0, 0, 1),
|
||||||
|
height var(--adaptive-splitter-size-transition, 0.3s)
|
||||||
|
cubic-bezier(0.2, 0, 0, 1),
|
||||||
|
min-height var(--adaptive-splitter-size-transition, 0.3s)
|
||||||
|
cubic-bezier(0.2, 0, 0, 1),
|
||||||
|
max-height var(--adaptive-splitter-size-transition, 0.3s)
|
||||||
|
cubic-bezier(0.2, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmcontrol-splitter
|
.farmcontrol-splitter
|
||||||
@ -1643,41 +1823,34 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
stroke-width: 8px;
|
stroke-width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-bar {
|
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-bar,
|
||||||
|
.dashboard-splitter.farmcontrol-splitter > .adaptive-splitter-bar {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-panel {
|
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-panel,
|
||||||
|
.dashboard-splitter.farmcontrol-splitter > .adaptive-splitter-panel {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-splitter.farmcontrol-splitter
|
.dashboard-splitter.farmcontrol-splitter
|
||||||
> .ant-splitter-bar
|
> .ant-splitter-bar
|
||||||
.ant-splitter-bar-dragger::before {
|
.ant-splitter-bar-dragger::before,
|
||||||
|
.dashboard-splitter.farmcontrol-splitter
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger::before {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-splitter.farmcontrol-splitter
|
.dashboard-splitter.farmcontrol-splitter
|
||||||
> .ant-splitter-bar
|
> .ant-splitter-bar
|
||||||
.ant-splitter-bar-dragger::after {
|
.ant-splitter-bar-dragger::after,
|
||||||
|
.dashboard-splitter.farmcontrol-splitter
|
||||||
|
> .adaptive-splitter-bar
|
||||||
|
.adaptive-splitter-bar-dragger::after {
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-splitter.farmcontrol-splitter
|
|
||||||
.ant-splitter-panel.dashboard-splitter-content-panel {
|
|
||||||
flex-grow: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-splitter.farmcontrol-splitter
|
|
||||||
.ant-splitter-panel.dashboard-splitter-sidebar-panel.is-collapsed {
|
|
||||||
flex-grow: 0 !important;
|
|
||||||
flex-shrink: 0 !important;
|
|
||||||
flex-basis: var(--dashboard-splitter-sidebar-size) !important;
|
|
||||||
width: var(--dashboard-splitter-sidebar-size) !important;
|
|
||||||
min-width: var(--dashboard-splitter-sidebar-size) !important;
|
|
||||||
max-width: var(--dashboard-splitter-sidebar-size) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-display-tag
|
.object-display-tag
|
||||||
.elipsis-text-wrapper.ant-typography
|
.elipsis-text-wrapper.ant-typography
|
||||||
code.elipsis-text-measure,
|
code.elipsis-text-measure,
|
||||||
|
|||||||
981
src/components/Dashboard/common/AdaptiveSplitter.jsx
Normal file
981
src/components/Dashboard/common/AdaptiveSplitter.jsx
Normal file
@ -0,0 +1,981 @@
|
|||||||
|
import {
|
||||||
|
Children,
|
||||||
|
isValidElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import {
|
||||||
|
DownOutlined,
|
||||||
|
LeftOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
UpOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
|
const SIZE_TRANSITION_MS = 300
|
||||||
|
const DEFAULT_EXPAND_DRAG_PX = 10
|
||||||
|
const KEYBOARD_STEP_PX = 8
|
||||||
|
|
||||||
|
const sizeType = PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||||
|
|
||||||
|
const AdaptiveSplitterPanel = () => null
|
||||||
|
AdaptiveSplitterPanel.displayName = 'AdaptiveSplitterPanel'
|
||||||
|
AdaptiveSplitterPanel.isAdaptiveSplitterPanel = true
|
||||||
|
AdaptiveSplitterPanel.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
size: sizeType,
|
||||||
|
defaultSize: sizeType,
|
||||||
|
min: sizeType,
|
||||||
|
max: sizeType,
|
||||||
|
resizable: PropTypes.bool,
|
||||||
|
collapsible: PropTypes.oneOfType([
|
||||||
|
PropTypes.bool,
|
||||||
|
PropTypes.shape({
|
||||||
|
start: PropTypes.bool,
|
||||||
|
end: PropTypes.bool
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
collapsed: PropTypes.bool,
|
||||||
|
collapsedSize: PropTypes.number,
|
||||||
|
collapseThreshold: PropTypes.number,
|
||||||
|
expandDragPx: PropTypes.number,
|
||||||
|
expandedSize: PropTypes.number,
|
||||||
|
onCollapse: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSizeToPx = (size, containerSize) => {
|
||||||
|
if (size == null || size === '') return null
|
||||||
|
if (typeof size === 'number') {
|
||||||
|
return Number.isFinite(size) ? size : null
|
||||||
|
}
|
||||||
|
const value = String(size).trim()
|
||||||
|
if (value.endsWith('%')) {
|
||||||
|
const pct = Number.parseFloat(value)
|
||||||
|
return Number.isFinite(pct) ? (pct / 100) * containerSize : null
|
||||||
|
}
|
||||||
|
const px = Number.parseFloat(value)
|
||||||
|
return Number.isFinite(px) ? px : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value))
|
||||||
|
|
||||||
|
const getPanelItems = (children) =>
|
||||||
|
Children.toArray(children)
|
||||||
|
.filter(
|
||||||
|
(child) =>
|
||||||
|
isValidElement(child) &&
|
||||||
|
(child.type === AdaptiveSplitterPanel ||
|
||||||
|
child.type?.isAdaptiveSplitterPanel)
|
||||||
|
)
|
||||||
|
.map((child) => ({
|
||||||
|
...child.props,
|
||||||
|
key: child.key
|
||||||
|
}))
|
||||||
|
|
||||||
|
const getCollapsibleSides = (collapsible) => {
|
||||||
|
if (!collapsible) return { start: false, end: false }
|
||||||
|
if (collapsible === true) return { start: true, end: true }
|
||||||
|
return {
|
||||||
|
start: !!collapsible.start,
|
||||||
|
end: !!collapsible.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBarEdge = (sizes, barIndex) => {
|
||||||
|
if (barIndex < 0) return 0
|
||||||
|
let edge = 0
|
||||||
|
for (let i = 0; i <= barIndex; i += 1) {
|
||||||
|
edge += sizes[i] || 0
|
||||||
|
}
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
|
||||||
|
const SplitBar = ({
|
||||||
|
active,
|
||||||
|
resizable,
|
||||||
|
isVertical,
|
||||||
|
lazy,
|
||||||
|
lazyOffset,
|
||||||
|
ariaNow,
|
||||||
|
ariaMin,
|
||||||
|
ariaMax,
|
||||||
|
startCollapsible,
|
||||||
|
endCollapsible,
|
||||||
|
onPointerDown,
|
||||||
|
onKeyDown,
|
||||||
|
onCollapseStart,
|
||||||
|
onCollapseEnd
|
||||||
|
}) => {
|
||||||
|
const startIcon = isVertical ? <UpOutlined /> : <LeftOutlined />
|
||||||
|
const endIcon = isVertical ? <DownOutlined /> : <RightOutlined />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'adaptive-splitter-bar',
|
||||||
|
'ant-splitter-bar',
|
||||||
|
active ? 'is-active' : '',
|
||||||
|
resizable ? '' : 'is-disabled'
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{lazy && active && (
|
||||||
|
<div
|
||||||
|
className='adaptive-splitter-bar-preview ant-splitter-bar-preview'
|
||||||
|
style={{
|
||||||
|
[isVertical ? 'top' : 'left']: '50%',
|
||||||
|
transform: isVertical
|
||||||
|
? `translate(-50%, calc(-50% + ${lazyOffset}px))`
|
||||||
|
: `translate(calc(-50% + ${lazyOffset}px), -50%)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
role='separator'
|
||||||
|
tabIndex={resizable ? 0 : -1}
|
||||||
|
aria-orientation={isVertical ? 'horizontal' : 'vertical'}
|
||||||
|
aria-valuenow={Math.round(ariaNow || 0)}
|
||||||
|
aria-valuemin={Math.round(ariaMin || 0)}
|
||||||
|
aria-valuemax={
|
||||||
|
Number.isFinite(ariaMax) ? Math.round(ariaMax) : undefined
|
||||||
|
}
|
||||||
|
className={[
|
||||||
|
'adaptive-splitter-bar-dragger',
|
||||||
|
'ant-splitter-bar-dragger',
|
||||||
|
active ? 'ant-splitter-bar-dragger-active' : '',
|
||||||
|
resizable ? '' : 'ant-splitter-bar-dragger-disabled'
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
onPointerDown={resizable ? onPointerDown : undefined}
|
||||||
|
onKeyDown={resizable ? onKeyDown : undefined}
|
||||||
|
/>
|
||||||
|
{startCollapsible && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='adaptive-splitter-bar-collapse adaptive-splitter-bar-collapse-start ant-splitter-bar-collapse-bar ant-splitter-bar-collapse-bar-start'
|
||||||
|
aria-label='Collapse start panel'
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={onCollapseStart}
|
||||||
|
>
|
||||||
|
{startIcon}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{endCollapsible && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='adaptive-splitter-bar-collapse adaptive-splitter-bar-collapse-end ant-splitter-bar-collapse-bar ant-splitter-bar-collapse-bar-end'
|
||||||
|
aria-label='Collapse end panel'
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
onClick={onCollapseEnd}
|
||||||
|
>
|
||||||
|
{endIcon}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitBar.propTypes = {
|
||||||
|
active: PropTypes.bool,
|
||||||
|
resizable: PropTypes.bool,
|
||||||
|
isVertical: PropTypes.bool,
|
||||||
|
lazy: PropTypes.bool,
|
||||||
|
lazyOffset: PropTypes.number,
|
||||||
|
ariaNow: PropTypes.number,
|
||||||
|
ariaMin: PropTypes.number,
|
||||||
|
ariaMax: PropTypes.number,
|
||||||
|
startCollapsible: PropTypes.bool,
|
||||||
|
endCollapsible: PropTypes.bool,
|
||||||
|
onPointerDown: PropTypes.func,
|
||||||
|
onKeyDown: PropTypes.func,
|
||||||
|
onCollapseStart: PropTypes.func,
|
||||||
|
onCollapseEnd: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdaptiveSplitter = ({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
style,
|
||||||
|
layout,
|
||||||
|
orientation,
|
||||||
|
vertical = false,
|
||||||
|
lazy = false,
|
||||||
|
onResizeStart,
|
||||||
|
onResize,
|
||||||
|
onResizeEnd,
|
||||||
|
onCollapse,
|
||||||
|
sizeTransitionMs = SIZE_TRANSITION_MS
|
||||||
|
}) => {
|
||||||
|
const isVertical =
|
||||||
|
orientation === 'vertical' || layout === 'vertical' || vertical
|
||||||
|
const items = useMemo(() => getPanelItems(children), [children])
|
||||||
|
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
const [containerSize, setContainerSize] = useState(0)
|
||||||
|
const [pxSizes, setPxSizes] = useState([])
|
||||||
|
const [dragging, setDragging] = useState(false)
|
||||||
|
const [activeBar, setActiveBar] = useState(null)
|
||||||
|
const [sizeTransitioning, setSizeTransitioning] = useState(false)
|
||||||
|
const [ready, setReady] = useState(false)
|
||||||
|
const [lazyOffset, setLazyOffset] = useState(0)
|
||||||
|
|
||||||
|
const pxSizesRef = useRef(pxSizes)
|
||||||
|
pxSizesRef.current = pxSizes
|
||||||
|
const itemsRef = useRef(items)
|
||||||
|
itemsRef.current = items
|
||||||
|
const containerSizeRef = useRef(containerSize)
|
||||||
|
containerSizeRef.current = containerSize
|
||||||
|
const dragRef = useRef(null)
|
||||||
|
const lastExpandedRef = useRef([])
|
||||||
|
const sizeTransitionTimeoutRef = useRef(null)
|
||||||
|
const skipNextTransitionRef = useRef(true)
|
||||||
|
const lazyOffsetRef = useRef(0)
|
||||||
|
const pendingAnimateSizesRef = useRef(null)
|
||||||
|
const animateRafRef = useRef([])
|
||||||
|
|
||||||
|
const getItemMin = useCallback((item, fallback = 0) => {
|
||||||
|
const parsed = parseSizeToPx(item?.min, containerSizeRef.current)
|
||||||
|
return parsed ?? fallback
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getItemMax = useCallback((item, fallback = Infinity) => {
|
||||||
|
const parsed = parseSizeToPx(item?.max, containerSizeRef.current)
|
||||||
|
return parsed ?? fallback
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const rememberExpandedSize = useCallback((size, index, item) => {
|
||||||
|
if (!item || item.collapsed) return
|
||||||
|
if (item.collapseThreshold != null && size < item.collapseThreshold) return
|
||||||
|
lastExpandedRef.current[index] = size
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const resolveExpandedSize = useCallback(
|
||||||
|
(index, item) => {
|
||||||
|
if (item?.expandedSize != null) return item.expandedSize
|
||||||
|
if (lastExpandedRef.current[index] != null) {
|
||||||
|
return lastExpandedRef.current[index]
|
||||||
|
}
|
||||||
|
const fromDefault = parseSizeToPx(
|
||||||
|
item?.defaultSize,
|
||||||
|
containerSizeRef.current
|
||||||
|
)
|
||||||
|
if (fromDefault != null) return fromDefault
|
||||||
|
const max = getItemMax(item)
|
||||||
|
if (Number.isFinite(max)) return max
|
||||||
|
return item?.collapsedSize ?? 0
|
||||||
|
},
|
||||||
|
[getItemMax]
|
||||||
|
)
|
||||||
|
|
||||||
|
const applyBarResize = useCallback(
|
||||||
|
(sizes, barIndex, nextStartSize) => {
|
||||||
|
const currentItems = itemsRef.current
|
||||||
|
const startItem = currentItems[barIndex]
|
||||||
|
const endItem = currentItems[barIndex + 1]
|
||||||
|
if (!startItem || !endItem) return sizes
|
||||||
|
|
||||||
|
const next = sizes.slice()
|
||||||
|
const startMin = getItemMin(startItem)
|
||||||
|
const startMax = getItemMax(startItem)
|
||||||
|
const endMin = getItemMin(endItem)
|
||||||
|
const endMax = getItemMax(endItem)
|
||||||
|
const oldStart = next[barIndex]
|
||||||
|
const oldEnd = next[barIndex + 1]
|
||||||
|
const pairTotal = oldStart + oldEnd
|
||||||
|
|
||||||
|
let startSize = clamp(nextStartSize, startMin, startMax)
|
||||||
|
let endSize = pairTotal - startSize
|
||||||
|
|
||||||
|
if (endSize < endMin) {
|
||||||
|
endSize = endMin
|
||||||
|
startSize = pairTotal - endMin
|
||||||
|
} else if (endSize > endMax) {
|
||||||
|
endSize = endMax
|
||||||
|
startSize = pairTotal - endMax
|
||||||
|
}
|
||||||
|
|
||||||
|
startSize = clamp(startSize, startMin, startMax)
|
||||||
|
next[barIndex] = startSize
|
||||||
|
next[barIndex + 1] = pairTotal - startSize
|
||||||
|
return next
|
||||||
|
},
|
||||||
|
[getItemMax, getItemMin]
|
||||||
|
)
|
||||||
|
|
||||||
|
const emitCollapse = useCallback(
|
||||||
|
(index, collapsed, sizes) => {
|
||||||
|
itemsRef.current[index]?.onCollapse?.(collapsed)
|
||||||
|
onCollapse?.(
|
||||||
|
itemsRef.current.map((panel, panelIndex) =>
|
||||||
|
panelIndex === index ? collapsed : !!panel.collapsed
|
||||||
|
),
|
||||||
|
sizes
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[onCollapse]
|
||||||
|
)
|
||||||
|
|
||||||
|
const remapDragOrigin = useCallback((pointerPos, edge) => {
|
||||||
|
const drag = dragRef.current
|
||||||
|
if (!drag) return
|
||||||
|
drag.originOffset = pointerPos - edge
|
||||||
|
drag.lastPointerPos = pointerPos
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const endDragSession = useCallback(() => {
|
||||||
|
const drag = dragRef.current
|
||||||
|
if (drag?.captureTarget?.hasPointerCapture?.(drag.pointerId)) {
|
||||||
|
drag.captureTarget.releasePointerCapture(drag.pointerId)
|
||||||
|
}
|
||||||
|
dragRef.current = null
|
||||||
|
lazyOffsetRef.current = 0
|
||||||
|
setLazyOffset(0)
|
||||||
|
setDragging(false)
|
||||||
|
setActiveBar(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const cancelPendingAnimate = useCallback(() => {
|
||||||
|
animateRafRef.current.forEach((id) => cancelAnimationFrame(id))
|
||||||
|
animateRafRef.current = []
|
||||||
|
pendingAnimateSizesRef.current = null
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const startSizeTransition = useCallback(() => {
|
||||||
|
setSizeTransitioning(true)
|
||||||
|
if (sizeTransitionTimeoutRef.current) {
|
||||||
|
clearTimeout(sizeTransitionTimeoutRef.current)
|
||||||
|
}
|
||||||
|
sizeTransitionTimeoutRef.current = setTimeout(() => {
|
||||||
|
setSizeTransitioning(false)
|
||||||
|
sizeTransitionTimeoutRef.current = null
|
||||||
|
}, sizeTransitionMs)
|
||||||
|
}, [sizeTransitionMs])
|
||||||
|
|
||||||
|
const scheduleAnimatedSizes = useCallback(
|
||||||
|
(nextSizes) => {
|
||||||
|
startSizeTransition()
|
||||||
|
cancelPendingAnimate()
|
||||||
|
pendingAnimateSizesRef.current = nextSizes
|
||||||
|
const ids = []
|
||||||
|
ids.push(
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ids.push(
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
animateRafRef.current = []
|
||||||
|
const pending = pendingAnimateSizesRef.current
|
||||||
|
pendingAnimateSizesRef.current = null
|
||||||
|
if (pending) setPxSizes(pending)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
animateRafRef.current = ids
|
||||||
|
},
|
||||||
|
[cancelPendingAnimate, startSizeTransition]
|
||||||
|
)
|
||||||
|
|
||||||
|
const measureContainer = useCallback(() => {
|
||||||
|
const node = containerRef.current
|
||||||
|
if (!node) return
|
||||||
|
const next = isVertical ? node.clientHeight : node.clientWidth
|
||||||
|
if (next !== containerSizeRef.current) {
|
||||||
|
setContainerSize(next)
|
||||||
|
}
|
||||||
|
}, [isVertical])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
measureContainer()
|
||||||
|
const node = containerRef.current
|
||||||
|
if (!node || typeof ResizeObserver === 'undefined') return undefined
|
||||||
|
const observer = new ResizeObserver(() => measureContainer())
|
||||||
|
observer.observe(node)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [measureContainer])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = requestAnimationFrame(() => setReady(true))
|
||||||
|
return () => cancelAnimationFrame(id)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const deriveSizes = useCallback(() => {
|
||||||
|
if (!containerSize || !items.length) return []
|
||||||
|
|
||||||
|
const parsed = items.map((item) => {
|
||||||
|
if (item.collapsed && item.collapsedSize != null && item.size == null) {
|
||||||
|
return item.collapsedSize
|
||||||
|
}
|
||||||
|
return parseSizeToPx(item.size ?? item.defaultSize, containerSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
const knownSum = parsed.reduce(
|
||||||
|
(sum, size) => sum + (size == null ? 0 : size),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const unknownCount = parsed.filter((size) => size == null).length
|
||||||
|
const leftover = Math.max(0, containerSize - knownSum)
|
||||||
|
const unknownShare = unknownCount ? leftover / unknownCount : 0
|
||||||
|
|
||||||
|
return parsed.map((size, index) => {
|
||||||
|
const item = items[index]
|
||||||
|
const next = size == null ? unknownShare : size
|
||||||
|
const min = getItemMin(item)
|
||||||
|
const max = getItemMax(item)
|
||||||
|
const clamped = clamp(next, min, Number.isFinite(max) ? max : next)
|
||||||
|
rememberExpandedSize(clamped, index, item)
|
||||||
|
return clamped
|
||||||
|
})
|
||||||
|
}, [containerSize, getItemMax, getItemMin, items, rememberExpandedSize])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const nextSizes = deriveSizes()
|
||||||
|
if (!nextSizes.length) return
|
||||||
|
|
||||||
|
const drag = dragRef.current
|
||||||
|
const prev = pxSizesRef.current
|
||||||
|
const sameLength = prev.length === nextSizes.length
|
||||||
|
const changed =
|
||||||
|
!sameLength || nextSizes.some((size, index) => size !== prev[index])
|
||||||
|
|
||||||
|
if (drag && sameLength && changed && drag.lastPointerPos != null) {
|
||||||
|
remapDragOrigin(drag.lastPointerPos, getBarEdge(nextSizes, drag.barIndex))
|
||||||
|
skipNextTransitionRef.current = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldAnimate =
|
||||||
|
changed &&
|
||||||
|
!drag &&
|
||||||
|
sameLength &&
|
||||||
|
!skipNextTransitionRef.current &&
|
||||||
|
prev.length
|
||||||
|
|
||||||
|
skipNextTransitionRef.current = false
|
||||||
|
|
||||||
|
if (shouldAnimate) {
|
||||||
|
scheduleAnimatedSizes(nextSizes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
cancelPendingAnimate()
|
||||||
|
setPxSizes(nextSizes)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
cancelPendingAnimate,
|
||||||
|
deriveSizes,
|
||||||
|
remapDragOrigin,
|
||||||
|
scheduleAnimatedSizes
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (sizeTransitionTimeoutRef.current) {
|
||||||
|
clearTimeout(sizeTransitionTimeoutRef.current)
|
||||||
|
}
|
||||||
|
animateRafRef.current.forEach((id) => cancelAnimationFrame(id))
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getPointerPos = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const node = containerRef.current
|
||||||
|
if (!node) return 0
|
||||||
|
const rect = node.getBoundingClientRect()
|
||||||
|
return isVertical ? event.clientY - rect.top : event.clientX - rect.left
|
||||||
|
},
|
||||||
|
[isVertical]
|
||||||
|
)
|
||||||
|
|
||||||
|
const commitSizes = useCallback(
|
||||||
|
(nextSizes, { notify = true, end = false } = {}) => {
|
||||||
|
cancelPendingAnimate()
|
||||||
|
setPxSizes(nextSizes)
|
||||||
|
nextSizes.forEach((size, index) =>
|
||||||
|
rememberExpandedSize(size, index, itemsRef.current[index])
|
||||||
|
)
|
||||||
|
if (!notify) return
|
||||||
|
if (end) onResizeEnd?.(nextSizes)
|
||||||
|
else onResize?.(nextSizes)
|
||||||
|
},
|
||||||
|
[cancelPendingAnimate, onResize, onResizeEnd, rememberExpandedSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolveDragSize = useCallback(
|
||||||
|
(barIndex, pointerPos, originOffset, currentSizes) => {
|
||||||
|
const item = itemsRef.current[barIndex]
|
||||||
|
const drag = dragRef.current
|
||||||
|
const collapsed =
|
||||||
|
drag?.barIndex === barIndex ? drag.collapsed : !!item?.collapsed
|
||||||
|
const rawEdge = pointerPos - originOffset
|
||||||
|
const startSize = rawEdge - getBarEdge(currentSizes, barIndex - 1)
|
||||||
|
|
||||||
|
const collapsedSize = item?.collapsedSize ?? getItemMin(item)
|
||||||
|
const expandDragPx = item?.expandDragPx ?? DEFAULT_EXPAND_DRAG_PX
|
||||||
|
const collapseThreshold = item?.collapseThreshold
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
const preview = Math.max(collapsedSize, startSize)
|
||||||
|
if (preview - collapsedSize > expandDragPx) {
|
||||||
|
return {
|
||||||
|
expand: true,
|
||||||
|
collapse: false,
|
||||||
|
startSize: preview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
collapse: false,
|
||||||
|
startSize: preview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drag?.blockCollapse) {
|
||||||
|
if (startSize <= collapsedSize + expandDragPx) {
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
collapse: true,
|
||||||
|
startSize: collapsedSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (collapseThreshold != null && startSize >= collapseThreshold) {
|
||||||
|
drag.blockCollapse = false
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
collapse: false,
|
||||||
|
startSize: Math.max(startSize, collapsedSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collapseThreshold != null && startSize < collapseThreshold) {
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
collapse: true,
|
||||||
|
startSize: collapsedSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
collapse: false,
|
||||||
|
startSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getItemMin]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const drag = dragRef.current
|
||||||
|
if (!drag || event.pointerId !== drag.pointerId) return
|
||||||
|
|
||||||
|
const pointerPos = getPointerPos(event)
|
||||||
|
drag.lastPointerPos = pointerPos
|
||||||
|
const currentSizes = pxSizesRef.current
|
||||||
|
const resolved = resolveDragSize(
|
||||||
|
drag.barIndex,
|
||||||
|
pointerPos,
|
||||||
|
drag.originOffset,
|
||||||
|
currentSizes
|
||||||
|
)
|
||||||
|
|
||||||
|
if (lazy) {
|
||||||
|
const startEdge = getBarEdge(drag.startSizes, drag.barIndex)
|
||||||
|
const nextOffset =
|
||||||
|
getBarEdge(
|
||||||
|
applyBarResize(drag.startSizes, drag.barIndex, resolved.startSize),
|
||||||
|
drag.barIndex
|
||||||
|
) - startEdge
|
||||||
|
lazyOffsetRef.current = nextOffset
|
||||||
|
setLazyOffset(nextOffset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSizes = applyBarResize(
|
||||||
|
currentSizes,
|
||||||
|
drag.barIndex,
|
||||||
|
resolved.startSize
|
||||||
|
)
|
||||||
|
|
||||||
|
if (resolved.expand) {
|
||||||
|
drag.collapsed = false
|
||||||
|
drag.blockCollapse = true
|
||||||
|
skipNextTransitionRef.current = true
|
||||||
|
remapDragOrigin(pointerPos, getBarEdge(nextSizes, drag.barIndex))
|
||||||
|
emitCollapse(drag.barIndex, false, nextSizes)
|
||||||
|
commitSizes(nextSizes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.collapse) {
|
||||||
|
const barIndex = drag.barIndex
|
||||||
|
endDragSession()
|
||||||
|
emitCollapse(barIndex, true, nextSizes)
|
||||||
|
onResizeEnd?.(nextSizes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commitSizes(nextSizes)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
applyBarResize,
|
||||||
|
commitSizes,
|
||||||
|
emitCollapse,
|
||||||
|
getPointerPos,
|
||||||
|
lazy,
|
||||||
|
endDragSession,
|
||||||
|
onResizeEnd,
|
||||||
|
remapDragOrigin,
|
||||||
|
resolveDragSize
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const stopDragging = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const drag = dragRef.current
|
||||||
|
if (!drag || (event && event.pointerId !== drag.pointerId)) return
|
||||||
|
|
||||||
|
let nextSizes = pxSizesRef.current.slice()
|
||||||
|
|
||||||
|
if (lazy) {
|
||||||
|
const pointerPos = event ? getPointerPos(event) : drag.lastPointerPos
|
||||||
|
const resolved = resolveDragSize(
|
||||||
|
drag.barIndex,
|
||||||
|
pointerPos,
|
||||||
|
drag.originOffset,
|
||||||
|
drag.startSizes
|
||||||
|
)
|
||||||
|
nextSizes = applyBarResize(
|
||||||
|
drag.startSizes,
|
||||||
|
drag.barIndex,
|
||||||
|
resolved.startSize
|
||||||
|
)
|
||||||
|
if (resolved.expand) {
|
||||||
|
drag.collapsed = false
|
||||||
|
drag.blockCollapse = true
|
||||||
|
emitCollapse(drag.barIndex, false, nextSizes)
|
||||||
|
}
|
||||||
|
if (resolved.collapse) {
|
||||||
|
drag.collapsed = true
|
||||||
|
drag.blockCollapse = false
|
||||||
|
emitCollapse(drag.barIndex, true, nextSizes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = itemsRef.current[drag.barIndex]
|
||||||
|
const collapsed = drag.collapsed ?? item?.collapsed
|
||||||
|
if (collapsed) {
|
||||||
|
const collapsedSize = item.collapsedSize ?? getItemMin(item)
|
||||||
|
if (nextSizes[drag.barIndex] !== collapsedSize) {
|
||||||
|
nextSizes = applyBarResize(nextSizes, drag.barIndex, collapsedSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endDragSession()
|
||||||
|
skipNextTransitionRef.current = true
|
||||||
|
commitSizes(nextSizes, { end: true })
|
||||||
|
},
|
||||||
|
[
|
||||||
|
applyBarResize,
|
||||||
|
commitSizes,
|
||||||
|
emitCollapse,
|
||||||
|
endDragSession,
|
||||||
|
getItemMin,
|
||||||
|
getPointerPos,
|
||||||
|
lazy,
|
||||||
|
resolveDragSize
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const startDragging = useCallback(
|
||||||
|
(barIndex, event) => {
|
||||||
|
const item = itemsRef.current[barIndex]
|
||||||
|
const nextItem = itemsRef.current[barIndex + 1]
|
||||||
|
if (
|
||||||
|
!pxSizesRef.current.length ||
|
||||||
|
item?.resizable === false ||
|
||||||
|
nextItem?.resizable === false
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.currentTarget.setPointerCapture?.(event.pointerId)
|
||||||
|
|
||||||
|
const pointerPos = getPointerPos(event)
|
||||||
|
const startSizes = pxSizesRef.current.slice()
|
||||||
|
|
||||||
|
dragRef.current = {
|
||||||
|
barIndex,
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
captureTarget: event.currentTarget,
|
||||||
|
originOffset: pointerPos - getBarEdge(startSizes, barIndex),
|
||||||
|
lastPointerPos: pointerPos,
|
||||||
|
startSizes,
|
||||||
|
collapsed: !!item.collapsed,
|
||||||
|
blockCollapse: false
|
||||||
|
}
|
||||||
|
skipNextTransitionRef.current = true
|
||||||
|
setSizeTransitioning(false)
|
||||||
|
setDragging(true)
|
||||||
|
setActiveBar(barIndex)
|
||||||
|
onResizeStart?.(startSizes)
|
||||||
|
},
|
||||||
|
[getPointerPos, onResizeStart]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dragging) return undefined
|
||||||
|
window.addEventListener('pointermove', handlePointerMove)
|
||||||
|
window.addEventListener('pointerup', stopDragging)
|
||||||
|
window.addEventListener('pointercancel', stopDragging)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove)
|
||||||
|
window.removeEventListener('pointerup', stopDragging)
|
||||||
|
window.removeEventListener('pointercancel', stopDragging)
|
||||||
|
}
|
||||||
|
}, [dragging, handlePointerMove, stopDragging])
|
||||||
|
|
||||||
|
const collapseViaControl = useCallback(
|
||||||
|
(barIndex, side) => {
|
||||||
|
const itemIndex = side === 'start' ? barIndex : barIndex + 1
|
||||||
|
const item = itemsRef.current[itemIndex]
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
const collapsedSize = item.collapsedSize ?? getItemMin(item)
|
||||||
|
const current = pxSizesRef.current[itemIndex]
|
||||||
|
const alreadyCollapsed =
|
||||||
|
item.collapsed ||
|
||||||
|
(item.collapseThreshold != null
|
||||||
|
? current < item.collapseThreshold
|
||||||
|
: current <= collapsedSize + 1)
|
||||||
|
const restored = resolveExpandedSize(itemIndex, item)
|
||||||
|
let nextStartSize
|
||||||
|
|
||||||
|
if (side === 'start') {
|
||||||
|
nextStartSize = alreadyCollapsed ? restored : collapsedSize
|
||||||
|
} else {
|
||||||
|
const neighbor = pxSizesRef.current[barIndex]
|
||||||
|
const nextPanelSize = alreadyCollapsed ? restored : collapsedSize
|
||||||
|
nextStartSize = neighbor + (current - nextPanelSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyCollapsed) {
|
||||||
|
rememberExpandedSize(current, itemIndex, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSizes = applyBarResize(
|
||||||
|
pxSizesRef.current,
|
||||||
|
barIndex,
|
||||||
|
nextStartSize
|
||||||
|
)
|
||||||
|
emitCollapse(itemIndex, !alreadyCollapsed, nextSizes)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
applyBarResize,
|
||||||
|
emitCollapse,
|
||||||
|
getItemMin,
|
||||||
|
rememberExpandedSize,
|
||||||
|
resolveExpandedSize
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleBarKeyDown = useCallback(
|
||||||
|
(barIndex, event) => {
|
||||||
|
const backward = isVertical
|
||||||
|
? event.key === 'ArrowUp'
|
||||||
|
: event.key === 'ArrowLeft'
|
||||||
|
const forward = isVertical
|
||||||
|
? event.key === 'ArrowDown'
|
||||||
|
: event.key === 'ArrowRight'
|
||||||
|
if (!backward && !forward) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
const delta = forward ? KEYBOARD_STEP_PX : -KEYBOARD_STEP_PX
|
||||||
|
const item = itemsRef.current[barIndex]
|
||||||
|
const proposed = pxSizesRef.current[barIndex] + delta
|
||||||
|
const collapsedSize = item?.collapsedSize ?? getItemMin(item)
|
||||||
|
const expandDragPx = item?.expandDragPx ?? DEFAULT_EXPAND_DRAG_PX
|
||||||
|
|
||||||
|
if (
|
||||||
|
item?.collapseThreshold != null &&
|
||||||
|
proposed < item.collapseThreshold &&
|
||||||
|
!item.collapsed
|
||||||
|
) {
|
||||||
|
rememberExpandedSize(pxSizesRef.current[barIndex], barIndex, item)
|
||||||
|
const collapsedSizes = applyBarResize(
|
||||||
|
pxSizesRef.current,
|
||||||
|
barIndex,
|
||||||
|
collapsedSize
|
||||||
|
)
|
||||||
|
emitCollapse(barIndex, true, collapsedSizes)
|
||||||
|
commitSizes(collapsedSizes, { end: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item?.collapsed && proposed - collapsedSize > expandDragPx) {
|
||||||
|
const expandedSizes = applyBarResize(
|
||||||
|
pxSizesRef.current,
|
||||||
|
barIndex,
|
||||||
|
resolveExpandedSize(barIndex, item)
|
||||||
|
)
|
||||||
|
emitCollapse(barIndex, false, expandedSizes)
|
||||||
|
commitSizes(expandedSizes, { end: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commitSizes(applyBarResize(pxSizesRef.current, barIndex, proposed), {
|
||||||
|
end: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[
|
||||||
|
applyBarResize,
|
||||||
|
commitSizes,
|
||||||
|
emitCollapse,
|
||||||
|
getItemMin,
|
||||||
|
isVertical,
|
||||||
|
rememberExpandedSize,
|
||||||
|
resolveExpandedSize
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const orientationClass = isVertical ? 'vertical' : 'horizontal'
|
||||||
|
const rootClassName = [
|
||||||
|
'adaptive-splitter',
|
||||||
|
'ant-splitter',
|
||||||
|
`adaptive-splitter-${orientationClass}`,
|
||||||
|
`ant-splitter-${orientationClass}`,
|
||||||
|
ready ? 'is-ready' : '',
|
||||||
|
sizeTransitioning ? 'is-size-transitioning' : '',
|
||||||
|
dragging ? 'is-dragging' : '',
|
||||||
|
className
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={rootClassName}
|
||||||
|
style={{
|
||||||
|
'--adaptive-splitter-size-transition': `${sizeTransitionMs}ms`,
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items.flatMap((item, index) => {
|
||||||
|
const size = pxSizes[index] ?? 0
|
||||||
|
const isFill =
|
||||||
|
item.size == null &&
|
||||||
|
item.defaultSize == null &&
|
||||||
|
!item.collapsed &&
|
||||||
|
index === items.length - 1
|
||||||
|
const minPx = getItemMin(item)
|
||||||
|
const maxPx = getItemMax(item)
|
||||||
|
const panelStyle = isFill
|
||||||
|
? {
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minWidth: isVertical ? 0 : minPx,
|
||||||
|
minHeight: isVertical ? minPx : 0,
|
||||||
|
overflow: 'auto',
|
||||||
|
...item.style
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
flex: `0 0 ${size}px`,
|
||||||
|
[isVertical ? 'height' : 'width']: size,
|
||||||
|
minWidth: isVertical ? undefined : minPx,
|
||||||
|
maxWidth:
|
||||||
|
isVertical || !Number.isFinite(maxPx) ? undefined : maxPx,
|
||||||
|
minHeight: isVertical ? minPx : undefined,
|
||||||
|
maxHeight:
|
||||||
|
!isVertical || !Number.isFinite(maxPx) ? undefined : maxPx,
|
||||||
|
overflow: 'auto',
|
||||||
|
...item.style
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapsible = getCollapsibleSides(item.collapsible)
|
||||||
|
const nextCollapsible = getCollapsibleSides(
|
||||||
|
items[index + 1]?.collapsible
|
||||||
|
)
|
||||||
|
const showBar = index < items.length - 1
|
||||||
|
const barResizable =
|
||||||
|
item.resizable !== false && items[index + 1]?.resizable !== false
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
<div
|
||||||
|
key={item.key ?? `adaptive-splitter-panel-${index}`}
|
||||||
|
className={[
|
||||||
|
'adaptive-splitter-panel',
|
||||||
|
'ant-splitter-panel',
|
||||||
|
item.collapsed ? 'is-collapsed' : '',
|
||||||
|
item.className
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={panelStyle}
|
||||||
|
>
|
||||||
|
{item.children}
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
|
||||||
|
if (showBar) {
|
||||||
|
nodes.push(
|
||||||
|
<SplitBar
|
||||||
|
key={`adaptive-splitter-bar-${index}`}
|
||||||
|
active={activeBar === index}
|
||||||
|
resizable={barResizable}
|
||||||
|
isVertical={isVertical}
|
||||||
|
lazy={lazy}
|
||||||
|
lazyOffset={lazyOffset}
|
||||||
|
ariaNow={size}
|
||||||
|
ariaMin={minPx}
|
||||||
|
ariaMax={maxPx}
|
||||||
|
startCollapsible={collapsible.end}
|
||||||
|
endCollapsible={nextCollapsible.start}
|
||||||
|
onPointerDown={(event) => startDragging(index, event)}
|
||||||
|
onKeyDown={(event) => handleBarKeyDown(index, event)}
|
||||||
|
onCollapseStart={() => collapseViaControl(index, 'start')}
|
||||||
|
onCollapseEnd={() => collapseViaControl(index, 'end')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
})}
|
||||||
|
{dragging && (
|
||||||
|
<div
|
||||||
|
className='adaptive-splitter-mask ant-splitter-mask'
|
||||||
|
style={{ cursor: isVertical ? 'row-resize' : 'col-resize' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AdaptiveSplitter.Panel = AdaptiveSplitterPanel
|
||||||
|
AdaptiveSplitter.SIZE_TRANSITION_MS = SIZE_TRANSITION_MS
|
||||||
|
|
||||||
|
AdaptiveSplitter.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
layout: PropTypes.oneOf(['horizontal', 'vertical']),
|
||||||
|
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
|
||||||
|
vertical: PropTypes.bool,
|
||||||
|
lazy: PropTypes.bool,
|
||||||
|
onResizeStart: PropTypes.func,
|
||||||
|
onResize: PropTypes.func,
|
||||||
|
onResizeEnd: PropTypes.func,
|
||||||
|
onCollapse: PropTypes.func,
|
||||||
|
sizeTransitionMs: PropTypes.number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdaptiveSplitter
|
||||||
@ -3,22 +3,21 @@ import {
|
|||||||
isValidElement,
|
isValidElement,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Layout, Splitter } from 'antd'
|
import { Layout } from 'antd'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
|
import AdaptiveSplitter from './AdaptiveSplitter'
|
||||||
|
|
||||||
const COLLAPSE_BELOW_PX = 150
|
const COLLAPSE_BELOW_PX = 200
|
||||||
const DEFAULT_SIDEBAR_PX = 300
|
const DEFAULT_SIDEBAR_PX = 300
|
||||||
const MAX_SIDEBAR_PX = 400
|
const MAX_SIDEBAR_PX = 400
|
||||||
const COLLAPSED_WIDTH_PX = 80
|
const COLLAPSED_WIDTH_PX = 80
|
||||||
const ELECTRON_COLLAPSED_WIDTH_PX = 55
|
const ELECTRON_COLLAPSED_WIDTH_PX = 55
|
||||||
const EXPAND_DRAG_PX = 10
|
const EXPAND_DRAG_PX = 10
|
||||||
const SIZE_TRANSITION_MS = 300
|
|
||||||
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
|
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
|
||||||
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
|
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
|
||||||
|
|
||||||
@ -56,7 +55,7 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
|||||||
)
|
)
|
||||||
const [sidebarSize, setSidebarSize] = useState(() => {
|
const [sidebarSize, setSidebarSize] = useState(() => {
|
||||||
if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) {
|
if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) {
|
||||||
return isElectron ? ELECTRON_COLLAPSED_WIDTH_PX : COLLAPSED_WIDTH_PX
|
return collapsedWidth
|
||||||
}
|
}
|
||||||
return clampExpandedSize(
|
return clampExpandedSize(
|
||||||
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
|
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
|
||||||
@ -68,118 +67,6 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
|||||||
const lastExpandedSizeRef = useRef(
|
const lastExpandedSizeRef = useRef(
|
||||||
clampExpandedSize(readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX))
|
clampExpandedSize(readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX))
|
||||||
)
|
)
|
||||||
const sidebarSizeRef = useRef(sidebarSize)
|
|
||||||
sidebarSizeRef.current = sidebarSize
|
|
||||||
const collapseSizeRafRef = useRef([])
|
|
||||||
const [sizeTransitioning, setSizeTransitioning] = useState(false)
|
|
||||||
const sizeTransitionTimeoutRef = useRef(null)
|
|
||||||
const [resizeLocked, setResizeLocked] = useState(false)
|
|
||||||
const resizeLockedRef = useRef(false)
|
|
||||||
const suppressGestureRef = useRef(false)
|
|
||||||
const lockUntilPointerUpRef = useRef(false)
|
|
||||||
const lockTimeoutRef = useRef(null)
|
|
||||||
const pointerUnlockRef = useRef(null)
|
|
||||||
const ignoreResizeEndRef = useRef(false)
|
|
||||||
|
|
||||||
const setResizeLockedState = useCallback((locked) => {
|
|
||||||
resizeLockedRef.current = locked
|
|
||||||
setResizeLocked(locked)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const clearPointerUnlock = useCallback(() => {
|
|
||||||
if (!pointerUnlockRef.current) return
|
|
||||||
window.removeEventListener('pointerup', pointerUnlockRef.current)
|
|
||||||
window.removeEventListener('pointercancel', pointerUnlockRef.current)
|
|
||||||
pointerUnlockRef.current = null
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const attachPointerUnlock = useCallback(() => {
|
|
||||||
if (pointerUnlockRef.current) return
|
|
||||||
const handler = () => {
|
|
||||||
clearPointerUnlock()
|
|
||||||
suppressGestureRef.current = false
|
|
||||||
if (lockUntilPointerUpRef.current) {
|
|
||||||
lockUntilPointerUpRef.current = false
|
|
||||||
setResizeLockedState(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pointerUnlockRef.current = handler
|
|
||||||
window.addEventListener('pointerup', handler)
|
|
||||||
window.addEventListener('pointercancel', handler)
|
|
||||||
}, [clearPointerUnlock, setResizeLockedState])
|
|
||||||
|
|
||||||
const startSizeTransition = useCallback(() => {
|
|
||||||
setSizeTransitioning(true)
|
|
||||||
if (sizeTransitionTimeoutRef.current) {
|
|
||||||
clearTimeout(sizeTransitionTimeoutRef.current)
|
|
||||||
}
|
|
||||||
sizeTransitionTimeoutRef.current = setTimeout(() => {
|
|
||||||
setSizeTransitioning(false)
|
|
||||||
sizeTransitionTimeoutRef.current = null
|
|
||||||
}, SIZE_TRANSITION_MS)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const cancelCollapseSizeAnimation = useCallback(() => {
|
|
||||||
collapseSizeRafRef.current.forEach((id) => cancelAnimationFrame(id))
|
|
||||||
collapseSizeRafRef.current = []
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const animateCollapsedSize = useCallback(() => {
|
|
||||||
cancelCollapseSizeAnimation()
|
|
||||||
const ids = []
|
|
||||||
ids.push(
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
ids.push(
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
collapseSizeRafRef.current = []
|
|
||||||
setSidebarSize(collapsedWidth)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
collapseSizeRafRef.current = ids
|
|
||||||
}, [cancelCollapseSizeAnimation, collapsedWidth])
|
|
||||||
|
|
||||||
const lockResizeUntilPointerUp = useCallback(() => {
|
|
||||||
ignoreResizeEndRef.current = true
|
|
||||||
suppressGestureRef.current = true
|
|
||||||
lockUntilPointerUpRef.current = true
|
|
||||||
setResizeLockedState(true)
|
|
||||||
attachPointerUnlock()
|
|
||||||
}, [attachPointerUnlock, setResizeLockedState])
|
|
||||||
|
|
||||||
const lockResizeFor = useCallback(
|
|
||||||
(ms) => {
|
|
||||||
ignoreResizeEndRef.current = true
|
|
||||||
suppressGestureRef.current = true
|
|
||||||
setResizeLockedState(true)
|
|
||||||
attachPointerUnlock()
|
|
||||||
if (lockTimeoutRef.current) {
|
|
||||||
clearTimeout(lockTimeoutRef.current)
|
|
||||||
}
|
|
||||||
lockTimeoutRef.current = setTimeout(() => {
|
|
||||||
lockTimeoutRef.current = null
|
|
||||||
if (!lockUntilPointerUpRef.current) {
|
|
||||||
setResizeLockedState(false)
|
|
||||||
}
|
|
||||||
}, ms)
|
|
||||||
},
|
|
||||||
[attachPointerUnlock, setResizeLockedState]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => () => {
|
|
||||||
if (sizeTransitionTimeoutRef.current) {
|
|
||||||
clearTimeout(sizeTransitionTimeoutRef.current)
|
|
||||||
}
|
|
||||||
if (lockTimeoutRef.current) {
|
|
||||||
clearTimeout(lockTimeoutRef.current)
|
|
||||||
}
|
|
||||||
cancelCollapseSizeAnimation()
|
|
||||||
clearPointerUnlock()
|
|
||||||
},
|
|
||||||
[cancelCollapseSizeAnimation, clearPointerUnlock]
|
|
||||||
)
|
|
||||||
|
|
||||||
const persistCollapsed = (nextCollapsed) => {
|
const persistCollapsed = (nextCollapsed) => {
|
||||||
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed))
|
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed))
|
||||||
@ -192,113 +79,73 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
|||||||
const collapseSidebar = useCallback(
|
const collapseSidebar = useCallback(
|
||||||
(expandedSize) => {
|
(expandedSize) => {
|
||||||
if (expandedSize >= COLLAPSE_BELOW_PX) {
|
if (expandedSize >= COLLAPSE_BELOW_PX) {
|
||||||
lastExpandedSizeRef.current = expandedSize
|
lastExpandedSizeRef.current = clampExpandedSize(expandedSize)
|
||||||
persistSize(expandedSize)
|
persistSize(lastExpandedSizeRef.current)
|
||||||
}
|
}
|
||||||
collapsedRef.current = true
|
collapsedRef.current = true
|
||||||
persistCollapsed(true)
|
persistCollapsed(true)
|
||||||
startSizeTransition()
|
|
||||||
setSidebarSize(sidebarSizeRef.current)
|
|
||||||
setCollapsed(true)
|
setCollapsed(true)
|
||||||
animateCollapsedSize()
|
setSidebarSize(collapsedWidth)
|
||||||
},
|
},
|
||||||
[animateCollapsedSize, startSizeTransition]
|
[collapsedWidth]
|
||||||
)
|
)
|
||||||
|
|
||||||
const expandSidebar = useCallback(() => {
|
const expandSidebar = useCallback(() => {
|
||||||
cancelCollapseSizeAnimation()
|
|
||||||
collapsedRef.current = false
|
collapsedRef.current = false
|
||||||
setCollapsed(false)
|
setCollapsed(false)
|
||||||
persistCollapsed(false)
|
persistCollapsed(false)
|
||||||
startSizeTransition()
|
const nextSize = clampExpandedSize(lastExpandedSizeRef.current)
|
||||||
setSidebarSize(DEFAULT_SIDEBAR_PX)
|
lastExpandedSizeRef.current = nextSize
|
||||||
persistSize(DEFAULT_SIDEBAR_PX)
|
setSidebarSize(nextSize)
|
||||||
lastExpandedSizeRef.current = DEFAULT_SIDEBAR_PX
|
persistSize(nextSize)
|
||||||
}, [cancelCollapseSizeAnimation, startSizeTransition])
|
}, [])
|
||||||
|
|
||||||
const handleSidebarCollapse = useCallback(
|
const handleSidebarCollapse = useCallback(
|
||||||
(nextCollapsed) => {
|
(nextCollapsed) => {
|
||||||
|
if (nextCollapsed === collapsedRef.current) return
|
||||||
if (nextCollapsed) {
|
if (nextCollapsed) {
|
||||||
collapseSidebar(sidebarSize)
|
collapseSidebar(lastExpandedSizeRef.current)
|
||||||
} else {
|
} else {
|
||||||
expandSidebar()
|
expandSidebar()
|
||||||
}
|
}
|
||||||
lockResizeFor(SIZE_TRANSITION_MS)
|
|
||||||
},
|
},
|
||||||
[collapseSidebar, expandSidebar, lockResizeFor, sidebarSize]
|
[collapseSidebar, expandSidebar]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleResizeStart = useCallback(() => {
|
const handleSplitterCollapse = useCallback(
|
||||||
if (!resizeLockedRef.current) {
|
(nextCollapsed) => {
|
||||||
ignoreResizeEndRef.current = false
|
if (nextCollapsed === collapsedRef.current) return
|
||||||
|
if (nextCollapsed) {
|
||||||
|
collapseSidebar(lastExpandedSizeRef.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collapsedRef.current = false
|
||||||
|
persistCollapsed(false)
|
||||||
|
setCollapsed(false)
|
||||||
|
},
|
||||||
|
[collapseSidebar]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleResize = useCallback((sizes) => {
|
||||||
|
const size = sizes[0]
|
||||||
|
setSidebarSize(size)
|
||||||
|
if (!collapsedRef.current && size >= COLLAPSE_BELOW_PX) {
|
||||||
|
lastExpandedSizeRef.current = clampExpandedSize(size)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleResize = useCallback(
|
|
||||||
(sizes) => {
|
|
||||||
if (resizeLockedRef.current || suppressGestureRef.current) {
|
|
||||||
if (collapsedRef.current) {
|
|
||||||
setSidebarSize(collapsedWidth)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = sizes[0]
|
|
||||||
|
|
||||||
if (collapsedRef.current) {
|
|
||||||
if (size - collapsedWidth > EXPAND_DRAG_PX) {
|
|
||||||
expandSidebar()
|
|
||||||
lockResizeUntilPointerUp()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSidebarSize(Math.max(collapsedWidth, size))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size < COLLAPSE_BELOW_PX) {
|
|
||||||
collapseSidebar(lastExpandedSizeRef.current)
|
|
||||||
lockResizeUntilPointerUp()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSidebarSize(size)
|
|
||||||
lastExpandedSizeRef.current = clampExpandedSize(size)
|
|
||||||
},
|
|
||||||
[collapseSidebar, collapsedWidth, expandSidebar, lockResizeUntilPointerUp]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleResizeEnd = useCallback(
|
const handleResizeEnd = useCallback(
|
||||||
(sizes) => {
|
(sizes) => {
|
||||||
if (
|
|
||||||
resizeLockedRef.current ||
|
|
||||||
suppressGestureRef.current ||
|
|
||||||
ignoreResizeEndRef.current
|
|
||||||
) {
|
|
||||||
ignoreResizeEndRef.current = false
|
|
||||||
if (collapsedRef.current) {
|
if (collapsedRef.current) {
|
||||||
startSizeTransition()
|
|
||||||
setSidebarSize(collapsedWidth)
|
setSidebarSize(collapsedWidth)
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collapsedRef.current) {
|
|
||||||
if (sizes[0] - collapsedWidth > EXPAND_DRAG_PX) {
|
|
||||||
expandSidebar()
|
|
||||||
lockResizeFor(SIZE_TRANSITION_MS)
|
|
||||||
} else {
|
|
||||||
startSizeTransition()
|
|
||||||
setSidebarSize(collapsedWidth)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = clampExpandedSize(sizes[0])
|
const next = clampExpandedSize(sizes[0])
|
||||||
lastExpandedSizeRef.current = next
|
lastExpandedSizeRef.current = next
|
||||||
setSidebarSize(next)
|
setSidebarSize(next)
|
||||||
persistSize(next)
|
persistSize(next)
|
||||||
},
|
},
|
||||||
[collapsedWidth, expandSidebar, lockResizeFor, startSizeTransition]
|
[collapsedWidth]
|
||||||
)
|
)
|
||||||
|
|
||||||
const sidebarNode = isValidElement(sidebar)
|
const sidebarNode = isValidElement(sidebar)
|
||||||
@ -320,38 +167,38 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ height: '100%', minHeight: 0 }}>
|
<Layout style={{ height: '100%', minHeight: 0 }}>
|
||||||
<Splitter
|
<AdaptiveSplitter
|
||||||
className={`farmcontrol-splitter dashboard-splitter${
|
className={`farmcontrol-splitter dashboard-splitter${
|
||||||
sizeTransitioning ? ' is-size-transitioning' : ''
|
collapsed ? ' is-sidebar-collapsed' : ''
|
||||||
}${collapsed ? ' is-sidebar-collapsed' : ''}`}
|
}`}
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
onResizeStart={handleResizeStart}
|
|
||||||
onResize={handleResize}
|
onResize={handleResize}
|
||||||
onResizeEnd={handleResizeEnd}
|
onResizeEnd={handleResizeEnd}
|
||||||
>
|
>
|
||||||
<Splitter.Panel
|
<AdaptiveSplitter.Panel
|
||||||
className={`dashboard-splitter-sidebar-panel${
|
className={`dashboard-splitter-sidebar-panel${
|
||||||
collapsed ? ' is-collapsed' : ''
|
collapsed ? ' is-collapsed' : ''
|
||||||
}`}
|
}`}
|
||||||
size={sidebarSize}
|
size={sidebarSize}
|
||||||
min={collapsedWidth}
|
min={collapsedWidth}
|
||||||
max={
|
max={MAX_SIDEBAR_PX}
|
||||||
collapsed && resizeLocked && !sizeTransitioning
|
collapsed={collapsed}
|
||||||
? collapsedWidth
|
collapsedSize={collapsedWidth}
|
||||||
: MAX_SIDEBAR_PX
|
collapseThreshold={COLLAPSE_BELOW_PX}
|
||||||
}
|
expandDragPx={EXPAND_DRAG_PX}
|
||||||
resizable={!resizeLocked}
|
expandedSize={lastExpandedSizeRef.current}
|
||||||
style={{
|
onCollapse={handleSplitterCollapse}
|
||||||
overflow: 'visible',
|
style={{ overflow: 'visible' }}
|
||||||
'--dashboard-splitter-sidebar-size': `${sidebarSize}px`
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{sidebarNode}
|
{sidebarNode}
|
||||||
</Splitter.Panel>
|
</AdaptiveSplitter.Panel>
|
||||||
<Splitter.Panel min={320} className='dashboard-splitter-content-panel'>
|
<AdaptiveSplitter.Panel
|
||||||
|
min={320}
|
||||||
|
className='dashboard-splitter-content-panel'
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Splitter.Panel>
|
</AdaptiveSplitter.Panel>
|
||||||
</Splitter>
|
</AdaptiveSplitter>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user