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-select-dropdown,
|
||||
.ant-splitter,
|
||||
.adaptive-splitter,
|
||||
.ant-steps-item-title,
|
||||
.ant-steps-icon,
|
||||
.ant-color-picker-presets-label,
|
||||
@ -528,11 +529,13 @@ body {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.farmcontrol-splitter > .ant-splitter-bar {
|
||||
.farmcontrol-splitter > .ant-splitter-bar,
|
||||
.farmcontrol-splitter > .adaptive-splitter-bar {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.farmcontrol-splitter.large > .ant-splitter-bar {
|
||||
.farmcontrol-splitter.large > .ant-splitter-bar,
|
||||
.farmcontrol-splitter.large > .adaptive-splitter-bar {
|
||||
margin: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@ -938,41 +941,18 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
}
|
||||
|
||||
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
||||
right: -1px;
|
||||
width: 0px !important;
|
||||
transition:
|
||||
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;
|
||||
top: 8px;
|
||||
right: calc(-1 * var(--scrollbox-vertical-right-padding) - 0.5px);
|
||||
width: 8px !important;
|
||||
}
|
||||
|
||||
.sidebar-scroll
|
||||
.simplebar-track.simplebar-vertical
|
||||
.simplebar-scrollbar:before {
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.ant-color-picker
|
||||
@ -1096,29 +1076,229 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.farmcontrol-splitter .ant-splitter-panel {
|
||||
.farmcontrol-splitter .ant-splitter-panel,
|
||||
.farmcontrol-splitter .adaptive-splitter-panel {
|
||||
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%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.farmcontrol-splitter.ant-splitter-horizontal.is-size-transitioning
|
||||
.ant-splitter-panel {
|
||||
transition:
|
||||
--dashboard-splitter-sidebar-size 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
||||
flex-basis 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
||||
width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
||||
min-width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
||||
max-width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
|
||||
.adaptive-splitter {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@property --dashboard-splitter-sidebar-size {
|
||||
syntax: '<length>';
|
||||
inherits: true;
|
||||
initial-value: 80px;
|
||||
.adaptive-splitter-vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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
|
||||
@ -1643,41 +1823,34 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-panel {
|
||||
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-panel,
|
||||
.dashboard-splitter.farmcontrol-splitter > .adaptive-splitter-panel {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-splitter.farmcontrol-splitter
|
||||
> .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;
|
||||
}
|
||||
|
||||
.dashboard-splitter.farmcontrol-splitter
|
||||
> .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;
|
||||
}
|
||||
|
||||
.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
|
||||
.elipsis-text-wrapper.ant-typography
|
||||
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,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Layout, Splitter } from 'antd'
|
||||
import { Layout } from 'antd'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
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 MAX_SIDEBAR_PX = 400
|
||||
const COLLAPSED_WIDTH_PX = 80
|
||||
const ELECTRON_COLLAPSED_WIDTH_PX = 55
|
||||
const EXPAND_DRAG_PX = 10
|
||||
const SIZE_TRANSITION_MS = 300
|
||||
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
|
||||
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
|
||||
|
||||
@ -56,7 +55,7 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
||||
)
|
||||
const [sidebarSize, setSidebarSize] = useState(() => {
|
||||
if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) {
|
||||
return isElectron ? ELECTRON_COLLAPSED_WIDTH_PX : COLLAPSED_WIDTH_PX
|
||||
return collapsedWidth
|
||||
}
|
||||
return clampExpandedSize(
|
||||
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
|
||||
@ -68,118 +67,6 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
||||
const lastExpandedSizeRef = useRef(
|
||||
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) => {
|
||||
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed))
|
||||
@ -192,113 +79,73 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
||||
const collapseSidebar = useCallback(
|
||||
(expandedSize) => {
|
||||
if (expandedSize >= COLLAPSE_BELOW_PX) {
|
||||
lastExpandedSizeRef.current = expandedSize
|
||||
persistSize(expandedSize)
|
||||
lastExpandedSizeRef.current = clampExpandedSize(expandedSize)
|
||||
persistSize(lastExpandedSizeRef.current)
|
||||
}
|
||||
collapsedRef.current = true
|
||||
persistCollapsed(true)
|
||||
startSizeTransition()
|
||||
setSidebarSize(sidebarSizeRef.current)
|
||||
setCollapsed(true)
|
||||
animateCollapsedSize()
|
||||
setSidebarSize(collapsedWidth)
|
||||
},
|
||||
[animateCollapsedSize, startSizeTransition]
|
||||
[collapsedWidth]
|
||||
)
|
||||
|
||||
const expandSidebar = useCallback(() => {
|
||||
cancelCollapseSizeAnimation()
|
||||
collapsedRef.current = false
|
||||
setCollapsed(false)
|
||||
persistCollapsed(false)
|
||||
startSizeTransition()
|
||||
setSidebarSize(DEFAULT_SIDEBAR_PX)
|
||||
persistSize(DEFAULT_SIDEBAR_PX)
|
||||
lastExpandedSizeRef.current = DEFAULT_SIDEBAR_PX
|
||||
}, [cancelCollapseSizeAnimation, startSizeTransition])
|
||||
const nextSize = clampExpandedSize(lastExpandedSizeRef.current)
|
||||
lastExpandedSizeRef.current = nextSize
|
||||
setSidebarSize(nextSize)
|
||||
persistSize(nextSize)
|
||||
}, [])
|
||||
|
||||
const handleSidebarCollapse = useCallback(
|
||||
(nextCollapsed) => {
|
||||
if (nextCollapsed === collapsedRef.current) return
|
||||
if (nextCollapsed) {
|
||||
collapseSidebar(sidebarSize)
|
||||
collapseSidebar(lastExpandedSizeRef.current)
|
||||
} else {
|
||||
expandSidebar()
|
||||
}
|
||||
lockResizeFor(SIZE_TRANSITION_MS)
|
||||
},
|
||||
[collapseSidebar, expandSidebar, lockResizeFor, sidebarSize]
|
||||
[collapseSidebar, expandSidebar]
|
||||
)
|
||||
|
||||
const handleResizeStart = useCallback(() => {
|
||||
if (!resizeLockedRef.current) {
|
||||
ignoreResizeEndRef.current = false
|
||||
const handleSplitterCollapse = useCallback(
|
||||
(nextCollapsed) => {
|
||||
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(
|
||||
(sizes) => {
|
||||
if (
|
||||
resizeLockedRef.current ||
|
||||
suppressGestureRef.current ||
|
||||
ignoreResizeEndRef.current
|
||||
) {
|
||||
ignoreResizeEndRef.current = false
|
||||
if (collapsedRef.current) {
|
||||
startSizeTransition()
|
||||
setSidebarSize(collapsedWidth)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (collapsedRef.current) {
|
||||
if (sizes[0] - collapsedWidth > EXPAND_DRAG_PX) {
|
||||
expandSidebar()
|
||||
lockResizeFor(SIZE_TRANSITION_MS)
|
||||
} else {
|
||||
startSizeTransition()
|
||||
setSidebarSize(collapsedWidth)
|
||||
}
|
||||
setSidebarSize(collapsedWidth)
|
||||
return
|
||||
}
|
||||
|
||||
const next = clampExpandedSize(sizes[0])
|
||||
lastExpandedSizeRef.current = next
|
||||
setSidebarSize(next)
|
||||
persistSize(next)
|
||||
},
|
||||
[collapsedWidth, expandSidebar, lockResizeFor, startSizeTransition]
|
||||
[collapsedWidth]
|
||||
)
|
||||
|
||||
const sidebarNode = isValidElement(sidebar)
|
||||
@ -320,38 +167,38 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%', minHeight: 0 }}>
|
||||
<Splitter
|
||||
<AdaptiveSplitter
|
||||
className={`farmcontrol-splitter dashboard-splitter${
|
||||
sizeTransitioning ? ' is-size-transitioning' : ''
|
||||
}${collapsed ? ' is-sidebar-collapsed' : ''}`}
|
||||
collapsed ? ' is-sidebar-collapsed' : ''
|
||||
}`}
|
||||
style={{ height: '100%' }}
|
||||
onResizeStart={handleResizeStart}
|
||||
onResize={handleResize}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
>
|
||||
<Splitter.Panel
|
||||
<AdaptiveSplitter.Panel
|
||||
className={`dashboard-splitter-sidebar-panel${
|
||||
collapsed ? ' is-collapsed' : ''
|
||||
}`}
|
||||
size={sidebarSize}
|
||||
min={collapsedWidth}
|
||||
max={
|
||||
collapsed && resizeLocked && !sizeTransitioning
|
||||
? collapsedWidth
|
||||
: MAX_SIDEBAR_PX
|
||||
}
|
||||
resizable={!resizeLocked}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
'--dashboard-splitter-sidebar-size': `${sidebarSize}px`
|
||||
}}
|
||||
max={MAX_SIDEBAR_PX}
|
||||
collapsed={collapsed}
|
||||
collapsedSize={collapsedWidth}
|
||||
collapseThreshold={COLLAPSE_BELOW_PX}
|
||||
expandDragPx={EXPAND_DRAG_PX}
|
||||
expandedSize={lastExpandedSizeRef.current}
|
||||
onCollapse={handleSplitterCollapse}
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
{sidebarNode}
|
||||
</Splitter.Panel>
|
||||
<Splitter.Panel min={320} className='dashboard-splitter-content-panel'>
|
||||
</AdaptiveSplitter.Panel>
|
||||
<AdaptiveSplitter.Panel
|
||||
min={320}
|
||||
className='dashboard-splitter-content-panel'
|
||||
>
|
||||
{children}
|
||||
</Splitter.Panel>
|
||||
</Splitter>
|
||||
</AdaptiveSplitter.Panel>
|
||||
</AdaptiveSplitter>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user